13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[태그:] CI/CD

  • [Cloud] GitHub Actions 비용 폭탄 방지: 불필요한 과금 줄이는 실전 전략

    [Cloud] GitHub Actions 비용 폭탄 방지: 불필요한 과금 줄이는 실전 전략

    GitHub Actions 비용 폭탄 방지: 불필요한 과금 줄이는 실전 전략

    안녕하세요, 13년차의 서버실 주인장입니다. 홈랩에서 이것저것 자동화 돌려보면서 GitHub Actions(깃허브 액션즈)를 정말 요긴하게 쓰고 있는데요. 처음엔 ‘우와, 이거 진짜 편하다!’ 하면서 신나게 돌리다가 어느 날 월말에 날아온 비용 청구서를 보고 깜짝 놀랐던 기억이 납니다. 저만 이런 경험 있는 거 아니죠? 😅 생각보다 GitHub Actions 비용이 만만치 않게 나올 때가 있더라고요. 특히 Public 리포지토리(Repository)는 무료 사용량이 넉넉하지만, Private 리포지토리(Repository)에서 조금만 무심하게 사용하다 보면 예상치 못한 과금 폭탄을 맞기 쉬워요. 저도 한동안 ‘이게 왜 이렇게 나왔지?’ 하면서 삽질 좀 했거든요. 그래서 오늘은 제가 직접 겪고 배운 GitHub Actions 과금을 줄이는 실전 전략들을 여러분께 공유해 드릴까 합니다. CI/CD(Continuous Integration/Continuous Delivery, 지속적인 통합/지속적인 배포) 환경을 효율적으로 운영하면서도 비용 걱정 없이 GitHub Actions를 활용하는 방법을 함께 알아봐요!

    GitHub Actions 비용 절감 전략 개요 다이어그램

    GitHub Actions는 편리하지만, 비용 관리가 중요해요. 워크플로우를 최적화하여 GitHub Actions 비용을 절감하는 여정을 시작해볼까요?

    개념 설명: GitHub Actions 과금은 어떻게 될까?

    본격적인 전략에 앞서, GitHub Actions가 어떤 기준으로 과금되는지 간단하게 짚고 넘어가야겠죠? 사실 GitHub Actions는 실행 시간에 따라 요금이 부과돼요. 즉, 워크플로우(Workflow)가 실행되는 시간이 길어질수록, 또는 실행 횟수가 많아질수록 비용이 늘어나는 구조더라고요. 공식 문서에 따르면, GitHub이 제공하는 호스팅 러너(Hosted Runner)를 사용할 경우 OS 종류(Ubuntu, Windows, macOS)와 인스턴스 사양에 따라 분당 요금이 다르게 책정되어 있어요. 예를 들어, Ubuntu(우분투)는 비교적 저렴하고 macOS(맥OS)는 비싼 편이죠. Private 리포지토리의 경우 기본적으로 매월 일정 시간(예: Free 계정은 2,000분)이 무료로 제공되는데, 이 시간을 초과하면 분당 요금이 청구되기 시작해요. 제 경험상, 작은 프로젝트라도 CI/CD 파이프라인(Pipeline)을 여러 개 돌리다 보면 이 무료 시간을 금방 소진하더라고요.

    실전 전략 1: 불필요한 워크플로우 실행 줄이기

    가장 기본적인 전략이지만, 의외로 간과하기 쉬운 부분입니다. 워크플로우는 필요한 순간에만 실행되도록 해야 해요. 불필요하게 모든 커밋(Commit)이나 모든 브랜치(Branch)에서 실행되도록 설정되어 있진 않은지 확인해봐야 합니다.

    • on 트리거(Trigger) 최적화:

      on 키워드는 워크플로우가 언제 실행될지 정의해요. 예를 들어, push 이벤트(Event) 시 특정 브랜치에서만 실행되도록 제한하거나, pull_request 이벤트 시에만 실행되도록 설정할 수 있습니다.

      name: CI
      
      on:
        push:
          branches:
            - main # main 브랜치에 push 될 때만 실행
        pull_request:
          branches:
            - main # main 브랜치로 PR이 열릴 때만 실행
        # workflow_dispatch: # 수동 실행을 위한 트리거 (필요할 때만)
      

      저는 보통 main 브랜치에 푸시되거나, main 브랜치로 풀 리퀘스트가 들어올 때만 CI/CD 워크플로우가 돌도록 설정하는 편이에요. 개발 브랜치에서는 테스트가 필요할 때만 수동으로 돌리거나, 아예 워크플로우를 분리해서 비용 효율적으로 관리하죠.

    • paths 필터(Filter) 활용:

      특정 파일이 변경되었을 때만 워크플로우가 실행되도록 설정할 수도 있어요. 예를 들어, 백엔드(Backend) 코드가 변경되었을 때만 백엔드 CI 워크플로우가 돌도록 하는 식이죠.

      name: Frontend CI
      
      on:
        push:
          paths:
            - 'frontend/**' # frontend 디렉토리 하위 파일 변경 시에만 실행
        pull_request:
          paths:
            - 'frontend/**'
      

      이렇게 설정하면 프론트엔드(Frontend) 코드만 바뀌었는데 백엔드 테스트까지 불필요하게 도는 일을 막을 수 있어요. 초기엔 이걸 몰라서 모든 변경에 모든 워크플로우가 다 돌았었는데, paths 필터 적용하고 나니 실행 시간이 확 줄더라고요. 💡

    실전 전략 2: 워크플로우 실행 시간 단축 및 리소스 최적화

    다음은 워크플로우 자체의 실행 시간을 줄이는 방법이에요. 실행 시간이 짧아질수록 과금되는 분(minute)도 줄어들겠죠?

    • 캐싱(Caching) 적극 활용:

      의존성(Dependencies) 설치는 워크플로우에서 상당한 시간을 차지합니다. actions/cache 액션(Action)을 사용해서 의존성이나 빌드(Build) 결과물을 캐싱하면 다음 실행부터는 훨씬 빠르게 작업을 시작할 수 있어요.

      jobs:
        build:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
            - name: Cache pnpm modules
              uses: actions/cache@v4
              with:
                path: ~/.pnpm-store
                key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
                restore-keys: |
                  ${{ runner.os }}-pnpm-
            - name: Setup pnpm
              uses: pnpm/action-setup@v3
              with:
                version: 8
                run_install: false
            - name: Install dependencies
              run: pnpm install --frozen-lockfile
            - name: Build
              run: pnpm build
      

      저는 Node.js(노드제이에스) 프로젝트에서 pnpm을 사용할 때 캐싱을 적용한 예시예요. 처음엔 캐싱이 적용 안 돼서 매번 pnpm install이 오래 걸렸는데, 캐싱하고 나니 빌드 시간이 절반 이하로 줄어들더라고요. ✅ 특히 자주 바뀌지 않는 의존성이라면 캐싱 효과가 정말 크더라고요.

    • 셀프 호스팅 러너(Self-hosted Runner) 고려:

      만약 항상 돌아가는 서버나 홈랩 장비가 있다면, 여기에 GitHub Actions 러너를 직접 설치해서 사용하는 셀프 호스팅 러너를 고려해볼 수 있어요. 이 경우 GitHub 호스팅 러너 사용에 대한 분당 요금은 발생하지 않습니다. 대신 러너를 운영하는 서버의 전기료나 유지보수 비용은 발생하겠죠.

      GitHub Actions 셀프 호스팅 러너 설정 화면

      셀프 호스팅 러너를 설정하는 화면입니다. 서버 환경에 맞춰 러너를 구성하고 토큰으로 연결하면 돼요.

      저는 홈랩을 운영하는 가장 큰 이유 중 하나가 바로 이런 부분이에요. GitHub Actions 러너를 제 미니 PC에 설치해서 돌리니까, 비용 걱정 없이 마구잡이로 워크플로우를 테스트해볼 수 있더라고요. 😆 물론 초기 설정 삽질은 좀 했지만, 장기적으로 보면 아주 효율적인 방법이에요. 특히 GPU(그래픽 처리 장치)가 필요한 머신러닝(Machine Learning) 워크로드(Workload) 같은 경우에는 셀프 호스팅 러너가 거의 필수적이라고 할 수 있어요.

    실전 전략 3: 매트릭스(Matrix) 전략과 병렬 처리 최적화

    워크플로우의 실행 시간을 줄이는 또 다른 방법은 작업을 효율적으로 병렬 처리하는 거예요. GitHub Actions의 매트릭스 전략은 여러 환경에서 동시에 테스트를 실행할 때 유용하죠.

    • 매트릭스 전략으로 병렬 작업:

      여러 버전의 언어나 운영체제에서 테스트를 실행해야 할 때 매트릭스 전략을 사용하면 각 조합을 병렬로 실행하여 전체 시간을 단축할 수 있어요.

      jobs:
        test:
          runs-on: ubuntu-latest
          strategy:
            matrix:
              node-version: [18.x, 20.x] # Node.js 18과 20에서 각각 실행
          steps:
            - uses: actions/checkout@v4
            - name: Use Node.js ${{ matrix.node-version }}
              uses: actions/setup-node@v4
              with:
                node-version: ${{ matrix.node-version }}
            - run: npm ci
            - run: npm test
      

      위 예시처럼 node-version을 18.x와 20.x로 설정하면, 두 가지 환경에서 동시에 테스트가 실행돼요. 각 환경별 테스트 시간이 총 워크플로우 실행 시간에 합산되지 않고 병렬로 처리되기 때문에 전체 시간이 확 줄어드는 효과가 있죠. 물론 동시 실행되는 러너 수만큼 비용은 발생할 수 있지만, 전체 빌드/테스트 시간을 줄여서 결과적으로 총 소요 분을 줄이는 데 도움이 되더라고요.

    트러블슈팅: 예상치 못한 비용 발생, 어떻게 확인하고 해결할까?

    저도 처음엔 비용이 왜 이렇게 많이 나왔는지 몰라서 막막했어요. GitHub Actions 대시보드에서 사용량을 확인하는 것이 첫걸음입니다.

    • 사용량(Usage) 대시보드 확인:

      GitHub 리포지토리의 Settings > Billing and plans > Usage 메뉴에서 GitHub Actions 사용량을 확인할 수 있어요. 여기서 어떤 워크플로우가 얼마나 많은 시간을 사용했는지, 어떤 OS 러너를 사용했는지 상세하게 볼 수 있습니다.

      GitHub Actions 사용량 대시보드 화면

      GitHub Actions 사용량 대시보드예요. 이 화면을 통해 어떤 워크플로우가 비용을 많이 쓰고 있는지 파악할 수 있어요.

      이 대시보드를 주기적으로 확인하는 습관을 들이는 게 중요해요. ‘어? 이 워크플로우는 이렇게 많이 돌 필요가 없는데?’ 같은 인사이트를 얻을 수 있거든요. 저는 이 대시보드를 보고 나서 불필요한 push 트리거를 꽤 많이 제거했어요. ⚠️

    • 워크플로우 로그(Log) 분석:

      특정 워크플로우가 너무 오래 걸린다면, 해당 워크플로우의 실행 로그를 자세히 살펴봐요. 어떤 스텝(Step)에서 시간이 오래 걸리는지 파악하고 그 부분을 최적화하는 것이 중요합니다. 예를 들어, 특정 테스트 스위트(Test Suite)가 너무 느리다면, 테스트 코드를 개선하거나 병렬 테스트 프레임워크를 도입하는 것을 고려해볼 수 있어요.

    마무리: 비용 효율적인 CI/CD를 위한 여정

    오늘은 GitHub Actions 비용 폭탄을 방지하고 불필요한 과금을 줄이는 실전 전략들을 함께 알아봤어요. 제가 직접 삽질하면서 터득한 내용들이라 여러분께도 도움이 되었으면 좋겠네요.

    전략 주요 내용 기대 효과
    트리거 최적화 on, paths 필터로 불필요한 실행 방지 ⚠️ 불필요한 과금 원천 차단
    캐싱 활용 의존성 및 빌드 결과물 캐싱 워크플로우 실행 시간 단축, 비용 절감
    셀프 호스팅 러너 자체 서버/홈랩에 러너 구축 GitHub 호스팅 러너 비용 0, 커스텀 환경 구축 가능
    병렬 처리 매트릭스 전략으로 동시 작업 실행 전체 워크플로우 시간 단축
    사용량 모니터링 GitHub 대시보드 및 로그 분석으로 문제 워크플로우 식별 지속적인 최적화 기회 확보
    GitHub Actions 비용 절감 핵심 팁 요약 인포그래픽

    GitHub Actions 비용 절감을 위한 핵심 팁을 한눈에 볼 수 있는 요약 인포그래픽이에요.

    GitHub Actions는 정말 강력하고 편리한 도구지만, 제대로 관리하지 않으면 예상치 못한 비용이 발생할 수 있어요. 오늘 알려드린 전략들을 잘 활용하셔서 여러분의 CI/CD 환경을 더욱 효율적이고 경제적으로 만드시길 바랍니다. 저도 여전히 새로운 기술을 실험하고 삽질하면서 배우고 있는데요, 혹시 더 좋은 팁이 있다면 댓글로 공유해 주시면 저도 다음 글에 참고하겠습니다! 다음번엔 좀 더 깊이 있는 GitHub Actions 최적화 팁으로 찾아올게요. 그때까지 다들 즐거운 개발 라이프 되세요! 🎉

  • [k8s] AWX로 쿠버네티스 워크플로우 자동화: 실전 가이드

    [k8s] AWX로 쿠버네티스 워크플로우 자동화: 실전 가이드

    안녕하세요, 13년차 인프라 엔지니어 ’13년차의 서버실’ 주인장입니다. 오늘은 많은 분들이 고민하시는 쿠버네티스(Kubernetes) 워크플로우 자동화에 대해 이야기해보려고 해요. 특히 AWX를 활용해서 어떻게 복잡한 배포 과정을 효율적으로 관리할 수 있는지, 제가 직접 겪었던 경험과 실전 사례를 중심으로 풀어볼까 합니다. Ansible과 DevOps 문화에 관심 있는 분들이라면 이번 글이 정말 도움이 될 거예요.

    저도 처음엔 쿠버네티스 클러스터에 애플리케이션을 배포하는 과정이 꽤나 번거롭더라고요. 매번 kubectl apply -f 명령어를 입력하고, 버전 관리하고, 환경별로 설정 바꾸고… 반복적인 작업은 언제나 실수의 여지를 남기기 마련이잖아요. 그러다 보니 자연스럽게 자동화의 필요성을 느끼게 됐고, 홈랩에서 AWX(Ansible Workflow eXecutor)를 활용한 솔루션을 모색하게 됐습니다. 직접 삽질해가며 구축했던 과정, 지금부터 솔직하게 공유해볼게요.

    AWX와 쿠버네티스 연동을 통한 워크플로우 자동화 개요 아키텍처 다이어그램, AWX가 Git에서 Ansible Playbook을 가져와 쿠버네티스에 배포하는 과정을 보여줍니다.

    AWX와 쿠버네티스 연동을 통한 워크플로우 자동화 개요 아키텍처 다이어그램. AWX가 Git 저장소에서 Ansible Playbook을 가져와 쿠버네티스 API를 통해 리소스를 배포하는 과정을 보여줍니다.

    AWX와 쿠버네티스, 왜 함께 써야 할까요?

    먼저 핵심 개념부터 짚고 넘어갈게요. AWX는 Ansible Automation Platform의 업스트림 오픈소스 프로젝트로, 웹 기반 UI를 통해 Ansible Playbook을 실행하고 관리해주는 도구예요. 쉽게 말해, 터미널에서 명령어로 실행하던 Ansible 작업을 UI로 편하게 스케줄링하고, 권한을 관리하고, 실행 결과를 모니터링할 수 있다는 거죠. 제가 홈랩에서 다양한 자동화 실험을 할 때 정말 유용하게 써먹고 있는 녀석입니다.

    그리고 쿠버네티스(Kubernetes, K8s)는 컨테이너화된 워크로드와 서비스를 자동으로 배포, 스케일링 및 관리해주는 오픈소스 시스템이에요. 컨테이너 오케스트레이션(Container Orchestration)의 사실상 표준이 되었죠. 선언적(Declarative) 방식으로 원하는 상태를 정의하면, 쿠버네티스가 그 상태를 유지하도록 알아서 관리해줍니다. 저는 주로 Deployment(디플로이먼트)와 Service(서비스), Ingress(인그레스, 외부 트래픽 진입점) 같은 리소스들을 YAML 파일로 정의해서 사용하고 있어요.

    이 둘을 함께 쓰면 어떤 시너지가 생길까요? 바로 선언적 인프라(Declarative Infrastructure) 관리가 가능해진다는 거예요. Ansible Playbook으로 쿠버네티스 리소스의 최종 상태를 정의하고, AWX가 이 Playbook을 실행해서 클러스터에 배포하는 방식이죠. 이렇게 하면 수동 작업으로 인한 오류를 줄이고, 배포 과정을 표준화하며, 지속적인 통합/배포(CI/CD) 워크플로우를 구축하는 데 정말 큰 도움이 돼요. 실제 운영 환경에서 이 조합은 정말 강력하더라고요.

    실전 구현: AWX로 쿠버네티스 워크플로우 자동화하기

    자, 그럼 이제 제가 직접 구축했던 방법을 단계별로 보여드릴게요. 목표는 AWX를 통해 간단한 Nginx 웹 서버를 쿠버네티스 클러스터에 배포하고, 외부에 노출시키는 거예요.

    1단계: AWX 환경 설정

    1. 인벤토리(Inventory) 생성: AWX에서 Playbook을 실행할 대상(Host)을 정의합니다. 여기서는 쿠버네티스 클러스터의 API 서버에 접근해야 하므로, 로컬 호스트를 대상으로 하거나, 쿠버네티스 API 엔드포인트를 지정해요. 저는 주로 localhost를 대상으로 하고, kubeconfig 파일을 통해 클러스터에 접근하는 방식을 선호합니다.
    2. 자격 증명(Credential) 생성: 쿠버네티스 클러스터에 접근하기 위한 자격 증명이 필요해요. kubeconfig 파일을 AWX에 등록하는 방법이 가장 일반적입니다.
    # AWX Credential 설정 예시 (Kubeconfig Type)
    # --- Kubeconfig 내용 --- (실제 파일 내용)
    apiVersion: v1
    clusters:
    - cluster:
        certificate-authority-data: ...
        server: https://your-kubernetes-api-server:6443
      name: my-cluster
    contexts:
    - context:
        cluster: my-cluster
        user: my-user
      name: my-context
    current-context: my-context
    kind: Config
    preferences: {}
    users:
    - name: my-user
      user:
        client-certificate-data: ...
        client-key-data: ...
    

    2단계: Ansible Playbook 작성

    쿠버네티스 리소스를 배포하기 위한 Playbook을 작성해요. Ansible이 제공하는 kubernetes.core.k8s 모듈로 쿠버네티스 리소스 관리가 정말 쉬워진다니까요. 저는 Nginx Deployment와 Service를 배포하는 Playbook을 만들었어요.

    # playbook.yaml
    ---
    - name: Deploy Nginx to Kubernetes
      hosts: localhost
      connection: local
      collections:
        - kubernetes.core
    
      vars:
        namespace: default
        app_name: my-nginx
        image_version: 1.25.3
    
      tasks:
        - name: Ensure Namespace exists
          kubernetes.core.k8s:
            api_version: v1
            kind: Namespace
            name: "{{ namespace }}"
            state: present
    
        - name: Deploy Nginx Deployment
          kubernetes.core.k8s:
            state: present
            definition: |
              apiVersion: apps/v1
              kind: Deployment
              metadata:
                name: "{{ app_name }}-deployment"
                namespace: "{{ namespace }}"
                labels:
                  app: "{{ app_name }}"
              spec:
                replicas: 2
                selector:
                  matchLabels:
                    app: "{{ app_name }}"
                template:
                  metadata:
                    labels:
                      app: "{{ app_name }}"
                  spec:
                    containers:
                    - name: "{{ app_name }}"
                      image: nginx:"{{ image_version }}"
                      ports:
                      - containerPort: 80
    
        - name: Expose Nginx Service
          kubernetes.core.k8s:
            state: present
            definition: |
              apiVersion: v1
              kind: Service
              metadata:
                name: "{{ app_name }}-service"
                namespace: "{{ namespace }}"
                labels:
                  app: "{{ app_name }}"
              spec:
                selector:
                  app: "{{ app_name }}"
                ports:
                  - protocol: TCP
                    port: 80
                    targetPort: 80
                type: NodePort # 또는 LoadBalancer, ClusterIP 등 환경에 맞게
    

    이 Playbook을 Git 저장소(예: GitHub, GitLab)에 커밋해요. AWX가 이 저장소에서 Playbook을 가져와 실행할 거거든요.

    3단계: AWX 프로젝트 및 작업 템플릿(Job Template) 설정

    AWX UI에서 다음을 설정합니다.

    1. 프로젝트(Project) 생성: Git 저장소를 연결하여 Playbook을 가져와요.
    2. 작업 템플릿(Job Template) 생성: 이 템플릿에 위에서 만든 인벤토리, 자격 증명, 프로젝트, 그리고 실행할 Playbook 파일(playbook.yaml)을 연결합니다.
    AWX 작업 템플릿 설정 화면 스크린샷, Git 프로젝트, 인벤토리, 쿠버네티스 자격 증명, 실행할 Playbook 파일을 지정하는 모습

    AWX 작업 템플릿 설정 화면 스크린샷. Git 프로젝트, 인벤토리, 쿠버네티스 자격 증명, 실행할 Playbook 파일을 지정하는 모습을 보여줍니다.

    ⚠️ 삽질 경험 & 워크플로우 자동화 트러블슈팅

    제가 이 AWX와 쿠버네티스 자동화 과정을 진행하면서 겪었던 몇 가지 삽질과 그 해결책을 공유할게요. 혹시 비슷한 문제를 겪는 분들이 있다면 도움이 될 거예요.

    • 인증 오류 (Authentication Error): 가장 흔한 문제 중 하나예요. kubeconfig 파일의 권한 문제, 또는 파일 내용 자체가 잘못된 경우가 많았어요. 특히 client-certificate-data와 client-key-data가 정확한 base64 인코딩 값인지 여러 번 확인했거든요. AWX가 실행되는 컨테이너 환경에서 kubeconfig 파일에 접근 권한이 없어서 생기는 경우도 있었고요. 💡 팁: AWX 컨테이너 내부에서 kubectl get pods를 직접 실행해보며 인증 문제를 디버깅해보세요.
    • YAML 문법 오류: 쿠버네티스 리소스 정의는 YAML 파일로 하는데, 들여쓰기나 오타 하나로도 워크플로우가 실행 안 돼요. 특히 Ansible Playbook 내 definition 블록 안에 YAML을 넣을 때는 더욱 주의해야 합니다. ⚠️ 경고: definition: | 다음 줄부터는 반드시 두 칸 이상 들여쓰기 해야 합니다!
    • Ansible kubernetes.core.k8s 모듈 문제: 처음에는 이 모듈을 쓰는 게 익숙하지 않아서 헤맸어요. 특히 state: present로 리소스를 생성하고, state: absent로 삭제하는 방식에 익숙해지는 데 시간이 좀 걸렸거든요. 그리고 kubeconfig 파일 경로를 명시적으로 지정해야 하는 경우도 있었습니다 (kubeconfig: /path/to/kubeconfig).
    • 네트워크 연결 문제: AWX가 쿠버네티스 API 서버에 접근할 수 없는 네트워크 환경일 경우 당연히 실패하죠. 방화벽 규칙이나 네트워크 정책을 다시 확인해야 해요.

    ✅ 결과 검증 및 자동화의 힘!

    모든 설정이 끝나면, AWX UI에서 작업 템플릿을 실행(Launch)해요. Playbook이 성공적으로 실행되면 AWX 작업 로그에서 초록색 SUCCESS 메시지를 확인할 수 있어요. 저도 이 메시지를 처음 봤을 때, “드디어 됐다!” 하고 쾌재를 불렀던 기억이 생생해요.

    이제 쿠버네티스 클러스터에서 실제로 리소스가 배포되었는지 확인해볼 차례입니다.

    
    kubectl get deployments -n default
    # NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
    # my-nginx-deployment   2/2     2            2           2m
    
    kubectl get services -n default
    # NAME              TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
    # my-nginx-service   NodePort   10.96.123.456   <none>        80:3xxxx/TCP   2m
    

    정상적으로 Nginx Deployment와 Service가 생성된 걸 확인할 수 있어요. 이제 NodePort로 할당된 포트를 통해 Nginx 웹 서버에 접속하면 “Welcome to Nginx!” 페이지를 볼 수 있을 거예요. 🎉 정말 편리하지 않나요? 몇 번의 클릭만으로 쿠버네티스에 애플리케이션을 배포하는 워크플로우 자동화가 완성된 거죠.

    AWX 작업 성공 로그와 kubectl get pods/deployments 명령 결과 비교 화면, AWX의 성공적인 Task 완료와 쿠버네티스 배포 리소스 확인

    AWX 작업 성공 로그와 kubectl get pods/deployments 명령 결과 비교 화면. AWX의 Job Output에서 Task가 성공적으로 완료된 것을 보여주고, 터미널에서 kubectl 명령어로 배포된 리소스들을 확인하는 모습을 나란히 보여줍니다.

    마무리하며: 자동화, 멈추지 않는 여정

    이번 AWX와 쿠버네티스 연동 사례를 통해 워크플로우 자동화가 얼마나 강력한지 다시 한번 느꼈어요. 단순한 배포를 넘어, IaC(Infrastructure as Code, 코드형 인프라)의 개념을 실현하고, DevOps 파이프라인의 핵심 구성 요소로 활용할 수 있다는 것이 핵심이죠.

    인프라 엔지니어로서 제가 얻은 가장 큰 교훈은 “반복되는 작업은 무조건 자동화하라”는 거예요. 처음엔 자동화 스크립트 작성에 시간이 들지언정, 장기적으로는 시간과 노력을 아끼고, 휴먼 에러를 줄여준다는 걸 뼈저리게 경험했거든요. 이 경험이 여러분의 인프라 관리에도 작은 영감이 되기를 바랍니다.

    다음 글에서는 이 워크플로우를 확장해서 Ingress 컨트롤러를 배포하고, 외부 도메인으로 서비스에 접근하는 방법을 다뤄볼까 해요. 기대해주세요!

    AWX, Ansible, Kubernetes, DevOps 로고들이 원형으로 배치된 인포그래픽. 각 로고들이 서로 연결되어 워크플로우 자동화를 이루는 모습을 시각적으로 보여줍니다.

  • [k8s] ArgoCD로 멀티 클러스터 GitOps 배포 자동화 완벽 가이드

    [k8s] ArgoCD로 멀티 클러스터 GitOps 배포 자동화 완벽 가이드

    ArgoCD로 멀티 클러스터 GitOps 배포 자동화 완벽 가이드

    안녕하세요, 13년차 서버실 지킴이입니다. 오늘도 어김없이 여러분의 인프라 여정에 도움이 될 만한 꿀팁을 들고 왔습니다. 혹시 여러 쿠버네티스 클러스터를 운영하면서 배포 때문에 머리 아팠던 경험 있으신가요? 개발, 스테이징, 운영 환경이 각각 다른 클러스터에 존재하거나, 온프레미스와 클라우드를 넘나드는 하이브리드 환경에서 일관된 배포를 유지하는 게 정말 쉽지 않거든요. 저도 처음엔 수동 배포 지옥에서 헤매던 시절이 있었죠. 클러스터마다 kubectl 명령어를 치고, YAML 파일을 수정하고, 휴먼 에러로 인해 서비스 장애를 겪었던 아찔한 순간들도 많았고요. 😱

    하지만 GitOps(깃옵스)를 만나고, 특히 ArgoCD(아르고CD)를 활용하면서 이런 배포 스트레스가 확 줄었습니다. 오늘은 저처럼 멀티 클러스터 환경에서 배포 자동화에 목마른 분들을 위해, ArgoCD로 GitOps를 구현하고 여러 클러스터에 애플리케이션을 자동으로 배포하는 멀티 클러스터 GitOps 배포 자동화 방법을 완벽하게 가이드해 드리려고 합니다. 제가 직접 삽질하며 터득한 경험들을 솔직하게 공유할 테니, 끝까지 따라오시면 분명 큰 도움이 될 겁니다! 자, 그럼 시작해 볼까요?

    ArgoCD를 활용한 멀티 클러스터 GitOps 배포 아키텍처는 위와 같이 구성될 수 있습니다. 하나의 ArgoCD 인스턴스가 여러 쿠버네티스 클러스터에 애플리케이션을 배포하고 관리하는 모습이죠. Git을 중심으로 모든 클러스터의 상태를 동기화하는 핵심 아이디어를 담고 있습니다.

    1. GitOps와 ArgoCD, 그리고 멀티 클러스터 배포, 이게 뭔가요?

    본격적인 실전에 앞서, 핵심 개념들을 짚고 넘어가는 게 중요하겠죠? 제가 늘 강조하는 부분인데, 도구를 잘 쓰는 것도 중요하지만, 그 도구가 왜 필요하고 어떤 철학을 담고 있는지 이해하는 게 진짜 실력입니다.

    • GitOps (깃옵스): 쉽게 말해, Git을 유일한 진실의 원천(Single Source of Truth)으로 삼아 인프라와 애플리케이션 배포를 자동화하는 운영 방식이에요. 모든 설정과 배포 상태를 Git 리포지토리에 코드로 관리하고, Git에 변경사항이 푸시되면 자동으로 인프라에 반영되도록 하는 거죠. 개발자들이 코드 관리하듯이 인프라를 관리한다고 생각하시면 됩니다. 일관성, 버전 관리, 감사 추적(Audit Trail)이 가능하다는 엄청난 장점이 있어요.

    • ArgoCD (아르고CD): 이 친구는 GitOps를 쿠버네티스(Kubernetes) 환경에서 구현해주는 선언적(Declarative) GitOps 지속적 배포(Continuous Delivery) 툴입니다. Git 리포지토리에 정의된 원하는 상태(Desired State)와 실제 쿠버네티스 클러스터의 현재 상태(Live State)를 끊임없이 비교하고, 만약 다르면 Git에 정의된 상태로 클러스터를 동기화(Sync)시켜 줍니다. 마치 클러스터 상태를 감시하는 파수꾼 같다고 할까요? 정말 든든한 친구더라고요.

    • 멀티 클러스터 배포 (Multi-Cluster Deployment): 여러 개의 쿠버네티스 클러스터에 동일하거나 다른 애플리케이션을 배포하고 관리하는 시나리오를 말합니다. 개발, 스테이징, 운영 환경을 분리하거나, 재해 복구(DR)를 위해 지리적으로 분산된 클러스터를 운영할 때 주로 사용하죠. ArgoCD는 하나의 중앙 인스턴스에서 여러 클러스터를 관리할 수 있어서, 멀티 클러스터 환경에서 배포를 중앙 집중화하고 자동화하는 데 아주 탁월한 솔루션입니다.

    2. 실전 구현: ArgoCD로 멀티 클러스터 GitOps 환경 구축하기

    자, 이제 이론은 충분히 봤으니, 저와 함께 직접 만들어 볼 시간입니다. 제가 홈랩에서 여러 번 시도하면서 가장 효율적이라고 생각했던 방법으로 안내해 드릴게요. 따라만 하시면 됩니다! 💡

    2.1. 사전 준비물 (Prerequisites)

    시작하기 전에 몇 가지 준비물이 필요합니다.

    • 쿠버네티스 클러스터 2개 이상: 하나는 ArgoCD를 설치할 ‘컨트롤 플레인 클러스터’로, 나머지는 애플리케이션을 배포할 ‘타겟 클러스터’로 사용할 겁니다. (저는 KIND나 K3s로 쉽게 구성했어요.)
    • kubectl: 쿠버네티스 클러스터를 제어하는 CLI 도구.
    • Helm (선택 사항): 쿠버네티스 패키지 매니저. ArgoCD 설치에 Helm을 사용하진 않지만, 애플리케이션 배포에 유용할 수 있습니다.
    • Git 리포지토리: 배포할 애플리케이션의 YAML 파일들을 저장할 공간 (GitHub, GitLab, Bitbucket 등).

    2.2. 단계 1: ArgoCD 설치 (컨트롤 플레인 클러스터)

    먼저 ArgoCD를 설치할 클러스터에 ArgoCD를 배포합니다. 이 클러스터가 모든 배포의 중앙 통제실 역할을 하게 됩니다.

    1. ArgoCD 네임스페이스 생성

      kubectl create namespace argocd
    2. ArgoCD 설치 YAML 적용

      kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

      이 명령은 ArgoCD의 모든 컴포넌트(API 서버, 컨트롤러, 레포 서버 등)를 설치합니다. 잠시 기다리면 파드들이 정상적으로 올라올 거예요.

    3. ArgoCD CLI 설치 (선택 사항이지만 강력 추천!)

      # macOS (Homebrew)
      brew install argocd
      
      # Linux (직접 다운로드)
      curl -sSL -o argocd-linux-amd64 https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
      sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd
      rm argocd-linux-amd64
    4. 초기 비밀번호 확인 및 UI 접근

      ArgoCD API 서버의 초기 비밀번호는 컨트롤 플레인 클러스터의 argocd-initial-admin-secret 시크릿에 저장되어 있습니다.

      kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d; echo

      UI에 접근하려면 포트 포워딩(Port Forwarding)을 해줘야 합니다.

      kubectl port-forward svc/argocd-server -n argocd 8080:443

      이제 웹 브라우저에서 https://localhost:8080으로 접속한 후, 사용자명 admin과 위에서 확인한 비밀번호로 로그인하세요. 첫 로그인 후 비밀번호를 변경하는 것을 추천합니다.

    2.3. 단계 2: Git Repository 준비

    배포할 애플리케이션의 YAML 파일들을 Git 리포지토리에 준비해야 합니다. 저는 간단한 Nginx 배포 파일을 예시로 들어볼게요.

    my-gitops-repo/apps/nginx/deployment.yaml

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: nginx-deployment
      labels:
        app: nginx
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: nginx
      template:
        metadata:
          labels:
            app: nginx
        spec:
          containers:
          - name: nginx
            image: nginx:1.14.2
            ports:
            - containerPort: 80

    my-gitops-repo/apps/nginx/service.yaml

    apiVersion: v1
    kind: Service
    metadata:
      name: nginx-service
    spec:
      selector:
        app: nginx
      ports:
        - protocol: TCP
          port: 80
          targetPort: 80
      type: LoadBalancer # 또는 ClusterIP, NodePort

    이 파일들을 Git 리포지토리에 푸시해 주세요. (예: https://github.com/your-org/my-gitops-repo.git)

    2.4. 단계 3: 타겟 클러스터 등록 (ArgoCD에 연결)

    이제 ArgoCD가 애플리케이션을 배포할 타겟 클러스터들을 ArgoCD에 등록해야 합니다. ArgoCD CLI를 사용하면 아주 쉽게 등록할 수 있어요.

    1. ArgoCD CLI 로그인

      argocd login localhost:8080

      초기 비밀번호로 로그인합니다.

    2. 타겟 클러스터 등록

      현재 kubeconfig 파일에 정의된 클러스터 컨텍스트(Context)를 사용해서 등록합니다. 등록할 클러스터의 컨텍스트로 kubectl config use-context <TARGET_CLUSTER_CONTEXT> 명령어를 실행한 후, 아래 명령어를 실행하세요.

      argocd cluster add <TARGET_CLUSTER_CONTEXT>

      예를 들어, dev-cluster와 prod-cluster라는 컨텍스트가 있다면 각각 등록해 줍니다.

      이 명령은 ArgoCD가 타겟 클러스터에 접근할 수 있도록 필요한 RBAC 설정과 시크릿을 자동으로 생성해 줍니다. ⚠️ 권한 문제로 많이들 삽질하시는데, 이 단계에서 정확한 컨텍스트를 선택했는지 꼭 확인하세요.

    3. 등록된 클러스터 확인

      argocd cluster list

      등록된 클러스터 목록이 보이면 성공입니다! ArgoCD UI에서도 ‘Clusters’ 메뉴에서 확인할 수 있습니다.

    ArgoCD UI에서 클러스터가 성공적으로 등록되고 ApplicationSet이 여러 클러스터에 배포되는 모습을 시각적으로 확인할 수 있습니다. 각 클러스터별로 애플리케이션 상태를 한눈에 파악할 수 있죠.

    2.5. 단계 4: ApplicationSet으로 멀티 클러스터 배포 자동화

    이제 이 글의 핵심인 ApplicationSet(애플리케이션셋)을 활용해서 여러 클러스터에 Nginx 애플리케이션을 배포해 볼 시간입니다. ApplicationSet은 여러 개의 ArgoCD Application 리소스를 동적으로 생성해주는 컨트롤러입니다. 특히 멀티 클러스터 환경에서 빛을 발하죠!

    ApplicationSet을 사용하면, 등록된 모든 클러스터에 동일한 애플리케이션을 배포하거나, 클러스터별로 약간 다른 설정을 적용하여 배포할 수 있습니다. 여기서는 Cluster Generator를 사용해서 ArgoCD에 등록된 모든 클러스터에 Nginx를 배포하도록 해볼게요.

    my-gitops-repo/applicationsets/nginx-applicationset.yaml

    apiVersion: argoproj.io/v1alpha1
    kind: ApplicationSet
    metadata:
      name: multi-cluster-nginx
      namespace: argocd
    spec:
      generators:
      - clusters: {}
      template:
        metadata:
          name: '{{name}}-nginx' # 클러스터 이름 기반으로 Application 이름 생성
          namespace: argocd
        spec:
          project: default
          source:
            repoURL: https://github.com/your-org/my-gitops-repo.git # 본인의 Git 리포지토리 URL로 변경
            targetRevision: HEAD
            path: apps/nginx
          destination:
            server: '{{server}}'
            namespace: default
          syncPolicy:
            automated:
              prune: true
              selfHeal: true
            syncOptions:
              - CreateNamespace=true # 대상 클러스터에 네임스페이스가 없으면 생성

    위 YAML 파일을 Git 리포지토리에 푸시한 후, ArgoCD가 설치된 컨트롤 플레인 클러스터에 적용합니다.

    kubectl apply -n argocd -f my-gitops-repo/applicationsets/nginx-applicationset.yaml

    잠시 후 ArgoCD UI의 ‘Applications’ 메뉴로 이동해 보세요. 등록된 클러스터 개수만큼 <클러스터이름>-nginx 형태의 애플리케이션이 자동으로 생성되고, 배포가 시작되는 것을 확인할 수 있을 겁니다. 정말 신기하더라고요! 🎉

    3. 주의사항 및 트러블슈팅: 저의 삽질 경험담 ⚠️

    제가 13년간 인프라 엔지니어로 일하면서 깨달은 건, 새로운 기술을 도입할 때 늘 예상치 못한 문제가 터진다는 겁니다. ArgoCD도 예외는 아니었죠. 제가 겪었던 주요 삽질 경험과 해결 팁을 공유해 드립니다.

    • RBAC (Role-Based Access Control) 문제: 타겟 클러스터 등록 시 ArgoCD가 해당 클러스터에 접근할 권한이 없어서 배포가 실패하는 경우가 많았습니다. argocd cluster add 명령이 자동으로 RBAC를 설정해주지만, 때로는 특정 리소스에 대한 추가 권한이 필요할 수 있어요. ArgoCD 서버의 서비스 어카운트(Service Account)에 필요한 ClusterRoleBinding이 제대로 되어있는지 확인하고, 필요하다면 수동으로 추가해줘야 합니다.

      # 예시: ArgoCD 서비스 어카운트에 Cluster-Admin 권한 부여 (실제 운영에서는 최소 권한 원칙 준수)
      apiVersion: rbac.authorization.k8s.io/v1
      kind: ClusterRoleBinding
      metadata:
        name: argocd-manager-role-binding
      subjects:
      - kind: ServiceAccount
        name: argocd-argocd-application-controller
        namespace: argocd
      roleRef:
        apiGroup: rbac.authorization.k8s.io
        kind: ClusterRole
        name: cluster-admin # or a more specific role
    • Kubeconfig 파일 접근 권한: ArgoCD가 타겟 클러스터에 접근하려면 컨트롤 플레인 클러스터에서 타겟 클러스터의 API 서버에 네트워크적으로 연결이 가능해야 합니다. 방화벽, VPC 설정 등을 꼼꼼히 확인하세요. 특히 온프레미스와 클라우드 하이브리드 환경에서는 네트워크 경로 설정이 복잡해질 수 있습니다.

    • Sync Policy 설정: automated: prune: true와 selfHeal: true 옵션은 배포를 매우 편리하게 해주지만, 자칫 잘못하면 예상치 못한 변경이 즉시 반영될 수 있습니다. 특히 운영 환경에서는 신중하게 접근하고, 처음에는 수동 동기화(Manual Sync)로 시작하여 동작을 충분히 이해한 후 자동화하는 것을 추천합니다.

    • ApplicationSet Generator 문제: Cluster Generator를 쓸 때, Git 리포지토리의 경로(path)나 targetRevision, 그리고 destination의 server와 namespace 설정에 오타가 없는지 여러 번 확인해야 합니다. 변수({{name}}, {{server}}) 사용법도 헷갈리기 쉽더라고요.

    4. 검증 및 결과: 드디어 됐다! ✅

    이제 ArgoCD UI에서 모든 애플리케이션들이 Synced 상태인지 확인해 보세요. 각 클러스터에 배포된 Nginx 파드들도 kubectl get pods -n default 명령으로 확인하면 정상적으로 실행되고 있을 겁니다. 드디어 모든 클러스터에 배포가 완료됐네요! 이 맛에 GitOps 하는 거 아니겠습니까? 🎉

    여기서 끝이 아닙니다! GitOps의 진정한 가치는 Git 변경 사항이 자동으로 반영되는 데 있거든요. my-gitops-repo/apps/nginx/deployment.yaml 파일의 replicas 수를 2에서 3으로 변경하고 Git에 푸시해 보세요. ArgoCD가 변경 사항을 감지하고, 몇 초 내에 모든 타겟 클러스터의 Nginx 파드 개수가 3개로 자동으로 업데이트되는 것을 볼 수 있을 겁니다. 정말 편하더라고요!

    ArgoCD 대시보드에서 여러 클러스터에 배포된 애플리케이션들의 실시간 상태를 모니터링하는 화면입니다. 모든 애플리케이션이 정상적으로 동기화(Synced)된 것을 확인할 수 있습니다.

    5. 마무리: GitOps로 한 단계 더 성장하기

    오늘은 ArgoCD를 활용하여 멀티 클러스터 GitOps 배포 자동화를 구현하는 과정을 저의 경험을 바탕으로 상세하게 설명해 드렸습니다. 어떠셨나요? 처음엔 복잡해 보이지만, 한 번 구축해 놓으면 배포의 일관성, 속도, 안정성이 비약적으로 향상되는 것을 체감하실 수 있을 겁니다.

    멀티 클러스터 환경에서 ArgoCD와 GitOps는 정말 강력한 조합입니다. 수동 작업에서 벗어나 휴먼 에러를 줄이고, Git을 통한 완벽한 버전 관리와 감사 추적이 가능해지거든요. 무엇보다 인프라 엔지니어가 더 중요하고 전략적인 업무에 집중할 수 있도록 도와주는 멋진 도구라고 생각합니다.

    물론 초기 설정에 시간과 노력이 필요하고, GitOps 문화에 적응하는 과정도 필요합니다. 하지만 그 투자 가치는 충분하다고 제가 직접 보증합니다. 제 홈랩에서도 이 방식으로 다양한 서비스를 배포하고 있거든요. 😉

    다음번에는 오늘 배운 ArgoCD와 GitOps 개념을 확장해서, Helm Chart 통합, Kustomize 활용, 그리고 Argo Rollouts(아르고 롤아웃)를 이용한 블루/그린, 카나리 배포 전략까지 자동화하는 경험담을 들려드릴게요! 이 글이 여러분의 인프라 여정에 작은 이정표가 되었기를 바랍니다. 다음에 또 만나요!

    GitOps와 기존 배포 방식, 그리고 ArgoCD의 멀티 클러스터 관리 장점을 요약한 인포그래픽입니다. 어떤 점들이 개선되었는지 한눈에 파악할 수 있습니다.

  • [k8s] ArgoCD GitOps CI/CD 구축: 쿠버네티스 자동 배포 완벽 가이드

    배포가 두려운 분들께 — GitOps가 바꿔놓은 제 일상

    솔직히 말씀드리면, 저도 예전에는 배포가 제일 무서웠어요. 스테이징에서는 멀쩡하던 게 프로덕션에 올리면 왜인지 모르게 터지고, “누가 언제 뭘 바꿨지?” 하고 kubectl로 이것저것 뒤지다 보면 어느새 새벽 2시가 되어 있는 그런 날들이요. 쿠버네티스를 도입하고 나서도 한동안은 이 상황이 크게 달라지지 않았습니다. 오히려 배포 방법이 늘어나면서 혼란이 더 심해지기도 했고요.

    그러다가 ArgoCD를 알게 됐습니다. 처음엔 “또 새로운 툴이네” 하고 반신반의했는데, 막상 써보니까 진짜 다르더라고요. GitOps 방식으로 쿠버네티스 배포를 자동화하면, Git 저장소가 클러스터의 “진실의 원천(Source of Truth)”이 됩니다. 코드를 푸시하면 ArgoCD가 알아서 클러스터 상태를 맞춰주는 거예요. 오늘은 제가 홈랩과 실무에서 직접 구축하면서 겪은 경험을 바탕으로, ArgoCD를 활용한 CI/CD 파이프라인 구축 방법을 처음부터 끝까지 같이 살펴보겠습니다.

    ArgoCD GitOps 전체 아키텍처 — Git 저장소 변경이 쿠버네티스 클러스터에 자동 반영되는 흐름

    GitOps와 ArgoCD, 쉽게 이해하기

    GitOps란 뭔가요?

    GitOps를 한 문장으로 정리하면 이렇습니다. “인프라와 애플리케이션의 원하는 상태(Desired State)를 Git에 선언적으로 저장하고, 자동화 도구가 실제 상태를 그것에 맞게 유지하는 방식”이에요.

    쉽게 말해서, 예전에는 “지금 클러스터에 뭐가 떠 있지?”를 확인하려면 kubectl get 명령어를 직접 쳐봐야 했잖아요. GitOps를 쓰면 Git 저장소만 봐도 현재 클러스터 상태를 알 수 있어요. 변경 이력도 다 남고, 롤백도 git revert 한 방이면 되고요.

    기존 CI/CD 방식과 무엇이 다른가요?

    구분 기존 Push 방식 CI/CD GitOps (ArgoCD Pull 방식)
    배포 트리거 CI 파이프라인이 kubectl apply 직접 실행 ArgoCD가 Git 변경 감지 후 자동 동기화
    클러스터 접근 권한 CI 서버가 클러스터 자격증명 보유 ArgoCD만 클러스터 내부에서 관리
    상태 추적 파이프라인 로그 확인 필요 Git 커밋 히스토리 = 배포 이력
    드리프트(Drift) 감지 수동 확인 필요 ArgoCD가 실시간 감지 및 알림
    롤백 파이프라인 재실행 or 수동 Git revert + 자동 동기화

    특히 드리프트(Drift) 감지가 저한테는 정말 킬러 피처였어요. 누군가 실수로 kubectl로 직접 설정을 바꿔놔도, ArgoCD가 “어? Git이랑 다른데?” 하고 바로 잡아주거든요. 이게 얼마나 안심되는지 모릅니다.

    ArgoCD 설치 — 생각보다 간단합니다

    사전 준비

    • 쿠버네티스 클러스터 (v1.19 이상 권장)
    • kubectl 설치 및 클러스터 접근 설정 완료
    • Git 저장소 (GitHub, GitLab, Bitbucket 모두 가능)
    • ArgoCD CLI (선택사항이지만 있으면 편해요)

    1단계: ArgoCD 네임스페이스 및 설치

    ArgoCD 공식 설치 방법은 정말 간단합니다. 공식 매니페스트를 그대로 적용하면 되거든요.

    # ArgoCD 전용 네임스페이스 생성
    kubectl create namespace argocd
    
    # ArgoCD 공식 매니페스트 적용
    kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
    
    # 설치 완료 확인 (모든 Pod가 Running 상태가 될 때까지 대기)
    kubectl get pods -n argocd -w

    처음에 저도 이게 너무 간단해서 “이게 맞나?” 싶었는데, 맞습니다 ㅎㅎ. 잠시 기다리면 아래와 같이 Pod들이 뜨기 시작해요.

    NAME                                                READY   STATUS    RESTARTS   AGE
    argocd-application-controller-0                     1/1     Running   0          2m
    argocd-applicationset-controller-xxx                1/1     Running   0          2m
    argocd-dex-server-xxx                               1/1     Running   0          2m
    argocd-notifications-controller-xxx                 1/1     Running   0          2m
    argocd-redis-xxx                                    1/1     Running   0          2m
    argocd-repo-server-xxx                              1/1     Running   0          2m
    argocd-server-xxx                                   1/1     Running   0          2m

    2단계: ArgoCD UI 접근 설정

    기본적으로 ArgoCD 서버는 외부에 노출되어 있지 않아요. 로컬에서 테스트할 때는 포트 포워딩을 쓰고, 실제 운영 환경에서는 Ingress를 설정하는 게 좋습니다.

    # 로컬 테스트용 포트 포워딩
    kubectl port-forward svc/argocd-server -n argocd 8080:443
    
    # 초기 admin 비밀번호 확인
    kubectl -n argocd get secret argocd-initial-admin-secret \
      -o jsonpath="{.data.password}" | base64 -d; echo

    💡 팁: 초기 비밀번호는 반드시 첫 로그인 후 바꿔주세요. 그리고 argocd-initial-admin-secret은 비밀번호 변경 후 삭제하는 게 보안상 좋습니다.

    3단계: ArgoCD CLI 설치 (선택 권장)

    # macOS
    brew install argocd
    
    # Linux
    curl -sSL -o /usr/local/bin/argocd \
      https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
    chmod +x /usr/local/bin/argocd
    
    # CLI로 ArgoCD 서버에 로그인
    argocd login localhost:8080 --username admin --password <위에서-확인한-비밀번호> --insecure

    ArgoCD 웹 UI 대시보드 — 애플리케이션 동기화 상태와 헬스 체크 결과를 한눈에 확인

    첫 번째 Application 등록 — 실전 GitOps 시작

    Git 저장소 구조 준비

    ArgoCD를 쓸 때 저장소 구조를 어떻게 잡느냐가 은근히 중요하더라고요. 저는 앱 코드와 쿠버네티스 매니페스트를 분리하는 방식을 선호합니다.

    my-gitops-repo/
    ├── apps/
    │   ├── dev/
    │   │   ├── deployment.yaml
    │   │   ├── service.yaml
    │   │   └── kustomization.yaml
    │   └── prod/
    │       ├── deployment.yaml
    │       ├── service.yaml
    │       └── kustomization.yaml
    └── README.md

    예시로 사용할 간단한 Deployment와 Service 매니페스트입니다.

    # apps/dev/deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: sample-app
      namespace: default
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: sample-app
      template:
        metadata:
          labels:
            app: sample-app
        spec:
          containers:
          - name: sample-app
            image: nginx:1.25
            ports:
            - containerPort: 80
            resources:
              requests:
                cpu: 100m
                memory: 128Mi
              limits:
                cpu: 200m
                memory: 256Mi
    # apps/dev/service.yaml
    apiVersion: v1
    kind: Service
    metadata:
      name: sample-app-svc
      namespace: default
    spec:
      selector:
        app: sample-app
      ports:
      - protocol: TCP
        port: 80
        targetPort: 80
      type: ClusterIP

    ArgoCD Application 생성

    이제 핵심입니다. ArgoCD에게 “이 Git 저장소의 이 경로를 이 클러스터에 배포해줘”라고 알려주는 Application 리소스를 만들어야 해요. YAML로 선언적으로 만드는 걸 추천합니다 — 이것 자체도 Git으로 관리하면 더 좋고요.

    # argocd-application.yaml
    apiVersion: argoproj.io/v1alpha1
    kind: Application
    metadata:
      name: sample-app-dev
      namespace: argocd
    spec:
      project: default
      source:
        repoURL: https://github.com/your-username/my-gitops-repo.git
        targetRevision: HEAD
        path: apps/dev
      destination:
        server: https://kubernetes.default.svc
        namespace: default
      syncPolicy:
        automated:
          prune: true        # Git에서 삭제된 리소스 자동 제거
          selfHeal: true     # 드리프트 감지 시 자동 복구
        syncOptions:
        - CreateNamespace=true
    # Application 생성
    kubectl apply -f argocd-application.yaml
    
    # 동기화 상태 확인
    argocd app get sample-app-dev
    
    # 수동 동기화 (처음 한 번)
    argocd app sync sample-app-dev

    여기서 selfHeal: true 옵션이 제가 아까 말씀드린 드리프트 자동 복구 기능이에요. 누군가 실수로 kubectl로 직접 replica 수를 바꿔도, ArgoCD가 Git에 선언된 값으로 되돌려줍니다. 처음에 이게 동작하는 걸 보고 “오오…” 했던 기억이 나네요.

    Private 저장소 연결

    실무에서는 당연히 Private 저장소를 쓰죠. SSH 키나 Personal Access Token으로 연결할 수 있어요.

    # HTTPS + Personal Access Token 방식
    argocd repo add https://github.com/your-username/my-gitops-repo.git \
      --username your-username \
      --password your-personal-access-token
    
    # SSH 키 방식
    argocd repo add [email protected]:your-username/my-gitops-repo.git \
      --ssh-private-key-path ~/.ssh/id_rsa

    CI 파이프라인과 연동 — 이미지 태그 자동 업데이트

    ArgoCD는 CD(Continuous Delivery) 도구입니다. CI(Continuous Integration)는 별도 도구를 써야 해요. 저는 GitHub Actions를 주로 쓰는데, 흐름은 이렇습니다.

    1. 개발자가 앱 코드를 main 브랜치에 push
    2. GitHub Actions가 Docker 이미지를 빌드하고 레지스트리에 push
    3. GitHub Actions가 GitOps 저장소의 이미지 태그를 새 버전으로 업데이트
    4. ArgoCD가 GitOps 저장소 변경을 감지하고 자동으로 클러스터에 배포
    # .github/workflows/deploy.yaml
    name: Build and Update GitOps Repo
    
    on:
      push:
        branches: [main]
    
    jobs:
      build-and-push:
        runs-on: ubuntu-latest
        steps:
        - name: Checkout app code
          uses: actions/checkout@v3
    
        - name: Set up Docker Buildx
          uses: docker/setup-buildx-action@v2
    
        - name: Login to Container Registry
          uses: docker/login-action@v2
          with:
            registry: ghcr.io
            username: ${{ github.actor }}
            password: ${{ secrets.GITHUB_TOKEN }}
    
        - name: Build and push
          uses: docker/build-push-action@v4
          with:
            push: true
            tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
    
        - name: Update GitOps repo image tag
          run: |
            git clone https://x-access-token:${{ secrets.GITOPS_TOKEN }}@github.com/your-username/my-gitops-repo.git
            cd my-gitops-repo
            # sed로 이미지 태그 업데이트
            sed -i "s|image: ghcr.io/your-username/sample-app:.*|image: ghcr.io/your-username/sample-app:${{ github.sha }}|"\
              apps/dev/deployment.yaml
            git config user.email "[email protected]"
            git config user.name "CI Bot"
            git add apps/dev/deployment.yaml
            git commit -m "chore: update image tag to ${{ github.sha }}"
            git push

    근데 여기서 주의할 점이 있어요. 앱 코드 저장소와 GitOps(매니페스트) 저장소를 분리하는 게 좋습니다. 같은 저장소에 두면 앱 코드 변경과 인프라 변경 이력이 섞여서 나중에 관리가 복잡해지더라고요. 처음에 같이 뒀다가 나중에 분리하는 삽질을 했었는데… 처음부터 분리하세요 ㅎㅎ.

    CI/CD 전체 파이프라인 흐름 — GitHub Actions가 이미지를 빌드하고 GitOps 저장소를 업데이트하면 ArgoCD가 자동 배포

    ⚠️ 삽질 모음 — 이것만 알면 시간 아낍니다

    1. OutOfSync 상태가 계속 유지되는 문제

    분명히 Git이랑 같은 내용인데 ArgoCD에서 계속 OutOfSync라고 뜨는 경우가 있어요. 대부분 리소스 정규화(normalization) 문제입니다. 쿠버네티스가 자동으로 추가하는 필드들(creationTimestamp, status 등)이 Git에 없는 거예요.

    # Application에 ignoreDifferences 추가로 해결
    spec:
      ignoreDifferences:
      - group: apps
        kind: Deployment
        jsonPointers:
        - /spec/replicas  # HPA가 관리하는 경우 replica 수 무시
      - group: ""
        kind: Service
        jsonPointers:
        - /spec/clusterIP

    2. Helm 차트 사용 시 values 파일 관리

    Helm을 ArgoCD와 함께 쓸 때 values 파일을 Git에 두면 됩니다.

    spec:
      source:
        repoURL: https://github.com/your-username/my-gitops-repo.git
        path: charts/my-app
        helm:
          valueFiles:
          - values-dev.yaml
          - values-secrets.yaml  # Sealed Secrets 등으로 암호화된 파일

    3. 시크릿(Secret) 관리 — 절대 Git에 평문으로 넣지 마세요

    이건 정말 중요해요. DB 비밀번호 같은 민감 정보를 실수로 Git에 넣는 사고가 생각보다 많이 일어납니다. 저는 Sealed Secrets나 External Secrets Operator를 씁니다.

    • Sealed Secrets: 공개키로 암호화된 SealedSecret 리소스를 Git에 저장, 클러스터 내 컨트롤러가 복호화
    • External Secrets Operator: AWS Secrets Manager, HashiCorp Vault 등 외부 시크릿 저장소와 연동

    4. ArgoCD 자체도 GitOps로 관리하기 — App of Apps 패턴

    ArgoCD Application 리소스가 많아지면 관리가 힘들어져요. 이럴 때 App of Apps 패턴을 씁니다. 루트 Application 하나가 다른 Application들을 관리하는 계층 구조예요.

    # root-application.yaml — 다른 Application들을 관리하는 루트
    apiVersion: argoproj.io/v1alpha1
    kind: Application
    metadata:
      name: root-app
      namespace: argocd
    spec:
      project: default
      source:
        repoURL: https://github.com/your-username/my-gitops-repo.git
        targetRevision: HEAD
        path: argocd-apps  # 이 폴더 안에 다른 Application YAML들이 있음
      destination:
        server: https://kubernetes.default.svc
        namespace: argocd
      syncPolicy:
        automated:
          prune: true
          selfHeal: true

    ✅ 배포 결과 확인 및 검증

    모든 설정이 끝났으면 이렇게 확인하시면 됩니다.

    # ArgoCD CLI로 앱 상태 확인
    argocd app list
    
    # 상세 상태 확인
    argocd app get sample-app-dev
    
    # 동기화 이력 확인
    argocd app history sample-app-dev
    
    # kubectl로 직접 확인
    kubectl get pods -n default
    kubectl get svc -n default

    ArgoCD UI에서 애플리케이션 상태가 Synced / Healthy로 표시되면 성공입니다. 🎉

    이제 Git 저장소에서 deployment.yaml의 이미지 태그만 바꿔서 커밋하면, 몇 분 안에 (기본 polling 주기는 3분) 클러스터에 자동으로 반영되는 걸 볼 수 있어요. 처음에 이게 자동으로 되는 걸 보고 “드디어 됐다!” 하고 혼자 좋아했던 기억이 납니다 ㅎㅎ.

    # 롤백도 이렇게 간단합니다
    argocd app rollback sample-app-dev 
    
    # 또는 Git에서 revert 후 push하면 ArgoCD가 자동으로 처리

    ArgoCD 애플리케이션 상세 화면 — Synced/Healthy 상태와 쿠버네티스 리소스 트리 시각화

    자주 묻는 질문 (FAQ)

    Q. ArgoCD는 무료인가요?

    네, ArgoCD는 오픈소스(Apache 2.0 라이선스)로 완전 무료입니다. CNCF(Cloud Native Computing Foundation) 졸업 프로젝트이기도 하고요.

    Q. Flux와 ArgoCD 중 어떤 걸 써야 하나요?

    둘 다 GitOps 도구이지만, ArgoCD는 UI가 풍부하고 직관적이라 팀에서 처음 GitOps를 도입할 때 진입 장벽이 낮습니다. Flux는 더 경량이고 CLI 친화적이에요. 저는 팀 협업 환경에서는 ArgoCD를, 단독 운영이나 자동화 중심이면 Flux를 추천합니다.

    Q. 여러 클러스터를 관리할 수 있나요?

    가능합니다. ArgoCD 하나로 여러 쿠버네티스 클러스터를 등록하고 관리할 수 있어요. argocd cluster add 명령어로 추가하면 됩니다.

    마무리 — GitOps, 한 번 맛보면 못 돌아갑니다

    ArgoCD와 GitOps를 도입하고 나서 제 일상이 꽤 달라졌어요. 배포가 무서운 이벤트에서 그냥 평범한 Git 커밋 하나로 바뀌었달까요. 팀원들도 “내가 뭘 배포했는지” Git 로그만 보면 바로 알 수 있으니 커뮤니케이션 비용도 줄었고요.

    물론 처음 셋업할 때 시크릿 관리나 멀티 클러스터 권한 설정 같은 부분에서 삽질이 좀 있긴 합니다. 근데 그 초기 투자를 하고 나면 이후에는 정말 편해요.

    오늘 다룬 내용을 정리하면 이렇습니다.

    • ✅ GitOps 개념과 기존 CI/CD 방식의 차이 이해
    • ✅ ArgoCD 설치 및 초기 설정
    • ✅ Application 리소스로 Git 저장소와 클러스터 연결
    • ✅ GitHub Actions와 연동한 완전 자동화 파이프라인 구축
    • ✅ 자주 겪는 문제와 해결법

    다음 글에서는 ArgoCD ApplicationSet을 활용해서 여러 환경(dev/staging/prod)을 템플릿 하나로 관리하는 방법을 다룰 예정입니다. App of Apps 패턴도 더 깊이 파볼 거고요. 이 글이 도움이 됐다면 댓글로 알려주세요! 궁금한 점도 편하게 물어봐 주시고요. 😊

  • [k8s] Flux CD GitOps 파이프라인 구축: 안정적인 쿠버네티스 배포 자동화

    [k8s] Flux CD GitOps 파이프라인 구축: 안정적인 쿠버네티스 배포 자동화

    안녕하세요, 13년차 서버실 지킴이입니다. 오늘은 많은 인프라 엔지니어분들이 한 번쯤은 고민해봤을 주제, 바로 쿠버네티스(Kubernetes) 배포 자동화에 대해 이야기해보려고 합니다. 특히 GitOps(깃옵스) 방법론과 그 구현체 중 하나인 Flux CD(플럭스 CD)를 활용한 파이프라인 구축 경험을 공유해 드릴게요.

    혹시 이런 경험 있으신가요? 개발팀에서 “배포해주세요!” 요청이 들어오면, kubectl apply -f 명령어를 조심스럽게 입력하면서, “혹시나 다른 설정이 덮어씌워지는 건 아닐까?”, “이전 버전으로 되돌리려면 어떻게 해야 하지?” 같은 걱정을 했던 순간 말이죠. 저는 셀 수 없이 많습니다. 수동 배포는 휴먼 에러의 온상이었고, 배포하다 새벽을 맞이하는 일도 비일비재했거든요. 이런 삽질을 거듭하다가 GitOps를 만나고 나서야 비로소 안정적인 배포의 빛을 보게 되었습니다. 제 홈랩에서도 여러 서비스를 GitOps로 관리하고 있는데, 정말 편하더라고요!

    GitOps 파이프라인은 Git 저장소를 ‘진리의 단일 출처(Single Source of Truth)’로 삼아, 쿠버네티스 클러스터의 상태를 Git에 선언된 대로 유지하는 방식입니다. 간단히 말해, Git에 저장된 설정 파일이 곧 클러스터의 현재 상태여야 한다는 거죠.

    GitOps와 Flux CD, 정말 좋은 이유

    처음 GitOps라는 개념을 들었을 때는 “어차피 Git에 YAML 파일 올리는 건 똑같은데, 뭐가 다르지?” 싶었어요. 근데 실제로 써보니 이 방식이 가진 장점이 정말 많더라고요. 제가 느낀 가장 큰 장점들을 꼽아보면 이렇습니다.

    • 일관성(Consistency): 모든 설정이 Git에 있으니, 개발, 스테이징, 운영 환경 간의 차이를 최소화할 수 있어요. “제 환경에서는 잘 되는데요?” 하는 말이 확 줄어듭니다.
    • 감사 및 추적(Auditability & Traceability): Git 커밋(Commit) 기록 자체가 변경 이력이 되니까요. 누가, 언제, 무엇을 변경했는지 명확하게 알 수 있죠. 문제 발생 시 롤백(Rollback)도 Git으로 너무나 쉽습니다.
    • 안정성(Reliability): Git에 선언된 상태와 클러스터의 실제 상태가 다르면, GitOps 에이전트가 자동으로 클러스터를 Git의 상태로 되돌립니다. 마치 자가 치유(Self-healing) 능력 같달까요?
    • 생산성(Productivity): 수동 작업이 줄어들고 자동화되면서, 엔지니어는 더 중요한 일에 집중할 수 있게 됩니다.

    이런 GitOps의 철학을 구현하는 대표적인 도구가 바로 Flux CD입니다. Flux CD는 쿠버네티스 클러스터 내에서 동작하는 컨트롤러(Controller)들의 집합인데요, 주기적으로 Git 저장소를 감시하다가 변경 사항이 감지되면 자동으로 클러스터에 적용해주는 역할을 합니다. 주요 컴포넌트로는 Source Controller(소스 컨트롤러), Kustomize Controller(커스터마이즈 컨트롤러), Helm Controller(헬름 컨트롤러) 등이 있습니다.

    Flux CD GitOps 파이프라인 구축 실전 가이드

    자, 그럼 이제 제 홈랩에서 Flux CD를 어떻게 구축했는지, 단계별로 자세히 알려드릴게요. 저도 처음엔 공식 문서를 보면서 여러 번 헤맸는데, 이 가이드가 여러분의 삽질 시간을 줄여주길 바랍니다!

    1단계: 사전 준비 및 Flux CLI 설치

    먼저, 쿠버네티스 클러스터에 접근 가능한 환경과 kubectl, git이 설치되어 있어야 합니다. 저는 로컬 PC에 flux CLI(Command Line Interface)를 설치하는 것부터 시작했어요.

    # Flux CLI 설치 (macOS 기준)
    brew install fluxcd/flux/flux
    
    # 설치 확인
    flux --version
    # flux version 2.x.x (최신 버전으로 설치됩니다)
    
    # 클러스터에 Flux CD가 설치될 네임스페이스 생성 (선택 사항)
    kubectl create namespace flux-system
    

    flux-system 네임스페이스는 Flux CD 컨트롤러들이 배포될 기본 공간입니다. 따로 지정하지 않으면 기본으로 이 네임스페이스에 설치되죠.

    2단계: Git 저장소 준비

    Flux CD는 Git 저장소를 바라보며 작동하기 때문에, 쿠버네티스 설정 파일(YAML)들을 저장할 Git 저장소가 필요합니다. 저는 GitHub에 gitops-k8s-homelab이라는 프라이빗(Private) 저장소를 만들었어요. 저장소 안에는 다음과 같은 구조로 파일을 구성할 예정입니다.

    gitops-k8s-homelab/
    ├── clusters/
    │   └── my-cluster/
    │       ├── flux-system/
    │       │   └── gotk-components.yaml
    │       │   └── gotk-sync.yaml
    │       └── apps/
    │           └── my-app/
    │               └── kustomization.yaml
    ├── apps/
    │   └── my-app/
    │       ├── deployment.yaml
    │       ├── service.yaml
    │       └── ingress.yaml
    └── README.md
    

    clusters/my-cluster/flux-system 경로에는 Flux CD 자체의 설정 파일들이, clusters/my-cluster/apps 경로에는 클러스터에 배포할 애플리케이션들을 정의하는 Kustomization 파일들이 들어갈 겁니다. 실제 애플리케이션 YAML 파일들은 apps/my-app 경로에 두고요.

    3단계: Flux CD 부트스트랩 (Bootstrap)

    이제 가장 중요한 단계입니다. flux bootstrap github 명령어를 사용해서 Flux CD를 쿠버네티스 클러스터에 설치하고, Git 저장소와 연결하는 작업을 수행합니다. 이 명령 한 방으로 Flux CD가 클러스터에 필요한 모든 컴포넌트를 배포하고, 지정된 Git 저장소를 바라보도록 설정해줍니다. 저는 GitHub를 사용했으니 github 옵션을 사용했어요.

    # 환경 변수 설정 (개인 GitHub 토큰과 사용자명으로 대체하세요!)
    export GITHUB_TOKEN="YOUR_GITHUB_TOKEN" # Personal Access Token
    export GITHUB_USER="YOUR_GITHUB_USERNAME"
    
    # Flux CD 부트스트랩 실행
    flux bootstrap github \
      --owner=${GITHUB_USER} \
      --repository=gitops-k8s-homelab \
      --branch=main \
      --path=clusters/my-cluster \
      --personal
    

    이 명령을 실행하면 Flux CD가 클러스터에 설치되고, clusters/my-cluster 경로를 기준으로 Git 저장소의 내용을 동기화하기 시작합니다. --personal 옵션은 개인 저장소에 주로 사용하고, 조직(Organization) 저장소의 경우 --owner를 조직명으로 지정하고 SSH 키 방식으로 인증하는 것이 일반적입니다. 저는 홈랩이라 개인 토큰으로 편하게 작업했어요.

    부트스트랩이 완료되면, Git 저장소의 clusters/my-cluster/flux-system 경로에 Flux CD 자체의 Kustomization 파일들이 자동으로 생성되어 커밋되는 것을 볼 수 있습니다. 이게 바로 Flux CD가 스스로를 GitOps 방식으로 관리하는 모습이죠. 신기하더라고요!

    4단계: 애플리케이션 배포를 위한 Kustomization 정의

    이제 클러스터에 배포할 애플리케이션을 정의하고 Flux CD가 이를 인식하도록 설정할 차례입니다. 먼저, apps/my-app 경로에 간단한 NGINX 애플리케이션을 정의하는 YAML 파일들을 만듭니다.

    # apps/my-app/deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: my-nginx-app
      labels:
        app: my-nginx-app
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: my-nginx-app
      template:
        metadata:
          labels:
            app: my-nginx-app
        spec:
          containers:
          - name: nginx
            image: nginx:latest
            ports:
            - containerPort: 80
    ---
    # apps/my-app/service.yaml
    apiVersion: v1
    kind: Service
    metadata:
      name: my-nginx-app-service
    spec:
      selector:
        app: my-nginx-app
      ports:
      - protocol: TCP
        port: 80
        targetPort: 80
      type: ClusterIP
    

    그리고 이 애플리케이션을 클러스터에 배포하도록 지시하는 Kustomization 파일을 clusters/my-cluster/apps/my-app/kustomization.yaml 경로에 생성합니다.

    # clusters/my-cluster/apps/my-app/kustomization.yaml
    apiVersion: kustomize.toolkit.fluxcd.io/v1
    kind: Kustomization
    metadata:
      name: my-nginx-app
      namespace: flux-system
    spec:
      interval: 1m0s
      sourceRef:
        kind: GitRepository
        name: flux-system
      path: ./apps/my-app
      prune: true
      validation: client
    

    이 파일들을 Git 저장소에 커밋(Commit)하고 푸시(Push)합니다.

    git add .
    git commit -m "Add my-nginx-app and kustomization"
    git push origin main
    

    Flux CD는 clusters/my-cluster/flux-system/gotk-sync.yaml 파일에 정의된 Kustomization에 의해 clusters/my-cluster 경로를 1분마다 동기화합니다. 그러면 방금 추가한 clusters/my-cluster/apps/my-app/kustomization.yaml 파일이 클러스터에 적용되고, 이 Kustomization이 다시 apps/my-app 경로의 NGINX 애플리케이션을 클러스터에 배포하게 됩니다. 이 모든 과정이 자동으로 이뤄지는 거죠!

    ⚠️ 삽질 경험 & 트러블슈팅 팁

    제가 Flux CD를 사용하면서 겪었던 몇 가지 삽질과 해결 팁을 공유해 드릴게요. 여러분은 저처럼 헤매지 마시라고요! ㅎㅎ

    • 동기화 지연 문제: Git에 변경사항을 푸시했는데, 클러스터에 바로 적용이 안 되는 경우가 있습니다. Kustomization 리소스의 interval 값이 기본 10분으로 설정되어 있거나, 네트워크 문제 등으로 Git 저장소에 접근이 안 되는 경우가 많았어요. 급할 때는 다음 명령어로 강제 동기화를 시도합니다.
      flux reconcile kustomization my-nginx-app --with-source
      

      이 명령은 해당 Kustomization과 그 소스 GitRepository를 즉시 동기화하도록 Flux CD에 지시합니다.

    • Git Repository 구조 설계: 처음에는 모든 YAML 파일을 한 폴더에 때려 넣었는데, 나중에 애플리케이션이 많아지고 환경이 복잡해지면서 관리가 힘들어지더라고요. clusters/[클러스터이름]/[환경]/apps/[앱이름] 같은 계층적인 구조나, clusters와 apps를 분리하는 구조로 가져가는 것이 훨씬 효율적입니다. 저는 지금 clusters와 apps를 분리해서 관리하고 있어요.
    • 시크릿(Secret) 관리: 데이터베이스 비밀번호 같은 민감한 정보는 Git에 평문으로 올릴 수 없죠. 저는 SOPS(Secrets OPerationS)를 사용해서 Git 저장소에 암호화된 시크릿을 저장하고, Flux CD가 배포 시 자동으로 복호화하도록 설정했습니다. EKS(Elastic Kubernetes Service) 같은 클라우드 환경에서는 KMS(Key Management Service)를 연동해서 SOPS를 사용하는 것이 일반적입니다.
    • CRD(Custom Resource Definition) 적용 순서: 때로는 특정 컨트롤러나 미들웨어의 CRD가 먼저 클러스터에 적용되어야만 해당 리소스를 배포할 수 있습니다. 예를 들어, Istio(이스티오)나 Cert-Manager(서트 매니저) 같은 도구들이 그렇죠. 이런 경우, Kustomization의 dependsOn 필드를 사용하거나, healthChecks를 설정하여 의존성을 명확히 해주는 것이 중요합니다.

    ✅ 배포 결과 확인 및 검증

    이제 모든 설정이 잘 적용되었는지 확인해볼 시간입니다. flux get kustomizations 명령어로 Flux CD가 관리하는 Kustomization 리소스들의 상태를 확인할 수 있습니다.

    flux get kustomizations
    NAME            REVISION        SUSPENDED   READY   MESSAGE                                         LAST ACTIVITY
    flux-system     main/a1b2c3d4   False       True    Kustomization reconciled successfully           2026-05-15T10:00:00Z
    my-nginx-app    main/e5f6g7h8   False       True    Kustomization reconciled successfully           2026-05-15T10:01:00Z
    

    READY 상태가 True이고 MESSAGE에 성공 메시지가 보인다면, Flux CD가 정상적으로 작동하고 있다는 뜻입니다. 이제 쿠버네티스 클러스터에 NGINX 애플리케이션이 잘 배포되었는지 확인해볼까요?

    kubectl get deployment my-nginx-app
    NAME           READY   UP-TO-DATE   AVAILABLE   AGE
    my-nginx-app   2/2     2            2           5m
    
    kubectl get service my-nginx-app-service
    NAME                     TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
    my-nginx-app-service     ClusterIP   10.96.100.123   <none>        80/TCP    5m
    

    🎉 드디어 성공입니다! my-nginx-app 배포가 정상적으로 2개의 레플리카(Replica)로 동작하고 있는 것을 확인할 수 있네요. 이제 Git 저장소의 apps/my-app/deployment.yaml 파일에서 replicas 수를 3으로 변경하고 푸시해보세요. 잠시 후 Flux CD가 이를 감지하고 자동으로 클러스터의 레플리카 수를 3으로 업데이트할 겁니다. 이 경험을 하고 나면 GitOps의 매력에서 헤어 나올 수 없을 거예요!

    GitOps는 이렇게 Git에 대한 변경만으로 클러스터의 상태를 관리할 수 있게 해줍니다. 저는 이 편리함 덕분에 홈랩에서 새로운 서비스를 배포할 때마다 kubectl 명령어를 직접 입력할 일이 거의 없어졌어요. 모든 변경사항은 Git에 기록되니, 누가 어떤 변경을 했는지도 명확하게 알 수 있고요. 정말 안정적이고 편리한 배포 파이프라인이 구축된 거죠.

    마무리하며: GitOps와 Flux CD, 현대 인프라의 필수 도구

    오늘은 13년차 인프라 엔지니어의 경험을 바탕으로 Flux CD GitOps 파이프라인 구축에 대해 상세히 이야기해봤습니다. 쿠버네티스 배포 자동화는 현대 인프라에서 선택이 아닌 필수가 되어가고 있습니다. 특히 GitOps는 그 중심에서 안정성, 일관성, 생산성이라는 세 마리 토끼를 동시에 잡을 수 있게 해주는 강력한 방법론이라고 생각합니다.

    물론 처음에는 GitOps 개념이나 Flux CD 사용법이 낯설고 어렵게 느껴질 수 있습니다. 저도 그랬거든요. 하지만 한 번 제대로 구축하고 나면, 인프라 관리의 패러다임이 확 바뀌는 것을 경험하실 수 있을 겁니다. 마치 수동으로 서버를 한 대 한 대 설치하다가 프로비저닝(Provisioning) 자동화를 접했을 때의 충격과 비슷하다고 할까요?

    다음 글에서는 오늘 다루지 못한 Helm Chart(헬름 차트)를 Flux CD로 관리하는 방법이나, 멀티 클러스터(Multi-cluster) 환경에서의 GitOps 전략, 그리고 SOPS를 이용한 시크릿 관리 등 좀 더 심화된 주제들을 다뤄볼 예정입니다. 이 글이 여러분의 쿠버네티스 여정에 작은 도움이 되었기를 바랍니다!

    궁금한 점이나 저의 삽질 경험에 대한 질문이 있으시면 언제든지 댓글 남겨주세요! 저도 계속 배우고 성장하는 중이거든요. 😊

  • [Cloud] GitHub Actions 모노레포: 매트릭스 빌드로 CI/CD 최적화하기

    모노레포에서 CI/CD, 이게 생각보다 복잡하더라고요

    처음 팀에서 모노레포(Monorepo)로 전환했을 때 솔직히 자신 있었거든요. “뭐, CI/CD 파이프라인 하나 잘 짜면 되는 거 아니야?” 했는데… 현실은 달랐습니다. 프론트엔드, 백엔드 API, 공통 라이브러리가 한 레포에 다 들어가 있으니까 서로 상관없는 변경사항인데도 전체 빌드가 돌고, 빌드 시간은 점점 길어지고. 결국 개발자들이 “PR 올리면 20분 기다려야 해요”라고 불만을 터뜨리기 시작했죠.

    그때 제대로 파고든 게 GitHub Actions 모노레포 환경에서의 매트릭스 빌드(Matrix Build) 전략이었습니다. 인프라 업무를 오래 하면서 CI/CD 툴을 여럿 써봤는데, GitHub Actions의 매트릭스 전략은 모노레포 환경에서 정말 강력하더라고요. 오늘은 그 경험을 바탕으로 실전에서 바로 쓸 수 있는 가이드를 공유해 드리려 합니다.

    ▲ GitHub Actions 모노레포 환경의 전체 CI/CD 파이프라인 흐름 — 변경된 패키지만 선택적으로 빌드/테스트하는 구조

    GitHub Actions 모노레포와 매트릭스 빌드, 쉽게 이해해 봅시다

    모노레포(Monorepo)란?

    쉽게 말해, 여러 개의 프로젝트나 패키지를 하나의 Git 저장소에서 관리하는 방식입니다. 예전에는 프로젝트마다 레포를 따로 만드는 멀티레포(Multi-repo) 방식이 흔했는데, 요즘은 Google, Meta, Microsoft 같은 대형 기업들도 모노레포를 적극 활용하고 있죠.

    • ✅ 코드 공유와 재사용이 쉬움
    • ✅ 의존성 관리가 일원화됨
    • ✅ 변경 사항의 영향 범위를 한눈에 파악 가능
    • ⚠️ CI/CD 파이프라인 설계가 까다로워짐
    • ⚠️ 레포 크기가 커질수록 빌드 시간이 늘어날 수 있음

    GitHub Actions 매트릭스 빌드(Matrix Build)란?

    GitHub Actions의 strategy.matrix 기능은 여러 변수 조합에 대해 병렬로 잡(Job)을 실행하는 기능입니다. 예를 들어 Node.js 16, 18, 20 버전에서 동시에 테스트를 돌린다거나, 여러 패키지를 동시에 빌드할 때 쓰죠. 모노레포에서는 이걸 “변경된 패키지 목록”과 조합해서 쓰면 진짜 강력해집니다.

    방식 빌드 대상 빌드 시간 장단점
    전체 빌드 (Naive) 모든 패키지 길다 설정 단순, 시간 낭비 큼
    매트릭스 + 변경 감지 변경된 패키지만 짧다 설정 복잡, 효율 극대화
    캐시 + 매트릭스 변경된 패키지만 매우 짧다 가장 최적화된 방식

    GitHub Actions 모노레포 CI/CD 실전 구현: 단계별로 따라해 보세요

    1단계: 레포 구조 잡기

    먼저 제가 실제로 운영하는 모노레포 구조를 보여드릴게요. 완벽할 필요는 없고, 이런 식으로 패키지가 분리되어 있으면 됩니다.

    my-monorepo/
    ├── .github/
    │   └── workflows/
    │       ├── ci.yml          # 메인 CI 워크플로우
    │       └── detect-changes.yml
    ├── packages/
    │   ├── frontend/           # React 프론트엔드
    │   │   ├── package.json
    │   │   └── src/
    │   ├── api-server/         # Node.js API 서버
    │   │   ├── package.json
    │   │   └── src/
    │   └── shared-lib/         # 공통 라이브러리
    │       ├── package.json
    │       └── src/
    ├── package.json            # 루트 package.json (워크스페이스 설정)
    └── pnpm-workspace.yaml     # pnpm 워크스페이스 설정

    2단계: 변경된 패키지 감지하기

    여기가 핵심입니다. 어떤 패키지가 바뀌었는지 감지해야 매트릭스를 동적으로 구성할 수 있거든요. 저는 git diff와 간단한 스크립트를 조합해서 씁니다.

    # .github/workflows/ci.yml
    name: Monorepo CI
    
    on:
      push:
        branches: [main, develop]
      pull_request:
        branches: [main, develop]
    
    jobs:
      # 1. 변경된 패키지 목록을 동적으로 생성
      detect-changes:
        runs-on: ubuntu-latest
        outputs:
          matrix: ${{ steps.set-matrix.outputs.matrix }}
          has-changes: ${{ steps.set-matrix.outputs.has-changes }}
        steps:
          - name: Checkout
            uses: actions/checkout@v4
            with:
              fetch-depth: 0  # 전체 히스토리 필요 (diff 비교용)
    
          - name: Detect changed packages
            id: set-matrix
            run: |
              # PR이면 base 브랜치와 비교, push면 이전 커밋과 비교
              if [ "${{ github.event_name }}" = "pull_request" ]; then
                BASE_SHA=${{ github.event.pull_request.base.sha }}
              else
                BASE_SHA=${{ github.event.before }}
              fi
    
              CHANGED_PACKAGES='[]'
    
              # packages/ 하위 디렉토리를 순회하며 변경 여부 확인
              for pkg_dir in packages/*/; do
                pkg_name=$(basename $pkg_dir)
                changed=$(git diff --name-only $BASE_SHA ${{ github.sha }} -- $pkg_dir | head -1)
                if [ -n "$changed" ]; then
                  CHANGED_PACKAGES=$(echo $CHANGED_PACKAGES | jq --arg p "$pkg_name" '. + [$p]')
                fi
              done
    
              echo "Changed packages: $CHANGED_PACKAGES"
    
              if [ "$CHANGED_PACKAGES" = "[]" ]; then
                echo "has-changes=false" >> $GITHUB_OUTPUT
              else
                echo "has-changes=true" >> $GITHUB_OUTPUT
              fi
    
              echo "matrix={\"package\":$CHANGED_PACKAGES}" >> $GITHUB_OUTPUT

    💡 팁: fetch-depth: 0 설정이 없으면 git diff가 제대로 안 됩니다. 처음에 이거 빠뜨려서 삽질 좀 했어요. 얕은 클론(shallow clone)에서는 비교 기준이 되는 커밋을 못 찾거든요.

    3단계: 매트릭스 빌드 잡 구성

    이제 감지된 패키지 목록으로 매트릭스를 구성합니다. needs 키워드로 앞 단계에 의존하게 만들고, if 조건으로 변경 사항이 없을 때는 스킵하게 하면 됩니다.

      # 2. 변경된 패키지만 병렬로 빌드 & 테스트
      build-and-test:
        needs: detect-changes
        if: needs.detect-changes.outputs.has-changes == 'true'
        runs-on: ubuntu-latest
        strategy:
          matrix: ${{ fromJson(needs.detect-changes.outputs.matrix) }}
          fail-fast: false  # 하나 실패해도 나머지는 계속 실행
        steps:
          - name: Checkout
            uses: actions/checkout@v4
    
          - name: Setup Node.js
            uses: actions/setup-node@v4
            with:
              node-version: '20'
              cache: 'pnpm'
    
          - name: Install pnpm
            uses: pnpm/action-setup@v3
            with:
              version: 8
    
          - name: Install dependencies
            run: pnpm install --frozen-lockfile
    
          # 캐시 설정 — 빌드 시간 단축에 핵심!
          - name: Cache build artifacts
            uses: actions/cache@v4
            with:
              path: packages/${{ matrix.package }}/.next
              key: ${{ runner.os }}-${{ matrix.package }}-${{ hashFiles('packages/${{ matrix.package }}/package.json') }}
              restore-keys: |
                ${{ runner.os }}-${{ matrix.package }}-
    
          - name: Build package
            run: pnpm --filter ${{ matrix.package }} build
    
          - name: Run tests
            run: pnpm --filter ${{ matrix.package }} test
    
          - name: Upload test results
            if: always()  # 테스트 실패해도 결과는 업로드
            uses: actions/upload-artifact@v4
            with:
              name: test-results-${{ matrix.package }}
              path: packages/${{ matrix.package }}/test-results/

    ▲ 매트릭스 빌드가 실행될 때 GitHub Actions 화면 — 변경된 패키지들이 병렬로 동시에 빌드되는 것을 확인할 수 있음

    4단계: 의존성 있는 패키지 처리

    근데 여기서 문제가 하나 생깁니다. shared-lib가 변경되면 이걸 쓰는 frontend나 api-server도 다시 빌드해야 하거든요. 이 의존성 체인을 처리하는 게 모노레포 CI/CD의 진짜 어려운 부분입니다.

    #!/bin/bash
    # scripts/detect-affected.sh
    # 변경된 패키지 + 그에 의존하는 패키지까지 모두 감지
    
    CHANGED_DIRECT=$1  # 직접 변경된 패키지 (JSON 배열 문자열)
    AFFECTED_PACKAGES=$CHANGED_DIRECT
    
    # 각 패키지의 package.json을 읽어서 의존성 체인 추적
    for pkg_dir in packages/*/; do
      pkg_name=$(basename $pkg_dir)
      pkg_json="$pkg_dir/package.json"
    
      if [ -f "$pkg_json" ]; then
        # 이 패키지가 변경된 패키지에 의존하는지 확인
        for changed in $(echo $CHANGED_DIRECT | jq -r '.[]'); do
          deps=$(cat $pkg_json | jq -r '.dependencies // {} | keys[]' 2>/dev/null)
          if echo "$deps" | grep -q "$changed"; then
            # 의존성이 있으면 affected 목록에 추가
            AFFECTED_PACKAGES=$(echo $AFFECTED_PACKAGES | jq --arg p "$pkg_name" '. + [$p] | unique')
          fi
        done
      fi
    done
    
    echo $AFFECTED_PACKAGES

    이 스크립트를 detect-changes 잡에서 호출하면 됩니다. 물론 Nx나 Turborepo 같은 모노레포 전용 툴을 쓰면 이런 의존성 그래프를 자동으로 처리해 주기도 하죠. 규모가 커지면 Turborepo로 마이그레이션하는 걸 고려하고 있어요.

    ⚠️ 실제로 겪은 GitHub Actions 모노레포 트러블슈팅

    문제 1: BASE_SHA가 비어있는 경우

    첫 번째 커밋이거나 새 브랜치를 push할 때 github.event.before가 0000000000000000000000000000000000000000(제로 SHA)로 오는 경우가 있습니다. 이때 git diff가 에러를 뱉거든요.

          - name: Detect changed packages
            id: set-matrix
            run: |
              # 제로 SHA 처리
              BASE_SHA=${{ github.event.before }}
              ZERO_SHA="0000000000000000000000000000000000000000"
    
              if [ "$BASE_SHA" = "$ZERO_SHA" ] || [ -z "$BASE_SHA" ]; then
                # 첫 커밋이면 모든 패키지를 빌드 대상으로
                echo "First push or new branch — building all packages"
                ALL_PACKAGES=$(ls packages/ | jq -R -s -c 'split("\\n") | map(select(length > 0))')
                echo "matrix={\"package\":$ALL_PACKAGES}" >> $GITHUB_OUTPUT
                echo "has-changes=true" >> $GITHUB_OUTPUT
              else
                # 기존 로직 실행
                # ... (이전 코드)
                :
              fi

    문제 2: 매트릭스가 빈 배열일 때 워크플로우 에러

    변경된 패키지가 없어서 매트릭스가 빈 배열 []이 되면 GitHub Actions가 에러를 냅니다. has-changes 출력값으로 if 조건을 걸어두는 게 중요한 이유가 여기 있어요. 또한 최종 상태 체크 잡을 별도로 두는 것도 좋은 패턴입니다.

      # 모든 빌드 완료 후 최종 상태 확인 잡
      ci-success:
        needs: [detect-changes, build-and-test]
        if: always()
        runs-on: ubuntu-latest
        steps:
          - name: Check all jobs status
            run: |
              if [ "${{ needs.detect-changes.result }}" != "success" ]; then
                echo "detect-changes job failed"
                exit 1
              fi
    
              # build-and-test가 스킵됐거나 성공한 경우 모두 OK
              if [ "${{ needs.build-and-test.result }}" = "failure" ]; then
                echo "Some builds failed!"
                exit 1
              fi
    
              echo "All checks passed! 🎉"

    문제 3: pnpm 워크스페이스에서 filter가 안 먹힐 때

    패키지 이름이 package.json의 name 필드와 달라서 --filter가 안 먹히는 경우가 있었습니다. 디렉토리명으로 필터하려면 --filter ./packages/패키지명 형식을 써야 합니다.

          - name: Build package
            run: |
              # 디렉토리 경로로 필터 (이름 불일치 문제 방지)
              pnpm --filter ./packages/${{ matrix.package }} build

    전체 GitHub Actions 워크플로우 완성본

    지금까지 설명한 내용을 하나로 합친 완성본입니다. 실제로 제가 운영 중인 설정을 기반으로 정리했어요.

    # .github/workflows/ci.yml
    name: Monorepo CI/CD
    
    on:
      push:
        branches: [main, develop]
      pull_request:
        branches: [main, develop]
    
    concurrency:
      group: ${{ github.workflow }}-${{ github.ref }}
      cancel-in-progress: true  # 같은 브랜치 중복 실행 방지
    
    jobs:
      detect-changes:
        runs-on: ubuntu-latest
        outputs:
          matrix: ${{ steps.set-matrix.outputs.matrix }}
          has-changes: ${{ steps.set-matrix.outputs.has-changes }}
        steps:
          - uses: actions/checkout@v4
            with:
              fetch-depth: 0
    
          - name: Detect changed packages
            id: set-matrix
            run: |
              if [ "${{ github.event_name }}" = "pull_request" ]; then
                BASE_SHA=${{ github.event.pull_request.base.sha }}
              else
                BASE_SHA=${{ github.event.before }}
              fi
    
              ZERO_SHA="0000000000000000000000000000000000000000"
              CHANGED_PACKAGES='[]'
    
              if [ "$BASE_SHA" = "$ZERO_SHA" ] || [ -z "$BASE_SHA" ]; then
                CHANGED_PACKAGES=$(ls packages/ | jq -R -s -c 'split("\\n") | map(select(length > 0))')
              else
                for pkg_dir in packages/*/; do
                  pkg_name=$(basename $pkg_dir)
                  changed=$(git diff --name-only $BASE_SHA ${{ github.sha }} -- $pkg_dir | head -1)
                  if [ -n "$changed" ]; then
                    CHANGED_PACKAGES=$(echo $CHANGED_PACKAGES | jq --arg p "$pkg_name" '. + [$p]')
                  fi
                done
              fi
    
              echo "Affected packages: $CHANGED_PACKAGES"
    
              if [ "$CHANGED_PACKAGES" = "[]" ]; then
                echo "has-changes=false" >> $GITHUB_OUTPUT
              else
                echo "has-changes=true" >> $GITHUB_OUTPUT
              fi
              echo "matrix={\"package\":$CHANGED_PACKAGES}" >> $GITHUB_OUTPUT
    
      build-and-test:
        needs: detect-changes
        if: needs.detect-changes.outputs.has-changes == 'true'
        runs-on: ubuntu-latest
        strategy:
          matrix: ${{ fromJson(needs.detect-changes.outputs.matrix) }}
          fail-fast: false
        steps:
          - uses: actions/checkout@v4
    
          - uses: pnpm/action-setup@v3
            with:
              version: 8
    
          - uses: actions/setup-node@v4
            with:
              node-version: '20'
              cache: 'pnpm'
    
          - name: Install dependencies
            run: pnpm install --frozen-lockfile
    
          - name: Cache build
            uses: actions/cache@v4
            with:
              path: |
                packages/${{ matrix.package }}/.next
                packages/${{ matrix.package }}/dist
              key: ${{ runner.os }}-build-${{ matrix.package }}-${{ github.sha }}
              restore-keys: |
                ${{ runner.os }}-build-${{ matrix.package }}-
    
          - name: Build
            run: pnpm --filter ./packages/${{ matrix.package }} build
    
          - name: Test
            run: pnpm --filter ./packages/${{ matrix.package }} test
    
          - name: Upload artifacts
            if: always()
            uses: actions/upload-artifact@v4
            with:
              name: results-${{ matrix.package }}
              path: packages/${{ matrix.package }}/test-results/
              retention-days: 7
    
      ci-success:
        needs: [detect-changes, build-and-test]
        if: always()
        runs-on: ubuntu-latest
        steps:
          - name: Final status check
            run: |
              if [ "${{ needs.build-and-test.result }}" = "failure" ]; then
                echo "Build failed!"
                exit 1
              fi
              echo "CI passed! 🎉"

    ▲ 완성된 CI/CD 파이프라인 실행 결과 — detect-changes → build-and-test (병렬) → ci-success 순서로 실행되는 것을 확인

    ✅ 결과: 빌드 시간이 얼마나 줄었을까요?

    이 방식을 적용하고 나서 체감이 꽤 컸습니다. 물론 레포마다 상황이 다르겠지만, 제 경우엔 이랬어요.

    • 📦 전체 패키지 수: 6개
    • ⏱️ 기존 전체 빌드 시간: 약 18~22분
    • ⚡ 매트릭스 빌드 적용 후 (1~2개 패키지 변경 시): 4~7분
    • 🎉 개발자들 PR 머지 속도 체감상 확 빨라짐

    특히 공통 라이브러리 변경이 없는 일반적인 기능 개발 PR에서 효과가 컸습니다. 프론트엔드만 건드렸을 때 백엔드 빌드까지 기다릴 필요가 없으니까요. 당연한 말 같지만, 이걸 자동화로 구현하는 게 핵심이죠.

    추가로 concurrency 설정으로 같은 브랜치에서 중복 실행을 방지한 것도 빌드 큐 낭비를 줄이는 데 도움이 됐습니다. GitHub Actions 무료 플랜은 동시 실행 잡 수에 제한이 있으니까요.

    💡 CI/CD 최적화 추가 팁

    캐시 전략으로 빌드 시간 단축

    • actions/cache의 key를 package.json 해시 기반으로 설정하면 의존성이 바뀔 때만 캐시가 무효화됩니다
    • 빌드 결과물(dist, .next)도 캐시하면 재빌드 시 시간 절약 가능
    • 캐시 hit rate는 Actions 실행 로그에서 확인 가능 — 이걸 모니터링하는 습관을 들이세요

    Turborepo와 함께 쓰기

    패키지가 10개 이상으로 늘어나면 Turborepo(터보레포) 같은 모노레포 빌드 시스템을 도입하는 게 좋습니다. 의존성 그래프 분석, 원격 캐시, 병렬 실행 최적화를 자동으로 해주거든요. 다음 글에서 Turborepo와 GitHub Actions를 함께 쓰는 방법을 다룰 예정입니다.

    Branch Protection Rules 연동

    앞서 만든 ci-success 잡을 GitHub 브랜치 보호 규칙(Branch Protection Rules)의 필수 상태 체크로 등록해 두세요. 매트릭스 빌드가 스킵된 경우에도 ci-success는 항상 실행되므로, PR 머지 조건으로 쓰기에 딱입니다.

    ▲ GitHub Actions 모노레포 CI/CD 최적화 전략 요약 — 변경 감지, 매트릭스 빌드, 캐시 전략의 세 가지 축

    자주 묻는 질문

    Q. Nx를 쓰면 이런 GitHub Actions 설정이 필요 없나요?

    Nx(엔엑스)를 쓰면 nx affected 명령어로 변경된 패키지 감지를 자동화할 수 있습니다. 다만 Nx 자체 학습 곡선이 있고, 기존 프로젝트에 도입하는 비용이 있어요. 팀 규모와 프로젝트 복잡도에 따라 선택하시면 됩니다. 오늘 설명한 방식은 외부 툴 없이 GitHub Actions만으로 구현할 수 있다는 게 장점이에요.

    Q. PR이 아닌 직접 push에서도 잘 되나요?

    네, 됩니다. 다만 github.event.before가 제로 SHA로 오는 케이스(새 브랜치 최초 push)를 반드시 처리해야 합니다. 위 코드에 그 처리가 포함되어 있으니 참고하세요.

    Q. 모노레포에서 Docker 이미지 빌드도 같은 방식으로?

    동일한 패턴으로 적용 가능합니다. 매트릭스의 각 항목에 대해 docker build를 실행하고 레지스트리에 push하면 되죠. 이 내용은 이전 글에서 다룬 Docker 멀티 스테이지 빌드 최적화와 함께 보시면 더 이해가 쉬울 거예요.

    마무리: GitHub Actions 모노레포 CI/CD, 처음엔 복잡해 보여도

    처음에 이 구조를 짤 때 “이게 맞나?” 싶은 순간이 여러 번 있었습니다. 특히 동적 매트릭스 생성 부분에서 fromJson이 제대로 안 먹혀서 한참 헤맸던 기억이 나네요. 근데 한번 제대로 잡아두면 이후에는 거의 손댈 일이 없어요.

    핵심을 정리하면 이렇습니다.

    1. 변경 감지: git diff로 변경된 패키지만 추려냄
    2. 동적 매트릭스: 감지 결과를 JSON으로 출력해서 매트릭스에 주입
    3. 병렬 빌드: strategy.matrix로 변경된 패키지들을 동시에 빌드
    4. 캐시 활용: actions/cache로 반복 빌드 시간 단축
    5. 안전장치: ci-success 잡으로 브랜치 보호 규칙과 연동

    혹시 레포 규모가 더 크거나, Turborepo/Nx 같은 툴과 함께 쓰는 방법이 궁금하신 분들은 댓글로 남겨주세요. 다음 글에서 다뤄볼게요. 오늘도 긴 글 읽어주셔서 감사합니다! 🎉

  • [AWS 배포] GitHub Actions로 S3/CloudFront 정적 웹사이트 배포 자동화 가이드

    수동 배포, 이제 그만 — GitHub Actions로 S3/CloudFront 배포 자동화하기

    혹시 이런 경험 있으신가요? 프론트엔드 코드 수정하고, 빌드하고, AWS 콘솔 열고, S3에 파일 올리고, CloudFront 캐시 무효화(Invalidation)하고… 이 반복 작업을 매번 손으로 하다가 어느 순간 ‘내가 지금 뭘 하고 있나’ 싶은 그 느낌. 저도 예전에 딱 그랬거든요.

    13년 동안 인프라 일 하면서 느낀 건데, 반복 작업은 반드시 자동화해야 합니다. 사람이 하면 언젠가는 실수가 납니다. 저도 한 번은 CloudFront 캐시 무효화 깜빡해서 사용자들이 한참 동안 옛날 버전 보고 있었던 적 있어요. 그날 이후로 배포 자동화는 선택이 아니라 필수라고 생각하게 됐습니다.

    오늘은 GitHub Actions를 활용해서 AWS S3와 CloudFront에 정적 웹사이트를 자동으로 배포하는 CI/CD 파이프라인을 처음부터 끝까지 만들어보겠습니다. React나 Vue, Next.js 정적 빌드 등 어떤 프레임워크든 적용할 수 있는 방법이에요.

    ▲ GitHub Actions → S3 업로드 → CloudFront 캐시 무효화로 이어지는 전체 배포 파이프라인 구조

    GitHub Actions AWS 배포의 핵심 개념 — CI/CD, S3, CloudFront

    일단 개념부터 간단히 정리하고 갈게요. 저도 처음엔 이 용어들이 뒤섞여서 헷갈렸거든요.

    GitHub Actions란?

    GitHub Actions는 GitHub에서 제공하는 CI/CD(지속적 통합/지속적 배포) 자동화 플랫폼입니다. 쉽게 말해서, 코드를 push하거나 PR을 올리는 등 특정 이벤트가 발생했을 때 미리 정의해둔 작업을 자동으로 실행해주는 도구예요. 빌드, 테스트, 배포까지 전부 코드로 관리할 수 있고, 무료 플랜도 꽤 넉넉해서 소규모 프로젝트엔 비용 걱정 없이 쓸 수 있습니다.

    S3 + CloudFront 조합이 왜 좋은가?

    AWS S3(Simple Storage Service)는 파일 저장소인데, 정적 웹사이트 호스팅 기능도 있어요. CloudFront는 AWS의 CDN(콘텐츠 전송 네트워크)으로, 전 세계 엣지 서버에 콘텐츠를 캐싱해서 사용자에게 빠르게 전달해줍니다. 이 둘을 조합하면 서버 없이도 빠르고 안정적인 웹사이트를 운영할 수 있어요. 저도 여러 프로젝트에서 이 구조를 썼는데, 정말 편하더라고요.

    항목 S3 단독 호스팅 S3 + CloudFront
    HTTPS 지원 ❌ (별도 설정 복잡) ✅ ACM 인증서 연동
    글로벌 속도 단일 리전 기준 CDN 엣지 캐싱으로 빠름
    커스텀 도메인 제한적 자유롭게 가능
    비용 저렴 약간 추가 (트래픽 기준)

    실제 프로덕션 환경이라면 S3 + CloudFront 조합을 강력 추천합니다. 오늘 가이드도 이 조합 기준으로 진행할게요.

    사전 준비 — AWS IAM 권한 설정부터

    GitHub Actions에서 AWS 리소스에 접근하려면 적절한 권한을 가진 IAM(Identity and Access Management) 사용자가 필요합니다. 여기서 많은 분들이 귀찮다고 AdministratorAccess 권한을 통째로 주는 경우가 있는데, 보안상 절대 권장하지 않아요. 최소 권한 원칙(Principle of Least Privilege)을 지켜야 합니다.

    IAM 정책 만들기

    AWS IAM 콘솔에서 아래 내용으로 커스텀 정책을 만들어주세요. S3 버킷 이름과 CloudFront Distribution ID는 본인 것으로 바꾸세요.

    {\n  "Version": "2012-10-17",\n  "Statement": [\n    {\n      "Effect": "Allow",\n      "Action": [\n        "s3:PutObject",\n        "s3:PutObjectAcl",\n        "s3:DeleteObject"\n      ],\n      "Resource": "arn:aws:s3:::your-bucket-name/*"\n    },\n    {\n      "Effect": "Allow",\n      "Action": [\n        "cloudfront:CreateInvalidation"\n      ],\n      "Resource": "arn:aws:cloudfront::YOUR-AWS-ACCOUNT-ID:distribution/YOUR-DISTRIBUTION-ID"\n    }\n  ]\n}
  • [k8s] ArgoCD 프로덕션 환경 GitOps 베스트 프랙티스: 안정적인 배포와 보안 강화

    [k8s] ArgoCD 프로덕션 환경 GitOps 베스트 프랙티스: 안정적인 배포와 보안 강화

    ArgoCD 프로덕션 환경 GitOps 베스트 프랙티스: 안정적인 배포와 보안 강화

    안녕하세요, 13년차 인프라 엔지니어입니다. 오늘은 쿠버네티스(Kubernetes) 배포의 핵심 툴 중 하나인 ArgoCD와 GitOps에 대해 이야기해보려고 해요. 사실 제가 처음 인프라 엔지니어를 시작했을 때는 배포라고 하면 SSH로 서버에 접속해서 스크립트 돌리고, 수동으로 설정을 변경하고, 문제가 터지면 눈으로 찾아 헤매는 게 일상이었거든요. 그러다 쿠버네티스 세상이 열리고 CI/CD 파이프라인이 중요해지면서, 더 안정적이고 효율적인 배포 방식에 대한 갈증이 커졌습니다.

    수많은 삽질 끝에 제가 정착한 방법이 바로 GitOps였습니다. 그리고 그 중심에는 ArgoCD가 있었죠. 처음엔 이게 뭔가 싶었는데, 막상 써보니까 ‘아, 이거 진짜 편하고 안전하네!’ 싶더라고요. 프로덕션 환경에서 ArgoCD를 안정적으로 운영하고 GitOps 보안을 강화하기 위한 저만의 경험과 베스트 프랙티스를 공유해볼까 합니다. 혹시 쿠버네티스 배포 때문에 밤잠 설치고 계신 분들이 있다면, 이 글이 조금이나마 도움이 되었으면 좋겠네요. 💡

    ArgoCD와 GitOps의 전체 아키텍처는 위 그림처럼 구성됩니다. Git을 중심으로 모든 것이 돌아가는 모습을 보실 수 있어요.

    왜 ArgoCD와 GitOps인가요? – 삽질 끝에 찾은 안정성

    저도 처음에는 Jenkins 같은 전통적인 CI/CD 툴로 쿠버네티스 배포를 시도했어요. 하지만 배포 스크립트 관리도 어렵고, 배포 이력 추적도 쉽지 않더라고요. 특히 긴급 롤백(Rollback) 상황에서는 정말 아찔한 경험도 많았습니다. 😱

    GitOps는 쉽게 말해, Git을 인프라의 ‘단 하나의 진실된 소스(Single Source of Truth)’로 삼는 운영 방식입니다. 모든 인프라와 애플리케이션의 상태를 Git 리포지토리(Repository)에 코드(Code)로 저장하고, Git에 변경사항이 푸시(Push)되면 자동으로 인프라에 반영하는 방식이죠. ArgoCD는 이 GitOps 철학을 쿠버네티스 환경에서 구현해주는 강력한 선언적(Declarative) GitOps 지속적 배포(Continuous Delivery) 툴입니다.

    ArgoCD는 쿠버네티스 클러스터(Cluster) 내부에서 동작하면서, Git 리포지토리의 상태와 실제 클러스터의 상태를 계속 비교합니다. 만약 두 상태가 다르면, Git 리포지토리의 상태(원하는 상태)로 클러스터의 상태를 동기화(Sync)하려고 시도하죠. 이걸 풀 기반(Pull-based) 배포라고 하는데, 기존의 푸시 기반(Push-based) CI/CD 방식보다 훨씬 안정적이고 보안에도 유리합니다. 직접 써보니, 이 방식이 정말 믿음직하더라고요. ✅

    ArgoCD와 GitOps, 쉽게 이해하기

    복잡하게 생각할 것 없이, GitOps는 우리가 늘 쓰던 Git으로 인프라도 관리하자는 이야기입니다. 애플리케이션 코드를 Git으로 관리하듯이, 쿠버네티스 매니페스트(Manifest) 파일들도 Git에 넣고 관리하는 거죠. 이렇게 하면 다음과 같은 장점들이 생깁니다.

    • 버전 관리(Version Control): 모든 변경 이력이 Git에 남으니, 누가 언제 무엇을 바꿨는지 명확하게 알 수 있습니다.
    • 롤백(Rollback) 용이: 문제가 생기면 Git 커밋(Commit)을 되돌리는 것만으로 이전 상태로 쉽게 돌아갈 수 있습니다.
    • 감사(Auditing) 및 보안 강화: 모든 변경이 Git을 통해 이루어지므로, 승인된 변경만 반영될 수 있도록 워크플로우(Workflow)를 구축하기 좋습니다.
    • 단일 진실 공급원(Single Source of Truth): 개발, 운영팀 모두 Git만 보면 현재 인프라 상태를 파악할 수 있습니다.

    ArgoCD는 이 GitOps를 쿠버네티스에서 실현시켜주는 컨트롤러(Controller)입니다. 주요 특징은 다음과 같아요.

    • 선언적(Declarative) 관리: 원하는 상태를 YAML 파일로 선언해두면, ArgoCD가 알아서 그 상태를 유지합니다.
    • 자동 동기화(Automatic Synchronization): Git 리포지토리의 변경을 감지하고 자동으로 클러스터에 반영할 수 있습니다.
    • 시각적인 UI: 웹 UI를 통해 애플리케이션의 배포 상태, 리소스(Resource) 현황, 동기화 이력 등을 한눈에 확인할 수 있습니다. 저처럼 눈으로 확인해야 직성이 풀리는 사람에겐 정말 최고더라고요.
    • 다양한 배포 전략 지원: 롤링 업데이트(Rolling Update), 카나리 배포(Canary Deployment), 블루/그린 배포(Blue/Green Deployment) 등 다양한 배포 전략을 연동하여 구현할 수 있습니다.

    프로덕션 환경을 위한 ArgoCD 설정 팁

    이제 본격적으로 프로덕션 환경에서 ArgoCD를 효과적으로 사용하는 베스트 프랙티스를 공유해볼게요. 제가 직접 겪으면서 터득한 노하우들이니, 꼭 참고하시면 좋겠습니다.

    1. Git Repository 구조화: Monorepo vs. Multirepo

    Git 리포지토리를 어떻게 구성할지는 GitOps 전략의 첫 단추입니다. 크게 모노레포(Monorepo)와 멀티레포(Multirepo) 두 가지 방식이 있어요.

    • 모노레포(Monorepo): 모든 애플리케이션과 인프라 설정 파일을 하나의 Git 리포지토리에 저장하는 방식입니다. 초기 설정이 간단하고, 모든 것을 한곳에서 관리할 수 있다는 장점이 있습니다.
    • 멀티레포(Multirepo): 각 애플리케이션이나 환경별로 별도의 Git 리포지토리를 사용하는 방식입니다. 팀별 권한 분리나 대규모 서비스 운영에 유리할 수 있습니다.

    저 같은 경우, 초기에는 모노레포로 시작했다가 규모가 커지면서 멀티레포 형태로 전환했어요. 하지만 ArgoCD를 통해 여러 애플리케이션을 관리할 때는 애플리케이션별 Git 리포지토리 + 인프라 설정용 Git 리포지토리를 분리하는 방식이 가장 효율적이라고 느꼈습니다. 프로덕션 환경에서는 GitOps 보안과 관리의 용이성을 위해 인프라 설정과 애플리케이션 코드를 분리하는 것을 추천합니다.

    예시: 인프라 설정 Git 리포지토리 구조

    ├── applications # ArgoCD Application 정의
    │   ├── app1.yaml
    │   ├── app2.yaml
    │   └── ...
    ├── environments
    │   ├── dev
    │   │   ├── kustomization.yaml
    │   │   ├── namespace.yaml
    │   │   └── ...
    │   ├── stage
    │   │   ├── kustomization.yaml
    │   │   └── ...
    │   └── prod
    │       ├── kustomization.yaml
    │       └── ...
    └── base # 공통 설정
        ├── deployment.yaml
        ├── service.yaml
        └── ...
    

    위 다이어그램은 제가 주로 사용하는 Git Repository 구조를 시각적으로 보여줍니다. 환경별로 설정을 분리하고, 공통 설정은 base에 두는 방식이에요.

    2. 환경별 분리 및 Kustomize/Helm 활용

    개발(dev), 스테이징(stage), 프로덕션(prod) 환경은 각각 다른 설정(리소스 요구 사항, 환경 변수 등)을 가집니다. 이를 효과적으로 관리하려면 Kustomize나 Helm을 적극적으로 활용해야 합니다.

    • Kustomize: 기존 YAML 파일을 수정하지 않고 덮어쓰기(Overlay) 방식으로 환경별 설정을 관리하는 데 매우 유용합니다. base 디렉터리에 공통 설정 파일을 두고, 각 환경별 overlays 디렉터리에서 필요한 부분만 변경하는 방식은 정말 깔끔하죠.
    • Helm: 재사용 가능한 쿠버네티스 애플리케이션 패키징(Packaging) 도구입니다. 데이터베이스(Database)나 메시지 큐(Message Queue)처럼 공통적으로 사용되는 미들웨어(Middleware)를 배포할 때 매우 편리합니다. 저도 처음엔 Helm이 좀 어렵게 느껴졌는데, 익숙해지니 없으면 안 되는 존재가 되더라고요.

    ArgoCD 애플리케이션 정의에서 Kustomize나 Helm을 소스(Source)로 지정하면, ArgoCD가 알아서 해당 툴을 사용해서 매니페스트를 렌더링(Rendering)하고 배포해줍니다. 🚀

    apiVersion: argoproj.io/v1alpha1
    kind: Application
    metadata:
      name: my-app-prod
      namespace: argocd
    spec:
      project: default
      source:
        repoURL: https://github.com/my-org/my-infra-gitops.git
        targetRevision: HEAD
        path: environments/prod/my-app # Kustomize overlay 경로
      destination:
        server: https://kubernetes.default.svc
        namespace: my-app-prod
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true
    

    3. Sync Options와 Health Checks

    ArgoCD의 syncPolicy는 배포의 안정성을 결정하는 중요한 부분입니다. 프로덕션 환경에서는 다음 옵션들을 신중하게 설정해야 합니다.

    • automated.prune: true: Git 리포지토리에서 삭제된 리소스를 클러스터에서도 자동으로 삭제합니다. 클러스터의 불필요한 리소스 잔여물을 방지해줍니다.
    • automated.selfHeal: true: 클러스터의 상태가 Git 리포지토리의 상태와 다를 경우, ArgoCD가 자동으로 Git의 상태로 되돌립니다. 누군가 수동으로 클러스터 설정을 변경했을 때 원래대로 복구해주는 강력한 기능이죠. GitOps의 핵심 정신과 일치하는 부분입니다.
    • syncOptions: - CreateNamespace=true: 해당 네임스페이스(Namespace)가 없으면 ArgoCD가 자동으로 생성하도록 합니다.
    • 커스텀 헬스 체크(Custom Health Checks): 애플리케이션이 단순히 배포되었다고 끝이 아닙니다. 실제로 정상 작동하는지 확인하는 헬스 체크가 중요하죠. ArgoCD는 기본적으로 Pod의 Readiness/Liveness Probe를 활용하지만, 경우에 따라 ConfigMap에 커스텀 헬스 체크를 정의하여 더 정교한 상태 감지를 할 수 있습니다.

    4. Rollback 전략

    아무리 잘 준비해도 문제는 발생하기 마련이죠. 이때 빠르고 안전하게 롤백하는 것이 중요합니다. ArgoCD 환경에서의 롤백은 크게 두 가지 방식이 있어요.

    1. Git Revert: 가장 권장되는 방식입니다. 문제가 발생한 커밋을 Git에서 되돌리면(Revert) ArgoCD가 이를 감지하고 자동으로 이전 상태로 클러스터를 동기화합니다. 이 방식은 Git의 변경 이력이 그대로 남으므로 감사 추적에도 유리합니다.
    2. ArgoCD UI 롤백: ArgoCD 웹 UI에서 특정 애플리케이션의 History and Rollback 탭을 통해 이전 동기화 지점(Sync Point)으로 롤백할 수 있습니다. 긴급 상황에서 빠르게 대처할 수 있는 방법이지만, Git의 변경 이력에는 남지 않으므로 나중에 Git Revert로 맞춰주는 게 좋습니다.

    저도 예전에 새벽에 긴급 롤백을 해야 했던 적이 있었는데, Git Revert 하나로 모든 상황이 깔끔하게 정리되었을 때의 안도감이란… 정말 겪어봐야 알 수 있습니다. 👍

    GitOps 보안 강화: ArgoCD 프로덕션 환경 보안 설정

    프로덕션 환경에서는 GitOps 보안이 최우선 고려사항입니다. ArgoCD를 통해 모든 것이 배포되므로, ArgoCD 자체의 보안 설정은 물론 주변 환경과의 연동도 중요합니다.

    1. RBAC (Role-Based Access Control)

    ArgoCD는 자체적인 RBAC(Role-Based Access Control)를 지원합니다. 이를 통해 누가 어떤 애플리케이션을 조회, 동기화, 삭제할 수 있는지 세밀하게 권한을 제어할 수 있어요. argocd-cm ConfigMap이나 argocd-rbac-cm ConfigMap을 수정하여 정책을 정의합니다.

    또한, ArgoCD 프로젝트(Project)를 활용하여 애플리케이션들을 논리적으로 묶고, 프로젝트별로 RBAC 정책을 적용하면 관리 효율성과 보안을 동시에 잡을 수 있습니다. 예를 들어, 개발팀은 dev-project에만 접근 가능하고, 운영팀은 prod-project에만 접근 가능하도록 설정하는 식이죠.

    # argocd-rbac-cm ConfigMap 예시
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: argocd-rbac-cm
      namespace: argocd
    data:
      policy.csv: |
        p, role:admin, applications, *, */*, allow
        p, role:dev, applications, get, dev-project/*, allow
        g, myuser, role:dev
      policy.default: role:readonly
    

    2. Secret Management (외부 Secret 연동)

    민감한 정보인 시크릿(Secret)을 Git 리포지토리에 평문으로 저장하는 것은 절대 금물입니다. 🙅‍♂️ GitOps 보안을 위해 외부 시크릿 관리 솔루션과 연동하는 것이 필수입니다.

    • Sealed Secrets: 클러스터에 배포된 컨트롤러가 시크릿을 암호화하고, Git에 암호화된 시크릿을 저장합니다. 암호화된 시크릿은 해당 클러스터에서만 복호화(Decrypt)될 수 있어 안전합니다. 제가 홈랩에서도 가장 즐겨 쓰는 방식이에요.
    • External Secrets Operator: AWS Secrets Manager, Azure Key Vault, Google Secret Manager, HashiCorp Vault 등 클라우드(Cloud) 기반 시크릿 관리 서비스와 연동하여 시크릿을 쿠버네티스 시크릿으로 동기화합니다. 프로덕션 환경에서는 이 방식이 가장 일반적입니다.

    어떤 방법을 선택하든, 절대 시크릿을 Git에 직접 올리는 일은 없어야 합니다! 이건 제가 정말 피땀 흘려 배운 교훈입니다. ⚠️

    3. Private Repository 접근

    대부분의 프로덕션 환경에서는 프라이빗 Git 리포지토리(Private Git Repository)를 사용합니다. ArgoCD가 이 리포지토리에 접근하려면 인증 정보가 필요하겠죠. 다음 방법들을 사용할 수 있습니다.

    • SSH 키(SSH Key): 가장 일반적이고 강력한 방법입니다. ArgoCD에 SSH 키를 등록하고, 해당 키를 사용하여 리포지토리에 접근합니다.
    • HTTPS with Username/Password 또는 Personal Access Token (PAT): Git 서비스에서 발급받은 PAT를 ArgoCD에 등록하여 사용할 수 있습니다. 보안상 일반적인 비밀번호보다는 PAT를 사용하는 게 좋습니다.

    인증 정보를 ArgoCD에 등록할 때는 쿠버네티스 시크릿으로 안전하게 관리해야 합니다. 당연한 이야기지만, 이걸 놓쳐서 문제가 되는 경우를 꽤 많이 봤거든요. 😅

    ArgoCD 트러블슈팅: 제가 겪었던 문제들 (그리고 해결책)

    13년차 엔지니어라고 해도 삽질은 피할 수 없는 운명이죠. ArgoCD를 쓰면서도 여러 번 머리를 쥐어뜯었습니다. 몇 가지 흔한 문제와 해결책을 공유해볼게요.

    • 문제: ImagePullBackOff 에러 (프라이빗 레지스트리)
      상황: 배포된 Pod에서 프라이빗 컨테이너 이미지 레지스트리(Private Container Image Registry)의 이미지를 당겨오지 못하고 ImagePullBackOff 에러가 발생하는 경우.
      해결: 해당 네임스페이스에 ImagePullSecrets를 제대로 설정했는지 확인해야 합니다. ArgoCD 애플리케이션이 배포될 때 이 시크릿이 같이 생성되도록 매니페스트에 포함하거나, 수동으로 시크릿을 생성하고 Pod Spec에 추가해야 합니다. 저는 주로 ArgoCD가 CreateNamespace=true와 함께 시크릿도 함께 배포하도록 설정합니다.

    • 문제: Resource is not available 또는 Failed to install CRD
      상황: 애플리케이션 배포 시 커스텀 리소스 정의(CRD: Custom Resource Definition)가 먼저 설치되어야 하는데, 순서가 맞지 않아 발생하는 에러.
      해결: ArgoCD의 Sync Waves 기능을 활용하면 배포 순서를 제어할 수 있습니다. CRD를 먼저 배포하고, 그 이후에 CRD를 사용하는 애플리케이션 리소스를 배포하도록 설정하는 거죠. 예를 들어, CRD는 sync-wave: "-1", 일반 리소스는 sync-wave: "0"으로 설정하면 됩니다. 이 기능을 알게 된 후로 정말 속이 시원했습니다. 🎉

    • 문제: 롤백 후에도 문제가 지속됨
      상황: Git Revert나 ArgoCD UI 롤백을 했는데도 애플리케이션이 정상 상태로 돌아오지 않는 경우.
      해결: 롤백 대상이 되는 커밋이 정말 이전의 ‘정상’ 상태를 가리키는지 확인해야 합니다. 때로는 환경 변수나 외부 의존성(예: 데이터베이스 스키마 변경) 때문에 롤백만으로는 해결되지 않을 수 있거든요. 이럴 때는 문제 발생 시점을 정확히 파악하고, 관련된 모든 변경사항(Git, DB 스키마, 외부 서비스 설정 등)을 함께 되돌려야 합니다. 그리고 롤백 후 ArgoCD UI에서 애플리케이션의 Health 상태와 Events 탭을 꼼꼼히 확인하는 습관을 들이는 게 좋습니다.

    결과 및 검증: 안정적인 배포, 이제 Git만 바라봅니다.

    ArgoCD와 GitOps 베스트 프랙티스를 적용하고 나니, 정말 배포 과정이 안정적이고 예측 가능해졌습니다. 더 이상 불안에 떨며 배포 버튼을 누를 필요가 없어졌죠. ✅

    배포가 완료되면 ArgoCD 웹 UI에서 각 애플리케이션의 상태를 한눈에 확인할 수 있습니다. 모든 리소스가 Healthy 상태이고, Synced 상태인지 확인하는 것이 중요해요. 혹시 OutOfSync 상태라면, 어떤 리소스가 Git과 다른지 명확하게 보여주므로 문제 파악이 매우 쉽습니다. 이 시각적인 피드백(Feedback) 덕분에 트러블슈팅 시간도 대폭 줄었습니다.

    ArgoCD UI에서 모든 애플리케이션이 건강하고(Healthy) 동기화(Synced)된 상태를 보여주는 화면입니다. 이 화면을 볼 때마다 뿌듯하더라고요!

    실제로 저는 홈랩에서 여러 서비스를 ArgoCD로 관리하고 있는데, Git에 커밋만 하면 알아서 배포가 되고, 문제가 생겨도 Git Revert 한 번으로 해결되는 경험은 정말 혁신적이었습니다. CI/CD 파이프라인의 완성도를 높이는 데 ArgoCD가 결정적인 역할을 했다고 생각합니다.

    ArgoCD GitOps를 프로덕션 환경에 적용하기 위한 핵심 베스트 프랙티스를 요약한 인포그래픽입니다.

    마무리: GitOps 여정, 계속됩니다

    오늘은 ArgoCD 프로덕션 환경 GitOps 베스트 프랙티스에 대해 저의 경험을 바탕으로 이야기해봤습니다. 안정적인 배포와 GitOps 보안 강화를 위해 Git 리포지토리 구조화, 환경별 설정 관리, Sync Policy, 롤백 전략, RBAC, 시크릿 관리 등 다양한 측면을 다루었네요.

    처음에는 복잡하게 느껴질 수도 있지만, 한 번 제대로 구축해두면 개발팀과 운영팀 모두에게 엄청난 효율성과 안정성을 가져다줄 겁니다. 저도 아직 배워야 할 것이 많고, 계속해서 새로운 기술을 실험하고 있습니다. 이 글이 여러분의 쿠버네티스 배포 여정에 작은 등불이 되기를 바랍니다. 궁금한 점이나 공유하고 싶은 삽질 경험이 있다면 언제든지 댓글로 남겨주세요! 다음에는 ArgoCD와 함께 CI/CD 파이프라인을 더욱 자동화하는 방법에 대해 다뤄보겠습니다. 그때까지 모두 즐거운 서버실 라이프 즐기시길 바랍니다! 😊