Go와 React로 만드는 실시간 채팅 서비스 개발기 (4) - GitHub Actions와 Argo CD를 활용한 GitOps CI/CD 파이프라인 구축

Administrator||조회수 35

지난 포스팅에서는 Terraform을 사용하여 GKE 클러스터와 Compute Engine(DB 서버), Artifact Registry 등 GCP 인프라를 코드로 구축(IaC)하는 과정을 다루었다. 인프라 프로비저닝은 완료되었으나, 실제 애플리케이션을 구동하기 위해서는 로컬에서 개발한 코드를 빌드하고 컨테이너화하여 클러스터에 배포하는 과정이 필요하다.

초기 개발 단계에서는 수동으로 docker build, docker push, kubectl apply 명령어를 반복하여 배포할 수 있겠지만, 이는 비효율적이며 실수할 여지가 많다. 따라서 이번 단계에서는 코드 변경 사항을 감지하여 자동으로 빌드 및 배포가 이루어지는 CI/CD(Continuous Integration/Continuous Deployment) 파이프라인을 구축하기로 했다. 특히 쿠버네티스 환경에 최적화된 GitOps 방법론을 적용하여, Git 저장소의 상태가 곧 실제 운영 환경의 상태가 되도록 구성했다.

1. CI/CD 아키텍처 설계 및 도구 선정

전체적인 배포 파이프라인은 CI(지속적 통합)CD(지속적 배포) 두 단계로 나뉜다. 각 단계에 적합한 도구를 선정하고 역할을 분리했다.

  • CI (GitHub Actions): 코드 저장소인 GitHub와의 연동성이 가장 뛰어나며, 별도의 CI 서버 구축 없이 워크플로우 파일(.yml)만으로 빌드 파이프라인을 정의할 수 있다. 코드가 main 브랜치에 푸시되면 테스트와 Docker 이미지 빌드를 수행하고, 결과물을 GCP Artifact Registry에 업로드하는 역할을 담당한다.
  • CD (Argo CD): 쿠버네티스 환경에서 사실상의 표준으로 자리 잡은 GitOps 도구다. 기존의 Jenkins와 같은 외부 CI 도구가 클러스터에 명령을 내리는 Push 방식과 달리, Argo CD는 클러스터 내부에서 동작하며 Git 저장소의 변경 사항을 감지하여 클러스터 상태를 동기화하는 Pull 방식을 사용한다. 이는 클러스터의 인증 정보를 외부에 노출할 필요가 없어 보안상 유리하며, 배포 상태를 직관적으로 모니터링할 수 있다는 장점이 있다.

2. CI 구축: GitHub Actions와 Artifact Registry 연동

CI 파이프라인 구축의 첫 단계는 GitHub Actions가 GCP의 Artifact Registry에 접근하여 Docker 이미지를 업로드할 수 있도록 권한을 부여하는 것이다.

최소 권한 원칙(Principle of Least Privilege) 적용 Phase 2에서 Terraform을 위해 생성했던 관리자급 권한(Editor)을 가진 서비스 계정을 CI에 그대로 사용하는 것은 보안상 위험하다. 따라서 이미지를 레지스트리에 업로드하는 작업만 수행할 수 있는 CI 전용 서비스 계정을 별도로 생성했다.

  1. GCP IAM: github-ci-sa라는 이름의 서비스 계정을 생성하고, Artifact Registry Writer 역할만 부여했다.
  2. GitHub Secrets: 생성한 서비스 계정의 JSON 키 파일 내용을 GitHub 저장소의 Secrets(GCP_CREDENTIALS)에 등록하여, 워크플로우 실행 시에만 안전하게 주입되도록 설정했다.

GitHub Actions 워크플로우 구현 (ci.yml) .github/workflows/ci.yml 파일을 작성하여 빌드 프로세스를 정의했다. 주요 단계는 다음과 같다.

  1. Checkout: 소스 코드를 가져온다.
  2. Auth: google-github-actions/auth 액션을 사용해 GCP에 인증한다.
  3. Docker Configure: gcloud CLI를 통해 Docker가 Artifact Registry(asia-northeast3-docker.pkg.dev)와 통신할 수 있도록 설정한다.
  4. Build & Push: 백엔드와 프론트엔드 각각에 대해 Docker 이미지를 빌드하고 레지스트리로 푸시한다.

이 과정에서 프론트엔드 빌드 시 환경 변수 주입에 대한 고려가 필요했다. React와 같은 SPA(Single Page Application)는 빌드 시점에 환경 변수가 정적 파일에 주입되어야 한다. 로컬 개발 환경(localhost)과 클라우드 배포 환경의 백엔드 API 주소가 다르기 때문에, 이를 동적으로 처리하기 위해 DockerfileARGENV를 설정하고, GitHub Actions에서 빌드 인자(--build-arg)로 API 주소를 전달하는 방식을 택했다.

# .github/workflows/ci.yml 발췌 - name: Build and Push Frontend run: | IMAGE_URI=${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPO_NAME }}/frontend # 빌드 시점에 백엔드 API 주소 주입 (추후 도메인 연결 시 변경 예정) docker build \ --build-arg VITE_API_BASE_URL=http://[LoadBalancer_IP] \ -t $IMAGE_URI:${{ github.sha }} \ -t $IMAGE_URI:latest \ -f apps/frontend/Dockerfile . docker push $IMAGE_URI:${{ github.sha }} docker push $IMAGE_URI:latest

3. CD 구축: Kubernetes Manifest 작성 및 Argo CD 연동

이미지가 준비되었으니, 이를 GKE 클러스터에 어떻게 배포할지 정의하는 Kubernetes Manifest 파일들을 작성했다. GitOps 철학에 따라 이 설정 파일들 역시 애플리케이션 코드와 함께 Git 저장소(k8s/ 디렉토리)에서 관리한다.

Kubernetes 리소스 정의 백엔드와 프론트엔드 각각에 대해 DeploymentService 리소스를 정의했다.

  • Deployment: 애플리케이션의 복제본(Replicas) 수, 사용할 컨테이너 이미지 경로, 포트 설정 등을 정의한다. 무중단 배포를 위한 전략(Rolling Update)은 쿠버네티스의 기본 동작을 따른다.
  • Service: LoadBalancer 타입을 사용하여 외부에서 파드(Pod)에 접근할 수 있도록 공인 IP를 할당받고, 트래픽을 분산시키는 역할을 한다.

Argo CD 설치 및 애플리케이션 생성 GKE 클러스터에 Argo CD를 설치하고, Git 저장소와 연동했다. Argo CD 대시보드에서 새로운 애플리케이션을 생성하며 Sync PolicyAutomatic으로 설정했다. 이를 통해 Git 저장소의 k8s/ 폴더 내 YAML 파일이 변경되거나, 리소스 정의가 실제 클러스터 상태와 달라질 경우 Argo CD가 이를 감지하고 자동으로 동기화(배포)를 수행하게 된다.

alt text

4. 트러블슈팅: 환경 차이와 보안 이슈 해결

CI/CD 파이프라인을 구축하고 첫 배포를 시도하는 과정에서, 로컬 환경에서는 발생하지 않았던 몇 가지 문제들이 드러났다. 이는 컨테이너화된 애플리케이션이 클라우드 환경에서 동작할 때의 특성을 간과했기 때문이었다.

이슈 1: 프론트엔드와 백엔드의 통신 실패 (CORS 및 주소 문제) 배포 직후 프론트엔드에서 백엔드 API 호출이 실패하는 현상이 발생했다.

  • 원인: 로컬 개발 시에는 localhost를 통해 통신했지만, GKE 환경에서는 프론트엔드(브라우저)가 백엔드 서비스의 실제 외부 IP를 알아야 했다. 또한, 도메인이 다른 환경에서의 요청이므로 브라우저의 CORS(Cross-Origin Resource Sharing) 정책에 의해 차단되었다.
  • 해결:
    1. kubectl get svc 명령어로 백엔드 서비스의 EXTERNAL-IP를 확인하고, 이를 GitHub Actions의 프론트엔드 빌드 인자(VITE_API_BASE_URL)로 주입하여 이미지를 다시 빌드했다.
    2. 백엔드 코드(main.go)의 CORS 미들웨어 설정에서 AllowOrigins를 수정하여 프론트엔드의 접근을 허용하도록 변경했다.

이슈 2: 백엔드 500 Internal Server Error (Secret 관리) API 호출은 성공했으나, 백엔드 서버가 500 에러를 반환했다. 로그를 확인해 보니 JWT 토큰 생성에 필요한 SECRET_KEY 환경 변수가 누락된 것이 원인이었다.

  • 원인: 로컬에서는 .env 파일을 통해 키를 주입했지만, 보안상 이 파일을 Git에 올리지 않았기 때문에 GKE상의 컨테이너에는 해당 환경 변수가 존재하지 않았다.
  • 해결: Kubernetes의 Secret 리소스를 활용했다. 보안을 위해 YAML 파일에 키 값을 직접 적는 대신, 터미널에서 kubectl create secret 명령어로 클러스터에 직접 비밀 값을 저장했다. 그리고 Deployment 매니페스트에서 valueFrom: secretKeyRef를 사용하여 해당 Secret을 환경 변수로 주입하도록 설정했다.
# k8s/backend.yaml (Secret 주입 설정) env: - name: SECRET_KEY valueFrom: secretKeyRef: name: backend-secrets key: SECRET_KEY

이슈 3: HTTP 환경에서의 보안 제약 (crypto.randomUUID) 프론트엔드에서 세션 생성 시 TypeError: crypto.randomUUID is not a function 오류가 발생했다.

  • 원인: crypto.randomUUID() API는 보안 컨텍스트(Secure Context), 즉 HTTPS 또는 localhost 환경에서만 사용할 수 있다. 현재는 도메인 없이 IP 주소(HTTP)로 접속하고 있어 브라우저가 해당 API를 차단한 것이다.
  • 해결: HTTPS 적용 전까지 임시로 uuid 라이브러리를 사용하여 UUID를 생성하도록 코드를 수정했다. 이는 추후 도메인 연결 및 SSL 인증서 적용 단계에서 다시 네이티브 API로 전환할 예정이다.

5. GitOps 파이프라인의 완성

이러한 과정을 거쳐 로컬에서 코드를 수정하고 git push를 수행하면, 자동으로 빌드와 테스트를 거쳐 GKE 클러스터에 배포되는 완전 자동화된 파이프라인을 완성했다.

특히 인상적이었던 점은 Argo CD의 동작 방식이었다. 이미지 태그를 latest로 고정해서 사용할 경우, 이미지가 갱신되어도 YAML 파일 내용에 변화가 없어 Argo CD가 배포를 트리거하지 않는 문제가 있었다. 이를 해결하기 위해 실무에서는 이미지 태그에 Git 커밋 해시(sha)를 사용하고, CI 파이프라인의 마지막 단계에서 YAML 파일의 이미지 태그를 자동으로 업데이트하여 커밋하는 방식을 사용한다는 점을 학습했다. 현재는 학습 편의상 수동 동기화를 사용하고 있지만, 추후 고도화 단계에서 이 부분까지 자동화할 계획이다.

마치며

이번 단계를 통해 인프라 구축에 이어 배포 자동화까지 완료하며, 개발자가 인프라나 배포 과정에 신경 쓸 필요 없이 오직 코드 작성에만 집중할 수 있는 환경을 마련했다.

다음 포스팅에서는 이 서비스에 도메인(Domain) 을 연결하고 HTTPS를 적용하며, PostgreSQL 데이터베이스를 연동하여 데이터를 영구적으로 저장하는 서비스 안정화 및 통합 과정을 다룰 예정이다. 또한, 현재 메모리에만 의존하고 있는 메시지 저장 방식을 DB로 전환하고, Prometheus와 Grafana를 도입하여 서비스 상태를 모니터링하는 방법도 함께 살펴볼 것이다.

Administrator
Written by

Administrator

안녕하세요! Deep Dive! 블로그 제작자 입니다.

댓글을 불러오는 중...
대화방 참여하기 👋