状态文件造出的东西,和它弄坏的东西
한국어 원문으로 표시합니다.
한 줄 요약
상태 파일은 코드와 실제 인프라를 잇는 유일한 다리이자, 잘못 다루면 팀 전체를 멈추게 하는 단일 실패 지점이다.
왜 이게 필요했나
상태 파일이 존재하는 이유는 네 가지다. 첫째 매핑이다. 코드에 적은 리소스 이름과 실제로 만들어진 리소스의 식별자를 이어 준다. 둘째 의존성 추적이다. 무엇을 먼저 지우고 무엇을 나중에 만들어야 하는지 알려면 관계를 기억해야 한다. 셋째 성능이다. 매번 모든 리소스를 조회하면 API 호출이 폭발하므로 마지막으로 본 값을 캐시처럼 쓴다. 넷째 협업이다. 팀원 사이에 같은 사실을 공유하는 기준점이 된다.
문제는 이 파일이 민감 정보를 평문으로 담을 수 있다는 점이다. DB 비밀번호나 인증서가 그대로 들어간다. 그래서 암호화된 원격 백엔드가 사실상 필수다. 출력에 sensitive = true 를 붙이는 것은 화면 마스킹일 뿐이고 상태 파일 안에서는 값이 그대로 보인다. 이 둘을 혼동하면 "가렸으니 안전하다"고 착각하게 된다.
어떻게 동작하나
동시 적용을 막는 장치가 잠금이다. apply 를 시작할 때 잠금 항목을 조건부로 생성하고, 다른 사람이 이미 잡고 있으면 충돌 에러가 난다. 여기서 자주 놀라는 지점이 대기 시간의 기본값이 0초, 즉 즉시 실패라는 것이다. 조용히 매달리는 것보다 빨리 실패해 사람이 상황을 보는 편이 낫다는 설계다.
잠금은 남기도 한다. 네트워크가 끊기거나, CI 러너가 타임아웃으로 죽거나, 누군가 Ctrl+C 로 강제 중단하면 잠금만 남는다. 강제 해제 명령이 있지만 다른 사용자가 실제로 작업 중이면 상태가 깨질 수 있으므로 최후의 수단이다. 먼저 그 잠금을 누가 언제 잡았는지 확인하는 절차가 있어야 한다.
상태는 손으로 고치는 게 아니라 전용 명령으로 다룬다. state mv 로 옮기고, state rm 으로 관리에서 빼고, import 로 이미 존재하는 리소스를 가져오고, 리팩터링은 moved 블록으로 표현한다. 특히 state rm 은 관리에서만 빼는 것이고 실제 리소스는 지워지지 않는다는 점을 정확히 알아야 한다. 이걸 삭제 명령으로 오해하면 정반대의 사고가 난다.
규모가 커지면 상태를 쪼개야 한다. 모든 것을 한 파일에 몰면 plan 이 10분 이상 걸리고 API rate limiting 이 걸린다. 네트워킹·컴퓨트·DB·모니터링처럼 컴포넌트별로 나눠 blast radius, 즉 폭발 반경을 최소화한다. 환경 분리 방식도 선택지가 있다. 워크스페이스는 코드 중복이 없지만 격리가 약하고 폭발 반경이 넓다. 디렉터리 분리는 일부 중복이 생기지만 환경별로 별도 백엔드와 IAM 을 둘 수 있어 폭발 반경이 좁다. 그래서 프로덕션은 디렉터리 분리, 단기 테스트 환경은 워크스페이스가 기본 공식이다.
현장에서 만나는 모습
드리프트는 세 유형으로 나뉜다. 속성이 바뀐 구성 드리프트, 코드 밖에서 만들어지거나 삭제된 존재 드리프트, 참조 관계가 깨진 종속성 드리프트다. 원인도 정해져 있다. 콘솔 수동 변경, 오토스케일링 같은 시스템 자동 변경, 프로바이더 업그레이드로 인한 기본값 변경, 병렬 apply.
감지는 최소 하루 한 번은 돌리고, 심각도 규칙을 붙인다. 보안그룹이나 IAM 정책 변경은 CRITICAL, 삭제나 교체 액션은 무조건 HIGH 이상이다. 다만 모든 드리프트를 감지하려 하면 false positive 가 넘쳐난다. 오토스케일러가 관리하는 desired_capacity 나 desired_size 같은 필드는 ignore_changes 로 빼야 알림이 신호로 남는다.
마지막은 문화다. 긴급 장애 대응에서 콘솔 접속을 완전히 금지하는 것은 비현실적이다. 대신 긴급 변경 후 24시간 이내에 코드를 동기화하는 문화를 정착시키고 자동 감지가 이를 감시하게 한다. 규칙으로 막는 대신 되돌아오게 만드는 설계다.
상태 파일을 잃어버렸을 때와 잠갔을 때
상태는 코드도 아니고 실제 자원도 아닌 제3의 진실이다. 이 셋이 어긋나는 방식이 곧 IaC 에서 겪는 사고의 목록이다.
상태가 없어지면 자원은 남고 관리만 사라진다. 테라폼은 상태에 없는 자원을
"아직 안 만든 것" 으로 보므로, 다음 apply 에서 같은 것을 또 만들려 하다가
이름 충돌로 실패하거나, 이름이 자동 생성되는 자원이라면 조용히 둘이 된다.
복구는 import 로 하나씩 되돌리는 길밖에 없다. 그래서 원격 백엔드에는
버전 관리와 삭제 보호를 반드시 함께 켠다.
상태에는 비밀이 평문으로 들어간다. DB 비밀번호를 변수로 넘겼다면 그 값이
상태 파일에 그대로 적힌다. 출력에서 sensitive = true 로 가려도 저장되는 값은
같다. 상태 저장소를 비밀 저장소와 같은 등급으로 다뤄야 하는 이유다. S3 라면
암호화와 접근 정책을, 로컬이라면 아예 커밋하지 않도록 .gitignore 를 먼저
확인한다.
잠금이 없으면 두 사람이 동시에 다른 미래를 쓴다. 두 apply 가 겹치면 나중에
끝난 쪽이 앞의 결과를 지운 상태를 올린다. 자원은 만들어졌는데 상태에는 없는,
가장 고치기 번거로운 상태가 된다. S3 백엔드는 DynamoDB 표로, GCS 와 Azure 는
자체 기능으로 잠근다.
잠금이 남아 있을 때 함부로 풀지 않는다. CI 가 중간에 죽으면 잠금이 남는다.
force-unlock 은 정말로 아무도 안 돌고 있을 때만 쓴다. 돌고 있는 apply 를
풀어 버리면 앞 문단의 상황을 손으로 만드는 셈이다. 잠금 정보에 적힌 사람과
시각을 먼저 확인한다.
손으로 고친 것은 다음 계획에서 드러난다. 급해서 콘솔에서 바꾼 설정은
terraform plan 에 되돌리겠다는 계획으로 나타난다. 이때 선택은 둘이다:
코드를 현실에 맞추거나, 현실을 코드에 맞추거나. 세 번째 선택인 "일단 무시" 를
고르면 그 자원은 다음 사람이 무심코 apply 할 때 되돌아간다.
terraform plan -refresh-only # 코드는 그대로 두고, 현실과의 차이만 본다
terraform state list # 상태가 아는 자원의 목록
terraform state show <주소> # 상태가 기억하는 속성
다음 실습에서 할 것
진짜 선언형 도구(OpenTofu)로 이 글의 사고를 차례로 일으킨다. 상태 파일을 옮겨 같은 서버가 둘이 되는 것을 보고, lineage 가 다른 상태를 덮어쓰려다 거절당하고, import 로 잃은 자원을 되찾고, 적용 중인 상태 잠금에 막혀 보고, 수명이 다른 층의 상태를 나눈다. 이어지는 실습에서는 sensitive 로 가린 비밀이 상태·계획 파일의 어디에 평문으로 남는지 찾고 상태 암호화로 막는다. 두 실습 뒤의 퀴즈에서 상태 파일의 존재 이유, 민감 정보 취급, 잠금과 강제 해제의 위험, 상태 분할과 폭발 반경을 점검한다.