클라우드 플랫폼(AWS, Google Cloud, Azure) 배포
VM, 관리형 장기 실행 컨테이너, Kubernetes, 함수 또는 잡 컨테이너를 운영 책임으로 비교하고 재현 가능한 NestJS 배포 기준을 세웁니다.
앞 글에서는 NestJS 애플리케이션의 빌드, 테스트, 배포를 자동화하는 CI/CD 파이프라인을 만들었습니다. 이 글에서는 그 파이프라인이 만든 컨테이너 이미지를 클라우드에 배포합니다.
상품 이름보다 먼저 확인할 것은 배포한 뒤에도 팀이 맡아야 할 운영 일입니다. 호스트 OS, 오케스트레이터, 애플리케이션 설정을 누가 관리하느냐에 따라 필요한 기술과 비용 구조가 달라집니다.
Nest · Operating responsibility
실행 모델별 남는 운영 책임
VM·관리형 서비스·Kubernetes·함수/잡은 공급자와 팀의 책임 경계가 다릅니다. 이미지·ID·네트워크·종료·관측·복구 책임과 확장·비용 조건을 함께 비교합니다.
| 실행 모델 | 대표 서비스 | 공급자 운영 | 팀 운영 | 확장과 비용 |
|---|---|---|---|---|
| VM 직접 운영 | EC2, Compute Engine, Azure VM | 시설, 물리 호스트와 가상화 계층 | 게스트 OS와 런타임 패치, 이미지, 용량, LB와 TLS, 배포와 롤백 | 팀이 ASG 등을 설계하며 프로비저닝한 인스턴스와 부속 자원에 과금 |
| 관리형 서비스 | ECS 서비스 on Fargate, Cloud Run 서비스, Container Apps | 호스트, 실행 제어면, 기본 스케줄링 | 이미지 digest, 서비스 설정, ID와 비밀, 네트워크, 상태 확인, 종료와 drain, 관측성, 롤아웃 | 최소, 최대 수, 동시성, 확장 신호를 설정하며 과금 모드와 최소 인스턴스를 확인 |
| 관리형 Kubernetes | EKS, GKE, AKS | 관리형 제어면과 선택한 자동 관리 기능 | 워크로드, RBAC와 네트워크 정책, 노드 모드, 업그레이드, 보안과 관측성 | HPA와 KEDA에도 메트릭이 필요하며 클러스터, 노드와 네트워크 비용이 남음 |
| 함수 / 잡 | Lambda 이미지, Cloud Run jobs, Container Apps jobs | 함수 호출 또는 잡 실행 인프라. trigger와 scheduler 제공 범위는 제품별로 다름 | Lambda 이벤트 연결, Cloud Run용 별도 Cloud Scheduler, Container Apps 잡 trigger, IAM, timeout, 재시도와 멱등성 | 실행과 동시성 기준을 확인하며 장기 실행 웹 서비스와 별도로 평가 |
VM 직접 운영
- 대표 서비스
- EC2, Compute Engine, Azure VM
- 공급자 운영
- 시설, 물리 호스트와 가상화 계층
- 팀 운영
- 게스트 OS와 런타임 패치, 이미지, 용량, LB와 TLS, 배포와 롤백
- 확장과 비용
- 팀이 ASG 등을 설계하며 프로비저닝한 인스턴스와 부속 자원에 과금
관리형 장기 실행 컨테이너
- 대표 서비스
- ECS 서비스 on Fargate, Cloud Run 서비스, Container Apps
- 공급자 운영
- 호스트, 실행 제어면, 기본 스케줄링
- 팀 운영
- 이미지 digest, 서비스 설정, ID와 비밀, 네트워크, 상태 확인, 종료와 drain, 관측성, 롤아웃
- 확장과 비용
- 최소, 최대 수, 동시성, 확장 신호를 설정하며 과금 모드와 최소 인스턴스를 확인
관리형 Kubernetes
- 대표 서비스
- EKS, GKE, AKS
- 공급자 운영
- 관리형 제어면과 선택한 자동 관리 기능
- 팀 운영
- 워크로드, RBAC와 네트워크 정책, 노드 모드, 업그레이드, 보안과 관측성
- 확장과 비용
- HPA와 KEDA에도 메트릭이 필요하며 클러스터, 노드와 네트워크 비용이 남음
함수 또는 잡 컨테이너
- 대표 서비스
- Lambda 이미지, Cloud Run jobs, Container Apps jobs
- 공급자 운영
- 함수 호출 또는 잡 실행 인프라. trigger와 scheduler 제공 범위는 제품별로 다름
- 팀 운영
- Lambda 이벤트 연결, Cloud Run용 별도 Cloud Scheduler, Container Apps 잡 trigger, IAM, timeout, 재시도와 멱등성
- 확장과 비용
- 실행과 동시성 기준을 확인하며 장기 실행 웹 서비스와 별도로 평가
“서버를 보지 않는다”는 “운영할 것이 없다”는 뜻이 아닙니다. 배포 이미지부터 트래픽, 자격 증명, 관측성과 복구까지 남는 책임을 팀 역량과 맞추세요.
위 표는 VM, 관리형 장기 실행 컨테이너, 관리형 Kubernetes, 함수나 잡 컨테이너라는 네 가지 실행 모델을 나란히 비교합니다. 줄마다 클라우드 공급자가 관리하는 범위, 팀이 계속 관리해야 하는 범위, 확장과 비용이 정해지는 방식이 적혀 있습니다. 아래로 내려갈수록 공급자가 맡는 범위가 넓어지지만, 이미지, 자격 증명, 네트워크, 헬스 확인, 종료 처리는 어느 모델에서든 팀의 몫으로 남는다는 점이 드러납니다.
실행 모델별로 팀이 맡는 일
VM에 직접 배포
AWS EC2, Google Compute Engine, Azure Virtual Machines에서는 팀이 가상 머신의 게스트 OS부터 관리합니다. Docker 설치, OS와 런타임 패치, 용량 계획, 로드 밸런서와 TLS, 배포와 롤백 자동화를 직접 설계해야 하지만, 그만큼 네트워크와 런타임을 세밀하게 제어할 수 있습니다.
VM 한 대로 시작할 수는 있지만, 운영에서는 장애 영역을 나눈 여러 인스턴스와 로드 밸런서, 감사할 수 있는 관리 경로를 함께 설계해야 합니다. 쓰지 않는 인스턴스에도 비용이 들므로 “가장 단순한 운영 모델”과 같은 말은 아닙니다.
EC2에서 컨테이너를 실행하는 최소 예시
아래 명령은 Ubuntu 배포판 패키지를 쓰는 예시입니다. 실제 운영 이미지는 설치 절차와 버전을 팀의 기준으로 고정합니다.
sudo apt-get update
sudo apt-get install -y docker.io
sudo systemctl enable --now docker
sudo usermod -aG docker deployer
# 그룹 변경은 deployer의 다음 로그인 세션부터 적용한다.docker 그룹에 속한 사용자는 Docker 데몬을 거쳐 호스트의 root 수준 권한을 얻을 수 있습니다. 전용 배포 사용자, 제한된 SSH나 Session Manager 경로, 감사 로그를 함께 적용하며, 자세한 위험은 Docker의 Linux 설치 후 단계를 참고합니다.
운영 이미지는 바뀔 수 있는 태그가 아니라 레지스트리가 돌려준 digest로 고정합니다. digest를 쓰면 같은 명령이 항상 같은 이미지 내용을 가리킵니다.
IMAGE='123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/nestjs-app@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
docker pull "$IMAGE"비밀 값은 명령줄에 직접 쓰지 않고 접근 권한이 제한된 env 파일이나 비밀 저장소에서 주입합니다. 아래 예시는 로컬 프록시나 로드 밸런서만 접근하도록 호스트의 loopback 주소에 바인딩합니다.
IMAGE='123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/nestjs-app@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
docker run -d \
--name nestjs-prod-app \
--restart unless-stopped \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--env-file /etc/nestjs-app/production.env \
-p 127.0.0.1:3000:3000 \
"$IMAGE"digest로 이미지를 내려받는 방법은 Docker pull의 digest 설명에서 확인할 수 있습니다.
관리형 장기 실행 컨테이너
AWS ECS 서비스 on Fargate, Google Cloud Run 서비스, Azure Container Apps는 호스트와 실행 제어면의 상당 부분을 공급자가 운영합니다. 하지만 팀은 여전히 다음을 정해야 합니다.
- 배포할 이미지 digest와 이전 정상 릴리스
- 워크로드 아이덴티티와 비밀 저장소 참조
- 공개·비공개 ingress, egress, TLS, 데이터베이스 연결 경로
- CPU, 메모리, 최소·최대 인스턴스 수, 동시성과 확장 신호
- 플랫폼별 시작, 트래픽 수신, 생존 확인과 로그·지표·알림, 롤아웃, 롤백 정책
- 종료 신호 전달, 새 요청 차단과 진행 중 요청 정리(drain), 데이터베이스와 큐 정리, 강제 종료 전의 grace 또는 stop timeout
이미지를 넘기는 배포 방식에서 플랫폼은 이미지를 가져와 실행할 뿐이며, 이미지 빌드는 별도의 CI나 빌드 서비스가 맡습니다. 소스 기반 배포를 명시적으로 고른 경우에만 빌드가 그 플랫폼의 흐름에 포함됩니다.
컨테이너가 축소나 롤아웃으로 종료될 때 애플리케이션이 지켜야 할 일도 남습니다. NestJS bootstrap에서 app.enableShutdownHooks()를 호출하고, beforeApplicationShutdown이나 onApplicationShutdown에서 새 작업을 받지 않게 한 뒤 진행 중인 요청을 마무리하고 데이터베이스 연결과 큐 consumer를 정리합니다. 컨테이너 명령은 Node 프로세스를 exec 형식으로 직접 실행하거나 init 프로세스가 SIGTERM을 전달하게 해서, 셸이 신호를 가로막지 않게 합니다. 정리에 걸리는 시간을 측정한 뒤 Cloud Run 컨테이너 런타임 규칙, ECS stopTimeout, Azure Container Apps 수명 주기, Kubernetes의 terminationGracePeriodSeconds에 맞춥니다. probe가 이 정상 종료 처리를 대신하지는 않습니다.
자동 확장도 저절로 켜지지 않습니다. 예를 들어 ECS 서비스는 Application Auto Scaling 정책과 최소·최대 태스크 수를 설정해야 하며, AWS는 수요와 용량의 관계를 나타내는 사용률이나 포화도 지표를 고르라고 안내합니다. ECS 서비스는 원하는 태스크 수를 유지하고 Fargate는 그 태스크를 실행할 용량을 제공하므로, Fargate 자체를 별도의 웹 서비스 상품처럼 다루지 않습니다.
Cloud Run은 설정에 따라 요청 기반이나 인스턴스 기반으로 과금되고, 최소 인스턴스를 두지 않은 서비스는 기본적으로 0까지 줄어들 수 있습니다. 그러니 “항상 쓴 요청만큼만 과금된다”고 일반화하지 말고 Cloud Run 과금 설정과 서비스 동작을 함께 확인합니다. Azure Container Apps도 HTTP, TCP, 사용자 정의 확장 규칙과 최소·최대 replica를 팀이 설정합니다.
헬스 확인의 이름과 동작도 플랫폼마다 다릅니다. Kubernetes와 Azure Container Apps는 startup, readiness, liveness probe를 구분합니다. Cloud Run도 세 종류의 probe를 제공하지만, startup이 성공한 직후 첫 readiness 결과보다 먼저 트래픽을 받을 수 있으므로 startup 자체가 안전하게 요청을 받을 수 있는 상태를 보장해야 합니다. ECS는 컨테이너 health check의 startPeriod와 로드 밸런서의 target health를 함께 씁니다. 그러니 Kubernetes의 필드 이름을 모든 상품에 그대로 옮기지 말고, “초기화 중 보호, 새 트래픽 수신 여부, 복구할 수 없는 프로세스의 재시작”이라는 의도를 각 플랫폼의 기능에 대응시킵니다.
AWS App Runner는 새 프로젝트의 기본 후보로 삼지 않습니다. AWS 공식 안내에 따르면 2026년 4월 30일부터 신규 고객을 받지 않으며, 기존 고객은 계속 쓸 수 있지만 새 기능은 계획되어 있지 않습니다. 새 AWS 워크로드는 요구사항에 따라 ECS Express Mode나 ECS 서비스 on Fargate 등을 검토합니다.
관리형 Kubernetes
Amazon EKS, Google Kubernetes Engine, Azure Kubernetes Service는 Kubernetes 제어면 관리의 일부를 공급자에게 맡깁니다. 팀은 Deployment와 Service 같은 워크로드 객체뿐 아니라 namespace와 RBAC, 네트워크 정책, 노드나 자동 관리 모드, 클러스터 업그레이드, 관측과 비용까지 운영해야 합니다. Kubernetes가 오픈소스라는 사실만으로 애플리케이션, IAM, 네트워크 구성이 클라우드 사이에서 그대로 옮겨지지는 않습니다.
다음 Deployment를 적용하기 전에 같은 namespace에 nestjs-app ServiceAccount와 app-secrets Secret을 먼저 만들어야 합니다. 없는 ServiceAccount를 쓰면 Pod 생성 단계에서 거부되고, 필요한 Secret을 찾지 못하면 컨테이너가 시작되지 않습니다. 또 Kubernetes ServiceAccount 이름만으로 클라우드 API 권한이 생기지는 않으므로, EKS Pod Identity나 IRSA, GKE Workload Identity Federation, AKS Workload Identity의 공급자 쪽 연결과 최소 권한 IAM 설정을 따로 구성합니다. /health/startup, /health/ready, /health/live도 Nest가 자동으로 만들어 주는 경로가 아니므로, 다음 글에서 다룰 @nestjs/terminus나 전용 controller로 구현해야 합니다.
이 준비가 끝났다는 전제에서, 다음 Deployment는 이미지 digest, 워크로드용 ServiceAccount, 비밀 참조, 서로 다른 세 probe, 정상 종료를 위한 grace period를 명시합니다. digest의 0123... 값은 구조를 설명하기 위한 자리표시자이므로, 실제로 레지스트리에 게시되어 권한 있는 실행 환경에서 내려받을 수 있는 digest로 바꿉니다. 이 장은 실행 모델마다 배포에서 정해야 할 것을 비교할 뿐, 실제 클라우드 계정, IAM, 과금 설정을 대신 구성하지 않습니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nestjs-app
labels:
app: nestjs-app
spec:
replicas: 3
selector:
matchLabels:
app: nestjs-app
template:
metadata:
labels:
app: nestjs-app
spec:
serviceAccountName: nestjs-app
terminationGracePeriodSeconds: 30
containers:
- name: nestjs-container
image: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/nestjs-app@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
ports:
- name: http
containerPort: 3000
env:
- name: DATABASE_HOST
valueFrom:
secretKeyRef:
name: app-secrets
key: DATABASE_HOST
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: app-secrets
key: JWT_SECRET
startupProbe:
httpGet:
path: /health/startup
port: http
periodSeconds: 2
failureThreshold: 30
readinessProbe:
httpGet:
path: /health/ready
port: http
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /health/live
port: http
periodSeconds: 10
failureThreshold: 3
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
securityContext:
runAsNonRoot: true
runAsUser: 10001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefaultstartupProbe가 성공하기 전에는 readiness와 liveness 확인이 시작되지 않습니다. 그 뒤 readiness가 실패하면 Pod가 Service 엔드포인트에서 빠지고, liveness가 실패하면 컨테이너가 재시작됩니다. 이 역할들을 하나의 /health 응답으로 처리하지 말고, Kubernetes probe 문서에 맞게 확인할 의존성을 나눕니다. Pod가 종료될 때는 엔드포인트가 트래픽 대상에서 빠지고 컨테이너에 SIGTERM이 전달됩니다. Nest는 grace period 안에 새 요청을 막고 진행 중인 요청, 데이터베이스 연결, 큐 작업을 정리해야 하며, 예시의 30초는 측정 결과에 맞춰 조정합니다.
Service는 클러스터 안에서 바뀌지 않는 목적지를 제공합니다. HTTP 라우팅과 TLS는 Ingress나 Gateway, 클라우드의 L7 로드 밸런서에서 따로 구성합니다.
apiVersion: v1
kind: Service
metadata:
name: nestjs-app
spec:
selector:
app: nestjs-app
ports:
- name: http
protocol: TCP
port: 80
targetPort: http
type: ClusterIPKubernetes Secret은 비밀 값을 전달하는 객체일 뿐 완전한 비밀 관리 체계가 아닙니다. Kubernetes 문서의 설명대로 Secret은 기본 설정에서 etcd에 암호화되지 않은 채 저장될 수 있으므로, 저장 시 암호화, 최소 권한 RBAC, Pod 접근 제한, 외부 비밀 저장소 연동을 함께 구성합니다. 클라우드 API 접근에는 정적 키보다 EKS Pod Identity나 IRSA, GKE Workload Identity Federation, AKS Workload Identity 같은 단기 워크로드 자격 증명을 우선합니다.
함수 또는 잡 컨테이너
함수와 잡은 둘 다 실행한 뒤 끝날 수 있지만, 무엇이 실행을 시작하는지가 다릅니다. Lambda 컨테이너 이미지는 요청이나 이벤트로 호출되는 함수 패키지입니다. Cloud Run job은 API, CLI, workflow 등으로 실행하는 작업이며, 정기 실행에는 별도의 Cloud Scheduler가 필요합니다. Azure Container Apps job은 수동, 일정, 이벤트 트리거를 제공합니다. 플랫폼은 함수 호출이나 잡 실행의 인프라를 운영하지만, 어떤 이벤트나 스케줄러가 실행을 시작하는지, 호출 IAM, timeout, 재시도, 멱등성, 동시 실행 수, 작업별 비용은 팀이 제품마다 설계해야 합니다.
특히 Lambda의 컨테이너 이미지는 일반 Docker 서비스를 그대로 호스팅하는 기능이 아니라, Lambda Runtime API에 맞춘 함수 패키징 방식입니다. Lambda 컨테이너 이미지 요구사항에 적힌 읽기 전용 파일 시스템과 /tmp, 런타임 인터페이스 같은 Lambda의 실행 규칙을 따라야 합니다. NestJS를 Lambda에 올리는 방법은 다음 장에서 따로 다룹니다.
더 깊이: SIGTERM을 받은 뒤에도 요청이 들어오는 이유
Kubernetes에서 Pod가 종료될 때 두 일은 차례로가 아니라 동시에 시작됩니다. Pod 수명 주기 문서에 따르면 kubelet이 Pod의 정상 종료를 시작하는 것과 같은 때에, 제어면은 그 Pod를 Service의 EndpointSlice에서 뺄지 판단합니다. 종료 중인 엔드포인트는 바로 지워지지 않고 ready가 false인 종료 상태로 표시되며, 로드 밸런서는 이 상태를 보고 새 트래픽을 보내지 않습니다. 이 변화가 모든 경로에 반영되기 전이라면, 애플리케이션이 SIGTERM을 받은 뒤에도 새 요청이 잠깐 도착할 수 있습니다.
kubelet 쪽의 순서도 정해져 있습니다. 컨테이너에 preStop hook이 있으면 그것을 먼저 실행한 뒤 각 컨테이너의 PID 1에 TERM 신호를 보내며, 이 모든 시간이 기본 30초인 terminationGracePeriodSeconds 안에 들어가야 합니다. 유예 시간이 끝났는데도 preStop이 실행 중이면 kubelet은 한 번만 2초를 더 주고, 그래도 끝나지 않은 컨테이너는 강제로 종료됩니다. 그래서 preStop으로 기다리는 시간과 Nest의 종료 hook이 정리에 쓰는 시간을 합쳐 유예 시간보다 짧게 정하고, 종료 hook이 시작된 뒤 잠시 들어오는 요청도 오류 없이 마무리할 수 있게 설계합니다.
배포 준비 상태를 한눈에 확인하기
클라우드 상품을 고른 뒤에도 “이미지가 실행되었다”를 완료 조건으로 삼지 않습니다. 현재 digest와 이전 정상 릴리스를 포함한 릴리스 기록을 버전별로 남기고, 배포 명세가 아이덴티티·비밀·네트워크 정책을 참조하는지, 플랫폼별 시작·트래픽·생존 확인과 용량 규칙이 실행을 제어하는지 확인합니다. 종료할 때는 SIGTERM이 Node 프로세스까지 전달되고, 새 요청 차단과 진행 중 요청 정리, 데이터베이스와 큐 정리가 플랫폼의 grace나 stop timeout 안에 끝나야 합니다. 로그, 지표, 알림에는 확장 변경이나 롤백을 결정할 담당자와 확인 결과를 연결합니다.
Cloud Run revision, Azure Container Apps revision, ECS service revision은 지원하는 범위 안에서 바뀌지 않는 릴리스 기록을 제공합니다. 반면 Kubernetes Deployment의 revision은 ReplicaSet에 남는 Pod template의 이력이라 revisionHistoryLimit에 따라 지워질 수 있고, VM에서는 팀이 이미지 digest와 배포 명세의 이력을 직접 보존해야 합니다. ECS의 실패 감지 뒤 자동 롤백도 deployment circuit breaker를 명시적으로 켜야 동작합니다. 어떤 플랫폼이든 자동 복구를 가정하지 말고, 이전 정상 릴리스 기록과 실행 권한, 승인 절차로 실제 롤백 경로를 검증합니다.
Nest · Deployment readiness
릴리스·제어면·런타임의 연결
세 구역은 시간 순서가 아니라 운영 책임 경계입니다. 현재와 이전 정상 release record, 플랫폼별 gate·capacity, 실제 운영 신호를 연결하고 owner가 복구와 결과 검증을 책임집니다.
현재와 이전 정상 릴리스 기록을 함께 보존
현재 배포는 승인, 빌드, 배포 기록과 같은 immutable image digest를 가리킵니다. 이전 정상 digest와 versioned record도 실행 가능하게 보존하고 승인된 명시적 rollback 대상으로 연결합니다. immutable revision은 이를 지원하는 서비스에서만 사용합니다.
ID, 비밀, 네트워크와 배포 정책 선언
배포 명세는 workload ID, secret 참조, ingress와 egress, TLS와 DB 경로, 현재 digest, traffic 대상, desired capacity, CPU와 memory를 선언합니다.
시작은 초기화 보호, 트래픽은 요청 수신 가능 여부, 생존은 복구 불가 프로세스 재시작을 뜻합니다. Kubernetes와 Container Apps는 세 probe를 구분하고, Cloud Run은 startup부터 요청 준비를 보장하고, ECS는 container health와 LB target health를 조합합니다. 각 플랫폼에 capacity와 사용률 신호를 별도로 설정합니다.
런타임 신호를 확장과 복구 판단으로 환류
task, replica, revision instance는 선언한 digest와 workload ID, 허용된 network 경로로 실행됩니다. 종료 시 SIGTERM을 전달하고 grace 또는 stop timeout 안에 요청을 drain하며 DB와 queue resource를 정리합니다.
구조화 log, metric, alert에서 오류율, 지연, 포화도 신호를 만들고 scale 변경, remediation 또는 이전 릴리스 rollback의 owner, 결정, 검증 결과를 기록합니다.
- 선언, binding, 제어 관계
- 런타임 evidence의 feedback
Cloud Run, Azure Container Apps, ECS가 제공하는 immutable revision은 지원 서비스의 release record입니다. Kubernetes Deployment revision은 보존 한도가 있는 ReplicaSet 이력이고 VM 이력은 팀이 관리합니다. 화살표는 시간 순서가 아니라 선언, 제어, evidence의 방향이며 강조선은 owner가 이전 정상 릴리스를 선택하는 명시적 rollback입니다.
위 그림은 배포 준비 상태를 릴리스 기록, 배포 제어, 실행 중 운영 정보라는 세 영역으로 나누어 보여 줍니다. 릴리스 기록에는 현재 이미지 digest와 이전 정상 릴리스, 플랫폼의 바뀌지 않는 revision이 남고, 배포 제어 영역에서는 워크로드 아이덴티티와 비밀 참조, 네트워크 정책, 헬스 확인과 용량 규칙이 실행을 다룹니다. 실행 중 운영 영역의 로그, 지표, 알림은 담당자와 롤백 결정으로 이어집니다.
팀이 게스트 OS 패치와 Docker 설치를 맡을 수 있고 특별한 네트워크 설정이 필요하다면 VM 직접 운영을 검토할 수 있습니다. 호스트 운영 부담을 줄이려고 관리형 장기 실행 서비스를 고르더라도, 이미지 digest, 요청 수신 조건, 비밀과 네트워크, 용량과 종료 처리는 여전히 팀이 확인할 항목입니다. 운영 모델을 고르는 일과 배포를 준비하는 일은 서로 다른 판단이며, 관리형 서비스도 애플리케이션 이미지, 자격 증명, 네트워크, 헬스 확인, 관측, 롤백까지 대신해 주지는 않습니다.
정리
- VM은 게스트 OS부터, 관리형 컨테이너는 이미지와 설정부터, 관리형 Kubernetes는 워크로드 객체와 클러스터 운영까지 팀이 맡으며, 어느 모델이든 이미지·자격 증명·네트워크·헬스 확인·종료 처리는 팀의 몫입니다.
- 운영 이미지는 태그가 아니라 digest로 고정하고, 현재 digest와 이전 정상 릴리스를 함께 기록합니다.
- 헬스 확인의 이름과 동작은 플랫폼마다 다르므로, 초기화 보호·트래픽 수신·재시작이라는 의도를 각 플랫폼 기능에 대응시킵니다.
- 종료할 때는
enableShutdownHooks()와 exec 형식 명령으로SIGTERM을 Nest까지 전달하고, 정리 시간을 플랫폼의 grace period에 맞춥니다. - AWS App Runner는 2026년 4월 30일부터 신규 고객을 받지 않으므로, 새 AWS 워크로드는 ECS 같은 다른 선택지를 검토합니다.
연습 문제
1. readiness만 실패하면
위 Kubernetes 예시에서 startup은 이미 성공했고, 같은 Pod가 DB 연결 장애로 readiness만 연속으로 실패한다고 가정합니다. Service를 통한 새 트래픽과 컨테이너 재시작에는 각각 어떤 영향이 있을까요? replicas: 3만으로 세 Pod가 서로 다른 장애 영역에 놓이고 자동으로 확장된다고 판단할 수 있을까요?
해설 보기
readiness 실패는 해당 Pod를 Service의 새 트래픽 대상에서 빼는 조건일 뿐, 컨테이너를 재시작하지는 않습니다. 재시작은 liveness 실패 같은 다른 원인이 있을 때 일어납니다.
replicas: 3은 원하는 Pod 수를 선언할 뿐, 장애 영역에 나누어 배치하거나 수요에 따라 확장하는 정책을 선언하지 않습니다. 분산 배치 제약과 실제 노드 배치, HPA 같은 확장 설정과 지표, 한도를 따로 확인해야 합니다. 남은 정상 Pod의 처리 용량과 DB 자체의 장애도 별도로 판단합니다.
2. docker 그룹에 넣은 배포 사용자
EC2 예시처럼 배포 사용자 deployer를 docker 그룹에 넣었습니다. 이 사용자의 SSH 키가 유출되면 공격자는 무엇까지 할 수 있을까요?
해설 보기
Docker 공식 문서의 경고처럼 docker 그룹은 사실상 root와 같은 권한을 줍니다. 공격자는 docker run -v /:/host ...처럼 호스트의 루트 파일 시스템을 컨테이너에 마운트해 시스템 파일을 읽고 고칠 수 있으므로, 배포 사용자 하나의 키 유출이 호스트 전체의 장악으로 이어질 수 있습니다.
그래서 배포 사용자는 정해진 배포 명령만 실행하게 제한하고, 접속은 Session Manager처럼 감사 로그가 남는 경로로 열며, 키는 짧은 수명의 자격 증명으로 바꾸거나 주기적으로 교체합니다. 가능하다면 호스트에 직접 접속하지 않는 관리형 실행 모델로 옮기는 것도 방법입니다.
3. 셸 형식의 CMD
누군가 Dockerfile의 마지막 줄을 CMD ["node", "dist/main.js"]에서 CMD node dist/main.js로 바꾸었습니다. enableShutdownHooks()는 켜져 있습니다. Kubernetes가 롤아웃 중에 이 Pod를 종료하면 어떤 일이 생길까요?
해설 보기
셸 형식의 CMD는 /bin/sh -c로 실행되므로 컨테이너의 1번 프로세스가 Node가 아니라 셸이 됩니다. Kubernetes가 보낸 SIGTERM은 셸이 받는데, 셸은 이 신호를 자식인 Node 프로세스에 전달하지 않을 수 있습니다.
그러면 Nest의 종료 hook이 실행되지 않아 진행 중인 요청과 데이터베이스 연결을 정리하지 못하고, grace period인 30초가 지난 뒤 SIGKILL로 강제 종료됩니다. 롤아웃이 느려지고 처리 중이던 요청이 끊길 수 있으므로, exec 형식으로 Node를 직접 실행하거나 신호를 전달하는 init 프로세스를 둡니다.
다음 글 모니터링과 로깅 시스템 구축에서는 배포한 애플리케이션의 헬스 확인을 @nestjs/terminus로 구현하고, 로그와 지표를 모아 운영 상태를 지켜보는 방법을 다룹니다.