2주차 (Ch3~4)

3장. 첫 번째 배포 파이프라인

지금까지 구성한 서비스의 배포 방식은 항상 생성형 AI 에게 질의를 통해 진행했었다.

이 방식에서 마주칠 수 있는 문제점은, AI 에게 질의를 했든 직접 클러스터의 구성 정보를 변경했든 간에 변경한 내용이 이력으로 남지 않는다는 것이다.

설정 드리프트가 발생하는 이유

📋

흔히 겪는 사례 — 쿠버네티스 설정 정보 변경하기

컨피그맵에 설정된 정보를 구성원이 임의로 변경했을 경우, 다른 동료가 이를 알아차리기 어렵다. 실제 정보와 코드로 관리하는 정보가 서로 다른 이 현상을 설정 드리프트라고 하며, 이 현상이 발생하는 순간부터 유지해야 하는 “진짜” 소스를 찾기 난해하다.

설정 드리프트의 구조적 문제

설정 드리프트를 해결하는 첫 번째 단계는 명령형을 선언형으로 관리하는 것이다

기존 작업 방식은 명령형이고, 이 방식으로 모든 리소스를 원하는 대로 구성했다 하더라도 실행이 끝나고 나면 흔적이 남지 않는다.

반대로 선언형은 매번 YAML 로 구성된 매니페스트를 수정한 뒤 깃에 동기화를 해야만 반영된다. 쿠버네티스는 철학 자체가 선언형이며, 왜 이 작업을 했는지에 대한 기록이 남게 된다.

설정 드리프트 자체가 불가능한 전략, GitOps

GitOps 를 구성하기 위해 자동으로 배포되는 파이프라인을 구축해야 한다. 그러기 위해서는 어떤 도구가 현재 상황에 적합한지 분석하는 것이 필요하다.

기술을 선택하기 위해 클로드 코드에게 질문을 해보면, 아래 이미지처럼 ArgoCD 를 추천하는 것을 볼 수 있다.

그 외 다른 도구들과 함께 비교해 보며 우리 서비스에 왜 ArgoCD 를 선택하게 되었는지 기술 선택의 기준을 물어보면 아래와 같이 친절하게 답변해 준다.

매니지먼트 UI 가 있다는 점이 큰 선택 이유로 다가왔을 것 같다. GitOps 는 결국 데브옵스 조직 외에 개발 조직도 접근해서 서비스를 배포해야 하는데, 웹 환경이 전혀 없고 오로지 터미널에서만 관리해야 한다면 그 마저도 불편함이 될 수 있기 때문일 것이다.

GitOps 자동 배포 파이프라인을 위해 ArgoCD 를 구성하고, 소스 레포지토리를 나의 서비스 레포지토리와 연결해 주면 자동으로 변경 사항을 감지하고 클러스터에 동기화를 시도할 것이다.

앞서 설정 드리프트에서 설명한 내용 중, 깃허브의 설정 정보와 쿠버네티스 리소스의 설정 정보가 다를 경우 ArgoCD 는 내부 캐시와 API 서버를 통해 이 차이를 판단하고 깃허브의 설정 정보로 동기화하는 특징이 있다.

그렇기 때문에 GitOps 전략에서 ArgoCD 를 도입했을 때, 깃허브에 매니페스트를 수정하면 ArgoCD 가 변경 사항을 감지하고 동기화하므로 지속적으로 배포가 이루어지는 구조를 구성할 수 있다.

만약 깃허브에 등록된 애플리케이션 버전이 잘못되어 당장 되돌려야 한다면 어떻게 해야 할까?

현재 프로젝트에서는 git revert 를 통해 깃허브 상태를 되돌려 이전 상태로 다시 동기화하는 방식이다.

이 방식은 깃허브 저장소와 ArgoCD 가 상시 동기화를 맺고 있다는 가정하에 선택하며, 운영 레벨에서는 자동 동기화 옵션을 비활성화해 둔 후 History And Rollback 을 활용해 되돌리고 원복시키는 방향을 선택한다.

코드가 변경되면 자동으로 배포까지

지금은 AI 가 작업을 도와주면서 변경 사항이 있을 때마다 gcloud builds submit 명령을 실행하고, 깃허브에 커밋 해시값을 업데이트해 주고 있었다.

실제로 AI 를 쓰지 않는 환경에서는 이 작업들이 모두 수동으로 이루어지는데, 이 과정을 한 단계 더 추상화시켜 코드 수정 사항을 깃허브에 올리기만 했을 뿐인데 배포까지 이루어지는 파이프라인을 구성하여 편의성을 갖춘다.

애플리케이션 빌드 자동화와 GitOps 레포지토리에 앱 변경 사항 커밋은 GitHub Actions 를 활용한다. 도구 선택 과정에서 가장 쉽게 구성할 수 있고 별도의 서버가 필요 없기 때문에 선택했다.

💡

시크릿 키 파일 관리

현재는 GCP 에서 키 파일을 발급받아 GitHub Secrets 에 저장하여 CI 가 참조하는 방식이다.

이 구조는 키 파일을 생성·관리하며 실수로 유출될 수 있는 우려가 있으므로, 구글 공식 문서에서도 가이드하는 WIF(Workload Identity Federation) 방식을 적용하는 것을 권장한다.

WIF 란 GitHub Actions 의 신원을 GCP 가 믿어주는 구조로, 장기 보관되는 키 값이 존재하지 않고 깃허브 토큰은 실행될 때마다 새로 생성되며 몇 분 내로 만료된다.

다만 GCP 초기 설정이 복잡하기 때문에, 간편하게 프로젝트를 구성하고 싶다면 기존 키 기반으로 활용하기도 한다.

실제 CI 내부 로직을 들여다보면 이미지 빌드와 GitOps 레포지토리의 매니페스트 파일을 업데이트할 뿐, 배포를 담당하지 않고 별도의 도구인 ArgoCD 가 이를 맡아 역할이 분리되어 있다.

그렇다면 CI 는 왜 kubectl 명령어로 직접 배포하지 않는 걸까?

CI 가 kubectl 을 수행해 직접 배포를 진행하면 매니페스트 파일을 업데이트하지 않으니 깃에 이력이 남지 않는다.

깃허브에 이력이 남지 않고 배포가 되는 순간부터, 어떤 코드가 실제 배포되어 운영 중인지 알기 어려우며 롤백하기도 어려운 단점이 있다.

이제 CI 를 통해 매니페스트를 업데이트하고, ArgoCD 가 이를 감지하여 ArgoCD repository 파드가 캐싱하고 있는 매니페스트와 다를 경우 동기화 과정을 진행하게 된다.

name: CI - Build and Push Image

on:
  push:
    branches:
      - main
    paths:
      - "app/**"
      - ".github/workflows/ci.yaml"

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          ref: main

      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v2
        with:
          credentials_json: ${{ secrets.GCP_SA_KEY }}

      - name: Configure Docker for Artifact Registry
        run: gcloud auth configure-docker asia-northeast3-docker.pkg.dev --quiet

      - name: Build and push image (SHA tag)
        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_URI=$IMAGE" >> $GITHUB_ENV

      - name: Update manifest image tag and push
        run: |
          echo "New image: $IMAGE_URI"
          # guard: IMAGE_URI가 비면 sed가 이미지 라인을 지우므로 즉시 실패시킨다
          test -n "$IMAGE_URI"
          sed -i "s|image: asia-northeast3-docker.pkg.dev/.*/notiflex/api:.*|image: ${IMAGE_URI}|" k8s/smb/deployment.yaml
          grep "image:" k8s/smb/deployment.yaml
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add k8s/smb/deployment.yaml
          if git diff --cached --quiet; then
            echo "매니페스트 변경 없음 — 커밋 생략"
          else
            git commit -m "ci: update image to sha-${GITHUB_SHA::7} [skip ci]"
            git push origin HEAD:main
          fi

CI 가 이미지 빌드 후 커밋 해시값을 다시 푸시하면 무한 반복으로 빌드되지 않을까?

CI 를 구성할 때 해당 레포지토리의 모든 변경 사항에 대해 구성하게 되면 무한 루프가 발생한다.

다만 위 코드처럼 app/ 디렉터리 내에 위치한 코드 변경만 동작하도록 트리거 범위를 좁히면 무한 루프를 방어할 수 있다.

📝

정리

이 챕터에서는 깃허브에 변경된 코드만 push 하면 자동으로 배포되는 체계를 구성했다.

  • ArgoCD 깃허브의 변경 사항을 감지하고 클러스터 상태를 자동으로 동기화한다.
  • GitHub Actions(CI) 이미지를 빌드하고 컨테이너 레지스트리에 배포한다.
  • CI 와 ArgoCD 연결 CI 가 컨테이너 레지스트리에 배포한 뒤 매니페스트 태그를 교체하여 깃허브에 반영하면, ArgoCD 가 이를 감지하여 동기화한다.

4장. 관측 가능성 한 번에 구축하기

깃허브에 코드를 반영했을 뿐인데 자동으로 배포까지 완성되었다. 하지만 어느 날 갑자기 서비스에 장애가 발생하고 운영 문의가 들어온다면, 이를 어떻게 탐지하고 해결해 나가야 할까?

관측 가능성

관측 가능성은 메트릭, 로그, 트레이스, 프로파일 4 요소로 이루어져 있다.

관측성 도구의 데이터 수집 방법

Push 기반 수집 방법

모니터링 대상이 되는 앱이 모니터링 서버에 데이터를 전송하는 방식이다. 앱마다 전송 코드를 심어야 하고, 모니터링 서버가 죽으면 데이터를 받을 수 없어 유실된다.

Pull 기반 수집 방법

모니터링 서버가 대상 앱의 메트릭 데이터를 가져오는 방식이다. 메트릭을 노출하는 엔드포인트로 현재 서버 상태만 응답해 주면, 관측성 도구가 그 데이터를 수집하여 화면에 노출한다.

📌

Pull 방식의 장점

  1. 모니터링 대상이 서버로 데이터를 전송할 필요가 없어 전송 코드를 일일이 넣지 않아도 된다.
  2. 새 Pod 가 생성되면 관측성 도구가 자동으로 발견해 메트릭을 수집한다.
  3. 모니터링 서버 장애가 나도 앱은 영향을 받지 않는다.

쿠버네티스 환경에서 메트릭 데이터 수집 방법

쿠버네티스 표준 도구로 구성된 관측성 Helm 차트를 설치하면 Prometheus, Grafana, Alertmanager, node-exporter, kube-state-metrics 가 한 번에 설치된다.

로그 수집

메트릭은 “무엇이 잘못됐는지”는 알려주지만 “왜 잘못됐는지”는 알려주지 않기 때문에, 그 이유는 로그에서 찾아야 한다.

메트릭으로 에러가 났다는 것은 알 수 있지만, 무슨 에러인지 알려면 Pod 의 로그를 조회해 봐야 한다.

만약 서비스 장애와 직결된 에러 로그인데 파드가 재시작된다면 어떻게 해야 할까?

이런 상황 때문에 중앙화된 로그 수집 파이프라인이 필요하다. 이 프로젝트에서는 Loki + Fluent Bit 를 사용한다.

📝

라벨 기반 인덱싱과 풀텍스트 인덱싱

풀텍스트 인덱싱은 로그 본문의 모든 단어를 색인하는 방식으로 ElasticSearch 가 사용한다. “connection” 같은 단어가 어떤 로그에 있는지 바로 검색할 수 있지만 메모리를 많이 쓰며, 엘라스틱서치는 최소 2GB 가 요구된다.

라벨 기반 인덱싱은 메타데이터만 인덱싱하고 본문은 인덱싱하지 않는다. {namespace=notiflex} 같은 라벨로 먼저 범위를 좁히고 그 안에서 본문을 grep 으로 찾는다. 풀텍스트보다 느리지만 메모리가 128Mi 로 경량화된 환경에서도 쓸 수 있다.

이상 탐지 시 알림 체계 구축하기

프로메테우스와 그라파나로 웹 환경에서 로그와 메트릭을 편리하게 볼 수 있게 되었다.

작업자는 24시간 대시보드를 보고 있을 수 없으므로, 중요한 이벤트가 발생할 때만 보도록 알림을 구성하는 것이 관측의 핵심이다.

현재 프로젝트에 사용하는 도구와 결합할 수 있는 PrometheusRuleAlertmanager 를 사용하며, Grafana UI 로 설정하는 알림은 변경 사항이 코드로 남지 않기 때문에 배제한다.

💡

어떤 알림을 받을지 고민될 때에는, 서비스 장애를 탐지하는 요소 위주로 구성하자

  1. Pod 가 여러 번 재시작되는 경우 CrashLoopBackOff, OOMKill 감지
  2. CPU/메모리 임계치 특정 노드의 자원 사용량 과다
  3. 5XX 에러 발생율 서비스 응답 에러
  4. 배포 실패 ArgoCD Sync 실패 탐지