Programming/IaC

[IaC] IaC/테라폼 이해, 기본 명령 사용법( command, HCL, block, resource)

Hayley Shim 2023. 10. 28. 17:58

안녕하세요. 최근 도서 “테라폼으로 시작하는 IaC” 의 내용을 기준으로 스터디한 내용을 정리했습니다. 해당 글에서는 IaC와 테라폼에 대해 간단히 이해하고 기본 명령 사용법에 대해 알아보겠습니다.

 

1. IaC/테라폼 이해

IaC와 테라폼

  • IaC : 코드형 인프라(Infrastructure as Code, IaC)는 코드를 통해 인프라를 관리하고 프로비저닝하는 것을 말합니다.
  • 테라폼 : 하시코프사에서 공개한 IaC 도구로 “워크플로우에 집중, 코드형 인프라, 실용주의”라는 하시코프의 철학을 담은 도구입니다.

테라폼 제공 유형별 기능

테라폼 클라우드 가격정책 — [참고: Pricing, Feature]

  • Free : 리소스 500개 까지 무료 → 커뮤니티 버전
  • Standard : Free + 워크플로우 기능 추가 + 동시실행(Concurrency 개수 3개)
  • Plus : 정책, 보안, 신뢰성, 확장성 등 기업형 고객에게 적합(대규모 사용자를 위한 비용모델)
  • Enterprise : Plus와 대부분 유사하며 설치형 모델

테라폼 환경 구성

  • 여러 실행 환경 구성 방법 중 OS 패키지 관리자 활용하여 테라폼을 설치합니다.
  1. OS에 맞게 테라폼 설치
# macOS 환경

# tfenv 설치
$ brew install tfenv

# 설치 가능 버전 리스트 확인
$ tfenv list-remote

# 테라폼 1.5.6 버전 설치
$ tfenv install 1.5.6

# tfenv로 설치한 버전 확인(변경 전)
$ tfenv list
  1.5.6
* 1.5.1 (set by /usr/local/Cellar/tfenv/3.0.0/version)

# 테라폼 1.5.6 버전 사용 설정 
$ tfenv use 1.5.6

# tfenv로 설치한 버전 확인(변경 후)
$ tfenv list
* 1.5.6 (set by /usr/local/Cellar/tfenv/3.0.0/version)
  1.5.1

# 테라폼 버전 정보 확인
$ terraform version

# 자동완성
$ terraform -install-autocomplete
## 참고 .zshrc 에 아래 추가됨
$ cat ~/.zshrc
autoload -U +X bashcompinit && bashcompinit
complete -o nospace -C /usr/local/bin/terraform terraform

windowOS에서 WSL2 리눅스 환경 설치 후 사용하는 것이 가능합니다. 하지만 WSL의 경우 종종 이슈가 발생하여 개인적으로 macOS 환경을 권장합니다.

2. OS 환경에 맞게 VS Code 설치 후 Externtions 확장 설치

  • VS Code — [Extentions]에서 HashiCorp HCL 을 검색 후 설치합니다.
  • 설정(Command+,) → 일반적으로 사용되는 설정 ⇒ Files: Auto Save 값을 afterDelay 로 선택하여 자동저장 설정을 합니다.

3. AWS CLI 설치 및 자격증명 설정

# 방안1. macOS 설치 방법
$ brew install awscli

# 방안2. Linux 설치 방법
$ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
$ unzip awscliv2.zip
$ sudo ./aws/install


# aws cli 버전 확인
$ aws --version
aws-cli/2.13.13 Python/3.11.5 Darwin/22.6.0 source/x86_64 prompt/off

# aws cli 사용 시도(실패)
$ aws s3 ls

#(결과예시)
1.InvalidToken: An error occurred (InvalidToken) when calling the ListBuckets operation: The provided token is malformed or otherwise invalid.
2.configure 미설정 : Unable to locate credentials. You can configure credentials by running "aws configure".
---

# 방안1. aws configure 로 자격증명 설정
$ aws configure
... >> 입력

$ aws configure list
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                <not set>             None    None
access_key     ****************67BY shared-credentials-file
secret_key     ****************67BY shared-credentials-file
    region           ap-northeast-2      config-file    ~/.aws/config

# 방안2. 환경 변수로 자격증명 설정
# Linux or macOS
$ export AWS_ACCESS_KEY_ID=AKIXXXXXXXXXX
$ export AWS_SECRET_ACCESS_KEY=QkHsXXXXXXXXXXXXXXXXXXXX
$ export AWS_DEFAULT_REGION=ap-northeast-2

# 페이저 사용 비활성화[참고: AWS CLI 페이지 매김]
$ export AWS_PAGER=""

# aws cli 사용 시도(성공)
$ aws s3 ls

4. 실습에 편리한 Tool 설치

# macOS
$ brew install tree jq watch

# Linux
$ sudo apt install -y tree jq

5. AWS 서울 리전(ap-northeast-2)에 default VPC 존재 확인 : 없을 경우 아래 (옵션) default VPC 생성 해두기 참고 — 링크

$ aws ec2 describe-vpcs --filter 'Name=isDefault,Values=true' | jq
{
...

$ aws ec2 describe-vpcs --filter 'Name=isDefault,Values=true' | jq '.Vpcs[0].VpcId'
"vpc-0dc6bc54bbf096999"


#aws ec2 describe-subnets --filter 'Name=vpc-id,Values=vpc-3912a952' --output table
$ aws ec2 describe-subnets --filter 'Name=vpc-id,Values=vpc-<자신의VPC ID>' --output table

--------------------------------------------------------------------------------------------------------------
|                                               DescribeSubnets                                              |
+------------------------------------------------------------------------------------------------------------+
||                                                  Subnets                                                 ||
|+------------------------------+---------------------------------------------------------------------------+|
||  AssignIpv6AddressOnCreation |  False                                                                    ||
||  AvailabilityZone            |  ap-northeast-2a                                                          ||
||  AvailabilityZoneId          |  apne2-az1                                                                ||
||  AvailableIpAddressCount     |  4091                                                                     ||
||  CidrBlock                   |  172.31.0.0/20                                                            ||
||  DefaultForAz                |  True                                                                     ||
||  EnableDns64                 |  False                    


# default vpc 없을 경우 생성
# default VPC를 생성
$ aws ec2 create-default-vpc

# default Subnet 생성
$ aws ec2 create-default-subnet --availability-zone ap-northeast-2a
$ aws ec2 create-default-subnet --availability-zone ap-northeast-2b
$ aws ec2 create-default-subnet --availability-zone ap-northeast-2c
$ aws ec2 create-default-subnet --availability-zone ap-northeast-2d

6. EC2 1대 배포

배포전 준비

# 각자 편한 디렉터리를 생성해준다
$ mkdir t101-1week-ec2
$ cd t101-1week-ec2

# Amazon Linux 2 최신 ami id 찾기 : ami-09bae2fe0380843e0 
#aws ec2 describe-images --owners self amazon
$ aws ec2 describe-images --owners self amazon --query 'Images[*].[ImageId]' --output text

$ aws ssm get-parameters-by-path --path /aws/service/ami-amazon-linux-latest
$ aws ssm get-parameters-by-path --path /aws/service/ami-amazon-linux-latest --query "Parameters[].Name"
$ aws ssm get-parameters-by-path --path /aws/service/ami-amazon-linux-latest --query "Parameters[].Value"


# EC2 생성 모니터링 - VS Code 터미널1에서 실행
$ export AWS_PAGER=""
$ while true; do aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text ; echo "------------------------------" ; sleep 1; done

EC2 1대 배포 실행 — 터미널1 : HCL Hashicorp Configuration Language 코드 파일 생성 — VS Code 터미널2에서 실행

  • provider : Terraform으로 정의할 Infrastructure Provider를 의미 — Docs
  • resource : 실제로 생성할 인프라 자원을 의미 — Docs
# VS Code 터미널2
$ cat <<EOT > main.tf
provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_instance" "example" {
  ami           = "ami-09bae2fe0380843e0"
  instance_type = "t2.micro"
}
EOT

배포 실행 — VS Code 터미널2에서 실행

# 초기화
$ terraform init
$ ls -al

total 16
drwxr-xr-x  5 yhshim  staff  160 Sep  2 21:03 .
drwxr-xr-x  3 yhshim  staff   96 Sep  2 20:52 ..
drwxr-xr-x  3 yhshim  staff   96 Sep  2 21:03 .terraform
-rw-r--r--  1 yhshim  staff  252 Sep  2 21:03 .terraform.lock.hcl
-rw-r--r--  1 yhshim  staff  157 Sep  2 21:03 main.tf

$ tree .terraform
.terraform
└── providers
    └── registry.terraform.io
        └── hashicorp
            └── aws
                └── 4.29.0
                    └── darwin_arm64
                        └── terraform-provider-aws_v4.29.0_x5

# plan 실행 시 아래와 같은 정보가 출력
$ terraform plan
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_instance.example will be created
  + resource "aws_instance" "example" {
      + ami                                  = "ami-09bae2fe0380843e0"
      + arn                                  = (known after apply)
      + associate_public_ip_address          = (known after apply)
      + availability_zone                    = (known after apply)
      + cpu_core_count                       = (known after apply)
      + cpu_threads_per_core                 = (known after apply)
      + disable_api_stop                     = (known after apply)
      + disable_api_termination              = (known after apply)
      + ebs_optimized                        = (known after apply)
      + get_password_data                    = false
      + host_id                              = (known after apply)
      + host_resource_group_arn              = (known after apply)
      + id                                   = (known after apply)
      + instance_initiated_shutdown_behavior = (known after apply)
      + instance_state                       = (known after apply)
      + instance_type                        = "t2.micro"
      + ipv6_address_count                   = (known after apply)
      + ipv6_addresses                       = (known after apply)
      + key_name                             = (known after apply)
      + monitoring                           = (known after apply)
      + outpost_arn                          = (known after apply)
      + password_data                        = (known after apply)
      + placement_group                      = (known after apply)
      + placement_partition_number           = (known after apply)
      + primary_network_interface_id         = (known after apply)
      + private_dns                          = (known after apply)
      + private_ip                           = (known after apply)
      + public_dns                           = (known after apply)
      + public_ip                            = (known after apply)
      + secondary_private_ips                = (known after apply)
      + security_groups                      = (known after apply)
      + source_dest_check                    = true
      + subnet_id                            = (known after apply)
      + tags_all                             = (known after apply)
      + tenancy                              = (known after apply)
      + user_data                            = (known after apply)
      + user_data_base64                     = (known after apply)
      + user_data_replace_on_change          = false
      + vpc_security_group_ids               = (known after apply)
    }

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

# apply 실행
$ terraform apply
 Enter a value: yes 입력

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_instance.example will be created
  + resource "aws_instance" "example" {
      + ami                                  = "ami-09bae2fe0380843e0"
      + arn                                  = (known after apply)
      + associate_public_ip_address          = (known after apply)
      + availability_zone                    = (known after apply)
      + cpu_core_count                       = (known after apply)
      + cpu_threads_per_core                 = (known after apply)
      + disable_api_stop                     = (known after apply)
      + disable_api_termination              = (known after apply)
      + ebs_optimized                        = (known after apply)
      + get_password_data                    = false
      + host_id                              = (known after apply)
      + host_resource_group_arn              = (known after apply)
      + id                                   = (known after apply)
      + instance_initiated_shutdown_behavior = (known after apply)
      + instance_state                       = (known after apply)
      + instance_type                        = "t2.micro"
      + ipv6_address_count                   = (known after apply)
      + ipv6_addresses                       = (known after apply)
      + key_name                             = (known after apply)
      + monitoring                           = (known after apply)
      + outpost_arn                          = (known after apply)
      + password_data                        = (known after apply)
      + placement_group                      = (known after apply)
      + placement_partition_number           = (known after apply)
      + primary_network_interface_id         = (known after apply)
      + private_dns                          = (known after apply)
      + private_ip                           = (known after apply)
      + public_dns                           = (known after apply)
      + public_ip                            = (known after apply)
      + secondary_private_ips                = (known after apply)
      + security_groups                      = (known after apply)
      + source_dest_check                    = true
      + subnet_id                            = (known after apply)
      + tags_all                             = (known after apply)
      + tenancy                              = (known after apply)
      + user_data                            = (known after apply)
      + user_data_base64                     = (known after apply)
      + user_data_replace_on_change          = false
      + vpc_security_group_ids               = (known after apply)
    }

Plan: 1 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

aws_instance.example: Creating...
aws_instance.example: Still creating... [10s elapsed]
aws_instance.example: Still creating... [20s elapsed]
aws_instance.example: Still creating... [30s elapsed]
aws_instance.example: Creation complete after 31s [id=i-0330d4be110f21edc]

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

# 모니터링 : [터미널1]에 Name 확인

EC2 태그 정보 수정 — 터미널2

cat <<EOT > main.tf
provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_instance" "example" {
  ami           = "ami-09bae2fe0380843e0"
  instance_type = "t2.micro"

  tags = {
    Name = "t101-study"
  }

}
EOT

배포 실행

# plan 실행 시 아래와 같은 정보가 출력
$ terraform plan
aws_instance.example: Refreshing state... [id=i-0330d4be110f21edc]

Terraform used the selected providers to generate the
following execution plan. Resource actions are indicated
with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # aws_instance.example will be updated in-place
  ~ resource "aws_instance" "example" {
        id                                   = "i-0330d4be110f21edc"
      ~ tags                                 = {
          + "Name" = "t101-study"
        }
      ~ tags_all                             = {
          + "Name" = "t101-study"
        }
        # (29 unchanged attributes hidden)

        # (7 unchanged blocks hidden)
    }

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

# apply 실행
$ terraform apply
 Enter a value: yes 입력

aws_instance.example: Refreshing state... [id=i-0330d4be110f21edc]

Terraform used the selected providers to generate the
following execution plan. Resource actions are indicated
with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # aws_instance.example will be updated in-place
  ~ resource "aws_instance" "example" {
        id                                   = "i-0330d4be110f21edc"
      ~ tags                                 = {
          + "Name" = "t101-study"
        }
      ~ tags_all                             = {
          + "Name" = "t101-study"
        }
        # (29 unchanged attributes hidden)

        # (7 unchanged blocks hidden)
    }

Plan: 0 to add, 1 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

aws_instance.example: Modifying... [id=i-0330d4be110f21edc]
aws_instance.example: Modifications complete after 0s [id=i-0330d4be110f21edc]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
# 모니터링 : [터미널1]에 Name 확인
1 to change
EC2 태그 정보 수정

EC2 삭제 — 터미널2

# 리소스 삭제
$ terraform destroy
 Enter a value: yes 입력

혹은
$ terraform destroy -auto-approve
 

2. 기본 사용법

  1. 주요 커맨드

실습 디렉터리 생성 (workspaces)

# 실습 디렉터리 생성 후 이동
$ mkdir workspaces
$ cd workspaces

# 테라폼 실행
$ terraform
Usage: terraform [-version] [-help] <command> [args]
...

VS Code 폴더 생성 (03.start) → 새파일 생성 (main.tf)

resource "local_file" "abc" {
content  = "abc!"
filename = "${path.module}/abc.txt"
}

help & init 초기화

# 서브커맨드 help 지원
$ terraform console -help
$ terraform init -help

# init 초기화
# 테라폼 실행을 위해 코드 파일이 있는 디렉터리로 이동
# (참고) 테라폼이 실행되는 디렉터리 = 모듈(테라폼 코드 파일과 변수 파일), 기본 작업디렉터리는 '루트 모듈', 호출 모듈은 '자식 모듈'
$ cd 03.start/

# plan 실행 시 에러 출력
$ terraform plan
│ Error: Inconsistent dependency lock file
│ 
│ The following dependency selections recorded in the lock file are inconsistent with the current configuration:
│   - provider registry.terraform.io/hashicorp/local: required by this configuration but no version is selected
│ 
│ To make the initial dependency selections that will initialize the dependency lock file, run:
│   terraform init

# 초기화 : 코드 사용 구문 기반으로 필요한 프로바이더 플러그인을 찾고 설치, 추가로 '프로바이더/모듈/백엔드' 구성 설정/변경 시 수행 필요
$ terraform init
$ ls -al
$ tree .terraform  # VS Code에서 탐색기 확인
.terraform
└── providers
    └── registry.terraform.io
        └── hashicorp
            └── local
                └── 2.4.0
                    └── darwin_arm64
                        └── terraform-provider-local_v2.4.0_x5

7 directories, 1 file
  • Error: Inconsistent dependency lock file : 참고

plan 계획 & apply 실행

# plan 실행 : 구성 내용을 바탕으로 어떤 리소스가 생성되는지 상세 내역 출력, 기본값 자동 입력 적용
$ terraform plan
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:

  # local_file.abc will be created
  + resource "local_file" "abc" {
      + content              = "abc!"
      + content_base64sha256 = (known after apply)
      + content_base64sha512 = (known after apply)
      + content_md5          = (known after apply)
      + content_sha1         = (known after apply)
      + content_sha256       = (known after apply)
      + content_sha512       = (known after apply)
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "./abc.txt"
      + id                   = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy. # 하나의 리소스가 추가되고, 변경되거나 삭제되는 것은 없을 예정


# plan 결과를 시스템 코드로 출력
$ terraform plan -detailed-exitcode
...

# 코드 확인 : 0(변경 사항이 없는 성공), 1(오류가 있음), 2(변경 사항이 있는 성공)
$ echo $?
2

# (참고) apply 결과에 적용 시도
$ terraform apply -auto-approve -detailed-exitcode

# apply 실행 : no 입력
$ terraform apply
...
Enter a value: no
...

# plan 결과를 지정된 파일(바이너리 형태) 이름으로 생성
$ terraform plan -out=tfplan
$ cat tfplan
+�"W    tfplanUT�G�dd��J�@����O�tᢈHwv)4ͥi�K��])N&�fj�    g��n}B�w+B �
                                                                    y��
�.����)����;�3�C"��     τ�425AORb                                      �
ٌ@E�Kʚ���O���Q�4*�����T�!໓e�(l����V8^�`�{�S0���$�
ƞW�"�
     �&k�Z���4�\K�R"�2[�N�'b���3L!��    7���a�؛�T       -��/��o�Ǽ�9��?{jB��{��\�;���q:�_.$��
�9�[]��:��w��P�ŕ�,�+�"W tfstateUT�G�dD�=                                                    8�����b���mzf���r,�mX�
B1
��P���rm�+�"W"�R�c+I���ݥqp�~��-�b��nr'�tk�Z��i9�w%)�1�ޑK�t�9�z����a��d��ru������Y�9��
                tfstate-prevUT�G�dD�=
B1
��P���rm�+�"W"�Rtfconfig/m-/main.tfUT�G�d�A��i9�w%)�1�ޑK�t�9�z����a��d��ru������Y�9��
1
 �����xIcD!m�� ��}h����罉�̅��������5��[/�q��E�^Ǘ㝊?��q���c���Pg�fV\+�"W  tfconfig/modules.jsonUT�G�d��RP��RPPPP�N�T�RPRҁp]2�@\=%.�Z�X@��P��k))+�"W     .terraform.lock.hclUT�G�dT�Mo�E
                                                       ���������{�cW��p��'q�����M6�ݤI�$���������t��Kn�(۹��v��>����x����px)��/q:iOwe��.��ݛ��?ꡄo���KYQ��l�%/��(���8��v����q*�S���/����ɛ���VϷ�OoG�þ��+�)N��x_ޗ=����ߕ�z����+���m{���o~�xw�������K�����;���ϧ���|x��-:�.����S��ʳ�6�\9�@V�Qk�Ԯ��F
                              �Jj
jyEͪ��<��q{��-��tn�f���8u̺2�Ve�9�)���]�G�B#v`X�B�uT05h�Bh�Vg\xE1(WL���xM��C��!Ʋ���%�$3%�q&�-W���ڣW'��X�ј���
���iZ�*45V�K

��Eu�6xd�Q�a7Y.ցyu��<����f�tE��N�F�-#+����ci�@ʜ�;�R^}��Qk�l��:����yA��l�Jck,�82�����U��6V�+*j��s�
                                                                                                 1�Ui!�3qWt���u��U-�*��̐��!   |z�-b��2�Zu!�tlC��Ô>����0UpL��tZ���6a�lR[W'��}-�e]��՞=M������?v��        ��P�2   ���+�"W�ŕ�,�    tfplanUT�G�d+�"W���rm�  itfstateUT�G�d+�"W���rm�
                        tfstate-prevUT�G�d+�"Wg�fV\     �tfconfig/m-/main.tfUT�G�d+�"W��k))     dtfconfig/modules.jsonUT�G�+�"W�2     ���     �.terraform.lock.hclUT�G�dPK��%      

$ file tfplan
tfplan: Zip archive data, at least v2.0 to extract, compression method=deflate

# apply 실행 : 실행계획이 있으므로 즉시 적용됨
$ terraform apply tfplan
$ ls -al abc.txt
-rwxr-xr-x  1 yhshim  staff  4 Sep  2 23:34 abc.txt

# apply 실행을 다시 하게 되면
# 테라폼은 선언적 구성 관리를 제공하는 언어로 멱등성 idempotence을 갖고, 
# 상태를 관리하기 때문에 동일한 구성에 대해서는 다시 실행하거나 변경하는 작업을 
# 수행하지 않음
$ terraform apply
local_file.abc: Refreshing state... [id=5678fb68a642f3c6c8004c1bdc21e7142087287b]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are
needed.

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


$ terraform state list
local_file.abc


코드 파일 수정

# 코드 파일 수정
resource "local_file" "abc" {
content = "abc!"
filename = "${path.module}/abc.txt"
}

resource "local_file" "dev" {
  content  = "def!"
  filename = "${path.module}/def.txt"
}
# 실행 > 어떻게 되나요?
$ terraform apply
...
Enter a value: yes
...

# 확인
$ terraform state list
$ tree
.
├── abc.txt
├── def.txt
├── main.tf
├── terraform.tfstate
├── terraform.tfstate.backup
└── tfplan

1 directory, 6 files

$ ls *.txt
abc.txt def.txt

# 현재 배포된 리소스 확인
$ terraform state list
local_file.abc
local_file.dev


# 변경 이전의 실행 계획 적용 시도하면 아래와 같은 에러 ㅏㄹ생
$ terraform apply tfplan

Error: Saved plan is stale
│ 
│ The given plan file can no longer be applied because the state was changed by another operation after the plan was created.

다시 코드 내용 삭제

resource "local_file" "abc" {
content = "abc!"
filename = "${path.module}/abc.txt"
}
# 실행 > 어떻게 되나요?
$ terraform apply
...
Enter a value: yes
...
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

local_file.dev: Destroying... [id=15f946cb27f0730866cefea4f0923248d9366cb0]
local_file.dev: Destruction complete after 0s]


# 확인
$ terraform state list
local_file.abc

$ tree
.
├── abc.txt
├── main.tf
├── terraform.tfstate
├── terraform.tfstate.backup
└── tfplan

1 directory, 5 files

$ ls *.txt
abc.txt

destroy 제거 & fmt

# 
$ terraform destroy
...
Enter a value: yes
...

# 확인
$ terraform state list
$ ls *.txt
zsh: no matches found: *.txt


# 적용 후 코드 파일 내용 확인 -> 들여쓰기 확인
$ terraform fmt
# fmt : format 또는 reformat 줄임 표시로 terraform tmt 명령어로 수행, 테라폼 구성 파일을 표준 형식과 표준 스타일로 적용. 코드 가독성 높임

추가실습 : EC2 1대 배포 & 웹 서버 설정

  • 테라폼 방식 : immutable, 이뮤터블(인프라스트럭처의 상태를 변경할 때, 기존의 인프라스트럭처를 수정하거나 업데이트하는 것이 아니라 새로운 infrastructure를 생성하여 이전 상태의 인프라스트럭처를 교체하는 방식)

실습 목표 : EC2 1대를 배포하면서 userdata에 웹 서버 설정하여 간단한 애플리케이션 설정을 자동화합니다.

배포 : Ubuntu 22.04 LTS 사용 (ami-0c9c942bd7bf113a2)

# 각자 편한 디렉터리를 생성
$ cd ..
$ mkdir t101-1week-web
$ cd t101-1week-web

코드 파일 작성 : user_data 에 실행 명령어 작성

cat <<EOT > main.tf
provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_instance" "example" {
  ami                    = "ami-0c9c942bd7bf113a2"
  instance_type          = "t2.micro"

  user_data = <<-EOF
              #!/bin/bash
              echo "Hello, T101 Study" > index.html
              nohup busybox httpd -f -p 8080 &
              EOF

  tags = {
    Name = "terraform-Study-101"
  }
}
EOT

배포 실행

# init
$ terraform init

# plan
$ terraform plan
+ user_data                            = "d91ca31904077f0b641b5dd5a783401396ffbf3f"

# apply 실행
$ terraform apply -auto-approve

웹 서버 접속 시도 : 터미널3에서 실행

# [터미널3] 변수 지정
$ PIP=<각자 자신의 EC2 Public IP>
$ PIP=3.35.10.132
while true; do curl --connect-timeout 1  http://$PIP:8080/ ; echo "------------------------------"; date; sleep 1; done

문제 해결 : 웹 서버 접속

  • 코드 파일 수정 : 보안 그룹 생성 후 연동
$ cat <<EOT > main.tf
provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_instance" "example" {
  ami                    = "ami-0c9c942bd7bf113a2"
  instance_type          = "t2.micro"
  vpc_security_group_ids = [aws_security_group.instance.id]

  user_data = <<-EOF
              #!/bin/bash
              echo "Hello, T101 Study" > index.html
              nohup busybox httpd -f -p 8080 &
              EOF

  tags = {
    Name = "Single-WebSrv"
  }
}

resource "aws_security_group" "instance" {
  name = var.security_group_name

  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

variable "security_group_name" {
  description = "The name of the security group"
  type        = string
  default     = "terraform-example-instance"
}

output "public_ip" {
  value       = aws_instance.example.public_ip
  description = "The public IP of the Instance"
}
EOT

Error: with the retirement of EC2-Classic no new Security Groups can be created without referencing a VPC

│ with aws_security_group.instance,
│ on main.tf line 21, in resource “aws_security_group” “instance”:
│ 21: resource “aws_security_group” “instance” {

위와 같은 에러가 발생할 경우 아래와 같이 수정합니다 : 참고

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

}

resource "aws_default_vpc" "default" {
  tags = {
    Name = "Default VPC"
  }
}


resource "aws_instance" "example" {
  ami                    = "ami-0c9c942bd7bf113a2"
  instance_type          = "t2.micro"
  vpc_security_group_ids = [aws_security_group.instance.id]

  user_data = <<-EOF
              #!/bin/bash
              echo "Hello, T101 Study" > index.html
              nohup busybox httpd -f -p 8080 &
              EOF

  tags = {
    Name = "Single-WebSrv"
  }
}

resource "aws_security_group" "instance" {
  name = var.security_group_name

  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  vpc_id      = aws_default_vpc.default.id
  
}

variable "security_group_name" {
  description = "The name of the security group"
  type        = string
  default     = "terraform-example-instance"
}

output "public_ip" {
  value       = aws_instance.example.public_ip
  description = "The public IP of the Instance"
}

배포 실행

# plan/apply
$ terraform plan
$ terraform apply -auto-approve

# 모니터링 : EC2 정보와 curl 접속 확인


# (옵션) 리소스 생성 그래프 확인
$ terraform graph
digraph {
        compound = "true"
        newrank = "true"
        subgraph "root" {
                "[root] aws_default_vpc.default (expand)" [label = "aws_default_vpc.default", shape = "box"]
                "[root] aws_instance.example (expand)" [label = "aws_instance.example", shape = "box"]
                "[root] aws_security_group.instance (expand)" [label = "aws_security_group.instance", shape = "box"]
                "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" [label = "provider[\"registry.terraform.io/hashicorp/aws\"]", shape = "diamond"]
                "[root] var.security_group_name" [label = "var.security_group_name", shape = "note"]
                "[root] aws_default_vpc.default (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]"
                "[root] aws_instance.example (expand)" -> "[root] aws_security_group.instance (expand)"
                "[root] aws_security_group.instance (expand)" -> "[root] aws_default_vpc.default (expand)"
                "[root] aws_security_group.instance (expand)" -> "[root] var.security_group_name"
                "[root] output.public_ip (expand)" -> "[root] aws_instance.example (expand)"
                "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_instance.example (expand)"
                "[root] root" -> "[root] output.public_ip (expand)"
                "[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)"
        }
}

# graph 확인 > 파일 선택 후 오른쪽 상단 DOT 클릭
$ terraform graph > graph.dot

리소스 삭제

# 리소스 삭제
$ terraform destroy -auto-approve

2. HCL

  • HCL HashiCorp Configuration Language은 하시코프사에서 IaC와 구성 정보를 명시하기 위해 개발된 오픈 소스 도구

HCL 표현식

// 한줄 주석 방법1
# 한줄 주석 방법2

/*
라인
주석
*/

locals {
  key1     = "value1"     # = 를 기준으로 키와 값이 구분되며
  myStr    = "TF ♡ UTF-8" # UTF-8 문자를 지원한다.
  multiStr = <<EOF
  Multi
  Line
  String
  with anytext
EOF

  boolean1    = true   # boolean true
  boolean2    = false  # boolean false를 지원한다.
  deciaml     = 123    # 기본적으로 숫자는 10진수,
  octal       = 0123   # 0으로 시작하는 숫자는 8진수,
  hexadecimal = "0xD5" # 0x 값을 포함하는 스트링은 16진수,
  scientific  = 1e10   # 과학표기 법도 지원한다.

  # funtion 호출 예
  myprojectname = format("%s is myproject name", var.project)

  # 3항 연산자 조건문을 지원한다.
  credentials = var.credentials == "" ? file(var.credentials_file) : var.credentials
}
  • 테라폼으로 인프라를 구성하기 위한 선언 블록도 다수 존재 : terraform, resource, data, variable, local, output

3. 테라폼 블록

테라폼 블록 : 테라폼 구성을 명시하는 데 사용

  • 테라폼 버전이나 프로바이더 버전과 같은 값들은 자동으로 설정되지만, 함께 작업할 때는 버전을 명시적으로 선언하고 필요한 조건을 입력하여 실행 오류를 최소화 할 것을 권장 [Docs] [Docs 예제]
  • 버전 체계는 시맨틱 버전 관리 Semantic Versioning(SemVer) 방식을 따른다 [참고], [Kubernetes Release Versioning]
terraform {
  required_version = "~> 1.3.0" # 테라폼 버전

  required_providers { # 프로바이더 버전을 나열
    random = {
      version = ">= 3.0.0, < 3.1.0"
    }
    aws = {
      version = "4.2.0"
    }
  }

  cloud { # Cloud/Enterprise 같은 원격 실행을 위한 정보 [참고: Docs]
    organization = "<MY_ORG_NAME>"
    workspaces {
      name = "my-first-workspace"
    }
  }

  backend "local" { # state를 보관하는 위치를 지정 [참고: Docs, local, remote, s3]
    path = "relative/path/to/terraform.tfstate"
  }
}

테라폼 버전 : required_version

프로바이더 버전

  • 테라폼 0.13 버전 이전에는 provider 블록에 함께 버전을 명시했지만 해당 버전 이후 프로바이더 버전은 terraform 블록에서 required_providers에 정의
  • 코드 파일 수정 : 프로바이더 버전을 과하게 높게 설정 — 링크
terraform {
  required_version = ">= 1.0.0"
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = ">=10000.0.0"
    }
  }
}

resource "local_file" "abc" {
  content  = "123!"
  filename = "${path.module}/abc.txt"
}

cloud 블록

  • Terraform Cloud, Terraform Enterprise는 CLI, VCS, API 기반의 실행 방식을 지원하고 cloud 블록으로 선언

백엔드 블록

  • 백엔드 블록의 구성은 테라폼 실행 시 저장되는 State(상태 파일)의 저장 위치를 선언 (기본: local)
  • 주의할 점은 하나의 백엔드만 허용한다는 점
  • State에는 외부로 노출되면 안 되는 패스워드 또는 인증서 정보 같은 민감한 데이터들이 포함될 수 있으므로 State의 접근 제어 및 안전한 관리방안 대책수립이 필요

4. 리소스

리소스 구성 : 리소스 블록은 선언된 항목을 생성하는 동작을 수행

종속성

  • 테라폼 종속성은 resource, module 선언으로 프로비저닝되는 각 요소의 생성 순서를 구분함
  • 기본적으로 다른 리소스에서 값을 참조해 불러올 경우 생성 선후 관계에 따라 작업자가 의도하지는 않았지만 자동으로 연관 관계가 정의되는 암시적 종속성을 갖게 되고, 강제로 리소스 간 명시적 종속성을 부여할 경우에는 메타인수인 depends_on을 활용함 — [참고: Docs]
  • 리소스 참조값을 설정해 두 개의 리소스 간 암시적 종속성 부여
  • 리소스의 속성을 주입하지 않아도 두 리소스 간에 종속성이 필요한 경우에, depends_on 선언으로 적용 가능(명시적 종속성 부여)

리소스 속성 참조

  • 리소스 구성에서 참조 가능한 값은 인수 속성
  • 인수 : 리소스 생성 시 사용자가 선언하는 값
  • 속성 : 사용자가 설정하는 것은 불가능하지만 리소스 생성 이후 획득 가능한 리소스 고유 값

수명주기

  • lifecycle은 리소스의 기본 수명주기를 작업자가 의도적으로 변경하는 메타인수다. 메타인수 내에는 아래 선언이 가능.
  • create_before_destroy (bool): 리소스 수정 시 신규 리소스를 우선 생성하고 기존 리소스를 삭제
  • prevent_destroy (bool): 해당 리소스를 삭제 Destroy 하려 할 때 명시적으로 거부
  • ignore_changes (list): 리소스 요소에 선언된 인수의 변경 사항을 테라폼 실행 시 무시
  • precondition: 리소스 요소에 선언해 인수의 조건을 검증
  • postcondition: Plan과 Apply 이후의 결과를 속성 값으로 검증