한 줄 정의

인프라의 모든 구성 요소를 버전 관리되는 텍스트 코드로 정의하되, 요구사항의 성격에 따라 선언형·명령형 언어를 나눠 쓰는 실행 방법입니다.

쉽게 말하면

식당에서 원하는 음식을 얻는 방법은 두 가지입니다.

하나는 “미디엄레어 스테이크 주세요”라고 결과만 주문하는 것입니다. 굽는 방법은 주방이 알아서 하고, 몇 번을 주문해도 매번 같은 스테이크가 나옵니다. 이게 선언형 코드입니다 — 원하는 최종 상태(desired state)만 적으면 도구가 그 상태를 만드는 방법을 알아서 처리합니다.

다른 하나는 “팬을 달구고, 소금을 뿌리고, 한 면당 3분씩 굽고…”라고 절차를 단계별로 적는 요리법입니다. 손님 수나 화력에 따라 조리 단계를 조건문·반복문으로 바꿀 수 있습니다. 이게 명령형 코드입니다 — 결과를 만드는 로직을 직접 씁니다.

둘 중 하나가 옳은 게 아닙니다. 매번 똑같은 결과를 원하면 주문(선언형)이 깔끔하고, 상황에 따라 결과가 달라져야 하면 요리법(명령형)이 필요합니다. 이 장의 핵심은 “어느 패러다임을 고를까”가 아니라 “이 요구사항엔 어느 쪽이 맞나”를 따지는 것입니다.

왜 중요한가?

‘모든 것을 코드로 정의한다’는 인프라를 빠르고 안정적으로 변경하는 세 가지 핵심 실행 방법 중 첫 번째입니다(나머지는 ‘지속적으로 테스트·딜리버리한다’, ‘작고 간단하게 빌드한다’).

코드로 옮기면 재사용성·일관성·투명성이 따라오고, 그렇게 얻은 속도를 지렛대 삼아 품질까지 끌어올릴 수 있습니다. 웹 콘솔에서 클릭으로 서버를 만들거나 CLI를 한 번 두드려 네트워크를 구성하는 편이 당장은 더 간단해 보입니다. 그러나 그 방식은 기록도, 재현도, 검토도 남기지 못합니다.

그리고 이 장이 진짜 파고드는 지점은 언어의 선택 입니다. 같은 ‘코드로 정의한다’라도 선언형이냐 명령형이냐, DSL이냐 범용 언어냐에 따라 코드의 성격이 완전히 달라집니다. 올바른 언어를 올바른 방식으로 쓰는 것은 업계도 아직 정답을 못 낸 과제입니다.

핵심 내용

코드로 정의할 수 있는 것

도구마다 소스 코드를 부르는 이름은 제각각입니다(플레이북, 쿡북, 매니페스트, 템플릿). 통칭 인프라 코드 또는 인프라 정의라 부르며, 원하는 구성 요소와 구성 방법을 함께 정의합니다. 도구를 실행하면 신규 인프라를 만들거나 기존 인프라를 코드와 일치하도록 수정합니다.

대상내용
인프라 스택클라우드 플랫폼에서 프로비저닝된 구성 요소 모음
서버 구성 요소패키지, 파일, 사용자 계정, 서비스
서버 역할단일 서버에 함께 적용되는 구성 요소 묶음
서버 이미지 정의여러 서버 인스턴스를 찍어낼 이미지
애플리케이션 패키지컨테이너 등 배포 가능한 아티팩트의 빌드 방법
딜리버리 설정·스크립트파이프라인과 배포
운영 서비스 구성모니터링 등
유효성 검사 규칙자동화된 테스트, 규정준수 규칙

외부 구성 패턴과 버전 관리

코드형 인프라의 전제는 인프라 정의를 외부 텍스트 파일 에 둔다는 것입니다(외부 구성 패턴, externalized configuration pattern). 파일이 도구와 분리돼 있어야 원하는 편집기·분석 도구로 자유롭게 읽고 고칠 수 있습니다.

반대로 정의를 자체 포맷 안에 감추고 전용 GUI·API·CLI로만 다루게 하는 도구를 폐쇄형(closed-box) 이라 부릅니다. 이런 도구는 버전 관리·변경 감지가 어려워 CI/CD, TDD 같은 애자일 엔지니어링을 붙이기 힘듭니다.

소스 코드에서 얻은 교훈

대부분의 소프트웨어는 소스 코드를 외부 파일에 두고 버전 관리합니다. 인프라 정의도 똑같이 다뤄야 한다는 것이 외부 구성 패턴의 요지입니다.

인프라 코드는 반드시 버전 관리 시스템(VCS) 에 저장합니다. 그래야 다음이 따라옵니다.

이점내용
추적 가능성무엇을·누가·왜 바꿨는지 기록 — 디버깅의 핵심
롤백장애 시 정상 동작하던 이전 상태로 복원
연관성태그·버전으로 스크립트·사양·구성의 관계 파악
가시성모두가 커밋을 보고 팀 상황·장애 원인 커밋 확인
행동 가능성커밋이 CI 작업·CD 파이프라인을 트리거

비밀 정보는 코드에 넣지 않는다

패스워드·키 등 암호화되지 않은 비밀 정보는 소스 코드에 넣지 않습니다. 저장소가 비공개라도 기록과 검토 과정에서 쉽게 유출되며, 이는 보안 침해의 가장 흔한 원인입니다.

인프라 언어의 두 축

인프라 언어는 서로 독립된 두 축으로 이해하면 깔끔합니다: 선언형 ↔ 명령형, 그리고 DSL ↔ 범용 언어. 두 축은 직교하므로 “선언형 DSL”(대부분의 Terraform·Ansible), “명령형 범용 언어”(Pulumi·CDK)처럼 조합됩니다.

선언형 vs 명령형

선언형 코드는 원하는 상태(desired state) 만 적고, 그 상태를 만드는 로직은 도구가 처리합니다. 명령형 코드는 결과를 만드는 절차·로직을 직접 씁니다.

명령형(슈도코드)은 서버가 있는지 확인하고, 없으면 만들고, 준비될 때까지 기다리는 로직까지 직접 씁니다.

app_server = CloudApi.find_server('my_application_server')
if (app_server == null) {
  app_server = CloudApi.create_server(
    name: 'my_application_server', image: 'base_linux', cpu: 2, ram: '2GB', ...
  )
  while (app_server.ready == false) { wait 5 }
  if (app_server.ok != true) { throw ServerFailedError }
  app_server.provision(provisioner: servermaker, role: tomcat_server)
}

선언형은 같은 서버를 ‘원하는 형상’으로만 적습니다. 존재 확인·대기·비교·변경은 도구가 대신합니다.

virtual_machine:
  name: my_application_server
  source_image: 'base_linux'
  cpu: 2
  ram: 2GB
  network: private_network_segment
  provision:
    provisioner: servermaker
    role: tomcat_server

RAM을 늘리려면? 명령형은 “속성을 확인하고 필요하면 바꾸는” 로직을 더 넣어야 하지만, 선언형은 파일의 ram 값만 고치고 다시 실행하면 됩니다.

멱등성

선언형이 “다시 실행해도 안전한” 이유가 멱등성(idempotency) 입니다. 여러 번 실행해도 결과가 바뀌지 않는 성질입니다. 구성 드리프트를 막으려면 코드를 반복 적용해야 하는데, 멱등성이 없으면 반복이 곧 사고가 됩니다.

# 멱등하지 않음: 열 번 실행하면 spock 항목이 열 개 추가된다
echo "spock:*:1010:1010:Spock:/home/spock:/bin/bash" >> /etc/passwd
# 멱등함: 몇 번을 실행해도 spock은 항상 하나만 존재한다
user:
  name: spock
  uid: 1010
  gid: 1010
  home: /home/spock
  shell: /bin/bash

명령형이 필요한 순간

선언형은 항상 같은 결과를 냅니다. 그러나 상황에 따라 결과가 달라져야 할 때가 있습니다. 예를 들어 국가마다 데이터 센터 수가 다르고 각 데이터 센터에 VLAN을 하나씩 만들어야 한다면 — 몇 개인지 동적으로 조회하고 반복문으로 각각 생성해야 합니다.

data_centers = CloudApi.find_data_centers(country: getArgument("country"))
for $DATA_CENTER in data_centers {
  CloudApi.vlan.apply(name: "public_vlan_${DATA_CENTER.name}", data_center: $DATA_CENTER.id, ...)
}

이런 동적 로직은 선언형으로 표현할 수 없습니다. 그래서 선언형 도구도 언어를 확장해 명령형 기능을 붙입니다(Ansible의 YAML 반복문·조건, Terraform HCL의 표현식). Pulumi·AWS CDK는 아예 범용 프로그래밍 언어로 돌아갑니다.

DSL vs 범용 언어

많은 인프라 도구는 도메인 특화 언어(DSL) 를 씁니다. 특정 도메인(서버 설정, 스택)을 모델링하도록 설계돼 코드를 쓰고 읽기 쉽습니다.

  • 내부 DSL: 범용 언어의 부분집합으로 작성 (예: Chef = Ruby)
  • 외부 DSL: 별도 언어를 해석 (예: Terraform HCL — 구현 언어 Go와 무관)
  • 상당수는 기존 마크업의 확장 (YAML: Ansible·CloudFormation·Kubernetes, JSON: Packer)

반면 범용 언어(JS·Python·Ruby·TS) 의 강점은 성숙한 도구 생태계 입니다. 문법 강조, 리팩터링, IDE, 그리고 특히 단위 테스트. 라이브러리와 추상화 계층처럼 출력이 가변적인 코드일수록 테스트가 필수이고, 여기서 범용 언어가 빛납니다.

구현 원칙

코드베이스를 깔끔하게 유지해야 이해·테스트·유지보수가 쉬워집니다. 이 장이 제시하는 두 원칙입니다.

원칙 1 — 선언형 코드와 명령형 코드를 분리한다

둘이 뒤섞인 코드는 디자인 스멜 입니다. 한 번에 여러 일을 하려 든다는 신호이므로, 요구사항 성격에 따라 코드 조각을 분리합니다. 선언형 구문에 조건·반복을 욱여넣거나, 단순 구성 데이터를 절차형 코드에 파묻는 것이 대표적 실패 유형입니다.

원칙 2 — 인프라 코드를 실제 코드처럼 다룬다

인프라 코드를 ‘진짜’ 코드로 취급하지 않으면 설정 파일과 유틸리티 스크립트가 관리 불가능한 상태로 방치됩니다. 코드 리뷰, 페어 프로그래밍, 자동화 테스트 등 애플리케이션 코드와 같은 품질 관행을 적용하고 기술 부채를 관리해야 합니다.

문서로서의 코드

잘 관리된 인프라 코드는 항상 정확하고 최신인 시스템 기록이라 문서보다 유용할 때가 많습니다. 신규 참여자는 코드로 시스템을 배우고, 감사자는 버전 기록으로 상황을 파악합니다. 다만 기술 스택을 모르는 관리자를 위한 상위 레벨 문서까지 대체하지는 못하며, ADR처럼 문서 자체를 코드로 관리하는 방법도 있습니다.

비교 / 트레이드오프

선언형과 명령형을 패러다임 대결로 보는 대신, 주어진 고려사항에 어느 쪽이 맞는지로 판단합니다.

기준선언형명령형(프로그래밍 가능)
명시하는 것원하는 최종 상태(what)상태를 만드는 로직(how)
잘 맞는 곳일관되게 반복 생성하는 인프라, 재사용 가능한 환경·스택상황에 따라 결과가 달라지는 동적 인프라, 라이브러리·추상화 계층
결과항상 동일입력·조건에 따라 달라짐
한계복잡한 로직을 담으면 YAML/JSON이 비대해짐단순 구성엔 과함
대표 도구Terraform, CloudFormation, Ansible, PuppetPulumi, AWS CDK

핵심은 패러다임 하나를 통째로 고르는 게 아니라, 코드베이스 안에서 요구사항마다 맞는 쪽을 골라 분리해서 쓰는 것입니다.

내 생각

  • 선언형/명령형은 대립이 아니라 도구함의 두 도구입니다. Terraform(선언형)으로 리소스를 찍어내되 개수·이름이 동적으로 변하는 부분은 for_each·모듈 로직(명령형 확장)으로 처리하는 게 실무 패턴입니다. 문제는 이 둘이 한 파일에 뒤섞일 때 생깁니다.
  • 폐쇄형 도구 회피 기준이 명확합니다. “정의가 텍스트 파일로 나와 git에 올라가나?”가 실질적 리트머스입니다. 콘솔 클릭이나 GUI 전용 도구는 CI/CD·리뷰·롤백을 원천 차단합니다.
  • 멱등성은 IaC의 안전장치입니다. >> /etc/passwd가 반복 실행에서 터지는 예시는 선언형 도구를 왜 쓰는지 한 줄로 설명합니다. 직접 스크립트를 짤 때도 “다시 돌려도 안전한가”를 항상 자문해야 합니다.
  • Pulumi/CDK의 진짜 이점은 테스트입니다. 범용 언어로 돌아가는 이유가 단지 익숙함이 아니라, 단위 테스트·리팩터링·IDE 같은 성숙한 생태계를 그대로 가져오기 위함이라는 점이 핵심입니다.

관련 개념