앱 배포 골격
21장에서 띄운 비어 있는 EKS 클러스터 위에 샘플 서비스 myshop-api를 한 묶음의 매니페스트로 배포합니다. Namespace · ServiceAccount · ConfigMap · Secret · Deployment · Service · Ingress · HPA · PodDisruptionBudget의 9개 객체를 한 사이클로 정리하고, AWS Load Balancer Controller로 ALB를 자동 프로비저닝합니다. 그 묶음을 Helm 차트로 추상화해 dev / prod에 서로 다른 values로 적용하는 방식까지 한 번에 따라갑니다.
21장 EKS 클러스터 셋업에서 비어 있는 EKS 클러스터 한 대가 준비되었습니다. 노드는 떠 있고 시스템 Pod (CoreDNS, kube-proxy, VPC CNI, EBS CSI)는 살아 있지만, 우리의 워크로드는 한 줄도 그 위에 올라가지 않은 상태입니다. 이번 챕터는 그 빈 곳을 채웁니다. 샘플 백엔드 서비스 myshop-api를 9개의 매니페스트로 정리하고, AWS Load Balancer Controller로 ALB를 자동 프로비저닝해 외부 진입점을 만들고, 그 모든 매니페스트를 Helm 차트로 묶어 dev / prod 두 환경에 다른 values로 배포되도록 만듭니다.
이번 챕터의 끝에서는 myshop-api가 외부에서 HTTPS로 접근 가능한 상태가 됩니다. 데이터 저장소가 없어서 /health/ready만 200을 돌려주는 빈 껍데기지만, 그 빈 부분은 다음 23장에서 RDS · External Secrets로 채웁니다.
myshop-api의 표준 매니페스트 묶음 #
운영 워크로드 한 개를 K8s에 올리는 표준 묶음을 한 표로 정리합니다.
| 객체 | 역할 | 본 책의 출처 |
|---|---|---|
Namespace | 워크로드를 묶는 격리 단위 | 7장 |
ServiceAccount | Pod의 ID + IRSA 부착점 | 16장 |
ConfigMap | 환경 설정값 (로그 레벨, feature flag) | 6장 |
Secret | DB 비밀번호 등 | 6장 |
Deployment | 실제 워크로드 (Pod replicas) | 4장 |
Service | 클러스터 내부의 가상 IP | 5장 |
Ingress | 외부 진입점 (ALB로 풀림) | 10장 |
HorizontalPodAutoscaler | CPU 기반 Pod 자동 조정 | 13장 |
PodDisruptionBudget | 자발적 disruption의 가용성 하한선 | 13장 |
1~3부에서 객체 차원으로 나눠 다룬 모델들이 이 한 표 안에 모입니다. 이번 챕터는 그 모델들을 한 묶음의 운영 매니페스트로 조립하는 단계입니다. 각 객체의 단독 사용법보다, 묶음으로 조립했을 때의 환경 변수 주입 방식에 집중합니다.
Namespace와 ServiceAccount #
apiVersion: v1
kind: Namespace
metadata:
name: myshop
labels:
name: myshop
team: platform
env: prod
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: myshop-api
namespace: myshop
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/myshop-prod-apiNamespace의 라벨은 7장 Namespace와 라벨에서 다룬 표준입니다. team / env / cost-center 같은 라벨을 일관되게 붙여 두면 25장 모니터링 · 알람의 Prometheus가 워크로드를 그루핑할 때 그대로 활용되고, 28장 비용 최적화의 비용 추적에서도 같은 라벨을 키로 씁니다.
ServiceAccount의 IRSA annotation은 16장 RBAC / ServiceAccount 깊이의 그 annotation입니다. 적힌 IAM Role의 권한을 myshop-api Pod가 이 ServiceAccount로 동작하면서 자동으로 받습니다. Role 자체의 정의는 21장에서 정리한 Terraform 패턴으로 관리합니다.
module "myshop_api_irsa" {
source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
version = "~> 5.0"
role_name = "myshop-prod-api"
oidc_providers = {
main = {
provider_arn = var.oidc_provider_arn
namespace_service_accounts = ["myshop:myshop-api"]
}
}
role_policy_arns = {
secrets = aws_iam_policy.myshop_secrets_read.arn
}
}이 Terraform이 만든 Role의 ARN을 ServiceAccount annotation에 적습니다. 이후 Pod 안의 AWS SDK는 이 Role의 권한으로 Secrets Manager · S3 · CloudWatch를 호출할 수 있습니다. namespace_service_accounts의 trust 제약이 myshop 네임스페이스의 myshop-api ServiceAccount만 이 Role을 가져갈 수 있도록 묶는 보안 경계입니다.
ConfigMap과 Secret #
apiVersion: v1
kind: ConfigMap
metadata:
name: myshop-api
namespace: myshop
data:
LOG_LEVEL: "info"
FEATURE_NEW_CHECKOUT: "true"
CACHE_TTL_SECONDS: "300"
AWS_REGION: "ap-northeast-2"
---
apiVersion: v1
kind: Secret
metadata:
name: myshop-api
namespace: myshop
type: Opaque
stringData:
DATABASE_URL: "postgresql://will-be-replaced-by-external-secrets"Secret의 실제 값은 매니페스트에 두지 않습니다. 23장 DB 연동에서 External Secrets Operator가 AWS Secrets Manager의 비밀을 K8s Secret으로 자동 동기화하는 방식을 다룹니다. 이번 챕터에서는 골격만 잡아 두고, 20장 GitOps의 비밀 관리 모델과 함께 24장 CI / CD 파이프라인에서 다시 엮습니다.
Deployment — Pod replicas #
apiVersion: apps/v1
kind: Deployment
metadata:
name: myshop-api
namespace: myshop
labels:
app.kubernetes.io/name: myshop-api
app.kubernetes.io/component: backend
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: myshop-api
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app.kubernetes.io/name: myshop-api
app.kubernetes.io/component: backend
spec:
serviceAccountName: myshop-api
containers:
- name: api
image: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myshop-api:1.4.2
ports:
- containerPort: 8000
name: http
envFrom:
- configMapRef:
name: myshop-api
- secretRef:
name: myshop-api
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "1"
memory: "512Mi"
readinessProbe:
httpGet:
path: /health/ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /health/live
port: http
initialDelaySeconds: 30
periodSeconds: 10
startupProbe:
httpGet:
path: /health/live
port: http
failureThreshold: 30
periodSeconds: 10
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: myshop-api운영 매니페스트의 표준 요소들이 모여 있습니다. 각각이 본 책의 어느 챕터에서 풀렸는지를 짚으면 다음과 같습니다.
strategy.rollingUpdate— 4장 Deployment / ReplicaSet의 롤링 업데이트.maxUnavailable: 0으로 새 버전이 충분히 떠올라야만 옛 버전을 내리는 무중단의 기본 보호입니다.envFrom— 6장 ConfigMap · Secret의 묶음 주입. ConfigMap과 Secret의 모든 키를 한 번에 환경변수로 풀어 줍니다.- 세 가지 probe — 12장 헬스 체크의 그 셋. readiness는 즉시 (5초 후 시작), liveness는 보수적으로 (30초 후 시작), startup은 초기화가 긴 경우의 유예입니다.
resources— 11장 자원 요청과 한도의 requests / limits. 이번 워크로드는 requests < limits의 Burstable QoS 클래스에 속합니다.topologySpreadConstraints— Pod를 여러 AZ에 분산. AZ 한 곳이 죽어도 다른 AZ의 Pod가 살아남습니다.app.kubernetes.io/표준 라벨 — 7장의 그 표준. selector · 모니터링 · 로깅이 모두 이 라벨로 그루핑됩니다.
Service — 클러스터 내부의 고정 진입점 #
apiVersion: v1
kind: Service
metadata:
name: myshop-api
namespace: myshop
labels:
app.kubernetes.io/name: myshop-api
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: myshop-api
ports:
- name: http
port: 80
targetPort: http
protocol: TCPtype: ClusterIP가 핵심입니다. Service 자체는 클러스터 내부에서만 접근 가능한 가상 IP이고, 외부 노출은 다음에 만들 Ingress가 담당합니다. 5장 Service에서 다룬 그 모델 그대로입니다. 앱의 외부 노출은 Service가 아니라 Ingress에서 결정한다 — 이 분리가 운영 클러스터의 표준 패턴이고, 내부 통신과 외부 노출의 결을 다른 레이어로 묶는 보안 · 운영의 출발점입니다.
AWS Load Balancer Controller — Ingress의 ALB 매핑 #
EKS의 Ingress는 K8s 표준 Ingress 객체이지만, 그 객체를 실제 ALB (AWS Application Load Balancer)로 풀어내려면 AWS Load Balancer Controller라는 컴포넌트가 클러스터에 설치되어 있어야 합니다. 10장 Ingress의 §“클라우드별 Ingress Controller"에서 짚었던 그 컴포넌트가 본격적인 셋업 단계에서 다뤄집니다.
helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
-n kube-system \
--set clusterName=myshop-prod \
--set serviceAccount.create=false \
--set serviceAccount.name=aws-load-balancer-controllerServiceAccount는 미리 IRSA로 만들어 둡니다 (21장의 EBS CSI IRSA 매니페스트와 같은 패턴). 이 controller가 클러스터 안에서 Ingress 객체를 watch 하다가, Ingress가 만들어지면 그에 맞는 ALB를 AWS API로 프로비저닝합니다. Controller는 K8s와 AWS의 두 결을 잇는 브리지라는 위치를 기억해 두는 게 좋습니다 — 같은 패턴이 23장의 External Secrets Operator, 25장의 CloudWatch Container Insights에서도 반복됩니다.
Ingress 매니페스트 #
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myshop-api
namespace: myshop
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
alb.ingress.kubernetes.io/ssl-redirect: '443'
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-northeast-2:123456789012:certificate/abc-123
alb.ingress.kubernetes.io/healthcheck-path: /health/ready
external-dns.alpha.kubernetes.io/hostname: api.myshop.example.com
spec:
rules:
- host: api.myshop.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myshop-api
port:
number: 80annotation 하나씩의 의미를 정리합니다.
scheme: internet-facing— 인터넷 노출 ALB입니다. 내부용은internal로 두면 됩니다.target-type: ip— Pod IP를 ALB 타깃 그룹에 직접 등록합니다. ALB가 Pod IP 변동을 watch 하는 흐름입니다.listen-ports/ssl-redirect— HTTPS만 받고 HTTP는 자동 redirect 합니다.certificate-arn— ACM 인증서. Route 53과 묶인 도메인의 인증서를 ACM으로 발급해 둡니다.healthcheck-path— ALB가 각 Pod에 보내는 헬스 체크 경로. 12장의 readiness probe와 같은 경로로 맞추는 게 일반적입니다.external-dns.alpha.kubernetes.io/hostname— external-dns 컴포넌트가 이 ALB의 DNS를 Route 53의 A 레코드로 자동 등록합니다.
이 매니페스트를 적용한 시점부터 약 1~2분 후, AWS 콘솔에서 새 ALB가 떠 있고 api.myshop.example.com 도메인이 그 ALB로 연결되어 있는 모양이 만들어집니다.
HPA — 트래픽에 따른 Pod 자동 조정 #
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: myshop-api
namespace: myshop
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: myshop-api
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
behavior:
scaleUp:
stabilizationWindowSeconds: 30
scaleDown:
stabilizationWindowSeconds: 300CPU 사용률이 평균 60%를 넘으면 Pod 개수를 늘리고, 떨어지면 줄입니다. 13장 오토스케일링의 그 모델 그대로입니다. behavior의 stabilization 윈도우가 운영의 키 — scale-up은 30초로 빠르게, scale-down은 5분으로 보수적으로 두는 비대칭이 핵심입니다. 트래픽 스파이크에는 빠르게 대응하면서도 일시적 dip에 Pod가 들쑥날쑥 줄어들지 않게 만드는 결입니다.
PodDisruptionBudget — 가용성의 하한선 #
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: myshop-api
namespace: myshop
spec:
minAvailable: 2
selector:
matchLabels:
app.kubernetes.io/name: myshop-api이 한 장이 무엇을 막는가 — 노드 업그레이드 / 노드 드레인 / Cluster Autoscaler의 노드 회수 같은 자발적 disruption 시 myshop-api Pod가 동시에 2개 미만으로 떨어지지 않게 보장합니다. K8s 운영의 단골 사고 — “EKS 업그레이드 중에 Pod가 한 번에 다 내려가서 다운타임 발생"을 막는 도구입니다. 26장 운영 체크리스트의 클러스터 업그레이드 절차에서 PDB가 왜 필수인지가 본격적으로 풀립니다.
PDB의 핵심은 **자발적 (voluntary)**이라는 한정입니다. 노드의 갑작스러운 장애 같은 비자발적 disruption은 PDB로 막을 수 없습니다 — 그 결은 topologySpreadConstraints와 multi-AZ 노드 그룹이 담당합니다.
Helm 차트로 묶기 #
지금까지 적은 9장의 매니페스트는 그 자체로도 동작합니다. 그러나 dev / prod에 같은 워크로드를 다른 값으로 배포하려면 매니페스트를 환경마다 복제해야 하는 부담이 생깁니다. 이 지점에 Helm이 들어옵니다. 18장 CRD와 Operator의 §“Helm 차트로 묶기"에서 짚었던 Helm의 위치가 본격적인 운영 차트로 다뤄지는 단계입니다.
Chart 구조 #
charts/myshop-api/
├── Chart.yaml
├── values.yaml # 기본값
├── values-dev.yaml # dev override
├── values-prod.yaml # prod override
└── templates/
├── _helpers.tpl
├── namespace.yaml
├── serviceaccount.yaml
├── configmap.yaml
├── secret.yaml
├── deployment.yaml
├── service.yaml
├── ingress.yaml
├── hpa.yaml
└── pdb.yamlChart.yaml #
apiVersion: v2
name: myshop-api
description: myshop API service
type: application
version: 0.1.0
appVersion: "1.4.2"
maintainers:
- name: platform-team
email: platform@myshop.example.comversion이 차트 자체의 버전, appVersion이 그 차트가 배포하는 애플리케이션의 버전입니다. 둘이 분리되는 이유 — 차트 매니페스트는 그대로인데 이미지 태그만 올리는 흐름이 잦기 때문입니다.
values.yaml — 기본값 #
image:
repository: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myshop-api
tag: "1.4.2"
pullPolicy: IfNotPresent
replicaCount: 3
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 1
memory: 512Mi
serviceAccount:
create: true
name: myshop-api
irsaRoleArn: ""
config:
LOG_LEVEL: info
FEATURE_NEW_CHECKOUT: "true"
CACHE_TTL_SECONDS: "300"
ingress:
enabled: true
host: api.myshop.example.com
certificateArn: ""
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 20
targetCPUUtilizationPercentage: 60
pdb:
enabled: true
minAvailable: 2values-prod.yaml — 환경별 override #
replicaCount: 5
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 2
memory: 1Gi
serviceAccount:
irsaRoleArn: arn:aws:iam::123456789012:role/myshop-prod-api
config:
LOG_LEVEL: warn
ingress:
host: api.myshop.example.com
certificateArn: arn:aws:acm:ap-northeast-2:123456789012:certificate/abc-123
autoscaling:
minReplicas: 5
maxReplicas: 50Template — Deployment 한 장 예시 #
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myshop-api.fullname" . }}
labels:
{{- include "myshop-api.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "myshop-api.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "myshop-api.selectorLabels" . | nindent 8 }}
spec:
serviceAccountName: {{ .Values.serviceAccount.name }}
containers:
- name: api
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
envFrom:
- configMapRef:
name: {{ include "myshop-api.fullname" . }}
- secretRef:
name: {{ include "myshop-api.fullname" . }}
resources:
{{- toYaml .Values.resources | nindent 12 }}{{ include "myshop-api.fullname" . }} 같은 Helper는 _helpers.tpl에 정의해 둡니다. Helm 차트 표준 패턴이고, helm create로 골격을 만들면 자동으로 들어옵니다.
배포 — 같은 차트, 다른 환경 #
helm upgrade --install myshop-api charts/myshop-api \
-n myshop --create-namespace \
-f charts/myshop-api/values.yaml \
-f charts/myshop-api/values-dev.yamlhelm upgrade --install myshop-api charts/myshop-api \
-n myshop --create-namespace \
-f charts/myshop-api/values.yaml \
-f charts/myshop-api/values-prod.yaml같은 차트가 환경별로 다르게 적용됩니다. 이 한 명령은 24장 CI / CD 파이프라인의 ArgoCD Application sync로 대체되지만, 개발 단계의 수동 배포에는 그대로 유효합니다. 20장 GitOps의 git 단일 소스 모델이 이 Helm 차트의 디렉터리로 자연스럽게 이어집니다.
cert-manager와 external-dns — 부가 컴포넌트 #
ACM 인증서를 직접 발급 · 갱신하는 대신 클러스터 안에서 Let’s Encrypt 인증서를 자동으로 받고 싶다면 cert-manager, Route 53 DNS 레코드를 매니페스트로 자동 관리하려면 external-dns를 같이 도입합니다.
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager \
-n cert-manager --create-namespace \
--set installCRDs=truehelm install external-dns external-dns/external-dns \
-n external-dns --create-namespace \
--set provider=aws \
--set aws.region=ap-northeast-2 \
--set "domainFilters[0]=myshop.example.com"운영 클러스터의 표준 셋업입니다. 도메인이 추가될 때마다 Route 53 콘솔을 열지 않고 매니페스트의 annotation 한 줄로 끝나는 흐름이 만들어집니다. ACM 인증서를 쓸 때는 cert-manager가 필요하지 않지만, 멀티 클러스터 · 멀티 클라우드 환경에서는 cert-manager가 더 일관됩니다. 두 컴포넌트의 ServiceAccount도 21장의 EBS CSI IRSA와 같은 패턴으로 IAM Role을 받아야 Route 53의 레코드를 변경할 수 있습니다.
첫 배포 후 점검 #
배포 직후 점검할 항목들입니다.
kubectl get all -n myshop
kubectl get hpa -n myshop
kubectl get pdb -n myshop
kubectl get ingress -n myshopkubectl describe ingress myshop-api -n myshop | grep "Address"curl -i https://api.myshop.example.com/health/ready이 네 단계로 Pod · HPA · Ingress · 외부 진입까지가 동작하는 상태인지 확인됩니다. 200 응답이 떨어지면 클러스터의 첫 워크로드가 운영에 들어선 시점입니다. 만약 ALB의 Address가 비어 있거나 503이 떨어진다면 27장 kubectl 디버깅 패턴의 Ingress 디버깅 절을 미리 봐 두는 게 도움이 됩니다.
연습문제 #
- 본 챕터의 9개 매니페스트를 한 디렉터리에 풀어 두고, 21장에서 띄운 dev EKS 클러스터에 차례로 적용합니다.
kubectl get all -n myshop의 출력에서 Deployment가 3 / 3, ReplicaSet이 동일, Pod가 모두 Running, Service의 ClusterIP가 할당된 것까지 확인합니다. Ingress의 ALB Address가 떠오를 때까지 약 1~2분이 걸리는데, 이 시간 동안 AWS 콘솔에서 ALB · TargetGroup · Route 53의 A 레코드가 차례로 만들어지는 모양을 함께 관찰합니다. - 본 챕터의 9개 매니페스트를 Helm 차트 한 묶음으로 변환합니다.
helm create myshop-api로 골격을 만든 뒤,values.yaml·values-dev.yaml·values-prod.yaml의 세 파일이 어떻게 갈라지는지를 정리하고, dev의 replicaCount와 prod의 replicaCount가 다르게 적용되는지helm template로 사전 검증합니다. 이 사전 검증이 24장 CI / CD의 ArgoCD diff 모델과 어떻게 연결되는지 한 단락으로 메모합니다. topologySpreadConstraints의whenUnsatisfiable을ScheduleAnyway와DoNotSchedule두 가지로 바꿔 가며 Pod의 분포가 어떻게 달라지는지 관찰합니다. AZ가 두 개뿐인데 replicaCount가 3 인 경우, 어느 옵션이 어떤 트레이드오프를 만드는지 본인의 운영 시나리오에 비춰 한 단락으로 비교합니다. 21장의 VPC 모듈에서 단일 AZ vs 멀티 AZ의 결정이 이 옵션의 결과를 어떻게 흔드는지도 같이 짚습니다.
한 줄 요약: 운영 워크로드 한 개의 표준은 Namespace · ServiceAccount · ConfigMap · Secret · Deployment · Service · Ingress · HPA · PodDisruptionBudget의 9개 객체이고, 이 묶음을 Helm 차트로 추상화하면 같은 차트가 dev / prod에 다른 values로 풀려나온다. EKS의 Ingress는 AWS Load Balancer Controller가 ALB로 실제 풀어내고, ACM 인증서 + Route 53 + external-dns가 도메인 · TLS · DNS의 세 결을 매니페스트의 annotation 한 줄로 묶는다.
다음 챕터 #
이 시점에서 myshop-api는 외부에서 HTTPS로 접근 가능한 상태이지만, 데이터 저장소가 없어 /health/ready만 200을 돌려주는 빈 껍데기입니다. 다음 챕터에서는 그 빈 부분을 채웁니다.
23장 DB 연동에서는 RDS PostgreSQL과의 연결, Secrets Manager로 DB 비밀번호를 안전하게 주입하는 길, External Secrets Operator로 K8s Secret과 클라우드 비밀 저장소를 동기화하는 흐름, 그리고 커넥션 풀의 운영 원칙까지를 한 사이클로 다룹니다.