Studi Kasus: Pipeline CI/CD

Studi Kasus: Pipeline CI/CD

Bitnesia Sep 13, 2026 11 EN

Konsep build otomatis, testing dalam container, registry, dan strategi deployment sudah dibahas satu per satu pada bab integrasi CI/CD. Namun di lapangan, keempat potongan itu jarang berdiri sendiri-sendiri; Sysadmin/DevOps Engineer justru dituntut merangkainya jadi satu pipeline utuh yang berjalan otomatis dari commit pertama sampai aplikasi benar-benar melayani trafik di production, lengkap dengan jalur aman kalau ada langkah yang gagal di tengah jalan. Bab ini membahas studi kasus merangkai pipeline CI/CD lengkap untuk aplikasi REST API sederhana bernama notes-api, mulai dari menyiapkan Dockerfile yang siap dipakai lintas tahap, menyusun workflow build dan test otomatis, mengatur deployment bertahap ke staging lalu production dengan gerbang persetujuan manual, sampai memantau kesehatan hasil deployment dan mencatat jejak auditnya.

37.1 Skenario dan Arsitektur Pipeline

notes-api adalah REST API sederhana berbasis Node.js dan Express untuk mencatat catatan pengguna, memakai PostgreSQL sebagai penyimpanan data. Skenario pada studi kasus ini memakai GitHub Actions sebagai CI/CD platform, GitHub Container Registry (ghcr.io) sebagai tempat menyimpan image, serta dua server terpisah yang menjalankan Docker Swarm sebagai target deployment: satu untuk staging dan satu untuk production.

37.1.1 Alur Pipeline dari Commit sampai Production

Pipeline pada studi kasus ini mengalir lewat lima tahap berurutan setiap kali ada push ke branch main: build image, jalankan test di dalam container, push image ke registry kalau test lolos, deploy otomatis ke staging, lalu deploy ke production hanya setelah ada persetujuan manual dari Sysadmin/DevOps Engineer. Urutan ini sengaja dibuat linier dan bertahap, bukan langsung menembak production, supaya setiap perubahan kode selalu melewati lingkungan staging lebih dulu sebagai jaring pengaman sebelum menyentuh pengguna sungguhan.

notes-api/
├── src/
│   ├── index.js
│   └── db.js
├── test/
│   └── notes.test.js
├── Dockerfile
├── compose.test.yaml
├── package.json
└── .github/
    └── workflows/
        └── pipeline.yaml

Satu file workflow tunggal, .github/workflows/pipeline.yaml, menampung seluruh job dari build sampai deploy production. Pendekatan satu file ini dipilih supaya alur dependensi antar-tahap (lewat keyword needs) mudah dibaca dalam satu tempat, dibanding memecahnya ke banyak file workflow terpisah yang saling memicu satu sama lain.

37.1.2 Dockerfile Multi-Stage untuk Kebutuhan Pipeline

Satu Dockerfile yang sama dipakai di seluruh tahap pipeline, mulai dari test sampai image final yang di-deploy ke production, supaya tidak ada celah perbedaan antara image yang diuji dan image yang benar-benar berjalan di production. Multi-stage build memisahkan stage test yang membawa dependency development dari stage production yang hanya membawa dependency runtime.

FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM base AS test
COPY . .
CMD ["npm", "test"]

FROM base AS production
COPY . .
RUN npm ci --omit=dev
HEALTHCHECK --interval=15s --timeout=3s --retries=3 \
  CMD wget -qO- http://localhost:3000/health || exit 1
EXPOSE 3000
CMD ["node", "src/index.js"]

Instruksi HEALTHCHECK pada stage production penting disiapkan sejak tahap ini karena statusnya nanti dipakai berulang kali di tahap deployment dan monitoring, bukan cuma dekorasi. Endpoint /health pada src/index.js cukup mengembalikan status 200 kalau koneksi ke database masih hidup, sebuah pengecekan sederhana namun cukup untuk mendeteksi kondisi container yang sebenarnya berjalan tapi sudah tidak bisa melayani permintaan dengan benar.

// src/index.js (potongan)
app.get("/health", async (req, res) => {
  try {
    await db.query("SELECT 1");
    res.sendStatus(200);
  } catch {
    res.sendStatus(503);
  }
});

37.2 Build Automation

Tahap build pada pipeline ini bertugas menghasilkan satu image yang identik untuk dipakai di tahap test, staging, maupun production, ditandai dengan tag yang mengikuti hash commit supaya setiap image bisa ditelusuri balik ke baris kode persis yang menghasilkannya.

37.2.1 Job Build dengan Tagging Berbasis Commit SHA

Job build memakai docker/build-push-action untuk membangun image sekaligus menyiapkannya untuk didorong ke registry pada tahap berikutnya, tanpa langsung melakukan push di job ini supaya proses build dan test tetap terpisah jelas dari proses distribusi image.

name: Pipeline notes-api

on:
  push:
    branches: [main]

env:
  IMAGE: ghcr.io/${{ github.repository }}

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - name: Build image target production
        uses: docker/build-push-action@v6
        with:
          context: .
          target: production
          push: false
          load: true
          tags: ${{ env.IMAGE }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Simpan image sebagai artifact
        run: docker save ${{ env.IMAGE }}:${{ github.sha }} -o image.tar

      - uses: actions/upload-artifact@v4
        with:
          name: notes-api-image
          path: image.tar

Opsi load: true memuat hasil build langsung ke Docker daemon runner supaya image bisa dipakai lagi lewat perintah docker biasa di step berikutnya (misalnya docker save), sesuai dokumentasi resmi docker/build-push-action yang menyebut opsi ini tidak bisa dipakai bersamaan dengan build multi-platform. Image yang sudah dibangun disimpan sebagai artifact lewat docker save dan actions/upload-artifact, supaya job test dan job push setelahnya tinggal memuat ulang image yang sama persis lewat docker load, alih-alih membangunnya dari awal lagi dan berisiko menghasilkan image yang sedikit berbeda.

37.2.2 Reuse Image Production Antar-Job lewat Artifact

Setiap job pada GitHub Actions secara default berjalan di runner yang terpisah dan bersih, sehingga image hasil docker build di job build tidak otomatis tersedia di job lain kecuali dipindahkan secara eksplisit. Job yang nanti mengirim image ke registry cukup mengambil kembali artifact yang sama lewat actions/download-artifact dan docker load, tanpa perlu membangun ulang image production dari awal.

  push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: notes-api-image

      - name: Muat image dari artifact
        run: docker load -i image.tar

Pola ini memastikan image yang didorong ke registry dan akhirnya di-deploy adalah bit-for-bit sama dengan yang keluar dari job build, tidak pernah dibangun ulang di job manapun setelahnya sehingga tidak ada celah image berbeda muncul akibat perbedaan kondisi cache atau versi base image antar-run. Job test dan integration-test pada sub-bab berikutnya sengaja tidak memakai artifact ini, karena keduanya butuh image dari stage test pada Dockerfile yang membawa dependency development seperti test runner, isi yang memang sengaja tidak ada pada image stage production demi menjaga image production tetap ramping. Sumber kodenya tetap identik karena kedua stage dibangun dari commit checkout yang sama, jadi perbedaan ini murni soal isi image, bukan soal kode aplikasi yang diuji.

37.3 Test Automation

Image yang lolos build belum tentu berfungsi benar, sehingga pipeline butuh gerbang test otomatis yang menghentikan seluruh proses lebih awal begitu ada regresi, sebelum image tersebut sempat didorong ke registry atau di-deploy ke lingkungan mana pun.

37.3.1 Unit Test di Dalam Container

Job test membangun stage test pada Dockerfile langsung dari kode yang di-checkout, terpisah dari image production yang sudah dibangun di job build, karena stage test sengaja membawa dependency development seperti test runner yang tidak ada pada image production.

  test:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - name: Build target test
        uses: docker/build-push-action@v6
        with:
          context: .
          target: test
          push: false
          load: true
          tags: ${{ env.IMAGE }}:test

      - name: Jalankan unit test
        run: docker run --rm ${{ env.IMAGE }}:test

needs: build di sini murni mengatur urutan, bukan berbagi image, supaya job test baru berjalan setelah dipastikan image production berhasil dibangun. Kalau npm test di dalam container mengembalikan exit code bukan nol, docker run ikut mengembalikan exit code yang sama, sehingga GitHub Actions otomatis menandai step ini gagal dan menghentikan seluruh job berikutnya yang bergantung padanya lewat needs.

37.3.2 Integration Test dengan Database lewat Compose

notes-api butuh koneksi database sungguhan untuk integration test, bukan cuma mock, supaya query SQL yang ditulis benar-benar tervalidasi jalan sesuai skema. Susun compose.test.yaml yang menyediakan PostgreSQL sementara khusus untuk kebutuhan test ini.

services:
  app:
    image: ghcr.io/OWNER/notes-api:test
    command: npm run test:integration
    environment:
      DATABASE_URL: postgres://postgres:postgres@db:5432/notesdb
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: notesdb
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5

Ganti OWNER pada image: ghcr.io/OWNER/notes-api:test dengan nama akun atau organisasi pemilik repository sungguhan, supaya nilainya sama persis dengan ${{ env.IMAGE }} yang dipakai di file workflow. Tambahkan job integration-test yang membangun ulang image stage test dengan tag sesuai yang dirujuk compose.test.yaml, lalu menjalankan seluruh stack test lewat satu perintah. Image stage test dibangun lagi di sini, bukan dipakai bersama dari job test pada sub-bab sebelumnya, karena tiap job GitHub Actions berjalan di runner terpisah dan cache layer dari type=gha yang sudah dipakai di kedua job membuat build ulang ini tetap cepat.

  integration-test:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - name: Build target test
        uses: docker/build-push-action@v6
        with:
          context: .
          target: test
          push: false
          load: true
          tags: ${{ env.IMAGE }}:test
          cache-from: type=gha

      - name: Jalankan integration test
        run: docker compose -f compose.test.yaml up --abort-on-container-exit --exit-code-from app

      - name: Bersihkan container dan volume test
        if: always()
        run: docker compose -f compose.test.yaml down --volumes

Kondisi if: always() pada step pembersihan memastikan container dan volume test tetap dihapus meski step di atasnya gagal, mencegah runner GitHub Actions (yang memang bersifat sekali pakai per run) meninggalkan sisa proses yang menggantung. Job push yang mendorong image ke registry pada sub-bab berikutnya baru boleh berjalan setelah job test dan integration-test ini sama-sama sukses, diatur lewat needs: [test, integration-test].

37.4 Deployment Automation

Image yang sudah lolos seluruh test siap didorong ke registry lalu di-deploy, tapi deployment ke production tidak boleh berjalan otomatis begitu saja tanpa jaring pengaman. Bagian ini menyusun deployment bertahap: otomatis ke staging, lalu ke production hanya setelah dikonfirmasi manual.

37.4.1 Push Image dan Deploy Otomatis ke Staging

Job push mendorong image yang sudah lolos test ke ghcr.io, lalu job deploy-staging langsung menariknya ke server staging tanpa perlu persetujuan manual, karena staging memang dirancang sebagai lingkungan uji coba berisiko rendah.

  push:
    needs: [test, integration-test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: notes-api-image

      - run: docker load -i image.tar

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

      - run: docker push ${{ env.IMAGE }}:${{ github.sha }}

  deploy-staging:
    needs: push
    runs-on: ubuntu-latest
    steps:
      - name: Deploy ke staging
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.STAGING_HOST }}
          username: ${{ secrets.STAGING_USER }}
          key: ${{ secrets.STAGING_SSH_KEY }}
          script: |
            docker service update --image ghcr.io/${{ github.repository }}:${{ github.sha }} notes-api-staging

Server staging pada studi kasus ini sudah menjalankan Docker Swarm dengan service notes-api-staging yang dibuat sebelumnya lewat docker service create, sehingga job ini cukup memperbarui image-nya lewat docker service update. Kredensial STAGING_HOST, STAGING_USER, dan STAGING_SSH_KEY disimpan sebagai encrypted secrets di pengaturan repository GitHub, bukan dituliskan langsung di file workflow.

37.4.2 Deploy ke Production dengan Gerbang Persetujuan Manual

Job deploy-production memakai fitur environment milik GitHub Actions untuk memaksa pipeline berhenti sejenak menunggu persetujuan manual, sebelum benar-benar menyentuh server production. Fitur ini diaktifkan dengan mendaftarkan environment bernama production di pengaturan repository (menu Settings > Environments) dan mengaktifkan aturan Required reviewers di sana, sesuai dokumentasi resmi GitHub tentang deployment environments.

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production
    concurrency:
      group: deploy-production
      cancel-in-progress: false
    steps:
      - name: Deploy ke production
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.PROD_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            docker service update --image ghcr.io/${{ github.repository }}:${{ github.sha }} notes-api

Begitu job ini tercapai, run pipeline berhenti sejenak berstatus menunggu, dan hanya reviewer yang terdaftar pada environment production yang bisa menekan tombol Approve lewat tab Actions di GitHub sebelum step deployment sungguhan berjalan. Blok concurrency dengan cancel-in-progress: false mencegah dua run pipeline yang kebetulan tumpang tindih sama-sama mencoba deploy ke production bersamaan; run kedua akan mengantre menunggu run pertama selesai alih-alih membatalkannya secara paksa, sesuai dokumentasi resmi GitHub Actions mengenai concurrency.

37.4.3 Rollback Otomatis saat Deployment Gagal

Deployment yang sukses ter-update bukan jaminan aplikasi baru benar-benar sehat; image yang salah konfigurasi bisa saja berhasil ter-deploy tapi langsung crash-loop begitu menerima trafik sungguhan. Tambahkan step verifikasi setelah docker service update yang memantau status task Swarm, lalu memicu rollback otomatis lewat docker service rollback kalau service tidak kunjung mencapai kondisi sehat. Tempelkan skrip berikut tepat setelah baris docker service update baik pada job deploy-staging (ganti nama service jadi notes-api-staging) maupun job deploy-production yang sudah didefinisikan pada sub-bab sebelumnya.

            docker service update --image ghcr.io/${{ github.repository }}:${{ github.sha }} notes-api

            for i in $(seq 1 10); do
              RUNNING=$(docker service ps notes-api --filter "desired-state=running" --format "{{.CurrentState}}" | grep -c "Running")
              if [ "$RUNNING" -ge 1 ]; then
                echo "Service sehat, task Running terdeteksi"
                exit 0
              fi
              sleep 5
            done

            echo "Task baru tidak kunjung Running, rollback ke versi sebelumnya"
            docker service rollback notes-api
            exit 1

docker service rollback mengembalikan service ke spec sebelumnya yang tersimpan otomatis oleh Swarm setiap kali docker service update dijalankan, sesuai dokumentasi resmi Docker CLI reference untuk perintah tersebut. Skrip di atas memberi jendela waktu sekitar 50 detik (10 kali percobaan dengan jeda 5 detik) bagi task baru untuk mencapai status Running sebelum dianggap gagal; angka ini perlu disesuaikan dengan waktu startup aplikasi sungguhan, karena aplikasi yang butuh waktu inisialisasi lebih lama (misalnya menjalankan migrasi database saat start) bisa salah terdeteksi gagal padahal sebenarnya masih dalam proses normal. exit 1 di baris terakhir penting supaya job GitHub Actions ikut ditandai gagal begitu rollback terjadi, alih-alih diam-diam dianggap sukses padahal deployment sebenarnya dibatalkan.

37.5 Monitoring dan Logging Pipeline

Pipeline yang selesai berjalan tanpa error belum tentu berarti pengguna akhir benar-benar mendapat versi aplikasi yang baru dan sehat. Bagian ini menutup studi kasus dengan memastikan hasil setiap deployment terpantau dan tercatat, bukan berhenti begitu saja begitu perintah docker service update selesai dieksekusi.

37.5.1 Smoke Test Pasca-Deployment

Tambahkan step smoke test tepat setelah rollback-guard pada sub-bab sebelumnya, baik di job deploy-staging maupun job deploy-production, memanggil endpoint publik aplikasi lewat curl untuk memastikan aplikasi benar-benar merespons dari luar, bukan cuma dari sudut pandang status task Swarm di dalam server.

            curl -f --retry 5 --retry-delay 5 https://staging.notes-api.example.com/health
            curl -f --retry 5 --retry-delay 5 https://notes-api.example.com/health

Baris pertama ditambahkan di job deploy-staging dan baris kedua di job deploy-production, masing-masing menunjuk domain lingkungannya sendiri. Step ini penting karena status task Running di Swarm cuma menandakan proses di dalam container tidak langsung crash, bukan jaminan aplikasi tersebut benar-benar bisa dijangkau dan merespons permintaan HTTP dengan benar. Opsi -f pada curl membuat perintah ini keluar dengan exit code bukan nol kalau respons HTTP berupa kode error (400 ke atas), sehingga kegagalan smoke test di deploy-staging ikut menggagalkan job dan mencegah image yang bermasalah lanjut ke gerbang persetujuan production, sementara kegagalan smoke test di deploy-production menandai deployment production itu sendiri gagal meski proses rollback Swarm-nya sudah berjalan.

37.5.2 Notifikasi Status Pipeline

Sysadmin/DevOps Engineer yang bertugas memberi persetujuan manual di sub-bab deployment production perlu tahu begitu ada run pipeline yang menunggu approval, tanpa harus terus-menerus mengecek tab Actions secara manual. Tambahkan step notifikasi lewat webhook Slack di akhir job deploy-staging, dipicu terlepas dari hasil job sebelumnya lewat if: always().

      - name: Kirim notifikasi ke Slack
        if: always()
        run: |
          STATUS="${{ job.status }}"
          curl -X POST -H "Content-Type: application/json" \
            -d "{\"text\": \"Deploy staging notes-api commit ${{ github.sha }}: ${STATUS}. Menunggu approval production.\"}" \
            ${{ secrets.SLACK_WEBHOOK_URL }}

secrets.SLACK_WEBHOOK_URL disimpan sebagai encrypted secret, bukan ditulis langsung di file workflow, karena URL webhook Slack yang bocor bisa disalahgunakan Attacker untuk mengirim pesan palsu mengatasnamakan pipeline ke channel tim. Pola notifikasi yang sama ditambahkan juga di akhir job deploy-production, cukup mengganti isi pesan text, supaya tim tahu begitu ada deployment production yang berhasil maupun yang berakhir rollback karena smoke test-nya gagal.

37.5.3 Mencatat Jejak Audit Deployment

Tim yang menangani insiden di production sering kali butuh menjawab pertanyaan sederhana namun krusial: commit mana yang sedang berjalan di production saat ini, dan kapan persisnya commit itu di-deploy. Tambahkan step pencatatan ke file log terpusat di server production, dijalankan tepat setelah smoke test production dinyatakan sukses.

            echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) deploy sha=${{ github.sha }} by=${{ github.actor }}" \
              >> /var/log/notes-api-deployments.log

Catatan sesederhana ini, kalau dikumpulkan konsisten dari setiap deployment, jadi rujukan pertama saat menelusuri kapan sebuah regresi mulai muncul di production, cukup dengan mencocokkan waktu kemunculan gejala dengan waktu deployment terdekat di log ini. Untuk kebutuhan observabilitas yang lebih menyeluruh, seperti memantau metrik latency dan error rate aplikasi secara real-time atau mengumpulkan log dari banyak container sekaligus ke satu tempat pencarian terpusat, pipeline ini bisa diintegrasikan lebih lanjut dengan stack observabilitas berbasis Prometheus, Grafana, dan Loki yang sudah dibahas pada bab tersendiri. Deployment log sesederhana file teks di atas tetap punya tempatnya sebagai jejak audit paling dasar, independen dari tersedia atau tidaknya stack observabilitas yang lebih lengkap tersebut.