โ† Semua pembelajaran / Astro Nol โ†’ Portal Berita
Fase 10 ยท Deploy di AWS

CI/CD dengan GitHub Actions

Pipeline yang baik bukan yang cepat, melainkan yang menangkap masalah sebelum pembaca yang menangkapnya. Ini gerbang-gerbang yang layak dipasang.

Intisari

  • Gunakan OIDC ke AWS, bukan kunci akses jangka panjang di GitHub secrets.
  • Gerbang wajib: astro check, tes, audit produksi, dan pemindai rahasia di bundel klien.
  • Migrasi database dijalankan sebagai tugas ECS terpisah, sebelum deploy service.
  • Uji asap setelah deploy โ€” dan rollback otomatis kalau gagal.
  • Tag image dengan SHA git. latest membuat rollback jadi tebakan.

Alur

Pull request
  โ””โ”€ astro check, lint, tes, audit, pemindai rahasia          (~3 menit)

Merge ke main
  โ”œโ”€ Bangun image ARM64, dorong ke ECR dengan tag SHA
  โ”œโ”€ Jalankan migrasi database sebagai tugas ECS terpisah
  โ”œโ”€ Perbarui service ECS ke revisi baru
  โ”œโ”€ Tunggu deployment stabil
  โ”œโ”€ Uji asap terhadap produksi
  โ””โ”€ Kalau gagal โ†’ rollback ke revisi sebelumnya

Gerbang pull request

name: pr
on: pull_request

jobs:
  periksa:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0
        env: { MYSQL_ROOT_PASSWORD: uji, MYSQL_DATABASE: portal_uji }
        ports: ["3306:3306"]
        options: >-
          --health-cmd="mysqladmin ping" --health-interval=5s --health-retries=10

    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with: { node-version-file: .nvmrc, cache: pnpm }

      - run: pnpm install --frozen-lockfile

      - name: Muat skema uji
        run: mysql -h 127.0.0.1 -u root -puji portal_uji < db/skema.sql

      - name: Tipe database mutakhir
        run: |
          pnpm db:types
          git diff --exit-code src/lib/db-types.ts

      - run: pnpm astro check
      - run: pnpm test

      - name: Audit dependensi produksi
        run: pnpm audit --prod --audit-level high

      - run: pnpm build

      - name: Tidak ada rahasia di bundel klien
        run: |
          POLA='Mid-server-|SB-Mid-server-|BEGIN [A-Z ]*PRIVATE KEY|AKIA[0-9A-Z]{16}'
          if grep -rlE "$POLA" dist/client/ 2>/dev/null | grep -q .; then
            echo "FATAL: rahasia di bundel klien"; exit 1
          fi

      - name: Anggaran JavaScript
        run: |
          TOTAL=$(find dist/client/_astro -name '*.js' -exec sh -c \
            'gzip -c "$1" | wc -c' _ {} \; | awk '{s+=$1} END {print s}')
          echo "Total JS (gzip): $TOTAL byte"
          [ "$TOTAL" -lt 200000 ] || { echo "FATAL: melewati anggaran"; exit 1; }

Dua gerbang terakhir itu khusus untuk kasusmu. Pemindai rahasia melindungi server key Midtrans โ€” kebocorannya berarti siapa pun bisa memalsukan webhook. Anggaran JavaScript melindungi Core Web Vitals, yang untuk portal berita berdampak langsung ke peringkat pencarian. Keduanya adalah hal yang akan perlahan memburuk tanpa pagar otomatis.

OIDC, bukan kunci akses

permissions:
  id-token: write
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/portal-deploy
      aws-region: ap-southeast-3

Kunci akses jangka panjang di GitHub secrets adalah kredensial permanen yang tidak pernah dirotasi. Kalau bocor โ€” lewat log, lewat fork, lewat aksi pihak ketiga yang dikompromikan โ€” ia berlaku sampai seseorang menyadarinya. OIDC menukar identitas GitHub dengan kredensial sementara berumur satu jam, dan bisa dibatasi ke repositori serta branch tertentu lewat kondisi trust policy.

Deploy

name: deploy
on:
  push: { branches: [main] }

concurrency:
  group: deploy-produksi
  cancel-in-progress: false      # jangan pernah membatalkan deploy di tengah

jobs:
  deploy:
    runs-on: ubuntu-24.04-arm    # runner ARM: bangun ARM64 tanpa emulasi
    permissions: { id-token: write, contents: read }

    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/portal-deploy
          aws-region: ap-southeast-3

      - uses: aws-actions/amazon-ecr-login@v2
        id: ecr

      - name: Bangun & dorong
        env:
          REPO: ${{ steps.ecr.outputs.registry }}/portal
          TAG: ${{ github.sha }}
        run: |
          docker buildx build --platform linux/arm64 \
            --cache-from type=registry,ref=$REPO:cache \
            --cache-to   type=registry,ref=$REPO:cache,mode=max \
            -t $REPO:$TAG -t $REPO:$(git rev-parse --short HEAD) \
            --push .

      - name: Jalankan migrasi database
        run: |
          ARN=$(aws ecs run-task --cluster portal \
            --task-definition portal-migrasi \
            --launch-type FARGATE \
            --network-configuration 'awsvpcConfiguration={
                subnets=[subnet-priv-a,subnet-priv-b],
                securityGroups=[sg-portal-web],assignPublicIp=DISABLED}' \
            --overrides '{"containerOverrides":[{"name":"migrasi",
                "environment":[{"name":"IMAGE_TAG","value":"${{ github.sha }}"}]}]}' \
            --query 'tasks[0].taskArn' --output text)

          aws ecs wait tasks-stopped --cluster portal --tasks "$ARN"

          KODE=$(aws ecs describe-tasks --cluster portal --tasks "$ARN" \
            --query 'tasks[0].containers[0].exitCode' --output text)
          [ "$KODE" = "0" ] || { echo "Migrasi gagal (exit $KODE)"; exit 1; }

      - name: Perbarui service
        run: |
          aws ecs describe-task-definition --task-definition portal-web \
            --query taskDefinition > /tmp/td.json

          jq --arg img "${{ steps.ecr.outputs.registry }}/portal:${{ github.sha }}" \
            '.containerDefinitions[0].image = $img
             | del(.taskDefinitionArn, .revision, .status, .requiresAttributes,
                   .compatibilities, .registeredAt, .registeredBy)' \
            /tmp/td.json > /tmp/td-baru.json

          REV=$(aws ecs register-task-definition --cli-input-json file:///tmp/td-baru.json \
            --query 'taskDefinition.taskDefinitionArn' --output text)

          aws ecs update-service --cluster portal --service portal-web \
            --task-definition "$REV" --query 'service.serviceName'

      - name: Tunggu stabil
        run: |
          aws ecs wait services-stable --cluster portal --services portal-web

      - name: Uji asap
        run: ./skrip/uji-asap.sh

      - name: Rollback kalau uji asap gagal
        if: failure()
        run: |
          SEBELUM=$(aws ecs describe-services --cluster portal --services portal-web \
            --query 'services[0].deployments[?status==`ACTIVE`].taskDefinition' --output text)
          [ -n "$SEBELUM" ] && aws ecs update-service --cluster portal \
            --service portal-web --task-definition "$SEBELUM"

Migrasi sebagai tugas terpisah, sebelum service diperbarui. Ini yang mencegah dua belas task menjalankan migrasi bersamaan (Fase 2). Dan karena migrasimu kompatibel ke belakang โ€” aturan dari Fase 2 โ€” menjalankannya sebelum kode baru naik itu aman: kode lama tetap berfungsi dengan skema baru.

concurrency tanpa cancel-in-progress

concurrency:
  group: deploy-produksi
  cancel-in-progress: false

Membatalkan deploy yang sedang berjalan bisa meninggalkannya di keadaan setengah jadi โ€” migrasi sudah jalan tapi service belum diperbarui, atau sebaliknya. Antrekan deploy berikutnya; jangan potong yang sedang berlangsung.

Yang tidak boleh ada di pipeline

JanganKenapa
Deploy langsung dari laptopTidak ada jejak, tidak ada gerbang
Tag latestRollback jadi tebakan
Migrasi di entrypoint containerSemua task menjalankannya bersamaan
Kredensial AWS jangka panjangTidak pernah dirotasi
continue-on-error pada gerbangGerbang yang tidak menghentikan bukan gerbang
Deploy tanpa uji asapKegagalan ditemukan pembaca

Latihan: siapkan OIDC antara GitHub dan AWS, lalu bangun workflow PR dengan keenam gerbangnya. Buktikan tiap gerbang bekerja dengan sengaja melanggarnya: buat PR dengan error tipe, satu dengan impor server key ke komponen Vue, dan satu yang menambah dependensi besar yang melewati anggaran JS. Ketiganya harus gagal dengan pesan yang jelas.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.