13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[태그:] IaC 보안

  • [보안] Trivy 도입 1년 회고: DevSecOps 워크플로우 변화와 개선점 분석

    [보안] Trivy 도입 1년 회고: DevSecOps 워크플로우 변화와 개선점 분석

    본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다.

    Trivy 도입 회고를 1년치로 다시 적어보면, 제가 얻은 가장 큰 변화는 탐지율보다도 의사결정 방식의 변화였습니다. 전에는 취약점 공지가 뜨면 운영팀이 먼저 불안해했고, 개발팀은 “우리 코드 문제인지 베이스 이미지 문제인지”부터 감으로 추정했었죠. Trivy를 붙인 뒤에는 최소한 그 대화가 구조화됐습니다. 어떤 단계에서 막을지, 무엇을 예외로 둘지, 어떤 결과는 즉시 수정이고 어떤 결과는 추적 대상으로 둘지 기준선이 생겼거든요.

    중요한 건 여기입니다. Trivy는 보안을 완성해주는 도구가 아니라, 컨테이너와 IaC 영역에서 팀의 판단을 표준화해주는 도구에 가깝습니다. 이걸 잘못 기대하면 “스캔은 도는데 사고는 왜 나지?”라는 반응이 나오고, 반대로 위치를 정확히 잡으면 릴리스 직전의 불필요한 공황을 꽤 줄여주더라고요. 저도 홈랩과 업무 환경에서 비슷한 패턴으로 굴려보면서, Trivy를 도입한 팀과 그렇지 않은 팀의 차이는 기능 수보다 운영 규칙의 유무에서 갈린다고 느꼈습니다.

    이번 글은 단순 사용기가 아니라, 1년 동안 실제로 남은 습관과 버린 습관을 같이 적는 회고입니다. 특히 CI에서 보안 스캔이 형식화되기 쉬운 팀, 결과는 쌓이는데 누구도 우선순위를 정하지 못하는 팀이라면 바로 적용 가능한 형태로 정리해봤습니다.

    Trivy 도입 회고 기반 DevSecOps 워크플로우 개요 이미지

    Trivy 도입 회고와 DevSecOps 워크플로우 변화를 한눈에 보여주는 개요 이미지입니다.

    1. 왜 Trivy를 넣었는지: 제가 원한 건 “더 많은 경고”가 아니라 “더 빠른 판별”이었습니다

    제가 처음 Trivy를 넣은 이유는 단순했습니다. 컨테이너 이미지를 배포하는 속도는 빨라졌는데, 취약점 확인은 여전히 사람 손을 많이 탔기 때문입니다. 패키지 목록을 보고 CVE를 대조하는 식의 확인은 운영 주기가 짧아질수록 바로 무너지더라고요. 그때 필요한 건 화려한 플랫폼이 아니라, 이미지와 설정을 같은 문법으로 반복 점검할 수 있는 스캐너였습니다.

    Trivy가 실무에서 먹히는 이유는 범위가 넓어서가 아니라 도입 순서를 잘게 쪼갤 수 있어서입니다. 이미지 스캔만 먼저 넣고, 이후에 IaC와 secret 탐지로 넓혀도 워크플로우가 크게 흔들리지 않습니다. 반면 SAST나 DAST는 언어, 프레임워크, 런타임, 테스트 환경에 더 강하게 묶이는 경우가 많아서 첫 진입 비용이 큽니다.

    다만 여기서 선을 분명히 그어야 합니다. 애플리케이션 로직 취약점, 예를 들면 인증 우회나 권한 검증 실수 같은 건 Trivy가 대신 찾아주지 않습니다. 제 경험상 Trivy는 아래 상황에서 특히 강하고, 아래 상황에서는 과도한 기대를 버리는 편이 맞았습니다.

    상황 Trivy를 우선 쓸 때 다른 도구를 같이 봐야 할 때 제 판단
    컨테이너 배포 파이프라인 베이스 이미지, OS 패키지, 언어 패키지 점검 런타임 행위 분석이 필요할 때 Trivy를 기본 게이트로 둡니다
    Kubernetes / Terraform 운영 매니페스트, Terraform, Dockerfile 오설정 점검 조직 정책 강제와 예외 승인 체계가 별도일 때 Trivy config 또는 fs 스캔을 먼저 붙입니다
    애플리케이션 코드 보안 의존성 수준의 위험 신호 확인 인증, 권한, 비즈니스 로직 취약점 검토 SAST, 코드리뷰, 위협 모델링이 필요합니다
    비밀값 관리 저장소 내 하드코딩 secret 탐지 이미 유출된 키 회전, 감사 추적 탐지 후 운영 절차가 더 중요합니다

    한마디로 말하면, Trivy는 “전체 보안 솔루션”이 아니라 배포 경로에 가까운 위험을 빠르게 분류하는 도구로 볼 때 가장 실용적이었습니다.

    2. Trivy를 1년 써보니 바뀐 DevSecOps 워크플로우

    도입 전에는 릴리스 직전에 한 번 몰아서 확인하는 습관이 있었습니다. 이 방식의 문제는 취약점 자체보다 발견 시점이 늦다는 데 있습니다. 이미지 하나에서 HIGH나 CRITICAL이 나와도, 그게 애플리케이션 의존성인지 베이스 이미지 레이어인지 구분하는 데 시간이 들고, 그다음에는 재빌드와 회귀 테스트가 줄줄이 따라옵니다. 보안 이슈가 일정 이슈로 번지는 전형적인 패턴이죠.

    Trivy를 붙인 뒤에는 스캔 위치를 늘린 게 아니라, 질문이 다른 세 지점으로 나눴습니다.

    1. 개발 중: 지금 만든 변경이 새 위험을 들여왔는가
    2. CI 빌드 단계: 이 커밋은 배포 금지선에 걸리는가
    3. 배포 전 또는 정기 점검: 허용한 예외가 아직도 타당한가

    이 세 지점을 구분하고 나니 팀 대화가 훨씬 현실적으로 바뀌었습니다. 예전에는 “보안팀이 막았다”는 식으로 받아들였는데, 지금은 “이건 신규 유입이라 지금 고쳐야 한다”, “이건 기존 부채라 추적하되 이번 배포는 진행한다”처럼 이야기할 수 있게 됐습니다.

    특히 제가 강하게 느낀 건, 모든 HIGH/CRITICAL을 일괄 차단하는 정책은 생각보다 오래 못 간다는 점입니다. 신규 서비스라면 가능하지만, 오래된 베이스 이미지와 레거시 패키지가 섞인 환경에서는 첫날부터 파이프라인이 전부 빨갛게 변합니다. 그러면 팀이 배우는 게 아니라 우회 방법을 먼저 찾습니다. 그래서 저는 보통 이렇게 권했습니다.

    • 신규 서비스: HIGH, CRITICAL을 바로 게이트로 겁니다
    • 기존 서비스: 우선 JSON 리포트를 남기고 신규 유입분만 차단합니다
    • IaC 점검: 즉시 차단보다 반복되는 오설정 패턴을 템플릿에서 제거합니다

    이 접근이 좋은 이유는 단순합니다. 보안 게이트는 강도보다 지속 가능성이 먼저이기 때문입니다.

    3. 실전 구현: 1년 뒤에도 남은 기본 스캔 패턴

    처음엔 옵션을 과하게 붙였는데, 시간이 지나고 보니 오래 살아남는 건 단순한 패턴이었습니다. 다만 단순하다고 해서 대충 쓰면 안 됩니다. Trivy는 같은 명령처럼 보여도 어디서 어떤 플래그를 붙이느냐에 따라 해석이 꽤 달라집니다.

    3-1. 로컬 이미지 점검: “지금 만든 이미지가 배포 금지선에 걸리는지”만 빠르게 확인

    # 사람이 읽기 좋은 표 형태로 확인
    trivy image --severity HIGH,CRITICAL --ignore-unfixed --format table my-app:local
    
    # 보안 게이트 용도: 보안 이슈가 있으면 종료 코드 1
    trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 my-app:local
    
    # 결과를 남겨서 이전 스캔과 비교
    trivy image --severity HIGH,CRITICAL --format json --output trivy-image-report.json my-app:local

    여기서 제가 실제로 중요하게 본 건 세 가지입니다. --severity는 우선순위 절삭용이고, --exit-code 1은 자동화 제어용이며, --format json --output은 운영 대화용입니다. 스캔 결과를 눈으로만 보고 끝내면 회고가 안 남습니다. 반대로 JSON을 남기면 “원래 있던 위험인지”, “이번 커밋에서 새로 생긴 위험인지”를 구분하기가 쉬워집니다.

    또 하나, --ignore-unfixed는 편의 옵션처럼 보이지만 팀 피로도를 크게 좌우합니다. 패치가 아직 없는 항목까지 동일한 톤으로 경고하면 개발자는 금방 무감각해지기 마련입니다. 제가 운영에서 얻은 결론은 이렇습니다. 수정 가능한 위험과 당장 수정 불가능한 위험을 같은 줄에 세우지 마세요. 이 둘은 우선순위가 다릅니다.

    3-2. 저장소와 IaC 점검: 여기서 많이 놓치는 건 “기본값의 함정”입니다

    Trivy를 처음 붙일 때 많은 팀이 놓치는 포인트가 하나 있습니다. image, fs, repo 계열 스캔에서 misconfiguration 탐지는 기본 활성화가 아닐 수 있다는 점입니다. 즉, 취약점 스캔이 성공했다고 해서 Kubernetes 매니페스트나 Terraform 설정까지 본 게 아닐 수도 있더라고요. 저는 이 부분에서 한 번 크게 헷갈렸고, 이후에는 의도적으로 스캐너 종류를 명시했습니다.

    # 파일시스템 기준으로 취약점, 오설정, 비밀값을 같이 확인
    trivy fs --scanners vuln,misconfig,secret --severity HIGH,CRITICAL .
    
    # 설정 자체를 집중적으로 볼 때
    trivy config .
    
    # JSON 결과를 남겨 후처리
    trivy fs --scanners misconfig,secret --format json --output trivy-fs-report.json .

    제 경험상 운영 사고는 애플리케이션 코드보다 YAML과 Terraform에서 먼저 터지는 경우가 많았습니다. 예를 들어 보안 컨텍스트 누락, 과도한 capability 허용, 퍼블릭 노출 보안 그룹, 암호화 누락 같은 항목은 코드리뷰에서 지나가도 설정 스캔에서는 반복적으로 잡힙니다. 그래서 Kubernetes나 Terraform 비중이 큰 팀이라면 이미지 스캔보다 trivy config .가 체감 효용이 더 클 수도 있습니다.

    Trivy 도입 회고를 위한 CI 이미지 스캔과 설정 스캔 구성도

    빌드, 스캔, 배포 승인 단계로 이어지는 Trivy 기반 CI 흐름을 설명하는 이미지입니다.

    3-3. CI 파이프라인 예시: 속도와 신뢰도를 같이 잡으려면 캐시 전략이 먼저입니다

    초기에는 매 파이프라인마다 DB를 처음부터 내려받아서 체감 속도가 꽤 나빴습니다. 나중에 알게 된 건, 느린 원인이 Trivy 자체의 스캔 엔진보다도 DB cold start와 캐시 운영 방식에 있는 경우가 많다는 점입니다. 저는 이후에 “업데이트 단계”와 “실제 스캔 단계”를 분리하는 식으로 바꿨습니다.

    stages:
      - build
      - security
    
    variables:
      TRIVY_CACHE_DIR: .trivycache
    
    trivy_db_warmup:
      stage: security
      image: aquasec/trivy:latest
      script:
        - trivy image --download-db-only --cache-dir "$TRIVY_CACHE_DIR"
      cache:
        key: trivy-cache
        paths:
          - .trivycache
    
    trivy_scan:
      stage: security
      image: aquasec/trivy:latest
      script:
        - trivy image --cache-dir "$TRIVY_CACHE_DIR" --skip-db-update --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 --format json --output trivy-report.json "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
        - trivy config --severity HIGH,CRITICAL .
      cache:
        key: trivy-cache
        paths:
          - .trivycache
      artifacts:
        when: always
        paths:
          - trivy-report.json

    여기서 핵심은 세 가지입니다. 첫째, --download-db-only로 DB 준비를 분리합니다. 둘째, 실제 스캔에서는 --skip-db-update로 매번 네트워크 병목을 만들지 않습니다. 셋째, 캐시 디렉터리를 명시해 러너별 편차를 줄입니다. 이 패턴은 파이프라인 속도를 위해서도 중요하지만, 더 본질적으로는 팀이 우회하지 않게 만드는 장치입니다.

    주의할 점도 있습니다. 로컬 파일 시스템 캐시는 편하지만, 같은 캐시를 여러 프로세스가 동시에 잡는 환경에서는 잠금 문제가 생길 수 있습니다. 현업에서는 이걸 Trivy 오류로 오해하기 쉬운데, 실제 원인은 병렬 러너와 공유 캐시 구조인 경우가 많습니다. 그래서 병렬 잡이 많은 CI라면 캐시 공유 방식을 먼저 점검하는 편이 좋았습니다.

    3-4. 예외는 파일이 아니라 계약입니다

    예외 처리도 초반에 대충 시작하면 나중에 제일 큰 부채가 됩니다. 제가 권하는 방식은 단순합니다. 예외를 남길 때 ID, 만료일, 이유를 같이 기록하세요. 특히 YAML 기반 ignore 파일은 이 문맥을 남기기에 좋았습니다.

    vulnerabilities:
      - id: CVE-2023-29491
        expired_at: 2026-12-31
        statement: Upstream fix is not available yet; tracked in internal patch queue
    
    misconfigurations:
      - id: AVD-DS-0002
        paths:
          - "docs/Dockerfile"
        statement: Documentation image intentionally runs as root for example output

    제가 이 방식을 선호한 이유는, 예외를 기술 부채로 보이게 만들기 때문입니다. 그냥 .trivyignore에 ID만 추가해두면 몇 달 뒤에는 아무도 왜 허용했는지 기억하지 못합니다. 반면 만료일과 설명이 남아 있으면, 분기 점검 때 예외를 다시 심사할 수 있습니다.

    4. 어떤 기준으로 운영했는지: 무조건 차단보다 “신규 유입 차단”이 먼저였습니다

    1년 돌리고 나서 가장 확신하게 된 건, 보안 스캔의 성패는 탐지 정확도보다 운영 규칙의 해상도에 달려 있다는 점입니다. 리포트만 잘 뽑아서는 아무 일도 안 일어납니다. 누가 무엇을 보고 언제 막고 언제 예외로 넘길지 정해져야 비로소 도구가 시스템이 됩니다.

    운영 단계 스캔 대상 차단 기준 추천 방식 쓰지 말아야 할 방식
    초기 도입 컨테이너 이미지 경고만, 차단 없음 리포트 해석 루틴부터 정착 첫날부터 전체 서비스 일괄 차단
    안정화 단계 이미지 + IaC CRITICAL 또는 신규 HIGH 신규 서비스 우선 적용 기존 부채와 신규 유입을 같은 기준으로 처리
    정착 이후 이미지 + IaC + secret 정책 위반, 신규 HIGH/CRITICAL, 만료된 예외 예외 만료 관리와 추세 분석 병행 예외를 영구 허용처럼 방치

    제가 현장에서 자주 내린 판단은 아래와 같습니다.

    • exit code 1이 발생했다: 무조건 “보안팀 이슈”가 아니라, 우선 신규 유입인지 기존 누적인지부터 구분합니다
    • HIGH/CRITICAL이 많다: 애플리케이션 패키지보다 베이스 이미지 교체 가능성을 먼저 봅니다
    • misconfig 경고가 반복된다: 개별 서비스 수정 대신 템플릿과 헬름 차트 기본값을 손봅니다
    • secret 탐지가 나왔다: 파일 삭제로 끝내지 말고 키 회전, 배포 환경, Git 히스토리 노출까지 확인합니다

    이 기준이 실무적으로 유용했던 이유는 숫자에 덜 매달리게 해주기 때문입니다. 결과 개수는 서비스 성격에 따라 달라집니다. 반면 신규 유입, 반복 패턴, 만료된 예외는 팀의 운영 건강도를 훨씬 잘 보여줍니다.

    5. 실제로 겪은 문제들: 속도, 노이즈, 예외 관리, 그리고 가장 흔한 오해

    여기부터가 회고의 핵심입니다. 도입 자체보다 오래 가는 실패 모드를 알아야 팀이 덜 지칩니다.

    5-1. DB 업데이트 때문에 느려지는 문제

    표면적으로는 “Trivy가 느리다”로 보이지만, 근본 원인은 대개 매 잡마다 DB를 새로 받는 구조입니다. 특히 ephemeral runner를 쓰면 이 비용이 더 자주 드러납니다. 제 기준에서는 매 커밋 전체 스캔보다, DB 준비를 분리하고 실제 게이트는 핵심 브랜치나 배포 직전 단계에 두는 쪽이 훨씬 오래 갔습니다.

    5-2. 오설정을 본 줄 알았는데 사실 안 본 문제

    이건 실무에서 꽤 위험합니다. 취약점 스캔 결과가 잘 나오니까 팀은 “IaC도 같이 봤겠지”라고 생각합니다. 그런데 fs나 image 계열에서 misconfig 스캐너를 명시하지 않으면 기대한 범위를 다 보지 못할 수도 있더라고요. 근본 원인은 도구 성능이 아니라 스캔 범위에 대한 잘못된 가정입니다.

    5-3. 수정 불가능한 항목이 너무 많이 보이는 문제

    --ignore-unfixed 없이 모든 항목을 그대로 내보내면 개발자 입장에서는 행동 기준이 흐려집니다. “지금 고칠 수 없는 항목”과 “지금 당장 베이스 이미지 업데이트로 줄일 수 있는 항목”을 분리하지 않으면 리포트는 금방 소음이 됩니다. 저는 그 뒤로 “행동 가능한 결과 우선” 원칙을 고정했습니다.

    5-4. 예외 처리 파일이 영구 창고가 되는 문제

    예외는 필요합니다. 문제는 예외 자체가 아니라 재검토 주기가 없는 예외입니다. 파일 하나에 ID가 계속 쌓이기 시작하면, 그 순간부터 스캔 결과의 신뢰도는 빠르게 떨어집니다. 제 경험상 예외 항목은 추가보다 제거가 더 어렵기 때문에, 처음부터 만료일과 이유를 강제하는 편이 낫습니다.

    # 현재 결과 저장
    trivy image --severity HIGH,CRITICAL --format json --output current-scan.json my-app:latest
    
    # 새로 유입된 취약점 식별에 필요한 최소 필드만 추출
    jq '.Results[]?.Vulnerabilities[]? | {VulnerabilityID, PkgName, InstalledVersion, FixedVersion, Severity}' current-scan.json

    이 출력에서 제가 보는 건 단순 개수가 아닙니다. PkgName과 InstalledVersion, FixedVersion 조합을 보면 “패키지 업그레이드로 해결 가능한지”, “베이스 이미지 교체가 먼저인지” 판단이 빨라집니다. 리포트는 많아도, 실제로 손대야 하는 지점은 보통 몇 군데로 수렴합니다.

    Trivy 도입 회고 결과 검증과 예외 관리 대시보드 이미지

    스캔 결과, 심각도 분류, 예외 검토 흐름을 보여주는 결과 화면 이미지입니다.

    6. 재현 가능한 시나리오 하나: 코드보다 베이스 이미지가 먼저인 경우

    실제 현장에서 자주 본 장면을 하나 말씀드리겠습니다. 애플리케이션 코드는 크게 바뀌지 않았는데, 어느 날 이미지 스캔에서 OS 패키지 쪽 HIGH/CRITICAL이 갑자기 눈에 띄게 늘어납니다. 이때 개발팀이 바로 애플리케이션 의존성을 뒤지기 시작하면 시간이 꽤 낭비됩니다. 제가 먼저 확인하는 순서는 거의 항상 같습니다.

    1. 취약점이 애플리케이션 패키지인지 OS 패키지인지 구분합니다
    2. 현재 베이스 이미지 태그가 오래됐는지 확인합니다
    3. 가능하면 공식 베이스 이미지를 최신 태그 계열로 교체해 재빌드합니다
    4. 재스캔 후에도 남는 항목만 애플리케이션 레벨에서 봅니다

    예를 들어 JSON 결과에서 동일한 OS 계열 패키지가 여러 항목에 반복 등장하면, 그때는 코드보다 베이스 레이어를 먼저 의심하는 편이 맞았습니다. 반대로 언어 패키지 매니저 쪽 결과가 중심이면 애플리케이션 의존성 정리가 먼저일 때가 많았습니다. 이 판단을 빨리 내리면 보안 대응이 코드 수리전이 아니라 레이어 선택 문제로 바뀝니다.

    다만 베이스 이미지 교체가 만능은 아닙니다. 베이스를 올리면 libc나 openssl 같은 런타임 동작 차이로 회귀가 날 수 있습니다. 그래서 저는 Trivy 결과를 보고 베이스 이미지를 바꿀 때는, 보안 수정과 기능 검증을 분리하지 않고 같은 변경 세트로 다루는 편이 낫다고 봅니다. 취약점 수치만 줄이고 서비스가 깨지면 운영 관점에서는 실패니까요.

    7. 1년 운영 결과: 좋아진 점과 아직 아쉬운 점

    좋아진 점은 분명했습니다. 첫째, 보안 이슈가 배포 막판에야 드러나는 빈도가 줄었습니다. 둘째, 컨테이너 보안 검토가 특정 담당자의 기억이나 감에 덜 의존하게 됐습니다. 셋째, IaC 오설정이 템플릿 수준에서 반복되는 패턴으로 보이기 시작했습니다. 이건 단순히 경고를 더 본다는 의미가 아니라, 조직이 같은 실수를 덜 반복한다는 쪽에 가깝습니다.

    반면 한계도 뚜렷했습니다.

    • Trivy만으로 애플리케이션 보안이 완성되지는 않습니다
    • 예외 관리가 느슨해지면 스캔 결과는 빠르게 형식화됩니다
    • 캐시와 업데이트 전략을 안 잡으면 CI 병목이 바로 눈에 띕니다
    • 팀 성숙도보다 앞선 강한 차단 정책은 우회를 부릅니다

    그래서 지금의 제 결론은 꽤 명확합니다. Trivy는 만능 도구가 아니라 컨테이너와 설정 검증의 기본 레일로 두는 게 가장 잘 맞습니다. 이 위치를 벗어나면 기대가 과해지고, 이 위치를 정확히 잡으면 투자 대비 효용이 좋습니다.

    Trivy 도입 회고의 도입 전후 비교와 개선 포인트 인포그래픽

    도입 전 수동 점검과 도입 후 자동화된 DevSecOps 워크플로우를 비교하는 요약 이미지입니다.

    8. 마무리: 이런 팀이라면 저는 이렇게 적용하라고 권합니다

    제 추천은 상황별로 꽤 분명합니다. 이제 막 시작하는 팀이라면 이미지 스캔부터 붙이세요. 처음부터 모든 경고를 막지 말고, HIGH/CRITICAL 결과를 읽고 분류하는 루틴부터 만드시는 편이 낫습니다. 이미 CI가 있는 팀이라면 --exit-code 1과 심각도 기준을 먼저 명시하세요. 배포 차단 기준이 불명확하면 스캔은 돌아도 운영은 바뀌지 않습니다.

    Kubernetes나 Terraform 비중이 큰 팀이라면 trivy config . 또는 trivy fs --scanners vuln,misconfig,secret .를 같이 두는 편이 맞습니다. 실무에서 더 자주 사고를 내는 건 코드보다 설정인 경우가 많기 때문입니다. 반대로 애플리케이션 인증, 권한, 비즈니스 로직 취약점까지 Trivy 하나로 해결하려고 하시면 방향이 틀어집니다. 그건 다른 검토 체계가 필요합니다.

    장기 운영 팀이라면 선택이 더 단순합니다. 신규 유입 위험은 차단하고, 기존 부채는 추적하되, 예외는 만료시켜 다시 심사하세요. 이 세 가지만 지켜도 Trivy는 충분히 값어치를 합니다. 결국 Trivy 도입 회고는 도구 평가라기보다 운영 습관 평가에 가까웠습니다. 저에게 남은 교훈도 하나였습니다. 스캐너는 많이 도는 것보다, 팀이 어떤 기준으로 멈추고 어떤 기준으로 진행할지를 분명히 할 때 비로소 도움이 됩니다.

    마지막으로 다시 적어둡니다. 본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다. Trivy를 도입하실 때는 기능 목록보다 운영 규칙부터 설계해보세요. 그 순서가 맞아야 1년 뒤에도 파이프라인에 남습니다.

  • [Cloud] Terraform 원격 상태 관리: S3, GCS, Azure Blob 백엔드 설정 가이드

    로컬 tfstate 파일, 언제까지 쓸 건가요?

    Terraform을 처음 배울 때 저도 그냥 로컬에 terraform.tfstate 파일 두고 썼거든요. 혼자 쓸 때는 문제없었는데, 팀원이 생기고 나서 진짜 지옥이 시작됐습니다. 누군가 apply 해놓고 상태 파일 커밋을 깜빡한 거예요. 그 다음 날 제가 apply 했더니… 리소스 중복 생성 오류가 펑펑 터졌습니다. 😅

    그때부터 Terraform 원격 상태 관리를 제대로 공부하기 시작했어요. S3, GCS, Azure Blob — 클라우드별로 백엔드 설정 방법이 다 다르거든요. 오늘은 실제 현업에서 직접 써본 경험을 바탕으로, 세 가지 백엔드를 한 번에 정리해드리겠습니다.

    ▲ Terraform 원격 백엔드 구성 개요 — 여러 팀원이 동일한 원격 상태 파일을 공유하며 협업하는 구조입니다.


    Terraform 상태(State)란 뭔가요? — 개념부터 잡고 가기

    쉽게 말해서, Terraform은 자기가 만든 인프라를 상태 파일(state file)에 기록해둡니다. 다음에 plan이나 apply를 실행할 때 “지금 실제로 뭐가 있는지”를 이 파일 보고 판단하는 거예요. 일종의 인프라 장부 같은 거죠.

    기본값은 로컬 파일(terraform.tfstate)인데, 여기서 문제가 생깁니다.

    • 팀원끼리 상태 파일이 달라지는 상태 불일치(State Drift) 발생
    • 동시에 apply하면 상태 파일이 깨지는 동시성 문제
    • 상태 파일에 민감한 정보(비밀번호, 키 등)가 포함될 수 있는 보안 문제
    • 로컬 파일 분실 시 인프라 관리 불가능해지는 복구 불가 리스크

    이 모든 걸 해결해주는 게 바로 원격 백엔드(Remote Backend)입니다. 상태 파일을 클라우드 스토리지에 저장하고, 잠금(Lock) 메커니즘으로 동시 접근을 막는 방식이에요.

    백엔드별 잠금 메커니즘 비교

    백엔드 상태 저장소 상태 잠금 암호화 주요 클라우드
    S3 백엔드 AWS S3 DynamoDB 테이블 SSE-S3 / SSE-KMS AWS
    GCS 백엔드 Google Cloud Storage GCS 내장 Google 관리 키 / CMEK GCP
    Azure Blob 백엔드 Azure Blob Storage Blob 리스(Lease) Azure Storage 암호화 Azure

    GCS는 별도 잠금 인프라 없이 내장 기능으로 처리해줘서 가장 설정이 간단하더라고요. S3는 DynamoDB를 따로 만들어야 해서 초반에는 좀 번거롭긴 한데, 한 번 설정하면 정말 탄탄합니다.


    AWS S3 백엔드 설정 — Terraform 원격 상태 관리 실전 가이드

    AWS를 메인으로 쓰신다면 S3 백엔드가 가장 표준적인 선택입니다. 저도 현업에서 가장 많이 써본 조합이고, 안정성도 검증된 방식이거든요.

    1단계: S3 버킷과 DynamoDB 테이블 생성

    먼저 상태 파일을 저장할 S3 버킷과 잠금용 DynamoDB 테이블을 만들어야 합니다. 이걸 직접 Terraform으로 만들어도 되는데, 닭이 먼저냐 달걀이 먼저냐 문제가 생기거든요. 저는 보통 AWS CLI로 먼저 만듭니다.

    # S3 버킷 생성 (버전 관리 활성화 필수!)
    aws s3api create-bucket \
      --bucket my-terraform-state-prod \
      --region ap-northeast-2 \
      --create-bucket-configuration LocationConstraint=ap-northeast-2
    
    # 버전 관리 활성화 — 상태 파일 롤백을 위해 반드시 켜세요
    aws s3api put-bucket-versioning \
      --bucket my-terraform-state-prod \
      --versioning-configuration Status=Enabled
    
    # 퍼블릭 액세스 차단 — 보안상 필수
    aws s3api put-public-access-block \
      --bucket my-terraform-state-prod \
      --public-access-block-configuration \
        BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
    
    # DynamoDB 테이블 생성 (LockID가 파티션 키여야 합니다)
    aws dynamodb create-table \
      --table-name terraform-state-lock \
      --attribute-definitions AttributeName=LockID,AttributeType=S \
      --key-schema AttributeName=LockID,KeyType=HASH \
      --billing-mode PAY_PER_REQUEST \
      --region ap-northeast-2

    2단계: backend.tf 설정 파일 작성

    # backend.tf
    terraform {
      backend "s3" {
        bucket         = "my-terraform-state-prod"
        key            = "prod/ap-northeast-2/vpc/terraform.tfstate"
        region         = "ap-northeast-2"
        encrypt        = true  # 서버 사이드 암호화 활성화
        dynamodb_table = "terraform-state-lock"  # 잠금용 DynamoDB 테이블
    
        # KMS 키로 암호화하려면 아래 추가
        # kms_key_id = "arn:aws:kms:ap-northeast-2:123456789:key/your-key-id"
      }
    }

    💡 key 경로 설계 팁: 환경/리전/서비스명/terraform.tfstate 형태로 계층 구조를 잡으면 나중에 상태 파일이 수십 개 생겨도 관리하기 훨씬 편합니다. 저는 이 규칙 안 지켰다가 나중에 상태 파일 찾느라 정말 고생했거든요. 🤦

    3단계: 백엔드 초기화

    terraform init
    
    # 기존 로컬 상태를 원격으로 마이그레이션할 때
    terraform init -migrate-state

    S3 백엔드용 IAM 최소 권한 정책

    팀원들이 Terraform을 실행할 때 필요한 최소 권한만 부여하세요. 과도한 권한은 보안 위험이거든요.

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            "s3:ListBucket",
            "s3:GetBucketVersioning"
          ],
          "Resource": "arn:aws:s3:::my-terraform-state-prod"
        },
        {
          "Effect": "Allow",
          "Action": [
            "s3:GetObject",
            "s3:PutObject",
            "s3:DeleteObject"
          ],
          "Resource": "arn:aws:s3:::my-terraform-state-prod/prod/*"
        },
        {
          "Effect": "Allow",
          "Action": [
            "dynamodb:DescribeTable",
            "dynamodb:GetItem",
            "dynamodb:PutItem",
            "dynamodb:DeleteItem"
          ],
          "Resource": "arn:aws:dynamodb:ap-northeast-2:123456789012:table/terraform-state-lock"
        }
      ]
    }

    ⚠️ 중요: 123456789012는 여러분의 AWS 계정 ID로 바꿔야 합니다. 또한 /prod/* 경로만 허용해서 다른 환경의 상태 파일은 건드리지 못하게 제한하는 게 좋습니다.