개인적으로 개발하는 여행 서비스(웹 및 iPhone 앱)의 AWS 환경을 약 3주 만에 클라우드포메이션 (CloudFormation) 관리 하에 두었습니다.
RDS나 S3처럼 데이터를 포함하는 리소스는 재구축하지 않고 resource import 기능을 사용하여 가져왔습니다. 조사, 템플릿 작성, 절차서, 검토 과정은 AI(Claude Code)가 담당했고, 사람(저 자신)은 승인과 상용 환경에서의 실행만 담당했습니다.
이 글에서는 그 진행 방식과, AI가 작성한 변경 사항을 기계적으로 막는 메커니즘에 대해 설명합니다.
-
콘솔에서 만든 리소스와 SAM (Serverless Application Model)이 혼재된 AWS를 운영하며 IaC로 전환하고 싶은 사람
-
데이터를 포함하는 리소스를 재구축하지 않고 관리 하에 넣고 싶은 사람
-
AI 에이전트가 인프라를 건드리는 것이 두려운 사람
-
resource import 기능을 이용해 재구축 없이 가져오는 진행 방식
-
스택 분할 방법
-
AI가 작성한 변경 사항을 막는, 변경 세트(Change Set) 승인 목록 게이트
-
가져온 이후에 무너지지 않도록 하는 주 1회 드리프트 감지 (Drift Detection)
| 항목 | 내용 |
|---|
| 구성 | Amplify (웹), API Gateway + Lambda (Python), RDS PostgreSQL, S3 |
| ... | |
| API와 Lambda는 SAM 템플릿에 있었습니다. 반면, RDS, S3, VPC, Amplify, DNS, 인증서(Certificate), 공용 IAM 역할은 콘솔에서 만들어진 상태였습니다. | |
가장 어려웠던 점은 다음 세 가지였습니다.
- 무엇이 어디에 있는지 알 수 없음: 사용되지 않는 보안 그룹이나 오래된 Lambda, 빈 스택 등이 남아 있었습니다.
- 환경 경계의 모호함: 검증(Staging)용 Lambda가 상용 환경과 동일한 공용 실행 역할을 사용하고 있었습니다.
- 배포에 제동 장치가 없음: CI는 장기간의 액세스 키로 작동했으며,
sam deploy --no-confirm-changeset 명령어를 사용하여 변경 세트를 확인하지 않고 실행했습니다.
가장 먼저 한 일은 현재 구성도를 그리는 것이었습니다. AI에게 draw.io의 MCP를 이용해 다이어그램을 그리게 하고, 리소스 목록표(Inventory)를 만들었습니다. IaC로 전환할 범위는 이 표를 기준으로 결정했습니다.
도구는 SAM / CloudFormation을 계속 사용하는 것으로 했습니다.
| 관점 | SAM / CFN 유지 + import | CDK로 이전 | Terraform으로 이전 |
|---|---|
| 기존 스택 및 CI | 그대로 사용 가능 | 이전하거나 공존시킬 수 있음 | 이전하거나 공존시킬 수 있음 |
| ... |
결정적인 이유는 변경 세트에서 '대체(Replacement)' 여부를 기계적으로 확인할 수 있다는 점이었습니다. AI가 작성한 템플릿이라도 데이터가 포함된 리소스가 재구축되지 않음을 사람의 눈에 의존하지 않고 확인할 수 있었습니다.
가져올 때 지켜야 할 조건도 미리 정했습니다.
- 데이터가 포함된 리소스와 네트워크에는
DeletionPolicy: Retain과 UpdateReplacePolicy: Retain을 추가한다 -
- 가져오기는 반드시 IMPORT 변경 세트를 사용한다
- 템플릿의 값은 가져오는 시점의 실제 환경에 맞춘다 (가져오기와 설정 변경을 혼합하지 않는다)
- 검증 환경에서 먼저 가져와서 차이가 0임을 확인한 후에 상용으로 진행한다
- 상용 작업은 절차서를 만들고 사람이 승인한 후에 수행한다
- IaC의 변경은 모두 비판적인 검토를 거친다
| 업무 | 담당자 | 규칙 |
|---|---|
| 조사 | AI | AWS는 읽기 전용(Read-only) 명령어만 사용한다. 실행된 명령어는 모든 건을 나중에 확인해야 한다 |
| ... |
검토는 작성한 AI와 다른 AI에게 의뢰했습니다. 당시에는 OpenAI의 Codex를, 지금은 Claude의 Opus를 이용하고 있습니다.
검토에서 지적받았던 내용은 주로 대체(Replacement) 누락, 삭제 보호 설정 미흡, 권한 과다 부여였습니다. 하나의 절차서로 3~5회 반복하는 경우도 있었습니다.
실패 사례도 하나 공유합니다. 구성도를 그리게 했을 때, AI가 템플릿을 읽지 않고 추측으로 작성한 부분이 있어 검토에서 Major 항목이 3건 나왔습니다(인가 확인 방법 등). 그 이후로는 '사실은 템플릿/코드/AWS의 읽기 전용 정보를 통해 확인한다'는 규칙을 세웠습니다.
변경 빈도와 배포 방식에 따라 구분했습니다.
| 스택 | 내용물 | 배포 |
|---|
app-{env} (SAM) | API Gateway・Lambda・환경별 실행 역할 | CI(게이트 포함) |
data-{env} | RDS・S3・DB 자격 증명 | 수동(절차서 및 승인) |
frontend | Amplify・CloudFront Functions | 수동 |
ci | OIDC 프로바이더・배포용 역할・산출물 버킷 | 수동 |
shared-network | VPC・서브넷・보안 그룹 | 수동(검증 시에도 상용과 동일한 게이트) |
shared-dns | Route 53 존 및 레코드 | 위와 같음 |
shared-edge (us-east-1) | 인증서・WAF | 위와 같음 |
분리할 때의 규칙은 5가지입니다.
가져오기(Import)는 SAM이 아닌 순수 CloudFormation 스택에 넣는다. 애플리케이션 스택은 CI가 매번 배포하므로, 데이터를 넣으면 영향 범위가 넓어지는 데이터와 공유 인프라는 CI가 자동으로 배포하지 않는 스택에 둔다-
하나의 것(보안 그룹 규칙・역할 정책)을 두 개의 스택이 소유하지 않게 한다. 새로 만드는 스택에서는 규칙이나 정책을 본체 속성으로 작성하지 않고, 독립적인 리소스로 만든다 -
삭제 예정인 것은 가져오지 않는다. 가져온 후 지우는 것보다 안전하고 번거로움도 적다 -
참조 방향은 위에서 아래로만 한다. 같은 리전의 경우 !ImportValue를 사용한다.
또한, us-east-1 인증서 ARN은 파라미터로 전달하며, CI가 shared-edge의 출력과 일치하는지 확인합니다.
공용 실행 역할은 가져오지 않고 환경별 역할로 분리했습니다. 분리할 때 코드가 실제로 사용하는 작업을 모두 파악하여, AmazonS3FullAccess와 같이 너무 광범위한 권한도 제외했습니다.
순서는 다음과 같습니다. 이전 단계의 검사를 통과해야 다음 단계로 넘어갈 수 있습니다.
임시 자원으로, 가져올 수 있는지 검증하는 순서:
data-stg
ci
(OIDC 및 게이트 전환과 함께) - 실행 역할 분리 (검증 → 상용)
data-prod
shared-network
shared-dns
・shared-edge
frontend
가져온 직후의 검사는 매번 동일한 5가지로 진행했습니다.
| # | 검사 | 합격 조건 |
|---|
| A1 | drift detection (드리프트 감지) | 가져온 리소스가 모두 IN_SYNC 상태일 것 |
| A2 | drift detection 대상이 아닌 타입 비교 | 실제와 템플릿 간의 차이가 없을 것 |
| A3 | 아무것도 변경하지 않는 템플릿으로 변경 세트 생성 | 변경 없음 |
| A4 | 영향 없는 업데이트(태그 추가 등)를 1회 실행 | 성공하고, 교체(Replacement)나 삭제가 없을 것 |
| A5 | 통신 확인 | 가져오기 전과 동일하게 작동할 것 |
DNS의 단순 레코드는 가져올 수 없다
단순 라우팅 레코드는 import 대상에 포함시킬 수 없었습니다. 그래서 현재 값을 그대로 템플릿에 작성하여 '생성'했습니다. 값이 완전히 같다면 오류 없이 관리하에 들어오고, 다르면 실패하며 기존 레코드는 변경되지 않습니다.
SAM 스택으로의 IMPORT가 거부되었다
검증용 이미지 버킷의 정책을 SAM 스택으로 가져오려고 했더니, API 배포를 재작성하는 것이 동일한 변경 세트에 나타나면서 IMPORT가 통과되지 않았습니다. 따라서 정책은 버킷과 같은 data-stg에 두기로 했습니다.
값이 같아도 작성 방식이 바뀌면 교체(Replacement)로 인식된다
템플릿에 직접 작성한 ID(서브넷・보안 그룹 등)를 !ImportValue로 대체하려고 했더니, 값은 같은데 일부 리소스에서 변경 세트에 Replacement: True가 나타났습니다.
대체하는 것을 포기하고, 직접 작성된 상태 그대로 공유 스택의 출력과 비교 검사 스크립트로 일치 여부를 확인하기로 했습니다. 변경 세트는 만들었을 뿐 실행하지 않았습니다. 미리 만들어보고 실행하는 흐름으로 만든 덕분에 사전에 발견할 수 있었습니다.
sam deploy
로 바로 실행하는 것을 멈추고, 변경 세트를 만든 후 판정하도록 했습니다.
sam build
→sam package (산출물을 S3에)
aws cloudformation create-change-set
(이름은 CI 실행마다 하나씩) -
describe-change-set을 사용하여 변경 목록 전체를 읽고, 판정 스크립트로 모든 항목이 허용 목록에 속하는지 확인한다.
- 모두 허용되면
execute-change-set을 실행한다. 하나라도 벗어나면 실행하지 않고 실패하며, 변경 세트는 남겨둔다.
판정은 허용 목록 방식을 채택했다. 어떤 규칙에도 해당하지 않는 변경은 모두 중단시키고 사람에게 넘긴다. 생각을 간단하게 쓰자면 다음과 같다 (실제보다 단순화한 예).
# 단순화된 예시. 실제는 리소스의 유형(Type) 및 속성(Property)별로 규칙을 가짐
AUTO_OK = {
("AWS::Lambda::Function", "Code"),
...
}
중단되는 변경의 예로는 데이터를 가진 리소스의 교체나 삭제, 함수의 실행 역할(role) 변경, 판정할 수 없는 변경 등이 있다. 판정 스크립트에는 단위 테스트를 53개 추가했다.
AI가 템플릿을 수정하든, 사람이 수정하든 동일한 게이트를 통과한다. AI에게 "조심해라"라고 부탁하는 것보다, 지나가지 못하게 구조화해 두는 것이 더 확실하다.
상용 배포는 workflow_dispatch만 사용하고, 실행한 사람이 배포를 해도 되는 사람인지 워크플로우 내에서 확인한다. 무료 플랜의 비공개 리포지토리에서는 GitHub의 Environments 승인을 사용할 수 없었기 때문에 이 방식을 채택했다.
가져온 후에 콘솔에서 누군가(혹은 AI가) 수동으로 변경하면, 템플릿과 실제 환경이 어긋난다 (drift). 그래서 매주 월요일에 주기적인 실행을 통해 확인하고 있다.
- 모든 스택의 드리프트 감지(drift detection)
- 드리프트 감지의 대상에서 제외된 유형(보안 그룹 규칙, 버킷 정책, DNS 레코드 등)은 스크립트로 실제 환경과 대조하여 검사
- 관리 범위 밖에 있는 스택(확인을 위해 만든 일회용 스택 등)이 남아있지 않은지 확인
- 불합격 시 자동으로 이슈를 생성한다 (주기적 실행 실패 알림은 받는 사람이 한정적이므로)
운영을 시작하고 나서 가장 많았던 실수는, 템플릿을 추가했음에도 드리프트 감지 대상에 등록하는 것을 잊는 것이었다. 10월에는 주당 한 번 정도 발생했다. 지금은 PR의 CI 단계에서, 등록되지 않은 템플릿이나 어떤 방법으로도 확인하지 않은 리소스 유형이 있다면 실패하도록 하고 있다.
구성도를 그리고 나서 드리프트 감지 운영을 시작하기까지 약 3주가 걸렸다.
효과적이었던 것은 다음 세 가지다.
- 변경 세트를 만든 후 실행: 교체를 기계적으로 찾아낼 수 있다. AI의 변경이든 사람의 변경이든 동일한 게이트에서 중단된다.
- 검증 단계에서 먼저 가져오고, 차이가 0임을 확인한 후에 상용화: 매번 같은 5가지 검사를 통해 절차서 형태를 갖추었다.
- 작성한 AI와 다른 AI가 리뷰: 상용 배포 전에 교체/삭제 보호 누락이나 권한 과다 부여 등을 발견할 수 있었다.
남은 것은, 가져오지 않은 오래된 리소스 정리와, 구성도를 IaC 형태로 그린 후의 모양에 맞게 다시 그리는 것이다.
AI에게 인프라를 맡길 때 중요한 것은, AI를 믿는 것도 의심하는 것도 아니라, 실수해도 멈출 수 있는 구조를 먼저 만드는 것이라고 생각한다.
이 개발 체제(PM 한 명과 AI 조직)에 대한 이야기는 note에 작성했다.