CI/CD & IaC


GitHub Actions · Docker · Terraform · Helm · GitOps(ArgoCD)


  • 모노레포 표준: apps/api(FastAPI), apps/web(Next.js), infra/terraform, deploy/helm
  • Docker: 멀티스테이지, non-root, SBOM(스빔) 생성 + 이미지 서명(cosign)
  • CI: lint/test → 이미지 빌드 & 취약점 스캔(Trivy) → SBOM(syft) → GHCR/ECR 푸시
  • CD: GitOps(Argo CD). Actions가 ops 저장소에 이미지 태그만 PR → 승인 → 자동 배포
  • IaC: Terraform으로 VPC/RDS/Redis/EKS(또는 ECS) + 원격상태(S3+DynamoDB)
  • 롤아웃: Argo Rollouts 카나리(90/10→100), 자동 분석(p95/에러율)
  • 마이그: Alembic 확장-이행-수축(expand→migrate→contract) 패턴, PreSync 훅
  • 시크릿: GitHub OIDC→클라우드 롤, External Secrets Operator(ESO)로 Secret 동기화

1) 리포지토리 구조(권장)

repo/
  apps/
    api/         # FastAPI
    web/         # Next.js
  deploy/
    helm/
      api/
      web/
      base/      # 공통 템플릿 (HPA/PDB/네임스페이스 등)
  infra/
    terraform/
      global/    # 네트워크, ECR
      envs/
        dev/
        prod/
  .github/workflows/

2) Docker 베스트프랙티스

FastAPI (멀티스테이지 + non-root)

# apps/api/Dockerfile
FROM python:3.12-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1
RUN addgroup --system app && adduser --system --ingroup app app
WORKDIR /app
COPY apps/api/requirements.txt .
RUN pip install --upgrade pip && pip install -r requirements.txt gunicorn uvicorn
COPY apps/api/ .
USER app
EXPOSE 8000
CMD ["gunicorn","-k","uvicorn.workers.UvicornWorker","-w","4","-b","0.0.0.0:8000","app.main:app"]

Next.js (standalone 런타임)

# apps/web/Dockerfile
FROM node:20-alpine AS deps
WORKDIR /app
COPY apps/web/package*.json ./
RUN npm ci

FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY apps/web/ .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
USER node
EXPOSE 3000
CMD ["node","server.js"]

.dockerignorenode_modules, .git, tests, .next/cache 등 반드시 제외.


3) GitHub Actions: 빌드·스캔·서명·푸시

공통 재사용 워크플로(s) 예시.github/workflows/build.yml

name: build-and-push
on:
  push:
    branches: [main]
    paths: [ "apps/**", "deploy/helm/**", ".github/workflows/**" ]
  workflow_dispatch:
jobs:
  matrix-build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        app: [api, web]
    permissions:
      contents: read
      packages: write
      id-token: write  # OIDC (클라우드 권한 부여 시)
    steps:
    - uses: actions/checkout@v4

    - name: Set up QEMU & Buildx
      uses: docker/setup-qemu-action@v3
    - uses: docker/setup-buildx-action@v3

    - name: Login GHCR
      uses: docker/login-action@v3
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Build image
      uses: docker/build-push-action@v6
      with:
        context: .
        file: apps/${{ matrix.app }}/Dockerfile
        push: true
        tags: |
          ghcr.io/${{ github.repository }}/${{ matrix.app }}:${{ github.sha }}
          ghcr.io/${{ github.repository }}/${{ matrix.app }}:main
        cache-from: type=gha
        cache-to: type=gha,mode=max
        provenance: true

    - name: Trivy scan
      uses: aquasecurity/trivy-action@0.24.0
      with:
        image-ref: ghcr.io/${{ github.repository }}/${{ matrix.app }}:${{ github.sha }}
        format: sarif
        output: trivy-${{ matrix.app }}.sarif

    - name: Upload SARIF
      uses: github/codeql-action/upload-sarif@v3
      with: { sarif_file: trivy-${{ matrix.app }}.sarif }

    - name: Generate SBOM (syft)
      uses: anchore/sbom-action@v0
      with:
        image: ghcr.io/${{ github.repository }}/${{ matrix.app }}:${{ github.sha }}
        format: spdx-json
        output-file: sbom-${{ matrix.app }}.spdx.json

    - name: Cosign sign (keyless)
      uses: sigstore/cosign-installer@v3
    - run: cosign sign ghcr.io/${{ github.repository }}/${{ matrix.app }}@${{ steps.meta.outputs.digest }} --yes
      env: { COSIGN_EXPERIMENTAL: "true" }

포인트

  • provenance true: SLSA 빌드 출처 제공
  • OIDC로 클라우드 자격증명 없이 권한 위임(배포 단계에서 사용)

4) GitOps CD(Argo CD) — “코드로만 배포”

흐름

  1. 앱 저장소(repo): 위 Actions가 이미지 태그를 ops 저장소로 PR
  2. ops 저장소(예: ex-ops): Helm values의 image.tag만 변경 → 승인
  3. Argo CD가 주기적으로 풀링 → 차이 감지 → Sync(자동/수동)

태그 바꿔주는 스텝(ops 리포 PR)

- name: Bump image tag in ops repo
  uses: peter-evans/create-pull-request@v6
  with:
    token: ${{ secrets.GH_TOKEN_WITH_REPO_SCOPE }}
    commit-message: "chore(${ {matrix.app} }): bump to ${{ github.sha }}"
    title: "bump(${ {matrix.app} }): ${{ github.sha }}"
    branch: bump/${{ matrix.app }}-${{ github.sha }}
    base: main
    body: "Update image tag"
    add-paths: deploy/helm/${{ matrix.app }}/values-prod.yaml

5) Helm 차트(핵심만)

values.yaml (api)

image:
  repository: ghcr.io/org/repo/api
  tag: "sha-PLACEHOLDER"
  pullPolicy: IfNotPresent

service:
  port: 8000

resources:
  requests: { cpu: "200m", memory: "256Mi" }
  limits:   { cpu: "1",   memory: "512Mi" }

env:
  - name: OTLP_ENDPOINT
    value: http://otel-collector:4317

securityContext:
  runAsNonRoot: true
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false

Deployment (발췌)

livenessProbe:  { httpGet: { path: /healthz, port: 8000 }, initialDelaySeconds: 10, periodSeconds: 10 }
readinessProbe: { httpGet: { path: /readyz,  port: 8000 }, initialDelaySeconds: 5,  periodSeconds: 5 }
podDisruptionBudget:
  minAvailable: 1
hpa:
  min: 2
  max: 10
  cpu: 70

보안/시크릿: External Secrets Operator(ESO) 사용

# ExternalSecret 예시
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
spec:
  refreshInterval: 1h
  secretStoreRef: { name: aws-sm, kind: ClusterSecretStore }
  target: { name: api-secrets }
  data:
    - secretKey: DB_URL
      remoteRef: { key: prod/api/DB_URL }

6) 롤아웃: 카나리 + 자동분석

Argo Rollouts(요지)

strategy:
  canary:
    steps:
    - setWeight: 10
    - pause: { duration: 2m }
    - analysis:
        templates:
        - templateName: apislo
    - setWeight: 50
    - pause: { duration: 3m }
    - analysis:
        templates:
        - templateName: apislo
    - setWeight: 100

AnalysisTemplate (Prometheus로 p95/에러율 검증)

apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
spec:
  metrics:
  - name: api-p95
    interval: 1m
    successCondition: result < 0.35   # 350ms 미만
    provider:
      prometheus:
        address: http://prometheus:9090
        query: >
          histogram_quantile(0.95, sum by (le)
          (rate(http_request_duration_seconds_bucket{app="api"}[1m])))
  - name: api-error-rate
    interval: 1m
    successCondition: result < 0.005  # 0.5% 미만
    provider:
      prometheus:
        address: http://prometheus:9090
        query: >
          sum(rate(http_requests_total{app="api",status=~"5.."}[1m])) /
          sum(rate(http_requests_total{app="api"}[1m]))

7) DB 마이그레이션(무중단 규칙)

  • 확장(Expand): 컬럼/인덱스 추가(널 허용, 동시 인덱스)
  • 이행(Migrate): 앱이 새 필드 읽기/쓰기 시작 (롤아웃)
  • 수축(Contract): 구 필드 제거(검증 기간 후)

Helm PreSync 훅으로 Alembic

apiVersion: batch/v1
kind: Job
metadata:
  annotations: { "argocd.argoproj.io/hook": PreSync }
spec:
  template:
    spec:
      restartPolicy: OnFailure
      containers:
      - name: migrate
        image: ghcr.io/org/repo/api:{{ .Values.image.tag }}
        command: ["alembic","upgrade","head"]
        envFrom: [{ secretRef: { name: api-secrets } }]

8) Terraform(IaC) 골격

원격 상태

# infra/terraform/envs/prod/backend.tf
terraform {
  backend "s3" {
    bucket = "ex-tfstate"
    key    = "prod/eks.tfstate"
    region = "ap-northeast-2"
    dynamodb_table = "ex-tf-lock"
    encrypt = true
  }
}

EKS + RDS + ElastiCache (요지)

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  name    = "ex-vpc"
  cidr    = "10.0.0.0/16"
  azs     = ["ap-northeast-2a","ap-northeast-2b"]
  private_subnets = ["10.0.1.0/24","10.0.2.0/24"]
  public_subnets  = ["10.0.101.0/24","10.0.102.0/24"]
}

module "eks" {
  source          = "terraform-aws-modules/eks/aws"
  cluster_name    = "ex-eks"
  vpc_id          = module.vpc.vpc_id
  subnet_ids      = module.vpc.private_subnets
  manage_aws_auth = true
  eks_managed_node_groups = {
    default = { desired_size = 3, instance_types = ["m7g.large"] }
  }
}

module "rds" {
  source  = "terraform-aws-modules/rds/aws"
  identifier = "ex-pg"
  engine = "postgres"
  instance_class = "db.m6g.large"
  allocated_storage = 100
  multi_az = true
  username = "ex"
  create_random_password = true
  subnet_ids = module.vpc.private_subnets
}

module "redis" {
  source  = "terraform-aws-modules/elasticache/aws"
  engine  = "redis"
  cluster_mode = false
  node_type = "cache.t4g.medium"
  num_cache_nodes = 1
  subnet_group_name = "ex-redis-subnet"
}

배포자격: GitHub OIDC → AWS IAM으로 롤연동(액세스키 무사용).
시크릿: AWS Secrets Manager에 저장 → ESO로 K8s에 주입.


9) 테스트 파이프라인(요약)

  • API: pytest -q + 커버리지, DB는 Testcontainers/SQLite 대체 금지
  • Web: jest(유닛) + playwright(E2E), PR마다 프리뷰 환경
  • 계약 테스트: BFF ↔ API에 OpenAPI 스냅샷 비교

10) 보안·컴플라이언스

  • 컨테이너 non-root, readOnlyRootFilesystem, Cap Drop ALL
  • 네임스페이스 분리(dev/stage/prod), NetworkPolicy 적용
  • 서명 검증: 레지스트리/Admission에서 cosign verify 강제
  • 런타임 보호: Falco/Datadog CWS(선택)

11) 48시간 액션 플랜

  • GHCR로 이미지 빌드/푸시 워크플로 적용(Trivy, syft, cosign 포함)
  • ops 저장소 분리 + Helm values에서 image.tag만 변경하는 PR 자동 생성
  • Argo CD 설치 후 apps/api, apps/web 배포 파이프라인 연결
  • Alembic PreSync Job로 마이그 테스트(Dev)
  • Terraform 원격상태(S3+DynamoDB) 구성, VPC/ECR/EKS 프로비저닝

12) 2주 액션 플랜

  1. Argo Rollouts + AnalysisTemplate로 카나리 본가동
  2. External Secrets Operator로 DB_URL/REDIS_URL 주입 전환
  3. GitHub OIDC→IAM 역할 연결(액세스키 전면 제거)
  4. Playwright E2E + 프리뷰환경 자동화(웹)
  5. SLSA/서명 검증 Admission(cosign-policy) 적용
  6. DB 마이그 expand→migrate→contract 체크리스트 문서화 & CI 게이트

13) “복붙” 모음

A. 브랜치 보호 & 환경 승인(요약)

  • main 보호, 필수 체크: 테스트·스캔·SBOM·서명
  • prod 배포는 환경 승인자 필요(GitHub Environments)

B. K8s 보안 컨텍스트(공통 템플릿)

securityContext:
  runAsNonRoot: true
  runAsUser: 10001
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false
  capabilities: { drop: ["ALL"] }

C. ServiceMonitor(메트릭 스크레이프)

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
spec:
  selector: { matchLabels: { app: api } }
  endpoints: [{ port: http, path: /metrics, interval: 30s }]

마무리

  • CI는 “빠른 피드백·공급망 보안(SBOM/서명/스캔)”,
  • CD는 “GitOps로 안전한 변경·점진적 롤아웃”,
  • IaC는 “재현 가능한 인프라·권한 최소화(OIDC)”.
    이 3박자를 고정하면 대규모 배포에서도 속도와 안정성을 동시에 잡을 수 있습니다.

다음은 10편. 쿠버네티스 운영 — 오토스케일링(HPA/KEDA), 롤아웃, Mesh, PDB, 비용 최적화로 갈게요.

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다