[카테고리:] 미분류

  • 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, 비용 최적화로 갈게요.