# \[테라폼 중급] 테라폼으로 스타트업 인프라 구축하기

테라폼으로 AWS 환경 구축하기 중급강의

### 개요

본 가이드는 인프런 강의 \[DevOps : Infrastructure as Code with 테라폼(Terraform) and AWS 중급, 활용 1,2편] 의 강의 자료입니다. 가이드는 개념보다는 코드를 설명하는 방식으로 제작되어 있습니다. 개념은 강의에서 함께 들으실 수 있고, 궁금하신 부분은 인프런 질문란에서 문의 주시면 성실히 답변 드리겠습니다.

<https://inf.run/hJhd8> <- 강의는 옆 링크를 통해 구매하실 수 있으며, 강의자료가 강의내용을 모두 포함하지 않습니다.

강의에 앞서, 반드시 [필독사항](/0.)을 읽어주시기 바랍니다.

### 코드 다운로드

* 본 가이드에서 사용되는 모든 자료는 아래 github를 참조하시면 됩니다.
* **실제 운영 환경에 사용하실때는 Repo에 있는 파일 구성을 기반으로 커스텀하게 재구성해서 사용하실 것을 권장드립니다.**
* 반드시 해당 레포를 복사해서 사용해주세요. 해당 레포에 PR을 올리셔도 반영하실 수 없습니다.

```
$ git clone https://github.com/DevopsArtFactory/aws-provisioning.git
```

해당 강의는 [www.infleran.com](https://www.inflearn.com/) 에서 강의용도로 만들어진 자료로 무단 배포 및 강의자료로 활용하는 것을 허용하지 않습니다^^


# 필독사항

본 가이드를 하기 전에 반드시 확인이 필요한 사항입니다!

## :question: 본강의자료 자체는 무료이지만 가공하거나 상업적으로 사용할 수 는 없습니다. 아래 인프런을 위한 강의자료이며, 상업적 사용은 강의자들만 사용할 수 있습니다.

<https://inf.run/hJhd8> <- 강의는 옆 링크를 통해 구매하실 수 있으며, 강의자료가 강의내용을 모두 포함하지 않습니다.

## :question: 비용이 발생할 수 있나요?

강의는 테라폼을 활용하여 AWS 리소스를 생성하는 것이 목적이기 때문에 **비용이 발생할 수 있습니다**. 테라폼은 코드가 있으면 동일한 형상을 언제든 구성할 수 있기 때문에, **비용을 최소화하고 싶으신 경우에는 실습 후에 반드시 해당 리소스를 삭제하시기 바랍니다. 코드만 지우지 않으시면 필요할 때 재구성이 가능합니다.**

## :question: 본 가이드의 코드를 실제 운영에 활용할 수 있나요?

본  가이드에서 다룬 코드는 전부 실제 운영되고 있는 코드를 바탕으로 구성되었습니다. 따라서 처음부터 테라폼으로 전체 구성을 시작하고 싶으신 경우에는 코드를 자사의 상황에 맞게 조금씩 구성해 나가시면 이후 서비스 운영에 사용하실 수 있습니다.

### **단! 모든 코드를 다루지는 않습니다.**

회사마다 서로 다른 인프라 구조를 다루고 있는 만큼, 모든 내용을 다룰 수는 없습니다. 가이드를 진행하면서 반드시 들어가야하는 내용인데 빠진 부분이 있으면 언제든 아래 링크로 문의 부탁드립니다. &#x20;

* Github Issue :[ ](https://github.com/DevopsArtFactory/aws-provisioning/issues)<https://github.com/DevopsArtFactory/aws-provisioning/issues>

## :question: 테라폼 버전과 프로바이더 버전에 따라서 결과가 다를 수 있나요?

테라폼 버전과 프로바이더 버전에 따라서 작동하는 코드가 달라질 수 있습니다. 이는 새로운 기능들이 출시되고 코드를 리팩토링하는 과정에서 발생하는 이슈입니다. 따라서 실습할 때는 버전을 꼭 맞춰서 진행해주시기 바랍니다.

반드시 수강 전에 아래 페이지를 참조하셔서 필요한 도구를 설치하시기 바랍니다.

{% content-ref url="/pages/IRvip8dcMscdvlxw2j3y" %}
[필수 도구 설치하기](/undefined)
{% endcontent-ref %}

강의와 다르게 본 문서에는 지속적으로 업데이트가 발생할 수 있습니다. 그 이유는 새로운 기능이 추가되어 버전을 올렸을 시에 코드가 일부 수정될 수 있기 때문입니다.&#x20;

이 점 참고 부탁드립니다! :person\_bowing:

해당 강의는 [www.infleran.com](https://www.inflearn.com/) 에서 강의용도로 만들어진 자료로 무단 배포 및 강의자료로 활용하는 것을 허용하지 않습니다^^


# 필수 도구 설치하기

실습에 필요한 도구를 설치합니다.

본 강의 실습을 위해서는 다음의 도구가 필요합니다.

## &#x20;:wrench: 설치해야할 패키지

* 테라폼
* SOPS
* AWS CLI
* jq

***

{% tabs %}
{% tab title="테라폼(Terraform)" %}
:rocket: **테라폼(Terraform) = 1.8.0**

* [설치 링크](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli)

{% code overflow="wrap" %}

```bash
# MAC OS
$ brew tap hashicorp/tap
$ brew install hashicorp/tap/terraform

# Windows
$ choco install terraform

```

{% endcode %}

version 명령어를 통해 테라폼 버전이 v1.5.7임을 확인합니다.

```
# 버전 확인 
$ terraform version
Terraform v1.5.7
on darwin_arm64
```

{% endtab %}

{% tab title="SOPS" %}
:rocket: **SOPS = 3.8.0**

* [설치 링크](https://github.com/getsops/sops/releases)
* 보안상 감춰야하는 값을 관리할 때 사용하는 패키지입니다.

아래와 같이 패키지 확장자를 선택해서 설치할 수 있습니다. 예시는 macOS M1(Darwin.arm64) 에서 설치하는 방법입니다.

```
$ # Download the binary
curl -LO https://github.com/getsops/sops/releases/download/v3.8.0/sops-v3.8.0.darwin.arm64

# Move the binary in to your PATH
mv sops-v3.8.0.darwin.arm64 /usr/local/bin/sops

# Make the binary executable
chmod +x /usr/local/bin/sops
```

\--version 옵션을 통해서 버전이 3.8.0 임을 확인합니다.

```
$ sops --version
sops 3.8.0 (latest)
```

{% endtab %}

{% tab title="AWS CLI" %}
:rocket: **awscli >= 2.x**

* [설치 링크](https://docs.aws.amazon.com/ko_kr/cli/latest/userguide/getting-started-install.html)

\--version 옵션을 통해서 버전이 2.x이상 버전임을 확인합니다.

```
$ aws --version
aws-cli/1.29.49 Python/3.11.0 Darwin/22.5.0 botocore/1.31.49
```

{% endtab %}

{% tab title="jq" %}
:rocket: **jq >= 1.7**

* [설치 링크](https://jqlang.github.io/jq/download/)
* JSON 데이터를 다룰 때 사용하는 패키지입니다.

\--version 옵션을 통해서 버전이 1.7 이상 버전임을 확인합니다.

```
$ jq --version
jq-1.7
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
**테라폼(Terraform) 라이선스에 관하여**

테라폼은 2023년 8월 10일 S/W 라이선스를 ***MPL2.0***&#xC5D0;서 ***BSL1.1***&#xB85C; 변경하였습니다.

보통 오픈소스 커뮤니티에서는 이러한 결정을 사전에 공지하고 많은 의견을 받아서 결정하지만, \
Hashicorp사에서 사전 동의없이 결정하여 논란이 있었습니다.

저는 오랜 시간동안 테라폼을 사용한 사람으로서, 한명의 엔지니어로서 어느쪽이 옳다고 의견을 말씀 드리기가 어렵습니다. Hashicorp의 오픈소스 자체가 너무 훌륭하여 회사로써의 이득이 충분치 않았다는 것도 부정할 수 없기 때문입니다. 그리고 테라폼을 이용하여 다양한 것들을 시도하던 회사나 개인의 입장도 이해하기 때문입니다.

일반 사용자, 회사 입장에서는 큰 변화가 없을 수 도 있기 때문에 아래의 라이선스 정책에 대해서 정확히 이해하는 것이 더 좋을 것 같아 아래에 간략하게 정리했습니다.

참고 부탁드립니다.
{% endhint %}

{% hint style="info" %}
**MPL v2.0 License ( from ChatGPT4 )**

1. **본질**: MPL 2.0은 오픈 소스 라이선스로, 소스 코드의 공유를 요구하지만, 라이선스된 코드와 다른 코드를 결합하여 생성된 작품은 별도의 라이선스로 배포할 수 있게 합니다.
2. **파일 수준의 Copyleft**: MPL 2.0은 파일 수준의 copyleft 특성을 가집니다. 즉, MPL로 라이선스된 파일을 수정하면 해당 파일의 수정된 부분은 MPL 하에 배포되어야 합니다. 그러나, 해당 파일 외의 다른 파일들에는 MPL이 적용되지 않습니다.
3. **다른 코드와의 결합**: MPL 2.0 라이선스 하에 있는 코드는 다른 라이선스를 가진 코드와 자유롭게 결합할 수 있습니다. 결합된 결과물을 배포할 때 MPL로 라이선싱된 파일은 그대로 MPL 하에 배포되어야 하지만, 다른 파일들에 대해서는 그렇지 않습니다.
4. **특허 권리**: MPL 2.0은 코드 기여자들이 해당 코드와 관련된 특허 권리를 부여하는 조항을 포함하고 있습니다.
5. **소스 코드 배포**: MPL 2.0 하에 라이선스된 코드의 이진 버전을 배포할 때, 사용자는 그 코드의 소스 코드 버전에 액세스할 권리가 있습니다.

MPL 2.0에는 더 많은 조항과 세부 사항이 있습니다. 라이선스에 관한 완전한 내용과 조항을 이해하려면 공식 MPL 2.0 텍스트를 참조하는 것이 좋습니다.
{% endhint %}

{% hint style="info" %}
**BSL v1.1 ( from ChatGPT4 )**

1. **제한 기간**: BSL 라이센스 하에 릴리스된 소프트웨어는 "Change Date"로 지정된 날짜까지 특정 사용 조건에 제한됩니다.
2. **제한**: 라이센스에는 소프트웨어를 사용하는 데 몇 가지 제한이 포함될 수 있습니다. 예를 들어, 소프트웨어를 사용하여 특정 규모 이상의 데이터베이스를 운영하거나, SaaS로 제공하는 것을 금지할 수 있습니다.
3. **오픈소스 전환**: "Change Date" 이후, 소프트웨어는 지정된 오픈소스 라이선스, 예를 들어 Apache 2.0, under에 배포됩니다. 이 때부터, 제한 사항 없이 소프트웨어를 사용할 수 있게 됩니다.
4. **소스 코드 사용**: "Change Date" 이전에도 사용자는 소프트웨어의 소스 코드를 검토하거나 수정할 수 있지만, 라이선스의 제한 조건 내에서만 사용할 수 있습니다.

BSL은 오픈소스와 상업 라이선스의 특성을 결합하여, 기업들이 초기에는 소프트웨어를 상업적으로 보호하면서, 장기적으로는 커뮤니티와 함께 오픈소스로 사용할 수 있도록 하는 유연성을 제공합니다.

BSL의 구체적인 내용과 조항에 대한 완전한 이해를 위해서는 해당 라이선스 문서의 전문을 직접 참조하는 것이 좋습니다.
{% endhint %}

{% hint style="info" %}
**MPL 2.0에서 BSL 1.1로 라이선스 변경에 대한 의미 ( from ChatGPT4 )**

MPL 2.0에서 BSL 1.1로 라이선스가 변경되면, 해당 소프트웨어의 사용과 배포에 대한 접근 및 권한이 크게 바뀌게 됩니다. 이러한 변경의 의미와 가능한 추측을 아래에 나열해 보겠습니다:

1. **상업적 가치**: 기업이나 개발자는 MPL 2.0에서 BSL 1.1로 라이선스를 변경하는 것으로, 해당 소프트웨어에 상업적 가치가 있다고 판단했을 수 있습니다. BSL은 특정 기간 동안 소프트웨어의 일부 사용을 제한할 수 있기 때문에, 기업은 이 기간 동안 소프트웨어의 상업적 가치를 최대화하려고 시도할 수 있습니다.
2. **제한된 사용**: MPL 2.0은 오픈소스 라이선스로서 소프트웨어의 자유로운 사용, 수정, 배포를 허용합니다. BSL 1.1로의 전환은 특정 사용 사례나 상황에서의 소프트웨어 사용을 제한하려는 의도일 수 있습니다. 예를 들어, 대규모의 상업적 환경에서의 사용이나 SaaS로서의 제공을 제한하고자 할 수 있습니다.
3. **오픈소스로의 장기적 전환**: BSL 1.1은 "Change Date" 이후에 다시 오픈소스 라이선스로 전환됩니다. 따라서, 기업은 장기적으로 커뮤니티와 함께 해당 소프트웨어를 발전시키길 원하지만, 단기적으로는 상업적 이익을 추구하려는 전략을 선택했을 수 있습니다.
4. **커뮤니티 반응**: 이러한 라이선스 변경은 오픈소스 커뮤니티 내에서 다양한 반응을 일으킬 수 있습니다. 일부 사용자나 기여자는 제한적인 BSL에 반대할 수 있으며, 이로 인해 프로젝트의 포크나 대체 프로젝트의 시작을 볼 수도 있습니다.
5. **경쟁과 시장 동향**: 해당 소프트웨어의 시장 내 경쟁 상황, 동향, 그리고 사용자의 수요에 따라 라이선스 변경 결정이 내려졌을 수도 있습니다. 기업은 라이선스를 변경하여 특정 시장 세그먼트에서 경쟁력을 강화하려고 할 수 있습니다.

결론적으로, MPL 2.0에서 BSL 1.1로의 라이선스 변경은 소프트웨어의 상업적 활용, 사용 제한, 그리고 장기적인 오픈소스 전략 등 여러 요인을 반영하는 것으로 해석될 수 있습니다.
{% endhint %}

{% hint style="info" %}
**ChatGPT 의견에 대한 개인적 의견**

거의 모두 동의하며 처음 의견드린 것처럼 어느쪽에 편들기 어렵습니다.\
S/W엔지니어로서도, 비즈니스를 하는 하는 사람으로서도, 특히 Hashicorp는 이제 상장을 한 회사이기 때문에 투자자의 의견들도 무시할 수 없었을 것입니다.

ChatGPT가 설명해준 것처럼, 라이선스 정책에 관해서 정확한 검토는 필요합니다. 특히, 테라폼을 이용하여 어떠한 도구를 만들고 플랫폼을 만들때는 정확하게 사용해야합니다.

그리고, 이러한 정책은 테라폼의 버전에 따라서 적용되는 부분이 달라 그 부분도 찾아보고 공부 해보는 것을 \
권해드립니다.

하지만, 너무 라이선스 정책에 갇혀서 개인의 사용이나 회사에서의 도구 그대로에 대한 사용에 대해서는 주저할 필요는 없습니다. 설령 테라폼을 못쓰는 환경이 되더라도 그 사용에 의미와 선언적 의미에 대해서만 이해하더라도 개인에게 충분한 성장에 도움이 될 것 입니다.

{% endhint %}


# 실습에 앞서 초기 세팅하기

실습에 필요한 초기 세팅

본 장에서는 중급 강의 실습에 앞서 구성이 필요한 부분에 대해서 설명드리고자 합니다.

강의에서는 기본적으로 테라폼과 AWS를 활용하기 때문에 테라폼 및 AWS 설정이 반드시 필요합니다.

본 초기 세팅이 제대로 이뤄지지 않으면 강의를 따라오기 어려우니, 천천히 세팅을 진행해주시기 바랍니다.


# AWS 계정 생성

본 실습을 위해서는 AWS 계정이 필요합니다.  만약 이미 AWS 계정이 있으시다면 다음 장으로 넘어가셔도 됩니다!

AWS 계정 생성은 다음의 가이드를 참조해 주시기 바랍니다.

* 주의사항
  * Personal 계정으로 생성
  * 유효한 신용카드 필요
  * 유효한 이메일 필요

{% embed url="<https://docs.aws.amazon.com/ko_kr/accounts/latest/reference/manage-acct-creating.html>" %}


# 초기화 IAM 사용자 생성

테라폼 초기 세팅을 위한 IAM 사용자 생성

### 초기화 IAM 사용자

* 초기 세팅을 위해서 필요한 임시 IAM 유저를 생성합니다. IAM 사용자도 Terraform으로 생성할 수 있지만, 그 사용자를 만들기 위해서 필요한 IAM 권한이 필요합니다. 따라서, 임시로 사용자를 만들고, 테라폼으로 실제 사용자를 만들 예정입니다.
* 본 사용자는 Terraform으로 생성되지 않기 때문에, Terraform으로 실제 사용자를 생성한 후에 삭제합니다.

### IAM 사용자 생성

1. &#x20;IAM 콘솔에서 `사용자`를 클릭합니다.
2. `사용자 생성` 버튼을  클릭합니다.

<figure><img src="/files/Wsr9rLq4XMSC9EJ2CgnQ" alt=""><figcaption></figcaption></figure>

3. 사용자 이름을 입력하고, `다음` 을 클릭합니다.

<figure><img src="/files/fP7Vj08XyM2OTT296XnX" alt=""><figcaption></figcaption></figure>

4. `직접 정책 연결` 중 `AdministratorAccess`를 선택합니다.

<figure><img src="/files/ECkyXNboqwv4fZ2SAc2Q" alt=""><figcaption></figcaption></figure>

5. `다음` -> `사용자 생성` 을 클릭합니다.&#x20;
6. 다음과 같이 사용자 화면에서, `init-user` 를 선택합니다. 만약 사용자 이름을 다르게 설정하셨다면, 해당 사용자를 선택합니다.

<figure><img src="/files/HAHboRzo8EE18W3rFhkH" alt=""><figcaption></figcaption></figure>

7. `보안 자격 증명` -> `엑세스 키 만들기` 를 선택합니다.

<figure><img src="/files/qqe631GFaxtRexuG6dnM" alt=""><figcaption></figcaption></figure>

8. 사용 용도로 `Command Line Interface` 를 선택합니다. 태그는 넘어가고, `엑세스 키 만들기` 를 선택합니다.

<figure><img src="/files/Zt6cjN9oIrTB2rKeLkOH" alt=""><figcaption></figcaption></figure>

9. 다음과 같이 액세스 키를 복사합니다.&#x20;

<figure><img src="/files/in1KGyDQsCitc5WNvgFs" alt=""><figcaption></figcaption></figure>

10. `aws configure` 명령어를 통해서 설정을 진행합니다. 파일에서 수정하는 것도 가능합니다.

{% tabs %}
{% tab title="aws configure" %}

```bash
$ aws configure
AWS Access Key ID [****************MWEY]: < Your Access Key ID >
AWS Secret Access Key [****************2suN]: < Your Secret Access Key >
Default region name [ap-northeast-2]: ap-northeast-2
Default output format [json]: json
```

{% endtab %}

{% tab title="\~/.aws/credentials에 직접 추가" %}

<pre class="language-bash"><code class="lang-bash"># vim ~/.aws/credentials
[default]
aws_access_key_id = &#x3C; Your Access Key ID >
aws_secret_access_key = &#x3C; Your Secret Access Key >

<strong># vim ~/.aws/config
</strong>[default]
region = ap-northeast-2
output = json
</code></pre>

{% endtab %}
{% endtabs %}

11. &#x20; aws 명령어를 통해서 정상적으로 세팅이 되었는지 확인합니다.

```
$ aws sts get-caller-identity | jq -r .
{
  "UserId": "XXXXXXXXXXXXXXX",
  "Account": "816736805842",
  "Arn": "arn:aws:iam::816736805842:user/init-user"
}
```


# Github 코드 만들기

Github 소스 코드를 다운받고, 나만의 레포 만들기

본 가이드에 사용할 코드를 다운로드 받고, 이를 자신만의 레포로 변경합니다.

Fork 해서 진행해도 되지만, 만약 Private하게 구성하고자 한다면 다음과 같이 구성할 수 있습니다.

#### Private 레포지토리 구성하기

1. 먼저 Github 코드를 Clone합니다.

```
$ git clone https://github.com/DevopsArtFactory/aws-provisioning
'aws-provisioning'에 복제합니다...
remote: Enumerating objects: 940, done.
remote: Counting objects: 100% (135/135), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 940 (delta 36), reused 85 (delta 18), pack-reused 805
오브젝트를 받는 중: 100% (940/940), 170.45 KiB | 5.33 MiB/s, 완료.
델타를 알아내는 중: 100% (465/465), 완료.
```

2. 폴더로 들어가서 `.git` 파일을 수정합니다.

```
$ rm -rf .git
$ git init
$ git remote add origin <your private repositsory URL>
$ git add .
$ git commit -m "init"
$ git push origin <default branch>
```


# 네이밍 컨벤션을 위한 이름 정하기

자신이 만들고자 하는 서비스의 이름을 정합니다.

이번 실습에서는 실제 스타트업을 운영할 때 필요한 인프라를 만들어볼 예정입니다.&#x20;

실제 인프라를 구성할 때 네이밍이 상당히 중요합니다.

네이밍이 중요한 이유는 다음과 같습니다.&#x20;

**첫째, 이름을 보고 바로 리소스의 용도를 파악할 수 있습니다.**

**둘째, Scripting 작업 시에 간단하게 파싱할 수 있습니다.**

**셋째, 인프라 코드를 재사용할 때 용이합니다. 예컨대, sed 명령어를 통해 한번에 리소스 이름을 교체해서 인프라를 구성할 수 있습니다.**

본 가이드에서 제공해드린 코드에는 art라는 네이밍을 사용합니다.&#x20;

**본 가이드에서는 이를 devart 라는 이름으로 변경해서 실습을 진행할 예정입니다.**&#x20;

**여러분은 반드시 자신만의 이름을 3\~6자 내의 영어로 정해서 진행하시기 바랍니다.**


# Backend 구성하기

Backend용 S3 버킷과 lock을 위한 dynamoDB 생성

### **S3 bucket as backend**

* 테라폼의 상태를 저장하기 위해 S3 버킷을 생성합니다.
* S3 버킷에 최신 상태를 유지할 수 있기 때문에 협업이 가능합니다.

### **DynamoDB Table for Lock**

* 동시에 같은 파일을 수정하지 못하도록 하기 위해 DynamoDB에 작업에 대한 Lock을 생성합니다.

### **실습**

1. `terraform/init/art-id`  디렉터리를 복사해서 새로운 디렉터리를 만듭니다. 저는 devart-preprod 로 변경해서 진행합니다 .

* preprod는 개발/스테이지/QA 환경과 같이 실제 운영환경에 나가기 전에 필요한 검증을 위해 만든 인프라용 계정으로 쓰입니다.&#x20;

```
$ cp -R art-id devart-preprod
```

&#x20;**1.**  `terraform/init/devart-preprod/init.tf` 파일을 수정합니다.

```hcl
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.17.0"
    }
  }
}

provider "aws" {
  region  = "ap-northeast-2"
}

# S3 bucket for backend
resource "aws_s3_bucket" "tfstate" {
  bucket = "${var.account_id}-apnortheast2-tfstate"

  versioning {
    enabled = true # Prevent from deleting tfstate file
  }
}

# DynamoDB for terraform state lock
resource "aws_dynamodb_table" "terraform_state_lock" {
  name           = "terraform-lock"
  hash_key       = "LockID"
  billing_mode   = "PAY_PER_REQUEST"

  attribute {
    name = "LockID"
    type = "S"
  }
}

variable "account_id" {
  default = "devart-preprod" # Please use the account alias for id
}
```

2\.  `Terraform init`을 진행합니다.

```bash
$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.17.0"...
- Installing hashicorp/aws v5.17.0...
- Installed hashicorp/aws v5.17.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
```

3\. 생성 리소스를 확인합니다.

```bash
$ terraform plan -parallelism=30

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_dynamodb_table.terraform_state_lock will be created
  + resource "aws_dynamodb_table" "terraform_state_lock" {
      + arn              = (known after apply)
      + billing_mode     = "PAY_PER_REQUEST"
      + hash_key         = "LockID"
      + id               = (known after apply)
      + name             = "terraform-lock"
      + read_capacity    = (known after apply)
      + stream_arn       = (known after apply)
      + stream_label     = (known after apply)
      + stream_view_type = (known after apply)
      + tags_all         = (known after apply)
      + write_capacity   = (known after apply)

      + attribute {
          + name = "LockID"
          + type = "S"
        }
    }

  # aws_s3_bucket.tfstate will be created
  + resource "aws_s3_bucket" "tfstate" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "devart-preprod-apnortheast2-tfstate"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = (known after apply)
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + versioning {
          + enabled    = true
          + mfa_delete = false
        }
    }

Plan: 2 to add, 0 to change, 0 to destroy.
```

`Plan: 2 to add, 0 to change, 0 to destroy.` 이 나오면 정상입니다.

4\. 이제 리소를 생성합니다.

```
$ terraform apply -parallelism=30
(... 중략 ...)
Plan: 2 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes <------ 정확히 yes라고 입력하셔야 합니다.

aws_dynamodb_table.terraform_state_lock: Creating...
aws_s3_bucket.tfstate: Creating...
aws_s3_bucket.tfstate: Creation complete after 2s [id=devart-preprod-apnortheast2-tfstate]
aws_dynamodb_table.terraform_state_lock: Creation complete after 7s [id=terraform-lock]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
```

`Apply complete! Resources: 2 added, 0 changed, 0 destroyed.` 문구가 나오면 성공입니다.

### 결과 스크린샷

* **S3 버킷**

<figure><img src="/files/uEXftMsMysO5dW3ZjvKu" alt=""><figcaption></figcaption></figure>

* **DynamoDB Table**

<figure><img src="/files/LQ6frw8z9Ed6ldq6KmhL" alt=""><figcaption></figcaption></figure>

### (Optional)Multi Account 세팅

* 위의 동일한 절차를 다른 계정에도 적용해야 합니다. 해당 계정의 리소스 생성 정보는 생성하신 버킷에 저장됩니다.
* 여러 account를 구성하는 경우에는 `terraform/init/art-id` 폴더를 그대로 복사하셔서 `terraform/init/<계정 이름>`  으로 만드신 후에, `init.tf` 에서 account\_id 만 변경하시면 됩니다. 당연히 해당 내용을 적용하실 때는 AWS Credential 정보를 다른 계정으로 세팅하신 후에 진행하셔야 합니다.

먼저, 사용할 계정의 **IAM 초기화 사용자를 생성**하셔야 합니다. (첫 세팅만 사용)

{% content-ref url="/pages/-MPAKrIwHoEvEvP8Idsf" %}
[초기화 IAM 사용자 생성](/0.-terraform-intermediate/iam)
{% endcontent-ref %}

위의 실습 단계를 다시 진행합니다.

* `terraform/init/devart-prod` 폴더를 생성하고, `art-id`에 있는 내용을 복사합니다.

```bash
$ cp -R art-id devart-prod
```

* `init.tf` 파일에서 `account_id` 이름을 수정합니다.

```bash
(... 생략 ...)
variable "account_id" {
 default = "devart-prod" # Please change this value!!
}
```

이후 작업은 동일합니다.

* terraform init&#x20;
* terraform plan -parallelism=30
* terraform apply -parallelism=30

**초기화 IAM 사용자는 IAM 세팅 때도 필요합니다. 해당 작업이 끝난 후 삭제하시면 됩니다.**


# zsh and  oh-my-zsh

빠른 작업을 위한 Shell 최적화

{% hint style="info" %}
zsh는 linux 기반 OS에서만 설치가 가능합니다. Windows 사용자는 Cloud9과 같은 환경을 통해 설정을 진행해주시기 바랍니다!
{% endhint %}

## zsh 및 oh-my-zsh 설치

테라폼 작업은 주로 bash shell에서 진행됩니다. 따라서 bash를 조금이라도 편리하게 사용하는 것이 작업 속도를 높일 수 있는 방법입니다.

본 장에서는 bash 대신 사용할 zsh(z-shell) 과 oh-my-zsh 설정에 대해서 알아보겠습니다.&#x20;

### Linux 환경&#x20;

```bash
### Cloud9 환경에서 su 명령을 이용해 bash 로 변경. 유저는 선택사항입니다.
ec2-user:~/environment $ sudo -i
[root@ip-172-31-8-195]~# su - ec2-user
[ec2-user@ip-172-31-8-195]~% 

### zsh 설치
[ec2-user@ip-172-31-8-195]~% sudo yum install zsh         
Complete!

### ec2-user password 설정.
[ec2-user@ip-172-31-8-195]~% sudo -i
[root@ip-172-31-8-195 ~]# passwd ec2-user   
...
passwd: all authentication tokens updated successfully.

### bash 에서 zsh 로 shell 변경
[root@ip-172-31-8-195 ~]# exit
logout
[ec2-user@ip-172-31-8-195]~% $ which zsh
/bin/zsh
[ec2-user@ip-172-31-8-195]~% $ chsh -s /bin/zsh
Changing shell for ec2-user.
Password: 
Shell changed.
[ec2-user@ip-172-31-8-195]~% echo $SHELL  ### shell 변경 확인.
/bin/zsh

### Oh my zsh 설치
[ec2-user@ip-172-31-8-195]~% curl -L https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh | sh                                                                                                                                                                                                        
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100  8445  100  8445    0     0  12776      0 --:--:-- --:--:-- --:--:-- 37202
.....
         __                                     __
  ____  / /_     ____ ___  __  __   ____  _____/ /_
 / __ \/ __ \   / __ `__ \/ / / /  /_  / / ___/ __ \
/ /_/ / / / /  / / / / / / /_/ /    / /_(__  ) / / /
\____/_/ /_/  /_/ /_/ /_/\__, /    /___/____/_/ /_/
                        /____/                       ....is now installed!

Please look over the ~/.zshrc file to select plugins, themes, and options.
p.s. Follow us on https://twitter.com/ohmyzsh
p.p.s. Get stickers, shirts, and coffee mugs at https://shop.planetargon.com/collections/oh-my-zsh
Run zsh to try it out.
```

oh my zsh 의 테마 는 개인 선호도에 따라 선택하면 되지만,&#x20;

적절한 컬러, Git branch, commit 확인이 되는 테마를 고르는게 좋습니다. 저는 ys 를 선호합니다.

```bash
[ec2-user@ip-172-31-8-195]~% sudo vim ~/.zshrc

### in ~/.zshrc
ZSH_THEME="ys" ## THEME 변경 후 터미널 재실행
# ec2-user @ ip-172-31-8-195 in ~ [19:43:14] 
 
### 터미널 재실행

###  ~/.oh-my-zsh/lib/key-bindings.zsh 에서  echoti smkx 주석처리
###  해당 부분을 주석처리 하지 아니하면, 스크롤이 오동작하는 버그가 있습니다.
```

### Mac 환경

Mac 환경에서 zsh을 설치할 때는 brew를 사용하시면 편리합니다.

```
# install brew
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# install zsh
$ brew install zsh

# install oh-my-zsh
$ sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
```

oh-my-zsh 설정의 위와 동일합니다.


# AWS EC2 and SSH

AWS EC2 와 그 접속 방법인 SSH 에 대해서 알아봅니다.

## AWS EC2

Amazon Elastic Compute Cloud(EC2)는 안전하고 크기 조정이 가능한 컴퓨팅 용량을 클라우드에서 제공하는 웹 서비스입니다. 사용자는 간단한 웹 서비스 인터페이스를 통해 간편하게 필요한 용량으로 서버를 구성할 수 있습니다.

컴퓨팅 리소스에 대한 포괄적인 제어권을 제공하며, Amazon의 검증된 컴퓨팅 인프라에서 실행할 수 있습니다.

## EC2 SSH 접속 방법

{% embed url="<https://www.youtube.com/watch?v=oMVfhT_xlzk>" %}


# 0. SSM

접근제어 서버를 대신하는 AWS SSM

### Systems Manager의 작동 방식 <a href="#how-it-works" id="how-it-works"></a>

다음 다이어그램에서는 일부 Systems Manager 기능이 리소스에 대한 작업을 수행하는 방법을 설명합니다. 이 다이어그램에서 모든 기능을 다루지는 않습니다. 각 열거된 상호 작용은 다이어그램 앞에 설명되어 있습니다.

1. Systems Manager 액세스 - [Systems Manager 액세스](https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/what-is-systems-manager.html#access-methods)에 대해 사용 가능한 옵션 중 하나를 사용합니다.
2. Systems Manager 기능 선택 - 리소스에 대해 수행할 작업을 수행하는 데 도움이 될 수 있는 기능을 확인합니다. 이 다이어그램은 IT 관리자와 DevOps 인력이 리소스를 구성하고 관리하는 데 사용하는 몇 가지 기능만 보여줍니다.
3. 확인 및 처리 - Systems Manager는 사용자, 그룹 또는 역할에 지정한 작업을 수행하는 데 필요한 AWS Identity and Access Management(IAM) 권한이 있는지 확인합니다. 작업 대상이 관리형 노드인 경우 노드에서 실행 중인 Systems Manager 에이전트(SSM Agent)가 작업을 수행합니다. 다른 유형의 리소스의 경우 Systems Manager는 지정된 작업을 수행하거나 다른 AWS 서비스와(과) 통신하여 Systems Manager를 대신하여 작업을 수행합니다.
4. 보고 - Systems Manager, SSM Agent 및 Systems Manager를 대신하여 작업을 수행하는 기타 AWS 서비스은(는) 상태를 보고합니다. Systems Manager는 다른 AWS 서비스에 상태 세부 정보를 보낼 수 있습니다(구성된 경우).
5. Systems Manager 운영 관리 기능 - 활성화된 경우 Systems Manager 운영 관리 기능(예:Explorer, OpsCenter), Incident Manager는 리소스의 이벤트나 오류에 대응하여 운영 데이터를 집계하거나 아티팩트를 생성합니다. 이러한 아티팩트에는 운영 작업 항목(OpsItems) 및 인시던트가 있습니다. Systems Manager 운영 관리 기능은 애플리케이션 및 리소스에 대한 운영 통찰력과 자동화된 문제 해결 솔루션을 제공하여 문제를 해결하는 데 도움이 됩니다.

<figure><img src="/files/9V7h4qvxyesPhrszlEE7" alt=""><figcaption></figcaption></figure>

출처: <https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/what-is-systems-manager.html>

SSM Agent 설치

Mac OS

{% embed url="<https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/ssm-agent-macos.html>" %}

Linux

{% embed url="<https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/ssm-agent-linux.html>" %}

Windows

{% embed url="<https://docs.aws.amazon.com/ko_kr/systems-manager/latest/userguide/ssm-agent-windows.html>" %}


# 1. VPC

Terraform을 활용해 VPC 구성하기

## 목표

본 장에서는 테라폼을 활용해서 VPC를 구성하는 Best Practice에 대해서 배워볼 예정입니다. VPC는 AWS의 대부분의 서비스에서 근본이 되는 서비스이므로 정확히 이해하고 구성하는 것이 중요합니다.

&#x20;


# \[이론] VPC 기본 개념

Amazon VPC는 Amazon에서 제공하는 Private한 네크워크 망입니다. VPC는 AWS 인프라의 가장 기본이 되는 구조이기 때문에 초기에 구성해야하는 서비스 중 하나이며, 한 번 구성하게되면 이후에 변경하기 힘듭니다. 따라서 올바른 개념을 가지고 상황에 맞는 VPC 구성을 잘 하는 것이 중요합니다.

다음은 VPC의 핵심 구성요소입니다.

**Virtual Private Cloud(VPC)** — 사용자의 AWS 계정 전용 가상 네트워크입니다.

**Subnet** — VPC의 IP 주소 범위입니다.

**Route table** — 네트워크 트래픽을 전달할 위치를 결정하는 데 사용되는 라우팅이라는 규칙 집합입니다.

**Internet Gateway** — VPC의 리소스와 인터넷 간의 통신을 활성화하기 위해 VPC에 연결하는 게이트웨이입니다.

**NAT Gateway** — 네트워크 주소 변환을 통해 프라이빗 서브넷에서 인터넷 또는 기타 AWS 서비스에 연결하는 게이트웨이입니다.

**Security Group** — 보안 그룹은 인스턴스에 대한 인바운드 및 아웃바운드 트래픽을 제어하는 가상 방화벽 역할을 하는 규칙 집합입니다..

**VPC Endpoint** — 인터넷 게이트웨이, NAT 디바이스, VPN 연결 또는 AWS Direct Connect 연결을 필요로 하지 않고 PrivateLink 구동 지원 AWS 서비스 및 VPC 엔드포인트 서비스에 VPC를 비공개로 연결할 수 있습니다. VPC의 인스턴스는 서비스의 리소스와 통신하는 데 퍼블릭 IP 주소를 필요로 하지 않습니다. VPC와 기타 서비스 간의 트래픽은 Amazon 네트워크를 벗어나지 않습니다.

## Public Subnet vs Private Subnet

Subnet이란 VPC IP 주소 범위를 구간별로 나눈 리소스하고 생각하시면 편합니다. 전체 사용 가능한 IP 주소 pool에서 범위를 나눠서 구역별로 지정해주는 것입니다. Subnet은 기본적으로 AZ(가용 영역)에 종속적이기 때문에 AZ를 여러개 사용하시는 경우에는 AZ 별로 하나씩 생성해주셔야 합니다.

여기서 중요한 것은 Public과 Private Subnet을 구분하는 것입니다. 실제로 public / private subnet을 나누는 설정값은 존재하지 않습니다. 다 같은 subnet의 일종일 뿐입니다. 다만, 해당 서브넷에서 외부와 통신할 수 있는 routing rule을 어떻게 설정하느냐에 따라 Public과 Private을 나눠서 지칭할 뿐입니다.

Subnet에 붙어 있는 Gateway가 인터넷 게이트웨이(IGW)라면 이는 public subnet이라고 부르고, NAT 혹은 아무것도 없는 경우에는 private subnet이라고 부릅니다.  DB 레이어와 같이 인터넷 밖과 통신할 일이 거의 없는 서비스들은 NAT 설정 자체도 없는 서브넷에 두는 것이 보안상 좋습니다.

공식 문서 What is Amazon VPC

<https://docs.aws.amazon.com/ko_kr/vpc/latest/userguide/what-is-amazon-vpc.html>


# VPC 기본 구조 이해

<figure><img src="https://lh7-us.googleusercontent.com/nKNv9ZohoBg5OTdKtJBa9EQjQuYKMmYRQAGBi5ZPDv3nqzEBwGT3TyOOoGW33_LJo7yGtoThfiaKIrRKf0vQ6eLGUawa0_asRaAhfhk_8w3mwaQos8iQM71CMrr-7LZNEky5PNrnH-4hadiNWJDl=s2048" alt=""><figcaption></figcaption></figure>


# Best practice Subnet 이해

서브넷을 용도별로 구분?

<figure><img src="/files/EpY4NzJF65YkTJfs1oK1" alt=""><figcaption><p>VPC Subnets in 1 zone</p></figcaption></figure>

* 과거에는 On-premise 서브넷팅을 잘게잘게 쪼개는게 맞았다 ( o )
* Because 층별로 구역별로, 부서 by 부서, 사람 by 사람으로 인터넷 방화벽 셋팅이 필요했기 때문에
* IP추적이 필요했기 때문에

<figure><img src="/files/sp8IjDzpk7cM69aNF0Fj" alt=""><figcaption><p>Subnet by Zone</p></figcaption></figure>

* AWS Zone 을 High Availiability를 위해 Zone 을 나누는게 중요

<figure><img src="/files/4hOFK8bFWN5ij81YeUjE" alt=""><figcaption><p>Private and Public subnets-1</p></figcaption></figure>

* 과거에는 24bit 사용하는게 범용적이었다.
* Because 읽기 편하기 때문이었죠.
* C-Natative 더이상 IP를 사용하는 것보다 Security, IP대역 넣는게 아니라 SG ID를 넣는게 좋은 것이다.

<figure><img src="/files/jKGTEDxvUs7NbYJR9dFB" alt=""><figcaption><p>Private and Public subnets-2</p></figcaption></figure>

* Subnet을 나누는 기준은 용도별이 아니라 Routing Table 분할할때 사용하는 것이다.
* Route table 에 따라서 서브넷을 종속시키고, 구분시켜야 한다.


# Terraform VPC 기본 구조

VPC 및 Subnet 기본 구성하기

## VPC 구성

{% hint style="info" %}
본 장의 가이드에서 다룬 코드 경로는  terraform/vpc/artd\_apnortheast2 입니다.
{% endhint %}

VPC는 대부분의 서비스의 근간이 되는 네트워크입니다. 따라서 다른 AWS 리소스를 생성하기 전에 VPC를 미리 세팅해두어야 합니다. 단, 무작정 구성을 하게 되는 경우에는 나중에 수정하기 힘들 수 있습니다.&#x20;

&#x20;본 실습에서는 `artd_apnortheast2` 에 VPC를 구성해보도록 하겠습니다.&#x20;

실습에 앞서, 아래 과정은 개별적으로 진행 부탁드립니다. `backend.tf` 파일 수정은 앞선 IAM 세팅을 참고하시기 바랍니다.

## 개발용 VPC 환경 구성하기

### 1. 디렉터리 만들기

* 기존의 artd\_apnortheast2 VPC 코드를 복사하여 새로운 디렉터리를 만듭니다.

```bash
$ cd terraform/vpc
$ cp -R artd_apnortheast2 devartd_apnortheast2
```

### 2. 코드 수정 및 설명

기존 코드를 그대로 사용할 수 없기 때문에, 생성하고자 하는 환경에 대한 정보로 설정을 변경해야 합니다.

### :rocket: **backend.tf 변경**

```hcl
terraform {
  required_version = ">= 1.5.7"

  backend "s3" {
    bucket         = "devart-preprod-apnortheast2-tfstate"
    key            = "devart/terraform/vpc/devartd_apnortheast2/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-lock"
  }
}
```

### :rocket: **terraform.tfvars 변경**

* cidr\_numeral 값은 10.X.0.0/16 에서 X에 들어갈 값입니다.

{% code title="terraform.tfvars" %}

```hcl
aws_region   = "ap-northeast-2"
cidr_numeral = "10"

# Please change "devart" to what you want to use
# d after name indicates develop. This means that devartd_apnortheast2 VPC is for development environment VPC in Seoul Region.
vpc_name = "devartd_apnortheast2"

# Billing tag in this VPC
billing_tag = "dev"

# Availability Zone list
availability_zones = ["ap-northeast-2a", "ap-northeast-2c"]

# In Seoul Region, some resources are not supported in ap-northeast-2b
availability_zones_without_b = ["ap-northeast-2a", "ap-northeast-2c"]

# shard_id will be used later when creating other resources.
# With shard_id, you could distinguish which environment the resource belongs to
shard_id       = "devartdapne2"
shard_short_id = "devart01d"

# d means develop
env_suffix = "d"
```

{% endcode %}

### :rocket: **Route53 Private Hosted Zone 생성**

* VPC에서 사용할 Internal DNS Record를 생성합니다. 내부통신에서 DNS를 사용하기 위해서 VPC마다 생성하는 것이 좋습니다.&#x20;
* VPC마다 독립적이기 때문에 이름은 중복되도 괜찮습니다. 본 실습에서는 모든 VPC의 내부 DNS는 `devart.internal` 로 통일합니다.

{% code title="route53.tf" %}

```hcl
resource "aws_route53_zone" "internal" {
  name    = "devart.internal"
  comment = "${var.vpc_name} - Managed by Terraform"

  vpc {
    vpc_id = aws_vpc.default.id
  }
}
```

{% endcode %}

### :rocket: **VPC마다 공통으로 가지고 있을 보안 그룹 생성**

* VPC에서 **공통으로 사용할 Security Group**을 생성합니다.
* **사무실/집에서 SSH 접속을 위한 Security Group**과 **Private Subnet에 있는 인스턴스에 접속하기 위한 Bastion 서버의 Security Group을 생성**할 예정입니다.
* Security Group ID는 추후에 다른 리소스 생성에 필요하므로 Output으로 빼야 합니다.
* 공통으로 필요한 Security Group이 있으면 본 실습에서 추가로 생성하시면 됩니다.

#### Default/Home Security Group

* Default SG : 인스턴스가 공통으로 가져야 할 보안그룹입니다.
* Home SG : Admin 페이지, kibana 페이지 등 접근 제어가 필요한 웹사이트에 회사에서만 접속할 수 있도록 설정하기 위한 보안그룹입니다.

{% hint style="info" %}
아래 예시 중에서 Node Exporter, Jmx Exporter, Kafka, Elasticsearch에 대한 inbound/outbound rule은 사용법을 알려드리기 위해 추가한 것입니다. 필요한 설정으로 변경 또는 삭제 후에 apply하시기 바랍니다.
{% endhint %}

{% code title="default\_sg.tf" %}

```hcl
# Default Security Group
# This is the security group for most of instances should have
resource "aws_security_group" "default" {
  name        = "default-${var.vpc_name}"
  description = "default group for ${var.vpc_name}"
  vpc_id      = aws_vpc.default.id

  egress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "https any outbound"
  }

  egress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "https any outbound"
  }

  # Instance should allow ifselt to send the log file to kafka
  egress {
    from_port   = 9092
    to_port     = 9092
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "kafka any outbound"
  }


  # Instance should allow ifselt to send the log file to elasticsearch
  egress {
    from_port   = 9200
    to_port     = 9200
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "ElasticSearch any outbound"
  }

}

# Home Security Group
# This will be usually attached to the web server that users in the office should access through browser..
# This is used for all users in the company to access to the resources in the office or home..
resource "aws_security_group" "home" {
  name        = "home"
  description = "Home Security Group for ${var.vpc_name}"
  vpc_id      = aws_vpc.default.id

  ingress {
    from_port = 22
    to_port   = 22
    protocol  = "tcp"

    cidr_blocks = [
      "xxx.xxx.xxx.xxx/32" # Change here to your office or house ...
    ]
  }

  ingress {
    from_port = 443
    to_port   = 443
    protocol  = "tcp"

    cidr_blocks = [
      "xxx.xxx.xxx.xxx/32" # Change here to your office or house ...
    ]
  }

  egress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "https any outbound"
  }
}
```

{% endcode %}

#### Bastion Security Group (Optional)

* Bastion SG : Bastion 서버에 사용할 보안그룹입니다.&#x20;
* Bastion Aware SG : Bastion을 통해 접속을 할 수 있도록 권한을 허용해주는 보안그룹입니다. 이 보안그룹은 private 인스턴스에 붙일 예정입니다.&#x20;

{% hint style="info" %}
Bastion 서버 없이 AWS Session Manager, Teleport 등을 통해서 SSH 접속을 하는 경우에는 아래 보안그룹은 생성하실 필요가 없습니다.
{% endhint %}

{% code title="bastion\_sg.tf" %}

```hcl
# Security Group to the bastion server
resource "aws_security_group" "bastion" {
  name        = "bastion-${var.vpc_name}"
  description = "Allows SSH access to the bastion server"

  vpc_id = aws_vpc.default.id

  ingress {
    from_port = 22        # Specify the port you use for SSH
    to_port   = 22
    protocol  = "tcp"

    cidr_blocks = [
      "xxx.xxx.xxx.xxx/32"  # Change here to your office or house ...
    ]
  }

  egress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "http port any outbound"
  }

  egress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "https port any outbound"
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["10.0.0.0/8"]
  }

  tags = {
    Name = "bastion-${var.vpc_name}"
  }
}

# Security Group from the bastion server
# This will be attached to the private instance which user wants to access through bastion host
resource "aws_security_group" "bastion_aware" {
  name        = "bastion_aware-${var.vpc_name}"
  description = "Allows SSH access from the Bastion server"

  vpc_id = aws_vpc.default.id

  ingress {
    from_port       = 22
    to_port         = 22
    protocol        = "tcp"
    security_groups = [aws_security_group.bastion.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "bastionAware-${var.vpc_name}"
  }
}


```

{% endcode %}

### :rocket: **VPC 기본 세트 생성**

VPC에 필요한 기본 구성을 패키지로 설치합니다. 본 파일에는 아래의 구성요소들이 들어있습니다.

* VPC
* Public Subnet / Private Subnet / DB Subnet(private)
* Elastic IP for NAT
* Route Table
* Internet Gateway
* NAT Gateway

{% code title="vpc.tf" %}

```hcl
# VPC
# Whole network cidr will be 10.0.0.0/8 
# A VPC cidr will use the B class with 10.xxx.0.0/16
# You should set cidr advertently because if the number of VPC get larger then the ip range could be in shortage.
resource "aws_vpc" "default" {
  cidr_block           = "10.${var.cidr_numeral}.0.0/16" # Please set this according to your company size
  enable_dns_hostnames = true

  tags = {
    Name = "vpc-${var.vpc_name}"
  }
}

# Internet Gateway
resource "aws_internet_gateway" "default" {
  vpc_id = aws_vpc.default.id

  tags = {
    Name = "igw-${var.vpc_name}"
  }
}


## NAT Gateway 
resource "aws_nat_gateway" "nat" {
  # Count means how many you want to create the same resource
  # This will be generated with array format
  # For example, if the number of availability zone is three, then nat[0], nat[1], nat[2] will be created.
  # If you want to create each resource with independent name, then you have to copy the same code and modify some code
  count = length(var.availability_zones)

  # element is used for select the resource from the array 
  # Usage = element (array, index) => equals array[index]
  allocation_id = element(aws_eip.nat.*.id, count.index)

  #Subnet Setting
  # nat[0] will be attached to subnet[0]. Same to all index.
  subnet_id = element(aws_subnet.public.*.id, count.index)

  lifecycle {
    create_before_destroy = true
  }

  tags = {
    Name = "NAT-GW${count.index}-${var.vpc_name}"
  }

}

# Elastic IP for NAT Gateway 
resource "aws_eip" "nat" {
  # Count value should be same with that of aws_nat_gateway because all nat will get elastic ip
  count = length(var.availability_zones)
  domain   = "vpc"

  lifecycle {
    create_before_destroy = true
  }
}



#### PUBLIC SUBNETS
# Subnet will use cidr with /20 -> The number of available IP is 4,096  (Including reserved ip from AWS)
resource "aws_subnet" "public" {
  count  = length(var.availability_zones)
  vpc_id = aws_vpc.default.id

  cidr_block        = "10.${var.cidr_numeral}.${var.cidr_numeral_public[count.index]}.0/20"
  availability_zone = element(var.availability_zones, count.index)

  # Public IP will be assigned automatically when the instance is launch in the public subnet
  map_public_ip_on_launch = true

  tags = {
    Name = "public${count.index}-${var.vpc_name}"
  }
}

# Route Table for public subnets
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.default.id

  tags = {
    Name = "publicrt-${var.vpc_name}"
  }
}


# Route Table Association for public subnets
resource "aws_route_table_association" "public" {
  count          = length(var.availability_zones)
  subnet_id      = element(aws_subnet.public.*.id, count.index)
  route_table_id = aws_route_table.public.id
}

#### PRIVATE SUBNETS
# Subnet will use cidr with /20 -> The number of available IP is 4,096  (Including reserved ip from AWS)
resource "aws_subnet" "private" {
  count  = length(var.availability_zones)
  vpc_id = aws_vpc.default.id

  cidr_block        = "10.${var.cidr_numeral}.${var.cidr_numeral_private[count.index]}.0/20"
  availability_zone = element(var.availability_zones, count.index)

  tags = {
    Name    = "private${count.index}-${var.vpc_name}"
    Network = "Private"
  }
}

# Route Table for private subnets
resource "aws_route_table" "private" {
  count  = length(var.availability_zones)
  vpc_id = aws_vpc.default.id

  tags = {
    Name    = "private${count.index}rt-${var.vpc_name}"
    Network = "Private"
  }
}

# Route Table Association for private subnets
resource "aws_route_table_association" "private" {
  count          = length(var.availability_zones)
  subnet_id      = element(aws_subnet.private.*.id, count.index)
  route_table_id = element(aws_route_table.private.*.id, count.index)
}


# DB PRIVATE SUBNETS
# This subnet is only for the database. 
# For security, it is better to assign ip range for database only. This subnet will not use NAT Gateway
# This is also going to use /20 cidr, which might be too many IPs... Please count it carefully and change the cidr.
resource "aws_subnet" "private_db" {
  count  = length(var.availability_zones)
  vpc_id = aws_vpc.default.id

  cidr_block        = "10.${var.cidr_numeral}.${var.cidr_numeral_private_db[count.index]}.0/20"
  availability_zone = element(var.availability_zones, count.index)

  tags = {
    Name    = "db-private${count.index}-${var.vpc_name}"
    Network = "Private"
  }
}

# Route Table for DB subnets
resource "aws_route_table" "private_db" {
  count  = length(var.availability_zones)
  vpc_id = aws_vpc.default.id

  tags = {
    Name    = "privatedb${count.index}rt-${var.vpc_name}"
    Network = "Private"
  }
}

# Route Table Association for DB subnets
resource "aws_route_table_association" "private_db" {
  count          = length(var.availability_zones)
  subnet_id      = element(aws_subnet.private_db.*.id, count.index)
  route_table_id = element(aws_route_table.private_db.*.id, count.index)
}

```

{% endcode %}

### :rocket: **라우팅 테이블 구성**

이전에 만든 IGW와 NAT를 사용할 수 있도록 route table에 등록해야 합니다. 추후에 peering용으로 routes를 추가할 예정이므로 편의상 파일을 분리해서 관리합니다.

{% code title="route\_table\_routes.tf" %}

```hcl
# routes for internet gateway which will be set in public subent
resource "aws_route" "public_internet_gateway" {
  route_table_id         = aws_route_table.public.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.default.id
}

## routes for NAT gateway which will be set in private subent
resource "aws_route" "private_nat" {
  count                  = length(var.availability_zones)
  route_table_id         = element(aws_route_table.private.*.id, count.index)
  destination_cidr_block = "0.0.0.0/0"
  nat_gateway_id         = element(aws_nat_gateway.nat.*.id, count.index)
}
```

{% endcode %}

### Output

* VPC에서 생성된 리소스는 추후에 다른 리소스에서 사용할 예정입니다.
* 사용할 값들은 전부 output으로 추가해 놓습니다.

{% code title="outputs.tf" %}

```hcl
# Region
output "aws_region" {
  description = "Region of VPC"
  value = var.aws_region
}

output "region_namespace" {
  description = "Region name without '-'"
  value = replace(var.aws_region, "-", "")
}

# Availability_zones
output "availability_zones" {
  description = "Availability zone list of VPC"
  value = var.availability_zones
}

# VPC
output "vpc_name" {
  description = "The name of the VPC which is also the environment name"
  value       = var.vpc_name
}

output "vpc_id" {
  description = "VPC ID of newly created VPC"
  value = aws_vpc.default.id
}

output "cidr_block" {
  description = "CIDR block of VPC"
  value = aws_vpc.default.cidr_block
}

output "cidr_numeral" {
  description = "number that specifies the vpc range (B class)"
  value = var.cidr_numeral
}

# Shard
output "shard_id" {
  description = "The shard ID which will be used to distinguish the env of resources"
  value       = var.shard_id
}

output "shard_short_id" {
  description = "Short version of shard ID"
  value       = var.shard_short_id
}

# Prviate subnets
output "private_subnets" {
  description = "List of private subnet ID in VPC"
  value = aws_subnet.private.*.id
}

# Public subnets
output "public_subnets" {
  description = "List of public subnet ID in VPC"
  value = aws_subnet.public.*.id
}

# Private Database Subnets
output "db_private_subnets" {
  description = "List of DB private subnet ID in VPC"
  value = aws_subnet.private_db.*.id
}

# Route53
output "route53_internal_zone_id" {
  description = "Internal Zone ID for VPC"
  value = aws_route53_zone.internal.zone_id
}

output "route53_internal_domain" {
  description = "Internal Domain Name for VPC"
  value = aws_route53_zone.internal.name
}

# Security Group
output "aws_security_group_bastion_id" {
  description = "ID of bastion security group"
  value = aws_security_group.bastion.id
}

output "aws_security_group_bastion_aware_id" {
  description = "ID of bastion aware security group"
  value = aws_security_group.bastion_aware.id
}

output "aws_security_group_default_id" {
  description = "ID of default security group"
  value = aws_security_group.default.id
}

output "aws_security_group_home_id" {
  description = "ID of home security group"
  value = aws_security_group.home.id
}

# ETC
output "env_suffix" {
  description = "Suffix of the environment"
  value       = var.env_suffix
}

output "billing_tag" {
  description = "The environment value for biliing consolidation."
  value       = var.billing_tag
}


```

{% endcode %}

###

### 리소스 생성

Terraform plan / apply 를 통해서 리소스를 생성합니다.&#x20;

```bash
$ terraform plan -parallelism=30
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_eip.nat[0] will be created
  + resource "aws_eip" "nat" {
      + allocation_id        = (known after apply)
      + association_id       = (known after apply)
      + carrier_ip           = (known after apply)
      + customer_owned_ip    = (known after apply)
      + domain               = "vpc"
      + id                   = (known after apply)
      + instance             = (known after apply)
      + network_border_group = (known after apply)
      + network_interface    = (known after apply)
      + private_dns          = (known after apply)
      + private_ip           = (known after apply)
      + public_dns           = (known after apply)
      + public_ip            = (known after apply)
      + public_ipv4_pool     = (known after apply)
      + tags_all             = (known after apply)
      + vpc                  = (known after apply)
    }

  # aws_eip.nat[1] will be created
  + resource "aws_eip" "nat" {
      + allocation_id        = (known after apply)
      + association_id       = (known after apply)
      + carrier_ip           = (known after apply)
      + customer_owned_ip    = (known after apply)
      + domain               = "vpc"
      + id                   = (known after apply)
      + instance             = (known after apply)
      + network_border_group = (known after apply)
      + network_interface    = (known after apply)
      + private_dns          = (known after apply)
      + private_ip           = (known after apply)
      + public_dns           = (known after apply)
      + public_ip            = (known after apply)
      + public_ipv4_pool     = (known after apply)
      + tags_all             = (known after apply)
      + vpc                  = (known after apply)
    }
    
( ... 생략 ... )
 

Plan: 31 to add, 0 to change, 0 to destroy.
```

`Plan: xxx to add, 0 to change, 0 to destroy.` 가 나오면 정상입니다.

이제, 생성을 진행합니다.

```bash
$ terraform apply -parallelism=30

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:
(... plan 결과 생략 ...)

Plan: 31 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes
  
---------------------------------------------------------------------

Apply complete! Resources: 31 added, 0 changed, 0 destroyed.

Outputs:

availability_zones = tolist([
  "ap-northeast-2a",
  "ap-northeast-2c",
])
aws_region = "ap-northeast-2"
aws_security_group_bastion_aware_id = "sg-0aeeaf3ae5f70a154"
aws_security_group_bastion_id = "sg-08885ed25a1ced3fc"
aws_security_group_default_id = "sg-0d49f29a84b8072cc"
aws_security_group_home_id = "sg-0e812c59c8dffc60c"
billing_tag = "dev"
cidr_block = "10.10.0.0/16"
cidr_numeral = "10"
db_private_subnets = [
  "subnet-07164618b0ed67e43",
  "subnet-08c17bac56527428d",
]
env_suffix = "d"
private_subnets = [
  "subnet-022647cb8aa0138ba",
  "subnet-0e7f38f10d2e3a02b",
]
public_subnets = [
  "subnet-0ceb5148a5675d145",
  "subnet-045d93791eb59c00c",
]
region_namespace = "apnortheast2"
route53_internal_domain = "devart.internal"
route53_internal_zone_id = "Z0615571LPRS2FX9TEGL"
shard_id = "devartdapne2"
shard_short_id = "devart01d"
vpc_id = "vpc-0f1c1a1cfad0ac154"
vpc_name = "devartd_apnortheast2"
```

위의 예시처럼 output이 여러개 나오면 성공입니다. 콘솔에 가셔서 생성된 리소스를 한 번 확인해보시기 바랍니다.&#x20;


# (Optional) 멀티 VPC 구성

## Production 환경 구성

* Production 환경은 위의 파일 세팅과 동일하게 진행하여 생성하시면 됩니다.
* `backend.tf` 와  `terraform.tfvars` 에 있는 값만 Production에 맞게 변경해주시면 됩니다.

### :rocket: 운영환경용 폴더 생성

* 앞에서 생성한 `devartd_apnortheast2` 폴더를 통째로 복사합니다. 그리고, `.terraform` 에 상태값이 저장되기 때문에, 삭제하고 다시&#x20;

```bash
$ cp -R devartd_apnortheast2 devartp_apnortheast2
$ cd devartp_apnortheast2
$ rm -rf .terraform*
```

###

### :rocket: 변수값 변경

`backend.tf`에서 저장될 버킷 이름을 변경합니다. **다만, 본 실습에서는 같은 계정으로 진행하므로 변경은 생략합니다.**

{% code title="backend.tf" %}

```hcl
terraform {
  required_version = ">= 1.5.7"

  backend "s3" {
    bucket         = "devart-preprod-apnortheast2-tfstate"
    key            = "devart/terraform/vpc/devartp_apnortheast2/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-lock"
  }
}
```

{% endcode %}

`terraform.tfvars` 에 실제 변수값을 변경합니다.

{% code title="terraform.tfvars" %}

```hcl
aws_region   = "ap-northeast-2"
cidr_numeral = "11"

# Please change "devart" to what you want to use
# d after name indicates develop. This means that devartd_apnortheast2 VPC is for development environment VPC in Seoul Region.
vpc_name = "devartp_apnortheast2"

# Billing tag in this VPC
billing_tag = "prod"

# Availability Zone list
availability_zones = ["ap-northeast-2a", "ap-northeast-2c"]

# In Seoul Region, some resources are not supported in ap-northeast-2b
availability_zones_without_b = ["ap-northeast-2a", "ap-northeast-2c"]

# shard_id will be used later when creating other resources.
# With shard_id, you could distinguish which environment the resource belongs to
shard_id       = "devartpapne2"
shard_short_id = "devart01p"

# d means develop
env_suffix = "p"

# VPC Peering Connection Variables
# vpc_peer_connection_id_devartp_apne2 = "pcx-"
# devartp_destination_cidr_block       = "10.20.0.0/16"
```

{% endcode %}

###

### 리소스 생성

Terraform plan / apply 를 통해서 리소스를 생성합니다. 과정은 위와 동일하므로 생략합니다.

만약, account가 다른 경우에는 해당 Account로 assume을 다시 하셔야 합니다.


# VPC Endpoint

VPC에서 리소스와 직접 통신하기 위한 VPC Endpoint 생성

### VPC Endpoint 역할

* VPC Endpoint는 다른 AWS 서비스를 사용할 때 AWS 내부망을 사용하여 내부 통신을 하고자 할 때 사용합니다.
* VPC Endpoint를 사용하면 인터넷을 타고 나가지 않기 때문에 외부로부터 통신을 안전하게 보호할 수 있고, 내부망을 타기 때문에 속도가 향상될 수 있습니다.
* <https://docs.aws.amazon.com/ko_kr/vpc/latest/userguide/vpc-endpoints.html>

### Gateway형 Endpoint

* 인터넷으로 API통신을 하는 S3와 DynamoDB는 Gateway 형으로 Endpoint가 생성됩니다.&#x20;
* Gateway형 Endpoint는 따로 보안그룹을 생성하실 필요가 없습니다.

### Interface 형 Endpoint

* 기타 서비스들은 Interface형으로 Endpoint를 생성하실 수 있습니다.
* Interface형 Endpoint는 별도로 보안그룹이 필요합니다.
* 보안그룹은 기본적으로 443포트 인바운드를 허용해주시면 됩니다.&#x20;
* 본 실습에서는 API Gateway와 Secrets Manager에 대한 VPC Endpoint를 생성하도록 하겠습니다.

```hcl
# Gateway Type Endpoint ( S3, DynamoDB )
## S3 Endpoint
resource "aws_vpc_endpoint" "s3_endpoint" {
  vpc_id       = aws_vpc.default.id
  service_name = "com.amazonaws.${var.aws_region}.s3"
  
  tags = {
   Name = "s3-${var.shard_id}"
  }
}

# Add vpc endpoint to route table of private subnet
resource "aws_vpc_endpoint_route_table_association" "s3_endpoint_routetable" {
  count           = length(var.availability_zones)
  vpc_endpoint_id = aws_vpc_endpoint.s3_endpoint.id
  route_table_id  = aws_route_table.private[count.index].id
}


## DynamoDB Endpoint
resource "aws_vpc_endpoint" "dynamodb_endpoint" {
  vpc_id       = aws_vpc.default.id
  service_name = "com.amazonaws.${var.aws_region}.dynamodb"

  tags = {
   Name = "dynamodb-${var.shard_id}"
  }
}

# Add vpc endpoint to route table of private subnet
resource "aws_vpc_endpoint_route_table_association" "dynamodb_endpoint_routetable" {
  count           = length(var.availability_zones)
  vpc_endpoint_id = aws_vpc_endpoint.dynamodb_endpoint.id
  route_table_id  = aws_route_table.private[count.index].id
}


# Interface Type Endpoint 
# Security Group of VPC Endpoint (API Gateway)
resource "aws_security_group" "apigateway_vpc_endpoint_sg" {
  name = "apigateway_vpc_endpoint-${var.vpc_name}"
  vpc_id = aws_vpc.default.id

  ingress {
    from_port = 443
    to_port = 443
    protocol = "TCP"

    cidr_blocks = [
      "10.0.0.0/8"
    ]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["10.0.0.0/8"]
    description = "Internal outbound any traffic"
  }

  tags = {
    Name = "apigateway-${var.vpc_name}"
  }
}

## API Gateway Endpoint
resource "aws_vpc_endpoint" "apigateway_endpoint" {
  vpc_id            = aws_vpc.default.id
  service_name      = "com.amazonaws.${var.aws_region}.execute-api"
  vpc_endpoint_type = "Interface"

  security_group_ids = [
    aws_security_group.apigateway_vpc_endpoint_sg.id
  ]

  private_dns_enabled = true
  auto_accept         = true

  tags = {
   Name = "apigateway-${var.shard_id}"
  }
}

resource "aws_vpc_endpoint_subnet_association" "apigateway_endpoint" {
  count           = length(var.availability_zones)
  vpc_endpoint_id = aws_vpc_endpoint.apigateway_endpoint.id
  subnet_id       = element(aws_subnet.private.*.id, count.index)
}

## Secrets Manager Endpoint
# Security Group of VPC Endpoint (Secrets Manager)
resource "aws_security_group" "secretsmanager_vpc_endpoint_sg" {
  name = "secretsmanager_vpc_endpoint-${var.vpc_name}"
  vpc_id = aws_vpc.default.id

  ingress {
    from_port = 443
    to_port = 443
    protocol = "TCP"

    cidr_blocks = [
      "10.0.0.0/8"
    ]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["10.0.0.0/8"]
    description = "Internal outbound any traffic"
  }

  tags = {
    Name = "secretsmanager-${var.vpc_name}"
  }
}

# Secrets Manager VPC Endpoint
resource "aws_vpc_endpoint" "secretsmanager_endpoint" {
  vpc_id = aws_vpc.default.id
  service_name = "com.amazonaws.${var.aws_region}.secretsmanager"
  vpc_endpoint_type = "Interface"

  security_group_ids = [
    aws_security_group.secretsmanager_vpc_endpoint_sg.id
  ]

  private_dns_enabled = true
  auto_accept = true

  tags = {
   Name = "secretsmanager-${var.shard_id}"
  }
}

resource "aws_vpc_endpoint_subnet_association" "secretsmanager_endpoint" {
  count           = length(var.availability_zones)
  vpc_endpoint_id = aws_vpc_endpoint.secretsmanager_endpoint.id
  subnet_id = aws_subnet.private[count.index].id
}
```

Terraform apply를 하시고 콘솔에서 확인해보시면 Endpoint가 생성된 것을 확인하실 수 있습니다.&#x20;

<figure><img src="/files/IU59H122xvOioKYwGYMa" alt=""><figcaption></figcaption></figure>

라우팅 테이블을 확인해보면, Gateway형 Endpoint가 추가되어 있는 것을 보실 수 있습니다.

<figure><img src="/files/m2lbXkKfIu5QGYk026gt" alt=""><figcaption></figcaption></figure>


# (Optional) VPC Peering

VPC 간 통신을 위한 peering 작업

### VPC Peering

![VPC Peering](/files/-MP_DkNhnj_pj6AHWk7o)

### VPC 간 통신

* VPC 간 내부 통신을 위해서는 Peering을 맺어야 합니다.
* 단, 주의해야할 점은 peering하는 두 VPC의 CIDR가 겹치지 않아야 합니다.
* 최근에는 Transit Gateway도 유용하게 쓰이지만, 본 실습에서는 종단 간 VPC Peering을 맺도록 하겠습니다.
* VPC Peering을 맺기 위해서는 Requester는 Peering 요청을 하고, Acceptor는 요청을 승인해야 합니다. 이후에는 해당 peering ID를 destination IP range와 Route 테이블에 저장합니다.
  * Requester : artp VPC
  * Acceptor : artd VPC

{% hint style="info" %}
Requester는 Peering을 가장 많이 맺는 VPC로 정하시면 편리합니다.
{% endhint %}

### Requester 생성

먼저 `peering.tf`  파일을 생성(수정)해서 peering request 코드를 작성합니다. Peering request는 어느 계정에서 해도 상관 없지만, 관리의 편의성을 위해서 peering connection이 많은 VPC를 기준으로 설계하면 좋습니다.

이번에는 devartd VPC에서 devartp VPC로 요청을 보내는 시나리오로 구성해보겠습니다.

{% code title="peering.tf" %}

```hcl
resource "aws_vpc_peering_connection" "peering_connection" {
  for_each      = { for entry in var.vpc_peering_list : entry.peer_vpc_name => entry }
  peer_vpc_id   = each.value.peer_vpc_id
  peer_owner_id = each.value.peer_owner_id
  peer_region   = each.value.peer_region
  vpc_id        = aws_vpc.default.id

  tags = {
    Name          = "${var.shard_id}-with-${each.value.peer_vpc_name}"
    peer_vpc_name = each.value.peer_vpc_name
    Side          = "Requester"
  }
}
```

{% endcode %}

위 코드에 들어가는 변수를 `variables.tf` 에 추가합니다.&#x20;

{% code title="variables.tf" %}

```hcl
variable "vpc_peering_list" {
  type = list(object({
    peer_vpc_id   = string
    peer_vpc_name = string
    peer_region   = string
    vpc_cidrs     = list(string)
    peer_owner_id = string
  }))
  description = "A list of maps containing key/value pairs that define vpc peering."
}
```

{% endcode %}

변수가 들어갈 값을 `terraform.tfvars`에 추가합니다.&#x20;

{% code title="terraform.tfvars" %}

```hcl
# (...생략...)

# Peering List
vpc_peering_list = [
  {
    peer_vpc_id   = "vpc-0acb7d87bcbd212f0"
    peer_owner_id = "816736805842"
    peer_region   = "ap-northeast-2"
    peer_vpc_name = "devartp_apnortheast2"
    vpc_cidrs     = [ "10.11.0.0/16" ]
  }
]
```

{% endcode %}

그 다음, 해당 Peering을 사용하기 위한 라우팅 설정을 추가합니다.

{% code title="peering\_routes.tf" %}

```hcl
locals {
  private_peering_route_list = flatten([
    for pair in setproduct(var.vpc_peering_list, aws_route_table.private.*.id) : [
      for cidr in pair[0].vpc_cidrs : {
        peer_vpc_name  = pair[0].peer_vpc_name
        vpc_cidr       = cidr
        route_table_id = pair[1]
      }
    ]
  ])
}

resource "aws_route" "private_peering_with_request" {
  for_each                  = { for entry in local.private_peering_route_list : "${entry.peer_vpc_name}_${entry.vpc_cidr}_${entry.route_table_id}" => entry }
  route_table_id            = each.value.route_table_id
  destination_cidr_block    = each.value.vpc_cidr
  vpc_peering_connection_id = aws_vpc_peering_connection.peering_connection[each.value.peer_vpc_name].id
}
```

{% endcode %}

이제 terraform plan, apply를 통해서 request 요청을 보냅니다.

```bash
$ terraform plan -parallelism=30

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_route.private_peering_with_request["devartp_apnortheast2_10.11.0.0/16_rtb-0a1f84db0139d73f6"] will be created
  + resource "aws_route" "private_peering_with_request" {
      + destination_cidr_block    = "10.11.0.0/16"
      + id                        = (known after apply)
      + instance_id               = (known after apply)
      + instance_owner_id         = (known after apply)
      + network_interface_id      = (known after apply)
      + origin                    = (known after apply)
      + route_table_id            = "rtb-0a1f84db0139d73f6"
      + state                     = (known after apply)
      + vpc_peering_connection_id = "pcx-025501a85c5371c20"
    }

  # aws_route.private_peering_with_request["devartp_apnortheast2_10.11.0.0/16_rtb-0d76e08459db46366"] will be created
  + resource "aws_route" "private_peering_with_request" {
      + destination_cidr_block    = "10.11.0.0/16"
      + id                        = (known after apply)
      + instance_id               = (known after apply)
      + instance_owner_id         = (known after apply)
      + network_interface_id      = (known after apply)
      + origin                    = (known after apply)
      + route_table_id            = "rtb-0d76e08459db46366"
      + state                     = (known after apply)
      + vpc_peering_connection_id = "pcx-025501a85c5371c20"
    }

  # aws_vpc_peering_connection.peering_connection["devpartp_apnortheast2"] will be created
  + resource "aws_vpc_peering_connection" "peering_connection" {
      + accept_status = (known after apply)
      + id            = (known after apply)
      + peer_owner_id = "816736805842"
      + peer_region   = "ap-northeast-2"
      + peer_vpc_id   = "vpc-0acb7d87bcbd212f0"
      + tags          = {
          + "Name"          = "devartdapne2-with-devartp_apnortheast2"
          + "Side"          = "Requester"
          + "peer_vpc_name" = "devartp_apnortheast2"
        }
      + tags_all      = {
          + "Name"          = "devartdapne2-with-devartp_apnortheast2"
          + "Side"          = "Requester"
          + "peer_vpc_name" = "devartp_apnortheast2"
        }
      + vpc_id        = "vpc-0f1c1a1cfad0ac154"
    }

Plan: 3 to add, 0 to change, 0 to destroy.
```

```
$ terraform apply -parallelism=30
...

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
```

콘솔에서 확인해보시면 Request가 생성된 것을 확인하실 수 있습니다.&#x20;

<figure><img src="/files/XFIRwS1UIIiM473JAD0M" alt=""><figcaption></figcaption></figure>

### Accepter 생성

이제 `devartp` VPC 에서 Accepter를 생성하도록 하겠습니다.

{% code title="peering.tf" %}

```hcl
resource "aws_vpc_peering_connection_accepter" "peering_accepter" {
  for_each                  = { for entry in var.peering_requests : entry.id => entry }
  vpc_peering_connection_id = each.value.id
  auto_accept               = true
  tags = {
    Name = "vpc-peering-${var.shard_id}-with-${each.value.vpc_name}"
  }

  lifecycle {
    ignore_changes = [tags]
  }
}
```

{% endcode %}

추가한 변수를 `variables.tf`에 추가합니다.

{% code title="variables.tf" %}

```hcl
variable "peering_requests" {
  type = list(object({
    id         = string
    vpc_name   = string
    cidr_block = string
  }))
  description = "peering connection id for accepting the peering connection "
}
```

{% endcode %}

변수에 대한 값을 terraform.tfvars에 추가합니다. 이때 Peering ID에는 이전 단계에서 생성한 request에서 peering connection ID(pcx-xxx)를 넣어주시면 됩니다.

{% code title="terraform.tfvars" %}

```hcl
peering_requests = [
  {
    "id"         = "pcx-025501a85c5371c20"
    "vpc_name"   = "devartd_apnortheast2"
    "cidr_block" = "10.10.0.0/16"
  },
]
```

{% endcode %}

추가로, Peering을 사용할 Subnet에 라우팅을 연결합니다.

{% code title="peering\_routes.tf" %}

```hcl
locals {
  private_subnet_peerings = flatten([
    for pair in setproduct(aws_route_table.private.*.id, var.peering_requests) : {
      route_table_id = pair[0]
      cidr_block     = pair[1].cidr_block
      vpc_name       = pair[1].vpc_name
      peering_id     = pair[1].id
    }
  ])
}

resource "aws_route" "private_peering" {
  for_each                  = { for entry in local.private_subnet_peerings : "${entry.vpc_name}_${entry.route_table_id}" => entry }
  route_table_id            = each.value.route_table_id
  destination_cidr_block    = each.value.cidr_block
  vpc_peering_connection_id = each.value.peering_id
}
```

{% endcode %}

이제 terraform plan, apply를 통해서 accept를 적용합니다.

```bash
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_route.private_peering["devartd_apnortheast2_rtb-0061eee04f6d3089d"] will be created
  + resource "aws_route" "private_peering" {
      + destination_cidr_block    = "10.10.0.0/16"
      + id                        = (known after apply)
      + instance_id               = (known after apply)
      + instance_owner_id         = (known after apply)
      + network_interface_id      = (known after apply)
      + origin                    = (known after apply)
      + route_table_id            = "rtb-0061eee04f6d3089d"
      + state                     = (known after apply)
      + vpc_peering_connection_id = "pcx-025501a85c5371c20"
    }

  # aws_route.private_peering["devartd_apnortheast2_rtb-0232754f8aeab7d2d"] will be created
  + resource "aws_route" "private_peering" {
      + destination_cidr_block    = "10.10.0.0/16"
      + id                        = (known after apply)
      + instance_id               = (known after apply)
      + instance_owner_id         = (known after apply)
      + network_interface_id      = (known after apply)
      + origin                    = (known after apply)
      + route_table_id            = "rtb-0232754f8aeab7d2d"
      + state                     = (known after apply)
      + vpc_peering_connection_id = "pcx-025501a85c5371c20"
    }

  # aws_vpc_peering_connection_accepter.peering_accepter["pcx-025501a85c5371c20"] will be created
  + resource "aws_vpc_peering_connection_accepter" "peering_accepter" {
      + accept_status             = (known after apply)
      + auto_accept               = true
      + id                        = (known after apply)
      + peer_owner_id             = (known after apply)
      + peer_region               = (known after apply)
      + peer_vpc_id               = (known after apply)
      + tags                      = {
          + "Name" = "vpc-peering-devartpapne2-with-devartd_apnortheast2"
        }
      + tags_all                  = {
          + "Name" = "vpc-peering-devartpapne2-with-devartd_apnortheast2"
        }
      + vpc_id                    = (known after apply)
      + vpc_peering_connection_id = "pcx-025501a85c5371c20"
    }

Plan: 3 to add, 0 to change, 0 to destroy.
```

콘솔에서 확인해보시면 해당 connection이 Active로 변경된 것을 확인하실 수 있습니다.

<figure><img src="/files/GXg3o63nldt2Xjdsxg2c" alt=""><figcaption></figcaption></figure>


# 2. AWS IAM Role을 활용한 계정 관리

AWS IAM Role을 사용하면 여러 계정을 가지고 있더라도, 로그인 계정을 별도로 만들어서 관리할 수 있습니다. 그러면 AWS 계정을 만들 때마다 사람들에게 IAM 사용자를 만들어 줄 필요가 없습니다. 또한 Role을 다양하게 만들어서 사람들에게 부여하면, 최소 권한 부여 원칙을 지키면서 사용자에게 필요한 권한만을 부여할 수 있습니다.

아래는 설명을 위해 간단하게 상황을 가정한 예시입니다. 일반 개발자와 보안 엔지니어 모두 같은 AWS 계정으로 로그인하지만 실제 접근할 수 있는 AWS 계정은 서로 다릅니다.&#x20;

* 개발자: 서비스의 개발/운영 계정에 대한 권한을 부여하되, 개발 환경은 자유롭게 활용할 수 있도록 어드민 권한을 부여합니다. 단, 보안 계정은 접근할 필요가 없으므로,  권한을 부여하지 않습니다.
* 보안 엔지니어: 보안을 위해 필요한 기능들을 구축하고, 운영할 수 있도록 보안 계정에만 권한을 부여합니다. 일반 서비스 계정에는 접근할 필요가 없으므로, 권한을 부여하지 않습니다.

<figure><img src="/files/EtsDhNtHPcdBCMxT1L1b" alt=""><figcaption></figcaption></figure>


# 0. 사전 준비

이번 장에서는 로그인 계정(art-id)과 일반 운영환경 계정(Production) 간 Assume Role을 통한 계정 관리 사례를 실습해볼 예정입니다. 계정을 여러개 더 추가하고 싶으시다고 하면, 운영환경과 동일한 방식으로 여러번 진행하시면 빠르게 구축하실 수 있습니다.

* 실습을 위해서는 AWS 계정이 2개 있으면 좋습니다. IAM은 무료이기 때문에 계정을 2개 만드셔서 실습을 해보시는 것을 권장드립니다!
* 본 실습에서는 art-id, art-prod 와 같이 계정 alias를 사용합니다. 실습하실 때는 그대로 사용하기보다, `art-` 부분을 원하시는 이름으로 변경해서 사용하실 것을 권장드립니다. 참고로, AWS 계정의 alias는 중복을 허용하지 않으니 본인만의 이름을 사용하시는 것이 좋습니다.


# 1. 로그인 계정 설정

### backend와 provider 설정

* `terraform/iam/art-id/backend.tf` 파일을 수정합니다.

{% code title="terraform/iam/art-id/backend.tf" %}

```hcl
terraform {
  required_version = ">= 1.0.0" # Terraform Version

  backend "s3" {
    bucket         = "art-id-apnortheast2-tfstate" # Set bucket name 
    key            = "art/terraform/iam/art-id/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-lock" # Set DynamoDB Table
  }
}
```

{% endcode %}

{% code title="vim provider.tf" %}

```hcl
provider "aws" {
  region = "us-east-1" # IAM is global
}
```

{% endcode %}

###

### 공통 권한 생성

* 모든 사용자가 공통으로 가져야 할 권한을 생성합니다.
* 이후 각 그룹마다 권한을 매핑할 예정입니다.

`terraform/iam/art-id/every_policy.tf` 를 확인합니다. 수정하실 필요는 없습니다.

{% code title="terraform/iam/art-id/every\_policy.tf" %}

```hcl
#### Permission to rotate key
resource "aws_iam_policy" "rotate_keys" {
  name        = "RotateKeys"
  description = "allow users to change their aws keys, and passwords."

  policy = data.aws_iam_policy_document.rotate_keys.json
}

data "aws_iam_policy_document" "rotate_keys" {
  statement {
    actions = [
      "iam:*LoginProfile",
      "iam:*AccessKey*",
      "iam:*SSHPublicKey*"
    ]
    resources = ["arn:aws:iam::${var.account_id}:user/$${aws:username}"]
  }

  statement {
    actions = [
      "iam:ListAccount*",
      "iam:GetAccountSummary",
      "iam:GetAccountPasswordPolicy",
      "iam:ListUsers"
    ]
    resources = ["*"]
  }
}

#### Permission to self managed MFA
resource "aws_iam_policy" "self_managed_mfa" {
  name        = "SelfManageMFA"
  description = "allow a user to manage their own MFA device."

  policy = data.aws_iam_policy_document.self_managed_mfa.json
}

data "aws_iam_policy_document" "self_managed_mfa" {
  statement {
    actions = [
      "iam:*VirtualMFADevice"
    ]
    resources = ["arn:aws:iam::${var.account_id}:mfa/$${aws:username}"]
  }

  statement {
    actions = [
      "iam:GetAccountPasswordPolicy"
    ]
    resources = ["*"]
  }

  statement {
    actions = [
      "iam:ChangePassword"
    ]
    resources = ["arn:aws:iam::${var.account_id}:user/$${aws:username}"]
  }

  statement {
    actions = [
      "iam:DeactivateMFADevice",
      "iam:EnableMFADevice",
      "iam:ListMFADevices",
      "iam:ResyncMFADevice"
    ]
    resources = ["arn:aws:iam::${var.account_id}:user/$${aws:username}"]
  }

  statement {
    actions = [
      "iam:ListVirtualMFADevices"
    ]
    resources = ["arn:aws:iam::${var.account_id}:mfa/*"]
  }

  statement {
    actions = [
      "iam:ListUsers"
    ]
    resources = ["arn:aws:iam::${var.account_id}:user/*"]
  }
}

#### Force user to use MFA for security issue
resource "aws_iam_policy" "force_mfa" {
  name        = "ForceMFA"
  description = "disallow a user anything unless MFA enabled"

  policy = data.aws_iam_policy_document.force_mfa.json
}


data "aws_iam_policy_document" "force_mfa" {
  statement {
    not_actions = [
      "iam:*",
      "sts:AssumeRole",
      "s3:*",
      "dynamodb:*",
    ]
    resources = ["*"]
    effect    = "Deny"

    condition {
      test     = "Null"
      variable = "aws:MultiFactorAuthAge"
      values   = ["true"]
    }
  }
}
```

{% endcode %}

###

### Module 세팅

* Multi-account 세팅을 위해서 각 account마다 필요한 정책을 생성해야 합니다.
* 공통 리소스는 module을 통해서 생성합니다.&#x20;
  * `terraform/iam/art-id/_module_assume_policy` 폴더를 생성하여 정의

`terraform/iam/art-id/_module_assume_policy/main.tf` 를 확인합니다. **따로 수정하실 필요는 없습니다.**

{% code title=" terraform/iam/art-id/\_module\_assume\_policy/main.tf" %}

```hcl
resource "aws_iam_policy" "policy" {
  name        = "assume-${var.aws_account}-${var.subject}-policy"
  path        = "/"
  description = "${var.aws_account}-${var.subject}-policy"
  policy      = data.aws_iam_policy_document.policy-document.json
}

data "aws_iam_policy_document" "policy-document" {
  statement {
    effect = "Allow"

    resources = var.resources

    actions = [
      "sts:AssumeRole",
    ]
  }
}
```

{% endcode %}

###

### Production 계정 접근을 위한 Policy 생성

* Production 계정에 **Assume하기 위해서는 권한이 필요합니다.**
* id 계정에서 production계정의 role을 사용하기 위한 권한 설정이므로, 추후에 **production 계정에 해당 Role을 생성해야 합니다.( 본 설정만으로는 assume할 수 없습니다.)**

`terraform/iam/art-id/assume_policy_prod.tf` 를 수정합니다.

{% code title="terraform/iam/art-id/assume\_policy\_prod.tf" %}

```hcl
### Create policies for allowing user to assume the role in the production account
### You can copy this file and change `prod` to other environment if you have any other account

# Admin Access policy 
# If this policy is applied, then you will be able to assume role in the production account with admin permission
module "art_prod_admin" {
  source      = "./_module_assume_policy/"
  aws_account = "art-prod"
  subject     = "admin"
  resources   = ["arn:aws:iam::${var.prod_account_id}:role/assume-art-prod-admin"]
}

output "assume_art_prod_admin_policy_arn" {
  value = module.art_prod_admin.assume_policy_arn
}

# Poweruser Access policy 
# If this policy is applied, then you will be able to assume role in the production account with poweruser permission
module "art_prod_poweruser" {
  source      = "./_module_assume_policy/"
  aws_account = "art-prod"
  subject     = "poweruser"
  resources   = ["arn:aws:iam::${var.prod_account_id}:role/assume-art-prod-poweruser"]
}

output "assume_art_prod_poweruser_policy_arn" {
  value = module.art_prod_poweruser.assume_policy_arn
}


# ReadOnly Access policy 
# If this policy is applied, then you will be able to assume role in the production account with readonly permission
module "art_prod_readonly" {
  source      = "./_module_assume_policy/"
  aws_account = "art-prod"
  subject     = "readonly"
  resources   = ["arn:aws:iam::${var.prod_account_id}:role/assume-art-prod-readonly"]
}

output "assume_art_prod_readonly_policy_arn" {
  value = module.art_prod_readonly.assume_policy_arn
}
```

{% endcode %}

### 팀별로 그룹과 사용자 생성

* 편의상 아래와 같이 세팅했습니다.
  * **group\_devops\_black**
    * 모든 계정에서 `admin 권한`을 사용할 수 있는 사용자의 그룹
    * 예제 사용자 : `jupiter@devart.com`
  * **group\_devops\_white**
    * 모든 계정에서 `readonly 권한`을 사용할 수 있는 사용자의 그룹
    * 예제 사용자 : `daniel@devart.com`

### 사용자 생성

로그인 계정에 실제 로그인을 할 사용자를 생성합니다.&#x20;

{% code title="terraform/iam/art-id/user\_samples.tf" %}

```hcl
resource "aws_iam_user" "jupiter_devart_com" {
  name = "jupiter@devart.com"
}

resource "aws_iam_user" "daniel_devart_com" {
  name = "daniel@devart.com"
}

```

{% endcode %}

### 그룹 생성

사용자를 할당할 그룹을 생성합니다.&#x20;

#### Black Group&#x20;

{% code title="terraform/iam/art-id/group\_devops\_black.tf" %}

```hcl
############## art DevOps Group ##################
resource "aws_iam_group" "art_devops_black" {
  name = "art_devops_black"
}

resource "aws_iam_group_membership" "art_devops_black" {
  name = aws_iam_group.art_devops_black.name

  users = [
    aws_iam_user.jupiter_devart_com.name,
  ]

  group = aws_iam_group.art_devops_black.name
}

############### DevOps Basic Policy ##################
resource "aws_iam_group_policy" "art_devops_black" {
  name  = "art_devops_black"
  group = aws_iam_group.art_devops_black.id

  policy = data.aws_iam_policy_document.art_devops_black.json
}

data "aws_iam_policy_document" "art_devops_black" {
  statement {
    actions = [
      "*"
    ]

    resources = [
      "*"
    ]
  }
}

######################################################

########### DevOps Assume Policies ####################
resource "aws_iam_group_policy_attachment" "art_devops_black" {
  count      = length(var.assume_policy_art_devops_black)
  group      = aws_iam_group.art_devops_black.name
  policy_arn = var.assume_policy_art_devops_black[count.index]
}

variable "assume_policy_art_devops_black" {
  description = "IAM Policy to be attached to user"
  type        = list(string)

  default = [
    # Please change <account_id> to the real account id number of id account 
    "arn:aws:iam::<account_id>:policy/assume-art-prod-admin-policy", # Add admin policy to black group user 
  ]
}

#######################################################


############### MFA Manager ###########################
resource "aws_iam_group_policy_attachment" "art_devops_black_rotatekeys" {
  group      = aws_iam_group.art_devops_black.name
  policy_arn = aws_iam_policy.rotate_keys.arn
}

resource "aws_iam_group_policy_attachment" "art_devops_black_selfmanagemfa" {
  group      = aws_iam_group.art_devops_black.name
  policy_arn = aws_iam_policy.self_managed_mfa.arn
}

resource "aws_iam_group_policy_attachment" "art_devops_black_forcemfa" {
  group      = aws_iam_group.art_devops_black.name
  policy_arn = aws_iam_policy.force_mfa.arn
}

#######################################################


```

{% endcode %}

#### **White Group**&#x20;

```hcl
############## art DevOps Group ##################
resource "aws_iam_group" "art_devops_white" {
  name = "art_devops_white"
}

resource "aws_iam_group_membership" "art_devops_white" {
  name = aws_iam_group.art_devops_white.name

  users = [
    aws_iam_user.daniel_devart_com.name,
  ]

  group = aws_iam_group.art_devops_white.name
}

#######################################################

########### DevOps Assume Policies ####################
resource "aws_iam_group_policy_attachment" "art_devops_white" {
  count      = length(var.assume_policy_art_devops_white)
  group      = aws_iam_group.art_devops_white.name
  policy_arn = var.assume_policy_art_devops_white[count.index]
}

variable "assume_policy_art_devops_white" {
  description = "IAM Policy to be attached to user"
  type        = list(string)

  default = [
    # Please change <account_id> to the real account id number of id account 
    "arn:aws:iam::<account_id>:policy/assume-art-prod-readonly-policy", # Add readonly policy to while group user
  ]
}

#######################################################


############### MFA Manager ###########################
resource "aws_iam_group_policy_attachment" "art_devops_white_rotatekeys" {
  group      = aws_iam_group.art_devops_white.name
  policy_arn = aws_iam_policy.rotate_keys.arn
}

resource "aws_iam_group_policy_attachment" "art_devops_white_selfmanagemfa" {
  group      = aws_iam_group.art_devops_white.name
  policy_arn = aws_iam_policy.self_managed_mfa.arn
}

resource "aws_iam_group_policy_attachment" "art_devops_white_forcemfa" {
  group      = aws_iam_group.art_devops_white.name
  policy_arn = aws_iam_policy.force_mfa.arn
}

#######################################################


```

### 사용자 비밀번호 초기화

* 테라폼으로 생성한 사용자에게 초기 비밀번호를 세팅해줍니다.
* 세팅한 후에 해당 사용자에게 id와 비밀번호를 알려주고 패스워드 변경과 MFA 설정을 요청하시면 됩니다.

IAM > Users > 사용자 선택 > Security credentials 로 이동합니다.

<figure><img src="/files/rSk1eEfCK6NwDQJhe6f4" alt=""><figcaption></figcaption></figure>

`Enable console access` 버튼을 클릭하여 아래와 같이 세팅합니다.

<figure><img src="/files/MptVq4rm2zYYf92yLwzW" alt=""><figcaption></figcaption></figure>

`Enable console access` 를 진행한 후에, 해당 정보를 사용자에게 전달합니다.

<figure><img src="/files/47CPOsRV5Ioo6mvocvg4" alt=""><figcaption></figcaption></figure>


# 2. 사용자 패스워드 및 MFA 설정

신규 사용자 패스워드 및 MFA 설정

### 신규 사용자 계정 정보 수정

* 관리자로부터 계정을 발급받은 사용자는 패스워드와 MFA 설정을 진행합니다.
* **본 설정은 신규 사용자만 하시면 됩니다.**

### 패스워드 설정

로그인 페이지로 접속하고 `IAM user` 타입을 선택합니다.

`Account ID or account alias` 에 ID account의 alias를 입력합니다.

* `art-id` 값을 회사의 id 계정으로 변경합니다.

<figure><img src="/files/WnDDUZOpb2sowUXok1yx" alt=""><figcaption></figcaption></figure>

앞서 전달받은 `IAM username`과 `Password`를 입력하여 로그인합니다.

<figure><img src="/files/xTe8dgBspJ6y1uhdDPq0" alt=""><figcaption></figcaption></figure>

자신이 설정하고자 하는 패스워드를 입력합니다.

<figure><img src="/files/4CXWtOmonokCZY9I5aXk" alt=""><figcaption></figcaption></figure>

### MFA 설정

* **Device MFA를 진행하기 위해서는 Authy, Google Authenticator 과 같은 OTP 어플리케이션이 필요합니다.**
* 다른 MFA를 설정도 가능합니다.

IAM > Users > `자신의 계정 선택` > `Security credentials` 를 선택합니다.

Multi-factor authentication(MFA) 란에서 `Assign MFA device` 를 클릭합니다.

<figure><img src="/files/UmRwHqQJy5Zr1HGBLV16" alt=""><figcaption></figcaption></figure>

Device name에 자신의 계정 이름을 넣고, 원하는 디바이스를 선택합니다.

<figure><img src="/files/lOXch7UABhWmH1wFlxcj" alt=""><figcaption></figcaption></figure>

MFA 앱을 사용하여 MFA 코드를 발급받아 진행합니다.

<figure><img src="/files/3t8KW0dHgYwFJm3gUJCc" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/BzaFw1RF0xZgNwVHt28e" alt=""><figcaption></figcaption></figure>

### Access Key 발급

* 같은 페이지에서 `Access keys` 부분에 `Create access key` 를 클릭하여 새로운 access key를 발급받습니다.
* 발급 받은 `Access key ID`와 `Secret access key` 를 `~/.aws/credentials`에 입력합니다.

<figure><img src="/files/CODoipq1QJW4w5Ud81PM" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/3noiCFmacbsKoK279JKP" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/J6XW87qtgk2FslKhviBo" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/6DNkm20UR2ZeeC5pZ42P" alt=""><figcaption></figcaption></figure>

{% code title="vim \~/.aws/credentials" %}

```bash
[default]
aws_access_key_id = <Your Access Key ID>
aws_secret_access_key = <Your Secret Access Key>
```

{% endcode %}


# 3. 운영환경 계정 설정

### Production Account 세팅

* ID 계정에서 Assume할 역할을 생성합니다.
* **다른 계정이 추가로 있는 경우**에는 **본 가이드를 반복**하시면 됩니다.

작업을 위해서는 Account 생성에 필요한 **초기화 계정 반드시 필요**합니다. 아래 링크를 통해 초기화 사용자를 생성하시기 바랍니다.

### [초기화 IAM 사용자 생성](/0.-terraform-intermediate/iam)기

### backend와 provider 설정

`backend.tf` 파일에서 `id -> prod`로 변경합니다.

```hcl
terraform {
  required_version = ">= 1.0.0" # Terraform Version

  backend "s3" {
    bucket         = "art-prod-apnortheast2-tfstate" # Set bucket name 
    key            = "art/terraform/iam/art-prod/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-lock" # Set DynamoDB Table
  }
}

```

```hcl
provider "aws" {
  region  = "us-east-1"
}
```

###

### Assume Role 생성

* `id account(dayone-id)` 에서 Assume할 역할을 생성합니다.
* ID에서는 admin과 readonly를 생성했으니, 이에 맞는 역할을 생성합니다.

아래 두 개의 파일을 수정합니다. &#x20;

* `terraform/iam/art-prod/assume-art-prod-admin-with-art-id.tf`&#x20;
* `terraform/iam/art-prod/assume-art-prod-readonly-with-art-id.tf`&#x20;

{% code title="vim assume-art-prod-admin-with-art-id.tf" %}

```hcl
#
# art-prod administrator
#
resource "aws_iam_role" "assume_art_prod_admin" {
  name                 = "assume-art-prod-admin"
  path                 = "/"
  max_session_duration = "43200"
  assume_role_policy   = data.aws_iam_policy_document.assume_art_prod_admin_assume_role.json
}

data "aws_iam_policy_document" "assume_art_prod_admin_assume_role" {
  statement {
    actions = ["sts:AssumeRole"]
    effect  = "Allow"

    principals {
      type        = "AWS"
      identifiers = ["arn:aws:iam::${var.id_account_id}:root"]
    }
  }
}

resource "aws_iam_role_policy" "assume_art_prod_admin_passrole" {
  name = "assume-art-prod-admin-passrole"
  role = aws_iam_role.assume_art_prod_admin.id

  policy = data.aws_iam_policy_document.assume_art_prod_admin_pass_role.json
}

data "aws_iam_policy_document" "assume_art_prod_admin_pass_role" {
    statement {
        actions = ["iam:PassRole"]
        effect  = "Allow"

        resources = ["*"]
    }
}

resource "aws_iam_role_policy_attachment" "assume_art_prod_admin" {
  role       = aws_iam_role.assume_art_prod_admin.id
  policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
}



```

{% endcode %}

{% code title="vim assume-art-prod-readonly-with-art-id.tf" %}

```hcl
#
# art-prod readonly
#
resource "aws_iam_role" "assume_art_prod_readonly" {
  name                 = "assume-art-prod-readonly"
  path                 = "/"
  max_session_duration = "43200"
  assume_role_policy   = data.aws_iam_policy_document.assume_art_prod_readonly_assume_role.json
}

data "aws_iam_policy_document" "assume_art_prod_readonly_assume_role" {
  statement {
    actions = ["sts:AssumeRole"]
    effect  = "Allow"

    principals {
      type        = "AWS"
      identifiers = ["arn:aws:iam::${var.id_account_id}:root"]
    }
  }
}

resource "aws_iam_role_policy" "assume_art_prod_readonly_passrole" {
  name = "assume-art-prod-readonly-passrole"
  role = aws_iam_role.assume_art_prod_readonly.id

  policy = data.aws_iam_policy_document.assume_art_prod_readonly_pass_role.json
}

data "aws_iam_policy_document" "assume_art_prod_readonly_pass_role" {
  statement {
    actions = ["iam:PassRole"]
    effect  = "Allow"

    resources = ["*"]
  }
}

resource "aws_iam_role_policy_attachment" "assume_art_prod_readonly" {
  role       = aws_iam_role.assume_art_prod_readonly.id
  policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}



```

{% endcode %}

### Variables 정의 및 세팅

* `variables.tf` 에 필요한 변수를 정의합니다.

```hcl
variable "aws_region" {
  description = "The AWS region to deploy the shard storage layer into"
}

variable "id_account_id" {
  description = "The AWS account number of ID account"
}
```

* `terraform.tfvars` 에 각 변수에 대한 값을 입력합니다.

```hcl
aws_region = "us-east-1"
id_account_id = "<account_number_of_id>" #12-digit Number of ID account
```


# 4. Assume Role 세팅

Console에서 Assume Role 사용하기

### AWS Console 세팅

* ID 계정에서 다른 계정으로 Assume하는 방법에 관한 가이드입니다.
* 실습을 위해서 id 계정으로 로그인 하시기 바랍니다.

오른쪽 위 Account 이름을 선택 > `Switch Role` 클릭&#x20;

<figure><img src="/files/ffqjNQoOc6TmbONpX6X3" alt=""><figcaption></figcaption></figure>

`Switch Role`을 클릭하고, 아래와 같이 정보를 입력합니다.

* `Account`: Assume할 대상 계정의 alias
* `Role` : 자신이 Assume할 수 있는 대상 계정의 역할 이름
  * 관리자가 부여해준 권한에 따라 역할이 다를 수 있습니다.
  * 예시
    * Admin 권한 : `assume-art-prod-admin`
    * Readonly 권한 : `assume-art-prod-readonly`
* `Display Name` : 계정 구분을 위한 이름 (마음대로 정하시면 됩니다.)

<figure><img src="/files/ybTNWaqWo4XfeBeOTYn1" alt=""><figcaption></figcaption></figure>

`Switch Role` 을 클릭하시면 해당 계정으로 로그인을 하실 수 있습니다.

**앞으로는 모든 계정에 접근할 때 위와 같은 방식으로 접근하시면 됩니다.**

* 참고로,  Assume 기능을 편리하게 사용하기 위해 [Chrome extension](https://chrome.google.com/webstore/detail/aws-extend-switch-roles/jpmkfafbacpgapdghgdpembnojdlgkdl)을 사용하실 수 있습니다.


# 3. 어플리케이션 서비스 만들기


# 보편적인 Application 구조

보편적인 Application 구조에 대해서 알아봅니다.

## Application 의 일반적인 구조 ( a.k.a 3 tier architecture )

![](/files/-MPVVGQxPitazEhmzb8g)


# AWS ALB and NLB 생성하기

## Load Balancer 생성

Load Balancer는 여러 인스턴스에 트래픽을 분배해주기 위해서 반드시 필요한 리소스입니다. EC2 위에 올라가 있는 대부분의 서비스는 LB를 앞 단에 두는 것이 보통인데, LB의 경우에는 설정할 것이 상당히 많아 코드로 한 번 정리해놓으면 편리하게 생성/관리하실 수 있습니다.

Load Balancer는 NLB/ALB 선택해서 생성하시면 됩니다.

* **ALB (Application Load Balancer)**: Layer 7 로드밸런서로,  Security Group으로 접근제어가 가능하며, URI 별로 라우팅을 할 수 있습니다.
* **NLB (Network Load Balancer)**: Layer 4 로드밸런서로, ALB보다 전송 속도는 빠르지만 Security Group으로 과거에는 접근제어가 되지 않았지만, 현재는 가능합니다.

실습에서 생성할 리소스는 아래와 같습니다.

* Load Balancer
* Target Group
  * https(443) : redirect to instance
  * http(80) : redirect to 443
* Route53 Record
  * A record for ELB

### Module 구성

보통 서비스를 위한 리소스는 환경별로 동일하게 생성하는 경우가 많습니다. 따라서 같은 코드를 변수만 바꿔서 사용할 수 있도록 Module을 이용하여 생성합니다.

{% hint style="info" %}
Module을 사용할 때는 최대한 고유 명사를 없애고 변수처리하는 것이 좋습니다. 고유 명사를 변수로 대체하면 이후에 다른 서비스를&#x20;
{% endhint %}

`_module` 폴더의 구조는 아래와 같습니다.&#x20;

* **outputs.tf** : 모듈내에서의 output.
* **service.tf** : ALB 및 SG 와 관련된 리소스가 포함된 파일
* **var\_lb.tf** : Load Balancer 관련 변수 정의 파일
* **var\_sg.tf** : Security Group 관련 변수 정의 파일
* **variables.tf** : 기타 변수 정의 파일

```
$ tree
.
└── hello
    ├── outputs.tf
    ├── alb.tf
    ├── var_lb.tf
    ├── var_sg.tf
    └── variables.tf 
```

### ALB 리소스 생성

ALB 생성을 위한 리소스는 `terraform/services/hello/_module/hello/alb.tf` 에 있습니다. ALB 생성을 위해서는 일단 LB에 붙일 Security Group(보안 그룹)과 Load Balancer, 그리고 Target group입니다.

{% code title="vim terraform/services/hello/\_module/hello/alb.tf" %}

```hcl
############ Security Group For External LB
resource "aws_security_group" "external_lb" {
  name        = "${var.service_name}-${var.vpc_name}-ext"
  description = "${var.service_name} external LB SG"
  vpc_id      = var.target_vpc

  # Only allow access from IPs or SGs you specifiy in ext_lb_ingress_cidrs variables
  # If you don't want to use HTTPS then remove this block
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = var.ext_lb_ingress_cidrs
    description = "External service https port"
  }


  # Allow 80 port
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = var.ext_lb_ingress_cidrs
    description = "External service http port"
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["10.0.0.0/8"]
    description = "Internal outbound any traffic"
  }

  tags = var.sg_variables.external_lb.tags[var.shard_id]
}

#################### External ALB
resource "aws_lb" "external" {
  name     = "${var.service_name}-${var.shard_id}-ext"
  subnets  = var.public_subnets
  internal = false

  # For external LB,
  # Home SG (Includes Office IPs) could be added if this service is internal service.
  security_groups = [
    aws_security_group.external_lb.id,
  ]

  # For HTTP service, application LB is recommended.
  # You could use other load_balancer_type if you want.
  load_balancer_type = "application"

  tags = var.lb_variables.external_lb.tags[var.shard_id]
}

#################### External LB Target Group
resource "aws_lb_target_group" "external" {
  name                 = "${var.service_name}-${var.shard_id}-ext"
  port                 = var.service_port
  protocol             = "HTTP"
  vpc_id               = var.target_vpc
  slow_start           = var.lb_variables.target_group_slow_start[var.shard_id]
  deregistration_delay = var.lb_variables.target_group_deregistration_delay[var.shard_id]

  # Change the health check setting
  health_check {
    interval            = 15
    port                = var.healthcheck_port
    path                = "/"
    timeout             = 3
    healthy_threshold   = 3
    unhealthy_threshold = 2
    matcher             = "200"
  }

  tags = var.lb_variables.external_lb_tg.tags[var.shard_id]
}


#################### Listener for HTTP service
resource "aws_lb_listener" "internal_80" {
  load_balancer_arn = aws_lb.external.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    target_group_arn = aws_lb_target_group.external.arn
    type             = "forward"
  }
}
```

{% endcode %}

### ALB 관련 태그 및 설정

위 리소스를 생성하기 위해서는 기본적인 설정과 태그가 필요합니다.&#x20;

```hcl
# terraform/application/hello/_module/hello/var_lb.tf
variable "lb_variables" {
  default = {

    target_group_slow_start = {}

    target_group_deregistration_delay = {}

    internal_lb = {
      tags = {}
    }

    internal_lb_tg = {
      tags = {}
    }

    external_lb = {
      tags = {}
    }

    external_lb_tg = {
      tags = {}
    }
  }
}
```

실제 설정값은 아래와 같습니다.

```bash
# terraform/application/hello/devartd_apnortheast2/var_lb.tf
variable "lb_variables" {
  default = {

    target_group_slow_start = {
      devartdapne2   = 0
      artdapne2   = 0
      artpapne2   = 0
    }

    target_group_deregistration_delay = {
      devartdapne2   = 0
      artdapne2    = 60
      artpapne2    = 60
    }

    external_lb = {
      tags = {
        devartdapne2 = {
          Name    = "hello-devartd_apnortheast2-external-lb"
          app     = "hello"
          project = "hello"
          env     = "dev"
          stack   = "devartd_apnortheast2"
        },      
      
        artdapne2 = {
          Name    = "hello-artd_apnortheast2-external-lb"
          app     = "hello"
          project = "hello"
          env     = "dev"
          stack   = "artd_apnortheast2"
        },

        artpapne2 = {
          Name    = "hello-artp_apnortheast2-external-lb"
          app     = "hello"
          project = "hello"
          env     = "prod"
          stack   = "artp_apnortheast2"
        }
      }
    }

    external_lb_tg = {
      tags = {
      
        devartdapne2 = {
          Name    = "hello-devartd_apnortheast2-external-tg"
          app     = "hello"
          project = "hello"
          env     = "dev"
          stack   = "devartd_apnortheast2"
        },
        
        artdapne2 = {
          Name    = "hello-artd_apnortheast2-external-tg"
          app     = "hello"
          project = "hello"
          env     = "dev"
          stack   = "artd_apnortheast2"
        },

        artpapne2 = {
          Name    = "hello-artp_apnortheast2-external-tg"
          app     = "hello"
          project = "hello"
          env     = "prod"
          stack   = "artp_apnortheast2"
        }
      }
    }
  }
}
```


# Domain 등록하기

Route53에 새로운 도메인 등록하기

## 도메인 구매

도메인 구매는 AWS Route 53에서 손쉽게 구매하실 수 있습니다. 외부에서 구매한 도메인을 사용하셔도 무방합니다.

본 실습에서는 `devops-art-factory.com` 라는 도메인을 구입했다는 가정하에 정의하겠습니다.&#x20;

### Host zone 등록

코드 경로는 `terraform/route53/art-id/devops-art-factory.com` 입니다. **만약 도메인을 AWS 상에서 구매하셨다면 기본 host zone을 AWS에서 자동으로 생성해줍니다. 이 경우에는 해당 리소스를** [**import**](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_zone#import) **하시는 것이 좋습니다.**

{% code title="vim terraform/route53/art-id/devops-art-factory.com/route53.tf" %}

```hcl
# AWS Route53 Zone
resource "aws_route53_zone" "devopsartfactory_com" {
  name     = "devops-art-factory.com"
  comment  = "HostedZone created by Route53 Registrar - Manged Terraform"
}
```

{% endcode %}

## 서로 다른 Account에서 subdomain 등록하기

서비스를 운영하다보면, 환경별로 서로 다른 계정에서 도메인을 함께 쓰고 싶은 경우가 있습니다. 예를 들어 A 계정에서 example.com을 사용하고 있는데, B라는 계정에서 prod.example.com 도메인을 사용하는 것입니다. 이 경우에는 서브 도메인의 네임서버 리스트를 상위 도메인에 등록하면 됩니다.

정리하면, 과정은 아래와 같습니다.

1. &#x20;B 계정에 `prod.example.com` 이라는 aws route53 zone을 생성합니다.
2. &#x20;A 계정에서 `example.com` route53 zone에 NS 레코드로 prod.example.com 의 네임서버들을 등록합니다.

1번 과정 코드는 아래와 같습니다.

{% code title="vim terraform/route53/art-prod/prod.devops-art-factory.com/route53.tf" %}

```hcl
resource "aws_route53_zone" "prod_devops-art-factory_com" {
  name = "prod.devops-art-factory.com."
}
```

{% endcode %}

생성이 완료되면 아래 그림처럼 hosted zone이 생성되고, 레코드로 Name server들이 생성됩니다.

![](/files/-MZcCJiO-nmHPsPOHfdY)

2번 작업은 아래와 같습니다.

1번에서 생성된 name server list를 코드 `records`에 넣어주시면 됩니다.

{% code title="vim terraform/route53/art-id/devops-art-factory.com/route53.tf" %}

```hcl
resource "aws_route53_record" "ns_prod_devops-art-factory_com" {
  zone_id = aws_route53_zone.devopsartfactory_com.zone_id
  name    = "prod"
  type    = "NS"
  ttl     = "300"
  records = [
    # Refer to Route53 Zone for `prod.devops-art-factory.com in `art-id`
    "ns-316.awsdns-39.com.",
    "ns-1936.awsdns-50.co.uk.", 
    "ns-982.awsdns-58.net.",
    "ns-1191.awsdns-20.org."
  ]
}
```

{% endcode %}

이렇게 등록이 마무리 되면,  `*.prod.example.com` 형태로 ACM도 생성할 수 있고, sub domain을 추가로 만들어서 사용하실 수 있습니다.&#x20;

작동 원리는 DNS 질의 원리와 동일합니다.

1\) `prod.example.com` 을 질의하게 되면 `.com` 도메인에게 `example.com` 의 Name Server IP를 물어봅니다.

2\)  A 계정에 있는 `example.com` 네임서버가 이를 받아서 위에서 등록한 `prod.example.com` 네임서버 리스트 중에 하나를 반납해줍니다.&#x20;

3\) B 계정에 있는 `prod.example.com` 네임서버는 질의 받은 레코드에 대한 값을 반환합니다.


# Route53 생성하기

## Route53 생성

* Route53은 DNS 레코드를 생성/관리하는 서비스입니다.
* 원하시는 도메인을 직접 사실 수도 있고, 외부의 도메인을 route53에 import하실 수도 있습니다
* ALB의 경우 자체 도메인이 있지만 도메인 이름이 길고 복잡하기 때문에 특정 도메인으로 연결하는 것이 보기 좋습니다.
* 또한, ALB의 도메인은 SSL이 붙어 있지 않아 보안에 취약하기 때문에, Custom 도메인을 연결하여 SSL 설정하시는 것이 좋습니다.

Route53 리소스는 아래와 같습니다.

* `zone_id` 는 등록하는 도메인의 zone ID를 의미합니다.
* `type` 은 A 레코드(IPv4)를 사용합니다. (IPv6를 원하시는 경우에는 AAAA 타입을 사용하시면 됩니다)
* `alias` 는 AWS 서비스를 직접 연결할 수 있는 기능을 의미합니다. 여기서는 ALB의 도메인을 연결을 합니다.

```hcl
##################### Route53 Record
resource "aws_route53_record" "external_dns" {
  zone_id        = var.route53_external_zone_id
  name           = var.domain_name
  type           = "A"
  set_identifier = var.aws_region

  latency_routing_policy {
    region = var.aws_region
  }

  alias {
    name                   = aws_lb.external.dns_name
    zone_id                = aws_lb.external.zone_id
    evaluate_target_health = true
  }
}
```

실제로 plan을 돌려보시면 아래와 같이 A 레코드가 생성된다고 나옵니다.

```hcl
  # module.hello.aws_route53_record.external_dns will be created
  + resource "aws_route53_record" "external_dns" {
      + allow_overwrite = (known after apply)
      + fqdn            = (known after apply)
      + id              = (known after apply)
      + name            = "hello"
      + set_identifier  = "ap-northeast-2"
      + type            = "A"
      + zone_id         = "Z03407373NYH46ZMHFM7O"

      + alias {
          + evaluate_target_health = true
          + name                   = "hello-artdapne2-ext-1008005566.ap-northeast-2.elb.amazonaws.com"
          + zone_id                = "ZWKZPGTI48KDX"
        }

      + latency_routing_policy {
          + region = "ap-northeast-2"
        }
    }
```


# ACM을 이용한 HTTPS 설정

## ACM 생성 (via 콘솔)

* ACM은 AWS에서 무료로 제공해주는 SSL Certificate 인증서 솔루션입니다. 본인이 가지고 있는 도메인에 대해서는 Certificate를 자유롭게 생성하실 수 있습니다.
* ACM도 테라폼으로 생성해도 되지만, 인증서의 경우에는 자동 Renewal 되기도 하고, 한 번 만들면 수정하는 경우가 거의 없기 때문에 콘솔에서 생성하셔도 무방합니다.

1. AWS Certificate Manager -> Request 클릭

<figure><img src="/files/7gTyyfreR50jBLxF2ETo" alt=""><figcaption></figcaption></figure>

2. Next 클릭

<figure><img src="/files/HE2ZLlpnkuHNOR0TZdvr" alt=""><figcaption></figcaption></figure>

3. 사용하고자 하는 도메인을 입력

   1. 만약 subdomain 모두 같은 인증서를 사용하고 싶다면, \* 을 붙이시면 됩니다.
   2. 인증 방식은 DNS validation 을 사용합니다. 스스로 도메인을 가지고 있기 때문에, Route53에 자동으로 도메인 레코드가 등록되므로 인증이 훨씬 간편합니다.

   <figure><img src="/files/pKa6lROOXJirwUTBvVSw" alt=""><figcaption></figcaption></figure>
4. 상태가 `Issued` 가 될때까지 기다립니다.

<figure><img src="/files/PTDJoQ9JY4kZx6k9lqua" alt=""><figcaption></figcaption></figure>

## ACM 적용

이제 만들어놓은 ACM을 적용해보겠습니다.

`service.tf` 파일에 아래와 같이 값을 넣습니다.

* `acm_external_ssl_certificate_arn` : 위에서 생성한 ACM의 ARN
* 위 변수명은 원하시는대로 변경하셔도 됩니다. 단, 변경하신 이름을 반드시 `_modules/hello/variables.tf` 에 등록해주셔야 합니다.

```bash
  # Route53 variables
  acm_external_ssl_certificate_arn  = "arn:aws:acm:ap-northeast-2:816736805842:certificate/9d4a371f-80c5-4087-9cb5-b2636f554da7"
```

해당 변수는 모듈에서 사용하기 때문에 모듈에 같은 이름의 variable을 정의해주셔야 합니다.

```hcl
variable "acm_external_ssl_certificate_arn" {
  description = "ssl cert id"
  default     = ""
}
```

이후에 해당 SSL을 등록하기 위해서 443번 포트로 요청을 받을 타켓 그룹을 생성하고, 해당 타켓 그룹에 위 ACM을 등록합니다.

```hcl
#################### Listener for HTTPS service
resource "aws_lb_listener" "external_443" {
  load_balancer_arn = aws_lb.external.arn
  port              = "443"
  protocol          = "HTTPS"

  # If you want to use HTTPS, then you need to add certificate_arn here.
  certificate_arn   = var.acm_external_ssl_certificate_arn

  default_action {
    target_group_arn = aws_lb_target_group.external.arn
    type             = "forward"
  }
}
```


# EC2 ASG 생성하기

AWS Auto Scaling Group은 Amazon Web Services(AWS)에서 제공하는 서비스로, 클라우드 인프라 내의 리소스를 자동으로 확장하거나 축소할 수 있게 해주는 기능입니다. 사용자의 애플리케이션이 겪는 트래픽의 변화에 따라 필요한 컴퓨팅 리소스를 동적으로 조정하여, 최적의 성능을 유지하면서 비용을 효율적으로 관리할 수 있습니다.

Auto Scaling Group의 주요 기능은 다음과 같습니다:

* **Elastic**: 트래픽이 증가할 때 자동으로 EC2 인스턴스를 추가하여 처리 능력을 증가시키고, 트래픽이 감소할 때는 인스턴스를 자동으로 줄여 비용을 절약할 수 있습니다.
* **High Availability**: 여러 가용 영역(Availability Zones)에 걸쳐 인스턴스를 분산시켜, 하나의 영역에 문제가 생겼을 때도 애플리케이션이 계속 작동할 수 있게 합니다.
* **Load balancing**: Auto Scaling Group은 AWS Elastic Load Balancing(ELB)과 통합되어, 인스턴스 간에 들어오는 트래픽을 자동으로 분산시킬 수 있습니다.

\
service.tf

```bash
##### ASG

resource "aws_launch_template" "lt" {
  name_prefix   = "${var.service_name}-${var.vpc_name}-LT"
  image_id      = var.image_id
  instance_type = var.instance_type
  key_name      = var.key_name

  vpc_security_group_ids = [aws_security_group.ec2.id]

  tag_specifications {
    resource_type = "instance"
    tags = {
      Name = "${var.service_name}-${var.vpc_name}-LaunchTemplate"
    }
  }
}

resource "aws_autoscaling_group" "asg" {
  launch_template {
    id      = aws_launch_template.lt.id
    version = "$Latest"

  }

  min_size         = var.min_size
  max_size         = var.max_size
  desired_capacity = var.desired_capacity
  vpc_zone_identifier = var.private_subnets

  target_group_arns = [aws_lb_target_group.internal.arn]

  tag {
    key                 = "Name"
    value               = "${var.service_name}-${var.vpc_name}-asg"
    
    propagate_at_launch = true
  }
}


```

```bash
### variables.tf

variable "image_id" {
  description = "AMI ID for instance"
  type        = string
  default     = "ami-0032724dd60a24c31"
}

variable "instance_type" {
  description = "EC2 Instance type"
  type        = string
  default     = "t3.small"
}

variable "key_name" {
  description = "EC2 key-pair"
  type        = string
}

variable "min_size" {
  description = "Auto Scaling min size"
  type        = number
  default     = 1
}

variable "max_size" {
  description = "Auto Scaling max size"
  type        = number
  default     = 1
}

variable "desired_capacity" {
  description = "Auto Scaling desired capacity"
  type        = number
  default     = 1
}

```


# bastion server 만들기 ( EC2 ssh )

https\://youtu.be/oMVfhT\_xlzk?feature=shared


# 4. Static 페이지 만들기


# Cloudfront + S3를 사용하는 이유

왜 Cloudfront + S3를 사용하는가

장점

1. **확장성(Scalability)**:
   * **S3 (Simple Storage Service)**: S3는 매우 높은 확장성을 제공하여, 트래픽이 급격히 증가하더라도 문제없이 대응할 수 있습니다.
   * **CloudFront**: CloudFront는 글로벌 콘텐츠 배포 네트워크(CDN)로, 전 세계의 사용자에게 빠르게 콘텐츠를 전달합니다. 이는 서버 부하를 줄이고 사용자 경험을 향상시킵니다.
2. **성능(Performance)**:
   * **CloudFront**: CloudFront를 사용하면 캐싱을 통해 콘텐츠를 사용자의 가까운 위치에서 제공할 수 있어 웹 페이지 로딩 속도가 빨라집니다.
   * **S3**: S3는 고속 데이터 전송을 지원하여, CloudFront와 함께 사용하면 성능이 최적화됩니다.
3. **비용 효율성(Cost Efficiency)**:
   * **S3**: S3는 저장 용량에 따라 비용을 지불하며, 데이터 전송 비용도 비교적 저렴합니다.
   * **CloudFront**: CloudFront는 사용량 기반으로 과금되며, 전 세계적으로 콘텐츠를 배포할 때 매우 경제적입니다.
4. **보안(Security)**:
   * **S3**: S3는 버킷 정책, IAM 정책, ACL(Access Control Lists)을 통해 세부적인 접근 제어를 설정할 수 있습니다.
   * **CloudFront**: CloudFront는 AWS WAF(Web Application Firewall)과 통합하여 DDoS 공격을 방어할 수 있으며, SSL/TLS 암호화를 통해 데이터 전송의 보안을 강화합니다.
5. **간편한 설정 및 관리(Ease of Setup and Management)**:
   * AWS Management Console을 통해 S3 버킷 생성 및 관리가 매우 직관적이며, CloudFront 배포 설정도 쉽게 할 수 있습니다.

#### 단점

1. **복잡한 설정(Complex Configuration)**:
   * 처음 설정할 때 S3 버킷과 CloudFront 배포를 구성하는 과정이 다소 복잡할 수 있습니다. 특히, 보안 설정과 캐싱 전략을 제대로 설정하지 않으면 의도치 않은 문제를 겪을 수 있습니다.
2. **비용 관리(Cost Management)**:
   * 사용량이 증가함에 따라 비용이 예기치 않게 증가할 수 있습니다. 이를 모니터링하고 관리하지 않으면 비용이 급증할 위험이 있습니다.
3. **의존성(Dependency)**:
   * AWS 인프라에 크게 의존하게 되며, AWS 서비스 중단 시 영향을 받을 수 있습니다. 다중 클라우드 전략을 사용하지 않으면 이러한 리스크에 취약합니다.
4. **학습 곡선(Learning Curve)**:
   * AWS 서비스에 익숙하지 않은 사용자는 S3와 CloudFront의 개념과 사용 방법을 배우는 데 시간이 걸릴 수 있습니다.

#### 결론

CloudFront와 S3를 결합하여 웹 페이지를 호스팅하는 것은 확장성, 성능, 비용 효율성 및 보안 측면에서 많은 장점을 제공합니다. 하지만 설정의 복잡성, 비용 관리의 필요성, AWS 의존성, 그리고 학습 곡선 등의 단점도 고려해야 합니다. 웹 애플리케이션의 요구 사항과 사용 패턴에 따라 적절히 활용하는 것이 중요합니다.


# Cloudfront 와 S3 구성하기

Cloudfront와 S3를 활용하여 static한 페이지 구성하기

## AWS Cloudfront + S3

보통 정적 컨텐츠를 저장할 목적으로 S3를 많이 활용합니다. 이렇게 저장한 파일은 S3 자체에서 제공할수도 있지만, 비용 절감과 속도 향상을 위해서 CDN(Contents Delivery Network)을 사용합니다. AWS에서는 Cloudfront라는 대표적인 서비스를 제공하는데, 본 실습에서는 이 Cloudfront와 S3를 연동하여 한 번에 생성해보도록 하겠습니다.

### Backend 구성

{% code title="terraform/s3/art-id/id\_apnortheast2/backend.tf" %}

```hcl
terraform {
  required_version = ">= 1.0.0"

  backend "s3" {
    bucket         = "art-id-apnortheast2-tfstate"
    key            = "art/terraform/s3/art-id/id_apnortheast2/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-lock"
  }
}

```

{% endcode %}

### 도메인을 보유하지 않은 경우

* CloudFront의 경우, 자신이 원하는 도메인을 설정할 수 있습니다.
* 하지만, 별도로 도메인을 보유하지 않은 경우에도, AWS에서 제공해주는 기본 도메인을 사용할 수 있습니다.

이번에는 도메인 없이 CloudFront를 생성하는 방법을 알아보겠습니다.

{% code title="terraform/s3/art-id/id\_apnortheast2/contents-devart\_without\_domain.tf" %}

```hcl
# S3 Bucket for storing contents
resource "aws_s3_bucket" "contents_devart" {
  bucket = "${var.account_namespace}-contents-${var.shard_id}"
}

resource "aws_s3_bucket_public_access_block" "contents_devart" {
  bucket = aws_s3_bucket.contents_devart.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_cors_configuration" "contents_devart" {
  bucket = aws_s3_bucket.contents_devart.id

  cors_rule {
    allowed_headers = ["*"]
    allowed_methods = ["GET", "HEAD"]
    allowed_origins = ["*"]
    expose_headers  = ["ETag"]
    max_age_seconds = 3000
  }
}

resource "aws_s3_bucket_versioning" "contents_devart" {
  bucket = aws_s3_bucket.contents_devart.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_accelerate_configuration" "contents_devart" {
  bucket = aws_s3_bucket.contents_devart.id
  status = "Enabled"
}

resource "aws_s3_bucket_policy" "contents_devart" {
  bucket = aws_s3_bucket.contents_devart.id
  policy = data.aws_iam_policy_document.contents_devart.json
}

data "aws_iam_policy_document" "contents_devart" {
  statement {
    principals {
      type        = "Service"
      identifiers = ["cloudfront.amazonaws.com"]
    }

    condition {
      test     = "StringEquals"
      variable = "AWS:SourceArn"
      values = [
        aws_cloudfront_distribution.devart_cdn_distribution.arn,
      ]
    }

    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.contents_devart.arn}/*"]
  }
}

resource "aws_s3_bucket_lifecycle_configuration" "contents_devart" {
  bucket = aws_s3_bucket.contents_devart.id

  rule {
    id     = "contents_devart_rule"
    status = "Enabled"

    transition {
      days          = 30
      storage_class = "STANDARD_IA"
    }
  }
}

resource "aws_cloudfront_origin_access_control" "devart_contents" {
  name                              = "devart-contents"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

# Cloudfront Distribution
resource "aws_cloudfront_distribution" "devart_cdn_distribution" {
  origin {
    domain_name              = aws_s3_bucket.contents_devart.bucket_regional_domain_name
    origin_id                = "devart_origin"
    origin_access_control_id = aws_cloudfront_origin_access_control.devart_contents.id
  }

  enabled         = true
  is_ipv6_enabled = true
  comment         = "Cloudfront configuration for cdn"
  http_version    = "http2and3"

  # Default Cache behavior 
  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD", "OPTIONS"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "devart_origin"
    compress         = true

    forwarded_values {
      query_string = false

      cookies {
        forward = "all"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }

  # List of Custom Cache behavior
  # This behavior will be applied before default
  ordered_cache_behavior {

    path_pattern = "*.gif"

    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "devart_origin"
    compress         = false

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 3600

    forwarded_values {
      query_string            = true
      query_string_cache_keys = ["d"]

      cookies {
        forward = "all"
      }
    }
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  # You can set custom error response 
  custom_error_response {
    error_caching_min_ttl = 5
    error_code            = 404
    response_code         = 404
    response_page_path    = "/404.html"
  }

  custom_error_response {
    error_caching_min_ttl = 5
    error_code            = 500
    response_code         = 500
    response_page_path    = "/500.html"
  }

  custom_error_response {
    error_caching_min_ttl = 5
    error_code            = 502
    response_code         = 502
    response_page_path    = "/500.html"
  }

  # Tags of cloudfront
  tags = {
    Name = "cdn-contents without domain"
  }
}


```

{% endcode %}

### 생성된 CloudFront 테스트

* S3 버킷에 다음과 같은 샘플 html 파일을 올려서 테스트합니다.

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>DevOpsArt 소개</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f0f0f0;
        }
        .container {
            max-width: 800px;
            margin: auto;
            background: white;
            padding: 20px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        }
        h1 {
            text-align: center;
        }
        .video-container {
            text-align: center;
            margin-top: 20px;
        }
        iframe {
            width: 100%;
            height: 450px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>DevOpsArt 소개</h1>
        <p>DevOpsArt는 DevOps와 관련된 최신 기술과 트렌드를 다루는 전문적인 블로그 및 커뮤니티입니다. 우리는 DevOps의 개념, 도구, 그리고 최적의 실천 방법을 공유하며, 엔지니어들이 효과적이고 효율적으로 업무를 수행할 수 있도록 돕습니다.</p>
        <p>DevOpsArt의 목표는 DevOps 문화를 전파하고, 이를 통해 조직의 개발 속도와 품질을 높이는 것입니다. 우리는 다양한 튜토리얼, 가이드, 그리고 사례 연구를 제공하여, 모든 수준의 기술자들이 DevOps에 대해 이해하고 활용할 수 있도록 지원합니다.</p>
        <div class="video-container">
            <h2>유튜브 채널</h2>
            <iframe src="https://www.youtube.com/embed/vOhNCakx3kk" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
        </div>
    </div>
</body>
</html>
```

위 코드를 `sample.html` 로 저장한 후, 본인이 만든 버킷에 업로드 합니다.

업로드가 완료되면, 앞서 생성한 CloudFront 도메인을 통해 접속하실 수 있습니다. 도메인 뒤 경로에 파일명을 넣으시면 접속 가능합니다.

<figure><img src="/files/NPElegvsn0Ozc4kKVmtC" alt=""><figcaption></figcaption></figure>

```
https://dafbqt2yr09tj.cloudfront.net/sample.html
```

<figure><img src="/files/dKprWn9HMUyR6ZkE076p" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/fdJa1SetdjslBtshdwrD" alt=""><figcaption></figcaption></figure>


# (Optional)Custom 도메인 사용하기

내가 원하는 도메인을 구매해서 사용하기

{% hint style="info" %}
본 실습은 도메인을 이미 보유하고 있는 경우에만 가능하니, 도메인을 구매하지 않으신 분들은 참고만 하시면 됩니다!
{% endhint %}

도메인이 AWS Route53에 등록되어 있다면 다음과 같은 절차를 통해서 ACM(AWS Certificate Manager)를 생성하실 수 있습니다.

* CloudFront는 전세계에 배포되는 서비스이기 때문에,  ACM을 생성할 때 반드시 `us-east-1` 리전을 사용하셔야 합니다!

### AWS Certificate Manager 생성하기

[ACM을 이용한 HTTPS 설정](/2./acm-https)

이제 코드를 다음과 같이 수정합니다!

* CloudFront 코드 중 alias 에 사용할 도메인을 입력합니다.&#x20;
  * ex) `cdn.devops-art-factory.com`
* 위와 동일한 도메인을 aws\_route53\_record 에도 등록합니다.
* Route53 Zone ID를 입력합니다.

{% code title="terraform/s3/art-id/id\_apnortheast2/contents-devart\_with\_domain.tf" %}

```hcl
# S3 Bucket for storing contents
resource "aws_s3_bucket" "contents_devart" {
  bucket = "${var.account_namespace}-contents-${var.shard_id}"
}

resource "aws_s3_bucket_public_access_block" "contents_devart" {
  bucket = aws_s3_bucket.contents_devart.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_cors_configuration" "contents_devart" {
  bucket = aws_s3_bucket.contents_devart.id

  cors_rule {
    allowed_headers = ["*"]
    allowed_methods = ["GET", "HEAD"]
    allowed_origins = ["*"]
    expose_headers  = ["ETag"]
    max_age_seconds = 3000
  }
}

resource "aws_s3_bucket_versioning" "contents_devart" {
  bucket = aws_s3_bucket.contents_devart.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_accelerate_configuration" "contents_devart" {
  bucket = aws_s3_bucket.contents_devart.id
  status = "Enabled"
}

resource "aws_s3_bucket_policy" "contents_devart" {
  bucket = aws_s3_bucket.contents_devart.id
  policy = data.aws_iam_policy_document.contents_devart.json
}

data "aws_iam_policy_document" "contents_devart" {
  statement {
    principals {
      type        = "Service"
      identifiers = ["cloudfront.amazonaws.com"]
    }

    condition {
      test     = "StringEquals"
      variable = "AWS:SourceArn"
      values = [
        aws_cloudfront_distribution.devart_cdn_distribution.arn,
      ]
    }

    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.contents_devart.arn}/*"]
  }
}

resource "aws_s3_bucket_lifecycle_configuration" "contents_devart" {
  bucket = aws_s3_bucket.contents_devart.id

  rule {
    id     = "contents_devart_rule"
    status = "Enabled"

    transition {
      days          = 30
      storage_class = "STANDARD_IA"
    }
  }
}

resource "aws_cloudfront_origin_access_control" "devart_contents" {
  name                              = "devart-contents"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

# Cloudfront Distribution
resource "aws_cloudfront_distribution" "devart_cdn_distribution" {
  origin {
    domain_name              = aws_s3_bucket.contents_devart.bucket_regional_domain_name
    origin_id                = "devart_origin"
    origin_access_control_id = aws_cloudfront_origin_access_control.devart_contents.id
  }

  enabled         = true
  is_ipv6_enabled = true
  comment         = "Cloudfront configuration for cdn"
  http_version    = "http2and3"

  # Alias of cloudfront distribution
  aliases = ["<Your domain name>"] # change this to your domain name

  # Default Cache behavior
  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD", "OPTIONS"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "devart_origin"
    compress         = true

    forwarded_values {
      query_string = false

      cookies {
        forward = "all"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  # List of Custom Cache behavior
  # This behavior will be applied before default
  ordered_cache_behavior {

    path_pattern = "*.gif"

    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "devart_origin"
    compress         = false

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 3600

    forwarded_values {
      query_string            = true
      query_string_cache_keys = ["d"]

      cookies {
        forward = "all"
      }
    }
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  # Certification Settings
  viewer_certificate {
    acm_certificate_arn      = "<Your acm arn>" # change this to your acm arn in us-east-1
    minimum_protocol_version = "TLSv1.1_2016"
    ssl_support_method       = "sni-only"
  }

  # You can set custom error response
  custom_error_response {
    error_caching_min_ttl = 5
    error_code            = 404
    response_code         = 404
    response_page_path    = "/404.html"
  }

  custom_error_response {
    error_caching_min_ttl = 5
    error_code            = 500
    response_code         = 500
    response_page_path    = "/500.html"
  }

  custom_error_response {
    error_caching_min_ttl = 5
    error_code            = 502
    response_code         = 502
    response_page_path    = "/500.html"
  }

  # Tags of cloudfront
  tags = {
    Name = "<domain name>" #Change this to domain name that you want to use
  }
}

# Route 53 Record for cloudfront
resource "aws_route53_record" "devart_cdn" {
  zone_id = "<your zone id>" #Change this to your zone id
  name    = "<domain name>" #Change this to domain name that you want to use
  type    = "A"

  alias {
    name                   = aws_cloudfront_distribution.devart_cdn_distribution.domain_name
    zone_id                = "Z2FDTNDATAQYW2" # This is fixed value!
    evaluate_target_health = false
  }
}


```

{% endcode %}

### 도메인 테스트

CloudFront 도메인이 아니라, 직접 지정한 도메인으로 변경합니다!&#x20;

<figure><img src="/files/LD3mjjrriPWf5qHC0TDc" alt=""><figcaption></figcaption></figure>

```
https://cdn.devops-art-factory.com/sample.html
```

<figure><img src="/files/Zvf5aGtpFm9xOWMpgx1c" alt=""><figcaption></figcaption></figure>


# 5. Jenkins 배포 실습

##


# Jenkins란

## Jenkins란?&#x20;

Jenkins는 Application 빌드(CI) 및 배포(CD)를 도와주는 오픈소스 툴입니다. Jenkins를 사용하면 Github와 손쉽게 연동하여 코드를 배포할 수 있습니다. 본 실습에서는 Jenkins를 EC2 인스턴스에 배포하고, AWS Codebuild를 연동하여 배포를 하는 구조를 만들어보고자 합니다.

<https://aws.amazon.com/blogs/devops/simplify-your-jenkins-builds-with-aws-codebuild/>

![](/files/-MSsAnEkvSpXI6zR3jdH)

Jenkins를 배포하기 위한 기본 구조는 아래와 같습니다.

* Application Load Balancer + Autoscaling Group을 통해서 인스턴스를 생성합니다.
* Userdata는 `terraform template`파일을 통해 추가합니다.
* Jenkins에서 생성되는 data파일은 유실되면 안되는 중요한 정보가 많기 때문에, Amazon EFS를 생성해서 `/var/lib/jenkins` Directory에 마운트합니다.


# Jenkins 설치하기

Jenkins 설치 명령어 on Amazon Linux 2022

```
yum update -y

amazon-linux-extras install java-openjdk11 -y

wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io-2023.key

yum install -y jenkins

systemctl start jenkins

systemctl enable jenkins
```


# Jenkins 배포하기

## Jenkins 배포 핵심

Jenkins 배포에서 중요한 것은 EFS 활용과 Userdata 부분입니다. ALB + Autoscaling 을 생성하는 부분은 앞선 실습에서 했기 때문에 이번 실습에서는 생략하겠습니다.

{% content-ref url="/pages/-MP7LuXFsqXGBdB9AGFi" %}
[3. 어플리케이션 서비스 만들기](/2.)
{% endcontent-ref %}

### Elastic File System 생성

* Terraform으로 Elastic file system을 생성합니다.
* 생성된 EFS의 Domain name은 Instance의 Userdata에서 참조됩니다.

{% code title="vim terraform/platform/jenkins/\_module/jenkins/efs.tf" %}

```bash
# Security Group for EFS
## Should allow Jenkins Instance ONLY!!!
resource "aws_security_group" "efs" {
  name        = "${var.service_name}-efs-${var.vpc_name}"
  description = "${var.service_name} efs sg for ${var.vpc_name}"
  vpc_id      = var.target_vpc

  ingress {
    from_port = 2049 # for NFS

    to_port  = 2049
    protocol = "tcp"
    security_groups = [
      # EC2 Instance Security Group
      aws_security_group.ec2.id,
    ]
  }

  tags = {
    Name = "${var.service_name}-efs-${var.vpc_name}"
  }
}

resource "aws_efs_file_system" "file_system" {
  tags = {
    Name = "${var.service_name}-efs-${var.vpc_name}"
  }
  
  #You can control this value through variable
  provisioned_throughput_in_mibps = var.efs_provisioned_throughput_in_mibps
  
  #You can control this mode through variable
  throughput_mode = var.efs_throughput_mode

}

resource "aws_efs_mount_target" "mount_target" {
  # If you do not have NAT gateway  or NAT instance in private subnets,
  # You should deploy jenkins to public subnet!
  count          = length(var.private_subnets)
  #count          = length(var.public_subnets)
  file_system_id = aws_efs_file_system.file_system.id

  subnet_id      = element(var.private_subnets, count.index)
  #subnet_id      = element(var.public_subnets, count.index)
  security_groups = [
    aws_security_group.efs.id,
  ]
}
```

{% endcode %}

### User data 정의

* Userdata에 efs 도메인 이름을 참조하기 위해서 테라폼의 `template_file`을 활용합니다.

먼저 `userdata.sh.tpl` 을 살펴보겠습니다. Userdata에서 배포에 필요한 기본 패키지를 설치하고, 생성된 EFS를 `/var/lib/jenkins` 에mount합니다.&#x20;

{% code title="vim terraform/platform/jenkins/\_module/jenkins/scripts/userdata.sh.tpl" %}

```bash
#!/bin/bash -ex

function waitForJenkins() {
    echo "Waiting jenkins to launch on 8080..."

    while ! nc -z localhost 8080; do
      sleep 0.1 # wait for 1/10 of the second before check again
    done

    echo "Jenkins launched"
}

function waitForPasswordFile() {
    echo "Waiting jenkins to generate password..."

    while [ ! -f /var/lib/jenkins/secrets/initialAdminPassword ]; do
      sleep 2 # wait for 1/10 of the second before check again
    done

    echo "Password created"
}

amazon-linux-extras install corretto8

yum update -y
yum install -y jq git awscli nmap-ncat nfs-common

export JENKINS_HOME=/var/lib/jenkins
mkdir -p $JENKINS_HOME

mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport ${efs_dns_name}:/ $JENKINS_HOME

wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat-stable/jenkins.repo
rpm --import https://jenkins-ci.org/redhat/jenkins-ci.org.key

yum install -y jenkins

sed -i 's/Djava.awt.headless=true/Djava.awt.headless=true -Xmx2G -Xms2G -Dorg.apache.commons.jelly.tags.fmt.timeZone=Asia\/Seoul/g' /etc/sysconfig/jenkins

service jenkins start

```

{% endcode %}

위에서 정의한 `user_data` 파일은 `template_file` 형태로 참조합니다. 변수로는 EFS dns\_name을 넘겨줍니다.

{% code title="vim terraform/platform/jenkins/\_module/jenkins/template.tf" %}

```bash
data "template_file" "init" {
  template = "${file("${path.module}/scripts/userdata.sh.tpl")}"
  vars = {
    efs_dns_name = aws_efs_file_system.file_system.dns_name
  }
}
```

{% endcode %}

### Module 사용 부분

* Jenkins 전체 세트는 Module을 통해서 정의하고, 실제 변수들은 환경에 따라 다르게 세팅합니다.&#x20;
* `terraform/platform/jenkins/<환경>` directory를 통째로 복사해서 새로운 환경 변수를 정의하면, 동일한 jenkins 세트를 생성하실 수 있습니다.
* 배포하는 리전이 서울(ap-northeast-2)가 아니면, `terraform.tfvars` 파일에서 `jenkins_master_ami` 를 해당 리전의 AMI ID로 변경해주시기 바랍니다.(Amazon Linux 2)
* External LB는 보안을 위해서 `반드시 내부망만 오픈`하시기 바랍니다!!!&#x20;
  * Github Webhook을 사용하는 경우에는 퍼블릭으로 오픈하지 마시고, <https://api.github.com/meta> 사이트에서 hook 서버 IP들만 오픈하시는 것을 추천드립니다.
  * 또 webhook을 보내더라도 바로 jenkins로 보내지 마시고, API Gateway + Lambda 등을 중간에 두고 인증절차를 거치시기 바랍니다.
    * <https://github.com/serverless/examples/tree/master/aws-node-github-webhook-listener>

{% code title="vim terraform/platform/jenkins/dayonep\_apnortheast2/services.tf" %}

```bash
module "jenkins" {
  source           = "../_module/jenkins"
  service_name     = "jenkins"
  service_port     = 8080
  healthcheck_port = 8080
  account_id       = var.account_id.prod

  shard_id                 = data.terraform_remote_state.vpc.outputs.shard_id
  public_subnets           = data.terraform_remote_state.vpc.outputs.public_subnets
  private_subnets          = data.terraform_remote_state.vpc.outputs.private_subnets
  aws_region               = data.terraform_remote_state.vpc.outputs.aws_region
  target_vpc               = data.terraform_remote_state.vpc.outputs.vpc_id
  vpc_name                 = data.terraform_remote_state.vpc.outputs.vpc_name
  vpc_cidr_numeral         = data.terraform_remote_state.vpc.outputs.cidr_numeral
  route53_internal_domain  = data.terraform_remote_state.vpc.outputs.route53_internal_domain
  route53_internal_zone_id = data.terraform_remote_state.vpc.outputs.route53_internal_zone_id
  billing_tag              = data.terraform_remote_state.vpc.outputs.billing_tag

  newrelic_monitor        = "false"

  ssh_key_name            = "dayone-prod-master"

  instance_ami            = var.jenkins_master_ami
  tag_first_owner         = var.tag_first_owner
  tag_second_owner        = var.tag_second_owner
  tag_project             = var.tag_project
  efs_provisioned_throughput_in_mibps = 0
  
  #KMS Key for deployment
  deployment_common_arn     = data.terraform_remote_state.kms.outputs.aws_kms_key_prod_apne2_deployment_common_arn

  # Instance Count Variables
  instance_count_max     = 1
  instance_count_min     = 1
  instance_count_desired = 1

  # Route53 variables
  acm_external_ssl_certificate_arn  = var.r53_variables.prod.star_dayonedevops_com_acm_arn_apnortheast2
  route53_external_zone_id          = var.r53_variables.prod.dayonedevops_com_zone_id
  domain_name                       = "jenkins"

  # Resource LoadBalancer variables
  lb_variables                      = var.lb_variables

  # Security Group variables
  sg_variables                      = var.sg_variables  

  # Home Security Group via remote_state
  home_sg                       = data.terraform_remote_state.vpc.outputs.aws_security_group_home_id
  #github_hook_sg                = data.terraform_remote_state.vpc.outputs.aws_security_group_github_hook_id
  github_hook_sg                = ""

  # CIDR for external LB
  # Control allowed IP for external LB 
  ext_lb_ingress_cidrs = [
    "0.0.0.0/0"
  ]
}
```

{% endcode %}

## 배포 후 세팅

* Jenkins를 배포하고 나면 Route53 URL을 통해서 Jenkins에 접속하실 수 있습니다.&#x20;
* 홈페이지에 접속하시면 아래와 같이 init password를 입력하라고 나옵니다.
  * 배포된 인스턴스에 접속하셔서 웹페이지에 적힌 path 파일에서 패스워드를 얻으신 후에 붙여넣으시면 됩니다.
  * Instance 접속은 bastion host -> instance로 하셔도 되고, aws sessions manager를 통해서 접속하실 수도 있습니다.
  * SSM 접속을 위한 기본적인 Permission은 이미 IAM Role에 들어 있습니다.

```bash
[root@ip-10-20-19-82 ~]# cat /var/lib/jenkins/secrets/initialAdminPassword
04369ed0856c4f7bad4c64dde084cae9
```

![](/files/-MSsCO1LxXMu0V-aZiO0)

이후에 설치를 진행합니다. 원하시는 메뉴로 설치를 진행하시기 바랍니다.

![](/files/-MSsCQZvrCy6JW9IYpub)


# Codebuild 연동하기

## Codebuild란 무엇인가?

{% embed url="<https://www.beautiful.ai/player/-NzhRhLNat-M7XlL3QbJ/Introduction-to-AWS-CodeBuild>" %}

## Why Codebuild ?

{% file src="/files/SJ7SWXMEdj5tTQ93318b" %}

## Jenkins와 Codebuild

Jenkins와 Codebuild를 연동하게 되면, 빌드 또는 배포 job 은 jenkins에서 생성하고 실제 작업은 Codebuild를 통해서 진행하실 수 있습니다. Jenkins에서 Job이 실행되면 AWS 상에 독립적인 codebuild job이 생성 됩니다.

### 장점

* 각 Job을 독립적인 환경에서 빌드/배포할 수 있습니다.
* AWS Codebuild 특성상 특정 VPC내에서 실행하실 수 있기 때문에, 접근제어 또는 다른 서비스와의 연동 시 용이합니다.
* Codebuild의 모든 로그를 Amazon CloudWatch Logs를 통해서 보실 수 있고, 기존의 빌드 및 배포 이력을 손쉽게 찾아보실 수 있습니다.

{% hint style="info" %}
본 실습에서는 artp(prod) 계정에서 동일 환경에 배포해보겠습니다.
{% endhint %}

## 사전 준비 사항

* Jenkins와 Codebuild에 연동할 **Github 토큰**
* Jenkins에서 사용할 AWS IAM User 및 Credentials&#x20;
  * AWS\_ACCESS\_KEY\_ID
  * AWS\_SECRET\_ACCESS\_KEY

### Github 토큰

* Github 에서 `Settings` --> `Developer settings`  --> `Personal access tokens` 를 들어갑니다.
* `Generate new token` 을 클릭합니다.
* 이름을 넣고 `repo` 와 `read:org`권한을 추가한 후에 토큰을 발급합니다.
* 발급받은 토큰은 임시로 안전한 곳에 저장해두시기 바랍니다.

![](/files/-MSsDGNihjE_LBGJJhNA)

### IAM User 생성

* Jenkins를 배포할 계정의 IAM directory를 찾아서 들어갑니다.
  * 예시 :  `terraform/iam/art-prod` &#x20;
  * 없으신 경우에는 이전 [IAM 환경 구성](https://devops-art-factory.gitbook.io/devops-workshop/terraform/initialization)을 참조하셔서 먼저 환경 세팅을 해주시기 바랍니다.
* `GetLogEvents` 와 `Codebuild:*`에 관한 권한은 반드시 필요합니다. 다른 권한은 필요에 따라 추가/삭제하셔도 됩니다.
* 아래와 같이 테라폼 코드를 작성하시고 나서, terraform apply 를 통해 배포하시면 콘솔에서 유저를 찾으실 수 있습니다.

```bash
# Jenkins User
resource "aws_iam_user" "jenkins_codebuild" {
  name = "jenkins-codebuild"
}

# Permissions that jenkins needs to create codebuild job
# You can change s3 bucket below if you have any bucket that jenkins use for retrieving/uploading artifact
resource "aws_iam_user_policy" "jenkins_codebuild" {
  name   = "jenkins-codebuild"
  user   = aws_iam_user.jenkins_codebuild.name
  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Resource": ["arn:aws:logs:*:*:log-group:/aws/codebuild/*"],
            "Action": ["logs:GetLogEvents"]
        },
        {
            "Effect": "Allow",
            "Resource": ["arn:aws:s3:::art-deploy"],
            "Action": ["s3:GetBucketVersioning"]
        },
        {
            "Effect": "Allow",
            "Resource": ["arn:aws:s3:::art-deploy/*"],
            "Action": ["s3:PutObject"]
        },
        {
            "Effect": "Allow",
            "Resource": ["arn:aws:s3:::art-deploy/*"],
            "Action": ["s3:GetObject"]
        },
        {
            "Effect": "Allow",
            "Resource": ["arn:aws:codebuild:*:*:*"],
            "Action": [
                "codebuild:StartBuild",
                "codebuild:BatchGetBuilds",
                "codebuild:BatchGetProjects",
                "codebuild:StopBuild"
            ]
        }
	]
  }
EOF

}

```

생성이 완료되었으면 아래와 같이 Access Key를 발급 받습니다. 발급받으신 키는 ***반드시 안전한 곳에 보관***&#xD558;시기 바랍니다.

![](/files/-MSsDKPVLOED8Wi3Z4g9)

## CodeBuild 생성

이제 연동할 Codebuild 프로젝트를 생성하도록 하겠습니다. Codebuild 프로젝트를 위해서는 아래의 리소스가 필요합니다.&#x20;

* Codebuild에서 사용할 IAM 역할
* Codebuild의 기본 베이스가 되는 Docker image
* Codebuild에서 사용할 보안그룹
* Codebuild 프로젝트

### IAM 역할 생성

* Codebuild에서 사용할 역할이므로, 실제 빌드/배포에 필요한 권한을 가지고 있어야 합니다.
* Codebuild를 생성할 계정의 IAM 디렉토리로 가서 생성을 진행합니다. (`terraform/iam/art-prod`)

{% code title="vim terraform/iam/art-prod/codebuild-deployment.tf" %}

```bash
resource "aws_iam_role" "codebuild_deployment" {
  name               = "codebuild-deployment"
  path               = "/service-role/" assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "codebuild.amazonaws.com"
      },

      "Action": "sts:AssumeRole"

    }
  ]
}
EOF
}

resource "aws_iam_role_policy" "codebuild_deployment_operation" {
  name   = "codebuild-deployment-operation-access"
  role   = aws_iam_role.codebuild_deployment.id
  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DeploymentSecurityGroupAccess",
      "Action": [
        "ec2:CreateSecurityGroup",
        "ec2:DescribeSecurityGroups",
        "ec2:AuthorizeSecurityGroupIngress",
        "ec2:RevokeSecurityGroupIngress"
      ],
      "Effect": "Allow",
      "Resource": ["*"]
    },
    {
      "Sid": "DeploymentAMIAccess",
      "Action": [
        "ec2:RegisterImage",
        "ec2:DescribeImages"
      ],
      "Effect": "Allow",
      "Resource": ["*"]
    },
    {
      "Sid": "DeploymentSnapshotAccess",
      "Action": [
        "ec2:CreateSnapshot",
        "ec2:DeleteSnaphot",
        "ec2:DescribeSnapshots"
      ],
      "Effect": "Allow",
      "Resource": ["*"]
    },
    {
      "Sid": "DeploymentInstanceAccess",
      "Action": [
        "ec2:RunInstances",
        "ec2:StartInstances",
        "ec2:StopInstances",
        "ec2:RebootInstances",
        "ec2:TerminateInstances",
        "ec2:DescribeInstances",
        "ec2:CreateTags",
        "ec2:DescribeTags",
        "ec2:ModifyInstanceAttribute"
      ],
      "Effect": "Allow",
      "Resource": ["*"]
    },
    {
      "Sid": "DeploymentKeyPairAccess",
      "Action": [
        "ec2:DescribeKeyPairs"
      ],
      "Effect": "Allow",
      "Resource": ["*"]
    },
    {
      "Sid": "LaunchTemplates",
      "Action": [
        "ec2:DeleteLaunchTemplate",
        "ec2:CreateLaunchTemplate",
        "ec2:GetLaunchTemplateData",
        "ec2:DescribeLaunchTemplates",
        "ec2:DescribeLaunchTemplateVersions",
        "ec2:ModifyLaunchTemplate",
        "ec2:DeleteLaunchTemplateVersions",
        "ec2:CreateLaunchTemplateVersion"
      ],
      "Effect": "Allow",
      "Resource": ["*"]
    },
    {
      "Sid": "DeploymentVolumeAccess",
      "Action": [
        "ec2:AttachVolume",
        "ec2:CreateVolume",
        "ec2:DeleteVolume",
        "ec2:DescribeVolume*",
        "ec2:DetachVolume"
      ],
      "Effect": "Allow",
      "Resource": ["*"]
    },
    {
      "Sid": "DeploymentASGAccess",
      "Action": [
        "autoscaling:*"
      ],
      "Effect": "Allow",
      "Resource": ["*"]
    },
    {
      "Sid": "DeploymentVPCAccess",
      "Action": [
        "ec2:CreateNetworkInterface",
        "ec2:DescribeDhcpOptions",
        "ec2:DescribeNetworkInterfaces",
        "ec2:DeleteNetworkInterface",
        "ec2:DescribeSubnets",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeVpcs"
      ],
      "Effect": "Allow",
      "Resource": ["*"]
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:CreateNetworkInterfacePermission"
      ],
        "Resource": "arn:aws:ec2:ap-northeast-2:${var.account_id}:network-interface/*"
    },
    {
      "Sid": "DeploymentIAMAccess",
      "Action": [
        "iam:PassRole"
      ],
      "Effect": "Allow",
      "Resource": ["*"]
    },
    {
      "Sid": "DeploymentELBAccess",
      "Action": [
        "elasticloadbalancing:DescribeLoadBalancerAttributes",
        "elasticloadbalancing:DescribeLoadBalancers",
        "elasticloadbalancing:DescribeTargetGroupAttributes",
        "elasticloadbalancing:DescribeTargetHealth",
        "elasticloadbalancing:DescribeTargetGroups",
        "elasticloadbalancing:DescribeListeners",
        "elasticloadbalancing:DescribeTags",
        "elasticloadbalancing:DescribeRules",
        "elasticloadbalancing:DescribeInstanceHealth"
      ],
      "Effect": "Allow",
      "Resource": ["*"]
    },
    {
      "Sid": "CloudwatchAccess",
      "Action": [
        "cloudwatch:PutMetricAlarm"
      ],
      "Effect": "Allow",
      "Resource": ["*"]
    },
    {
      "Sid": "SSMSendCommand",
      "Action": [
        "ssm:SendCommand",
        "ssm:ListCommandInvocations"
      ],
      "Effect": "Allow",
      "Resource": ["*"]
    }
  ]
}
EOF
}

resource "aws_iam_role_policy" "codebuild_deployment_ecr" {
  name   = "codebuild-deployment-ecr"
  role   = aws_iam_role.codebuild_deployment.id
  policy = <<EOF
{
  "Statement": [
    {
      "Sid": "AllowGetAuthTokenAccess",
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken"
      ],
      "Resource": "*"
    },
    {
      "Sid": "AllowReadECRAccess",
      "Effect": "Allow",
        "Action": [
          "ecr:BatchCheckLayerAvailability",
          "ecr:GetDownloadUrlForLayer",
          "ecr:BatchGetImage",
          "ecr:PutImage",
          "ecr:InitiateLayerUpload",
          "ecr:UploadLayerPart",
          "ecr:CompleteLayerUpload"
        ],
        "Resource": "*"
    }

  ]
}
EOF
}

# If codebuild needs to deploy to another account
# then, it should have assume permission.
#resource "aws_iam_role_policy" "codebuild_deployment_assume_deploy" {
#  name   = "codebuild-deployment-assume-deploy"
#  role   = aws_iam_role.codebuild_deployment.id
#  policy = <<EOF
#{
#  "Statement": [
#    {
#      "Sid": "AllowAnsibleHeadDeployBucketAccess",
#      "Action": [
#        "sts:AssumeRole"
#      ],
#      "Resource": [
#        "arn:aws:iam::<target account id>:role/deployment"
#      ],
#      "Effect": "Allow"
#    }
#  ]
#}
#EOF
#}

# If you want to store secret data to AWS Parameter Store, 
# then you should give permission so that codebuild can retrieve those values for build and deployment
resource "aws_iam_role_policy" "codebuild_deployment_kms" {
  name   = "codebuild-deployment-kms-decryption"
  role   = aws_iam_role.codebuild_deployment.id
  policy = <<EOF
{
  "Statement": [
    {
      "Sid": "AllowSsmParameterAccess",
      "Action": [
        "ssm:GetParameter",
        "ssm:GetParameters"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:ssm:ap-northeast-2:${var.account_id}:parameter/CodeBuild/*"
      ]
    }
  ]
}
EOF
}

resource "aws_iam_role_policy" "codebuild_deployment_cloudwatch" {
  name   = "codebuild-deployment-cloudwatch"
  role   = aws_iam_role.codebuild_deployment.id
  policy = <<EOF
{
  "Statement": [
    {
      "Sid": "AllowCloudWatchAccess",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": [
        "arn:aws:logs:*:${var.account_id}:log-group:/aws/codebuild/*",
        "arn:aws:logs:*:${var.account_id}:log-group:/aws/codebuild/*:*",
        "arn:aws:logs:*:${var.account_id}:log-group:/*",
        "arn:aws:logs:*:${var.account_id}:log-group:/*:*",
        "arn:aws:logs:*:${var.account_id}:log-group:/*:*:*",
        "arn:aws:logs:*:${var.account_id}:log-group:*:*:*/*"
      ],
      "Effect": "Allow"
    }
  ]
}
EOF
}

resource "aws_iam_instance_profile" "codebuild_deployment" {
  name = "codebuild-deployment-profile"
  role = aws_iam_role.codebuild_deployment.name
}

resource "aws_iam_role_policy_attachment" "codebuild_deployment_attach" {
  role       = aws_iam_role.codebuild_deployment.name
  policy_arn = aws_iam_policy.app_universal.arn
}

output "codebuild_deployment_instance_profile" {
  value = aws_iam_instance_profile.codebuild_deployment.arn
}


output "codebuild_deployment_arn" {
  value = aws_iam_role.codebuild_deployment.arn
}

```

{% endcode %}

### Docker Image 생성(optional)

* Codebuild는 컨테이너로 생성되므로 어떤 이미지를 사용하여 Job을 돌릴지 정해주어야 합니다.&#x20;
* 본 가이드에서는 필요한 이미지를 빌드해서 ECR에 저장하도록 하겠습니다.
* ECR 레포지토리를 생성하는 디렉토리에 리소스를 추가합니다.
  * &#x20;`terraform/ecr/art-prod/prod_apnortheast2`
  * Policy 에 Account ID는 자신의 계정 번호를 넣어주시면 됩니다.

{% code title="vim terraform/ecr/art-prod/prod\_apnortheast2/art\_build.tf" %}

```bash
resource "aws_ecr_repository" "art_build" {
  name = "art-build"
}

resource "aws_ecr_repository_policy" "art_build" {
  repository = aws_ecr_repository.art_build.name

  policy = <<EOF
{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Sid": "new policy",
            "Effect": "Allow",
            "Principal": {
              "AWS": [
                "arn:aws:iam::<your account id>:root"
              ]
            },
            "Action": [
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "ecr:BatchCheckLayerAvailability",
                "ecr:DescribeRepositories",
                "ecr:GetRepositoryPolicy",
                "ecr:ListImages"
            ]
        }
    ]
}
EOF
}
```

{% endcode %}

* ECR 레포지토리 생성이 완료되면, 각자 원하는 Docker Image를 생성해서 해당 Repository에 푸시합니다. (샘플 이미지는 올려놓았습니다!)
  * `terraform/ecr/art-prod/prod_apnortheast2/Dockerfile-art-build-amazon`

### 보안그룹 생성

* VPC를 생성했던 곳으로 가서 Security Group을 생성합니다. (`terraform/vpc/artp_apnortheast2` )
* 만약 다른 곳에서 관리하고 계신 경우에는 해당 디렉토리에서 생성하시면 됩니다.
* 생성한 Security Group을 외부에서 참조할 수 있도록 output에도 추가합니다.

{% code title="vim terraform/vpc/artp\_apnortheast2/codebuild\_sg.tf" %}

```bash
# Codebuild Default Security Group
resource "aws_security_group" "codebuild_default" {
  name        = "codebuild-default-${var.vpc_name}"
  description = "codebuild-default group for ${var.vpc_name}"
  vpc_id      = aws_vpc.default.id

  ingress {
    from_port = 0
    to_port   = 65535
    protocol  = "tcp"
    cidr_blocks = [
      "0.0.0.0/0",
    ]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
```

{% endcode %}

{% code title="vim terraform/vpc/artp\_apnortheast2/outputs.tf" %}

```bash
output "aws_security_group_codebuild_default_id" {
  value = aws_security_group.codebuild_default.id
}
```

{% endcode %}

### Codebuild에 github 인증하기

* AWS CodeBuild 콘솔로 들어갑니다.

![](/files/-MSsDQm1dcwdATrJqZf-)

* `Create build project` 를 클릭합니다.
* `Connect using OAuth` 를 클릭합니다. (Personal Access Token으로 하셔도 무방합니다)

![](/files/-MSsDVcubJOmzhd1YH0f)

* 인증을 마치신 후에는 `Cancel` 누르고 빠져나오셔도 됩니다. 실제 프로젝트는 terraform 코드를 이용해서 생성할 예정입니다.

### Codebuild Project 생성

* Jenkins에서 사용할 Codebuild 프로젝트를 생성합니다.
* Codebuild 프로젝트와 관련된 전체 코드는 아래 디렉토리에 module 형태로 올려놓았으니 참조해주시기 바랍니다.
  * `terraform/codebuild/`
* 변경이 필요한 부분은 아래 주석으로 표시해 놓았습니다.

{% hint style="info" %}
본 가이드에서는 goployer 오픈소스를 활용하여 EC2를 배포할 예정입니다. goployer를 사용하고 싶으신 분들은 아래 링크를 참조해주시기 바랍니다.

<https://github.com/DevopsArtFactory/goployer.git>
{% endhint %}

{% code title="vim terraform/codebuild/artp-apnortheast2/deployment.tf" %}

```bash
module "deployment" {
  source                = "../_modules/deployment"
  service_name          = "deployment-prod"
  shard_id              = data.terraform_remote_state.vpc.outputs.shard_id
  public_subnets        = data.terraform_remote_state.vpc.outputs.public_subnets
  private_subnets       = data.terraform_remote_state.vpc.outputs.private_subnets
  aws_region            = data.terraform_remote_state.vpc.outputs.aws_region
  target_vpc            = data.terraform_remote_state.vpc.outputs.vpc_id
  vpc_name              = data.terraform_remote_state.vpc.outputs.vpc_name
  vpc_cidr_numeral      = data.terraform_remote_state.vpc.outputs.cidr_numeral
  billing_tag           = data.terraform_remote_state.vpc.outputs.billing_tag
  env_suffix            = data.terraform_remote_state.vpc.outputs.env_suffix

  image_credentials_type = "SERVICE_ROLE"

  # Change this to your security group ID
  deployment_default_sg = data.terraform_remote_state.vpc.outputs.aws_security_group_codebuild_default_id

  # Change this to your IAM role
  service_role          = "arn:aws:iam::816736805842:role/service-role/codebuild-deployment"

  # Change this repository which you want to pull code from.
  # In this repository, it should have buildspec file that you specify below
  github_repo            = "https://github.com/DevopsArtFactory/goployer.git"

  # buildspec file name you want to use
  buildspec              = "buildspec-deploy-prod.yml"

  # Change this to your docker image that you want to use for codebuild
  build_image            = "816736805842.dkr.ecr.ap-northeast-2.amazonaws.com/art-build:latest"
}

```

{% endcode %}

## Jenkins 설정

이제 Codebuild 연동을 위해서 Jenkins 세팅을 진행하도록 하겠습니. 본 세팅은 따로 코드로 정의하지 않았고, 아래 가이드를 통해서 한 번 설정하시고 나면 특별한 일이 없는 이상은 바꾸실 일이 거의 없습니다.&#x20;

### 플러그인 설치

* `Manage Jenkins` -> `Manage Plugins` 를 클릭합니다.

![](/files/-MSsDblUxUkfpEVdmtgc)

* `Available` 항목을 가셔서 codebuild를 검색해보시면 `AWS CodBuild` 플러그인이 보이실 겁니다. 이걸 클릭하신 후에 `Install now without restart` 를 클릭합니다.&#x20;

![](/files/-MSsDffysCPGwonXg_Aw)

설치가 진행되면 Jenkins를 재실행 할 수 있도록 옵션을 클릭해주고 기다립니다. Jenkins가 설치를 마치면 알아서 재시작합니다.&#x20;

![](/files/-MSsDilUpIrW8W_8GtJ0)

### Github 연동

* `Manage Jenkins` -> `Configure System` 를 클릭합니다.

![](/files/-MSsDmh8xsFqjYDkJIhx)

* Github 란으로 가셔서 `Add Github Server` 를 클릭합니다. 이름을 입력하신 후에, Credentials 쪽에 `Add` 버튼을 클릭합니다.

![](/files/-MSsDsB10-KXa7B3N-vr)

* Kind로 `Secret text` 를 선택하시고, Secret란에 이전에 발급받으신 `github 토큰`을 입력합니다. ID는 편하신대로 입력하셔도 됩니다. 완료되었으면 `Add` 버튼을 클릭합니다.

![](/files/-MSsDusGeOoxOQ-liqw9)

* 생성하신 Credentials를 선택하고 `Test Connection`을 클릭하셔서 정상적으로 연결되었는지 확인합니다.

![](/files/-MSsE1YP-8SvgQQ1IfhG)

* 정상적으로 테스트가 끝나면 `Save` 버튼을 누르셔서 현재 세팅을 저장합니다.

### Codebuild 연동&#x20;

* 메인화면에서 `New Item` 을 선택합니다.
* `Freestyle project` 를 선택하시고, 이름을 입력하신 후 ok를 클릭합니다.
  * 저는 hello production 어플리케이션을 배포할 예정입니다.

![](/files/-MSsE5lWnRe4pJvfqMcU)

* Build 란에서 AWS Codebuild를 선택합니다.
  * AWS Codebuild가 안보이시는 경우에는 위에서 플러그인이 제대로 설치되었는지 확인해주시기 바랍니다.&#x20;

![](/files/-MSsE89FpzHNlwmeEu9_)

* `Select credentials from jenkins` 를 선택하시고 `Add` 를 클릭해서 새로운 인증키를 생성합니다.

![](/files/-MSsEAOmn5zfqLGM0-an)

* 사전에 발급받은 Jenkins User의 AWS Access Key와 AWS Secret Access Key를 입력합니다.&#x20;

![](/files/-MSsECjT5mZ-ZF15F6-P)

* Region ID를 입력하시고, 이전에 생성했던 Codebuild 이름을 입력합니다. 이후에 `Use Project Source` 를 선택합니다.  따로 값은 입력하지 않으셔도 됩니다.

![](/files/-MSsEFC-SbRsxQC6uW-L)

* (Optional) 파라미터가 필요하신 경우에는 파라미터를 (원하시는만큼)입력합니다. 해당 파라미터는 codebuild로 전달하실 수 있습니다. 참고로 맨 위쪽에서 세팅하시면 됩니다.

![](/files/-MSsEIgYm4sTP5AtxpSh)

* 위의 파라미터를 Codebuild로 전달합니다. Codebuild 설정했던 부분의 `Build Configuration` 부분에 있습니다.

![](/files/-MSsENfBEvWSSMcUX-fw)

* 설정이 완료되었으면 save 버튼을 누릅니다.&#x20;

## Codebuild 사용하기

* `Build with Parameters` 를 클릭하셔서 Codebuild job을 실행합니다.

![](/files/-MSsEQUG_ehDlMN63opw)

* &#x20;빌드 번호를 선택하시고 `Console Output` 을 보시면 Codebuild의 로그를 보실 수 있습니다.

![](/files/-MSsEUV_Yte8PAueNfJw)

* Codebuild를 확인해보시면 아래와 같이 실행되고 있는 Job을 보실 수 있습니다.

![](/files/-MSsEXy4E74viFPAr2bn)


