이전 강의: 1강. DevOps는 왜 패턴이 필요해졌나 이번 강의 키워드: Snowflake Server, Configuration Drift, ClickOps, IaC, 멱등성, Immutable Infrastructure, Pets vs Cattle, Phoenix Server
들어가며
3년째 돌아가는 서버가 한 대 있습니다. 처음 구축한 담당자는 퇴사했고, 인수인계 문서는 설치 첫날의 상태만 적혀 있습니다. 어느 날 정기 점검으로 서버를 재부팅했는데 서비스가 올라오지 않습니다. 로그를 보니 어떤 환경변수가 없다고 합니다. 그 환경변수는 누가, 언제, 어디에 설정했을까요? 아무도 모릅니다.
이 서버는 망가진 게 아닙니다. 원래부터 아무도 다시 만들 수 없는 상태였는데, 재부팅 전까지 그 사실이 드러나지 않았을 뿐입니다. 1강의 세 질문 중 첫 번째인 재현성이 무너진 상태죠. 이번 강의에서는 이런 서버가 어떻게 만들어지는지, 그리고 업계가 이 문제를 두 단계에 걸쳐 어떻게 풀어왔는지 살펴봅니다.
Snowflake Server는 어떻게 만들어지나
Martin Fowler는 2012년 이런 서버를 Snowflake Server(눈송이 서버)라고 불렀습니다. 눈송이는 저마다 모양이 다르고, 한 번 녹으면 똑같이 다시 만들 수 없습니다.
눈송이 서버는 한 번에 만들어지지 않습니다. 처음에는 문서대로 설치한 깨끗한 서버였습니다. 그러다 새벽 장애 때 누군가 SSH로 들어가 설정 파일 한 줄을 고칩니다. 몇 달 뒤 보안 패치로 라이브러리 하나를 올리고, 성능 문제로 커널 파라미터를 조정하고, 급한 요청으로 크론 작업을 하나 추가합니다. 각 작업은 그 순간 가장 빠르고 합리적인 선택이었습니다. 1강에서 본 안티패턴의 첫 번째 조건, "처음에는 합리적으로 보인다"를 그대로 만족합니다.
문제는 이 변경들이 서버에만 기록되고 다른 어디에도 남지 않는다는 점입니다. 서버 자체가 유일한 설계도가 되는 셈이죠.
여기서 파생되는 현상이 Configuration Drift(설정 표류)입니다. 문서나 코드에 적힌 "있어야 할 상태"와 실제 서버 상태가 시간이 지나며 점점 벌어지는 현상입니다. 같은 역할의 서버가 여러 대일 때 더 교묘해집니다. 세 대 모두 똑같이 구성했는데 1번 서버에만 긴급 패치가 들어갔다면, "가끔 1번 서버로 요청이 갈 때만 나는 버그"가 생깁니다. 이런 버그는 재현도 추적도 어렵습니다.
클라우드 시대에는 같은 안티패턴이 ClickOps라는 모습으로 다시 나타났습니다. SSH 대신 웹 콘솔에서 클릭으로 보안 그룹을 열고 인스턴스 크기를 바꿉니다. 도구만 바뀌었을 뿐 구조는 같습니다. 변경이 콘솔에만 기록되고, 왜 그렇게 설정했는지는 아무도 모릅니다.
자기 서버가 눈송이인지 확인하는 간단한 질문이 있습니다.
이 서버가 지금 이 순간 사라진다면, 몇 시간 안에 똑같이 다시 만들 수 있는가? 그 작업을 담당자가 아닌 다른 사람도 할 수 있는가?
둘 중 하나라도 자신 없게 답하게 된다면 그 서버는 이미 눈송이입니다.
첫 번째 대응: 설정을 코드로
가장 먼저 떠오르는 해법은 설치 과정을 스크립트로 남기는 것입니다. 실제로 오랫동안 셸 스크립트가 그 역할을 했습니다. 그런데 셸 스크립트에는 근본적인 약점이 있습니다.
# setup.sh
echo "export APP_ENV=production" >> /etc/profile
useradd appuser이 스크립트는 깨끗한 서버에서 한 번 실행할 때만 의도대로 동작합니다. 두 번 실행하면 /etc/profile에 같은 줄이 두 번 들어가고, useradd는 이미 사용자가 있다며 실패합니다. 즉 "처음 상태"를 전제로 한 명령의 나열일 뿐, 이미 운영 중인 서버를 원하는 상태로 맞추는 도구가 아닙니다.
이 약점을 해결하는 핵심 개념이 멱등성(idempotency)입니다. 몇 번을 실행해도 결과가 같은 성질을 말합니다. 그리고 멱등성을 자연스럽게 얻는 방법이 선언형(declarative) 기술 방식입니다. "무엇을 실행하라"가 아니라 "어떤 상태여야 한다"를 적는 것이죠.
# Ansible
- name: APP_ENV 설정이 존재해야 한다
lineinfile:
path: /etc/profile
line: "export APP_ENV=production"
- name: appuser 계정이 존재해야 한다
user:
name: appuser
state: present도구는 현재 상태를 먼저 확인하고, 원하는 상태와 다를 때만 변경합니다. 이미 맞춰져 있으면 아무것도 하지 않습니다. 이렇게 서버를 선언된 상태로 끌어당기는 방식을 수렴(convergence) 모델이라고 부릅니다.
이 발상은 생각보다 오래됐습니다. 1993년 Mark Burgess의 CFEngine이 수렴 개념을 처음 도입했고, 2000년대 중후반 Puppet(2005)과 Chef(2009)가 대중화했습니다. 2012년에는 에이전트 설치 없이 SSH만으로 동작하는 Ansible이 나왔습니다. 이들은 주로 이미 존재하는 서버의 내부 설정을 관리하는 구성 관리(Configuration Management) 도구입니다.
클라우드가 보편화되면서 서버 자체를 만드는 일, 즉 네트워크·인스턴스·로드밸런서 같은 자원 생성도 코드로 다룰 필요가 생겼습니다. 2014년 HashiCorp가 내놓은 Terraform이 이 영역, 흔히 프로비저닝(Provisioning)이라 부르는 영역의 대표 도구가 됐습니다.
이 흐름 전체를 묶어 Infrastructure as Code(IaC)라고 부릅니다. IaC가 주는 가치는 단순히 "자동화"가 아닙니다. 인프라가 코드가 되면 애플리케이션 코드가 누리던 것들을 그대로 누릴 수 있습니다. Git 이력으로 누가 언제 왜 바꿨는지 추적할 수 있고, 변경 전에 리뷰를 받을 수 있고, 잘못되면 이전 버전으로 되돌릴 수 있습니다. 서버에만 있던 설계도가 저장소로 옮겨지는 것입니다.
수렴 모델의 한계
IaC로 많은 문제가 풀렸지만, 수렴 모델에는 구조적인 빈틈이 남아 있었습니다.
첫째, 도구가 관리하는 것만 관리됩니다. Ansible 플레이북에 적히지 않은 파일을 누군가 SSH로 고치면, 도구는 그 변경을 알지 못합니다. 드리프트가 줄어들 뿐 사라지지는 않습니다.
둘째, 서버의 이력이 결과에 영향을 줍니다. v1으로 설치한 뒤 v2, v3로 차례로 업그레이드한 서버와, 처음부터 v3로 설치한 서버는 같은 플레이북을 적용해도 완전히 같지 않을 수 있습니다. v1 시절 설치된 패키지나 남은 설정 파일이 흔적처럼 남기 때문입니다. 수렴 모델은 서버를 계속 고쳐 쓰기 때문에, 오래된 서버일수록 "거쳐온 길"이 상태에 섞입니다.
셋째, 실행 사이의 시간에는 무방비합니다. 30분마다 수렴을 돌린다면, 그 사이 29분 동안은 누가 무엇을 바꿔도 막을 수 없습니다.
세 문제의 공통 원인은 하나입니다. 운영 중인 서버를 계속 수정한다는 것입니다.
두 번째 대응: 고치지 말고 교체하라
그래서 나온 발상이 "아예 고치지 말자"입니다. 2013년 Chad Fowler는 "서버를 버리고 코드를 태워라"라는 도발적인 글에서 Immutable Infrastructure(불변 인프라)라는 개념을 널리 알렸습니다.
원리는 단순합니다. 서버는 한 번 만들어지면 절대 수정하지 않습니다. 설정을 바꾸거나 패치를 적용해야 하면, 변경이 반영된 새 이미지를 빌드하고, 그 이미지로 새 서버를 띄운 뒤, 기존 서버를 제거합니다. 이렇게 미리 완성해 둔 서버 이미지를 골든 이미지(Golden Image)라고 부르고, HashiCorp의 Packer 같은 도구로 만듭니다.
이렇게 하면 수렴 모델의 세 가지 빈틈이 한꺼번에 사라집니다. 수정하지 않으니 관리 밖의 변경이 끼어들 틈이 없고, 매번 새로 만드니 이력이 섞이지 않고, 실행 사이의 공백이라는 개념 자체가 없어집니다. 운영 중인 모든 서버는 특정 이미지 버전 하나로 정확히 설명됩니다.
컨테이너가 바로 이 사고방식의 가장 성공한 구현입니다. 실행 중인 컨테이너 안에 들어가 패키지를 설치하지 않고, Dockerfile을 고쳐 이미지를 다시 빌드하는 관행이 그대로 Immutable Infrastructure입니다. 컨테이너 기술이 이 철학을 가볍고 빠르게 만들어 준 덕분에 대중화된 것이죠.
같은 시기에 이 사고방식을 표현하는 비유들도 나왔습니다. Pets vs Cattle은 Microsoft의 Bill Baker가 쓰고 Randy Bias가 널리 퍼뜨린 비유입니다. 애완동물은 이름이 있고 아프면 정성껏 치료합니다. 가축은 번호로 불리고 아프면 교체합니다. 서버를 db-master-kim 같은 이름의 애완동물이 아니라 web-0017 같은 번호의 가축으로 다루라는 뜻입니다.
Kief Morris가 제안하고 Martin Fowler가 소개한 Phoenix Server도 같은 맥락입니다. 불사조처럼 주기적으로 태워 없애고 다시 태어나게 하라는 것이죠. 핵심은 파괴를 일상으로 만드는 것입니다. 서버를 정기적으로 폐기하고 재생성하면, 재현이 안 되는 부분이 있을 때 장애가 아니라 평범한 교체 작업 중에 발견됩니다. 앞서 본 "재부팅했더니 안 뜬다"는 사고를, 통제된 상황에서 미리 겪는 셈입니다.
두 모델을 비교하면 다음과 같습니다.
| 구분 | 수렴 모델 (Mutable) | 불변 모델 (Immutable) |
|---|---|---|
| 변경 방식 | 운영 중인 서버를 원하는 상태로 수정 | 새 이미지로 서버를 교체 |
| 대표 도구 | Ansible, Puppet, Chef | Packer, 컨테이너 이미지, Terraform |
| 드리프트 | 줄어들지만 남음 | 구조적으로 거의 없음 |
| 롤백 | 이전 설정을 다시 적용 (완전 복원은 어려움) | 이전 이미지로 교체 |
| 변경 속도 | 빠름 (한 줄 수정) | 느림 (이미지 빌드 필요) |
| 전제 조건 | 적음 | 이미지 파이프라인, 상태 분리 |
Immutable의 비용: 상태를 어디에 둘 것인가
불변 인프라에는 공짜가 아닌 대가가 있습니다. 가장 큰 것은 상태(state) 문제입니다.
서버를 언제든 버릴 수 있으려면, 버려서는 안 되는 것이 서버 안에 없어야 합니다. 데이터베이스 파일, 사용자가 업로드한 파일, 세션, 로그가 모두 여기에 해당합니다. 그래서 불변 인프라는 필연적으로 상태를 서버 밖으로 분리하는 설계를 요구합니다. 데이터는 외부 DB나 별도 볼륨에, 파일은 오브젝트 스토리지에, 세션은 Redis 같은 저장소에, 로그는 중앙 수집 시스템으로 보냅니다. 4강에서 다룰 Twelve-Factor App 원칙 상당수가 이 요구에서 나왔습니다.
반대로 말하면, 데이터베이스 서버처럼 상태 그 자체인 시스템은 순수하게 불변으로 다루기 어렵습니다. 현실의 인프라는 대개 두 모델을 섞어 씁니다. 상태가 없는 애플리케이션 계층은 불변으로, 상태를 가진 계층은 IaC로 관리하되 데이터는 백업과 복구 절차로 보호하는 식이죠. 불변 인프라를 모든 곳에 적용할 원칙이 아니라, 상태 없는 계층을 위한 강력한 선택지로 이해하는 것이 정확합니다.
또 다른 비용은 문화입니다. "SSH로 들어가서 확인해 보자"는 습관이 "서버에 접속할 일이 없도록 로그와 메트릭을 밖으로 빼자"로 바뀌어야 합니다. 이 전환은 6강의 Observability로 이어집니다.
IaC를 도입해도 생기는 안티패턴
마지막으로, IaC를 도입한 뒤에 흔히 빠지는 함정들을 보겠습니다. 도구가 바뀌어도 1강의 구조적 순환은 형태를 바꿔 돌아옵니다.
코드 옆의 수동 변경. Terraform으로 관리하는 자원을 급하다고 콘솔에서 직접 바꾸는 경우입니다. 이때 IaC 코드는 사실과 다른 내용을 담게 됩니다. 코드가 없는 것보다 더 위험할 수 있습니다. 사람들이 코드를 믿기 때문이죠. 다음 apply 때 수동 변경이 조용히 되돌려지면서 장애가 나기도 합니다. terraform plan을 정기적으로 돌려 차이를 감지하는 드리프트 탐지가 이 문제의 방어선입니다.
누군가의 노트북에서 실행하는 IaC. 코드는 저장소에 있지만 실행은 특정 담당자의 PC에서만 합니다. 그 PC의 도구 버전, 인증 정보, 로컬 상태 파일에 의존하게 되면 결국 서버 대신 노트북이 눈송이가 됩니다. 4강에서 다룰 Pipeline as Code와 GitOps는 이 문제를 "실행 주체를 사람에서 파이프라인으로 옮기는 것"으로 풉니다.
거대한 단일 구성. 모든 인프라를 하나의 Terraform 구성과 하나의 상태 파일에 몰아넣으면, 사소한 변경에도 전체를 계획하고 적용해야 합니다. 변경의 영향 범위가 커지니 사람들은 apply를 두려워하게 되고, 적용을 미루다 변경이 쌓입니다. 1강에서 본 "변경이 무서워 배포를 줄이는" 순환이 IaC 안에서 그대로 재현되는 것입니다. 구성을 네트워크, 공통 자원, 서비스별로 나눠 변경 단위를 작게 유지해야 합니다.
복사해 붙여넣은 환경. 개발, 스테이징, 운영 환경을 위해 같은 코드를 폴더째 복사해 두면, 시간이 지나며 세 벌이 조금씩 달라집니다. 서버 간 드리프트가 코드 간 드리프트로 옮겨간 셈입니다. 공통 부분을 모듈로 만들고 환경별 차이는 변수로만 표현해야 합니다.
정리
Snowflake Server는 합리적인 수동 변경이 쌓여 서버 자체가 유일한 설계도가 된 상태이고, Configuration Drift는 그 과정에서 선언과 현실이 벌어지는 현상입니다. 업계는 이 문제를 두 단계로 풀었습니다. 먼저 IaC와 수렴 모델로 설계도를 서버에서 저장소로 옮겼고, 다음으로 Immutable Infrastructure로 운영 중인 서버를 수정한다는 행위 자체를 없앴습니다. 불변 모델은 강력하지만 상태 분리라는 설계 비용을 요구하므로, 상태 없는 계층에 우선 적용하는 것이 현실적입니다.
1강의 세 질문으로 보면, 이번 강의의 패턴들은 모두 재현성을 되찾는 방법이었습니다. 그리고 IaC 이후의 안티패턴들은 IaC를 쓰더라도 작은 변경 단위를 잃으면 같은 순환에 다시 빠진다는 것을 보여줍니다.
다음 3강에서는 인프라에서 코드로 시선을 옮깁니다. 오래 살아있는 브랜치가 어떻게 Merge Hell을 만드는지, 그리고 Trunk-Based Development와 CI가 "작은 변경 단위"를 코드 흐름에서 어떻게 구현하는지 다룹니다.