LocalStack sebagai service container di GitHub Actions, menjalankan test secara paralel dengan caching image, dan integrasi dengan pytest, Jest, Mocha, serta testcontainers dan laporan coverage.

Di episode 17 kita menarik resource AWS nyata ke LocalStack untuk testing yang realistis. Tapi semua itu sia-sia jika pengujiannya hanya berjalan di mesin lokal. Episode ini mengotomatisasi semuanya: LocalStack di CI/CD, dengan fokus pada GitHub Actions. Pola yang sama berlaku untuk GitLab CI, CircleCI, atau Jenkins.
Mengapa ini penting? Test yang hanya lulus di laptop kalian tidak ada nilainya bagi tim. Dengan LocalStack sebagai service container di CI, setiap pull request mendapat environment AWS palsu yang fresh, cepat, dan tanpa biaya — sebelum kode menyentuh AWS sungguhan. Plus, mengikuti prinsip episode 15, kita memakai PERSISTENCE=0 dan kredensial unik di CI.
Cara paling umum adalah menjadikan LocalStack service container dalam sebuah job. Service container berbagi jaringan dengan job, sehingga test mengarahkan SDK ke localhost:4566 seperti di mesin lokal:
name: ci
on:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [18, 20, 22]
services:
localstack:
image: localstack/localstack:stable
ports:
- 4566:4566
env:
AWS_ENDPOINT_URL: http://localhost:4566
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: test
AWS_DEFAULT_REGION: us-east-1
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm testNilai matrix.node diganti per job oleh GitHub Actions, sehingga tiga versi Node berjalan paralel — masing-masing dengan instance LocalStack sendiri yang terisolasi. Itulah parallelism: tidak ada shared state, tidak ada port conflict karena tiap job punya runner sendiri.
Semakin banyak job paralel, semakin sering image LocalStack harus di-pull. Tanpa cache, tiap runner mengunduh image dari awal. Teknik yang umum dipakai adalah pra-pull image di awal job dan caching image memakai tar hasil docker save lewat tindihan actions/cache:
- name: Cache LocalStack image
uses: actions/cache@v4
with:
path: /tmp/localstack-image.tar
key: localstack-${{ runner.os }}
- name: Load atau pull image
run: |
if [ -f /tmp/localstack-image.tar ]; then
docker load -i /tmp/localstack-image.tar
else
docker pull localstack/localstack:stable
docker save localstack/localstack:stable -o /tmp/localstack-image.tar
fiEkspresi runner.os di dalam key membuat cache terpisah per OS runner. Kombinasi parallelism plus caching ini mempercepat pipeline secara signifikan, terutama untuk monorepo dengan banyak service.
Jika sebuah job butuh env var tambahan (misal DEBUG=1 atau SERVICES khusus), service container bisa diganti dengan step yang menjalankan container secara manual:
- name: Start LocalStack
run: |
docker run -d --name localstack \
-e DEBUG=1 -e PERSISTENCE=0 \
-p 4566:4566 localstack/localstack:stable
until curl -s http://localhost:4566/_localstack/health | grep -q running; do
sleep 1
doneLoop until memastikan job tidak mulai test sebelum LocalStack benar-benar siap — berguna sebagai readiness gate sederhana.
Fixture Boto3 yang mengarah ke endpoint lokal membuat test self-contained:
import boto3
import pytest
ENDPOINT_URL = "http://localhost:4566"
@pytest.fixture
def s3_client():
return boto3.client(
"s3",
endpoint_url=ENDPOINT_URL,
aws_access_key_id="test",
aws_secret_access_key="test",
)
def test_bucket_lifecycle(s3_client):
s3_client.create_bucket(Bucket="artifacts")
assert s3_client.list_buckets()["Buckets"][0]["Name"] == "artifacts"Di Jest, cukup set environment variable di setup file:
process.env.AWS_ENDPOINT_URL = "http://localhost:4566"
process.env.AWS_ACCESS_KEY_ID = "test"
process.env.AWS_SECRET_ACCESS_KEY = "test"
process.env.AWS_DEFAULT_REGION = "us-east-1"Mocha memakai setup file serupa melalui flag --require:
npx mocha --require ./test/setup.js ./test/aws.spec.jsUntuk test yang ingin instance benar-benar fresh per test suite, pakai modul @testcontainers/localstack. Container dibuat, dipakai, lalu di-stop otomatis:
const { LocalStackContainer } = require("@testcontainers/localstack")
const { S3Client, ListBucketsCommand } = require("@aws-sdk/client-s3")
test("s3 di dalam container lokal", async () => {
const container = await new LocalStackContainer().start()
const endpoint = container.getConnectionUri()
const client = new S3Client({
endpoint, region: "us-east-1",
credentials: { accessKeyId: "test", secretAccessKey: "test" },
})
await client.send(new ListBucketsCommand({}))
await container.stop()
})Di Python ada padanannya: paket testcontainers dengan modul localstack. Pola ini ideal untuk developer yang tidak ingin menjalankan LocalStack secara permanen.
Test tanpa coverage report sulit dipertanggungjawabkan. Aktifkan coverage di masing-masing framework, lalu upload sebagai artifact:
- run: npx jest --coverage
- uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.node }}
path: coverage/Untuk Python, pytest --cov=. --cov-report=term-missing memberi laporan per baris. Artifact tersebut bisa diunduh dari halaman run untuk ditinjau reviewer, atau di-publish ke layanan seperti Codecov.
Ringkasan integrasi CI/CD dengan LocalStack:
/_localstack/health sebelum test dimulai.Pipeline kini berjalan otomatis — tapi bagaimana jika tiba-tiba test gagal tanpa alasan jelas? Di episode 19 selanjutnya kita membahas Performance & Troubleshooting: membaca log dengan DEBUG=1, memanfaatkan health endpoint, memantau resource Docker, dan memecahkan masalah paling umum seperti port conflict dan Lambda yang tidak terinvoke. Sampai jumpa!