T101-4주차 [Terraform 101 Study]

cloudNet@ 팀의 가시다 님이 진행하는 테라폼 102 스터디 3주차 정리입니다. (테라폼으로 시작하는 IaC” (한빛미디어) 도서로 진행!)

State

  • 상태 파일은 배포할 때마다 변경되는 프라이빗 API private API로, 오직 테라폼 내부에서 사용하기 위한 것입니다.
  • 테라폼 상태 파일직접 편집하거나 직접 읽는 코드로 작성해서는 안됩니다.

팀 단위에서 테라폼 운영 시 문제점

  1. 상태 파일을 저장하는 공유 스토리지 Shared storage for state files
  • 각 팀원이 동일한 테라폼 상태 파일 사용을 위해서, 공유 위치에 저장이 필요

2. 상태 파일 잠금 Locking state files

  • 잠금 기능 없이 두 팀원이 동시에 테라폼 실행 시 여러 테라폼 프로세스가 상태 파일을 동시에 업데이트하여 충돌 가능(경쟁 상태 race condition)

3. 상태 파일 격리 Isolating state files

  • 예를 들면 테스트 dev 와 검증 stage 과 상용 prodction 각 환경에 대한 격리가 필요

상태 파일 공유로 버전 관리 시스템 비추천 (Git 같은 SCM)

  1. 수동 오류 Manual error
  • 테라폼을 실행하기 전에 최신 변경 사항을 가져오거나 실행하고 나서 push 하는 것을 잊기 쉽습니다.
  • 팀의 누군가가 이전 버전의 상태 파일로 테라폼을 실행하고, 그 결과 실수로 이전 버전으로 롤백하거나 이전에 배포된 인프라를 복제하는 문제가 발생 할 수 있음.

2. 잠금 Locking

  • 대부분의 버전 관리 시스템은 여러 명의 팀 구성원이 동시에 하나의 상태 파일에 terraform apply 명령을 실행하지 못하게 하는 잠금 기능이 제공되지 않음.

3. 시크릿 Secrets

  • 테라폼 상태 파일의 모든 데이터는 평문으로 저장됨. 민감 정보가 노출될 위험.

지원되는 원격 백엔드

AWS S3, Azure Blob Storage, Google Cloud Storage, Consul, Postgres database 등

https://developer.hashicorp.com/terraform/language/settings/backends/local

그중에서도 S3 + DynamoDB 권장 합니다

  • 수동 오류 해결 : plan/apply 실행 시 마다 해당 백엔드에서 파일을 자동을 로드, apply 후 상태 파일을 백엔드 에 자동 저장
  • 잠금 : apply 실행 시 테라폼은 자동으로 잠금을 활성화, -lock-timout=<TIME> 로 대기 시간 설정 지정 가능
  • 시크릿 : 대부분 원격 백엔드는 기본적으로 데이터를 보내거나 상태 파일을 저장할 때 암호화하는 기능을 지원

그림출처 https://github.com/binbashar/terraform-aws-tfstate-backend

  • s3 버킷 만들기 + DynamoDB 만들기
# AWS 프로바이더 설정
provider "aws" {
  # 리전 'ap-northeast-2'(서울) 설정
  region = "ap-northeast-2"
}

# S3 버킷 생성 리소스 정의
resource "aws_s3_bucket" "mys3bucket" {
  # 버킷 이름 설정(minhotest-t101study-tfstate 예시)
  bucket = "minhotest-t101study-tfstate"
}

# S3 버킷 버전 관리 리소스 정의
resource "aws_s3_bucket_versioning" "mys3bucket_versioning" {
  # 생성된 버킷의 아이디 사용
  bucket = aws_s3_bucket.mys3bucket.id
  # 버전 관리 구성 블록
  versioning_configuration {
    # 버전 관리 활성화
    status = "Enabled"
  }
}

# 출력 값(S3 버킷 ARN) 정의
output "s3_bucket_arn" {
  # 생성된 버킷의 ARN 사용
  value       = aws_s3_bucket.mys3bucket.arn
  # 출력 값 설명 설정
  description = "The ARN of the S3 bucket"
}

# DynamoDB 테이블 생성 리소스 정의
resource "aws_dynamodb_table" "mydynamodbtable" {
  # 테이블 이름 설정
  name         = "terraform-locks"
  # 결제 방식 설정: PAY_PER_REQUEST (요청 당 지불)
  billing_mode = "PAY_PER_REQUEST"
  # 해시 키 설정
  hash_key     = "LockID"

  # 속성 설정
  attribute {
    # 속성 이름 설정
    name = "LockID"
    # 속성 유형 설정: 'S' (문자열 저장)
    type = "S"
  }
}

# 출력 값(DynamoDB 테이블 이름) 정의
output "dynamodb_table_name" {
  # 생성된 테이블의 이름 사용
  value       = aws_dynamodb_table.mydynamodbtable.name
  # 출력 값 설명 설정
  description = "The name of the DynamoDB table"
}
terraform init && terraform apply
확인 후 yes 입력

위와 같이 S3 와 DynamoDB 확인을 하였습니다. 그다음 신규 디렉토리 만든 후 테라폼을 이용하여 test 하겠습니다.

main.tf
==
provider "aws" { 
  region = "ap-northeast-2"
}

#aws 인스턴스를 배포하겠다.
resource "aws_instance" "example" {
  ami                    = "ami-0c9c942bd7bf113a2"
  instance_type          = "t2.micro"

#유저데이타 ec2 최초 프로비저닝시 실행할 명령어 
  user_data = <<-EOF
              #!/bin/bash
              echo "Hello minho, T101 Study" > index.html
              nohup busybox httpd -f -p 8080 &
              EOF

  tags = {
    Name = "terraform-Study-101"
  }
}
==
terraform init && terraform apply

다른 디렉토리에 만든 후 init 및 apply 진행하면 로컬에 저장되는 것을 알수가 있습니다

하지만 backend를 사용하여 테라폼 init 진행 하게 되면 s3에 tf state들을 담게 됩니다.

cat <<EOT > backend.tf
# Terraform 설정
terraform {
  # AWS S3를 백엔드로 사용하여 상태 파일을 저장하는 구성
  backend "s3" {
    # 상태 파일을 저장할 S3 버킷의 이름 (예: '$NICKNAME-t101study-tfstate')
    bucket = "minhotest-t101study-tfstate"
    
    # 상태 파일 저장될 디렉토리와 파일 이름 (예: '3.2/terraform.tfstate')
    key    = "3.2/terraform.tfstate"
    
    # 상태 파일이 저장될 AWS 리전 (예: 'ap-northeast-2'(서울))
    region = "ap-northeast-2"
    
    # 상태 파일 잠금을 위해 사용할 DynamoDB 테이블 이름 (예: 'terraform-locks')
    # 실행 중 동시 업데이트를 방지하기 위한 목적으로 활용
    dynamodb_table = "terraform-locks"
    
    # (주석 처리된 코드) 상태 파일의 암호화를 설정하는 부분
    # encrypt 값을 'true'로 설정하면, 상태 파일을 서버 측 암호화로 암호화
    # encrypt        true
  }
}
EOT

Terraform 설정을 사용하여 AWS S3 백엔드로 상태 파일을 저장하고 관리하도록 지시하며, 동시 업데이트를 방지하기 위해 DynamoDB를 통해 잠금을 관리하도록 설정합니다. 이러한 설정은 코드의 중앙화된 저장소로서 작동하게 하고, 프로젝트 참여자들이 쉽게 협업할수 있도록 합니다.

# S3 버킷에 파일 확인
# DynamoDB에 LockID 확인
terraform init && terraform apply

만약 테라폼 코드를 수정할경우 S3에서 확인은?

위 이미지 처럼 버전 표시를 통하여 각 코드 버전을 확인 이 가능하다.

실습 완료되었다면 삭제 하자

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

# S3 버킷에 객체 삭제
aws s3 rm s3://$NICKNAME-t101study-tfstate --recursive

# S3 버킷에 버저닝 객체 삭제 
aws s3api delete-objects \
    --bucket $NICKNAME-t101study-tfstate \
    --delete "$(aws s3api list-object-versions \
    --bucket "${NICKNAME}-t101study-tfstate" \
    --output=json \
    --query='{Objects: Versions[].{Key:Key,VersionId:VersionId}}')"

# S3 버킷에 삭제마커 삭제
aws s3api delete-objects --bucket $NICKNAME-t101study-tfstate \
    --delete "$(aws s3api list-object-versions --bucket "${NICKNAME}-t101study-tfstate" \
    --query='{Objects: DeleteMarkers[].{Key:Key,VersionId:VersionId}}')"

# 백엔드 리소스 삭제
tfstate-backend$ terraform destroy -auto-approve

State 동기화

https://kschoi728.tistory.com/135

테라폼 구성과 State 흐름 : Plan 과 Apply 중 각 리소스에 발생할 수 있는 네 가지 사항, 아래 실행 계획 출력 기호와 의미 합니다.

– Replace 동작은 기본값을 삭제 후 생성하지만 lifecycle의 create_before_destroy 옵션을 통해 생성 후 삭제 설정 가능 합니다.

유형 별 실습

유형1 : 신규 리소스 정의 → Apply ⇒ 리소스 생성

# 선언할 로컬 변수를 지정하는 locals 블록
locals {
  # 로컬 변수 'name'을 'mytest' 값으로 설정
  name = "mytest"
}

# 첫 번째 IAM 사용자 리소스 생성
resource "aws_iam_user" "myiamuser1" {
  # 이름은 로컬 변수 'name'에 '1'을 추가한 문자열로 설정 (예: "mytest1")
  name = "${local.name}1"
}

# 두 번째 IAM 사용자 리소스 생성
resource "aws_iam_user" "myiamuser2" {
  # 이름은 로컬 변수 'name'에 '2'를 추가한 문자열로 설정 (예: "mytest2")
  name = "${local.name}2"
}
# 
terraform init && terraform apply -auto-approve
terraform state list
terraform state show aws_iam_user.myiamuser1

#
ls *.tfstate
==
aws_iam_user.myiamuser1
aws_iam_user.myiamuser2
==
cat terraform.tfstate | jq

#한번더 할경우 backup 파일이 생성된다 
terraform apply -auto-approve
ls -al
==
-rw-r--r-- 1 root  root  1544 Jul 24 23:06 terraform.tfstate
-rw-r--r-- 1 root  root  1548 Jul 24 23:06 terraform.tfstate.backup
==

# iam 사용자 리스트 확인
aws iam list-users | jq

유형2 : 실제 리소스 수동 제거 → Apply ⇒ 리소스 생성

# 실제 리소스 수동 제거
aws iam delete-user --user-name mytest1
aws iam delete-user --user-name mytest2
aws iam list-users | jq

# 아래 명령어 실행 결과 차이는?
terraform plan
==
+ create

Terraform will perform the following actions:

  # aws_iam_user.myiamuser1 will be created
  + resource "aws_iam_user" "myiamuser1" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "mytest1"
      + path          = "/"
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
    }

  # aws_iam_user.myiamuser2 will be created
  + resource "aws_iam_user" "myiamuser2" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "mytest2"
      + path          = "/"
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
    }

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

terraform plan -refresh=false
==
#아무런 변화가 없다.
No changes. Your infrastructure matches the configuration.   
==
cat terraform.tfstate | jq .serial

#
terraform apply -auto-approve
terraform state list
cat terraform.tfstate | jq .serial

# iam 사용자 리스트 확인
aws iam list-users | jq

정리!

refresh를 안할경우 -> 로컬의 tf 코드와 state 정보로 리소스 유무 체크

refresh 할 경우 -> state와 실제 리소스 (aws)의 정보와 비교하여 상태 체크

결론 : 세 가지가 삼위일체 필요!

-구성파일(.tf)

-상태파일(.tfstate)

-실제 리소스

유형3 : Apply → Apply ← 코드, State, 형상 모두 일치한 경우

#
terraform apply -auto-approve
cat terraform.tfstate | jq .serial
terraform apply -auto-approve
cat terraform.tfstate | jq .serial
terraform apply -auto-approve
cat terraform.tfstate | jq .serial

유형4 : 코드에서 일부 리소스 삭제 → Apply

main.tf 파일 수정

locals {
  name = "mytest"
}

resource "aws_iam_user" "myiamuser1" {
  name = "${local.name}1"
}
# 
terraform apply -auto-approve
==
# aws_iam_user.myiamuser2 will be destroyed
  # (because aws_iam_user.myiamuser2 is not in configuration)
  - resource "aws_iam_user" "myiamuser2" {
      - arn           = "arn:aws:iam::373523363156:user/mytest2" -> null
      - force_destroy = false -> null
      - id            = "mytest2" -> null
      - name          = "mytest2" -> null
      - path          = "/" -> null
      - tags          = {} -> null
      - tags_all      = {} -> null
      - unique_id     = "AIDAVN552GVKAW2NAD325" -> null
    }
==

terraform state list
terraform state show aws_iam_user.myiamuser1

#
ls *.tfstate
cat terraform.tfstate | jq

# iam 사용자 리스트 확인
aws iam list-users | jq

유형6 : 실수로 tfstate 파일 삭제 → plan/apply

# 실수로 tfstate 파일 삭제
rm -rf terraform.tfstate*

#
terraform plan
==
+ create

Terraform will perform the following actions:

  # aws_iam_user.myiamuser1 will be created
  + resource "aws_iam_user" "myiamuser1" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "mytest1"
      + path          = "/"
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
==

terraform plan -refresh=false
==
+ create

Terraform will perform the following actions:

  # aws_iam_user.myiamuser1 will be created
  + resource "aws_iam_user" "myiamuser1" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "mytest1"
      + path          = "/"
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
    }
==

#
terraform apply -auto-approve
==
│ Error: creating IAM User (mytest1): EntityAlreadyExists: User with name mytest1 already exists.
│       status code: 409, request id: bd635bcb-802e-4af3-a32a-550340c6b1cd
│ 
│   with aws_iam_user.myiamuser1,
│   on main.tf line 5, in resource "aws_iam_user" "myiamuser1":
│    5: resource "aws_iam_user" "myiamuser1" {
==

terraform state list
cat terraform.tfstate | jq

# iam 사용자 리스트 확인
aws iam list-users | jq

# 다음 실습을 위해 iam user 삭제
aws iam delete-user --user-name mytest1

⇒ 위 상황에서 복구? 하는 방법은? import 등 방법이 있습니다

워크스페이스

https://kschoi728.tistory.com/136

  • 테라폼 구성 파일은 동일하지만 작업자는 서로 다른 State를 갖는 실제 대상을 프로비저닝할 수 있다.
  • 워크스페이스는 기본 default로 정의된다. 로컬 작업 환경의 워크스페이스 관리를 위한 CLI 명령어로 workspace가 있다.
terraform workspace list
* default

실습을 위해서 5.3 디렉터리를 신규 생성 후 열기 → main.tf 파일 생성

mkdir 5.3 && cd 5.3
# AWS 인스턴스 리소스 생성
			#프로바이더_유형    이름
resource "aws_instance" "mysrv1" {
  # 사용할 AMI(애플리케이션 머신 이미지)의 ID
  ami = "ami-0ea4d4b8dc1e46212"

  # 인스턴스 유형 설정 (여기서는 t2.micro)
  instance_type = "t2.micro"

  # 인스턴스에 태그 설정
  tags = {
    # 인스턴스의 이름 태그 설정 (예: "t101-week4")
    Name = "t101-week4"
  }
}

실행

# [분할/터미널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

#
terraform init && terraform apply -auto-approve
terraform state list
==
aws_instance.mysrv1
==

#
cat terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
cat terraform.tfstate | jq -r '.resources[0].instances[0].attributes.private_ip'

# terraform.tfstate에 private 담긴 내용은?
cat terraform.tfstate | jq -r '.resources[0].instances[0].private' | base64 -d | jq
==
{
  "e2bfb730-ecaa-11e6-8f88-34363bc7c4c0": {
    "create": 600000000000,
    "delete": 1200000000000,
    "update": 600000000000
  },
  "schema_version": "1"
}
==

# 워크스페이스 확인
terraform workspace list

# graph 확인
terraform graph > graph.dot

해석

e2bfb730-ecaa-11e6-8f88-34363bc7c4c0**:

◦ Terraform에서 생성한 고유한 리소스 식별자입니다.

◦ 이 객체에는 리소스의 수명 주기에 대한 정보와 관련된 시간 값들이 포함되어 있습니다.

◦ create: 리소스를 생성할 때 사용된 시간 (600,000,000,000 나노초, 대략 10분).

◦ delete: 리소스를 삭제할 때 사용된 시간 (1,200,000,000,000 나노초, 대략 20분).

◦ update: 리소스를 업데이트할 때 사용된 시간 (600,000,000,000 나노초, 대략 10분).

schema_version:

◦ 파일의 스키마 버전을 나타냅니다. 여기서는 버전 1입니다.

◦ 이 버전 값은 Terraform 내부에서 리소스 구성의 데이터 구조를 추적하는 데 사용됩니다.

신규 워크스페이스 생성 및 확인

# 새 작업 공간 workspace 생성 : mywork1
terraform workspace new mywork1
terraform workspace show

# 서브 디렉터리 확인
tree terraform.tfstate.d
terraform.tfstate.d
└── mywork1

# plan 시 어떤 결과 내용이 출력되나요?
terraform plan

# apply 해보자!
terraform apply -auto-approve


# 워크스페이스 확인
terraform workspace list

#
cat terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
cat terraform.tfstate.d/mywork1/terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'

# graph 확인
terraform graph > graph.dot


# 새 작업 공간 workspace 생성 : mywork2
terraform workspace new mywork2

# 서브 디렉터리 확인
tree terraform.tfstate.d
...

# plan & apply
terraform plan && terraform apply -auto-approve
cat terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
cat terraform.tfstate.d/mywork1/terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
cat terraform.tfstate.d/mywork2/terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'

# workspace 정보 확인
terraform workspace show
terraform workspace list

# 실습 리소스 삭제
terraform workspace select default
terraform destroy -auto-approve
terraform workspace select mywork1
terraform destroy -auto-approve
terraform workspace select mywork2
terraform destroy -auto-approve
# AWS 인스턴스 생성에 관한 테라폼 코드
resource "aws_instance" "web" {
  # 작업 공간이 'default'일 경우, 5개의 인스턴스를 생성하고 그렇지 않은 경우 1개의 인스턴스를 생성
  count         = "${terraform.workspace == "default" ? 5: 1}"

  # 생성할 때 사용할 Amazon Machine Image (AMI) ID 설정
  ami           = "ami-0ea4d4b8dc1e46212"

  # 인스턴스 유형 설정
  instance_type = "t2.micro"

  # 리소스에 태그 정보 추가
  tags = {
    # 인스턴스 이름 설정 ('t101-' + 작업 공간 이름)
    Name = "t101- ${terraform.workspace}"
  }
}
  • 장점
  • 하나의 루트 모듈에서 다른 환경을 위한 리소스를 동일한 테라폼 구성으로 프로비저닝하고 관리
  • 기존 프로비저닝된 환경에 영향을 주지 않고 변경 사항 실험 가능
  • 깃의 브랜치 전략처럼 동일한 구성에서 서로 다른 리소스 결과 관리

  • 단점
  • State가 동일한 저장소(로컬 또는 백엔드)에 저장되어 State 접근 권한 관리가 불가능
  • 모든 환경이 동일한 리소스를 요구하지 않을 수 있으므로 테라폼 구성에 분기 처리가 다수 발생 가능
  • 프로비저닝 대상에 대한 인증 요소를 완벽히 분리하기 어려움
  • → 가장 큰 단점은 완벽한 격리가 불가능
  • ⇒ 해결하기 위해 루트 모듈을 별도로 구성하는 디렉터리 기반의 레이아웃을 사용할 수 있다 or Terraform Cloud 환경의 워크스페이스를 활용

Module

  • 테라폼으로 인프라와 서비스를 관리하면 시간이 지날수록 구성이 복잡해지고 관리하는 리소스가 늘어나게 된다. 테라폼의 구성 파일과 디렉터리 구성에는 제약이 없기 때문에 단일 파일 구조상에서 지속적으로 업데이트할 수 있지만, 다음과 같은 문제가 발생한다.
  1. 테라폼 구성에서 원하는 항목을 찾고 수정하는 것이 점점 어려워짐
  2. 리소스들 간의 연관 관계가 복잡해질수록 변경 작업의 영향도를 분석하기 위한 노력이 늘어남
  3. 개발/스테이징/프로덕션 환경으로 구분된 경우 비슷한 형태의 구성이 반복되어 업무 효율이 줄어듬
  4. 새로운 프로젝트를 구성하는 경우 기존 구성에서 취해야 할 리소스 구성과 종속성 파악이 어려움

모듈은 루트 모듈과 자식 모듈로 구분됩니다.

  • 루트 모듈 (Root Module) : 테라폼을 실행하고 프로비저닝하는 최상위 모듈
  • 자식 모듈 (Chile Module) : 루트 모듈의 구성에서 호출되는 외부 구성 집합

모듈 사용의 장점

  • 관리성 : 모듈은 서로 연관있는 구성의 묶음이다. 원하는 구성요소를 단위별로 쉽게 찾고 업데이트할 수 있다. 모듈은 다른 구성에서 쉽게 하나의 덩어리로 추가하거나 삭제할 수 있다. 또한 모듈이 업데이트되면 이 모듈을 사용하는 모든 구성에서 일관된 변경 작업을 진행할 수 있다.
  • 캡슐화 : 테라폼 구성 내에서 각 모듈은 논리적으로 묶여져 독립적으로 프로비저닝 및 관리되며, 그 결과는 은닉성을 갖춰 필요한 항목만을 외부에 노출시킨다.
  • 재사용성 : 구성을 처음부터 작성하는 것에는 시간과 노력이 필요하고 작성 중간에 디버깅과 오류를 수정하는 반복 작업이 발생한다. 테라폼 구성을 모듈화하면 이후에 비슷한 프로비저닝에 이미 검증된 구성을 바로 사용할 수 있다.
  • 일관성과 표준화 : 테라폼 구성 시 모듈을 활용하는 워크플로는 구성의 일관성을 제공하고 서로 다른 환경과 프로젝트에도 이미 검증한 모듈을 적용해 복잡한 구성과 보안 사고를 방지할 수 있다.

모듈 작성의 원칙

기본 원칙 : 모듈은 대부분의 프로그래밍 언어에서 쓰이는 라이브러리나 패키지와 역할이 비슷하다

  • 모듈 디렉터리 형식을 terraform-<프로바이더 이름>-<모듈 이름> 형식을 제안한다. 이 형식은 Terraform Cloud, Terraform Enterprise에서도 사용되는 방식으로 1) 디렉터리 또는 레지스트리 이름이 테라폼을 위한 것이고, 2) 어떤 프로바이더의 리소스를 포함하고 있으며, 3) 부여된 이름이 무엇인지 판별할 수 있도록 한다.
  • 테라폼 구성은 궁극적으로 모듈화가 가능한 구조로 작성할 것을 제안한다. 처음부터 모듈화를 가정하고 구성파일을 작성하면 단일 루트 모듈이라도 후에 다른 모듈이 호출할 것을 예상하고 구조화할 수 있다. 또한 작성자는 의도한 리소스 묶음을 구상한 대로 논리적인 구조로 그룹화할 수 있다.
  • 각각의 모듈을 독립적으로 관리하기를 제안한다. 리모트 모듈을 사용하지 않더라도 처음부터 모듈화가 진행된 구성들은 떄로 루트 모듈의 하위 파일 시스템에 존재하는 경우가 있다. 하위 모듈 또한 독립적인 모듈이므로 루트 모듈 하위에 두기보다는 동일한 파일 시스템 레벨에 위치하거나 별도 모듈만을 위한 공간에서 불러오는 것을 권장한다. 이렇게 하면 VCS를 통해 관리하기가 더 수월하다.
  • 공개된 테라폼 레지스트리의 모듈을 참고하기를 제안한다. 대다수의 테라폼 모듈은 공개된 모듈이 존재하고 거의 모든 인수에 대한 변수 처리, 반복문 적용 리소스, 조건에 따른 리소스 활성/비활성 등을 모범 사례로 공개해두었다. 물론 그대로 가져다 사용하는 것보다는 프로비저닝하려는 상황에 맞게 참고하는 것을 권장한다.
  • 작성된 모듈은 공개 또는 비공개로 게시해 팀 또는 커뮤니티와 공유하기를 제안한다. 모듈의 사용성을 높이고 피드백을 통해 더 발전된 모듈을 구성할 수 있는 자극이 된다.
  • 모듈을 독립적으로 관리하기 위해 디렉터리 구조를 생성할 때 모듈을 위한 별도 공간을 생성하는 방식으로 진행한다. 특정 루트 모듈 하위에 자식 모듈을 구성하는 경우 단순히 복잡한 코드를 분리하는 요도로 명시되며 종속성이 발생하므로 루트 모듈 사이에 모듈 디렉터리를 지정한다.

모듈 실습 해보기

mkdir -p 06-module-traning/modules/terraform-random-pwgen
# main.tf
resource "random_pet" "name" {
  keepers = {
    ami_id = timestamp()
  }
}

resource "random_password" "password" {
  length           = var.isDB ? 16 : 10
  special          = var.isDB ? true : false
  override_special = "!#$%*?"
}

실행 : 자식 모듈 동작 테스트

#
cd 06-module-traning/modules/terraform-random-pwgen

#
ls *.tf
terraform init && terraform plan
==
+ create

Terraform will perform the following actions:

  # random_password.password will be created
  + resource "random_password" "password" {
      + bcrypt_hash      = (sensitive value)
      + id               = (known after apply)
      + length           = 10
      + lower            = true
      + min_lower        = 0
      + min_numeric      = 0
      + min_special      = 0
      + min_upper        = 0
      + number           = true
      + numeric          = true
      + override_special = "!#$%*?"
      + result           = (sensitive value)
      + special          = false
      + upper            = true
    }

  # random_pet.name will be created
  + resource "random_pet" "name" {
      + id        = (known after apply)
      + keepers   = {
          + "ami_id" = (known after apply)
        }
      + length    = 2
      + separator = "-"
    }

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

Changes to Outputs:
  + id = (known after apply)
  + pw = (known after apply)
==

# 테스트를 위해 apply 시 변수 지정
terraform apply -auto-approve -var=isDB=true
==
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:

  # random_password.password will be created
  + resource "random_password" "password" {
      + bcrypt_hash      = (sensitive value)
      + id               = (known after apply)
      + length           = 16
      + lower            = true
      + min_lower        = 0
      + min_numeric      = 0
      + min_special      = 0
      + min_upper        = 0
      + number           = true
      + numeric          = true
      + override_special = "!#$%*?"
      + result           = (sensitive value)
      + special          = true
      + upper            = true
    }

  # random_pet.name will be created
  + resource "random_pet" "name" {
      + id        = (known after apply)
      + keepers   = {
          + "ami_id" = (known after apply)
        }
      + length    = 2
      + separator = "-"
    }

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

Changes to Outputs:
  + id = (known after apply)
  + pw = (known after apply)
random_pet.name: Creating...
random_pet.name: Creation complete after 0s [id=assuring-quetzal]
random_password.password: Creating...
random_password.password: Creation complete after 0s [id=none]

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

Outputs:

id = "assuring-quetzal"
pw = "4vyg?1Y7XY!E%*rQ"
==

# 확인
terraform state list
==
random_password.password
random_pet.name
==

terraform state show random_pet.name
==
# random_pet.name:
resource "random_pet" "name" {
    id        = "assuring-quetzal"
    keepers   = {
        "ami_id" = "2023-07-25T13:56:45Z"
    }
			#2개의 단어 사용됨
    length    = 2
    separator = "-"
}
==

terraform state show random_password.password
==
# random_password.password:
resource "random_password" "password" {
    bcrypt_hash      = (sensitive value)
    id               = "none"
    length           = 16
    lower            = true
    min_lower        = 0
    min_numeric      = 0
    min_special      = 0
    min_upper        = 0
    number           = true
    numeric          = true
    override_special = "!#$%*?"
    result           = (sensitive value)
    special          = true
    upper            = true
}
==

# tfstate에 모듈 정보 확인
cat terraform.tfstate | grep module

자식 모듈 호출 실습

  • 다수의 리소스를 같은 목적으로 여러 번 반복해서 사용하려면 리소스 수만큼 반복해 구성 파일을 정의해야 하고 이름도 고유하게 설정해줘야 하는 부담이 있지만, 모듈을 활용하면 반복되는 리소스 묶음을 최소화할 수 있다.
  • 디렉터리 생성 및 06-module-traning/06-01-basic/main.tf 파일 생성
mkdir -p 06-module-traning/06-01-basic
   #모듈이름
module "mypw1" {
						#리소스가 아닌 모듈이다 즉 모듈로 사용한다.
				#..이니 06-module-traning올라감
  source = "../modules/terraform-random-pwgen"
}

module "mypw2" {
  source = "../modules/terraform-random-pwgen"
  isDB   = true
}

output "mypw1" {
  value  = module.mypw1
}

output "mypw2" {
  value  = module.mypw2
}

실행 : 자식 모듈을 호출해 반복 재사용하는 루트 모듈의 결과

#
cd 06-module-traning/06-01-basic

#
terraform init && terraform plan && terraform apply -auto-approve
==
+ create

Terraform will perform the following actions:

  # module.mypw1.random_password.password will be created
  + resource "random_password" "password" {
      + bcrypt_hash      = (sensitive value)
      + id               = (known after apply)
      + length           = 10
      + lower            = true
      + min_lower        = 0
      + min_numeric      = 0
      + min_special      = 0
      + min_upper        = 0
      + number           = true
      + numeric          = true
      + override_special = "!#$%*?"
      + result           = (sensitive value)
      + special          = false
      + upper            = true
    }

  # module.mypw1.random_pet.name will be created
  + resource "random_pet" "name" {
      + id        = (known after apply)
      + keepers   = {
          + "ami_id" = (known after apply)
        }
      + length    = 2
      + separator = "-"
    }

  # module.mypw2.random_password.password will be created
  + resource "random_password" "password" {
      + bcrypt_hash      = (sensitive value)
      + id               = (known after apply)
      + length           = 16
      + lower            = true
      + min_lower        = 0
      + min_numeric      = 0
      + min_special      = 0
      + min_upper        = 0
      + number           = true
      + numeric          = true
      + override_special = "!#$%*?"
      + result           = (sensitive value)
      + special          = true
      + upper            = true
    }

  # module.mypw2.random_pet.name will be created
  + resource "random_pet" "name" {
      + id        = (known after apply)
      + keepers   = {
          + "ami_id" = (known after apply)
        }
      + length    = 2
      + separator = "-"
    }

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

Changes to Outputs:
  + mypw1 = {
      + id = (known after apply)
      + pw = (known after apply)
    }
  + mypw2 = {
      + id = (known after apply)
      + pw = (known after apply)
    }
module.mypw1.random_pet.name: Creating...
module.mypw2.random_pet.name: Creating...
module.mypw1.random_pet.name: Creation complete after 0s [id=cunning-cheetah]
module.mypw1.random_password.password: Creating...
module.mypw2.random_pet.name: Creation complete after 0s [id=rational-stork]
module.mypw2.random_password.password: Creating...
module.mypw1.random_password.password: Creation complete after 0s [id=none]
module.mypw2.random_password.password: Creation complete after 0s [id=none]

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

Outputs:

mypw1 = {
  "id" = "cunning-cheetah"
  "pw" = "fEyfNBXqL8"
}
mypw2 = {
  "id" = "rational-stork"
  "pw" = "QCrnlezWep!%jx9s"
}
==

# 확인
terraform state list
==
dule.mypw1.random_password.password
module.mypw1.random_pet.name
module.mypw2.random_password.password
module.mypw2.random_pet.name
==

# tfstate에 모듈 정보 확인
cat terraform.tfstate | grep module
==
			"module": "module.mypw1",
      "module": "module.mypw1",
      "module": "module.mypw2",
      "module": "module.mypw2",
==

# terraform init 시 생성되는 modules.json 파일 확인
tree .terraform
==
.terraform
├── modules
│   └── modules.json
└── providers
    └── registry.terraform.io
        └── hashicorp
            └── random
                └── 3.5.1
                    └── linux_amd64
                        └── terraform-provider-random_v3.5.1_x5
==

## 모듈로 묶여진 리소스는 module이라는 정의를 통해 단순하게 재활용하고 반복 사용할 수 있다.
## 모듈의 결과 참조 형식은 module.<모듈 이름>.<output 이름>으로 정의된다.
cat .terraform/modules/modules.json | jq
{
  "Modules": [
    {
      "Key": "",
      "Source": "",
      "Dir": "."
    },
    {
      "Key": "mypw1",
      "Source": "../modules/terraform-random-pwgen",
      "Dir": "../modules/terraform-random-pwgen"
    },
    {
      "Key": "mypw2",
      "Source": "../modules/terraform-random-pwgen",
      "Dir": "../modules/terraform-random-pwgen"
    }
  ]
}

# graph 확인
terraform graph > graph.dot

모듈과 프로바이더

  • 모듈에서 사용되는 모든 리소스는 관련 프로바이더의 정의가 필요함
  • 프로바이더 정의 ( inner vs outer )

자식 모듈에서 프로바이더 정의

  • 모듈에서 사용하는 프로바이더 버전과 구성 상세를 자식 모듈에서 정의 하는 방법이다.
  • 프로바이더 버전과 구성에 민감하거나, 루트 모듈에서 프로바이더 정의 없이 자식 모듈이 독립적인 구조 일때 사용
  • 하지만 동일한 프로바이더가 루트와 자식 양쪽에 또는 서로 다른 자식 모듈에 버전 조건 합의가 안 되면, 오류가 발생하고 모듈에 반복문을 사용할 수 없다는 단점이 있으므로 잘 사용하지 않는다.

루트 모듈에서 프로바이더 정의

  • 자식 모듈은 루트 모듈의 프로바이더 구성에 종속되는 방식이다
  • 디렉터리 구조로는 분리되어 있지만 테라폼 실행 단계에서 동일 계층으로 해석되므로 프로바이더 버전과 구성은 루트 모듈의 설정이 적용된다.
  • 프로바이더를 모듈 내 리소스와 데이터 소스에 일괄 적용하고, 자식 모듈에 대한 반복문 사용에 자유로운 것이 장점이다.
  • 자식 모듈에 특정 프로바이더 구성의 종속성은 반영할 수 없으므로 자식 모듈을 프로바이더 조건에 대해 기록하고, 자식 모듈을 사용하는 루트 모듈에서 정의하는 프로바이더에 맞게 업데이트 해야 한다.
mkdir -p 06-module-traning/modules/terraform-aws-ec2/
# main.tf
# AWS 프로바이더 설정
terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws" # AWS 프로바이더의 소스
    }
  }
}

# 기본 VPC 리소스 설정
resource "aws_default_vpc" "default" {}

# Amazon Linux 2 AMI 데이터 검색 설정
data "aws_ami" "default" {
  most_recent = true         # 가장 최근의 AMI 사용
  owners      = ["amazon"]   # 소유자가 Amazon인 AMI

  # owner-alias 필터
  filter {
    name   = "owner-alias"
    values = ["amazon"]
  }

  # name 필터 (amzn2-ami-hvm* 이름)
  filter {
    name   = "name"
    values = ["amzn2-ami-hvm*"]
  }
}

# AWS EC2 인스턴스 생성 설정
resource "aws_instance" "default" {
  depends_on    = [aws_default_vpc.default] # 기본 VPC 생성 종속성 설정
  ami           = data.aws_ami.default.id   # 데이터 검색한 AMI 사용
  instance_type = var.instance_type         # 변수로부터 인스턴스 유형 설정

  # 태그 설정
  tags = {
    Name = var.instance_name  # 변수로부터 인스턴스 이름 설정 (태그)
  }
}

작성된 모듈을 사용할 루트 모듈 디렉터리 생성 및 06-module-traning/multi_provider_for_module/main.tf output.tf 파일 생성

mkdir -p 06-module-traning/multi_provider_for_module/
# main.tf
provider "aws" {
  region = "ap-southeast-1"  
}

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

module "ec2_singapore" {
  source = "../modules/terraform-aws-ec2"
}

module "ec2_seoul" {
  source = "../modules/terraform-aws-ec2"
  providers = {
    aws = aws.seoul
  }
  instance_type = "t3.small"
}

실행 : 프로바이더 구성을 테스트

#
cd 06-module-traning/multi_provider_for_module/
terraform init
cat .terraform/modules/modules.json | jq
==
{
  "Modules": [
    {
      "Key": "",
      "Source": "",
      "Dir": "."
    },
    {
      "Key": "ec2_seoul",
      "Source": "../modules/terraform-aws-ec2",
      "Dir": "../modules/terraform-aws-ec2"
    },
    {
      "Key": "ec2_singapore",
      "Source": "../modules/terraform-aws-ec2",
      "Dir": "../modules/terraform-aws-ec2"
    }
  ]
}
==

#
terraform apply -auto-approve
terraform output
terraform state list
terraform state show module.ec2_seoul.data.aws_ami.default
terraform state show module.ec2_singapore.data.aws_ami.default
cat terraform.tfstate | grep module

# graph 확인
terraform graph > graph.dot

# aws cli로 ec2 확인
aws ec2 describe-instances --region ap-northeast-2 --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text
aws ec2 describe-instances --region ap-southeast-1 --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text

# 실습 완료 후 리소스 삭제
terraform destroy -auto-approve
  • 모듈의 반복문
  • 모듈 또한 리소스에서 반복문을 사용하듯 구성할 수 있다.
  • 모듈이라는 리소스 정의 묶음을 원하는 수량으로 프로비저닝할 수 있으므로 모듈 없이 구성하는 것과 대비해 리소스 종속성 관리와 유지 보수에 장점이 있다. count를 사용한 반복문 사용은 리소스에서의 사용 방식처럼 module 블록 내에 선언한다.

디렉터리 생성 및 06-module-traning/module_loop_count/main.tf 파일 생성

mkdir -p 06-module-traning/module_loop_count/
provider "aws" {
  region = "ap-northeast-2"  
}

module "ec2_seoul" {
  count  = 2
  source = "../modules/terraform-aws-ec2"
  instance_type = "t3.small"
}

output "module_output" {
  value  = module.ec2_seoul[*].private_ip   
}
모듈의 반복문 테스트

#
cd 06-module-traning/module_loop_count/
terraform init
cat .terraform/modules/modules.json | jq

#
terraform apply -auto-approve
terraform output
terraform state list
cat terraform.tfstate | grep module

# graph 확인
terraform graph > graph.dot

# aws cli로 ec2 확인
aws ec2 describe-instances --region ap-northeast-2 --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text

# 실습 완료 후 리소스 삭제
terraform destroy -auto-approve
참조
https://themapisto.tistory.com/201
https://dev.classmethod.jp/articles/s3-remotely-manage-terraform-tfstate-files-using-dynamodb/
위로 스크롤