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.
latestmembuat 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
| Jangan | Kenapa |
|---|---|
| Deploy langsung dari laptop | Tidak ada jejak, tidak ada gerbang |
Tag latest | Rollback jadi tebakan |
| Migrasi di entrypoint container | Semua task menjalankannya bersamaan |
| Kredensial AWS jangka panjang | Tidak pernah dirotasi |
continue-on-error pada gerbang | Gerbang yang tidak menghentikan bukan gerbang |
| Deploy tanpa uji asap | Kegagalan 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.