← posts/b.log()

blog92@web:~$ cat posts/devops-patterns-08-air-gapped.md

DEVOPS10 min read

DevOps 패턴과 안티패턴 특별편 — 폐쇄망에서의 DevOps

npm install도 docker pull도 안 되는 폐쇄망에서 안티패턴이 자라는 이유와, 내부 레지스트리·패키지 미러·릴리스 번들·다이제스트 고정·오프라인 취약점 스캔·복구 리허설로 시리즈의 패턴을 이식하는 방법.

이전 강의: 7강. 조직: 도구로는 해결되지 않는 것 이번 강의 키워드: 폐쇄망(Air-gapped), 망연계, 반입 비용, 내부 레지스트리, 패키지 미러, 릴리스 번들, 체크섬, 다이제스트 고정, 오프라인 취약점 스캔, 복구 리허설

들어가며

제조, 금융, 공공, 국방 분야의 많은 시스템은 외부 인터넷과 분리된 폐쇄망(air-gapped network)에서 운영됩니다. 이 환경에 처음 들어가면 지금까지 당연했던 것들이 하나씩 사라집니다. npm install이 동작하지 않고, docker pull도 안 되며, GitHub Actions 같은 SaaS CI는 쓸 수 없고, OS 보안 업데이트도 자동으로 받을 수 없습니다. 외부의 파일을 안으로 들이려면 승인 절차를 거쳐 망연계 솔루션이나 저장 매체로 반입해야 합니다.

그리고 많은 경우 이런 풍경을 만나게 됩니다. 버전 정보 없이 반입된 컨테이너 이미지 파일들, 누가 어떤 옵션으로 띄웠는지 모르는 컨테이너 스택, 담당자의 기억 속에만 있는 기동 순서. 1강부터 7강까지 다룬 안티패턴이 한자리에 모여 있는 셈입니다.

이번 특별편에서는 왜 폐쇄망이 안티패턴의 온상이 되는지를 먼저 분석하고, 시리즈의 패턴들을 이 환경에 맞게 어떻게 변형할 수 있는지 살펴봅니다.

왜 폐쇄망에서는 안티패턴이 자라기 쉬운가

핵심은 반입 비용입니다. 인터넷이 연결된 환경에서 의존성 하나를 추가하는 비용은 명령어 한 줄입니다. 폐쇄망에서는 외부에서 파일을 받고, 반입 신청서를 쓰고, 보안 검토를 기다리고, 매체나 망연계로 옮기고, 내부에서 다시 설치하는 과정이 필요합니다. 이 비용이 모든 변경에 붙습니다.

1강의 악순환을 떠올려 보세요. 변경 비용이 크면 사람들은 변경을 모아서 합니다. 한 번 반입할 때 몇 달치 업데이트를 한꺼번에 들여오고, 배치가 커지니 반입 후 설치 과정에서 문제가 생길 확률도, 원인을 찾기 어려울 확률도 높아집니다. 문제가 생긴 경험은 다음 반입을 더 미루게 만듭니다. 폐쇄망은 1강의 순환이 물리적으로 강제되는 환경입니다.

여기서 여러 안티패턴이 자연스럽게 파생됩니다.

현장 수정의 유혹. 반입이 번거로우니, 문제가 생기면 서버에 직접 들어가 설정을 고치는 것이 가장 빠릅니다. 2강의 Snowflake Server와 Configuration Drift가 여기서 자랍니다.

버전 없는 반입. 외부에서 docker pull myapp:latest 후 docker save로 만든 파일을 myapp.tar라는 이름으로 반입합니다. 폐쇄망 안에서는 그게 어떤 버전인지 확인할 방법이 없습니다. 4강의 latest 태그 문제가 파일 이름 수준으로 옮겨온 것입니다.

기억에 의존하는 운영. 외부 문서나 커뮤니티를 참고하기 어렵고 구성이 특수하다 보니, 시스템에 대한 지식이 구축한 사람의 머릿속에 집중됩니다. 6강의 영웅 문화와 버스 팩터 1이 쉽게 만들어지고, 담당자가 바뀌면 "인수인계 없이 컨테이너 스택을 되살려야 하는" 상황이 생깁니다.

보안 업데이트의 장기 지연. 반입 비용 때문에 패치가 수개월씩 밀리고, "폐쇄망이니 괜찮다"는 논리가 이를 정당화합니다. 하지만 폐쇄망도 내부자, 반입 매체, 망연계 경로를 통한 위협에서 자유롭지 않습니다.

이 안티패턴들은 모두 1강의 정의대로 그 순간에는 합리적인 선택에서 출발합니다. 그래서 개인의 성실함에 기대서는 풀리지 않고, 반입 비용 자체를 구조적으로 낮추는 설계가 필요합니다.

패턴 1. 외부 의존성을 내부에 미러링하기

가장 먼저 할 일은 폐쇄망 내부에 외부 저장소의 대체물을 두는 것입니다.

컨테이너 레지스트리. Harbor 같은 사설 레지스트리를 내부에 구축합니다. 반입한 이미지는 개별 서버에 docker load로 흩뿌리지 않고, 먼저 내부 레지스트리에 푸시한 뒤 모든 서버가 여기서 pull하도록 합니다. 이렇게 하면 "이미지가 어느 서버에 어떤 버전으로 있는지" 문제가 레지스트리 한 곳으로 모입니다.

패키지 저장소. Nexus Repository나 Artifactory 같은 도구는 npm, Maven, PyPI, 컨테이너 이미지 등 여러 형식의 저장소를 한 곳에서 제공합니다. 개발자와 빌드 서버의 패키지 매니저가 이 내부 저장소를 바라보도록 설정해 두면, 폐쇄망 안에서도 평소와 같은 명령어로 설치할 수 있습니다. OS 패키지(apt, yum/dnf)도 같은 방식으로 내부 미러를 구성합니다.

소스 저장소와 CI. Gitea나 GitLab 같은 자체 호스팅 Git 서버와, 그 위에서 도는 CI 러너를 내부에 둡니다. 3강과 4강의 Trunk-Based Development와 Pipeline as Code를 폐쇄망 안에서도 그대로 적용할 수 있는 기반입니다. CI 도구 자체의 플러그인이나 러너 이미지도 미리 반입해 두어야 한다는 점을 잊지 말아야 합니다.

이 인프라를 갖추면 반입의 성격이 바뀝니다. 서버 여러 대에 파일을 개별 복사하는 작업에서 내부 저장소 한 곳을 갱신하는 작업이 되는 것이죠. 이후의 배포와 설치는 폐쇄망 안에서 평범한 DevOps 흐름으로 진행됩니다.

패턴 2. 반입 단위를 릴리스 번들로 표준화하기

반입 자체도 절차로 만들어야 합니다. 핵심은 반입할 파일을 버전이 매겨지고, 검증 가능하고, 자기 설명적인 묶음, 즉 릴리스 번들로 만드는 것입니다.

text
release-bundle-myapp-1.4.2/
├── MANIFEST.md          # 버전, 커밋 해시, 빌드 일시, 변경 내역, 반입 목적
├── images/
│   ├── myapp_1.4.2.tar
│   └── images.txt       # 이미지 이름:태그와 sha256 다이제스트 목록
├── packages/            # 필요한 OS·언어 패키지
├── deploy/
│   ├── compose.yaml     # 다이제스트 또는 명시적 버전으로 고정
│   └── .env.example     # 필요한 설정 키 목록 (값은 없음)
├── sbom/                # 구성 요소 목록
└── SHA256SUMS           # 번들 내 모든 파일의 체크섬

이 구조에는 앞선 강의의 패턴들이 녹아 있습니다. 버전과 다이제스트로 이미지를 식별하는 것은 4강의 latest 태그 대응이고, 설정 키만 두고 값은 폐쇄망 안에서 주입하는 것은 4강의 Twelve-Factor 원칙입니다. SHA256SUMS는 반입 과정에서 파일이 손상되거나 바뀌지 않았음을 확인하게 해줍니다.

bash
# 외부에서 번들 생성 시
find images packages deploy sbom MANIFEST.md -type f -print0 \
  | sort -z | xargs -0 sha256sum > SHA256SUMS
 
# 내부 반입 후 검증
sha256sum -c SHA256SUMS

그리고 가장 중요한 원칙은 4강의 Build Once, Deploy Many입니다. 외부 개발 환경에서 빌드해 검증한 바로 그 아티팩트를 번들에 담아 반입하고, 폐쇄망 안의 모든 환경(검증계, 운영계)에 같은 아티팩트를 씁니다. 폐쇄망 안에서 다시 빌드해야 하는 구조라면, 빌드에 필요한 의존성까지 모두 내부 저장소에 고정된 버전으로 준비하고, lockfile 기반의 재현 가능한 빌드(npm ci 등)를 사용해야 합니다. 두 방식 중 무엇을 택하든, "외부에서 검증한 것과 내부에서 돌아가는 것이 같은가"라는 4강의 질문에 답할 수 있어야 합니다.

반입 비용이 크다는 제약은 바꿀 수 없지만, 번들을 작고 자주 만드는 방향은 여전히 유효합니다. 번들 생성과 검증 과정이 스크립트로 자동화돼 있으면 한 번의 반입에 드는 사람의 수고가 줄고, 그만큼 반입 주기를 짧게 가져갈 여지가 생깁니다.

패턴 3. 폐쇄망 안의 구성도 코드로

2강의 IaC는 폐쇄망에서 오히려 가치가 커집니다. 외부 문서를 참고할 수 없는 환경에서, 코드가 곧 가장 정확한 문서이기 때문입니다.

Ansible은 폐쇄망과 특히 궁합이 좋습니다. 대상 서버에 별도 에이전트를 설치할 필요 없이 SSH만으로 동작하기 때문입니다. 서버의 패키지, 사용자, 설정 파일, 서비스 등록 상태를 플레이북으로 선언하고 내부 Git에 보관합니다.

컨테이너 구성 파일도 반드시 저장소에 둡니다. docker run 명령어를 기억에 의존해 실행하는 대신, Compose 파일에 이미지 버전, 볼륨, 네트워크, 재시작 정책, 헬스체크, 의존 관계를 모두 선언합니다. 컨테이너 스택의 기동 순서와 의존성이 파일에 명시되어 있으면, 담당자가 바뀌어도 같은 스택을 같은 방식으로 띄울 수 있습니다.

yaml
services:
  app:
    image: registry.internal/myapp@sha256:7f3e...c91a
    restart: unless-stopped
    env_file: /etc/myapp/app.env        # 설정은 서버의 관리된 경로에서 주입
    depends_on:
      storage:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
      interval: 30s

그리고 2강에서 경고한 안티패턴, 코드 옆의 수동 변경을 특히 조심해야 합니다. 폐쇄망에서는 "급하니까 일단 서버에서 고치고 나중에 코드에 반영하자"는 유혹이 훨씬 강합니다. 현장에서 수정했다면 당일 안에 저장소에 반영하는 것을 팀 규칙으로 삼고, 주기적으로 플레이북을 점검 모드(--check --diff)로 돌려 드리프트를 확인하는 것이 좋습니다.

패턴 4. 보안 검사를 오프라인으로

4강의 Shift-Left는 폐쇄망에서도 포기할 이유가 없습니다. 오히려 반입 시점이 자연스러운 검사 관문이 됩니다.

외부에서 번들을 만들 때 이미지 취약점 스캔과 SBOM 생성을 파이프라인에 넣고, 그 결과를 번들에 함께 담아 반입 심사 자료로 씁니다. 폐쇄망 내부에서도 주기적으로 검사하고 싶다면, 취약점 데이터베이스를 별도로 내려받아 반입해 오프라인으로 사용할 수 있는 스캐너(Trivy 등)를 활용할 수 있습니다. 취약점 DB 역시 주기적으로 갱신 반입해야 의미가 있다는 점을 기억해야 합니다.

SBOM을 보관해 두면 외부에서 특정 라이브러리의 심각한 취약점이 공개됐을 때, 폐쇄망 안의 어떤 시스템이 영향을 받는지 서버에 접속하지 않고도 파악할 수 있습니다.

패턴 5. 복구를 리허설하기

마지막 패턴은 2강의 Phoenix Server, 5강의 롤백 연습, 6강의 카오스 엔지니어링을 폐쇄망 규모로 옮긴 것입니다. 시스템을 처음부터 다시 세우는 과정을 정기적으로 연습하는 것입니다.

운영 서버를 무작위로 끄는 것은 폐쇄망의 보수적인 운영 환경에서 받아들여지기 어렵습니다. 대신 검증계나 별도의 연습 환경에서, 저장소의 코드와 반입된 번들만으로 서비스를 처음부터 기동해 보는 리허설을 분기마다 해볼 수 있습니다. 이때 지켜야 할 규칙은 하나입니다. 담당자의 기억이나 기존 서버를 참고하지 않는다. 리허설 중 막히는 지점이 바로 코드와 문서에서 빠진 부분이고, 그 지점을 채우는 것이 리허설의 산출물입니다.

리허설 결과로 런북을 계속 갱신합니다. 서비스 기동·중지 순서, 의존 관계, 데이터 백업과 복원 절차, 흔한 장애와 대응법을 담습니다. 6강에서 버스 팩터를 높이는 방법으로 런북을 꼽았는데, 외부 자료를 참고하기 어려운 폐쇄망에서 런북은 사실상 유일한 운영 지식 저장소입니다.

리허설에는 데이터 복원도 포함해야 합니다. 백업은 복원해 보기 전까지는 백업이 아닙니다.

폐쇄망 증상별 처방

증상해당 안티패턴처방
서버마다 docker load로 이미지를 따로 넣음서버 간 드리프트 (2강)내부 레지스트리
myapp.tar의 버전을 아무도 모름latest 의존 (4강)릴리스 번들, 다이제스트 고정, MANIFEST
반입 파일이 손상됐는지 알 수 없음재현성 상실 (1강)체크섬 검증
컨테이너 기동 명령이 담당자 기억에만 있음영웅 문화 (6강)Compose 파일의 저장소 관리, 런북
급한 수정은 서버에서 직접Snowflake Server (2강)Ansible, 당일 코드 반영 규칙, 드리프트 점검
반입은 반년에 한 번, 한꺼번에빅뱅 릴리스 (5강)번들 자동화로 반입 비용 낮추기, 작고 잦은 반입
보안 패치가 수개월 밀림막판 보안 게이트 (4강)반입 전 스캔, 오프라인 스캐너, 정기 DB 갱신
담당자 교체 후 아무도 재기동을 못 함버스 팩터 1 (6강)복구 리허설

시리즈를 마치며

여덟 편의 강의를 한 줄로 요약하면 이렇습니다. DevOps 안티패턴은 "변경을 막을수록 변경이 위험해지는" 순환의 여러 얼굴이고, DevOps 패턴은 그 순환을 뒤집는 여러 방법이다.

1강에서 세운 세 가지 질문을 다시 떠올려 보겠습니다.

재현 가능한가? 2강의 IaC와 Immutable Infrastructure, 4강의 불변 아티팩트와 다이제스트 고정, 특별편의 릴리스 번들과 복구 리허설이 이 질문에 대한 답이었습니다.

변경 단위가 작은가? 3강의 Trunk-Based Development, 5강의 Canary, Feature Flag, Expand and Contract, Strangler Fig가 이 질문에 대한 답이었습니다.

피드백이 빠른가? 3강의 CI와 테스트 피라미드, 4강의 Shift-Left, 6강의 관측 가능성과 SLO 기반 알림이 이 질문에 대한 답이었습니다.

그리고 6강의 Blameless Postmortem과 7강의 조직 구조는 이 모든 패턴이 뿌리내릴 토양에 관한 이야기였습니다. 안티패턴은 국소적으로 합리적인 선택이 쌓인 구조이므로, 사람을 탓하는 대신 구조를 바꿔야 한다는 것이죠.

폐쇄망처럼 제약이 많은 환경에서는 이 패턴들을 교과서 그대로 적용할 수 없습니다. 하지만 도구가 바뀌어도 질문은 바뀌지 않습니다. 새로운 도구나 관행을 만났을 때, 혹은 지금 운영 중인 시스템을 돌아볼 때, 이 세 가지 질문을 던져 보시기 바랍니다. 답이 흐릿한 곳에 다음 개선 과제가 있습니다.

COMMENTS (…)

댓글을 불러오는 중이에요.

NEW COMMENT0 / 1000
⌘↵ 전송

blog92@web:~$ cd ..