몇 년째 콘솔에서 손으로 만들어 온 AWS 계정을 Terraform으로 끌어왔다. 두 달, 커밋 139개짜리 작업의 첫 주 기록이다.
terraform plan의 create는 "AWS에 없다"는 뜻이 아니다. state 기준이라 콘솔에서 만든 실물이 멀쩡히 있어도 create로 나오고, apply는 그걸 통째로 덮어쓴다.
import가 끝났다는 판정 기준은 하나다. 첫 plan이 "No changes"인 것.
왜 갑자기 코드로
그 계정만 있었다면 굳이 바꿀 이유가 없었다. 계기는 다른 리전에 계정을 하나 더 세워야 한다는 것이었다.
콘솔로 또 만들면 두 계정은 반드시 달라진다. 그리고 그 차이는 문제가 터진 다음에, "어? 저쪽은 왜 되지" 하는 형태로 드러난다. 그래서 새로 만드는 쪽을 코드로 세우기로 했고, 그러려면 기존 계정도 같은 언어로 적혀 있어야 비교가 됐다.
첫 커밋은 이렇게 남았다.
Initial commit: AWS infra as Terraform (기존 리전 import baseline + 신규 리전 modules)
저장소 구조부터 정했다
infra/
├── modules/ # 재사용 단위 (모든 환경 공용)
│ ├── network/ # VPC, subnet, IGW, NAT, route table
│ ├── alb/ # ALB, target group, listener, ACM(리전)
│ ├── compute/ # EC2, security group
│ ├── rds/ # RDS, subnet/parameter group
│ ├── edge/ # S3(origin) + CloudFront + ACM(us-east-1)
│ ├── dns/ # Route 53 zone + record
│ ├── messaging/ # SNS, SES
│ └── lambda/ # Lambda + IAM role
├── envs/
│ ├── kr/seoul/ # 기존 인프라 import
│ └── sg/tokyo/ # 신규 구축
├── scripts/ # 배포·롤백·접속
└── docs/
규칙은 한 줄이다. 리전 하나 = 디렉터리 1 = workspace 1 = state 1.
한 state에 여러 리전을 담으면 plan이 느려지는 것도 문제지만, 더 큰 건 한쪽 실수가 다른 쪽까지 흔든다는 것이다. A 리전을 작업하다 B 리전 리소스를 지우는 일이 구조적으로 불가능해야 한다.
state는 처음에 관리형 서비스에 뒀다가 일주일 만에 S3 백엔드로 옮겼다. 옮기면서 그동안 쌓인 드리프트도 같이 정리했다.
import의 완료 조건은 "No changes"
새로 만드는 것보다 이미 돌아가는 걸 코드로 끌어오는 게 훨씬 어렵다. 서비스는 멈출 수 없고, 콘솔에서 만든 리소스는 이름 규칙도 제각각이다.
terraformer로 일괄 export해서 출발점을 만들었다. 다만 나온 코드를 그대로 쓰지는 못한다. 장황하고 모듈화가 안 되어 있어서 참고용 초안으로 두고 손으로 정리했다.
여기서 기준을 하나 세웠다. import가 끝났다는 건 첫 plan이 "No changes"라는 뜻이다. 뭐라도 뜨면 코드와 실물이 다르다는 것이고, 그 상태로 apply하면 운영이 바뀐다.
함정 1 — create는 "AWS에 없다"는 뜻이 아니다
이게 이 작업에서 가장 아찔했던 것이다.
plan은 Terraform state 기준이다. 콘솔에서 만든 실물이 멀쩡히 있어도, state에 없으면 create로 나온다. 그리고 apply는 그걸 통째로 덮어쓴다.
파일 서빙용 버킷 정책이 그랬다. 코드에 새 문장만 적고 create를 봤는데, 실물에는 CDN이 쓰는 문장이 이미 들어 있었다. 그대로 갔으면 지금 도는 CDN이 죽었다.
- 실물을 먼저 조회해서 코드에 담는다
- 값이
(known after apply)라plan으로 검증이 안 되면, 일부만 담은 상태로 먼저apply해서 결과를 대조한다
함정 2 — ignore_changes는 콘솔에서 바꾸기 전에 넣는다
CDN 정액제 플랜은 요금제 종류를 강제하고 WAF를 자동으로 붙인다. 콘솔에서 먼저 바꾸고 나중에 ignore_changes를 넣으면, 그 사이 apply가 요금제를 되돌린다. 순서가 전부다.
함정 3 — 자동 생성된 config는 초안이지 정답이 아니다
plan -generate-config-out이 뱉은 HCL로 DNS 레코드를 import하려다 막혔다. provider가 multivalue_answer_routing_policy = false를 채워 넣는데, 이 속성은 set_identifier와 짝이라 단순 라우팅 레코드에서는 이렇게 끝난다.
all of multivalue_answer_routing_policy, set_identifier must be specified
그 줄을 지우면 된다. 요령은 하나 — 같은 파일의 기존 레코드와 비교해서 안 붙어 있던 속성이 붙어 있으면 의심한다.
함정 4 — inline 리스트로 관리되는 리소스에 따로 규칙을 더하지 않는다
기존 계정의 default 보안 그룹이 inline ingress 리스트로 관리되고 있었다. 여기에 규칙을 별도 리소스로 추가하면, Terraform이 리스트에 없는 규칙을 이물질로 보고 지워 버린다. 반드시 리스트 안에 넣어야 한다.
함정 5 — 타깃 그룹의 Port는 장식인데 ForceNew다
등록할 때 포트를 명시하고 헬스체크도 traffic-port라서 값 자체는 무관하다. 그런데 바꾸면 타깃 그룹이 재생성되고 ARN이 바뀐다. 리스너 규칙·IAM 정책·배포 스크립트가 전부 그 ARN을 들고 있으니 한꺼번에 깨진다.
"의미 없는 값이니 정리하자"가 제일 위험하다.
plan을 grep으로 걸러 보지 않는다
출력이 길다고 grep으로 필터하면, 패턴에 없는 속성 변경이 화면에서 사라진다. 그리고 apply에서야 드러난다. 변경 블록은 통째로 본다.
terraform plan | sed -n '/will be updated/,/^Plan:/p'
그리고 apply -auto-approve는 직전 plan을 쓰지 않고 다시 계산한다. 그 사이에 실물이 바뀌면 그 차이까지 반영한다 — 배포 중이던 로드밸런서 타깃을 Terraform이 도로 등록해 버린 적이 있다.
정확히 그것만 적용하려면 plan -out으로 저장한 파일을 apply에 넘긴다. 개수가 plan과 다르면 그 사이에 뭔가 바뀐 것이다.
정리
plan의create는 state 기준이다. 실물을 덮어쓸 수 있다.ignore_changes는 콘솔에서 바꾸기 전에 넣는다.- 생성된 config는 초안이다. 기존 코드와 비교한다.
plan은 통째로 본다.grep은 변경을 숨긴다.- import는 첫
plan이 "No changes"일 때 끝난 것이다.
다음 편은 새로 세운 리전 이야기다. 베스천 없이 서버에 붙는 구조를 만들었는데, 편해진 만큼 새로 생긴 위험도 있었다.
'AWS' 카테고리의 다른 글
| S3 프리티어 초과 청구 — 5GB보다 2,000 PUT이 먼저 터진다 (0) | 2026.09.03 |
|---|---|
| 930일 동안 이중화가 0이었다 — mod_jk에서 ALB 블루그린으로 (0) | 2026.09.02 |
| SSM Session Manager로 베스천 없이 EC2에 접속하기 (0) | 2026.09.01 |
| CloudWatch Logs 요금이 계속 오르는 이유와 보관 기간 점검 (0) | 2026.08.30 |
| NAT 게이트웨이 요금이 갑자기 늘었을 때 확인할 순서 (0) | 2026.08.29 |
댓글