보안 강화와 Terraform 운영 패턴
Terraform 보안은 HCL 문법보다 실행 경계의 문제다
Terraform 보안을 이야기할 때 흔히 sensitive = true나 secret 변수부터 떠올린다. 물론 중요하다. 하지만 운영에서 더 큰 사고는 보통 "비밀번호가 화면에 보였다"보다 넓은 경계에서 난다. 너무 강한 cloud 권한으로 CI가 실행되고, state 파일을 읽을 수 있는 사람이 과도하게 많고, production 리소스 삭제가 plan 리뷰 없이 통과되고, 콘솔 수동 변경이 다음 apply 때 뒤늦게 발견되는 식이다.
그래서 Terraform 보안은 한 가지 기능으로 끝나지 않는다. 다음 네 경계를 같이 설계해야 한다.
- 실행 주체: Terraform을 실행하는 사람·CI·HCP Terraform workspace가 어떤 권한을 갖는가.
- state와 plan: 인프라 구조와 secret이 들어갈 수 있는 파일을 누가 읽고 보관하는가.
- 정책 게이트: 위험한 변경을 코드 리뷰자의 기억이 아니라 자동 규칙으로 막는가.
- 운영 피드백: drift, 수동 변경, 실패한 apply를 어떻게 발견하고 복구하는가.
아래 그림처럼 Terraform의 보안 통제 지점은 코드 저장소 하나가 아니라 실행 전후 전체 흐름에 걸쳐 있다.
state와 plan은 민감 정보로 취급한다
Terraform state는 단순 캐시가 아니다. 리소스 ID, 속성, provider가 반환한 값, 초기 비밀번호, connection string 같은 값이 들어갈 수 있다. HashiCorp 문서는 local state가 평문 파일로 저장될 수 있고, state와 plan 파일을 민감 데이터처럼 보호해야 한다고 설명한다.
여기서 중요한 오해가 하나 있다. sensitive = true는 CLI 출력이나 HCP Terraform UI에서 값을 가려주는 기능이지, 그 값이 state나 plan에 절대 저장되지 않는다는 뜻이 아니다. Terraform은 민감 변수를 참조한 출력과 일부 plan 표시를 (sensitive value)로 가릴 수 있지만, 파일에 접근할 수 있는 사람은 여전히 원문 값을 볼 수 있는 경우가 있다. Terraform 1.10 이후의 ephemeral = true, ephemeral block, Terraform 1.11 이후 provider가 지원하는 write-only argument는 값을 state와 plan에 남기지 않기 위한 별도 선택지다. 단, provider 지원 범위가 다르므로 registry 문서를 확인해야 한다.
운영 기준은 다음처럼 잡는 편이 안전하다.
| 대상 | 기본 원칙 | 이유 |
|---|---|---|
terraform.tfstate | Git에 절대 커밋하지 않고 remote backend 사용 | state에는 실제 리소스 속성과 secret이 들어갈 수 있음 |
| plan artifact | 짧은 보관 기간과 제한된 다운로드 권한 | plan도 민감 값을 포함할 수 있음 |
| variable 값 | CI secret, HCP Terraform variable, secret manager 사용 | .tfvars 파일 유출 방지 |
| output | 필요한 값만 노출하고 secret output은 sensitive = true | 로그와 UI 노출 최소화 |
| backend 접근 | state 읽기 권한과 apply 권한을 분리 | state 조회만으로도 민감 정보가 노출될 수 있음 |
예시는 이렇게 시작할 수 있다.
variable "database_password" {
description = "Initial database password"
type = string
sensitive = true
}
output "database_endpoint" {
value = aws_db_instance.main.endpoint
}
output "database_password" {
value = var.database_password
sensitive = true
}이 설정은 출력 노출을 줄이지만, 비밀번호를 state에서 완전히 제거한다는 보장은 아니다. 가능하면 provider의 write-only argument나 secret manager 연동을 우선 검토하고, state backend 자체를 암호화·접근제어·버전관리·감사로그가 있는 위치에 둔다.
실행 권한은 사람 계정이 아니라 작업 단위로 나눈다
Terraform은 cloud API를 대신 호출한다. 따라서 Terraform 실행 권한이 과하면 HCL 한 줄의 실수가 네트워크 공개, 데이터베이스 삭제, 과도한 비용으로 이어질 수 있다. AWS Prescriptive Guidance도 Terraform AWS Provider 보안에서 least privilege, IAM role, remote state 보호, 소스·인프라 스캔, 정책 검사를 핵심으로 제시한다.
실무에서 흔한 위험한 출발점은 "일단 AdministratorAccess로 붙이고 나중에 줄이자"이다. 실제로는 나중에 줄이기 어렵다. module이 커지고 resource 종류가 늘어난 뒤에는 어떤 권한이 필요한지 역추적하기 어렵고, 실패가 production 배포 지연으로 보이기 때문이다. 더 나은 방식은 좁게 시작하고, 실패한 API를 근거로 필요한 권한만 추가하는 것이다.
권한 모델은 최소한 다음 축으로 나눈다.
- 환경별 분리: dev, stage, prod role을 나눈다.
- 단계별 분리: plan role과 apply role을 가능하면 나눈다. plan은 읽기 중심, apply는 변경 권한이다.
- 실행 위치별 분리: 로컬, CI, HCP Terraform agent가 같은 장기 key를 공유하지 않는다.
- 수명 분리: 장기 access key보다 OIDC, STS assume role, workload identity, HCP Terraform dynamic provider credentials 같은 단기 credential을 쓴다.
HCP Terraform이나 Terraform Enterprise의 dynamic provider credentials는 run마다 임시 credential을 발급받아 provider에 전달하는 방식이다. cloud provider는 Terraform이 발급한 OIDC 기반 workload identity token을 검증하고, plan/apply 단계와 workspace 같은 metadata를 바탕으로 임시 권한을 줄 수 있다. 이 모델은 workspace 변수에 오래 사는 static credential을 저장하고 주기적으로 회전하는 방식보다 운영 부담과 노출 시간을 줄인다.
직접 CI에서 실행하더라도 원칙은 같다. GitHub Actions라면 OIDC trust policy에 repository, branch, environment 조건을 넣고, production apply role은 protected environment 승인 이후에만 assume되게 만든다.
lifecycle은 안전장치이지만 정책을 대신하지 않는다
Terraform의 lifecycle meta-argument는 운영 사고를 줄이는 데 도움이 된다. 대표적으로 세 가지를 많이 쓴다.
| 기능 | 쓰는 이유 | 주의점 |
|---|---|---|
prevent_destroy | DB, bucket, VPC 같은 중요 리소스 삭제를 plan 단계에서 막음 | resource block을 설정에서 제거하면 이 보호도 같이 사라질 수 있음 |
create_before_destroy | replacement 때 새 리소스를 먼저 만들고 기존 리소스를 나중에 삭제 | 이름 중복, quota, dependency 전파를 확인해야 함 |
ignore_changes | autoscaler나 외부 controller가 바꾸는 값을 Terraform이 되돌리지 않게 함 | 너무 넓게 쓰면 실제 drift를 숨김 |
예를 들어 production database에는 다음처럼 삭제 방지와 입력 검증을 같이 둘 수 있다.
variable "environment" {
type = string
validation {
condition = contains(["dev", "stage", "prod"], var.environment)
error_message = "environment must be one of dev, stage, prod."
}
}
resource "aws_db_instance" "main" {
identifier = "orders-${var.environment}"
engine = "postgres"
allocated_storage = 100
lifecycle {
prevent_destroy = true
precondition {
condition = var.environment != "prod" || var.allocated_storage_gb >= 100
error_message = "production database must start with at least 100GB."
}
}
}다만 lifecycle은 리소스 단위의 안전장치다. 조직 전체의 금지 규칙, 예를 들어 "production security group은 0.0.0.0/0 SSH를 허용할 수 없다" 또는 "금요일에는 production apply를 막는다" 같은 규칙은 policy-as-code가 더 적합하다. HashiCorp 문서 기준으로 HCP Terraform 정책은 Sentinel 또는 OPA policy set으로 Terraform plan을 검사할 수 있고, policy set은 organization, project, workspace 범위에 붙일 수 있다.
검증은 module 내부, policy, run task로 층을 나눈다
보안 검사를 한 곳에 몰아넣으면 오래가지 않는다. module 작성자가 알아야 하는 제약과 조직 보안팀이 강제해야 하는 제약은 다르기 때문이다.
좋은 구조는 다음처럼 층을 나눈다.
- Variable validation: module 입력의 형식과 허용 범위를 빠르게 잡는다.
- Precondition/Postcondition: 특정 리소스나 data source의 가정을 plan/apply 시점에 검증한다.
- Check block: 경고성 검증이나 HCP Terraform health assessment에서 반복 확인할 조건을 둔다.
- OPA/Sentinel policy: workspace나 조직 전체에 적용할 보안·컴플라이언스 규칙을 둔다.
- 외부 IaC scanner: Terraform 코드와 cloud 설정의 알려진 위험 패턴을 PR에서 검사한다.
Terraform custom conditions 문서에 따르면 variable validation은 plan 생성 전 입력을 막고, precondition은 resource/data/output 평가 전 가정을 검증하며, postcondition은 생성 또는 조회 후 보장을 확인한다. check block은 plan/apply 끝에서 warning 중심으로 동작할 수 있어, 즉시 배포를 막기보다 지속적인 상태 확인에 어울린다.
예를 들어 module 내부에서는 CIDR 형식과 instance 크기를 제한하고, 조직 policy에서는 공개 SSH나 특정 region 사용 금지를 막는다. 이렇게 나누면 module은 재사용성을 유지하고, 보안팀의 전사 규칙은 각 팀 repository에 복사하지 않아도 된다.
drift 처리는 자동 복구보다 분류가 먼저다
Drift는 Terraform 코드와 실제 인프라가 달라진 상태다. 수동 콘솔 변경, 장애 대응 중 임시 수정, 외부 controller, autoscaling, provider 기본값 변화가 모두 원인이 될 수 있다. terraform plan은 refresh를 통해 실제 리소스와 state를 비교하므로 drift를 드러낼 수 있고, HCP Terraform의 health assessment와 drift detection은 정기적으로 divergence를 확인하는 방식으로 쓰인다.
하지만 drift를 발견했다고 바로 자동 apply로 되돌리는 것은 위험하다. 수동 변경이 장애 대응을 위한 임시 조치였을 수도 있고, 외부 controller가 의도적으로 관리하는 값일 수도 있다. 운영 절차에서는 drift를 먼저 분류해야 한다.
| drift 유형 | 예시 | 처리 |
|---|---|---|
| 정상 drift | autoscaling desired capacity, Kubernetes controller가 바꾸는 annotation | ignore_changes를 좁게 적용하거나 Terraform 관리 범위에서 제외 |
| 긴급 수동 변경 | 장애 대응 중 security group 임시 허용 | 사후 PR로 코드 반영 또는 원복, incident 기록 |
| 무단 변경 | 승인 없이 public ingress, IAM 권한 확대 | 알림, 권한 회수, audit log 확인, 코드/인프라 원복 결정 |
| provider/API 변화 | provider upgrade 후 기본값 diff 발생 | provider changelog 확인, lock file과 version constraint 정리 |
운영적으로 중요한 것은 drift 알림 자체보다 triage 경로다. 누가 plan을 읽고, 어떤 기준으로 코드 반영과 원복을 결정하며, 긴급 변경을 얼마나 오래 허용할지 정해져 있어야 한다.
운영 runbook에는 실패한 apply와 state 조작 절차가 있어야 한다
Terraform 운영에서 가장 긴장되는 순간은 apply 중간 실패다. 일부 리소스는 이미 만들어졌고, 일부는 실패했으며, state가 실제와 어느 정도 맞는지 확인해야 한다. 이때 즉흥적으로 terraform state rm이나 cloud console 삭제를 하면 다음 apply가 더 위험해진다.
runbook에는 최소한 다음 절차가 있어야 한다.
- 실패한 run의 commit, workspace, plan, apply log, actor를 기록한다.
terraform plan을 다시 실행해 Terraform이 현재 상태를 어떻게 보는지 확인한다.- 실제 cloud 리소스와 state의 차이를 비교한다.
- state 조작이 필요하면
import,state mv,state rm의 목적과 rollback을 PR 또는 incident note에 남긴다. - production에서는 state 조작 권한을 제한하고, 단독 실행을 막는다.
- 복구 후에는 drift detection 또는 다음 plan으로 깨끗한 상태를 확인한다.
특히 state rm은 "인프라를 삭제하지 않고 Terraform 관리 대상에서만 제거"하는 강한 명령이다. 잘못 쓰면 실제 리소스가 orphan으로 남고, 비용·보안·소유권 추적에서 사라질 수 있다. 반대로 terraform import는 이미 존재하는 리소스를 Terraform 관리로 가져오는 절차지만, import만으로 HCL이 자동으로 완성되는 것은 아니므로 configuration과 state를 맞추는 리뷰가 필요하다.
실무 체크리스트
Terraform 보안을 운영 수준으로 끌어올릴 때는 아래 체크리스트부터 점검하면 좋다.
- [ ] local state를 쓰지 않고, 암호화·잠금·접근 제어가 있는 remote backend를 쓴다.
- [ ] state와 plan artifact 접근 권한을 최소화하고, plan artifact 보관 기간을 제한한다.
- [ ]
.tfvars, state, crash log, provider credential이 Git에 들어가지 않게 secret scanning을 켠다. - [ ] CI/HCP Terraform은 장기 access key 대신 OIDC나 dynamic provider credentials를 사용한다.
- [ ] dev/stage/prod role과 plan/apply role을 분리한다.
- [ ]
.terraform.lock.hcl을 커밋하고 provider version constraint를 명시한다. - [ ] production 리소스에는
prevent_destroy, 승인 게이트, 백업 정책을 같이 둔다. - [ ]
ignore_changes는 이유를 주석으로 남기고 최소 속성에만 적용한다. - [ ] OPA/Sentinel 또는 IaC scanner로 공개 네트워크, 과도한 IAM, 암호화 누락을 PR에서 막는다.
- [ ] drift 알림을 받는 사람, triage 기준, 사후 코드 반영 절차를 문서화한다.
- [ ] state 조작 명령은 runbook과 승인 절차 없이는 실행하지 않는다.
마무리
Terraform 보안의 핵심은 "코드가 안전한가"보다 "코드가 어떤 권한으로, 어떤 검증을 거쳐, 어떤 state를 바꾸며, 실패 후 어떻게 복구되는가"에 있다. sensitive, lifecycle, precondition 같은 언어 기능은 중요한 부품이지만, 그것만으로 운영 안전성이 생기지는 않는다.
작게 시작한다면 세 가지부터 고정하는 것이 좋다. 첫째, state와 plan을 민감 정보로 보호한다. 둘째, Terraform 실행 권한을 환경과 단계별로 나누고 단기 credential을 쓴다. 셋째, 반복되는 보안 판단은 리뷰어 기억이 아니라 policy-as-code와 검증 단계로 옮긴다. 이 세 가지가 잡히면 Terraform은 단순 자동화 도구가 아니라, 변경을 통제하고 감사 가능한 인프라 운영 경로가 된다.
References
- HashiCorp Developer, Manage sensitive data in Terraform configuration
- HashiCorp Developer, lifecycle meta-argument reference
- HashiCorp Developer, Validate your infrastructure in Terraform's configuration language
- HashiCorp Developer, Dynamic provider credentials in Terraform Enterprise
- HashiCorp Developer, HCP Terraform policy enforcement overview
- HashiCorp Developer, Detect infrastructure drift and enforce policies
- AWS Prescriptive Guidance, Security best practices for Terraform AWS Provider