유성

GitHub Actions와 ArgoCD로 완성하는 선언적 쿠버네티스 배포 시스템 본문

DevOps

GitHub Actions와 ArgoCD로 완성하는 선언적 쿠버네티스 배포 시스템

백엔드 유성 2026. 1. 29. 18:22

1. '명령'하는 배포에서 '선언'하는 배포로

이전 글에서는 가장 직관적이지만, 한계 또한 명확한 '명령(Push) 방식'의 배포를 다루었다.

 

정답 없는 CI/CD, 우리 환경에 맞는 최적의 파이프라인 설계

이 글에서는 단순한 코드 전송을 넘어, 복잡한 환경에서 어떻게 안정적이고 효율적인 CI/CD 파이프라인을 구축했는지 그 설계 과정을 공유한다. 1. CI/CD란 무엇인가?: 규모에 따른 변칙적 대응CI/CD

youseong.tistory.com

 

 

초기 단계에서는 GitHub Actions의 Runner가 클러스터에 직접 접속해 kubectl 명령을 날리는 방식이 빠르고 효율적이다.

하지만 프로젝트가 커지고 인프라가 복잡해질수록 다음과 같은 치면적인 문제들에 직면하게 된다.

 

기존 yaml 배포 방식의 페인 포인트

  • 추적 불가능한 상태: 클러스터에 어떤 이미지가 돌아가고 있는지 확인하려면 매번 서버에 접속해야 한다.
    Git의 코드와 서버의 실시간 상태가 동기화되지 않기 때문이다.
  • 휘발성 설정: 기존에는 kubectl set image 명령어를 통해 이미지를 교체했다. 이는 마스터 노드의 YAML 파일을 직접 수정하는 것이 번거로워 선택한 임식 방편이었지만, 장애가 발생해 Pod가 재시작되거나 누군가 실수로 설정을 바꾸면 이전 상태로 복구하기가 매우 까다롭다.
  • 위험한 롤백: 배포에 문제가 생겼을 때, "안정적이었던 이전 태그가 뭐였지?"를 찾으며 Git 이력과 DockerHub를 뒤져야 한다. 긴박한 장애 상황에서 사람이 직접 YAML을 수정하거나 태그를 입력한느 행위는 또 다른 '휴먼 에러'를 낳을 수 있다.
  • 보안의 취약성: GitHub Actions가 클러스터를 제어하기 위해 kubeconfig라는 강력한 권한을 외부에 노출한다거나, Runner에 노출해야 하는 부담이 있다.

해결책: Helm과 ArgoCD의 결합

이러한 문제를 해결하기 위해, 우리는 배포의 패러다임을 '명령'에서 '선언'으로 전환했다.

  • Helm: 복잡한 쿠버네티스 리소스들을 템플릿화하여 관리한다. 이제 "어떤 이미지를 쓸 것인가"는 vlaues.yaml 이라는 단 하나의 파일에 선언된다.
  • ArgoCD: 클러스터 내부에서 Git 저장송(Helm Chart)를 24시간 감시하는 감시자 역할을 한다.

 

2. 전체적인 흐름

전체 파이프라인은 크게 3단계의 '파이프라인'으로 이루어진다.

  1. CI 단계 (GitHub Actions): 코드를 빌드하고, Docker에 이미지를 생성하여 레지스트리(Docker Hub)에 푸시한다.
  2. Manifest 업데이트: 빌드가 성공하면 GitHub Actions가 Git 저장소에 있는 Helm Chart의 values.yaml 내 이미지 태그를 자동으로 수정한다.
  3. CD 단계 (ArgoCD): 클러스터 내부에 설치된 ArgoCD가 Git의 변경 사항을 감지(Polling)하고, 클러스터의 현재 상태를 Git에 정의된 상태와 똑같이 맞춘다.

Docker Hub 푸시 후 values.yaml 변경
빌드 완료 후 values.yaml을 자동 수정 후 커밋 내역
ArgoCD가 Github 의 Chart를 확인하고, 변경이 있으면 Sync(배포)를 맞춤



3. GitHub Actions의 변화: 더이상 클러스터를 바라보지 않는다.

기존의 CD 방식에서 GitHub Actions는 클러스터의 '지휘관'이었다.

직접 클러스터에 접속하여 명령을 내려야 했기에, GitHub Actions는 클러스터의 모든 권한이 담긴 KUBE_CONFIG라는 강력한 열쇠를 쥐고 있어야만 했다.

 

하지만 GitOps 도입 후, Github Actions의 역할은 '빌드'와 '기록'으로 명확하게 범위가 좁혀졌다.

 

보안의 강화 "열쇠를 맡기지 않아도 된다."

  • 권한 노출 제거: 이제 GitHub Secrets에 더 이상 민감한 클러스터 접속 정보를 저장할 필요가 없다. 단순히 이미지 빌드와 Git 저장소에 대한 수정 권한만 있으면 된다.
  • 인바운드 차단: 클러스터 외부에서 내부로 들어오는 통로를 열어줄 필요가 없다. 클러스터는 외부의 명령을 기다리는 대신, 내부에 상주하는 ArgoCD를 통해 스스로 필요한 정보를 가져오는 '아웃바운드' 방식을 택했기 때문이다.

 

github actions CI 코드

더보기
name: Production Deployment

on:
  push:
    branches: [ "master" ]

concurrency:
  group: production-deploy
  cancel-in-progress: true

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: write
	
    # 1. 코드 체크아웃
    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    # 2. Docker Hub 로그인
    - name: Login to Docker Hub
      uses: docker/login-action@v3
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_TOKEN }}

    # 3. Docker 이미지 빌드 및 푸시
    - name: Build and push Docker image
      run: |
        IMAGE_TAG=${{ github.sha }}
        docker build --platform linux/amd64 -t ${{ secrets.DOCKERHUB_USERNAME }}/demo12:$IMAGE_TAG .
        docker push ${{ secrets.DOCKERHUB_USERNAME }}/demo12:$IMAGE_TAG

	# 4. 푸시가 완료된 후 yq 명령어를 사용하여 values.yaml 파일 수정
    - name: Update Helm Chart values
      run: |
        cd deploy
        yq -i '.image.tag = "${{ github.sha }}"' values.yaml
        git config --global user.email "github-actions@github.com"
        git config --global user.name "github-actions"
        git add values.yaml
        git commit -m "chore: update demo12 image tag to ${{ github.sha }}"
        git push

 

4. ArgoCD: GitHub만을 감시하는 '상태 감시자'

ArgoCD는 클러스터 외부에 존재하는 것이 아니라, 클러스터 내부에서 상주하며 동작하는 Standalone 또는 중앙 집중형인 Hub-and-spoke 모델로 존재한다.

마치 스타크래프트의 '옵저버'처럼, ArgoCD는 클러스터의 구석구석을 모니터링하며 사전에 정의된 설정과 실제 환경이 일치하는지 실시간으로 감시한다.

 

Desired State vs Current State

ArgoCD의 작동 원리는 매우 명확하다. 두 가지 상태를 끊임없이 비교하는 것.

  • Desired State(사용자가 원하는 상태): GitHub 저장소에 저장된 Helm 차트와 Manifest 파일들이다. "우리 서버는 이 설정대로 돌아가야 해"라는 약속이다.
  • Current State(현재 돌아가고 있는 상태): 쿠버네티스 클러스터에서 실제로 실행 중인 리소스들의 실시간 상태이다.

이 두 상태 사이에 균열(mismatch)이 생기는 시점, 즉 GitHub Actions가 CI 과정을 마치고 Git의 이미지 태그를 Helm코드에서 수정하는 순간에 workflow를 시작하게 된다.

  • 감지: GitHub Actions가 values.yaml을 수정하면, ArgoCD는 저장된 설정과 현재 클러스터 사이에 Mismatch가 발생했음을 즉시 감지한다.
  • 배포 트리거: 상태가 불일치함을 확인하는 순간, ArgoCD는 배포 파이프라인을 가동한다.
  • 동적 템플릿 완성: ArgoCD는 GitHub에 올려놓은 Helm Chart(배포 템플릿)을 가져와 현재 수정된 values.yaml의 변수들을 결합하여 최종적인 쿠버네티스 리소스 파일을 동적으로 완성시킨다.

동적으로 생성되는 템플릿 샘플(helm template . 명령어 수행 결과)

더보기

---
# Source: deploy/templates/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: release-name-deploy
  labels:
    helm.sh/chart: deploy-0.1.0
    app.kubernetes.io/name: deploy
    app.kubernetes.io/instance: release-name
    app.kubernetes.io/version: "1.16.0"
    app.kubernetes.io/managed-by: Helm
automountServiceAccountToken: true
---
# Source: deploy/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: release-name-deploy
  labels:
    helm.sh/chart: deploy-0.1.0
    app.kubernetes.io/name: deploy
    app.kubernetes.io/instance: release-name
    app.kubernetes.io/version: "1.16.0"
    app.kubernetes.io/managed-by: Helm
spec:
  type: NodePort
  ports:
    - port: 8080
      targetPort: http
      protocol: TCP
      name: http
  selector:
    app: demo12
---
# Source: deploy/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo12
  labels:
    helm.sh/chart: deploy-0.1.0
    app.kubernetes.io/name: deploy
    app.kubernetes.io/instance: release-name
    app.kubernetes.io/version: "1.16.0"
    app.kubernetes.io/managed-by: Helm
spec:
  replicas: 2
  selector:
    matchLabels:
      app: demo12
  template:
    metadata:
      labels:
        app: demo12
    spec:
      serviceAccountName: release-name-deploy
      containers:
        - name: demo12
          image: "*****/demo12:ebccc97b1ba3cc717c926bac232151d52c4f7ce9"
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 8080
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /actuator/health
              port: http
            initialDelaySeconds: 40
            periodSeconds: 15
            timeoutSeconds: 5

          readinessProbe:
            httpGet:
              path: /actuator/health
              port: http
            initialDelaySeconds: 40
            periodSeconds: 10
            failureThreshold: 3
          resources:
            httpGet:
              path: /
              port: http
---
# Source: deploy/templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "release-name-deploy-test-connection"
  labels:
    helm.sh/chart: deploy-0.1.0
    app.kubernetes.io/name: deploy
    app.kubernetes.io/instance: release-name
    app.kubernetes.io/version: "1.16.0"
    app.kubernetes.io/managed-by: Helm
  annotations:
    "helm.sh/hook": test
spec:
  containers:
    - name: wget
      image: busybox
      command: ['wget']
      args: ['release-name-deploy:8080']
  restartPolicy: Never

템플릿을 토대로 자동 배포를 시작하게 되고, 아래는 2개의 레플리카를 띄운 후 배포를 진행하는 과정으로

ArgoCD 대시보드 에서는 다운 타임 없는 롤링 업데이트를 확인할 수 있다.

 

여기서 rs라고 표시된 항목은 ReplicaSet으로 버전업이 될 때 이전 버전을 삭제하지 않고, pod만 죽여서 관리한다.

만약 긴급롤백 을 수행해야 할 경우 해당 ReplicaSet을 사용하여 즉시 롤백이 가능하다.

(물론 설정이 꼬일 수 있으므로 git revert가 롤백 수행간에 더 권장되는 방식이다)

 

5. 트러블 슈팅: 설계 시 고려해야 할 점

자동화 파이프라인 뒤에는 언제나 예기치 못한 복잡한 설정이 포함되어있다.

이번 GitOps 환경을 구축하면서 겪었던 문제들과 해결책을 공유한다.

아키텍처의 함정: exec format error (환경 분리에 따른 구성 설정 필요)

  • 문제: Mac(개발) + GitHub(빌드) + Linux(운영) 환경의 다름으로 인해 각 빌드와 배포 단계가 각 OS에 맞지 않으면 에러 발생
  • 해결: jdk등의 HostOS에서 올라가는 이미지들을 각 환경에 맞춰서 올려주어야 함.

계속되는 감시로 인해 필요한 health check (초기 복잡성 증가)

  • 문제: ArgoCD는 서비스가 정상적으로 떠 있는지 계속해서 확인을 하므로 엔드포인트를 만들어주어야 함
  • 해결: 서비스 초기 설정부터 커스텀 health check 또는 actuator 엔드포인트 등록이 필요함

라벨 셀렉터의 불일치

Service와 Pod 사이의 연결 고리는 라벨을 사용해야 한다.

  • 문제: 서비스의 seletor와 포드의 labels가 한글자라도 다르면 서비스는 전달 대상을 찾지 못해 ENDPOINTS <none> 상태가 된다.
  • 해결: 배포 후 kubectl get ep 명령어로 엔드포인트가 정상적으로 포드 IP를 가리키는지 배포시점에 수시로 확인해야 하며, Helm 차트를 작성할 때 공동 라벨 관리 전략을 미리 세우는 것이 좋을것으로 생각된다.

 

6. 결론: 자동화 넘어의 안정성

지금까지 다양한 환경에서 배포를 경험하면서 가장 힘들었던 지점은 '서버마다 파편화된 세팅'과 이를 관리하는 '수동의 기록들' 이였다.

 

서버의 IP, 포트 번호, 환경 변수들을 엑셀이나 PDF 파일로 정리하고, 변경 사항이 생길 때마다 일일이 현행화하는 과정은 여간 소모적인 일이 아니였다.

  • 현행화의 늪: 바쁜 장애 대응 중에 문서를 먼저 고치는 일은 거의 불가능에 가깝다. 결국 "서버 설정은 1.5버전인데 문서는 1.1버전"인 상태가 반복된다.
  • 파편화된 정보: 이 파일이 어느 팀원 컴퓨터에 있는지, 어떤 게 최신 버전인지 찾는 데에만 개발 시간을 낭비하게 된다.

GitOps: 살아있는 문서가 되는 인프라

하지만 이번에 구축한 GitHub Actions와 ArgoCD 기반의 GitOps 환경은 이 패러다임을 완전히 바꾼다.

  • Git이 곧 진실: 이제 인프라의 모든 설정은 엑셀이 아닌 Git 저장소의 Helm CHart에 담긴다. Git에 커밋된 내용이 곧 서버의 현재 상태를 대변하므로, 별도의 현행화 문서가 필요가 없다.
  • 추적 가능성: 누가 언제, 왜 설정을 바꿨는지 Git 히스토리를 통해 1초 만에 파악이 가능하다. "누가 이 포트 열었어?"라고 물어볼 필요가 없다.
  • 따라오는 안정감: 수동 배포는 "혹시 내가 설정 하나를 빼먹지 않았을까?" 하는 불안함이 늘 따라다녔지만, 이제는 ArgoCD가 24시간 클러스터를 감시하며 우리가 선언한 상태를 강제로 유지해주기에, 개발자는 오직 비즈니스 로직에만 집중할 수 있는 자유를 얻게 된다.

물론 과거 방식의 특정 온프레미스 IP에 요청을 보내는 것 처럼 특정 pod만 골라서 요청을 보내는 것은, IP가 가변적으로 변화는 환경에서는 더 어려워진다.

그래서 특정 Pod를 알지 못해도 처리가 가능한 Kafka 디커플링 또는 처음부터 clusterIP를 None으로 설정하면서 IP리스트를 세팅하는 복잡한 과정이 추가될 수 있다.

 

그러므로 이러한 안정성을 위해 감수해야 하는 부분도 물론 존재하고, 아키텍처를 설계할 때 이를 충분히 고민하여 설계하는 것이 바람직하다.