한 줄 정의

인프라 코드 테스트는 커버리지를 채우거나 피라미드 형식을 맞추는 활동이 아니라, 변경에 따르는 위험을 관리 하기 위해 위험이 있는 곳에 검증을 배치하는 활동입니다.

쉽게 말하면

요리를 하면서 간은 언제 봐야 할까요.

다 만들어 손님상에 내기 직전에 한 번 보면 이미 늦습니다. 고칠 방법이 없거나, 처음부터 다시 만들어야 합니다. 그래서 볶으면서 보고, 국물을 낼 때마다 봅니다.

그런데 “레시피에 소금 한 스푼이라고 적혀 있고 한 스푼을 넣었다”를 다시 확인하는 건 의미가 없습니다. 적힌 대로 넣었으니까요. 정작 봐야 할 건 재료 상태나 불 세기에 따라 결과가 달라지는 부분입니다. 선언형 인프라 코드를 그대로 되읽는 테스트가 딱 이 헛수고입니다.

그리고 조리 중에 아무리 간을 봐도, 손님이 실제로 어떻게 먹을지는 내보내봐야 압니다. 프로덕션 테스트가 필요한 이유입니다.

왜 중요한가?

인프라 구축을 일회성 활동으로 보면 테스트 묶음을 만들 이유가 없어 보입니다. 하지만 인프라는 구축 후에 예상보다 훨씬 더 많이 변경되고, 그때마다 “이 변경이 시스템을 망가뜨리는가”라는 질문이 반복됩니다.

빠른 피드백이 없으면 문제의 원인을 찾는 데 드는 시간이 늘어나고, 그 비용을 피하려고 변경을 미루면 기술 부채가 쌓입니다. 엄격하고 짧은 피드백 주기가 지속적인 테스트의 핵심입니다.

한편 인프라 코드는 애플리케이션 코드와 상당히 다르기 때문에, 애자일 테스트 기법을 그대로 가져오면 대부분 헛돕니다. 애플리케이션 테스트의 기술과 관점 중 일부만 실용적으로 적용해야 합니다.

핵심 내용

지속적인 테스트란

기존에는 전체 기능을 구현한 뒤 테스트했습니다. 타임박스(스프린트) 방식은 개발 중 주기적으로, 린·칸반은 스토리가 완료되면 테스트합니다.

진정한 지속적 테스트는 이보다 더 자주 합니다. 스토리를 완성하기 전에 코드를 쓰고 테스트하며, 중앙 빌드 시스템으로 하루에 한 번 이상 푸시합니다.

각 테스트는 성격에 따라 두 가지로 나뉩니다.

구분시점
즉각적 테스트코드를 쓰거나 푸시하자마자편집기의 구문 강조·유효성 검사(LSP), entr·inotifywait 기반 자동 실행, 페어 프로그래밍
최종 테스트시간이 흐른 뒤, 정해진 일정 또는 수동 검토 후릴리스 전 통합 테스트, 승인 단계

CI 빌드와 CD 파이프라인은 변경을 푸시할 때마다 즉시 실행되어야 합니다. 주기적으로만 실행하면 여러 사람의 변경이 한 실행에 섞이고, 하나라도 실패했을 때 어떤 변경이 원인인지 파악하기가 훨씬 어려워집니다.

인프라에서 무엇을 테스트하는가

품질 보증(QA)은 코드를 적용할 때의 위험을 관리하는 것 입니다. “코드를 적용할 때 시스템이 중단될까? 인프라가 제대로 작동할까? 운영 기준을 충족할까? 규제를 준수할까?”라는 질문에 답하는 활동입니다.

흔히 자동화된 테스트라 하면 단위 테스트나 UI 기반 테스트 같은 기능 테스트를 떠올리지만, 위험의 범위는 기능적 결함보다 넓습니다. 순수 기능 이상의 제약과 요구사항을 비기능 요구사항(NFR) 또는 교차기능 요구사항(CFR) 이라고 합니다.

위험 영역인프라에서 테스트하는 것
코드 품질코드가 읽기 쉽고 팀 표준(형식·구조)을 따르는가. 구문 오류·복잡도 스캔
기능성애플리케이션을 인프라에 배포해 올바르게 실행되는지 확인. 인프라 구성을 간접 검증
보안오픈 포트, 사용자 계정 처리, 액세스 권한. 코드 스캔부터 취약점 스캐너, 프로덕션 모니터링까지
규정 준수법률·산업 표준·조직 정책 위반을 신속히 파악하고 감사 증거를 제공
성능A지점에서 B지점까지의 네트워크 속도 등, 배포 전에 구성·플랫폼 문제를 발견
확장성자동 확장 클러스터에 노드가 실제로 추가되는가, 추가해도 다른 곳의 병목으로 효과가 없지는 않은가
가용성클러스터 노드를 삭제했을 때 자동 교체되는가. 자동 복구되지 않는 시나리오가 정상 처리되는가
운용성모니터링(오류 생성·탐지·보고), 로깅, 자동화된 관리 작업

인프라 코드 테스트의 세 가지 문제

문제 1 — 선언형 코드는 테스트 가치가 낮다

값이 하드코딩된 선언에 대한 테스트는 받아쓰기 연습 이 됩니다. 아래 테스트는 코드를 다른 문법으로 옮겨 적은 것에 불과하고, 코드를 바꿀 때마다 테스트도 같이 고쳐야 합니다.

subnet:
  name: private_A
  address_range: 192.168.0.0/16
assert:
  subnet("private_A").exists
assert:
  subnet("private_A").address_range is("192.168.0.0/16")

이 테스트로 막을 수 있다고 기대하는 위험은 세 가지인데, 어느 것도 이 테스트를 정당화하지 못합니다.

위험왜 이 테스트가 답이 아닌가
인프라 코드가 적용되지 않았다실제 위험이지만, 각 선언을 모두 테스트할 필요는 없습니다. 한 번의 테스트로 충분합니다
도구가 적용에 실패했는데 오류를 반환하지 않았다도구의 버그이므로 버그를 고치거나 도구를 바꿀 문제입니다. 알려진 문제를 테스트로 막는 건 괜찮지만, 도구 버그에 대비해 코드를 세부 테스트로 덮는 건 낭비입니다
코드를 바꾸고 테스트를 안 바꿨다순환 로직입니다. 테스트를 없애면 위험도 함께 사라집니다

선언형 코드 테스트가 유용해지는 두 가지 상황 이 있습니다.

첫째, 코드가 다른 결과를 만들 수 있을 때입니다. 변수만 추가한 수준(${MY_APP}-${MY_ENVIRONMENT})이라면 값을 잘못 넣어도 도구가 적용에 실패하며 오류를 반환하므로 여전히 위험이 없습니다. 그러나 함수 호출이 들어가면 결과의 다양성이 커지고, 테스트해야 할 로직이 생깁니다.

subnet:
  name: ${MY_APP}-${MY_ENVIRONMENT}
  address_range: get_networking_subrange(
    get_vpc(${MY_ENVIRONMENT}),
    data_centers.howmany,
    data_centers.howmany++
  )

두 함수 중 하나가 실패하거나 예상치 못한 방식으로 상호작용할 수 있으므로, 이 코드의 결과는 입력과 전후 사정에 따라 달라집니다.

둘째, 여러 선언을 결합할 때입니다. 주소 블록, 로드 밸런서, 라우팅 규칙, 게이트웨이 설정 각각은 테스트가 불필요할 만큼 간단하지만, 이들의 조합 은 테스트할 가치가 있습니다. 선언한 항목이 생성됐는지보다 원하는 결과를 얻을 수 있는지를 테스트하는 것이 더 중요합니다.

복잡해지면 로직을 밖으로 꺼낸다

복잡한 테스트가 필요할 정도로 선언형 코드가 복잡해졌다면, 로직을 떼어내 절차적 언어로 작성된 라이브러리로 보내야 합니다. 그 함수를 별도로 테스트하면 선언 코드 자체의 테스트는 다시 단순해집니다.

문제 2 — 인프라 코드 테스트는 느리다

인프라 코드를 테스트하려면 인프라에 적용해야 하고, 특히 클라우드 플랫폼에서 테스트용 인스턴스를 프로비저닝하는 건 느립니다. 다음 전략을 조합해 해결합니다.

전략내용
인프라 분할작고 느슨하게 결합된 조각은 프로비저닝과 테스트가 빠르고, 위험 노출이 적어 테스트를 쓰고 유지하기도 쉽습니다
의존성 최소화테스트 인스턴스를 남에게 의존하거나 공유하면 느리고, 비싸고, 신뢰할 수 없고, 데이터가 일관되지 않습니다. 테스트 더블로 분리합니다
프로그레시브 테스트실행 시간이 짧은 테스트를 먼저 돌려 실패 시 빠른 피드백을 얻고, 통과한 후에만 길고 넓은 테스트를 실행합니다
임시 vs 영구 인스턴스임시는 시간이 더 들지만 명확하고 일관된 결과를, 영구는 시간을 아끼지만 변경사항이 남고 불일치가 늘어납니다
온라인 vs 오프라인오프라인 테스트(구문 검사, 로컬 VM·컨테이너)가 훨씬 빠르므로 먼저 실행합니다. 테스트 더블로 클라우드 API를 에뮬레이션할 수도 있습니다

신뢰할 수 없는 테스트는 제거 대상이다

테스트가 일관성 없는 결과를 내면 이유를 조사해 고치거나 교체해야 합니다. 거의 실패하지 않거나 항상 다른 테스트와 함께 실패하는 테스트는 제거해도 됩니다. 테스트 대상이 아니라 테스트 자체 를 고치는 데 시간이 더 든다면 문제를 단순화할 방법을 찾아야 합니다.

문제 3 — 의존성은 테스트를 복잡하게 만든다

의존 인프라를 설정하는 시간이 테스트를 느리게 만듭니다. 의존성을 테스트 더블 로 대체하면 컴포넌트를 분리해 빠르게 테스트할 수 있습니다. 테스트 더블의 유형은 mock, fake, stub 세 가지입니다.

클라우드 벤더 API를 모방하는 도구(LocalStack, Moto)로 로컬 모의 클라우드에 인프라 코드를 적용할 수 있습니다. 다만 이런 도구는 네트워킹 구조가 정상 작동하는지는 알려주지 못하고, 대략적으로 유효한지만 알려줍니다. 그래서 인프라 플랫폼 자체보다 인프라 컴포넌트에 테스트 더블을 쓰는 편이 더 유용 합니다.

프로그레시브 테스트

프로그레시브 테스트는 테스트 묶음을 순서대로 실행하는 것입니다. 범위가 좁고 의존성이 낮은 구성 요소를 빠르게 테스트한 다음, 점진적으로 더 많은 컴포넌트와 통합해가며 실행합니다.

범위가 넓은 테스트가 실패하면 조사해야 할 컴포넌트와 의존성의 범위도 넓습니다. 그래서 가능한 한 가장 작은 범위로 가장 빠른 시점에 잠재적 실패 영역을 찾아야 합니다.

또 다른 목표는 전체 테스트 모음을 관리 가능한 상태로 유지하는 것, 즉 단계 간 테스트 중복을 피하는 것입니다. 서버 구성 코드의 로그 폴더 권한 테스트는 서버 구성 단계에서 실행되어야 하며, 클라우드에 전체 스택을 프로비저닝하는 단계에 또 있어서는 안 됩니다.

테스트 피라미드와 인프라 다이아몬드

테스트 피라미드는 하위 계층(단위 테스트)에 테스트가 가장 많고 위로 갈수록(통합 → UI) 적어지는 모델입니다. 애플리케이션 소프트웨어 개발을 위해 고안됐습니다.

선언형 인프라 코드베이스에서는 피라미드의 가치가 떨어집니다. Terraform·CloudFormation으로 작성된 하위 레벨 선언형 스택 코드는 단위 테스트를 하기에는 너무 크고 플랫폼에 따라 성격이 달라집니다. 선언형 모듈은 (문제 1에서 본 대로) 테스트 가치가 낮은 데다 인프라 없이 테스트할 수 있는 것도 적습니다.

즉 하위 계층에서의 인프라 테스트는 피라미드가 제안하는 것만큼 많지 않습니다. 그래서 선언형 인프라의 테스트 묶음은 가운데가 두꺼운 다이아몬드 형태가 됩니다.

모델계층 (아래 → 위)어울리는 코드베이스
피라미드단위 테스트(다수) → 통합 테스트 → UI 테스트(소수)명령형 언어로 쓰인 동적 라이브러리. 다양한 결과를 만드는 작은 컴포넌트가 많아 테스트할 것도 많음
다이아몬드오프라인 테스트(적음) → 스택 테스트(다수) → 시스템 테스트(적음)선언형 스택 코드. 하위 계층에서 의미 있게 테스트할 것이 적음

스위스 치즈 모델

주어진 테스트 계층에는 스위스 치즈 조각의 구멍처럼 결함이나 위험을 놓치는 부분이 있습니다. 그러나 여러 계층을 겹쳐 놓으면 완전히 통과할 수 있는 구멍이 없는 블록처럼 보입니다.

위험 관리는 소프트웨어 산업 밖에서 온 개념입니다. 인프라 테스트에서 이 모델을 쓰는 요점은 위험이 예상되는 위치를 집중적으로 테스트한다 는 것입니다. 가능하면 초기 계층에서 문제를 찾고 싶지만, 중요한 건 전체 모델의 어딘가에서 반드시 테스트된다는 사실입니다.

Quote

핵심은 형식에 맞추는 것이 아니라 위험에 기초하여 테스트하는 것입니다.

인프라 딜리버리 파이프라인

CD 파이프라인은 프로그레시브 테스트의 구현과 프로덕션 경로의 코드 전달을 결합합니다. 변경을 검토하고 환경에서 예비 테스트를 수행할 수는 있지만, 변경을 수동으로 배포·적용해서는 안 되고 즉흥적으로 구성 옵션을 선택하거나 결정을 내려서도 안 됩니다. 그런 작업은 코드로 정의되어 시스템에서 실행되어야 합니다.

파이프라인의 다운스트림 단계에서 오류를 발견하더라도 그 단계에서 고치지 않습니다. 저장소의 코드를 수정하고 푸시한 뒤 새 파이프라인을 시작합니다. 그래야 모든 변경이 완전하게 테스트됩니다.

파이프라인의 각 단계

요소내용
트리거단계 실행을 시작하는 이벤트. 코드 푸시, 이전 단계 성공, 또는 사람의 수동 트리거
작업단계가 하는 일. 예를 들어 코드 적용 → 스택 프로비저닝 → 테스트 실행 → 스택 삭제
승인통과/실패 표시 방식. 자동화된 테스트 통과로 ‘그린’ 처리하거나, 탐색 테스트·거버넌스 종료를 위해 사람이 승인
결과단계별 아티팩트와 자료. 인프라 코드 패키지, 테스트 보고서 등

각 단계가 넓혀가는 세 가지 범위

프로그레시브 테스트는 세 축을 동시에 넓혀갑니다. 각 축은 독립적으로 설계할 대상이 아니라, 같은 파이프라인 단계에서 함께 커집니다.

초기 단계이후 단계
컴포넌트 범위테스트 웹 서버 패키지 설치 → 서버 역할웹 서버 인프라 스택 → 다른 스택과 통합한 애플리케이션 인프라
의존성 범위모의 의존성 (예: 모의 ID 관리 서비스)의존성의 테스트 인스턴스 → 프로덕션 인스턴스
플랫폼 요소로컬 VM·컨테이너에서 테스트플랫폼의 독자적 요소 → 플랫폼의 스택에서 테스트

반드시 필요한 단계만 추가한다

각 단계는 딜리버리 프로세스에 시간과 비용을 더합니다. 단순히 완성도를 높이려는 목적으로 컴포넌트·통합마다 별도 단계를 만들지 않습니다. 추가 오버헤드만큼의 가치가 있을 때만, 즉 속도·안정성·비용·제어를 위해 필요할 때만 하나의 테스트를 여러 단계로 분할합니다.

파이프라인 소프트웨어 선택지

유형특징
빌드 서버Jenkins, Team City, Bamboo, GitHub Actions스트림지향이 아닌 작업지향 이라 여러 작업에 걸친 코드·아티팩트·실행 버전을 본질적으로 연관시키지 않음
CD 소프트웨어GoCD, ConcourseCI, BuildKite파이프라인 개념을 중심으로 구축되어 코드 버전과 아티팩트를 앞뒤로 추적 가능
SaaSCircleCI, TravisCI, AppVeyor, Drone, BoxFuse호스팅 서비스형
클라우드 플랫폼 서비스AWS CodeBuild/CodePipeline, Azure Pipelines벤더 제공
소스 코드 저장소 서비스GitHub Actions, GitLab CI/CD저장소에 CI 지원이 통합됨

여기 언급된 서비스는 모두 응용 소프트웨어를 염두에 두고 설계됐습니다. 추가 작업이 필요할 수 있지만 대부분 인프라용 파이프라인을 구축할 수 있습니다.

Atlantis처럼 Terraform PR을 관리하며 planapply를 실행하는 제품도 있는데, 테스트를 실행하지는 않고 코드 검토와 승인을 처리하는 제한된 파이프라인 을 만드는 데 씁니다.

프로덕션 테스트

시스템이 복잡해지고 규모가 커지면 프로덕션 외부에서 확인할 수 있는 위험의 범위가 축소됩니다. 릴리스 전 테스트가 위험을 포괄적으로 해결한다고 믿으면 두 가지 결과를 얻습니다. 하나는 수확 체감 그래프의 최고점을 넘길 때까지 릴리스 전 테스트에 과잉 투자 하는 것이고, 다른 하나는 프로덕션 테스트에 과소 투자 하는 것입니다.

프로덕션 외부에서 복제할 수 없는 것

항목이유
데이터복제 가능한 용량보다 큰 데이터셋, 사용자가 만드는 예상치 못한 값과 조합
사용자사용자가 너무 많아 QA 담당자보다 훨씬 창의적으로 이상한 작업을 하는 사람이 나옴
트래픽실제로 겪는 트래픽 작업의 수와 유형을 복제할 수 없음
동시성여러 사용자를 에뮬레이션할 수는 있지만, 동시에 수행하는 작업의 비정상적 조합은 복제 불가

이 구분은 결국 아는 모름(known unknown)과 모르는 모름(unknown unknown) 의 차이입니다. 프로덕션 전 테스트는 아는 모름을 해결하고, 프로덕션에서의 테스트는 예측할 수 없는 위험인 모르는 모름을 해결합니다. 프로덕션 외의 환경에서는 “작동이 되는가, 코드가 실행되는가, 예측 가능한 범위에서 실패하는가, 이전에 실패한 방식으로 실패하는가”를 명확히 정의해 테스트해야 합니다.

프로덕션 테스트의 위험 관리

프로덕션에서의 테스트는 새로운 위험을 만듭니다. 이를 관리하는 방법입니다.

방법내용
모니터링테스트로 인해 문제가 발생하는 시기를 감지해 신속하게 중단시킬 수 있다는 확신을 줌
관찰 가능성시스템 내부의 세부 상황을 파악해 조사·수정 속도를 높이고 테스트 항목의 품질도 향상
다운타임 없는 배포빠르고 원활하게 배포·롤백할 수 있으므로 오류의 위험이 줄어듦
점진적 배포컴포넌트의 다른 버전을 동시에 실행하거나 사용자 그룹별로 다른 구성을 주어, 사용자에게 노출하기 전에 테스트
데이터 관리데이터를 부적절하게 변경하거나 민감 데이터를 노출하지 않음. 실제 작업을 트리거하지 않는 테스트 데이터 기록을 유지
카오스 엔지니어링알려진 유형의 오류를 의도적으로 주입해 수습 시스템이 올바르게 작동하는지 테스트

모니터링은 소극적 테스트다

모니터링은 조치를 취하지 않고 결과를 확인하지도 않으므로 실제로는 테스트가 아니지만, 사용자의 일반적인 작업을 관찰하고 바람직하지 않은 결과를 살펴봅니다. 시스템의 위험을 관리하기 위한 작업의 일부이므로 테스트 전략에 포함되어야 합니다.

비교 / 트레이드오프

한쪽다른 쪽선택 기준
임시 vs 영구 인스턴스임시: 명확하고 일관된 결과영구: 실행 시간 단축, 대신 변경사항이 남고 불일치 누적테스트 환경별로 선택하고 작업 결과에 따라 재검토
온라인 vs 오프라인오프라인: 훨씬 빠름, 먼저 실행온라인: 실제 플랫폼 프로비저닝 필요네트워킹 구조는 온라인, 서버 패키지 설치는 오프라인 가능
피라미드 vs 다이아몬드피라미드: 명령형·동적 라이브러리다이아몬드: 선언형 스택 코드코드베이스의 성격이 결정. 형식을 먼저 정하지 않음
릴리스 전 vs 프로덕션 테스트릴리스 전: 아는 모름프로덕션: 모르는 모름릴리스 전 테스트에 과잉 투자하면 프로덕션 테스트에 과소 투자하게 됨
테스트 더블 대상인프라 플랫폼 전체 모방인프라 컴포넌트플랫폼 모방은 대략적 유효성만 알려주므로 컴포넌트 쪽이 더 유용

내 생각

  • “선언형 코드는 테스트 가치가 낮다”는 애플리케이션 테스트 직관을 그대로 옮기는 실수를 막는 브레이크입니다. Terraform 리소스마다 assert를 거는 순간 코드와 테스트를 두 벌 관리하게 됩니다. 테스트는 변수·함수·조합처럼 결과가 갈리는 지점에만 겁니다.
  • 다이아몬드 모델은 “인프라에 단위 테스트가 부족하다”는 자책을 끊어줍니다. 인프라 테스트의 무게 중심은 스택 테스트고, 여기에 투자하는 게 맞습니다.
  • 다운스트림 단계에서 고치지 않고 새 파이프라인을 돌린다는 규칙이 실무에서 가장 자주 깨집니다. 스테이징에서 손으로 고치는 순간 그 수정은 어디에도 테스트되지 않은 채 남고, Ch06에서 본 구성 드리프트가 시작됩니다.
  • 모니터링을 테스트 전략에 포함시키라는 관점이 실용적입니다. 모니터링 알람을 운영 업무로만 보면, 정작 프로덕션에서만 드러나는 모르는 모름을 잡을 유일한 장치를 테스트 계획 밖에 두게 됩니다.

관련 개념