AWS menyediakan banyak layanan untuk membangun ekosistem containerized application di cloud. Opsi deployment disediakan dari yang paling mudah di App Runner, hingga yang paling kompleks di EKS. Bahkan, untuk CI/CD saja, disediakan CodePipeline, CodeBuild, CodeDeploy, dan CodeCatalyst. Tapi pada kenyataannya, tidak semua project butuh orkestrasi yang kompleks. Kadang, ECS ditambah dengan CodeBuild buildspec.yaml custom saja sudah lebih dari cukup untuk menangani seluruh lifecycle containermu.
Artikel ini mendokumentasikan studi kasus ekosistem pengembangan aplikasi berbasis AI, Aibeecara Indonesia, dengan komponen AI wrapper Django, backend Django, dan mobile apps Flutter. Dua aplikasi pertama dideploy ke Amazon ECS dengan launch type EC2, kemudian mobile apps diupload ke Google Drive menggunakan gdrive CLI.
Arsitektur#
Sebelum mulai, mari kita pahami bersama diagram rancangan infrastruktur Aibeecara Indonesia.

Sebagai catatan, perlu diingat bahwa produk Aibeecara saat ini sudah memasuki tahap production. Estimasi pengguna awal adalah 10-15 pengguna, dengan traffic yang sangat sporadis.
Secara garis besar, ekosistem container yang akan dibangun terdiri dari komponen-komponen berikut:
- CodeBuild, untuk menjalankan CI/CD
- ECR, untuk menyimpan container image
- ECS, untuk menjalankan container
- Secrets Manager, untuk menyimpan secrets
Sebagai wadah deployment, disini kami memilih menggunakan ECS dengan launch type EC2.
Kok, gak pake fargate aja?
Yes, sekali lagi ingat bahwa kondisi produk Aibeecara saat ini adalah production skala kecil. Kami butuh solusi yang minim expense, namun juga butuh service dengan reliablity tinggi.
ECS sendiri tidak memungut biaya untuk control planenya. Yang dibayar hanyalah compute resource yang menjalankan container. Pada launch type EC2, compute resource tersebut adalah instance EC2 yang dikelola sendiri. Fargate memang lebih simpel, AWS yang mengelola computenya, tapi dengan harga yang lebih mahal.
Sebagai gambaran, instance EC2 t4g.small dengan 2vCPU dan 2GB RAM dibanderol seharga $15.48 per bulan di ap-southeast-3. Apalagi jika dikombinasikan dengan Reserved Instances atau Savings Plans, penghematan bisa mencapai 40 sampai 60% dibandingkan Fargate.1
Ya, memang ada spot fargate dengan harga yang mirip dengan EC2 on-demand. Namun tetap saja, secara naturalnya, spot instance tidak bisa menjanjikan SLA, sehingga kurang cocok untuk use case ini.
Alur Deployment#

Untuk Backend dan AI#
Kedua aplikasi ini memiliki alur CI/CD yang identik, hanya berbeda di nama service, repository ECR, dan task definitionnya.
- Developer melakukan push ke branch di repository
- Webhook CodeBuild tertrigger
- CodeBuild menjalankan buildspec.yaml:
- Phase pre_build: Login ke ECR dan menyiapkan variabel image tag
- Phase build: Membuild Docker image dan mentagnya
- Phase post_build: Mempush image ke ECR, mendaftarkan task definition baru, kemudian mengupdate ECS service
- ECS melakukan rolling update, mengganti task lama dengan task baru yang menggunakan image terbaru
Proses rolling update ini bersifat zero-downtime. ECS akan menjalankan task baru terlebih dahulu, memastikan task tersebut healthy, baru kemudian menghentikan task lama. Dengan begitu, aplikasi tetap bisa diakses selama proses deployment berlangsung.
Untuk Mobile Apps#
- Developer melakukan push ke branch di repository
- Webhook CodeBuild tertrigger
- CodeBuild menjalankan buildspec.yaml:
- Phase install: Menyiapkan Java, Android SDK, Flutter SDK, dan gdrive CLI, kemudian mengimport akun Google Drive
- Phase pre_build: Meresolve branch ke folder Drive tujuan, mengambil versi dari pubspec.yaml, mendecode keystore, menyiapkan key.properties, dan menjalankan flutter pub get
- Phase build: Membuild APK untuk semua branch, dan AAB untuk branch selain develop
- Phase post_build: Mengenerate changelog dari git log, mengupload folder release ke Google Drive sesuai branch, kemudian menghapus file signing sensitif
Pertama, Yuk Persiapkan IAM#
Agar seluruh ekosistem ini berjalan, ada tiga IAM role yang perlu dikonfigurasi, yaitu task execution role ECS, service role CodeBuild, dan instance role ECS.
ECS Task Execution Role#
Task execution role adalah role yang digunakan oleh ECS agent untuk menarik image dari ECR dan mengambil secrets dari Secrets Manager pada saat task dijalankan. Role ini berbeda dengan task role yang digunakan oleh aplikasi di dalam container.
Buka menu IAM, pilih Roles di menu sebelah kiri, dan pilih Create Role

Pada step 1, Trusted entity type, pilih AWS service. Kemudian, untuk use case, pilih Elastic Container Service Task. Kemudian klik next untuk ke step selanjutnya.

Di step 2, cari dan pilih AmazonECSTaskExecutionRolePolicy, kemudian klik next.

Terakhir, beri nama dan deskripsi, lalu klik Create role di bagian bawah


Setelah berhasil membuat role baru, pergi ke halaman detail role dan add permission. Pilih yang inline policy

Masukkan policy untuk ECS task execution role berikut.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:ap-southeast-3:xxxxxxxxxxxx:secret:*"
}
]
}Beri nama untuk policy baru ini, kemudian klik create policy.

AWS sebenarnya sudah menyediakan managed policy AmazonECSTaskExecutionRolePolicy yang mencakup permission ECR dan CloudWatch Logs. Tapi karena policy tersebut tidak mencakup Secrets Manager, kita perlu menambahkan permission secretsmanager:GetSecretValue secara terpisah.
CodeBuild Service Role#
CodeBuild service role membutuhkan permission untuk berinteraksi dengan ECR, ECS, dan Secrets Manager.
Pilih Create role pada menu IAM sekali lagi. Kali ini, pilih CodeBuild sebagai use casenya.

Skip step ke-2, kita tidak membutuhkan AWS managed policy pada role ini. Klik next untuk lanjut ke step 3 dan memberikan nama beserta deskripsi. Kemudian, scroll ke bawah untuk menekan tombol create role.


Setelah role berhasil dibuat, tambahkan policy CodeBuild berikut ini. Tambahkan melalui inline policy, seperti yang sebelumnya telah dilakukan pada ECS task execution role
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload"
],
"Resource": [
"arn:aws:ecr:ap-southeast-3:xxxxxxxxxxxx:repository/backend",
"arn:aws:ecr:ap-southeast-3:xxxxxxxxxxxx:repository/ai"
]
},
{
"Effect": "Allow",
"Action": [
"ecs:DescribeTaskDefinition",
"ecs:RegisterTaskDefinition",
"ecs:UpdateService",
"ecs:DescribeServices"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": [
"arn:aws:iam::xxxxxxxxxxxx:role/aibeecaraECSTaskExecutionRole",
"arn:aws:iam::xxxxxxxxxxxx:role/ecsTaskRole"
]
},
{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:ap-southeast-3:xxxxxxxxxxxx:secret:*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
}Kemudian simpan. Perhatikan bahwa ecr:GetAuthorizationToken membutuhkan Resource * karena action ini memang tidak mendukung resource-level permission. Berbeda dengan action ECR lainnya yang discope ke repository spesifik supaya CodeBuild hanya bisa mempush ke repository tersebut. Remember to always scope your policy properly, bro.

ECS Instance Role#
ECS Instance role digunakan oleh ECS instance untuk mendaftarkan diri ke cluster. Role ini harus memiliki AWS managed policy yaitu AmazonEC2ContainerServiceforEC2Role.
Pilih Create role pada menu IAM sekali lagi. Kali ini, pilih EC2 sebagai use casenya. Klik next untuk lanjut ke step selanjutnya.

Pada step 2, cari dan pilih AmazonEC2ContainerServiceforEC2Role. Centang dan klik next untuk pergi ke tahap review

Beri nama, lalu create role


Selesai! Ketiga role ini akan diattach pada tahapan selanjutnya.
Konfigurasi Secrets Manager#
Secrets Manager digunakan untuk menyimpan semua kredensial dan konfigurasi sensitif yang dibutuhkan oleh aplikasi maupun proses CI/CD. Setiap secret disimpan dalam format JSON key-value pair.
Secrets Manager memungut biaya sebesar $0.40 per *secret* per bulan dan $0.05 per 10.000 API call.2 Maka dari itu, sebaiknya gabungkan beberapa key-value pair yang terkait ke dalam satu secret untuk menghemat biaya.
Untuk menambahkan secret baru, pergi ke menu Secrets Manager dan pilih Store a new secret.

Untuk secret type, pilih yang other type of secret.

Untuk ekosistem Aibeecara, secret dikelompokkan berdasarkan aplikasi sebagai berikut.
Pertama, secret untuk AI wrapper, berisi kredensial ke layanan AI eksternal.
{
"GEMINI_KEY": "AIzaSyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"DEEPGRAM_KEY": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"EVALUATION_BASE_URL": "https://evaluation.internal.aibeecara/api"
}Kedua, secret untuk backend, berisi seluruh environment variable sensitif termasuk koneksi database, Redis, JWT, Midtrans, dan SMTP.
{
"SECRET_KEY": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"JWT_SECRET_KEY": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"DB_NAME": "aibeecara_prod",
"DB_USER": "aibeecara_user",
"DB_PASSWORD": "xxxxxxxxxxxxxxxxxxxxxxxx",
"DB_HOST": "db.internal.aibeecara",
"DB_PORT": "5432",
"REDIS_URL": "redis://redis.internal.aibeecara:6379/0",
"MIDTRANS_SERVER_KEY": "Mid-server-xxxxxxxxxxxxxxxxxxxxxxxx",
"MIDTRANS_CLIENT_KEY": "Mid-client-xxxxxxxxxxxxxxxxxxxxxxxx",
"EMAIL_HOST_USER": "[email protected]",
"EMAIL_HOST_PASSWORD": "xxxxxxxxxxxxxxxx"
}Ketiga, secret untuk mobile signing, berisi keystore dan kredensial signing.
{
"KEYSTORE_BASE64": "MIIKXQIBAzCCCh...base64encodedkeystore...",
"KEYSTORE_PASSWORD": "xxxxxxxxxxxxxxxx",
"KEY_ALIAS": "aibeecara",
"KEY_PASSWORD": "xxxxxxxxxxxxxxxx"
}Keempat, secret untuk gdrive account archive, disimpan sebagai plaintext secret dalam format base64. Archive ini berasal dari perintah gdrive account export yang dijalankan di localhost. Karena kita belum sampai disana, berikan string dummy terlebih dahulu.
anGzb2FAAAAAAA=Kelima, secret untuk Github personal access token yang akan digunakan sebagai autentikasi CodeBuild ke GitHub saat mendaftarkan webhook. Secret ini juga Disimpan sebagai plaintext secret. Proses pembuatan Github personal access token dapat merujuk ke dokumentasi resmi. Scope yang dibutuhkan untuk personal access tokennya adalah admin:repo_hook dan repo.
Berdasarkan (dokumentasi resmi)[https://docs.aws.amazon.com/codebuild/latest/userguide/asm-create-secret.html], format penyimpanan kredensial Github untuk Codebuild adalah sebagai berikut.
{
"ServerType": "GITHUB",
"AuthType": "PERSONAL_ACCESS_TOKEN",
"Token": "ghp_8jYxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx0Gy"
}Setelah memasukkan JSONnya, beri nama untuk secret tersebut dan klik next hingga step terakhir, kemudian store.

Ulangi prosesnya hingga keempat secret untuk masing-masing aplikasi berhasil dibuat.

Dengan metode seperti ini, total secret yang digunakan hanya ada 5, yaitu seharga $2.00 per bulan. Jika masing masing variable di*treat* sebagai satu secret, maka nantinya akan ada puluhan *secret* yang totalnya bisa tembus lebih dari $10 per bulan.
Retrieve Secrets di ECS Task Definition, Gimana sih?#
ECS dapat menginjeksi secrets langsung ke dalam container melalui task definition. Dengan cara ini, environment variable sensitif tidak perlu dihardcode di mana pun. ECS akan mengambil nilainya dari Secrets Manager pada saat task dijalankan.
{
"containerDefinitions": [
{
"name": "backend",
"image": "xxxxxxxxxxxx.dkr.ecr.ap-southeast-3.amazonaws.com/backend:latest",
"secrets": [
{
"name": "SECRET_KEY",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-3:xxxxxxxxxxxx:secret:aibeecara/backend/prod-xxxxxx:SECRET_KEY::"
},
{
"name": "DB_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-3:xxxxxxxxxxxx:secret:aibeecara/backend/prod-xxxxxx:DB_PASSWORD::"
}
]
}
]
}Format ARN untuk mengambil key spesifik dari sebuah secret adalah sebagai berikut.
arn:aws:secretsmanager:<region>:<account_id>:secret:<secret_name>-<6_random_chars>:<json_key>::Perhatikan 6 karakter acak di belakang nama secret. Karakter ini otomatis digenerate Secrets Manager saat secret dibuat dan wajib dicantumkan di ARN. Jika ARN ditulis tanpa karakter ini, ECS akan gagal start task dengan error ResourceInitializationError: unable to pull secrets.3
Dua titik dua di akhir ARN adalah version-stage dan version-id yang dikosongkan, sehingga ECS akan selalu mengambil versi terbaru dari secret tersebut.
Perlu diingat juga, execution role dari task definition harus memiliki permission secretsmanager:GetSecretValue terhadap ARN secret yang dirujuk. Tanpa permission ini, task akan gagal saat startup karena tidak bisa mengambil secret.
Kalo Retrieve Secrets di CodeBuild, Gimana?#
CodeBuild juga bisa mengambil secrets dari Secrets Manager dan menjadikannya environment variable selama proses build. Konfigurasinya bisa dilakukan di dua tempat, yaitu di level project CodeBuild atau langsung di buildspec.yaml.
Pada konfigurasi environment variable CodeBuild project, pilih tipe Secrets Manager dan masukkan referensi secret beserta keynya.
| Name | Value | Type |
|---|---|---|
| DB_PASSWORD | aibeecara/backend/prod:DB_PASSWORD | Secrets Manager |
| MIDTRANS_SERVER_KEY | aibeecara/backend/prod:MIDTRANS_SERVER_KEY | Secrets Manager |
Atau, secrets juga bisa dideklarasikan langsung di buildspec.yaml menggunakan section secrets-manager.
env:
secrets-manager:
DB_PASSWORD: aibeecara/backend/prod:DB_PASSWORD
MIDTRANS_SERVER_KEY: aibeecara/backend/prod:MIDTRANS_SERVER_KEYKedua cara ini menghasilkan hal yang sama, yaitu environment variable yang bisa diakses di dalam buildspec.yaml seperti environment variable biasa. Perbedaannya adalah, konfigurasi di level project tidak akan terekspos di source code. Sedangkan, konfigurasi di buildspec.yaml akan lebih mudah untuk ditrack perubahannya melalui version control.
Konfigurasi ECR#
Amazon Elastic Container Registry, biasa disingkat ECR, digunakan sebagai private registry untuk menyimpan container image. Setiap aplikasi memiliki repository ECRnya masing-masing.
ECR memungut biaya sebesar $0.10 per GB per bulan untuk penyimpanan image di private repository.4 Biaya ini tergolong murah, tapi bisa membengkak jika image lama tidak pernah dibersihkan. Untuk menghemat storage, aktifkan lifecycle policy yang secara otomatis menghapus image lama.
Buat repository ECR untuk setiap aplikasi.
aws ecr create-repository --repository-name backend --region ap-southeast-3
aws ecr create-repository --repository-name ai --region ap-southeast-3Cek menu Elastic Container Registry pada AWS console dan perhatikan bahwa kedua repository telah berhasil dibuat.

Lifecycle Policy#
Tanpa lifecycle policy, setiap kali CodeBuild mempush image baru, image lama tetap tersimpan di ECR. Seiring waktu, storage yang terpakai akan terus bertambah. Lifecycle policy mengatasi masalah ini dengan menghapus image secara otomatis berdasarkan kriteria tertentu.
Buat file bernama lifecycle-policy.json dan masukkan lifecycle policy berikut supaya repository hanya menyimpan 10 image terakhir.
{
"rules": [
{
"rulePriority": 1,
"description": "Simpan hanya 10 image terakhir",
"selection": {
"tagStatus": "any",
"countType": "imageCountMoreThan",
"countNumber": 10
},
"action": {
"type": "expire"
}
}
]
}Terapkan lifecycle policy ke repository.
aws ecr put-lifecycle-policy \
--repository-name backend \
--lifecycle-policy-text file://lifecycle-policy.json \
--region ap-southeast-3Perlu diperhatikan bahwa lifecycle policy dievaluasi dalam waktu 24 jam setelah diterapkan, bukan langsung saat itu juga.5 Jadi jangan panik kalau image lama belum langsung terhapus.
Untuk repository ai, jalankan command yang sama dengan mengganti parameter –repository-name menjadi ai.
Konfigurasi ECS#
Konfigurasi ECS pada study case ini terdiri dari empat bagian. Pertama, ECS cluster sebagai environment untuk menjalankan task dan service. Kedua, ECS Instance, yaitu EC2 tempat container akan berjalan nantinya. Ketiga, task definition untuk backend dan AI wrapper yang mendefinisikan bagaimana masing-masing container akan dijalankan. Keempat, ECS service yang membuat task tersebut berjalan di instance yang terdaftar di cluster.
ECS Cluster#
Langkah pertama adalah membuat ECS cluster dengan nama dan region yang sesuai. Aibeecara memiliki user base di Indonesia, sehingga lokasi yang cocok untuk production adalah di Jakarta.
aws ecs create-cluster \
--cluster-name production \
--region ap-southeast-3ECS Instance#
ECS instance adalah EC2 instance yang menjalankan ECS Agent dan terdaftar ke cluster. nstance ini harus menggunakan ECS-optimized AMI versi ARM karena instance yang digunakan adalah t4g.small yang berbasis Graviton.
Sebelum membuat instance, siapkan beberapa variabel yang dibutuhkan terlebih dahulu. Pertama, siapkan AMI ID yang akan digunakan.
AMI_ID=$(aws ssm get-parameters \
--names /aws/service/ecs/optimized-ami/amazon-linux-2/arm64/recommended \
--query 'Parameters[0].Value' \
--output text \
--region ap-southeast-3 | jq -r '.image_id')Kemudian, persiapkan subnet id yang akan digunakan.
SUBNET_ID=$(aws ec2 describe-subnets \
--filters Name=default-for-az,Values=true \
--query 'Subnets[0].SubnetId' \
--output text \
--region ap-southeast-3)Jika terjadi error karena tidak ada default VPC, jalankan
aws ec2 create-default-vpc --region ap-southeast-3terlebih dahulu, lalu ulangi command di atas.
Kemudian, buat key pair untuk akses SSH ke instance.
aws ec2 create-key-pair \
--key-name aibeecara-key \
--query 'KeyMaterial' \
--output text \
--region ap-southeast-3 > aibeecara-key.pem
chmod 400 aibeecara-key.pemBuat security group dan buka port yang dibutuhkan. Port 8000 untuk backend dan port 22 untuk SSH. Port 50051 tidak perlu dibuka karena AI wrapper hanya dikomunikasikan secara internal via localhost dari backend.
SECURITY_GROUP_ID=$(aws ec2 create-security-group \
--group-name aibeecara-sg \
--description "Security group for aibeecara ECS instance" \
--query 'GroupId' \
--output text \
--region ap-southeast-3)
aws ec2 authorize-security-group-ingress \
--group-id $SECURITY_GROUP_ID \
--protocol tcp --port 8000 --cidr 0.0.0.0/0 \
--region ap-southeast-3
aws ec2 authorize-security-group-ingress \
--group-id $SECURITY_GROUP_ID \
--protocol tcp --port 22 --cidr 0.0.0.0/0 \
--region ap-southeast-3Setelah semua variabel siap, launch EC2 instance dengan menyisipkan User Data. Tujuannya adalah, supaya ECS Agent otomatis mendaftarkan instance ke cluster production saat pertama kali boot.
aws ec2 run-instances \
--image-id $AMI_ID \
--instance-type t4g.small \
--key-name aibeecara-key \
--security-group-ids $SECURITY_GROUP_ID \
--subnet-id $SUBNET_ID \
--iam-instance-profile Name=aibeecaraECSInstanceRole \
--user-data '#!/bin/bash
echo ECS_CLUSTER=production >> /etc/ecs/ecs.config' \
--region ap-southeast-3Verifikasi melalui AWS console bahwa instance tersebut sudah masuk ke dalam container instances milik cluster production

ECS Task Definition#
Berikut contoh task definition untuk AI wrapper. Aplikasi ini nantinya akan listen di port 50051 untuk gRPC dan dipanggil oleh backend via localhost.
Buat file bernama ai-task.json dengan isi sebagai berikut:
{
"family": "ai-task",
"networkMode": "host",
"requiresCompatibilities": ["EC2"],
"executionRoleArn": "arn:aws:iam::xxxxxxxxxxxx:role/aibeecaraECSTaskExecutionRole",
"cpu": "1024",
"memory": "1024",
"containerDefinitions": [
{
"name": "ai",
"image": "xxxxxxxxxxxx.dkr.ecr.ap-southeast-3.amazonaws.com/ai:latest",
"essential": true,
"secrets": [
{
"name": "GEMINI_KEY",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-3:xxxxxxxxxxxx:secret:aibeecara/ai/prod-xxxxxx:GEMINI_KEY::"
},
{
"name": "DEEPGRAM_KEY",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-3:xxxxxxxxxxxx:secret:aibeecara/ai/prod-xxxxxx:DEEPGRAM_KEY::"
},
{
"name": "EVALUATION_BASE_URL",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-3:xxxxxxxxxxxx:secret:aibeecara/ai/prod-xxxxxx:EVALUATION_BASE_URL::"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/ai",
"awslogs-region": "ap-southeast-3",
"awslogs-stream-prefix": "ecs",
"awslogs-create-group": "true"
}
}
}
]
}Task definition untuk backend polanya sama, hanya saja image menunjuk ke repository backend, bagian environment dan secrets disesuaikan, dan AI_GRPC_TARGET diset ke localhost:50051.
Buat juga backend-task.json dengan isi sebagai berikut:
{
"family": "backend-task",
"networkMode": "host",
"requiresCompatibilities": ["EC2"],
"executionRoleArn": "arn:aws:iam::xxxxxxxxxxxx:role/aibeecaraECSTaskExecutionRole",
"cpu": "1024",
"memory": "1024",
"containerDefinitions": [
{
"name": "backend",
"image": "xxxxxxxxxxxx.dkr.ecr.ap-southeast-3.amazonaws.com/backend:latest",
"essential": true,
"environment": [
{ "name": "ENV", "value": "production" },
{ "name": "DEBUG", "value": "False" },
{ "name": "ALLOWED_HOSTS", "value": "*" },
{ "name": "AI_GRPC_TARGET", "value": "localhost:50051" },
{ "name": "AI_GRPC_SECURE", "value": "False" },
{ "name": "EMAIL_HOST", "value": "smtp.gmail.com" },
{ "name": "EMAIL_PORT", "value": "587" },
{ "name": "EMAIL_USE_TLS", "value": "True" }
],
"secrets": [
{
"name": "SECRET_KEY",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-3:xxxxxxxxxxxx:secret:aibeecara/backend/prod-xxxxxx:SECRET_KEY::"
},
{
"name": "JWT_SECRET_KEY",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-3:xxxxxxxxxxxx:secret:aibeecara/backend/prod-xxxxxx:JWT_SECRET_KEY::"
},
{
"name": "DB_NAME",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-3:xxxxxxxxxxxx:secret:aibeecara/backend/prod-xxxxxx:DB_NAME::"
},
{
"name": "DB_USER",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-3:xxxxxxxxxxxx:secret:aibeecara/backend/prod-xxxxxx:DB_USER::"
},
{
"name": "DB_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-3:xxxxxxxxxxxx:secret:aibeecara/backend/prod-xxxxxx:DB_PASSWORD::"
},
{
"name": "DB_HOST",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-3:xxxxxxxxxxxx:secret:aibeecara/backend/prod-xxxxxx:DB_HOST::"
},
{
"name": "DB_PORT",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-3:xxxxxxxxxxxx:secret:aibeecara/backend/prod-xxxxxx:DB_PORT::"
},
{
"name": "REDIS_URL",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-3:xxxxxxxxxxxx:secret:aibeecara/backend/prod-xxxxxx:REDIS_URL::"
},
{
"name": "MIDTRANS_SERVER_KEY",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-3:xxxxxxxxxxxx:secret:aibeecara/backend/prod-xxxxxx:MIDTRANS_SERVER_KEY::"
},
{
"name": "MIDTRANS_CLIENT_KEY",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-3:xxxxxxxxxxxx:secret:aibeecara/backend/prod-xxxxxx:MIDTRANS_CLIENT_KEY::"
},
{
"name": "EMAIL_HOST_USER",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-3:xxxxxxxxxxxx:secret:aibeecara/backend/prod-xxxxxx:EMAIL_HOST_USER::"
},
{
"name": "EMAIL_HOST_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:ap-southeast-3:xxxxxxxxxxxx:secret:aibeecara/backend/prod-xxxxxx:EMAIL_HOST_PASSWORD::"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/backend",
"awslogs-region": "ap-southeast-3",
"awslogs-stream-prefix": "ecs",
"awslogs-create-group": "true"
}
}
}
]
}Variabel yang tidak bersifat rahasia seperti ENV, DEBUG, AI_GRPC_TARGET, dan EMAIL_HOST ditaruh di environment saja. Tidak perlu ditaruh di secrets supaya tidak terhitung sebagai secret baru di Secrets Manager.
Registerkan kedua file task definition yang telah dibuat menggunakan command berikut.
aws ecs register-task-definition \
--cli-input-json file://ai-task.json \
--region ap-southeast-3
aws ecs register-task-definition \
--cli-input-json file://backend-task.json \
--region ap-southeast-3Dapat diverifikasi menggunakan console bahwa kedua task definition sudah berhasil dibuat

ECS Service#
ECS service untuk kedua aplikasi dibuat dengan schedulingStrategy: DAEMON. Dengan strategy ini, desired count tidak perlu diset manual, ECS otomatis menjalankan satu task di setiap container instance di cluster.6
aws ecs create-service \
--cluster production \
--service-name ai-service \
--task-definition ai-task \
--launch-type EC2 \
--scheduling-strategy DAEMON \
--region ap-southeast-3Service untuk backend dibuat dengan pola yang sama.
aws ecs create-service \
--cluster production \
--service-name backend-service \
--task-definition backend-task \
--launch-type EC2 \
--scheduling-strategy DAEMON \
--region ap-southeast-3Setiap instance akan dapat satu backend dan satu AI wrapper. Nantinya, scaling akan dilakukan di level EC2 Auto Scaling Group. Untuk melakukan scaling, cukup tambah instance untuk menambah kapasitas backend dan AI wrapper sekaligus.
Konfigurasi CodeBuild#
CodeBuild adalah fully managed build service yang menjalankan proses CI/CD berdasarkan instruksi di buildspec.yaml. CodeBuild memberikan 100 menit build gratis per bulan untuk tipe general1.small atau arm1.small.7 Free tier ini tersedia untuk pelanggan baru maupun lama, dan tidak ikut kedaluwarsa bersama free tier 12 bulan. Untuk kebanyakan proses build Docker image yang sederhana, general1.small sudah lebih dari cukup.
Integrasi GitHub#
Sebelum membuat project CodeBuild, CodeBuild perlu mendapatkan izin untuk mengakses repository Github. Caranya adalah dengan mendaftarkan personal access token dari Github ke CodeBuild.
Personal access token yang dibutuhkan harus memiliki scope repo dan admin:repo_hook. Pastikan sudah tersimpan ke dalam Secrets Manager seperti yang sudah dilakukan di bagian sebelumnya. Kemudian, jalankan command berikut.
aws codebuild import-source-credentials \
--server-type GITHUB \
--auth-type SECRETS_MANAGER \
--token arn:aws:secretsmanager:ap-southeast-3:xxxxxxxxxxxx:secret:aibeecara/repo/prod \
--region ap-southeast-3Codebuild Build Projects#
Satu hal yang sering terlewat saat pertama kali setup CodeBuild untuk Docker build adalah privileged mode. Flag ini wajib diaktifkan supaya Docker daemon bisa berjalan di dalam container build. Tanpa privileged mode, semua perintah docker build akan gagal dengan pesan Cannot connect to the Docker daemon.8
Aktifkan privileged mode saat membuat project via CLI dengan parameter privilegedMode=true di dalam environment.
aws codebuild create-project \
--name backend-cicd \
--source "type=GITHUB,location=https://github.com/Aibeecara-Development/aibeecara_be.git" \
--artifacts "type=NO_ARTIFACTS" \
--environment "type=LINUX_CONTAINER,image=aws/codebuild/amazonlinux-x86_64-standard:5.0,computeType=BUILD_GENERAL1_SMALL,privilegedMode=true" \
--service-role arn:aws:iam::xxxxxxxxxxxx:role/aibeecaraCloudBuildRole \
--region ap-southeast-3Lakukan hal yang sama untuk project AI wrapper. Karena juga membuild Docker image, privileged mode tetap wajib diaktifkan.
aws codebuild create-project \
--name ai-cicd \
--source "type=GITHUB,location=https://github.com/Aibeecara-Development/ai-aibeecara.git" \
--artifacts "type=NO_ARTIFACTS" \
--environment "type=LINUX_CONTAINER,image=aws/codebuild/amazonlinux-x86_64-standard:5.0,computeType=BUILD_GENERAL1_SMALL,privilegedMode=true" \
--service-role arn:aws:iam::xxxxxxxxxxxx:role/aibeecaraCloudBuildRole \
--region ap-southeast-3Untuk project mobile, privileged mode tidak diperlukan karena tidak membuild Docker image.
aws codebuild create-project \
--name mobile-cicd \
--source "type=GITHUB,location=https://github.com/Aibeecara-Development/aibeecara_mobile.git" \
--artifacts "type=NO_ARTIFACTS" \
--environment "type=LINUX_CONTAINER,image=aws/codebuild/amazonlinux-x86_64-standard:5.0,computeType=BUILD_GENERAL1_SMALL" \
--service-role arn:aws:iam::xxxxxxxxxxxx:role/aibeecaraCloudBuildRole \
--region ap-southeast-3Ketiga build projects tersebut dapat divalidasi melalui AWS console, pada menu Developer Tools > CodeBuild > Build projects

Webhook Trigger#
CodeBuild dapat menerima webhook dari GitHub, GitHub Enterprise Server, GitLab, GitLab Self Managed, dan Bitbucket.9 Dengan webhook, setiap push ke branch yang ditentukan akan otomatis mentrigger proses build tanpa perlu menjalankannya secara manual.
Buat webhook untuk project backend dan AI menggunakan command berikut.
aws codebuild create-webhook \
--project-name backend-cicd \
--filter-groups '[[{"type":"EVENT","pattern":"PUSH"},{"type":"HEAD_REF","pattern":"^refs/heads/main$"}]]' \
--region ap-southeast-3
aws codebuild create-webhook \
--project-name ai-cicd \
--filter-groups '[[{"type":"EVENT","pattern":"PUSH"},{"type":"HEAD_REF","pattern":"^refs/heads/main$"}]]' \
--region ap-southeast-3
aws codebuild create-webhook \
--project-name mobile-cicd \
--filter-groups '[[{"type":"EVENT","pattern":"PUSH"},{"type":"HEAD_REF","pattern":"^refs/heads/main$"}]]' \
--region ap-southeast-3Pada konfigurasi di atas, filter group dibuat supaya hanya push ke branch main yang mentrigger build, supaya push ke branch feature tidak memakan jatah build minutes.
Setelah berhasil dibuat, hasil konfigurasi webhook dapat dilihat melalui AWS console, pada menu Developer Tools > CodeBuild > Build projects > nama build projects

Buildspec Backend dan AI#
Berikut adalah contoh buildspec.yaml untuk aplikasi backend. Konfigurasi untuk AI wrapper pada dasarnya sama, hanya berbeda di nama service, repository, dan task definition.
version: 0.2
env:
variables:
AWS_ACCOUNT_ID: "xxxxxxxxxxxx"
AWS_DEFAULT_REGION: "ap-southeast-3"
IMAGE_REPO_NAME: "backend"
ECS_CLUSTER: "production"
ECS_SERVICE: "backend-service"
TASK_FAMILY: "backend-task"
CONTAINER_NAME: "backend"
phases:
pre_build:
commands:
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
- REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME
- COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
- IMAGE_TAG=${COMMIT_HASH:=latest}
build:
commands:
- docker build -t $REPOSITORY_URI:latest .
- docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
post_build:
commands:
- docker push $REPOSITORY_URI:latest
- docker push $REPOSITORY_URI:$IMAGE_TAG
- TASK_DEFINITION=$(aws ecs describe-task-definition --task-definition "$TASK_FAMILY" --region "$AWS_DEFAULT_REGION")
- NEW_TASK_DEF=$(echo $TASK_DEFINITION | jq --arg IMAGE "$REPOSITORY_URI:$IMAGE_TAG" '.taskDefinition | .containerDefinitions[0].image = $IMAGE | del(.taskDefinitionArn) | del(.revision) | del(.status) | del(.requiresAttributes) | del(.compatibilities) | del(.registeredAt) | del(.registeredBy) | del(.deregisteredAt) | del(.enableFaultInjection)')
- NEW_TASK_INFO=$(aws ecs register-task-definition --region "$AWS_DEFAULT_REGION" --cli-input-json "$NEW_TASK_DEF")
- NEW_REVISION=$(echo $NEW_TASK_INFO | jq -r '.taskDefinition.taskDefinitionArn')
- aws ecs update-service --cluster $ECS_CLUSTER --service $ECS_SERVICE --task-definition $NEW_REVISION --region $AWS_DEFAULT_REGIONTambahkan buildspec.yaml di root project repositori Aibeecara supaya dapat dibaca oleh Codebuild. Variabel xxxxxxxxxxxx disini hanya sebagai contoh placeholder, demi alasan keamanan.

Bagian yang paling penting adalah proses update task definition di fase post_build. Karena task definition bersifat immutable, setiap deployment harus membuat revision baru.10 Proses ini dilakukan dengan mengambil task definition yang sedang aktif, mengganti image URInya, menghapus field yang tidak diperlukan untuk registrasi ulang, kemudian mendaftarkannya sebagai revision baru*. Setelah itu, update-service dengan revision terbaru memaksa ECS untuk melakukan rolling update.
Buildspec Mobile Apps#
Berbeda dengan backend yang cukup butuh Docker, mobile apps butuh toolchain yang lebih panjang yaitu Java JDK, Android SDK, Flutter SDK, plus gdrive CLI untuk upload hasil build. Image CodeBuild default tidak menyediakan Flutter dan Android SDK, jadi keduanya perlu diinstall di fase install.
gdrive CLI dari Glotlabs yang dipakai di sini adalah command-line client untuk Google Drive yang mendukung operasi upload, download, list, dan delete.11 Untuk penggunaan di environment CI/CD yang headless, akun gdrive perlu disetup terlebih dahulu di localhost, kemudian diexport dan diimport ke environment CodeBuild.
Langkah persiapan di localhost:
- Buat Google OAuth Client credentials di Google Cloud Console
- Download binary gdrive dari halaman release
- Tambahkan akun Google ke gdrive. Selengkapnya di sini
gdrive account add- Export akun yang sudah ditambahkan
gdrive account export [email protected]- Simpan file archive hasil export ke Secrets Manager sebagai plaintext secret. Archive diencode ke base64 terlebih dahulu supaya aman saat diinject sebagai environment variable di CodeBuild.
Bagi pengguna Linux, gunakan command ini:
base64 -w 0 gdrive_export-nama_akun_gmail_com.tarBagi pengguna Windows PowerShell, gunakan command ini:
[Convert]::ToBase64String([IO.File]::ReadAllBytes("$PWD\gdrive_export-nama_akun_gmail_com.tar"))- Update di secret manager.
Berikut adalah contoh buildspec.yaml untuk mobile apps Flutter, diport dari workflow GitHub Actions existing. Buildspec ini mendukung tiga branch, yaitu production, staging, dan develop, masing-masing mengarah ke folder Google Drive yang berbeda.
version: 0.2
env:
variables:
FLUTTER_VERSION: "3.35.0"
JAVA_VERSION: "21"
GDRIVE_VERSION: "3.9.1"
secrets-manager:
GDRIVE_ACCOUNT_ARCHIVE: aibeecara/mobile/gdrive-account
KEYSTORE_BASE64: aibeecara/mobile/signing:KEYSTORE_BASE64
KEYSTORE_PASSWORD: aibeecara/mobile/signing:KEYSTORE_PASSWORD
KEY_ALIAS: aibeecara/mobile/signing:KEY_ALIAS
KEY_PASSWORD: aibeecara/mobile/signing:KEY_PASSWORD
DRIVE_PRODUCTION: aibeecara/mobile/drive:DRIVE_PRODUCTION
DRIVE_STAGING: aibeecara/mobile/drive:DRIVE_STAGING
DRIVE_PREVIEW: aibeecara/mobile/drive:DRIVE_PREVIEW
phases:
install:
runtime-versions:
java: corretto21
commands:
- mkdir -p /opt/android-sdk/cmdline-tools
- curl -LO https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
- unzip -q commandlinetools-linux-11076708_latest.zip -d /opt/android-sdk/cmdline-tools
- mv /opt/android-sdk/cmdline-tools/cmdline-tools /opt/android-sdk/cmdline-tools/latest
- export ANDROID_HOME=/opt/android-sdk
- export PATH=$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$PATH
- yes | sdkmanager --licenses > /dev/null
- sdkmanager "platform-tools" "platforms;android-34" "build-tools;34.0.0" > /dev/null
- git clone https://github.com/flutter/flutter.git -b stable --depth 1 /opt/flutter
- cd /opt/flutter && git checkout $FLUTTER_VERSION
- export PATH=/opt/flutter/bin:$PATH
- flutter --disable-analytics
- flutter precache --android
- curl -LO https://github.com/glotlabs/gdrive/releases/download/$GDRIVE_VERSION/gdrive_linux-x64.tar.gz
- tar -xzf gdrive_linux-x64.tar.gz
- chmod +x gdrive
- mv gdrive /usr/local/bin/
- echo "$GDRIVE_ACCOUNT_ARCHIVE" | base64 -d > /tmp/gdrive-account.tar
- gdrive account import /tmp/gdrive-account.tar
- rm /tmp/gdrive-account.tar
pre_build:
commands:
- BRANCH_NAME=$(echo "$CODEBUILD_WEBHOOK_HEAD_REF" | sed 's|refs/heads/||')
- |
if [ "$BRANCH_NAME" = "develop" ]; then
FOLDER_PREFIX="frontend"
DRIVE_PARENT="$DRIVE_PREVIEW"
elif [ "$BRANCH_NAME" = "production" ]; then
FOLDER_PREFIX="aibeecara"
DRIVE_PARENT="$DRIVE_PRODUCTION"
else
FOLDER_PREFIX="aibeecara"
DRIVE_PARENT="$DRIVE_STAGING"
fi
- VERSION=$(grep '^version: ' pubspec.yaml | cut -d ' ' -f 2 | tr -d '\r')
- VERSION="$VERSION-build-$CODEBUILD_BUILD_NUMBER"
- FOLDER_NAME="$FOLDER_PREFIX v$VERSION"
- mkdir "$FOLDER_NAME"
- sed -i 's/minSdk *= *23/minSdk = flutter.minSdkVersion/' android/app/build.gradle.kts
- echo "$KEYSTORE_BASE64" | base64 -d > android/app/aibeecara.jks
- echo "storePassword=$KEYSTORE_PASSWORD" > android/key.properties
- echo "keyPassword=$KEY_PASSWORD" >> android/key.properties
- echo "keyAlias=$KEY_ALIAS" >> android/key.properties
- echo "storeFile=aibeecara.jks" >> android/key.properties
- flutter pub get
build:
commands:
- flutter build apk --release
- mv build/app/outputs/flutter-apk/app-release.apk "$FOLDER_NAME/aibeecara.apk"
- |
if [ "$BRANCH_NAME" != "develop" ]; then
flutter build appbundle --release
mv build/app/outputs/bundle/release/app-release.aab "$FOLDER_NAME/aibeecara.aab"
fi
post_build:
commands:
- |
if [ -n "$CODEBUILD_WEBHOOK_PREV_COMMIT" ]; then
COMMIT_RANGE="$CODEBUILD_WEBHOOK_PREV_COMMIT..$CODEBUILD_RESOLVED_SOURCE_VERSION"
else
COMMIT_RANGE="$CODEBUILD_RESOLVED_SOURCE_VERSION~10..$CODEBUILD_RESOLVED_SOURCE_VERSION"
fi
- git log --pretty=format:"- %s (%an)" --no-merges $COMMIT_RANGE > "$FOLDER_NAME/changelog.md" || echo "- No new commits" > "$FOLDER_NAME/changelog.md"
- gdrive files upload --recursive --parent "$DRIVE_PARENT" "$FOLDER_NAME"
- rm android/app/aibeecara.jks android/key.propertiesFile keystore dan key.properties sengaja dihapus di akhir fase post_build. Meskipun environment CodeBuild bersifat ephemeral dan dihancurkan setelah build selesai, menghapus file sensitif secara eksplisit adalah kebiasaan yang baik untuk menghindari risiko jika build environment dicache atau direuse.
It’s a Wrap!#
Setelah semua konfigurasi selesai, nantinya, ketika ada push di branch yang telah ditentukan, maka Codebuild akan menjalankan build run baru secara otomatis.

Log dari setiap build run dapat dilihat pada menu Developer Tools > CodeBuild > Build projects > nama build projects > nama build run > Build logs

Pipeline backend dan AI wrapper akan membuat task definition revision baru di setiap deployment. Task definition revision ini berisi image terbaru yang sudah dipush ke ECR dengan tag sesuai commit hash. Nantinya, ECS service akan diupdate untuk menggunakan revision baru tersebut.

Artefak yang dihasilkan dari pipeline mobile akan dapat diakses melalui Google Drive yang sebelumnya telah dikonfigurasi.

Berapa Ya, Biayanya?#
Mari kita hitung estimasi biaya bulanan untuk ekosistem CI/CD ini, dengan asumsi penggunaan di region ap-southeast-3 Jakarta.
EC2 t4g.small dibanderol seharga $0.0212 per jam di ap-southeast-3. Untuk 1 instance yang berjalan 24/7 selama 730 jam, maka:
- 1 x $0.0212 x 730 = $15.48 per bulan
ECR mengenakan $0.10 per GB per bulan. Dengan asumsi rata-rata 5GB storage untuk 2 repository yang masing-masing menyimpan 10 image terakhir.
- 5 x $0.10 = $0.50 per bulan
CodeBuild general1.small mengenakan $0.005 per menit. Dengan asumsi 300 menit build per bulan, dikurang dengan 100 menit free tier maka:
- (300 - 100) x $0.005 = $1.00 per bulan
Secrets Manager mengenakan $0.40 per secret per bulan. Dengan 4 secret yang disimpan pada project Aibeecara, maka:
- 5 x $0.40 = $2.00 per bulan
Sehingga, dapat disimpulkan bahwa estimasi biayanya adalah:
| Layanan | Penggunaan | Biaya/Bulan |
|---|---|---|
| ECS | Control plane tidak dipungut biaya | $0 |
| EC2 | 1 instance t4g.small | $15.48 |
| ECR | 5GB storage untuk 2 repository | $0.50 |
| CodeBuild | 200 menit dengan general1.small | $1.00 |
| Secrets Manager | 5 secrets | $2.00 |
| Total | $18.98 |
Ingat bahwa estimasi tersebut belum termasuk biaya EBS, data transfer, dan Elastic IP. Jangan lupa juga bahwa biaya EC2 juga bisa lebih murah jika menggunakan Reserved Instances atau Savings Plans. Sebagai referensi, EC2 Instance Savings Plans untuk t4g.small dengan komitmen 1 tahun tanpa upfront bisa menghemat sekitar 37% dari harga on-demand.12


