Menggunakan AWS Spot Instance sebagai CI/CD Runner
CI/CD pipeline di banyak organisasi sudah jauh melampaui sekadar go test dan go build. Terraform plan yang harus menarik state dari ratusan resource, SonarQube analysis yang memakan 4 vCPU selama 10 menit, Java Gradle build dengan dependency graph yang besar, Docker build multi-stage dengan layer cache yang tebal — semua ini mengubah runner dari “komputer kecil yang sesekali dipakai” menjadi beban infrastruktur yang serius. Dan masalahnya konsisten: runner self-hosted yang always-on over-provisioned untuk peak load tapi idle sebagian besar waktu, atau shared runner yang menciptakan antrean panjang yang membuat developer frustrasi menunggu feedback. AWS Spot Instance menawarkan jalan keluar yang elegan: compute kelas atas, harga 70–90% lebih murah dari On-Demand, dipakai hanya saat dibutuhkan, dan dihancurkan begitu job selesai. Artikel ini membahas cara kerja, implementasi lengkap, dan trade-off yang perlu dipahami sebelum mengadopsinya.
Mengapa CI/CD adalah Spot Workload yang Sempurna
Spot Instance memiliki satu karakteristik yang tidak bisa ditawar: AWS bisa mengambilnya kembali kapan saja dengan pemberitahuan 2 menit. Bagi production server, ini adalah disqualifier. Bagi CI/CD runner, ini hampir tidak relevan — jika dipahami dengan benar.
Perhatikan karakteristik CI/CD job yang membuatnya cocok secara alami:
CI/CD JOB vs PRODUCTION WORKLOAD
CI/CD Job:
✓ Stateless — tidak menyimpan state di luar artifact
✓ Ephemeral — dirancang untuk mati setelah selesai
✓ Retriable — jika gagal, tinggal jalankan ulang
✓ Durasi pendek — kebanyakan 5–30 menit
✓ Bisa dijadwalkan — tidak ada user yang menunggu secara langsung
✓ Idempotent — hasil yang sama jika dijalankan ulang dari awal
Production Server:
✗ Stateful — menyimpan in-memory state, active connections
✗ Long-running — uptime minggu hingga bulan
✗ Tidak bisa di-interrupt sembarangan — user sedang dilayani
✗ Recovery kompleks — restart bisa memengaruhi user
Kombinasi ini membuat CI/CD runner adalah kandidat Spot Instance yang hampir sempurna — bahkan lebih cocok dari batch processing sekalipun, karena infrastruktur retry-nya (GitHub Actions, GitLab CI) sudah built-in.
Berapa Potensi Penghematannya
Angka ini bukan teori. Spot pricing bervariasi per region dan instance type, tapi diskon dibanding On-Demand konsisten besar.
| Instance Type | vCPU | RAM | On-Demand/jam | Spot/jam | Hemat |
|---|---|---|---|---|---|
| c6i.2xlarge | 8 | 16 GB | $0.34 | ~$0.09 | ~73% |
| c6i.4xlarge | 16 | 32 GB | $0.68 | ~$0.18 | ~74% |
| m6i.4xlarge | 16 | 64 GB | $0.768 | ~$0.19 | ~75% |
| c6i.8xlarge | 32 | 64 GB | $1.36 | ~$0.32 | ~76% |
Satu job berat yang membutuhkan c6i.4xlarge selama 20 menit:
- On-Demand:
$0.68 × (20/60)= $0.23 per job - Spot:
$0.18 × (20/60)= $0.06 per job
Di tim dengan 200 job berat per hari, penghematannya mencapai sekitar $34/hari atau ~$1.000/bulan — hanya dari satu jenis job. Tim yang lebih besar dengan pipeline lebih banyak bisa menghemat jauh lebih besar.
Cara Kerja Spot Market
Memahami mekanisme Spot Market membantu merancang strategi mitigasi yang tepat dan menghindari keputusan konfigurasi yang salah.
flowchart TD
subgraph SpotMarket["AWS Spot Market"]
POOL["Spot Instance Pool\n(kapasitas AWS yang tidak terpakai)"]
PRICE["Spot Price\n(berfluktuasi per AZ per instance type)"]
end
subgraph Request["Request dari CI/CD"]
REQ["Spot Instance Request\n+ optional Max Price"]
end
subgraph Outcomes["Kemungkinan Hasil"]
OK["Instance berjalan\n✓ Spot price < max price\n✓ Kapasitas tersedia"]
INTERRUPT["Instance diinterupsi\n2 menit sebelum terminate\n⚠ Kapasitas dibutuhkan AWS kembali"]
NOFULFILL["Request tidak dipenuhi\n✗ Tidak ada kapasitas\ndi AZ yang dipilih"]
end
Request -->|Submit| SpotMarket
SpotMarket --> OK
SpotMarket --> INTERRUPT
SpotMarket --> NOFULFILL
OK -->|Mitigasi tidak diperlukan| DONE1["Job berjalan normal"]
INTERRUPT -->|Mitigasi| RETRY["Job di-retry\ndi instance baru"]
NOFULFILL -->|Mitigasi| FALLBACK["Fallback ke AZ lain\natau On-Demand"]
style OK fill:#e8f5e9,stroke:#43a047
style INTERRUPT fill:#fff3e0,stroke:#fb8c00
style NOFULFILL fill:#ffebee,stroke:#e53935Ada dua hal yang perlu dipahami tentang interupsi Spot:
Pertama, interupsi bukan hal biasa sehari-hari. AWS mempublish data bahwa rata-rata interupsi untuk sebagian besar instance type di region populer adalah di bawah 5% per bulan. Instance type yang populer (c5, c6i, m5) di availability zone dengan kapasitas besar bahkan lebih rendah lagi.
Kedua, Kita bisa memilih instance type yang lebih tidak berisiko interupsi. AWS menyediakan Spot Instance Advisor yang menampilkan frekuensi interupsi historis per instance type. Untuk CI/CD runner, pilih instance type dengan label “< 5% interruption rate”.
Arsitektur Ephemeral Runner
Pola yang paling bersih untuk CI/CD runner berbasis Spot adalah arsitektur fully ephemeral: tidak ada runner yang “standby”, setiap job menciptakan instance baru, dan instance menghancurkan dirinya sendiri setelah job selesai.
sequenceDiagram
participant Dev as Developer
participant GH as GitHub
participant WF as Workflow (ubuntu-latest)
participant AWS as AWS EC2
participant SPOT as Spot Instance (Runner)
Dev->>GH: git push / PR
GH->>WF: Trigger workflow
WF->>AWS: ec2 run-instances\n(Spot, custom AMI)
AWS-->>WF: Instance ID
Note over AWS,SPOT: ~60-90 detik cold start\n(lebih cepat dengan custom AMI)
SPOT->>SPOT: User data script berjalan:\n1. Install tools (jika tidak pakai custom AMI)\n2. Register sebagai GitHub runner\n3. Mulai mendengarkan job
GH->>SPOT: Kirim job ke runner
SPOT->>SPOT: Jalankan CI job\n(build, test, scan, dll)
SPOT-->>GH: Upload artifacts, report status
SPOT->>AWS: ec2 terminate-instances\n(self-terminate)
Note over SPOT: Runner --ephemeral:\notomatis unregister dari GitHub
AWS-->>SPOT: Instance terminatedKeindahan arsitektur ini ada di dua detail kecil yang sangat penting: flag --ephemeral pada konfigurasi runner memastikan runner otomatis unregister dari GitHub setelah satu job, dan self-termination di akhir user data script memastikan tidak ada instance yang lupa dimatikan dan terus ditagih.
Menyiapkan Infrastruktur
IAM Role untuk EC2 Runner
Instance perlu permission untuk menghancurkan dirinya sendiri. Ini adalah minimal policy yang dibutuhkan.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSelfTerminate",
"Effect": "Allow",
"Action": "ec2:TerminateInstances",
"Resource": "*",
"Condition": {
"StringEquals": {
"ec2:ResourceTag/Purpose": "ci-runner"
}
}
},
{
"Sid": "AllowDescribeForSelfIdentification",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:DescribeTags"
],
"Resource": "*"
}
]
}
Kondisi ec2:ResourceTag/Purpose: ci-runner memastikan instance hanya bisa mematikan instance lain yang juga berlabel runner — mencegah bug atau eksploitasi yang bisa mematikan instance lain secara tidak sengaja.
User Data Script (Bootstrap)
Script ini dijalankan saat instance pertama kali boot. Untuk pendekatan tanpa custom AMI, ini lebih panjang karena harus install semua dependencies.
#!/bin/bash
set -euo pipefail
# Redirect output ke log untuk debugging
exec > >(tee /var/log/runner-bootstrap.log) 2>&1
echo "Bootstrap dimulai: $(date)"
# --- Variabel dari environment atau SSM Parameter Store ---
GITHUB_ORG="${GITHUB_ORG}"
GITHUB_REPO="${GITHUB_REPO}"
RUNNER_TOKEN="${RUNNER_TOKEN}" # Sebaiknya ambil dari AWS SSM, bukan inject langsung
RUNNER_VERSION="2.316.0"
RUNNER_LABELS="spot,large,ci"
AWS_REGION="ap-southeast-1"
# --- Install dependencies ---
yum update -y -q
yum install -y -q docker git jq curl unzip
# Install AWS CLI v2 jika belum ada
if ! command -v aws &> /dev/null; then
curl -sL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip
unzip -q /tmp/awscliv2.zip -d /tmp
/tmp/aws/install
fi
# Start Docker
systemctl enable docker
systemctl start docker
usermod -aG docker ec2-user
# --- Install dan konfigurasi GitHub Actions Runner ---
RUNNER_DIR="/opt/actions-runner"
mkdir -p ${RUNNER_DIR}
cd ${RUNNER_DIR}
curl -sL -o runner.tar.gz \
"https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz"
tar xzf runner.tar.gz
rm runner.tar.gz
# Ambil runner token dari SSM Parameter Store (lebih aman dari inject langsung)
# RUNNER_TOKEN=$(aws ssm get-parameter \
# --name "/ci/github-runner-token" \
# --with-decryption \
# --region ${AWS_REGION} \
# --query "Parameter.Value" \
# --output text)
# Register runner ke GitHub
# --ephemeral: runner hanya menerima satu job lalu unregister otomatis
./config.sh \
--url "https://github.com/${GITHUB_ORG}/${GITHUB_REPO}" \
--token "${RUNNER_TOKEN}" \
--name "spot-runner-$(hostname)-$(date +%s)" \
--labels "${RUNNER_LABELS}" \
--unattended \
--ephemeral
echo "Runner berhasil terdaftar: $(date)"
# --- Jalankan runner ---
# Jalankan sebagai service agar bisa di-manage dengan systemd
./svc.sh install
./svc.sh start
# Tunggu sampai runner selesai menerima dan menjalankan job
# Polling: cek apakah runner process masih berjalan
while systemctl is-active --quiet actions.runner.*; do
sleep 10
done
echo "Job selesai, mulai self-terminate: $(date)"
# --- Self-terminate ---
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
aws ec2 terminate-instances \
--instance-ids "${INSTANCE_ID}" \
--region "${AWS_REGION}"
Jangan injectRUNNER_TOKENlangsung ke user data script sebagai plaintext jika bisa dihindari. User data script bisa dibaca oleh siapa pun yang punya aksesDescribeInstances. Gunakan AWS SSM Parameter Store (dengan enkripsi KMS) untuk menyimpan token dan ambil saat bootstrap dengan IAM role yang tepat.
Custom AMI untuk Startup Lebih Cepat
Salah satu kelemahan pendekatan vanilla adalah startup time yang lama jika semua tools diinstall saat boot. Untuk pipeline Java + Terraform + SonarQube, bisa membutuhkan 3–5 menit hanya untuk setup.
Solusinya adalah custom AMI yang sudah preinstall semua tools.
# Script untuk membangun custom AMI (jalankan di EC2 yang akan di-snapshot)
#!/bin/bash
set -euo pipefail
# Java 21 (untuk Gradle/Maven build)
yum install -y java-21-amazon-corretto-headless
# Gradle
GRADLE_VERSION="8.7"
curl -sL "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" -o /tmp/gradle.zip
unzip -q /tmp/gradle.zip -d /opt
ln -s /opt/gradle-${GRADLE_VERSION}/bin/gradle /usr/local/bin/gradle
# Terraform
TERRAFORM_VERSION="1.8.4"
curl -sL "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip" \
-o /tmp/terraform.zip
unzip -q /tmp/terraform.zip -d /usr/local/bin
chmod +x /usr/local/bin/terraform
# SonarQube Scanner
SONAR_VERSION="6.1.0.4477"
curl -sL "https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${SONAR_VERSION}-linux-x64.zip" \
-o /tmp/sonar.zip
unzip -q /tmp/sonar.zip -d /opt
ln -s /opt/sonar-scanner-${SONAR_VERSION}-linux-x64/bin/sonar-scanner /usr/local/bin/sonar-scanner
# Docker (sudah terinstall, pastikan update ke versi terbaru)
yum update -y docker
systemctl enable docker
# GitHub Actions Runner (preinstall tapi jangan register dulu)
RUNNER_VERSION="2.316.0"
mkdir -p /opt/actions-runner
cd /opt/actions-runner
curl -sL -o runner.tar.gz \
"https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz"
tar xzf runner.tar.gz && rm runner.tar.gz
# Setelah script ini selesai:
# 1. Buat AMI dari instance ini via AWS Console atau CLI
# 2. Gunakan AMI ID ini di workflow GitHub Actions
# aws ec2 create-image --instance-id i-xxxx --name "ci-runner-v1.0" --no-reboot
Dengan custom AMI, user data script saat boot hanya perlu melakukan registrasi runner ke GitHub — startup time turun dari 3–5 menit menjadi 60–90 detik.
Implementasi GitHub Actions
Workflow Lengkap dengan Error Handling
name: Heavy CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
env:
AWS_REGION: ap-southeast-1
INSTANCE_TYPE: c6i.4xlarge
AMI_ID: ami-0abcdef1234567890 # Custom AMI
jobs:
# Job 1: Launch Spot Instance
# Job ini berjalan di runner GitHub standard (murah, cepat)
launch-runner:
runs-on: ubuntu-latest
outputs:
instance-id: ${{ steps.launch.outputs.instance-id }}
runner-label: ${{ steps.launch.outputs.runner-label }}
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-launcher
aws-region: ${{ env.AWS_REGION }}
- name: Get GitHub Runner Token
id: get-token
run: |
TOKEN=$(curl -sX POST \
-H "Authorization: Bearer ${{ secrets.GH_PAT }}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/actions/runners/registration-token" \
| jq -r .token)
echo "::add-mask::${TOKEN}"
echo "token=${TOKEN}" >> $GITHUB_OUTPUT
- name: Launch Spot Instance
id: launch
run: |
RUNNER_LABEL="spot-$(echo ${{ github.run_id }}-${{ github.run_attempt }})"
# Encode user data sebagai base64
USER_DATA=$(cat <<EOF | base64 -w 0
#!/bin/bash
export GITHUB_ORG="${{ github.repository_owner }}"
export GITHUB_REPO="${{ github.event.repository.name }}"
export RUNNER_TOKEN="${{ steps.get-token.outputs.token }}"
export RUNNER_LABELS="${RUNNER_LABEL},spot,large"
export AWS_REGION="${{ env.AWS_REGION }}"
bash /opt/bootstrap-runner.sh
EOF
)
INSTANCE_ID=$(aws ec2 run-instances \
--image-id ${{ env.AMI_ID }} \
--instance-type ${{ env.INSTANCE_TYPE }} \
--iam-instance-profile Name=ci-runner-instance-profile \
--instance-market-options '{"MarketType":"spot","SpotOptions":{"SpotInstanceType":"one-time","InstanceInterruptionBehavior":"terminate"}}' \
--user-data "${USER_DATA}" \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Purpose,Value=ci-runner},{Key=RunID,Value=${{ github.run_id }}}]' \
--metadata-options '{"HttpTokens":"required","HttpEndpoint":"enabled"}' \
--region ${{ env.AWS_REGION }} \
--query 'Instances[0].InstanceId' \
--output text)
echo "instance-id=${INSTANCE_ID}" >> $GITHUB_OUTPUT
echo "runner-label=${RUNNER_LABEL}" >> $GITHUB_OUTPUT
echo "Instance ${INSTANCE_ID} diluncurkan, menunggu runner siap..."
- name: Wait for runner to be ready
run: |
# Tunggu sampai runner terdaftar di GitHub (max 3 menit)
TIMEOUT=180
ELAPSED=0
RUNNER_LABEL="${{ steps.launch.outputs.runner-label }}"
while [ $ELAPSED -lt $TIMEOUT ]; do
RUNNER_COUNT=$(curl -s \
-H "Authorization: Bearer ${{ secrets.GH_PAT }}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/actions/runners" \
| jq "[.runners[] | select(.labels[].name == \"${RUNNER_LABEL}\")] | length")
if [ "${RUNNER_COUNT}" -gt "0" ]; then
echo "Runner siap setelah ${ELAPSED} detik"
exit 0
fi
sleep 10
ELAPSED=$((ELAPSED + 10))
done
echo "Timeout: runner tidak terdaftar dalam ${TIMEOUT} detik"
exit 1
# Job 2: CI berat yang berjalan di Spot Instance
build-and-test:
needs: launch-runner
runs-on: [self-hosted, "${{ needs.launch-runner.outputs.runner-label }}"]
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- name: Gradle Build
run: ./gradlew build --no-daemon --parallel
- name: Run Tests
run: ./gradlew test --no-daemon
- name: SonarQube Scan
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
run: |
sonar-scanner \
-Dsonar.projectKey=${{ github.event.repository.name }} \
-Dsonar.sources=src \
-Dsonar.java.binaries=build/classes
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-output
path: build/libs/
# Job 3: Cleanup — pastikan instance dihapus meski job gagal
cleanup:
needs: [launch-runner, build-and-test]
runs-on: ubuntu-latest
if: always() # Jalankan selalu, bahkan jika build gagal
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-launcher
aws-region: ${{ env.AWS_REGION }}
- name: Terminate instance if still running
run: |
INSTANCE_ID="${{ needs.launch-runner.outputs.instance-id }}"
STATE=$(aws ec2 describe-instances \
--instance-ids ${INSTANCE_ID} \
--query 'Reservations[0].Instances[0].State.Name' \
--output text \
--region ${{ env.AWS_REGION }} 2>/dev/null || echo "terminated")
if [ "${STATE}" != "terminated" ] && [ "${STATE}" != "shutting-down" ]; then
echo "Instance masih berjalan, melakukan terminate..."
aws ec2 terminate-instances \
--instance-ids ${INSTANCE_ID} \
--region ${{ env.AWS_REGION }}
else
echo "Instance sudah tidak berjalan: ${STATE}"
fi
Job cleanup dengan kondisi if: always() adalah safety net yang penting — ia memastikan instance tidak lupa dimatikan bahkan ketika job gagal di tengah jalan sebelum self-terminate sempat dieksekusi.
Menangani Spot Interruption
Spot interruption adalah kondisi yang harus diantisipasi, bukan dihindari. Strategi yang tepat adalah membuatnya tidak menyakitkan.
flowchart TD
JOB[Job sedang berjalan\ndi Spot Instance] --> INT{Spot Interruption\nNotice diterima}
INT -- Tidak terjadi --> DONE[Job selesai normal\nInstance self-terminate]
INT -- Ya, 2 menit sebelum terminate --> SIGNAL["Instance menerima\nITB-Termination-Notice\ndi metadata endpoint"]
SIGNAL --> HANDLER["Interruption handler script:\n1. Kirim SIGTERM ke runner\n2. Runner checkpoint state\n3. Upload partial artifacts"]
HANDLER --> TERMINATE[Instance terminate]
TERMINATE --> RETRY["GitHub Actions:\njob ditandai failed"]
RETRY --> RERUN["Rerun job secara manual\natau otomatis via retry config"]
style INT fill:#fff3e0,stroke:#fb8c00
style DONE fill:#e8f5e9,stroke:#43a047
style RERUN fill:#e3f2fd,stroke:#1e88e5# Script untuk handle Spot Interruption Notice
# Tambahkan di user data atau jalankan sebagai background service
#!/bin/bash
# spot-interruption-handler.sh
while true; do
# Cek Spot Interruption Notice dari metadata endpoint
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "X-aws-ec2-metadata-token: $(curl -s -X PUT \
-H 'X-aws-ec2-metadata-token-ttl-seconds: 60' \
http://169.254.169.254/latest/api/token)" \
http://169.254.169.254/latest/meta-data/spot/termination-time)
if [ "${HTTP_CODE}" == "200" ]; then
echo "SPOT INTERRUPTION NOTICE diterima! Melakukan graceful shutdown..."
# Kirim sinyal ke GitHub Actions runner agar bisa cleanup
pkill -SIGTERM -f "Runner.Listener"
# Tunggu sebentar lalu terminate
sleep 30
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
aws ec2 terminate-instances --instance-ids "${INSTANCE_ID}" --region ap-southeast-1
exit 0
fi
sleep 5
done
Untuk pipeline yang berjalan lebih dari 15 menit (Terraform plan besar, analisis SonarQube, Maven build project besar), sangat direkomendasikan memecah pipeline menjadi tahap-tahap yang lebih kecil dengan artifact yang di-upload di antara tahap. Dengan begitu jika interruption terjadi di satu tahap, retry hanya mengulang tahap tersebut — bukan seluruh pipeline.
Optimasi Lanjutan
Strategi Multi-AZ untuk Ketersediaan Lebih Tinggi
Spot capacity bervariasi per Availability Zone. Strategi terbaik adalah meminta instance dari beberapa AZ sekaligus dan menggunakan instance type yang fleksibel.
# Gunakan EC2 Fleet untuk permintaan multi-AZ dan multi-instance-type
- name: Launch Spot via EC2 Fleet
run: |
aws ec2 create-fleet \
--launch-template-configs '[
{"LaunchTemplateSpecification":{"LaunchTemplateName":"ci-runner-template","Version":"$Latest"}},
]' \
--target-capacity-specification '{
"TotalTargetCapacity": 1,
"SpotTargetCapacity": 1,
"DefaultTargetCapacityType": "spot"
}' \
--spot-options '{
"AllocationStrategy": "price-capacity-optimized",
"InstanceInterruptionBehavior": "terminate"
}' \
--overrides '[
{"InstanceType":"c6i.4xlarge","SubnetId":"subnet-aaa","WeightedCapacity":1},
{"InstanceType":"c5.4xlarge","SubnetId":"subnet-aaa","WeightedCapacity":1},
{"InstanceType":"c6i.4xlarge","SubnetId":"subnet-bbb","WeightedCapacity":1},
{"InstanceType":"c5a.4xlarge","SubnetId":"subnet-bbb","WeightedCapacity":1}
]'
AllocationStrategy: price-capacity-optimized adalah strategi terbaik untuk CI/CD — ia memilih pool dengan kapasitas terbesar yang juga memiliki harga kompetitif, sehingga probabilitas interruption lebih rendah.
Menggunakan GitLab Runner Manager (Alternatif)
Jika menggunakan GitLab CI, tersedia gitlab-runner dengan executor docker+machine yang otomatis mengelola Spot Instance lifecycle. Setup ini lebih sederhana karena manajemen instance ditangani oleh runner manager.
# /etc/gitlab-runner/config.toml
[[runners]]
name = "spot-runner"
url = "https://gitlab.com"
token = "RUNNER_TOKEN"
executor = "docker+machine"
[runners.docker]
image = "docker:latest"
[runners.machine]
IdleCount = 0 # Tidak ada instance standby
IdleTime = 300 # Terminate setelah 5 menit idle
MaxBuilds = 1 # Terminate setelah 1 job (ephemeral)
MachineDriver = "amazonec2"
MachineName = "gitlab-runner-%s"
MachineOptions = [
"amazonec2-instance-type=c6i.4xlarge",
"amazonec2-region=ap-southeast-1",
"amazonec2-request-spot-instance=true",
"amazonec2-spot-price=0.30",
"amazonec2-ami=ami-xxxx",
"amazonec2-iam-instance-profile=ci-runner-instance-profile",
"amazonec2-tags=Purpose,ci-runner",
]
Kapan Tepat dan Tidak Tepat
SANGAT DIREKOMENDASIKAN jika:
✓ CI job berjalan > 10 menit secara konsisten
✓ Resource yang dibutuhkan besar (>= 8 vCPU, >= 16 GB RAM)
✓ Ada banyak parallel job yang sering antri
✓ Tim DevOps cukup matang untuk mengelola EC2
✓ Budget infra CI mulai tidak proporsional
✓ Semua job bersifat idempotent dan bisa di-retry
PERLU PERTIMBANGAN LEBIH LANJUT jika:
⚠ Ada job yang tidak idempotent (database migration, deployment tanpa rollback)
⚠ Pipeline butuh startup time sangat cepat (< 1 menit)
⚠ Tim belum familiar dengan AWS dan EC2
TIDAK DIREKOMENDASIKAN jika:
✗ CI job kecil dan cepat (< 5 menit) — overhead setup tidak worth it
✗ Job yang tidak bisa di-retry sama sekali
✗ Frekuensi pipeline sangat rendah (< 10 job/hari)
✗ Tidak ada monitoring dan alerting untuk failed job
Ringkasan
- CI/CD adalah Spot workload sempurna — stateless, ephemeral, retriable, dan durasi pendek adalah semua karakteristik yang membuat Spot Instance ideal untuk runner.
- Penghematan 70–90% bukan teori — dengan Spot pricing yang konsisten, pipeline berat yang sebelumnya mahal bisa dijalankan dengan biaya yang sangat berbeda di skala ratusan job per hari.
- Arsitektur ephemeral adalah kunci — runner dibuat saat job akan mulai, mati setelah job selesai. Tidak ada instance standby yang idle dan menambah biaya.
- Flag
--ephemeralwajib digunakan — tanpanya, runner tetap terdaftar di GitHub setelah job selesai dan bisa menerima job lain saat instance sudah tidak ada.- Jangan simpan Runner Token di user data secara plaintext — gunakan AWS SSM Parameter Store dengan enkripsi KMS dan ambil via IAM role saat bootstrap.
- Custom AMI memangkas startup time secara signifikan — preinstall Java, Terraform, Docker, SonarQube, dan GitHub Runner binary sehingga saat boot hanya perlu registrasi, bukan instalasi.
- Job cleanup
if: always()adalah safety net wajib — jika job gagal sebelum self-terminate berjalan, job cleanup yang berjalan setelahnya memastikan instance tetap dihapus.price-capacity-optimizedadalah allocation strategy terbaik untuk CI/CD — ia memilih pool dengan kapasitas terbesar, yang berarti probabilitas interruption lebih rendah dibanding strategilowest-price.