-
CI/CD 파이프라인 구축: GitHub Actions로 완전 자동화하기실제 경험과 인사이트를 AI와 함께 정리한 글 2025. 10. 22. 00:04
CI/CD 파이프라인 구축: GitHub Actions로 완전 자동화하기
작성일: 2025-10-21
태그: CI/CD, GitHub Actions, Docker, Helm, GitOps
난이도: 중급~고급들어가며
"코드를 푸시하면 자동으로 테스트되고, 빌드되고, 배포되는 세상"
이 글에서는 imprun.dev 프로젝트의 CI/CD 파이프라인 구축 과정을 단계별로 소개합니다. GitHub Actions를 활용하여 개발자가
git push만 하면 프로덕션까지 자동 배포되는 완전한 자동화를 구현합니다.목표 아키텍처
┌──────────────┐ │ Developer │ │ git push │ └──────┬───────┘ │ ▼ ┌──────────────────────────────────────────────────────┐ │ GitHub Actions Workflow │ ├──────────────────────────────────────────────────────┤ │ 1. Lint & Test (타입 체크, 단위 테스트) │ │ 2. Build Docker (멀티 플랫폼: amd64 + arm64) │ │ 3. Push to Registry (Docker Hub / ECR) │ │ 4. Update Helm (values 파일 버전 업데이트) │ │ 5. Deploy to K8s (kubectl / ArgoCD) │ │ 6. Smoke Test (Health Check) │ │ 7. Notify (Slack / Discord) │ └──────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────┐ │ Kubernetes │ │ Cluster │ │ (Production) │ └──────────────────┘
Part 1: GitHub Actions 기초
Workflow 파일 구조
# .github/workflows/deploy.yml name: Deploy to Production # Workflow 이름 on: # 트리거 조건 push: branches: [main] pull_request: branches: [main] jobs: # 실행할 작업들 test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run tests run: npm test build: needs: test # test 성공 후 실행 runs-on: ubuntu-latest steps: - name: Build Docker image run: docker build -t myapp .Secrets 관리
GitHub Repository Settings → Secrets and Variables → Actions에서 등록:
# Workflow에서 사용 env: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} KUBECONFIG_DATA: ${{ secrets.KUBECONFIG_DATA }}필요한 Secrets:
DOCKER_USERNAME,DOCKER_PASSWORD: Docker Hub 인증KUBECONFIG_DATA: Kubernetes 클러스터 접근SLACK_WEBHOOK_URL: 알림 (선택)
Part 2: 단계별 파이프라인 구축
Step 1: Lint & Test
목적: 코드 품질 검증, 버그 조기 발견
# .github/workflows/test.yml name: Test & Lint on: push: branches: [main, develop] pull_request: branches: [main] jobs: test-server: name: Test API Server runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' cache: 'pnpm' - name: Install pnpm uses: pnpm/action-setup@v2 with: version: 8 - name: Install dependencies run: pnpm install --frozen-lockfile - name: TypeScript type check working-directory: ./server run: pnpm typecheck - name: ESLint working-directory: ./server run: pnpm lint - name: Run unit tests working-directory: ./server run: pnpm test env: NODE_ENV: test DATABASE_URL: mongodb://localhost:27017/test_db # MongoDB 서비스 컨테이너 services: mongodb: image: mongo:6.0 ports: - 27017:27017 options: >- --health-cmd "mongosh --eval 'db.adminCommand({ping: 1})'" --health-interval 10s --health-timeout 5s --health-retries 5 test-console: name: Test Web Console runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '18' cache: 'pnpm' - uses: pnpm/action-setup@v2 with: version: 8 - run: pnpm install --frozen-lockfile - name: Next.js build test working-directory: ./frontend run: pnpm build - name: TypeScript check working-directory: ./frontend run: pnpm typecheck핵심 포인트:
services로 MongoDB 테스트 환경 구성working-directory로 Monorepo 대응cache: 'pnpm'으로 의존성 캐싱 (빌드 시간 단축)
Step 2: Docker 빌드 & 푸시 (멀티 플랫폼)
목적: ARM64 + AMD64 이미지 동시 빌드
# .github/workflows/build-and-push.yml name: Build & Push Docker Images on: push: tags: - 'v*' # v1.0.0, v1.0.1 등 env: REGISTRY: docker.io IMAGE_PREFIX: junsik jobs: build-server: name: Build imprun-server runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 with: platforms: linux/amd64,linux/arm64 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Extract version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push uses: docker/build-push-action@v5 with: context: ./server file: ./server/Dockerfile platforms: linux/amd64,linux/arm64 push: true tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/imprun-server:${{ steps.version.outputs.VERSION }} ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/imprun-server:latest cache-from: type=gha cache-to: type=gha,mode=max outputs: version: ${{ steps.version.outputs.VERSION }} build-console: name: Build imprun-console runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: docker/setup-qemu-action@v3 with: platforms: linux/amd64,linux/arm64 - uses: docker/setup-buildx-action@v3 - id: version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT - uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - uses: docker/build-push-action@v5 with: context: ./frontend file: ./frontend/Dockerfile platforms: linux/amd64,linux/arm64 push: true tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/imprun-console:${{ steps.version.outputs.VERSION }} ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/imprun-console:latest cache-from: type=gha cache-to: type=gha,mode=max outputs: version: ${{ steps.version.outputs.VERSION }}핵심 포인트:
docker/setup-qemu-action: 멀티 플랫폼 빌드 활성화cache-from/cache-to: type=gha: GitHub Actions 캐시 활용 (빌드 시간 70% 단축)tags: 버전 태그 + latest 동시 푸시
Step 3: Helm Values 업데이트 (GitOps)
목적: 배포 버전 추적, 롤백 가능성
# .github/workflows/update-helm-values.yml name: Update Helm Values on: workflow_run: workflows: ["Build & Push Docker Images"] types: - completed jobs: update-values: name: Update Helm Values runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - name: Checkout uses: actions/checkout@v4 with: token: ${{ secrets.PAT_TOKEN }} # Personal Access Token (write 권한) - name: Extract version id: version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT - name: Update values-production.yaml run: | # yq 설치 (YAML 파서) sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 sudo chmod +x /usr/local/bin/yq # imprun-server 이미지 태그 업데이트 yq eval -i '.imprun-server.image.tag = "${{ steps.version.outputs.VERSION }}"' k8s/imprun/values-production.yaml # imprun-console 이미지 태그 업데이트 yq eval -i '.imprun-console.image.tag = "${{ steps.version.outputs.VERSION }}"' k8s/imprun/values-production.yaml - name: Commit and push run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add k8s/imprun/values-production.yaml git commit -m "chore: update image tags to ${{ steps.version.outputs.VERSION }}" git push결과:
# k8s/imprun/values-production.yaml (자동 업데이트됨) imprun-server: image: tag: 1.2.3 # v1.2.3 태그에서 자동 추출Step 4: Kubernetes 배포
방법 A: kubectl 직접 사용 (간단)
# .github/workflows/deploy-k8s.yml name: Deploy to Kubernetes on: push: branches: [main] paths: - 'k8s/imprun/values-production.yaml' jobs: deploy: name: Deploy to Production runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup kubectl uses: azure/setup-kubectl@v3 with: version: 'v1.28.0' - name: Configure kubeconfig run: | mkdir -p ~/.kube echo "${{ secrets.KUBECONFIG_DATA }}" | base64 -d > ~/.kube/config chmod 600 ~/.kube/config - name: Install Helm uses: azure/setup-helm@v3 with: version: 'v3.13.0' - name: Deploy with Helm run: | cd k8s/imprun helm dependency update helm upgrade imprun . \ --namespace imprun-system \ --values values-production.yaml \ --wait \ --timeout 10m - name: Verify deployment run: | kubectl rollout status deployment/imprun-imprun-server -n imprun-system kubectl rollout status deployment/imprun-imprun-console -n imprun-system - name: Run smoke test run: | # Health check 엔드포인트 확인 kubectl run curl-test --rm -i --restart=Never --image=curlimages/curl -- \ curl -f http://imprun-imprun-server.imprun-system.svc/v1/health/liveness echo "✅ Deployment successful!"방법 B: ArgoCD (GitOps, 권장)
# .github/workflows/trigger-argocd.yml name: Trigger ArgoCD Sync on: push: branches: [main] paths: - 'k8s/imprun/values-production.yaml' jobs: sync: name: Sync ArgoCD Application runs-on: ubuntu-latest steps: - name: Trigger ArgoCD sync run: | # ArgoCD CLI 설치 curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64 chmod +x argocd sudo mv argocd /usr/local/bin/ # ArgoCD 로그인 argocd login ${{ secrets.ARGOCD_SERVER }} \ --username ${{ secrets.ARGOCD_USERNAME }} \ --password ${{ secrets.ARGOCD_PASSWORD }} \ --insecure # Application 동기화 argocd app sync imprun-production --prune # 동기화 완료 대기 argocd app wait imprun-production --healthArgoCD Application 정의:
# argocd/application.yaml apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: imprun-production namespace: argocd spec: project: default source: repoURL: https://github.com/imprun/imprun.dev targetRevision: main path: k8s/imprun helm: valueFiles: - values-production.yaml destination: server: https://kubernetes.default.svc namespace: imprun-system syncPolicy: automated: prune: true # 삭제된 리소스 자동 제거 selfHeal: true # Drift 자동 복구 allowEmpty: false syncOptions: - CreateNamespace=true revisionHistoryLimit: 10 # 롤백 가능한 히스토리 수
Part 3: 버전 태깅 자동화
시맨틱 버저닝 자동화
Conventional Commits 사용:
feat: 새로운 기능 추가 → MINOR 버전 증가 (1.2.0 → 1.3.0) fix: 버그 수정 → PATCH 버전 증가 (1.2.0 → 1.2.1) BREAKING CHANGE: 호환성 깨짐 → MAJOR 버전 증가 (1.2.0 → 2.0.0)semantic-release 설정:
# .github/workflows/release.yml name: Release on: push: branches: [main] jobs: release: name: Create Release runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # 전체 히스토리 필요 - uses: actions/setup-node@v4 with: node-version: '18' - name: Install semantic-release run: npm install -g semantic-release @semantic-release/git @semantic-release/changelog - name: Run semantic-release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: semantic-release설정 파일 (
.releaserc.json):{ "branches": ["main"], "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/changelog", [ "@semantic-release/git", { "assets": ["CHANGELOG.md", "package.json"], "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" } ], "@semantic-release/github" ] }결과:
- 커밋 분석 → 버전 결정
- CHANGELOG.md 자동 생성
- Git 태그 생성 (
v1.3.0) - GitHub Release 생성
- Docker 빌드 파이프라인 트리거!
Part 4: 자동 롤백 전략
Health Check 실패 시 자동 롤백
# .github/workflows/deploy-with-rollback.yml name: Deploy with Auto Rollback on: push: branches: [main] jobs: deploy: name: Deploy to Production runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # ... (kubectl 설정, Helm 배포) - name: Wait for rollout id: rollout run: | kubectl rollout status deployment/imprun-imprun-server -n imprun-system --timeout=5m continue-on-error: true - name: Health check id: health if: steps.rollout.outcome == 'success' run: | sleep 30 # Pod 안정화 대기 # 5번 재시도 for i in {1..5}; do if kubectl run curl-test --rm -i --restart=Never --image=curlimages/curl -- \ curl -f http://imprun-imprun-server.imprun-system.svc/v1/health/liveness; then echo "Health check passed!" exit 0 fi echo "Retry $i/5..." sleep 10 done echo "Health check failed!" exit 1 continue-on-error: true - name: Rollback on failure if: steps.health.outcome == 'failure' || steps.rollout.outcome == 'failure' run: | echo "🔴 Deployment failed! Rolling back..." # Helm rollback helm rollback imprun -n imprun-system # 롤백 성공 확인 kubectl rollout status deployment/imprun-imprun-server -n imprun-system # Slack 알림 curl -X POST ${{ secrets.SLACK_WEBHOOK_URL }} \ -H 'Content-Type: application/json' \ -d '{ "text": "❌ Production deployment failed and rolled back!", "attachments": [{ "color": "danger", "fields": [ {"title": "Commit", "value": "${{ github.sha }}", "short": true}, {"title": "Author", "value": "${{ github.actor }}", "short": true} ] }] }' exit 1 - name: Success notification if: steps.health.outcome == 'success' run: | curl -X POST ${{ secrets.SLACK_WEBHOOK_URL }} \ -H 'Content-Type: application/json' \ -d '{ "text": "✅ Production deployment successful!", "attachments": [{ "color": "good", "fields": [ {"title": "Version", "value": "${{ steps.version.outputs.VERSION }}", "short": true}, {"title": "Author", "value": "${{ github.actor }}", "short": true} ] }] }'Canary Deployment (점진적 배포)
# .github/workflows/canary-deploy.yml name: Canary Deployment on: push: tags: - 'v*' jobs: canary: name: Canary Deployment runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # ... (kubectl 설정) - name: Deploy canary (10%) run: | # Canary Deployment 생성 kubectl set image deployment/imprun-imprun-server-canary \ api-server=${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/imprun-server:${{ steps.version.outputs.VERSION }} \ -n imprun-system kubectl scale deployment/imprun-imprun-server-canary --replicas=1 -n imprun-system - name: Monitor canary run: | sleep 300 # 5분 모니터링 # 에러율 확인 (Prometheus) ERROR_RATE=$(curl -s 'http://vmsingle.monitoring.svc:8428/api/v1/query' \ --data-urlencode 'query=rate(http_requests_total{status=~"5..",deployment="canary"}[5m])') if (( $(echo "$ERROR_RATE > 0.01" | bc -l) )); then echo "Canary error rate too high! Aborting..." kubectl scale deployment/imprun-imprun-server-canary --replicas=0 -n imprun-system exit 1 fi - name: Promote canary to production run: | # Production Deployment 업데이트 kubectl set image deployment/imprun-imprun-server \ api-server=${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/imprun-server:${{ steps.version.outputs.VERSION }} \ -n imprun-system kubectl rollout status deployment/imprun-imprun-server -n imprun-system # Canary 제거 kubectl scale deployment/imprun-imprun-server-canary --replicas=0 -n imprun-system
Part 5: 고급 패턴
1. Matrix Strategy (병렬 빌드)
여러 서비스 동시 빌드:
jobs: build: name: Build ${{ matrix.service }} runs-on: ubuntu-latest strategy: matrix: service: - imprun-server - imprun-console - imp-runtime-exporter include: - service: imprun-server context: ./server dockerfile: ./server/Dockerfile - service: imprun-console context: ./frontend dockerfile: ./frontend/Dockerfile - service: imp-runtime-exporter context: ./runtimes/imp-runtime-exporter dockerfile: ./runtimes/imp-runtime-exporter/Dockerfile steps: - uses: actions/checkout@v4 - uses: docker/build-push-action@v5 with: context: ${{ matrix.context }} file: ${{ matrix.dockerfile }} platforms: linux/amd64,linux/arm64 push: true tags: junsik/${{ matrix.service }}:${{ github.sha }}2. Reusable Workflows
공통 워크플로우 재사용:
# .github/workflows/reusable-build.yml name: Reusable Build on: workflow_call: inputs: service-name: required: true type: string context: required: true type: string secrets: docker-username: required: true docker-password: required: true jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: docker/build-push-action@v5 with: context: ${{ inputs.context }} push: true tags: junsik/${{ inputs.service-name }}:latest호출:
# .github/workflows/build-server.yml name: Build Server on: [push] jobs: build-server: uses: ./.github/workflows/reusable-build.yml with: service-name: imprun-server context: ./server secrets: docker-username: ${{ secrets.DOCKER_USERNAME }} docker-password: ${{ secrets.DOCKER_PASSWORD }}3. Environment Protection Rules
GitHub Settings → Environments → production:
# .github/workflows/deploy.yml jobs: deploy: runs-on: ubuntu-latest environment: name: production url: https://app.imprun.dev steps: # ... 배포 스텝설정:
- ✅ Required reviewers: 1명 이상 승인 필요
- ✅ Wait timer: 5분 대기 (실수 방지)
- ✅ Deployment branches: main만 허용
Part 6: 모니터링 & 알림
Slack 알림 통합
- name: Notify deployment start uses: slackapi/slack-github-action@v1 with: payload: | { "text": "🚀 Deployment started", "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "*Deployment Started*\n*Commit:* <${{ github.event.head_commit.url }}|${{ github.sha }}>\n*Author:* ${{ github.actor }}" } } ] } env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}Discord Webhook
- name: Discord notification run: | curl -X POST ${{ secrets.DISCORD_WEBHOOK_URL }} \ -H 'Content-Type: application/json' \ -d '{ "username": "Deploy Bot", "avatar_url": "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png", "embeds": [{ "title": "✅ Deployment Successful", "description": "Version: v${{ steps.version.outputs.VERSION }}", "color": 5763719, "fields": [ {"name": "Commit", "value": "${{ github.sha }}", "inline": true}, {"name": "Author", "value": "${{ github.actor }}", "inline": true} ], "timestamp": "${{ github.event.head_commit.timestamp }}" }] }'
실전 시나리오
시나리오 1: 핫픽스 긴급 배포
# 1. hotfix 브랜치 생성 git checkout -b hotfix/critical-bug main # 2. 버그 수정 후 커밋 git commit -m "fix: critical security vulnerability (CVE-2024-1234)" # 3. 푸시 (자동 테스트 실행) git push origin hotfix/critical-bug # 4. PR 생성 & 승인 (Production environment protection) # 5. main 병합 → 자동 배포! git checkout main git merge hotfix/critical-bug git push # 6. GitHub Actions가 자동으로: # - 테스트 # - 빌드 (v1.2.4-hotfix.1 태그) # - 배포 # - Slack 알림소요 시간: 5~10분 (수동 승인 포함)
시나리오 2: 배포 실패 후 롤백
# Workflow 로그: # ✅ Build successful # ✅ Push to Docker Hub # ✅ Helm upgrade # ❌ Health check failed! (5/5 retries) # 🔄 Rolling back... # ✅ Rollback successful # Slack 알림: # ❌ Production deployment failed and rolled back! # Commit: abc123 # Reason: Health check timeout # 조치: # 1. 로컬에서 디버깅 # 2. 수정 후 재배포
체크리스트
CI/CD 파이프라인 설계
- 테스트 → 빌드 → 배포 단계 명확히 분리
- 각 단계마다 실패 시 중단
- 병렬 실행 가능한 작업은 Matrix Strategy 사용
- Reusable Workflows로 중복 제거
보안
- Secrets는 GitHub Secrets에 저장 (코드에 하드코딩 금지)
- KUBECONFIG는 base64 인코딩 후 저장
- Docker Hub Token 사용 (패스워드 대신)
- Environment Protection Rules 설정 (Production)
배포 안정성
- Health Check 구현 (Liveness + Readiness)
- Rollback 전략 수립
- Canary Deployment (선택)
- Deployment 타임아웃 설정 (10분)
모니터링
- Slack/Discord 알림 설정
- 배포 성공/실패 알림
- Rollout 상태 모니터링
- Workflow 실행 시간 추적
결론
완전 자동화된 CI/CD 파이프라인의 장점:
- 빠른 배포: 코드 푸시 → 프로덕션 배포 5분
- 안정성: 자동 테스트 + Health Check + Rollback
- 추적성: Git 태그 = Docker 태그 = Helm 버전
- 협업: PR 리뷰 → 승인 → 자동 배포
imprun.dev는 이 파이프라인을 통해 하루 10회 이상 안전하게 배포하며, 장애 발생 시 1분 내 롤백이 가능합니다.
참고 자료
질문이나 피드백은 GitHub Issues로!
📧 GitHub Issues시리즈 전체 보기
- Kubernetes 이미지 업데이트 전략
- Helm 차트 관리 Best Practices
- Kubernetes 리소스 최적화
- CI/CD 파이프라인 구축 (현재 글)
'실제 경험과 인사이트를 AI와 함께 정리한 글' 카테고리의 다른 글
Kubernetes 민감정보 관리 완벽 가이드: Secret, 암호화, 그리고 실전 전략 (0) 2025.10.22 Next.js SSR 환경에서 API URL 환경변수 관리 전략 (0) 2025.10.22 Kubernetes 리소스 최적화: ARM64 환경에서 효율적으로 운영하기 (0) 2025.10.22 Helm 차트 관리 Best Practices: Umbrella Chart부터 Secret 관리까지 (0) 2025.10.22 클러스터 전체 모니터링을 위한 VictoriaMetrics 설치 및 관리 (0) 2025.10.21