AWS Cloud School 13기 88일차

Forever 김·2026년 5월 6일

AWS Cloud School

목록 보기
78/97

87일에는 휴가 였기 때문에 88일차로 바로 작성한다.

TIL

오늘은 드디어 CI/CD의 CD 파트를 제대로 다뤘다. ArgoCD를 EKS 환경에 설치하고, GitHub 레포지토리와 연동해서 매니페스트가 바뀌면 자동으로 배포되는 흐름을 직접 구성해봤다. 수동으로 kubectl apply를 치던 것과 비교하면 확실히 편하고, 롤백도 UI에서 클릭 몇 번으로 끝난다는 게 인상적이었다. 마지막엔 Svelte + FastAPI 앱을 컨테이너화해서 EKS에 배포하는 실습까지 이어졌다.


CI/CD란?

본격적인 내용 전에 개념부터 짚고 넘어가자.

용어의미
CI (Continuous Integration)코드 변경 시 자동으로 빌드 & 테스트
CD (Continuous Delivery/Deployment)빌드된 결과물을 자동으로 배포

여기서 Continuous의 의미를 정확히 짚고 가자.

  • Continuous = 자동(트리거) + 여러 번 반복
  • Deployment = 배포 (kubectl apply -f 매니페스트)
  • Delivery = 매니페스트의 이동, 이미지의 이동

CD가 Delivery와 Deployment 두 가지 의미를 가지는 이유가 여기 있다. 이미지를 레지스트리에 전달하는 것(Delivery)과 실제로 클러스터에 배포하는 것(Deployment) 모두 CD의 범주다.

오늘 다룬 ArgoCD는 CD 파트다. CI는 보통 GitHub Actions, Jenkins 같은 툴이 담당하고, ArgoCD는 그 결과물(컨테이너 이미지)을 k8s 환경에 자동으로 배포하는 역할을 한다.

일반적인 배포 과정을 생각해보면:

  1. 소스코드로 앱을 빌드
  2. 앱을 넣을 컨테이너 이미지를 빌드
  3. 컨테이너 이미지를 컨테이너 레지스트리에 push
  4. k8s나 도커 환경에서 컨테이너 이미지를 pull하여 배포 (kubectl apply -f <매니페스트> or docker run...)

ArgoCD는 4번 단계를 자동화한다. GitHub 레포지토리에 있는 매니페스트 파일을 모니터링하다가, 변경이 감지되면 자동으로 kubectl apply를 실행해준다.


ArgoCD — Kubernetes 환경의 CD 툴

ArgoCD는 Kubernetes 환경에서 CD(Continuous Deployment)를 담당하는 툴이다. CNCF(Cloud Native Computing Foundation)에서 강력하게 추천하는 도구이기도 하다.

GitOps 방식

ArgoCD는 GitOps 패턴을 따른다. GitOps란 Git 레포지토리를 "단일 진실의 원천(Single Source of Truth)"으로 삼는 방식이다.

  • 배포 상태를 코드(매니페스트)로 관리
  • Git에 push하는 것 자체가 배포 트리거
  • 실제 클러스터 상태와 Git의 상태가 다르면 ArgoCD가 자동으로 동기화

이 방식의 장점은 배포 이력이 곧 Git 커밋 이력이라는 점이다. 언제 누가 무엇을 배포했는지 Git log만 봐도 알 수 있다.


실습 1 — ArgoCD 설치 및 자동 배포 구성

1. EKS 클러스터 준비

퍼블릭 서브넷에 EKS 클러스터를 먼저 생성한다.

eksctl create cluster \
  --vpc-public-subnets <퍼블릭서브넷1>,<퍼블릭서브넷2> \
  --name myc \
  --region ap-northeast-2 \
  --version 1.35 \
  --nodegroup-name mycng \
  --node-type t3.small \
  --nodes 2 --nodes-min 2 --nodes-max 5

2. ArgoCD 설치

# argocd 네임스페이스 생성
kubectl create ns argocd

# ArgoCD 공식 매니페스트로 설치
kubectl apply -n argocd -f \
  https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# 설치 확인
kubectl get pod -n argocd
kubectl get svc -n argocd

설치 직후에는 argocd-server 서비스가 ClusterIP 타입이라 외부에서 접근이 안 된다. LoadBalancer로 바꿔줘야 한다.

# 서비스 타입을 LoadBalancer로 변경
kubectl patch svc argocd-server -n argocd \
  -p '{"spec": {"type": "LoadBalancer"}}'

이렇게 하면 클래식 로드밸런서가 자동으로 생성된다. DNS 이름으로 80번 포트에 접속하면 ArgoCD 웹 UI가 뜬다.

# 초기 admin 비밀번호 확인 (base64 디코딩)
kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath="{.data.password}" | base64 -d; echo

초기 계정은 admin, 비밀번호는 위 명령어로 확인한 값이다. 로그인 후 반드시 비밀번호를 변경하자.

💡 Helm으로도 설치 가능하다. 공식 방법과 Helm 방법 모두 결과는 동일하다.


3. Service 먼저 생성

실습 디렉토리를 만들고 Service를 먼저 배포한다. NLB 어노테이션을 붙여서 NLB 타입으로 생성한다.

mkdir /app && cd /app
# app-svc.yml
apiVersion: v1
kind: Service
metadata:
  name: svc-ip
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
    service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
spec:
  type: LoadBalancer
  selector:
    app: nginx
  ports:
  - port: 80
    targetPort: 80
kubectl apply -f app-svc.yml

4. 앱 이미지 빌드 & ECR push

httpd:alpine 이미지를 베이스로 커스텀 이미지를 만들어 ECR에 push한다.

echo apptest > index.html
# Dockerfile
FROM public.ecr.aws/docker/library/httpd:alpine
COPY index.html /usr/local/apache2/htdocs/index.html
# ECR 환경변수 설정 (본인 계정 ID로)
export ECR=<본인계정ID>.dkr.ecr.ap-northeast-2.amazonaws.com

# ECR 로그인
aws ecr get-login-password --region ap-northeast-2 | \
  docker login --username AWS --password-stdin $ECR

# 빌드 & push
docker build -t $ECR/app:1 .
docker push $ECR/app:1

# 로컬에서 동작 확인
docker run -dp 5959:80 --name myapp $ECR/app:1

5. Deployment 작성 & 수동 배포

# app-dep.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-dep
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      name: nginx-pod
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx-con
        image: <본인 ECR 레지스트리 주소>/app:1
kubectl apply -f app-dep.yml

# 로드밸런서 DNS로 접속 확인
kubectl get svc
curl <로드밸런서 DNS>
# apptest 출력 확인

여기까지가 수동으로 Deployment를 배포한 것이다.


6. 수동 업데이트 (ArgoCD 없이)

ArgoCD를 붙이기 전에 수동 업데이트 흐름을 먼저 이해해보자.

# 앱 내용 변경
echo app-updated >> index.html

# 새 버전으로 이미지 빌드 & push (태그를 2로 변경)
docker build -t $ECR/app:2 .
docker push $ECR/app:2

# app-dep.yml의 image를 app:2로 수정 후 재배포
kubectl apply -f app-dep.yml

# 업데이트 확인
curl <로드밸런서 DNS>
# apptest / app-updated 출력 확인

이게 수동 방식이다. 매번 이미지 빌드 → push → 매니페스트 수정 → kubectl apply를 반복해야 한다. 서비스가 많아지면 관리가 힘들어진다.


7. ArgoCD 자동 배포 설정

수동으로 배포했던 Deployment를 삭제하고 ArgoCD로 전환한다.

kubectl delete -f app-dep.yml

① GitHub에 매니페스트 레포 생성

web-app이라는 이름으로 GitHub 레포를 만들고, app-dep.yml을 push한다.

mkdir app-web && cd app-web
cp ../app-dep.yml .

git init
git add .
git remote add origin https://github.com/<본인계정>/web-app.git
git commit -m "first"
git branch -M main
git push origin main

GitHub push 시 Personal Access Token(PAT)이 필요하다. GitHub → Settings → Developer settings → Personal access tokens에서 발급받는다. repo 권한 항목을 전부 체크하고 토큰을 메모장에 저장해두자. push 시 비밀번호 입력란에 토큰을 두 번 붙여넣으면 된다(아이디/패스 모두 토큰).

② ArgoCD에서 Application 생성

ArgoCD 웹 UI에서 + NEW APP 버튼을 클릭하고 아래 항목을 설정한다.

항목
Application Nameweb-app
Projectdefault
Repository URLGitHub 레포 URL
RevisionHEAD
Path. (루트 경로)
Cluster URLhttps://kubernetes.default.svc
Namespacedefault

설정 후 CREATE를 누르면 ArgoCD가 GitHub 레포를 바라보기 시작한다. 잠시 후 Deployment → ReplicaSet → Pod가 자동으로 생성되는 걸 UI에서 확인할 수 있다.

# 배포 확인
curl <로드밸런서 DNS>
# apptest / app-updated 출력 확인

8. 앱 업데이트 → ArgoCD 자동 배포 확인

이제 앱을 업데이트하면 ArgoCD가 자동으로 배포하는지 확인해보자.

cd /app

# 앱 내용 변경
echo appupdate-v3 >> index.html

# 새 이미지 빌드 & ECR push
docker build -t $ECR/app:3 .
docker push $ECR/app:3

# 매니페스트의 이미지 태그를 app:3으로 변경
# 경로에 '/'가 포함되어 있으므로 sed 구분자를 '/'가 아닌 '@'로 변경
cd /app/app-web
sed -i "s@image: .*/app:.*@image: $ECR/app:3@g" app-dep.yml

# GitHub에 push → ArgoCD가 자동으로 감지하고 배포
git add .
git commit -m "second"
git push origin main

GitHub에 push하는 순간 ArgoCD가 변경을 감지하고 자동으로 kubectl apply를 실행한다. 깃허브를 수정하자마자 ArgoCD가 kubectl apply -f app-dep.yml을 한다고 생각하면 이해하기 쉽다.

# 업데이트 확인
curl <로드밸런서 DNS>
# apptest / app-updated / appupdate-v3 출력 확인

9. 롤백

ArgoCD의 또 다른 강점은 롤백이 쉽다는 점이다.

ArgoCD UI → 앱 선택 → HISTORY AND ROLLBACK 탭을 보면 이전 배포 이력이 남아있다. 원하는 버전을 선택해서 ROLLBACK 버튼을 누르면 끝이다.

# app:2로 롤백 후 확인
curl <로드밸런서 DNS>
# apptest / app-updated 출력 (app:2 버전으로 롤백됨)

kubectl rollout undo를 직접 치지 않아도 되고, 어떤 버전으로 롤백했는지 UI에서 한눈에 파악할 수 있다.


실습 2 — nginx 이미지로 ArgoCD 자동 배포 실습

ECR 퍼블릭 레포의 nginx:alpine 이미지로 'CD is difficult' 내용의 index.html을 배포하고, ArgoCD를 통해 'CD is easy'로 자동 업데이트되도록 구성하는 실습이다.

cp -r /app /app2
cd /app2

# 소스코드 수정
echo 'CD is difficult' > index.html
# Dockerfile
FROM public.ecr.aws/nginx/nginx:alpine
COPY index.html /usr/share/nginx/html/index.html
# 빌드 & push
docker build -t $ECR/app2:1 .
docker push $ECR/app2:1

# 로컬 테스트
docker run -dp 5858:80 --name app2 $ECR/app2:1
# app-dep.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web2-dep
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx2
  template:
    metadata:
      name: nginx-pod
      labels:
        app: nginx2
    spec:
      containers:
      - name: nginx-con
        image: <본인계정ID>.dkr.ecr.ap-northeast-2.amazonaws.com/app2:1
# app-svc.yml (클래식 LB 타입)
apiVersion: v1
kind: Service
metadata:
  name: svc-app2
spec:
  type: LoadBalancer
  selector:
    app: nginx2
  ports:
  - port: 80
    targetPort: 80
kubectl apply -f app-svc.yml

GitHub에 app2라는 레포를 만들고 매니페스트를 push한 뒤, ArgoCD에서 해당 레포를 바라보도록 Application을 생성한다. 이후 앱을 업데이트하면 자동 배포가 된다.

# 앱 업데이트
echo 'CD is easy' > index.html
docker build -t $ECR/app2:2 .
docker push $ECR/app2:2

# 매니페스트 수정 후 GitHub push → ArgoCD 자동 배포

실습 3 — Svelte + FastAPI 앱 EKS에 ArgoCD로 배포

GitHub에서 포크한 sfapp 레포를 컨테이너화해서 EKS 환경에 ArgoCD로 배포하는 실습이다. 프론트엔드는 Svelte, 백엔드는 FastAPI로 구성된 2-tier 앱이다.

목표 아키텍처:

사용자
  → NLB
    → Svelte Pod (Nginx + 정적 파일)
      → /api 요청 → FastAPI Pod (uvicorn, svc-fast:8000)

각 Deployment는 ArgoCD로 자동 배포된다.

git clone <포크한 레포 주소>
cd sfapp
# fastapi / svelte 두 디렉토리로 구성되어 있음

백엔드 — FastAPI Dockerfile

FROM public.ecr.aws/docker/library/python:3.12.4-alpine
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
# 전체 대역에서 접근 가능하도록 --host 0.0.0.0 필수
CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0"]
docker build -t $ECR/fast:1 .
docker run -dp 8000:8000 --name fast $ECR/fast:1
# http://<IP>:8000/docs 로 Swagger UI 접속해서 API 동작 확인
docker rm -f fast

프론트엔드 — Svelte Dockerfile (멀티 스테이지 빌드)

# Stage 1: Node.js 환경에서 빌드
FROM public.ecr.aws/docker/library/node:24.11.1-alpine as builder
WORKDIR /app
COPY package*.json .
RUN npm install
COPY . .
RUN npm run build
# npm run build 결과물이 ./dist 경로에 생성됨

# Stage 2: Nginx로 정적 파일 서빙
FROM public.ecr.aws/nginx/nginx:alpine
COPY ./default.conf /etc/nginx/conf.d/default.conf
# nginx 리버스 프록시 설정 파일 복사
COPY --from=builder /app/dist /usr/share/nginx/html
# stage1에서 빌드된 정적 파일을 nginx 웹루트에 복사

멀티 스테이지 빌드를 쓰는 이유가 있다. Node.js 환경에서 빌드한 결과물(정적 파일)만 Nginx에 복사하기 때문에, 최종 이미지에 Node.js 런타임이 포함되지 않는다. 이미지 크기가 훨씬 작아지고 보안 취약점도 줄어든다.

로컬 테스트 시에는 --link 옵션으로 컨테이너 간 연결한다.

docker run -dp 8000:8000 --name fast $ECR/fast:1
docker run -dp 8989:80 --link fast --name svelte $ECR/svelte:1

EKS 배포 — FastAPI

핵심 포인트: FastAPI의 Service 이름을 svc-fast로 지정해야 한다. Svelte의 Nginx default.conf에서 /api 경로를 http://svc-fast:8000으로 리버스 프록시하기 때문이다. k8s 내부 DNS가 svc 이름을 IP로 해석해준다.

# fastapi 매니페스트 (svc + deployment)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dep-fast
spec:
  replicas: 1
  selector:
    matchLabels:
      app: fast
  template:
    metadata:
      labels:
        app: fast
    spec:
      containers:
      - name: con-fast
        image: <ECR계정ID>.dkr.ecr.ap-northeast-2.amazonaws.com/fast:1
---
apiVersion: v1
kind: Service
metadata:
  name: svc-fast   # 이 이름이 프론트의 리버스 프록시 주소가 됨
spec:
  selector:
    app: fast
  ports:
    - port: 8000
      targetPort: 8000
  type: ClusterIP   # ArgoCD 배포용 레포에서는 ClusterIP로 변경

수동 테스트 후 ArgoCD 배포 레포에 올릴 때는 type: LoadBalancerClusterIP로 변경한다.

# ECR 로그인 & 이미지 push
aws ecr get-login-password --region ap-northeast-2 | \
  docker login --username AWS --password-stdin $ECR
docker push $ECR/fast:1

# 수동 배포로 동작 확인 (Swagger: <LB DNS>:8000/docs)
kubectl apply -f local-fastapi.yml
kubectl get svc  # svc-fast의 EXTERNAL-IP 확인

# 동작 확인 후 수동 배포 삭제
kubectl delete -f local-fastapi.yml

EKS 배포 — Svelte

FastAPI의 svc 이름(svc-fast)에 맞게 default.conf의 리버스 프록시 설정을 수정한 뒤 이미지를 빌드한다.

# default.conf 수정 부분
location /api/ {
    proxy_pass http://svc-fast:8000;  # FastAPI svc 이름으로 변경
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}
docker build -t $ECR/svel:1 .
docker push $ECR/svel:1
# svelte 매니페스트 (NLB로 외부 노출)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dep-svel
spec:
  replicas: 1
  selector:
    matchLabels:
      app: svel
  template:
    metadata:
      labels:
        app: svel
    spec:
      containers:
      - name: con-svel
        image: <ECR계정ID>.dkr.ecr.ap-northeast-2.amazonaws.com/svel:1
---
apiVersion: v1
kind: Service
metadata:
  name: svel
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
    service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
spec:
  selector:
    app: svel
  ports:
  - port: 80
    targetPort: 80
  type: LoadBalancer

ArgoCD로 자동 배포 연결

GitHub 배포 레포에 fastapi/svelte 매니페스트를 각각 디렉토리로 나눠서 push하고, ArgoCD에서 각 경로를 바라보는 Application을 생성한다.

배포 레포 구조 예시:
sfapp-deploy/
├── fastapi/
│   └── fastapi.yml   (Deployment + ClusterIP Service)
└── svelte/
    └── svelte.yml    (Deployment + NLB Service)

ArgoCD에서 Application을 2개 생성한다.

  • app-fast: Repository URL → 배포 레포, Path → fastapi/
  • app-svel: Repository URL → 배포 레포, Path → svelte/

NLB의 DNS 주소로 접속하면 Svelte 앱이 뜨고, FastAPI와 연동되어 데이터가 정상 출력된다.

# 실습 완료 후 클러스터 삭제 (로드밸런서도 함께 삭제되는지 반드시 확인)
eksctl delete cluster --name myc

ArgoCD vs 수동 배포 비교

항목수동 배포ArgoCD
배포 방법kubectl apply -f 직접 실행GitHub push → 자동 배포
배포 이력없음 (기억에 의존)Git 커밋 이력 = 배포 이력
롤백kubectl rollout undoUI에서 클릭
상태 확인kubectl get 명령어웹 UI에서 시각적으로 확인
다중 서비스 관리서비스마다 수동 반복레포 하나로 전체 관리

정리

오늘 핵심은 "GitHub에 push하는 것 자체가 배포" 라는 GitOps 개념이다. ArgoCD를 쓰고 나니 수동으로 kubectl apply를 치던 게 얼마나 번거로운 일이었는지 체감됐다. 특히 여러 서비스를 동시에 관리할 때 GitHub 레포 하나만 보면 전체 배포 상태를 파악할 수 있다는 게 큰 장점이다.

다음 단계는 CI 파트(GitHub Actions로 이미지 빌드 자동화)까지 연결하는 것이다. 코드를 push하면 GitHub Actions가 이미지를 빌드해서 ECR에 push하고, 매니페스트를 자동으로 수정해서 다시 push하면 ArgoCD가 배포하는 완전 자동화 파이프라인을 구성할 수 있다.

이번 velog는 AWS Kiro를 통해 작성하였다.

profile
나를 한줄로

0개의 댓글