terraform plan이 느릴 때: refresh 부하, 병렬도, 스택 분할 신호
스택이 자라면 어느 날부터 terraform plan이 몇 분씩 걸리기 시작합니다. 결론을 먼저 적으면, plan 시간의 본체는 변경 계산이 아니라 refresh입니다. state에 있는 리소스마다 실제 API를 읽어 현재 상태를 확인하는 단계라, 소요 시간은 코드 복잡도가 아니라 리소스 수 × API 왕복으로 결정됩니다. 그래서 대책도 계산 최적화가 아니라 API 읽기를 줄이는 방향이고, 옵션들로 버티다가 한계가 오면 스택 분할이 근본 대책입니다. state와 plan의 동작 원리는 기초 #5 state의 정체에서 다뤘습니다.
시간이 어디로 가는가: refresh의 구조 #
plan은 세 단계로 움직입니다. state를 읽고, state에 기록된 리소스마다 provider를 통해 실제 상태를 조회하고(refresh), 그 결과와 코드를 비교해 변경을 계산합니다. 마지막 계산은 로컬 연산이라 리소스 수천 개도 순식간입니다. 병목은 가운데의 refresh로, 리소스 하나마다 최소 한 번의 API 호출이 나갑니다.
리소스 500개 스택이면 500회 이상의 API 왕복입니다. 기본 병렬도 10으로 나눠도 왕복 지연이 쌓이고, 더 큰 문제는 AWS API의 rate limit입니다. 호출이 몰리면 스로틀링이 걸리고 provider가 백오프 재시도를 하면서 체감 시간이 급격히 늘어납니다. plan이 느린 스택에서 CloudTrail을 보면 Describe 계열 호출이 벽처럼 쌓여 있는 것이 이 구조입니다.
옵션 사다리: 각 옵션의 올바른 용도와 한계 #
-refresh=false는 refresh를 건너뛰고 state를 그대로 믿는 옵션입니다. 효과는 극적이지만 전제가 있습니다. state와 실물이 일치한다는 확신입니다. 올바른 용도는 코드를 반복 수정하며 plan을 여러 번 돌리는 개발 루프의 중간입니다. 방금 refresh된 것을 알고 있으니 반복 회차에서는 건너뛰어도 안전합니다. 반대로 최종 apply 전의 plan, PR 리뷰에 올릴 plan은 refresh를 포함해야 합니다. drift가 있는데 건너뛰면 존재하지 않는 상태를 근거로 변경을 계산하게 됩니다. drift 관리 자체는 운영 #2의 주제입니다.
-parallelism은 기본 10인 동시 작업 수 조절입니다. 리소스가 많고 스로틀링이 안 걸리는 구간에서는 20〜30으로 올려 단축 효과를 볼 수 있지만, rate limit에 이미 부딪히는 스택에서는 올릴수록 스로틀링과 재시도가 늘어 역효과입니다. 올려 보고 CloudTrail의 ThrottlingException이 늘면 되돌리는 실측 조정이 필요합니다.
-target은 성능 도구로 자주 오용됩니다. 특정 리소스만 지정하면 refresh 범위가 줄어 빨라지는 것은 사실이지만, 이 옵션의 실제 효과는 의존성 그래프의 일부만 적용된 상태를 만드는 것입니다. 반복되는 -target 운영은 스택 전체의 정합이 언제 마지막으로 확인됐는지 아무도 모르는 상태로 흘러갑니다. 긴급 대응의 격리 수단으로만 쓰고, 일상 성능 문제의 답으로 쓰지 않습니다.
CI 쪽에는 구조적 선택지가 하나 더 있습니다. PR마다 도는 plan과 별개로, drift 감지용 refresh는 야간 스케줄로 분리하는 방식입니다. PR plan은 직전 스케줄의 신뢰를 업고 빠르게 돌고, 전체 refresh는 하루 한 번 돌아 drift를 잡습니다.
근본 대책: 분할 신호를 읽습니다 #
옵션은 증상 완화이고, 리소스 수 자체가 큰 스택의 근본 대책은 분할입니다. 다음 신호가 겹치면 분할 시점입니다.
- plan이 5분을 넘기 시작합니다: PR 리뷰 루프에서 plan을 기다리는 시간이 리뷰 자체를 방해하는 수준이 기준입니다.
- state 리소스 수가 수백 개를 넘습니다: 절대 기준은 아니지만, 수백 개를 넘으면 refresh 시간과 잠금 경합이 함께 늘어납니다.
- 변경 빈도가 다른 것들이 한 스택에 있습니다: 하루에 몇 번 바뀌는 앱 계층과 1년에 몇 번 바뀌는 네트워크 계층이 한 state에 있으면, 앱 배포마다 네트워크까지 refresh하는 낭비가 구조화됩니다.
- 잠금 대기가 일상이 됩니다: 여러 사람·파이프라인이 한 state를 두고 잠금 경합을 겪는 빈도가 늘면, 성능 문제이자 협업 문제입니다.
분할 기준(계층별·변경 빈도별·블라스트 반경별)과 스택 간 참조 방법은 운영 #5 대규모 구조화에서 다뤘습니다. 성능 관점에서 요점만 얹으면, 분할의 효과는 plan 시간을 리소스 수에 비례해 줄이는 동시에, 자주 바뀌는 스택만 자주 refresh되도록 빈도를 재배치하는 데 있습니다.
정리 #
- plan 시간의 본체는 refresh이고, 소요는 리소스 수 × API 왕복으로 결정됩니다. rate limit에 걸리면 비선형으로 나빠집니다.
- -refresh=false는 개발 반복 루프 전용입니다. 최종 apply 전과 PR용 plan에서는 refresh를 건너뛰지 않습니다.
- parallelism은 실측으로 조정합니다. 스로틀링이 이미 있는 스택에서 올리면 역효과입니다.
- -target은 성능 도구가 아니라 격리 도구입니다. 반복 사용은 부분 적용 상태를 일상화합니다.
- plan 5분 초과, 리소스 수백 개, 변경 빈도 혼재, 잠금 경합이 겹치면 옵션이 아니라 스택 분할이 답입니다.