Dev Terraform 구성 - 100-hours-a-week/KTB4-2nd-Cloud GitHub Wiki

Terraform 구성

V1의 AWS 리소스는 콘솔에서 만들지 않고 모두 Cloud 저장소의 Terraform으로 관리합니다. VPC부터 EC2, S3, IAM, CloudWatch Alarm까지 하나의 Root Module에 있습니다.

State는 S3에 두고, 로컬과 CI가 같은 코드로 실행되도록 인증 설정은 코드 밖으로 뺐습니다.


디렉터리 구성

terraform/
├─ versions.tf          Terraform, Provider 버전
├─ providers.tf         리전, 공통 태그
├─ backend.tf           S3 Backend 공통 설정
├─ backend.hcl          실제 State 버킷 이름 (Git 제외)
├─ locals.tf            이름 규칙, 공통 태그
├─ variables.tf         입력 값과 검증
├─ network.tf / security-groups.tf
├─ ec2.tf / ebs.tf / s3.tf / iam.tf
├─ outputs.tf
└─ .terraform.lock.hcl  Provider 버전과 체크섬

버전 고정

terraform {
  required_version = "~> 1.16.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

AWS Provider는 6.x 안에서만 올라가게 하고, 실제 설치된 6.64.0은 lock 파일에 남겼습니다.

lock 파일에는 로컬 맥의 darwin_arm64와 GitHub Actions의 linux_amd64 체크섬을 둘 다 넣었습니다. CI에서 Terraform을 실행해도 같은 Provider를 받습니다.


인증

로컬에서는 IAM Identity Center의 SSO 프로필로 실행합니다.

aws sso login --profile yeodam-admin
export AWS_PROFILE=yeodam-admin

Provider 코드에는 프로필 이름을 넣지 않았습니다. 코드에 사람용 프로필이 들어가면 CI에서 OIDC로 받은 자격 증명을 쓸 때 같은 코드를 그대로 쓸 수 없어서, 인증은 실행 환경에서 정하도록 했습니다.

Access Key나 계정 ID는 코드와 변수 파일 어디에도 넣지 않습니다.


이름과 태그

모든 리소스 이름은 yeodam-v1-{용도} 형식을 씁니다.

locals {
  name_prefix = "${var.project_name}-${var.deployment_version}"

  common_tags = {
    Project   = var.project_name
    Version   = var.deployment_version
    ManagedBy = "Terraform"
  }
}

공통 태그는 Provider의 default_tags로 한 번에 붙입니다. yeodam-v1-app, yeodam-v1-worker, yeodam-v1-app-sg처럼 콘솔에서 봐도 어느 버전의 무엇인지 바로 구분됩니다.


변수 검증으로 V1 값 고정

리전, CIDR, 인스턴스 타입, 디스크 크기처럼 V1에서 정해둔 값은 변수 검증으로 다른 값을 막았습니다.

variable "worker_instance_type" {
  type    = string
  default = "c7i.xlarge"

  validation {
    condition     = var.worker_instance_type == "c7i.xlarge"
    error_message = "The Yeodam V1 Worker EC2 instance type must be c7i.xlarge."
  }
}

설계에서 비용과 성능을 계산해 정한 값이라, tfvars를 잘못 넣어 사양이 바뀌는 일이 없도록 했습니다. 값을 바꿔야 할 때는 검증 조건부터 수정해야 해서 변경이 PR에 드러납니다.


Remote State

항목 설정
버킷 yeodam-v1-tfstate-<계정 ID>-ap-northeast-2-an
Key yeodam/v1/terraform.tfstate
암호화 SSE-S3
Versioning 사용
Lock S3 lock 파일 (use_lockfile = true)
DynamoDB 사용하지 않음

State 버킷은 Terraform이 자기 Backend를 만드는 순환이 생기지 않도록 콘솔에서 한 번만 직접 만들었습니다. 이 버킷은 Terraform 관리 대상이 아닙니다.

버킷 이름에 계정 ID가 들어가서 backend.tf에는 공통 설정만 두고, 실제 이름은 Git에서 제외한 backend.hcl로 넘깁니다.

terraform -chdir=terraform init -reconfigure -backend-config=backend.hcl

첫 apply 뒤 State 객체가 암호화된 채로 버전이 쌓이는 것과, apply 중에 만들어진 lock 파일이 끝나고 사라지는 것을 확인했습니다. 로컬에는 State 파일이 생기지 않습니다.


apply 뒤 Plan이 비어야 완료

apply가 성공해도 바로 끝내지 않고, 다시 Plan을 돌려 No changes가 나오는지까지 확인합니다. 실제로 apply 직후 Plan에서 예상하지 못한 교체가 나온 적이 두 번 있었습니다.

App EC2 교체 Plan

App EC2를 처음 만든 뒤 Plan을 다시 돌리자 아무것도 바꾸지 않았는데 App 교체가 제안됐습니다.

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

원인은 두 가지였습니다.

  • Elastic IP를 붙인 뒤 Provider가 associate_public_ip_address를 true로 읽어 코드의 false와 달라짐
  • 따로 붙인 MySQL EBS가 인스턴스의 ebs_block_device에도 잡혀 제거 대상이 됨

Public IP는 aws_eip가 관리하므로 인스턴스에서 해당 속성을 빼고, MySQL EBS는 ebs_block_device 변경만 무시하도록 했습니다.

lifecycle {
  ignore_changes = [
    ebs_block_device,
  ]
}

EBS 크기와 암호화는 aws_ebs_volume, 연결은 aws_volume_attachment에서 그대로 관리합니다. 수정 뒤 Plan이 No changes로 돌아왔습니다.

Worker EC2 교체 Plan

Worker가 꺼져 있을 때 Plan을 돌리면 Worker 교체가 나왔습니다. 자세한 과정은 중지된 Worker 때문에 EC2 교체 Plan 발생에 있습니다.

두 경우 모두 apply가 성공했다는 사실만 봤다면 다음 작업 때 운영 서버가 교체될 뻔했습니다. 그래서 작업 범위와 무관한 change나 destroy가 하나라도 보이면 원인을 먼저 찾고 apply합니다.

⚠️ **GitHub.com Fallback** ⚠️