한 줄 정의

서버 인스턴스의 원본이 되는 서버 이미지를 코드로 정의하고 파이프라인으로 빌드·테스트·딜리버리하여, 이미지 자체를 버전이 관리되는 산출물로 다루는 방법입니다.

쉽게 말하면

서버 이미지는 복사기에 올려 두는 원본 문서입니다. 원본을 잘 만들어 두면 몇 장을 뽑아도 똑같은 결과가 나오지만, 원본에 오타가 있으면 뽑은 전부에 오타가 있습니다. 더 곤란한 것은 원본을 고쳐도 이미 뿌려진 복사본은 그대로라는 점입니다.

그래서 이 장의 질문은 세 가지로 좁혀집니다. 원본을 어떻게 만들 것인가(빌드), 뽑기 전에 오타가 없는지 어떻게 확인할 것인가(테스트), 새 원본이 나왔을 때 이미 뿌려진 복사본을 어떻게 할 것인가(버전 관리와 교체)입니다.

왜 중요한가?

이미지를 만들지 않고도 서버는 굴러갑니다. 벤더가 주는 스톡 이미지를 쓰고 생성 시점에 전부 구성하면 됩니다. 그런데도 이미지를 직접 굽는 이유는, 이미지가 모든 서버의 단일 출처 이기 때문입니다. 여기서 보장한 것은 전 서버에서 자동으로 참이 되고, 여기서 놓친 것은 전 서버에서 자동으로 거짓이 됩니다.

이미지를 수동으로 만들면 이 단일 출처가 검증되지 않은 채 복제됩니다. 반대로 코드로 빌드하면 OS 패치 하나를 반영할 때마다 일관되게 다시 구울 수 있고, 취약점이 공개됐을 때 “어느 이미지 버전이 몇 대에서 돌고 있는가”에 답할 수 있습니다.

핵심 내용

서버 이미지 생명 주기

flowchart LR
    origin["원본 이미지"] -->|"신규 서버 이미지 빌드"| custom["사용자 정의 이미지"]
    custom -->|"신규 서버 인스턴스 생성"| run["서버 인스턴스 실행 중"]

플랫폼마다 이미지 형식이 있습니다(AWS의 AMI, Azure의 관리 이미지, VMware의 VM 템플릿). 이 장이 다루는 것은 첫 번째 화살표, 즉 원본에서 사용자 정의 이미지를 만드는 구간입니다.

왜 사용자 정의 이미지를 빌드하는가
이유내용
거버넌스 준수규제 산업처럼 더 엄격한 지침을 이미지 한 곳에서 강제
보안 강화미사용 계정·시스템 서비스 비활성화, 불필요한 포트 차단, 파일 시스템·폴더 권한 잠금
성능 최적화보안 강화로 제거한 서비스가 CPU·메모리도 함께 절약하고, 이미지가 작을수록 인스턴스 생성이 빨라져 확장·복구에 유리
공통 패키지 설치모니터링 에이전트, 시스템 계정, 조직별 유틸리티를 모든 인스턴스에 기본 탑재
서버 역할별 이미지컨테이너 클러스터 노드, 애플리케이션 서버, CI 에이전트처럼 목적에 맞춰 조정

성능 최적화가 보안 강화의 부산물이라는 점이 흥미롭습니다. 안 쓰는 것을 지우는 하나의 작업이 공격 표면과 리소스 사용량을 동시에 줄입니다.

온라인 빌드 vs 오프라인 빌드

온라인오프라인
절차신규 인스턴스를 부팅해 구성한 뒤 이미지로 변환디스크 볼륨을 마운트해 파일을 복사한 뒤 부팅 가능한 이미지로 변환
속도느림 — 부팅과 구성에 몇 분에서 수십 분빠름
난이도쉬움 — 실행 중인 서버에 평소 쓰던 구성 도구를 그대로 적용까다로움 — 실행 중이 아닌 파일 시스템을 다뤄야 함

오프라인이 까다로운 이유는 도구들이 “실행 중인 서버에 설치한다”는 전제로 만들어졌기 때문입니다. yum install javayum install --prefix /mnt/image_builder/ java처럼 경로를 바꿔 실행할 수는 있지만 틀리기 쉽고 모든 도구가 이 옵션을 지원하지도 않습니다. 그래서 다른 파일 시스템 디렉터리를 루트로 삼아 명령을 실행하는 chroot를 씁니다.

chroot /mnt/image_builder/ yum install java

chroot는 도구를 가리지 않고 쓸 수 있다는 점이 장점이며, Packer 같은 주요 도구도 기본 지원합니다.

온라인 빌드의 코드

이미지 빌드 도구(Netflix의 Aminator, HashiCorp의 Packer, AWS·Azure의 Image Builder 서비스)는 인스턴스와 디스크 볼륨을 만들고 구성 도구를 실행한 뒤 플랫폼 API로 이미지 변환을 조정합니다. 그리고 이 프로세스 자체가 코드로 정의됩니다.

image:
  name: shopspinner-linux-image
  origin: fci-12345678
  region: europe
  instance_size: small
  subnet: ${IMAGE_BUILDER_SUBNET_ID}
  configure:
    tool: servermaker
    code_repo: servermaker.shopspinner.xyz
    server_role: appserver

주목할 점은 subnet처럼 인프라 리소스가 파라미터로 주입된다는 것입니다. 빌더 인스턴스도 결국 실제 인프라 위에서 돌아야 하는데, 기존 인프라를 재활용하면 편하지만 이미지 빌드 프로세스가 오염될 수 있습니다. 일회용 인프라를 만들어 이미지를 굽고 삭제하는 편이 안전하며, 프로비저닝하는 접근 권한도 최소화합니다 — 구성을 push하는 경우에만 인바운드를 열고, 아웃바운드는 패키지·구성 다운로드에 필요한 만큼만 허용합니다.

서버 구성 도구가 과할 때

이미지 빌드는 알려지고 일관성 있는 원본에서 출발하므로, 구성 도구의 멱등성·수렴 로직이 필요 없을 수 있습니다. 이럴 때는 Bash·PowerShell 같은 단순한 스크립트가 더 적절합니다. 조건 분기가 많은 거대한 스크립트 하나보다 단일 작업에 집중한 작은 스크립트를 여러 개 두고, 실행 순서를 명확히 하기 위해 10-install-monitoring-agent.sh처럼 앞에 숫자를 붙입니다.

서버 이미지의 원본

이미지 하나를 만드는 데 이미지 빌더 코드(예: Packer 템플릿), 원본 이미지, 서버 구성 코드, 여러 서버 요소(패키지 저장소 등)가 함께 들어갑니다. 이 중 원본 이미지의 선택이 특히 중요합니다.

원본내용유의점
스톡 이미지플랫폼·OS 벤더·오픈소스가 제공하는 이미지대다수 사용자를 겨냥해 과잉 프로비저닝되어 있음 — 불필요한 콘텐츠를 제거하거나 JEOS(just enough operating system)처럼 경량 원본을 고름
처음부터 빌드ISO 등 OS 설치 이미지에서 부팅해 설치 스크립트 실행 (Kickstart, JumpStart, Preseed, Windows answer file)내부 클라우드·가상화 플랫폼처럼 스톡 이미지가 없거나 신뢰하지 않을 때. 명확하고 일관된 시작점을 제공
출처와 공급망을 검증한다

서드파티 콘텐츠인 이상 이미지도 공급망 리스크를 안고 있습니다. 이미지를 제공한 주체와 그 빌드 프로세스를 이해해야 하며, 확인할 지점은 다음과 같습니다.

  • 이미지와 패키지에 알려진 취약점이 있는 소프트웨어가 포함되어 있는가
  • 벤더는 정적 코드 분석 같은 검사 단계를 수행하는가
  • 포함된 소프트웨어가 조직 정책이나 현지 법률에 반하는 방식으로 데이터를 수집하지는 않는가
  • 벤더는 변조를 어떻게 감지·방지하며, 패키지 서명 확인처럼 사용자 측에서 구현해야 할 것은 무엇인가

벤더를 완전히 신뢰하지 않고 자체 검사를 구현하는 것이 요점이고, 그 검사를 넣을 자리가 뒤에 나오는 이미지 파이프라인입니다.

이미지 변경 — 재가열 vs 새로 굽기

이미지는 방치하면 실제 운영 중인 서버 현실과 벌어집니다. 그렇다고 생성 시마다 패치를 적용하면 그 프로세스가 점점 길어져 이미지를 쓰는 이점이 사라지므로, 이미지를 정기적으로 다시 만들어야 합니다. 방법은 두 가지입니다.

재가열(reheat)새로 굽기
원본이미지의 이전 버전최초와 동일한 원본 소스
결과명시적으로 변경한 사항만 반영되어 버전 간 일관성이 높음이전 빌드의 잔재가 전혀 없어 더 깨끗함
제거 작업이전 이미지의 패키지가 남으므로 명시적 제거 코드가 필요하고, 그 코드가 부채로 축적됨애초에 설치되지 않으므로 제거 코드 불필요

동일한 소스를 쓴다면 새로 굽는 것이 재가열과 같은 결과를 내야 하고, 오히려 더 안정적으로 재생산됩니다. “제거 코드가 쌓인다”는 지적이 핵심인데, 이는 마이그레이션 스크립트를 계속 쌓아 가는 상태 기반 관리와 선언형 재생성의 차이와 같은 구도입니다.

버전 관리와 인스턴스 업데이트

대부분의 인프라 플랫폼은 이미지에 버전 번호를 지정하는 기능을 제공하지 않으므로, 이름에 넣거나(appserver-3, basic-linux-2) 태그로 붙입니다(Name=appserver, Version=3). 태그는 검색하기 쉽고 이름은 사람이 보기 쉬우므로 둘 다 쓰는 팀이 많습니다. 시맨틱 버전에 타임스탬프를 더한 1.0.4-20200229_0420 같은 형식도 언제 빌드됐는지 바로 보여 줍니다.

이미지 신규 버전이 나왔을 때 인스턴스를 어떻게 할지가 다음 문제입니다.

  • 즉시 리빌드: 업무에 지장을 주고 시간이 많이 들지만 서버의 일관성을 보장하고 복원력을 지속적으로 검증합니다. 무중단 변경과 좋은 파이프라인이 갖춰진 성숙한 팀이 선호합니다
  • 자연 교체 대기: 오래 걸리고 다양한 버전의 서버가 공존하게 됩니다. OS 패키지와 구성 버전이 제각각인 애플리케이션 서버가 생겨 일관성 없는 실행과 취약점 잔존으로 이어집니다

대기 전략을 택한다면 완화책이 필요하며, 기본은 이미지 버전별 인스턴스 수를 추적 하는 것입니다.

ImageVersionInstance count
basic-linux14
basic-linux28
appserver12
appserver211
appserver38
container-node12
container-node215
container-node35

취약점 패치가 basic-linux-2, appserver-3, container-node-2에 포함됐다면 이 표만으로 리빌드가 시급한 19대(기본 Linux 4대 + 애플리케이션 서버 13대 + 컨테이너 노드 2대)를 즉시 식별할 수 있습니다. “3개월 지난 이미지로 만든 인스턴스는 교체한다” 같은 기간 제한 정책도 같은 데이터 위에서 굴러갑니다.

팀 간 제공과 사용

이미지를 만드는 팀과 쓰는 팀이 다를 때, 이상적인 형태는 이미지를 각 팀의 인프라 파이프라인에 공급하는 것입니다. 이미지 소유 팀이 신규 버전을 릴리스하면 사용 팀의 파이프라인이 이를 가져와 자동 테스트하고 서버를 업데이트합니다. 자동화가 성숙하지 않은 팀은 버전을 고정해 두고 수동으로 확인한 뒤 올리는 보수적 방식을 택하는데, 이는 검증 비용을 자동화 대신 사람에게 지불하는 선택입니다.

주요 변경(예: 애플리케이션 서버 소프트웨어의 메이저 업그레이드)은 사소한 업데이트로 흘려보내지 말고 시맨틱 버전의 큰 자리를 올려 신호를 주거나, 아예 별도 이미지로 분리합니다. 기본 Linux 이미지를 CentOS 9.x에서 10.x로 옮길 때 basic-linux10-1.0.0을 새로 만들면, OS 마이그레이션이 일상적인 업데이트와 섞이지 않고 명시적인 작업이 됩니다.

이미지 파이프라인 — 빌드·테스트·딜리버리

flowchart LR
    build["빌드 단계"] --> test["테스트 단계"] --> deliver["딜리버리 단계"]

몇 달에 한 번보다 매주 굽는 편이 낫습니다. 빌드 간격이 길수록 한 번에 들어가는 변경이 많아져 문제를 찾고 고치는 작업이 늘어나기 때문이며, 이는 지속적 통합의 논리를 그대로 이미지에 적용한 것입니다.

빌드 단계

이미지 빌더 코드 커밋, 벤더의 신규 원본 이미지 게시, 서버 구성 코드 변경, 패키지 신규 버전 게시가 트리거 후보입니다. 다만 수십~수백 개 패키지의 모든 업데이트를 감지해 매번 굽는 것은 비현실적이므로, 주요 변경만 자동 트리거하고 나머지는 예약 빌드(예: 매주)로 흡수 하는 조합이 현실적입니다.

빌드 결과물은 릴리스 후보일 뿐이므로 아직 사용 가능하다고 표시하면 안 됩니다. Release_Status=Candidate 같은 태그로 미테스트 상태를 명시합니다.

테스트 단계

온라인 빌드 중에(즉 이미지로 변환하기 전 인스턴스에서) 테스트를 실행할 수 있지만 두 가지 문제가 있습니다.

빌드 중 테스트의 두 가지 함정

하나는 테스트가 이미지를 오염시킨다는 점입니다. 테스트 파일·로그 항목·잔류물이 부작용으로 남고, 테스트용 사용자 계정처럼 이미지에서 생성된 서버에 접근할 수 있는 수단이 남을 수도 있습니다. 다른 하나는 인스턴스를 이미지로 변환하는 과정이 사용자 액세스 권한 같은 중요한 측면을 바꿀 수 있어, 테스트한 인스턴스가 실제로 만들어질 서버와 다를 수 있다는 점입니다.

따라서 가장 안정적인 방법은 빌드 단계가 만든 최종 이미지로 임시 인스턴스를 새로 띄워 테스트하는 것입니다. 피드백이 느려지는 대신 실제로 배포될 것과 동일한 대상을 검증합니다. 통과하면 태그를 붙여 다음 단계 준비 완료를 표시합니다.

딜리버리 단계

테스트를 통과한 버전에 사용 준비 완료 태그를 지정합니다. 여기에 보안 테스트 같은 단계를 더하거나, 신규 이미지로 서버를 리빌드해 개발 → 스테이징 → 프로덕션으로 환경을 옮겨 가며 애플리케이션 배포·테스트를 트리거하는 파이프라인을 구성할 수도 있습니다. 다른 팀의 파이프라인은 여기서 나온 신규 이미지 버전을 자신의 새로운 입력으로 받습니다.

멀티 서버 이미지 — 계층화 vs 코드 공유

여러 인프라 플랫폼, 여러 OS, 여러 CPU 아키텍처(x86·ARM), 여러 역할을 지원하려면 이미지가 늘어납니다. 역할의 경우 판단 기준이 분명한데, 애플리케이션 서버처럼 설치할 소프트웨어가 많으면 생성 시 역할을 구성하는 대신 전용 이미지로 굽는 편이 낫고, 그렇지 않다면 범용 이미지 하나에 역할을 적용하면 됩니다. 별도 이미지 유지 비용(파이프라인·테스트·스토리지)을 서버 생성 시 구성의 단점(속도·효율성·의존성)과 비교해 결정합니다.

역할 기반 이미지가 많아지면 이미지를 계층으로 쌓는 방식이 자연스러워 보입니다.

flowchart LR
    base["기본 Linux 이미지"] --> app["애플리케이션 서버 이미지"]
    base --> node["컨테이너 호스트 이미지"]
    app --> shop["쇼핑 애플리케이션 서버 이미지"]
    app --> search["검색 서비스 서버 이미지"]

문제는 피드백 주기입니다. 각 이미지가 별도 파이프라인으로 빌드되고 입력 이미지가 바뀔 때 다음 빌드가 트리거되므로, 기본 Linux 이미지를 바꾸면 애플리케이션 서버 → 검색 서비스로 연쇄됩니다. 파이프라인 하나가 10~15분이면 전체 반영에 최대 45분이 걸립니다.

대안은 계층을 이미지가 아니라 구성 코드에서 만드는 얕은(shallower) 빌드입니다. 서버 구성 코드를 역할 정의에 맞게 계층화하되, 이미지를 빌드할 때는 상위 이미지를 원본으로 삼지 않고 관련 코드를 전부 결합해 원본 이미지에서 한 번에 적용합니다. 검색 서비스 이미지 파이프라인이 기본 Linux 코드 변경 즉시 실행되므로 맨 아래 계층의 빌드 시간이 짧아집니다.

거버넌스와 서버 이미지

기존 거버넌스는 신규 이미지가 만들어질 때마다 사람이 검토·승인하는 형태가 많습니다. 이미지 자체를 검사하는 대신 거버넌스 담당자가 이미지를 생성하는 코드 를 검사하고, 인프라팀과 협력해 규정 준수를 확인하는 자동화된 검사를 만들어 파이프라인에서 실행하는 편이 강력합니다. 승인이 병목이 아니라 파이프라인 단계가 됩니다.

비교 / 트레이드오프

이미지 계층화코드 공유(얕은 빌드)
계층 구현 위치이미지 상속 — 상위 이미지를 원본으로 사용구성 코드 계층화 — 코드를 결합해 원본에서 한 번에 적용
빌드 시간상위 파이프라인 연쇄로 누적 (10~15분 × 계층 수)계층 깊이와 무관하게 단일 빌드
변경 반영상위 이미지가 빌드·릴리스된 뒤에야 하위가 트리거코드 변경 즉시 각 이미지 파이프라인 실행
재사용 단위빌드된 이미지 산출물구성 코드

내 생각

  • 이미지 계층화의 딜레마는 Docker 베이스 이미지 운영에서 그대로 재현됩니다. 사내 base → jdk → app 체인을 만들면 base 패치 하나에 전 체인 재빌드가 필요하고, 그래서 멀티스테이지 빌드로 계층을 얕게 가져가는 선택이 나옵니다.
  • “빌드 중 테스트가 이미지를 오염시킨다”는 컨테이너에서도 유효합니다. 빌드 스테이지에서 테스트 도구를 설치하면 런타임 이미지에 남으므로, 테스트 스테이지를 분리하고 최종 이미지로 컨테이너를 띄워 검증하는 것이 같은 원칙의 구현입니다.
  • 이미지 버전별 인스턴스 수 대시보드가 이 장에서 가장 실무적입니다. 취약점 공개 시 “우리가 영향을 받는가”에 몇 분 안에 답하려면 결국 이 매핑이 있어야 하고, 없는 조직은 매번 전수 조사를 다시 합니다.
  • 재가열이 남기는 제거 코드는 마이그레이션 부채와 같은 구조입니다. 이전 상태에서 출발하면 되돌리는 코드가 쌓이고, 원본에서 매번 새로 만들면 그 코드 자체가 필요 없어집니다.

관련 개념