
Terraform 코드를 팀에서 같이 굴리다 보면 결국 부딪히는 문제가 있다. 누가 어디서 plan을 돌렸는지 모르고, 로컬에서 apply 친 사람이 state를 깨먹고, PR 리뷰는 코드만 보고 끝나는데 정작 실제 변경 영향은 머지된 뒤에야 보인다. 이걸 해결하는 도구가 Atlantis다. PR에 plan 결과를 코멘트로 붙이고, atlantis apply 같은 명령어로 머지 직전에 실행을 위임한다.
이 글은 Atlantis를 GitHub과 EKS 위에 셀프호스팅으로 띄우는 가이드다. 우리 팀에서 최근에 v0.42.0으로 올렸는데, OpenTofu 지원이 정식으로 들어가면서 마이그레이션 부담이 좀 줄었다. 그 과정에서 정리한 내용이다.
왜 또 Atlantis인가
요즘은 Spacelift, Env0, Scalr 같은 SaaS 옵션이 많다. 솔직히 그쪽이 더 편하다. 그런데 우리는 두 가지 이유로 셀프호스팅을 유지하고 있다.
첫째, state 백엔드가 사내 S3에 있고 KMS 키 정책상 외부 서비스에 IAM 권한을 위임하는 게 까다롭다. 둘째, 비용. 워크스페이스 수가 많아지면 SaaS 가격이 빠르게 올라간다. Atlantis는 EKS 위에 작은 파드 하나로도 돌릴 수 있다.
대신 단점도 있다. 상용 백엔드가 없어서 critical fix가 커뮤니티 PR에 의존한다. 최근 1년간 릴리즈 주기는 안정적이지만, 직접 운영한다는 부담은 감수해야 한다.
아키텍처 흐름
전체 동작 순서는 이렇다.
- 개발자가 GitHub에 PR을 올린다
- GitHub이 Atlantis 서버로 webhook을 쏜다
- Atlantis가 PR 브랜치를 clone하고 변경된 디렉토리만 골라서
terraform plan실행 - plan 결과가 PR 코멘트로 붙는다
- 리뷰어가 코드와 plan을 같이 본 뒤
atlantis apply코멘트를 단다 - Atlantis가 apply를 실행하고 워크스페이스에 락을 건다 — 다른 PR이 같은 디렉토리를 건드리지 못함
- PR이 머지되면 락이 해제된다
핵심은 6번이다. 로컬 apply를 막고, state 충돌을 PR 단계에서 직렬화한다.
EKS에 띄우기
Helm 차트가 공식이다. values만 잘 채우면 된다.
# atlantis-values.yaml
image:
repository: ghcr.io/runatlantis/atlantis
tag: v0.42.0
orgAllowlist: github.com/myorg/*
githubApp:
id: 123456
slug: myorg-atlantis
privateKey: |
-----BEGIN RSA PRIVATE KEY-----
...
webhookSecret: <secret>
serviceAccount:
create: true
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::111111111111:role/atlantis-runner
ingress:
enabled: true
ingressClassName: alb
hosts:
- host: atlantis.example.com
paths:
- path: /
pathType: Prefix
statefulSet:
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
memory: 4Gi
storage: 20Gi
여기서 두 가지가 흔히 빠진다.
githubApp 방식을 쓴다. PAT(personal access token)도 되긴 하는데, 개인 계정에 묶이면 그 사람 퇴사할 때 다 깨진다. GitHub App으로 organization 단위 인증을 거는 게 정석이다.
eks.amazonaws.com/role-arn은 IRSA. Atlantis 파드가 AWS Provider로 plan/apply를 칠 때 쓸 IAM 역할이다. 이 역할에 어떤 권한을 줄지가 사실 가장 어려운 문제인데, 뒤에서 다룬다.
repo-level workflow 설정
Atlantis의 진짜 힘은 atlantis.yaml과 server-side config에 있다. 우리 팀은 server-side에 workflow를 정의해두고, 각 repo는 어떤 workflow를 쓸지만 고른다.
# server-config.yaml (Atlantis 서버 측)
repos:
- id: /.*/
apply_requirements: [approved, mergeable]
workflow: default
allowed_overrides: [workflow]
allow_custom_workflows: false
workflows:
default:
plan:
steps:
- init
- plan:
extra_args: ["-lock-timeout=300s"]
- run: conftest test $PLANFILE -p /policies
apply:
steps:
- apply
opentofu:
plan:
steps:
- env:
name: TF_DISTRIBUTION
value: opentofu
- init
- plan
apply:
steps:
- apply
apply_requirements: [approved, mergeable]이 중요하다. PR이 승인되고 머지 가능 상태일 때만 apply를 허용한다. 이거 빼면 누구나 코멘트로 apply를 칠 수 있어서 사고 난다. 실제로 우리 팀에서 한 번 당했다 — 다행히 dev 환경이었지만.
allow_custom_workflows: false도 중요하다. repo 쪽에서 임의로 workflow를 정의해서 임의 명령어를 실행할 수 없게 막는다. 셀프호스팅에서는 이게 보안 경계다.
OPA/Conftest로 정책 강제
위 workflow에 슬쩍 끼워둔 conftest test $PLANFILE 라인이 핵심이다. plan 결과를 Rego 정책으로 검사한다. 우리는 이런 식으로 쓴다.
# policies/s3.rego
package main
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket"
not resource.change.after.tags["Owner"]
msg := sprintf("S3 bucket %s missing Owner tag", [resource.address])
}
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket_public_access_block"
resource.change.after.block_public_acls == false
msg := sprintf("Bucket %s allows public ACLs", [resource.address])
}
이런 정책은 별도 CI 잡으로도 돌릴 수 있지만, Atlantis workflow에 박아두면 plan 단계에서 실패하면 apply 자체가 차단된다. 별개 잡이면 누군가 무시하고 머지할 수 있다.
IAM 권한 설계 — 이게 진짜 어렵다
Atlantis 파드가 사용할 IAM 역할에 어떤 권한을 줄 것인가. 두 가지 흔한 함정이 있다.
함정 1: 편하다고 AdministratorAccess를 붙인다. 그러면 PR 코멘트로 임의 리소스를 다 만들 수 있게 된다. 정책으로 막아둔다고 해도, plan/apply가 도는 컨테이너에 super-admin 권한이 떠 있는 셈이다.
함정 2: 너무 좁게 준다. 매번 새 리소스 추가할 때마다 IAM 정책을 갱신해야 해서 실무 마찰이 어마하다.
우리가 정착한 방식은 환경별로 IAM 역할을 분리하고, AssumeRole 체인을 쓰는 거다. Atlantis 파드 자체는 최소 권한만 갖고, 각 환경별 역할로 assume한 뒤 작업한다. provider config에서 assume_role 블록으로 환경을 분기한다.
provider "aws" {
region = "ap-northeast-2"
assume_role {
role_arn = "arn:aws:iam::222222222222:role/terraform-deploy-prod"
}
}
이렇게 하면 prod 역할에는 prod 권한만, dev 역할에는 dev 권한만 붙는다. Atlantis 파드 자체의 IAM은 STS AssumeRole만 가능하면 끝. 사고 났을 때 폭발 반경(blast radius)이 환경 단위로 제한된다.
운영하면서 만난 함정들
1년 좀 넘게 굴리면서 부딪힌 것들.
Plan 결과가 PR에서 잘림. plan 출력이 길면 GitHub 코멘트 65536자 제한에 걸린다. --hide-prev-plan-comments 옵션 쓰고, 그래도 길면 plan 결과를 S3에 저장하고 링크만 코멘트에 붙이는 식으로 우회했다.
워크스페이스 락 영구 점유. PR이 닫히지 않은 채 몇 달 방치되면 락도 안 풀린다. 락 상태를 정기적으로 확인하는 cron을 따로 돌린다 — 30일 이상 묵은 락은 슬랙 알림 보내고 강제 해제 후보로 올린다.
Provider 캐시. 매 PR마다 terraform init에서 provider를 다 받으면 시간이 오래 걸린다. PVC로 plugin cache를 마운트하고 TF_PLUGIN_CACHE_DIR을 설정해두면 한참 빨라진다. 우리는 이걸 안 해뒀다가 plan 시간이 평균 90초 → 25초로 줄었다.
OpenTofu 마이그레이션. v0.42.0부터 OpenTofu가 정식 지원된다. workflow에 TF_DISTRIBUTION=opentofu 환경변수만 박으면 된다. 다만 state 파일은 호환되지만, 일부 provider 동작이 미세하게 달라서 dev 환경에서 한 달 정도 병행 운영했다.
결론 같지 않은 결론
Atlantis는 화려한 도구는 아니다. UI도 투박하고, 대시보드는 거의 없다시피 하다. 그런데 정확히 한 가지 일을 잘 한다 — Terraform 실행을 PR에 묶는다. 그게 필요한 팀이라면 충분히 값어치를 한다.
요즘 같으면 SaaS로 바로 가도 되긴 한다. 근데 셀프호스팅의 통제권을 놓기 싫은 팀이라면, Atlantis는 여전히 무난한 선택이다.
다음에는 우리 팀이 Atlantis 위에 얹은 슬랙 봇 — apply 직전에 슬랙으로 한 번 더 컨펌받게 하는 — 을 다뤄보려고 한다.
'IT > IaC' 카테고리의 다른 글
| 이제 Terraform state에 password 안 넣어도 된다 (0) | 2026.05.06 |
|---|---|
| Terraform vs OpenTofu, 2년 써보고 내리는 조심스러운 결론 (0) | 2026.04.25 |
| Terraform S3 backend, 이제 DynamoDB 없이 lock 걸 수 있다 (0) | 2026.04.25 |