앞으로 4주간 온라인 모각코를 통해 스터디한 내용을 업로드할 예정입니다.
모각코란 모여서 각자 코딩의 준말로, CloudNet@ 가시다님과 함께 책 하나를 돌파하는 스터디입니다.
이번 모각코는 책, AI 시대에 개발자가 알아야 할 인프라 구성 배포 with 클로드 코드 를 기반으로 진행합니다.
배포 파이프라인 구성 이유
이번 장에서는 코드를 푸시하면 자동으로 배포되는 Gitops 파이프라인을 구성합니다.
배포 파이프라인을 구성하는 이유는 변경 관리와 히스토리 추적이 가능하기 때문입니다.
쿠버네티스의 모든 객체는 YAML로 선언형으로 관리됩니다.
YAML를 관리하기 위해 GitOp를 사용하는데 단일 진실 공급원(Single Source of Truth)로 활용되고, ArgoCD를 통해 클러스터를 유지시킬 수 있는데 활용할 수 있습니다.
여담 (비용)
지난 주 GKE 구성 이후 1주일 간 유지 비용입니다.
초기 제공하는 크레딧 내로 넉넉한 모습입니다.

구성

- GitOps 연결 (ArgoCD)
- Rolling Update로 배포 자동화 체험
- CI 자동화 (GitHub Actions)
- CI ↔ ArgoCD 연결 (push하면 자동 배포)
→ 배포 파이프라인 구성은 클로드 코드를 활용합니다. 아래 저자님의 지침에 따라 구성할 예정입니다.

ArgoCD 설치하기
ArgoCD를 설치하고, 이전 장에서 배포한 Application를 ArgoCD에 등록합니다.
ArgoCD로 설치해주고, 어떤 지침에 따라서 이러한 명령어를 쳤는 지 알려줘

(참고) ArgoCD 지침 가이드
# 3.2 ArgoCD 설치 및 GitOps 연결
## 실행 지침
### 단계 1: ArgoCD 설치
```bash
kubectl --context gke-sysnet4admin_book_gitaiops create namespace argocd
kubectl --context gke-sysnet4admin_book_gitaiops apply -n argocd \
-f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml \
--server-side=true --force-conflicts=true
```
> ⚠️ **CRD 설치 실패**: ArgoCD CRD의 `metadata.annotations`가 너무 길어 일반 `kubectl apply`가 실패할 수 있다. 반드시 `--server-side=true --force-conflicts=true` 옵션을 사용한다.
> 💡 **kubectl `--context` 사용 권장**: 여러 클러스터를 다룰 때 의도하지 않은 클러스터에 명령이 적용되는 사고를 막기 위해 모든 kubectl 명령에 `--context gke-sysnet4admin_book_gitaiops`를 명시한다. 책 본문 시연도 같은 컨텍스트로 진행된다.
ArgoCD가 Running 상태가 될 때까지 대기:
```bash
kubectl --context gke-sysnet4admin_book_gitaiops wait --for=condition=Available \
deployment/argocd-server -n argocd --timeout=120s
```
### 단계 2: ArgoCD UI 접속
```bash
# 초기 admin 비밀번호 확인
kubectl --context gke-sysnet4admin_book_gitaiops -n argocd get secret argocd-initial-admin-secret \
-o jsonpath='{.data.password}' | base64 -d
# 포트포워딩
kubectl --context gke-sysnet4admin_book_gitaiops port-forward svc/argocd-server -n argocd 8443:443
```
브라우저에서 `https://localhost:8443` 접속 (ID: admin).
### 단계 3: ArgoCD Application 생성
`argocd/notiflex-smb.yaml` 파일을 생성한다:
- **소스**: notiflex-platform 저장소의 `k8s/smb` 디렉터리
- **대상**: notiflex 네임스페이스
- **sync policy**: automated (prune: true, selfHeal: true)
```bash
kubectl --context gke-sysnet4admin_book_gitaiops apply -f argocd/notiflex-smb.yaml
```
### 단계 4: 전체 컴포넌트 재시작
설치 직후 repo secret 캐시 문제를 선제 방지하기 위해 전체 재시작한다:
```bash
kubectl --context gke-sysnet4admin_book_gitaiops rollout restart deployment -n argocd
kubectl --context gke-sysnet4admin_book_gitaiops rollout restart statefulset -n argocd
kubectl --context gke-sysnet4admin_book_gitaiops rollout status deployment/argocd-server -n argocd --timeout=120s
```
### 단계 5: private 저장소 인증 (필요시)
저장소가 private이면 ArgoCD에 GitHub 토큰을 등록한다:
```bash
argocd repo add https://github.com//notiflex-platform.git \
--username --password
```
또는 Secret으로 등록:
```yaml
apiVersion: v1
kind: Secret
metadata:
name: repo-notiflex-platform
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repository
stringData:
url: https://github.com//notiflex-platform.git
username:
password:
type: git
forceHttpBasicAuth: "true"
```
> ⚠️ **ArgoCD v3에서 `forceHttpBasicAuth` 필수**: ArgoCD v3.x(stable manifest 기준 v3.3.6+)에서는 HTTP basic auth가 기본 비활성화되었다. `forceHttpBasicAuth: "true"`가 없으면 private repo에 대해 "Repository not found" 에러가 발생하며 Sync Unknown 상태가 지속된다.
> ⚠️ **repo secret YAML을 git에 커밋하지 말 것**: 이 Secret을 YAML 파일로 저장하여 git에 push하면, 7장에서 App of Apps(root-app)가 `argocd/` 디렉터리를 sync할 때 placeholder 값으로 덮어쓰기된다. 반드시 `kubectl create secret` 또는 `kubectl apply`로 클러스터에 직접 적용하고, git에는 포함하지 않는다.
## 트러블슈팅
### ArgoCD v3 NetworkPolicy가 repo-server egress 차단
→ ArgoCD v3 stable manifest에는 NetworkPolicy가 포함되어 있다. 이 정책이 repo-server의 외부(GitHub) 접근을 차단하여, Application이 Sync 상태이지만 실제 매니페스트가 업데이트되지 않는 현상이 발생한다. 증상: `sync.revision`은 최신 커밋이지만 rollout 내용이 이전 버전 그대로.
```bash
# NetworkPolicy 삭제 후 전체 재시작
kubectl --context gke-sysnet4admin_book_gitaiops delete networkpolicy -n argocd --all
kubectl --context gke-sysnet4admin_book_gitaiops rollout restart deployment -n argocd
kubectl --context gke-sysnet4admin_book_gitaiops rollout restart statefulset -n argocd
kubectl --context gke-sysnet4admin_book_gitaiops rollout status deployment/argocd-server -n argocd --timeout=120s
```
### Application referencing project `default` which does not exist
→ ArgoCD 전체 `rollout restart` 직후 기본 AppProject(`default`)가 유실되어 Application이 `ComparisonError`로 전환되는 경우가 있다. 증상: `kubectl get app -n argocd`가 `SyncStatus=Unknown`이고 `status.conditions`에 `Application referencing project default which does not exist` 메시지.
→ 기본 AppProject를 재생성한다:
```bash
kubectl --context gke-sysnet4admin_book_gitaiops apply -f - <<'EOF'
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: default
namespace: argocd
spec:
description: Default project
sourceRepos: ['*']
destinations:
- namespace: '*'
server: '*'
clusterResourceWhitelist:
- group: '*'
kind: '*'
EOF
```
적용 후 1분 내에 Sync 상태가 정상화된다.
### repo Secret 등록 후에도 Sync Unknown 유지
→ ArgoCD가 repo secret을 캐시하고 있어 즉시 반영되지 않는다. repo secret을 등록한 뒤 **repo-server를 포함한 전체 재시작**이 필요하다:
```bash
kubectl --context gke-sysnet4admin_book_gitaiops rollout restart deployment -n argocd
kubectl --context gke-sysnet4admin_book_gitaiops rollout restart statefulset -n argocd
```
개별 Pod 삭제(argocd-server, application-controller만)로는 부족한 경우가 있다. 전체 rollout restart가 확실하다.
재시작 후 30~60초 내에 Synced 상태로 전환된다.
## 💬 질문해보기
> "ArgoCD 말고 다른 GitOps 도구는 뭐가 있어? Flux와 비교하면 어떤 차이가 있어?"
server-side=true ?
❯ server-side=true? client-side=ture? 차이점이 뭐야? 어디서 쓴거지
--server-side(서버사이드 적용)와 기본값인 클라이언트사이드 적용의 차이입니다. 참고로 --client-side=true라는 플래그는 따로 없고, --server-side를 안 붙이면 그게 곧 client-side입니다.
클라이언트사이드 apply (기본값)
kubectl apply -f 그냥 실행하면:
- kubectl이 로컬에서 "이전에 적용한 설정 vs 지금 적용하려는 설정 vs 클러스터의 현재 상태"를 3-way merge로 비교
- 이전에 적용한 설정 전체를 오브젝트의 kubectl.kubernetes.io/last-applied-configuration 어노테이션에 JSON 통째로 저장
- 문제: 이 어노테이션에 크기 제한(약 256KB)이 있는데, ArgoCD의 CRD(Application, AppProject 등)는 OpenAPI 스키마가 통째로 박혀 있어서 매니페스트 자체가 매우 큼 → 어노테이션이 한계를 넘으면 metadata.annotations: Too long 에러로 apply 자체가 실패
서버사이드 apply (--server-side=true)
- 병합 로직이 API 서버 쪽에서 수행됨 (필드 단위 소유권 추적, managedFields)
- last-applied-configuration 같은 거대한 어노테이션을 아예 안 씀 → ArgoCD CRD 크기 문제가 원천적으로 해결됨
- 대신 "이 필드는 누가 소유하고 있다"를 추적하다가, 다른 필드 매니저가 이미 소유한 필드를 덮어쓰려 하면 충돌 에러를 냄 → 이때 --force-conflicts로 "그래도 내가 가져간다"고 강제해야 함
어디서 썼나
prompt-guardrails/ch3/3.2-argocd.md 단계1, ArgoCD 설치 명령에서 그대로 가져왔습니다:
kubectl apply -n argocd \
-f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml \
--server-side=true --force-conflicts=true
가이드에 명시된 이유:
▎ ArgoCD CRD의 metadata.annotations가 너무 길어 일반 kubectl apply가 실패할 수 있다.
즉 일반 kubectl apply -f로 이 파일을 그대로 적용했다면 정확히 위에서 설명한 어노테이션 크기 초과 에러가 났을 거고, --server-side=true --force-conflicts=true가 그 문제를 우회하는 표준 해법입니다.
ArgoCD 내 Application 싱크 확인
ARgoCD 포트포워딩한 UI에 들어가고 싶어 admin계정 비밀번호 어떻게 알지?

kubectl --context gke-sysnet4admin_book_gitaiops -n argocd get secret argocd-initial-admin-secret \
-o jsonpath='{.data.password}' | base64 -d


롤링 업데이트
Notiflex에 새 엔드포인트 또는 기능을 추가하고 버전을 v0.1.1로 올린다.
롤링 업데이트 진행해줘 시나리오가 뭐고 어떻게 진행하는지, 어떤 지침을 보고 진행하는지, 어떤 명령어를 사용했는지 알려줘


(참고) ArgoCD로 롤링 업데이트 가이드
# 3.3 ArgoCD로 롤링 업데이트
## 실행 지침
### 단계 1: 새 기능 추가
Notiflex에 새 엔드포인트 또는 기능을 추가하고 버전을 v0.1.1로 올린다.
### 단계 2: 빌드 및 배포
```bash
gcloud builds submit --tag asia-northeast3-docker.pkg.dev/<PROJECT_ID>/notiflex/api:v0.1.1 app/
```
K8s 매니페스트의 이미지 태그를 v0.1.1로 변경한 뒤 git push. ArgoCD가 자동 감지하여 배포한다.
### 단계 3: 배포 확인
```bash
kubectl --context gke-sysnet4admin_book_gitaiops get pods -n notiflex -w
```
> 💡 **kubectl `--context` 사용 권장**: 책 본문 시연도 모든 kubectl 명령에 `--context gke-sysnet4admin_book_gitaiops`를 명시한다. 의도하지 않은 클러스터에 명령이 적용되는 사고를 막는다.
롤링 업데이트로 Pod이 하나씩 교체되는 것을 확인한다.
### 단계 4: 롤백 테스트
독자가 "혹시 방금 배포한 버전에 문제가 있으면 어떻게 돌려?"라고 물으면 `git revert`로 롤백한다. ArgoCD가 이전 커밋 상태를 감지해 자동으로 클러스터도 되돌린다.
```bash
git revert HEAD --no-edit && git push
# ArgoCD가 auto-sync로 v0.1.0 상태로 복원
```
롤백 확인 후 다시 v0.1.1로 되돌린다(한 번 더 `git revert`).
## 다음 단계 안내
단계 3(배포 확인) 완료 직후, 독자에게 먼저 제안할 질문 순서:
1. "혹시 방금 배포한 버전에 문제가 있으면 어떻게 돌려?" (→ 단계 4 롤백 테스트, 3.3.2)
2. 롤백 테스트 완료 후 → "지금 빌드할 때마다 명령어를 직접 치고 있는데, 자동으로 돌게 할 수 없어?" (→ 3.4 CI 탐색)
롤백 안전성 확인이 GitOps의 핵심 가치이므로, CI 자동화 제안보다 먼저 배치한다.
## 💬 질문해보기
> "롤링 업데이트가 뭐야? 서비스 중단 없이 배포하는 원리가 뭐야? maxSurge랑 maxUnavailable은 어떤 역할이야?"


GitOps 를 통한 원복 방안 : revert 사용
방금 배포한 버전에 문제가 있으면 어떻게 돌려?

- 책 지침과 달리 커밋이 다른 경우 확인하고 신규 커밋하는 모습이다. 놀랍다.
- git revert는 변경을 취소하는 신규 커밋을 생성하는 명령어다.
서비스 영향도 확인 방법
배포하는 동안 서비스가 진짜로 안 끊기는 지 확인할 수 있어?

- 클로드가 실험 및 테스트를 다한다.
kubectl run downtime-test -n <네임스페이스> --image=curlimages/curl:latest --restart=Never -- \
sh -c 'i=0; while [ $i -lt 180 ]; do
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 1 http://<서비스명>.<네임스페이스>.svc.cluster.local/<헬스체크경로>);
echo "$(date +%H:%M:%S) $code";
i=$((i+1));
sleep 1;
done'
- --max-time 1: 1초 넘게 응답 없으면 타임아웃 처리 (연결 실패도 놓치지 않고 기록됨)
- 180번 반복 × sleep 1 = 총 3분간 1초 간격 측정 (숫자는 필요에 맞게 조절)
- <서비스명>.<네임스페이스>.svc.cluster.local: ClusterIP Service를 클러스터 내부 DNS로 직접
배포 제어 정책 확인
롤링 업데이트 속도는 어떻게 제어해? 한 번에 몇 개 씩 교체할 지 정할 수 있나? 이것은 노드 교체할 때와 똑같이 동작하는 건가?
GKE 노드 업그레이드할 때도 파드가 교체될떄 동일하게 동작하는거야?

깃허브 액션 CI 구성
이미지 빌드도 자동화하기 위해 깃허브 액션 CI 파이프라인을 구성하겠습니다.
깃허브 액션 CI 만들어줘

(참고) 지침
# 3.4 GitHub Actions CI
## 사전 조건
- GCP 프로젝트 + Artifact Registry 저장소 생성 완료 (ch2)
- GitHub 저장소 + GCP 서비스 계정 생성 완료 (ch2)
## 실행 지침
### 단계 1: CI 파이프라인 생성
`.github/workflows/ci.yaml` 파일을 생성한다:
- **트리거**: main 브랜치에 push (app/ 디렉터리 변경 시)
- **단계**:
1. 코드 체크아웃
2. GCP 인증 (Workload Identity 또는 Service Account Key)
3. Docker 이미지 빌드
4. Artifact Registry에 푸시
5. 이미지 태그: git SHA 기반 (앞 7자리)
### 단계 2: GCP 인증 설정
GitHub Secrets에 GCP 서비스 계정 키를 등록한다:
- `GCP_PROJECT_ID`
- `GCP_SA_KEY` (JSON)
### 단계 3: 이미지 빌드 방식 선택
**방식 A — `docker build` + `docker push`** (책 본문 시연 채택)
ubuntu runner에서 직접 `docker build`로 빌드 후 Artifact Registry에 push한다. `google-github-actions/auth` + `gcloud auth configure-docker`로 GCP 인증을 docker 클라이언트에 위임한다. CI SA에 필요한 권한은 단순하다.
```yaml
- uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- run: gcloud auth configure-docker asia-northeast3-docker.pkg.dev --quiet
- run: |
IMAGE="asia-northeast3-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/notiflex/api:sha-${GITHUB_SHA::7}"
docker build -t "$IMAGE" app/
docker push "$IMAGE"
```
CI SA 권한 (방식 A):
```bash
PROJECT_ID=<your-project>
CI_SA=<ci-sa>@${PROJECT_ID}.iam.gserviceaccount.com
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:${CI_SA}" --role="roles/artifactregistry.writer" --condition=None
```
**방식 B — `gcloud builds submit`** (Cloud Build에 위임)
빌드를 Cloud Build에 위임하면 ubuntu runner는 빌드 명령만 보낸다. 다만 권한이 더 많이 필요하고, 로그 스트리밍 권한 문제가 따로 있다.
```yaml
- name: Build and push image (SHA tag, async)
run: |
IMAGE="asia-northeast3-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/notiflex/api:sha-${GITHUB_SHA::7}"
BUILD_ID=$(gcloud builds submit --tag "$IMAGE" --async \
--project=${{ secrets.GCP_PROJECT_ID }} --format='value(id)' app/)
echo "BUILD_ID=$BUILD_ID" >> $GITHUB_ENV
echo "IMAGE=$IMAGE" >> $GITHUB_ENV
- name: Wait for build to complete
run: |
for i in $(seq 1 30); do
STATUS=$(gcloud builds describe "$BUILD_ID" \
--project=${{ secrets.GCP_PROJECT_ID }} --format='value(status)')
echo "[$i] build status: $STATUS"
case "$STATUS" in
SUCCESS) exit 0 ;;
FAILURE|INTERNAL_ERROR|TIMEOUT|CANCELLED) exit 1 ;;
esac
sleep 10
done
exit 1
```
CI SA 권한 (방식 B):
```bash
PROJECT_ID=<your-project>
PROJECT_NUM=$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')
CI_SA=<ci-sa>@${PROJECT_ID}.iam.gserviceaccount.com
for ROLE in \
roles/artifactregistry.writer \
roles/cloudbuild.builds.editor \
roles/cloudbuild.builds.builder \
roles/storage.objectAdmin \
roles/serviceusage.serviceUsageConsumer \
roles/logging.viewer ; do
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:${CI_SA}" --role="$ROLE" --condition=None
done
# Cloud Build 기본 SA(Compute Engine default) impersonation 허용
gcloud iam service-accounts add-iam-policy-binding \
${PROJECT_NUM}-compute@developer.gserviceaccount.com \
--member="serviceAccount:${CI_SA}" \
--role=roles/iam.serviceAccountUser
```
> ⚠️ 방식 B에서 `iam.serviceAccountUser`가 빠지면 `cannot act as service account ${PROJECT_NUM}-compute@developer.gserviceaccount.com` 에러로 빌드가 제출되지 않는다.
> 💡 **방식 선택**: 학습 환경에서는 권한이 단순한 방식 A(docker push)를 권장한다. 책 본문 시연도 방식 A를 사용한다. 방식 B는 빌드 클러스터 자원이 따로 필요하거나, ubuntu runner의 디스크/메모리가 부족한 경우에 검토한다.
## 트러블슈팅
### `remote: Permission to <owner>/<repo>.git denied to github-actions[bot]` (manifest push 403)
→ 2023년 이후 신규로 생성한 저장소는 `Settings → Actions → General → Workflow permissions` 기본값이 `Read repository contents`이다. ci.yaml의 `permissions: contents: write`만으로는 ArgoCD 매니페스트 push가 403으로 실패한다. repo 레벨 `default_workflow_permissions`도 write여야 한다.
```bash
gh api -X PUT repos/<owner>/<repo>/actions/permissions/workflow \
-f default_workflow_permissions=write \
-f can_approve_pull_request_reviews=false
```
또는 GitHub UI: `Settings → Actions → General → Workflow permissions → "Read and write permissions"` 선택 후 Save.
### `Permission "storage.objects.create" denied` (Artifact Registry 푸시 실패, 방식 B)
→ `roles/artifactregistry.writer` + `roles/storage.objectAdmin` 두 개가 동시에 필요하다. Cloud Build가 내부적으로 임시 GCS 버킷을 사용하기 때문.
### `cannot act as service account ...-compute@developer.gserviceaccount.com` (방식 B)
→ CI SA에 Compute Engine default SA 대상으로 `roles/iam.serviceAccountUser` 바인딩이 빠진 경우. 단계 3 방식 B의 `add-iam-policy-binding` 마지막 블록 실행.
### `Permission 'logging.logEntries.list' denied` (빌드는 SUCCESS인데 step이 실패, 방식 B)
→ `gcloud builds submit` 기본 모드가 로그 스트리밍을 시도한 것이다. `--async` + describe 폴링 패턴으로 전환한다 (단계 3 방식 B 참조).
### `denied: Permission "artifactregistry.repositories.uploadArtifacts" denied` (방식 A)
→ 방식 A에서는 `roles/artifactregistry.writer`만으로 충분하다. 권한 추가 후 잠시 기다린 뒤 재시도한다 (IAM 전파 지연).
## 💬 질문해보기
> "GitHub Actions 말고 다른 CI 도구는 뭐가 있어? Jenkins, GitLab CI와 비교해줘."
깃허브 액션 구성 확인
깃허브 액션 구성 방법과 실행 방법을 몰라 시크릿만 생성하고 .github/workflows/ci.yaml 만 쓰면 되는거야?

# Actions 탭과 `gh run list`에 표시될 워크플로우 이름
name: CI
# 트리거 조건: main 브랜치에 push되고, 그 커밋에 app/ 아래 변경이 포함될 때만 실행
# (JOURNEY.md처럼 app/ 밖의 파일만 바뀐 push는 이 필터에 걸러져 실행되지 않는다)
on:
push:
branches: [main]
paths:
- "app/**"
# 이 워크플로우가 수행할 작업(job) 목록. 하나의 job = 하나의 실행 단위
jobs:
build-and-push:
# GitHub이 매번 새로 띄워주는 우분투 가상머신에서 실행 (직접 서버를 준비할 필요 없음)
runs-on: ubuntu-latest
steps:
# 1단계: 이 가상머신에 우리 저장소 코드를 clone
- name: Checkout
uses: actions/checkout@v4
# 2단계: GCP_SA_KEY 시크릿(서비스 계정 키 JSON)으로 이 VM을 GCP에 로그인시킨다
- name: Authenticate to GCP
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
# 3단계: 방금 받은 GCP 인증을 docker 명령어가 쓸 수 있도록 credential helper로 연결
- name: Configure Docker for Artifact Registry
run: gcloud auth configure-docker asia-northeast3-docker.pkg.dev --quiet
# 4단계: 실제 빌드 및 푸시
# - GCP_PROJECT_ID 시크릿으로 이미지 경로를 채우고
# - GITHUB_SHA(이 커밋의 해시, GitHub이 자동 제공)의 앞 7자리를 태그로 사용
- name: Build and push image
run: |
IMAGE="asia-northeast3-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/notiflex/api:sha-${GITHUB_SHA::7}"
docker build -t "$IMAGE" app/
docker push "$IMAGE"
깃허브 액션 비용 확인
- Private 저장소: GitHub Free 플랜은 월 2,000분 무료 (ubuntu runner 기준 1분=1분 차감)
- Public 저장소: 무제한 무료
- 지금 notiflex-platform은 private이라 무료 한도 안에서 씁니다. 방금 실행이 49초 걸렸으니 실질적으로 거의 소모되지 않는 수준입니다.
- 한도를 넘으면 분당 과금되지만, 이 실습 규모(가끔 몇십 초짜리 빌드)로는 절대 도달하지 않을 양입니다.

깃허브 액션(CI) & ArgoCD(CD) 연결 구성
이미지 빌드를 새로 만들면, 배포도 자동으로 되게 매니페스트 수정이 필요합니다.
CI & CD 구성을 어떻게 하는 지 클로드 코드로 확인하겠습니다.
3.5로 넘어가서 CI랑 ArgoCD 연결해줘, 어떻게 하는거야?
(참고) 지침
# 3.5 CI + ArgoCD 연결
## 실행 지침
### 단계 1: CI에서 매니페스트 자동 업데이트
CI 파이프라인에 단계를 추가한다:
- 빌드 완료 후 k8s 매니페스트의 이미지 태그를 새 버전으로 자동 변경
- 변경된 매니페스트를 git commit & push
- ArgoCD가 변경을 감지하여 자동 배포
### 단계 2: 전체 파이프라인 테스트
코드를 수정하고 push하여 전체 흐름을 확인한다:
1. 코드 push → GitHub Actions CI 트리거
2. 이미지 빌드 → Artifact Registry 푸시
3. 매니페스트 이미지 태그 업데이트 → git push
4. ArgoCD 감지 → 자동 배포
> ⚠️ **GITHUB_ENV vs GITHUB_OUTPUT**: CI에서 step 간에 변수를 전달할 때는 `echo "VAR=value" >> $GITHUB_ENV`를 사용해야 한다. `$GITHUB_OUTPUT`에 저장한 값은 다음 step에서 환경변수(`$VAR`)로 접근할 수 없다. `$GITHUB_OUTPUT`은 `steps.<id>.outputs.<name>` 표현식으로만 참조 가능하다.
> ⚠️ **sed 패턴 주의**: CI에서 매니페스트의 이미지 태그를 sed로 교체할 때, 빈 이미지 라인(`image: `)도 매칭될 수 있다. `IMAGE_URI` 변수가 빈 값이면 이미지가 삭제되므로, 반드시 `$GITHUB_ENV`로 변수를 전달하고, sed 실행 전 `echo $IMAGE_URI`로 값을 확인한다.
## 트러블슈팅
### CI SHA 태그와 로컬 버전 태그 merge conflict (반복 발생)
→ CI가 빌드 후 rollout.yaml의 이미지 태그를 git SHA(예: `a1b2c3d`)로 자동 업데이트하여 push한다.
→ 로컬에서 `v0.3.0` 등 시맨틱 버전으로 커밋한 상태라면, 다음 push 시 reject된다.
→ **이 충돌은 ch6~ch8에서 app/ 수정이 있을 때마다 반복 발생한다.**
해결 패턴:
```bash
git pull origin main --no-rebase
# merge conflict 발생 → rollout.yaml에서 로컬 버전 태그(v0.x.0) 유지
git add k8s/smb/rollout.yaml
git commit -m "merge: resolve CI SHA conflict, keep v0.x.0"
git push origin main
```
> ⚠️ `--rebase`를 쓰면 CI 커밋 위에 로컬 커밋을 올리게 되어 또 다른 충돌이 생길 수 있다. `--no-rebase`로 merge commit을 만드는 것이 안전하다.
## 다음 단계 안내
단계 1(CI에서 매니페스트 자동 업데이트) 구현을 마친 뒤에도 단계 2 엔드투엔드 테스트 전까지는 "완료"라고 말하지 않는다. 구현 직후 독자에게 다음 질문을 먼저 제안한다:
- "이제 코드만 고치고 push하면 진짜로 배포까지 자동으로 되는 거야? 한번 해보자" (→ 단계 2 전체 파이프라인 테스트, 3.5.3)
구현과 엔드투엔드 검증은 분리된 단계이고, 3장은 검증까지 완료해야 끝난다.
## 💬 질문해보기
> "CI에서 매니페스트를 자동으로 업데이트하고 git push하는 게 GitOps 방식이야? 이미지 태그를 git에서 관리하면 뭐가 좋아?"
변경 워크플로우 확인 : 워크플로우 내 메니페스트 수정 및 배포
# Actions 탭과 `gh run list`에 표시될 워크플로우 이름
name: CI
# 트리거 조건: main 브랜치에 push되고, 그 커밋에 app/ 아래 변경이 포함될 때만 실행
# (JOURNEY.md처럼 app/ 밖의 파일만 바뀐 push는 이 필터에 걸러져 실행되지 않는다)
on:
push:
branches: [main]
paths:
- "app/**"
# CI가 매니페스트를 git push하려면 저장소 쓰기 권한이 필요하다
permissions:
contents: write
# 이 워크플로우가 수행할 작업(job) 목록. 하나의 job = 하나의 실행 단위
jobs:
build-and-push:
# GitHub이 매번 새로 띄워주는 우분투 가상머신에서 실행 (직접 서버를 준비할 필요 없음)
runs-on: ubuntu-latest
steps:
# 1단계: 이 가상머신에 우리 저장소 코드를 clone
- name: Checkout
uses: actions/checkout@v4
# 2단계: GCP_SA_KEY 시크릿(서비스 계정 키 JSON)으로 이 VM을 GCP에 로그인시킨다
- name: Authenticate to GCP
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
# 3단계: 방금 받은 GCP 인증을 docker 명령어가 쓸 수 있도록 credential helper로 연결
- name: Configure Docker for Artifact Registry
run: gcloud auth configure-docker asia-northeast3-docker.pkg.dev --quiet
# 4단계: 실제 빌드 및 푸시
# - GCP_PROJECT_ID 시크릿으로 이미지 경로를 채우고
# - GITHUB_SHA(이 커밋의 해시, GitHub이 자동 제공)의 앞 7자리를 태그로 사용
# - IMAGE 값을 $GITHUB_ENV에 저장해서 다음 step에서도 환경변수로 쓸 수 있게 한다
# (steps.<id>.outputs로 저장하면 다음 step에서 env var로 바로 못 씀 — GITHUB_OUTPUT과의 차이)
- name: Build and push image
run: |
IMAGE="asia-northeast3-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/notiflex/api:sha-${GITHUB_SHA::7}"
docker build -t "$IMAGE" app/
docker push "$IMAGE"
echo "IMAGE=$IMAGE" >> "$GITHUB_ENV"
# 5단계: k8s 매니페스트의 이미지 태그를 방금 만든 이미지로 교체
# - IMAGE 값을 실행 전에 echo로 확인 (비어 있으면 sed가 image: 라인을 통째로 지워버릴 수 있음)
- name: Update manifest image tag
run: |
echo "IMAGE=$IMAGE"
sed -i "s|image: asia-northeast3-docker.pkg.dev/.*/notiflex/api:.*|image: $IMAGE|" k8s/smb/deployment.yaml
cat k8s/smb/deployment.yaml
# 6단계: 변경된 매니페스트를 git commit & push
# - CI가 kubectl apply를 직접 하지 않고, Git을 통해서만 배포를 반영한다 (GitOps 원칙)
# - ArgoCD가 이 커밋을 감지해서 자동으로 클러스터에 Sync한다
- name: Commit and push manifest
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add k8s/smb/deployment.yaml
git commit -m "ci: deploy $IMAGE" || echo "no changes to commit"
git push origin main
구성 의문
❯ 깃 REPO 가 서로 다른건가? 겹치면 안되는거아니야? 무한 루프 돌 수 있는거 아닌가

전체 파이프라인 테스트
코드를 수정시키게 하고 전체 파이프라인을 테스트하겠습니다.
이제 코드만 고치고 push하면 진짜로 배포까지 자동으로 되는 거야? 한번 해보자




→ 잘 됩니다!
'AI' 카테고리의 다른 글
| [GitAIOps로 인프라 구성하기] 4. 모니터링 구성하기 (1) | 2026.07.12 |
|---|---|
| [GitAIOps로 인프라 구성하기] 2. 환경 구성 (0) | 2026.07.05 |
| [GitAIOps로 인프라 구성하기] 1. 스터디 소개 (0) | 2026.07.05 |
| SRE Incident Response Agent 와 기능 개발 (0) | 2026.04.20 |
| EKS 최적화 보고서 기능 개발(보안 편) (0) | 2026.04.12 |