← Semua pembelajaran / Go Nol → Enterprise
Fase 10 · Deploy di AWS

ECS Fargate — menjalankan container

Fargate menjalankan container tanpa kamu menyentuh EC2. Yang tersisa untuk diputuskan adalah ukuran task, cara rahasia disuntikkan, dan bagaimana ECS tahu container-mu sehat.

Intisari

  • Task definition adalah cetakan (image, CPU, memori, env); service menjaga jumlah task tetap sesuai.
  • Satu image, beberapa service: server, worker, cron — cukup timpa command.
  • stopTimeout harus lebih besar dari total waktu graceful shutdown-mu (Fase 3).
  • Health check ECS tidak boleh menyentuh database — kalau tidak, gangguan sebagian jadi pemadaman total.
  • Rahasia lewat secrets (Secrets Manager), bukan environment.

Task definition

{
  "family": "toko-server",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "runtimePlatform": {
    "cpuArchitecture": "ARM64",
    "operatingSystemFamily": "LINUX"
  },
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::123456789012:role/tokoTaskRole",

  "containerDefinitions": [{
    "name": "server",
    "image": "123456789012.dkr.ecr.ap-southeast-1.amazonaws.com/toko/server:a1b2c3d",
    "essential": true,

    "portMappings": [{ "containerPort": 8080, "protocol": "tcp" }],

    "environment": [
      { "name": "APP_ENV",    "value": "production" },
      { "name": "ADDR",       "value": ":8080" },
      { "name": "GOMEMLIMIT", "value": "800MiB" },
      { "name": "LOG_LEVEL",  "value": "info" }
    ],

    "secrets": [
      { "name": "DATABASE_URL",
        "valueFrom": "arn:aws:secretsmanager:ap-southeast-1:123456789012:secret:prod/db-AbC123" },
      { "name": "JWT_SECRET",
        "valueFrom": "arn:aws:secretsmanager:ap-southeast-1:123456789012:secret:prod/jwt-XyZ789" }
    ],

    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/ecs/toko-server",
        "awslogs-region": "ap-southeast-1",
        "awslogs-stream-prefix": "ecs",
        "mode": "non-blocking",
        "max-buffer-size": "4m"
      }
    },

    "healthCheck": {
      "command": ["CMD", "/server", "-health"],
      "interval": 15,
      "timeout": 3,
      "retries": 3,
      "startPeriod": 30
    },

    "stopTimeout": 60,
    "readonlyRootFilesystem": true,
    "user": "65532:65532"
  }]
}
SetelanKenapa seperti itu
GOMEMLIMIT: 800MiB~80% dari 1024 MB — mencegah OOM kill (Fase 9)
stopTimeout: 60Harus > jeda + Shutdown + worker (Fase 3)
mode: non-blockingAplikasi tidak tertahan saat CloudWatch lambat
readonlyRootFilesystemBinari Go tidak menulis ke disk; matikan seluruh kelas serangan
user: 65532Bukan root — sesuai distroless:nonroot
ARM64Graviton: sekitar 20% lebih murah untuk kinerja setara
startPeriod: 30Kegagalan health check selama masa ini tidak dihitung

Dua peran IAM yang selalu tertukar: executionRoleArn dipakai agen ECS untuk menarik image dan membaca rahasia — ia butuh izin ECR dan Secrets Manager. taskRoleArn dipakai aplikasimu untuk memanggil AWS (S3, SQS) — dan ini yang harus seketat mungkin. Menaruh izin S3 di execution role adalah kesalahan yang membuat izin jadi lebih luas dari seharusnya.

Ukuran task

CPUMemori yang sahUntuk
256 (.25 vCPU)0,5 / 1 / 2 GBTerlalu kecil untuk API produksi
512 (.5 vCPU)1–4 GBTitik awal yang wajar
1024 (1 vCPU)2–8 GBBeban sedang
2048 (2 vCPU)4–16 GBBeban CPU tinggi

Untuk aplikasi Go, lebih banyak task kecil biasanya mengalahkan sedikit task besar. Alasannya: penyebaran lebih merata antar AZ, dampak satu task mati lebih kecil, dan penskalaan lebih halus. Yang membatasi adalah aritmetika koneksi database (Fase 4) — bukan CPU.

Dan sejak Go 1.25, GOMAXPROCS membaca batas CPU cgroup, sehingga task 0,5 vCPU tidak lagi menjadwalkan seolah punya 64 core (Fase 9).

Health check: dua jenis, dua tujuan

1. Health check CONTAINER (di task definition)
   → "prosesnya hidup?"  ECS me-restart container kalau gagal
   → JANGAN menyentuh database

2. Health check TARGET GROUP (di ALB)
   → "boleh dikirimi lalu lintas?"  ALB berhenti mengirim kalau gagal
   → boleh memeriksa dependensi, dan inilah yang berubah saat shutdown
// Mode health check tanpa curl — image distroless tidak punya curl.
func main() {
	cekSehat := flag.Bool("health", false, "periksa kesehatan lalu keluar")
	flag.Parse()

	if *cekSehat {
		resp, err := http.Get("http://127.0.0.1:8080/sehat")
		if err != nil || resp.StatusCode != http.StatusOK {
			os.Exit(1)
		}
		os.Exit(0)
	}
	...
}

Health check yang menyentuh database mengubah gangguan jadi bencana. Kalau database sempat lambat 30 detik, semua task dinyatakan tidak sehat, ECS mematikan semuanya, task baru juga gagal health check, dan aplikasimu benar-benar mati — padahal sebagian besar endpoint (yang di-cache di CDN, yang tidak menyentuh database) sebenarnya masih bisa melayani.

Beberapa service dari satu image

Service "toko-server"     command: ["/server"]    desiredCount: 4   di belakang ALB
Service "toko-worker"     command: ["/worker"]    desiredCount: 2   tanpa ALB
Service "toko-cron"       command: ["/cron"]      desiredCount: 1   ← WAJIB 1

desiredCount: 1 untuk penjadwal bukan pilihan. Dua task cron berarti setiap job berjalan dua kali — email dobel, invoice dobel, laporan dobel. Alternatif yang lebih tahan: EventBridge Scheduler yang memicu run-task sekali per jadwal, sehingga tidak ada proses yang harus tetap hidup.

Jaringan

VPC
├── Subnet publik  (2 AZ)   → ALB, NAT Gateway
└── Subnet privat  (2 AZ)   → task ECS, RDS, ElastiCache

Security group:
  sg-alb     masuk  : 443 dari 0.0.0.0/0 (atau hanya dari CloudFront)
  sg-task    masuk  : 8080 HANYA dari sg-alb
  sg-db      masuk  : 5432 HANYA dari sg-task
  sg-redis   masuk  : 6379 HANYA dari sg-task

Rujuk security group lain, jangan rentang CIDR. sg-db yang mengizinkan 10.0.0.0/16 berarti apa pun di VPC bisa menyentuh database — termasuk instans yang dibuat orang lain nanti. Merujuk sg-task membuat izinnya mengikuti peran, bukan alamat.

Menjalankan migrasi

# Task terpisah, dijalankan pipeline SEBELUM service diperbarui (Fase 4)
aws ecs run-task \
  --cluster toko \
  --task-definition toko-migrate:12 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-a,subnet-b],securityGroups=[sg-task]}" \
  --started-by "deploy-$COMMIT"

# Tunggu selesai, lalu periksa exit code-nya
aws ecs wait tasks-stopped --cluster toko --tasks $TASK_ARN
aws ecs describe-tasks --cluster toko --tasks $TASK_ARN \
  --query 'tasks[0].containers[0].exitCode'

Periksa exit code-nya secara eksplisit. aws ecs wait tasks-stopped hanya berarti task berhenti — bukan berhasil. Tanpa pemeriksaan ini, migrasi yang gagal akan diikuti deploy yang tetap berjalan, dan kode baru bertemu skema lama.

Latihan: buat cluster ECS dengan satu service Fargate dari image aplikasimu. Setel stopTimeout: 5 lalu picu deploy sambil mengirim permintaan lambat — amati permintaan terpotong. Naikkan ke 60 dan ulangi. Lalu ubah health check container agar memeriksa database, matikan database sebentar, dan lihat seluruh service ikut mati.

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