Tulisan ini adalah bagian dari seri Membangun Aplikasi PHP/Yii2 Modern dengan AWS. Di tulisan ini saya akan mendemonstrasikan bagaimana cara membangun sebuah CI/CD Pipeline untuk kode yang saya simpan di Github untuk mendeploy ke cluster kita di Amazon Elastic Container Service (ECS). Saya akan menggunakan AWS CodePipeline, AWS CodeBuild, dan Amazon Elastic Container Registry (ECR) dengan AWS Cloud Development Kit untuk memodelkan infrastruktur.
Dalam pengalaman pribadi saya, adalah kebiasaan yang bagus untuk mempersiapkan Continuous Integration/Continuous Deliver (CI/CD) setiap saya membuat proyek baru. Ini akan mengurangi jumlah friksi untuk melakukan pengujian di setiap commit di lingkungan produksi atau staging. Dengan demikian saya bisa dengan mudah mendeteksi kode yang cacat sedini mungkin sebelum terlambat. Membangun CI/CD pipeline bisa terasa mengintimidasi dan mempunya kurva pembelajaran yang cukup terjal, makanya banyak developer yang melakukannya di tahap akhir pengembangan. Tapi dengan menggunakan konsep Infrastructure as a Code, developer dapat dengan mudah mengkonfigurasi CI/CD Pipeline dan lagi, mereka dapat menggunakan templatenya untuk proyek selanjutnya.
Konfigurasinya akan terlihat seperti di bawah. Kita akan menggunakan AWS CodePipeline untuk memodelkan pipeline. Pipeline akan memiliki tiga tahapan: Source, Build, dan Deploy.
Tahap pertama dari Source akan mengambil kode dari GitHub, dan mengirimnya sebagai artifak ke tahap selanjutnya. Tahap kedua Build akan menggunakan AWS CodeBuild untuk membangun Docker image dari artifak kode dan menyimpannya ke Amazon ECR. Tahap ini akan mengeluarkan nama container dan URL dari image sebagai artifak ke tahap selanjutnya. Dan tahap terakhir Deploy akan mengupdate cluster di Amazon ECS untuk menggunakan URL image yang baru untuk containernya.
Mempersiapkan Akses Github
Sebelum kita mulai, kita akan mempersiapkan akses kita ke repository Github dan menyimpannya ke AWS Systems Manager Parameter Store dan AWS Secrets Manager sehingga pipeline kita dapat mengaksesnya dengan aman.
STEP 1 Buka halaman Github Settings / Developer settings / Personal access token.
STEP 2 Pilih Generate new token.
STEP 3 Isi nama dari token yang baru dan pilih repo
sebagai scope.
STEP 4 Pilih Generate new token
Sekarang kita akan menyimpan nama repositori dan pemilik github ke Parameter Store dan access token ke Secrets Manager. Eksekusi perintah berikut, dengan mengganti owner
, repo
, dan abcdefg1234abcdefg56789abcdefg
dengan konfigurasi Anda. Anda bisa juga mengganti name
dengan nama parameter yang mudah diingat.
aws ssm put-parameter \
--name /myapp/dev/GITHUB_OWNER \
--type String \
--value owner
aws ssm put-parameter \
--name /myapp/dev/GITHUB_REPO \
--type String \
--value repo
aws secretsmanager create-secret \
--name /myapp/dev/GITHUB_TOKEN \
--secret-string abcdefg1234abcdefg56789abcdefg
Jika Anda mengakses konsol Parameter Store, Anda dapat melihat bahwa parameternya sudah tersimpan.
Sama halnya kalau Anda mengakses konsol Secrets Manager, Anda dapat melihat akses rahasianya sudah tersimpan.
Menulis kode AWS CDK
Sekarang kita kembali ke kode kita dari artikel sebelumnya. Atau jika Anda baru, Anda dapat mengaksesnya di Github saya.
Kita sekarang butuh menginstalasi dependensi CDK untuk AWS CodePipeLine, AWS CodeBuild, dan Amazon ECR. Eksekusi kode berikut.
npm update
npm install @aws-cdk/aws-codepipeline @aws-cdk/aws-codepipeline-actions @aws-cdk/aws-codebuild @aws-cdk/aws-ecr @aws-cdk/aws-ssm @aws-cdk/aws-secretsmanager
Kita akan melakukan refactor dari kode sekarang WebStack
menjadi tiga Construct, Cluster
, WebApp
dan Pipeline
. Lihat Gist berikut untuk kode dari ketiganya.
Sekarang anda perlu mengeksekusi perintah berikut untuk melakukan deployment.
cdk deploy MyWebAppStack
Setelah deployment selesai, Anda dapat memeriksa konsol CodePipeline untuk melihat pipeline yang sudah dibuat.
Penjelasan Kode
Hal terbaik dari AWS CDK yang saya suka adalah, dengan menggunakan bahasa pemrograman umum seperti Javascript, Python, Java, dan Typescript, saya masih bisa mempraktikan banyak best practices di pemrograman yang familiar bagi saya. Dengan demikian, kode akan lebih mudah dibaca dan dijelaskan ke orang lain.
Cluster Construct
Construct Cluster
sangat sederhana. Construct ini hanya membuat cluster ECS. Instans dari construct ini nantinya akan dilewatkan ke WebApp
.
readonly ecsCluster: ecs.Cluster;
constructor(scope: cdk.Construct, id: string) {
super(scope, id);
this.ecsCluster = new ecs.Cluster(this, 'EcsCluster');
this.output();
}
Dalam tahap ini, tidak ada alasan besar mengapa kita perlu mengeluarkan ECS Cluster dari WebApp
construct selain dari menunjukkan bahwa kita bisa menggunakan beberapa construct. Tapi hal ini menunjukkan konsep yang bagus karena sebuah ECS Cluster dapat memiliki lebih dari satu ECS Service di dalamnya.
WebApp Construct
Construct ini sangat mirip dengan artikel sebelumnya. Ini hanya membangun sebuah layanan Fargate (dan mempersiapkan autoscaling, seperti yang kita bahas di artikel tersebut). Bedanya kita melakukan refactor dengan mengeluarkan ECS Cluster dari WebApp
dan memasukkanya ke Cluster
construct. Selain itu kita juga mengenalkan repositori ECR di construct ini untuk menyimpan docker image yang akan digunakan.
Di baris berikut kita melakukan tiga hal. Pertama kita membuat repositori ECR. Kedua, kita memberikan akses ke Fargate task execution role agar dapat mengakses ECR repositori untuk mengambil Docker image. Ketiga kita menyimpan nama ECS service dan container ke variabel agar bisa diberikan ke pipeline.
this.ecrRepo.grantPull(this.fargateService.taskDefinition.executionRole!);
this.service = this.fargateService.service;
this.containerName = this.fargateService.taskDefinition.defaultContainer!.containerName;
Pipeline Construct
Construct untuk pipeline agak sedikit kompleks. Kita membutuhkan instans WebApp
untuk konstruktor. Dari sini kita akan mendapatkan ECS Service dan nama container untuk diperbarui, dan repositori ECR untuk mengunggah Docker image. Setelah itu kita akan memanggil createPipeline
untuk menginstansiasi konstruk Pipeline
milik pustaka CodePipeline.
interface PipelineProps {
readonly webapp: WebApp;
}
class Pipeline extends cdk.Construct {
private readonly webapp: WebApp;
readonly service: ecs.IBaseService;
readonly containerName: string;
readonly ecrRepo: ecr.Repository;
public readonly pipeline: codepipeline.Pipeline;
constructor(scope: cdk.Construct, id: string, props: PipelineProps) {
super(scope, id);
this.webapp = props.webapp;
this.service = this.webapp.service;
this.ecrRepo = this.webapp.ecrRepo;
this.containerName = this.webapp.containerName;
this.pipeline = this.createPipeline();
this.output();
}
Pipeline
Kita membuat instans dari construct Pipeline
dengan tiga tahap dan dua artifak. Tiap tahap ini akan direfaktor menjadi tiga methods agar kodenya lebih mudah dibaca. Tiap artifak akan dilewatkan dan dikembalikan dari tahapan ke tahapan. Misalnya Artifak sourceOutput
akan menjadi keluaran dari tahap Source
dan masukan untuk tahap Build
. Artifak buildOutput
akan menjadi keluaran dari Build
dan masukan untuk Deploy
.
private createPipeline(): codepipeline.Pipeline {
const sourceOutput = new codepipeline.Artifact();
const buildOutput = new codepipeline.Artifact();
return new codepipeline.Pipeline(this, 'Pipeline', {
stages: [
this.createSourceStage('Source', sourceOutput),
this.createImageBuildStage('Build', sourceOutput, buildOutput),
this.createDeployStage('Deploy', buildOutput),
]
});
}
Source Stage
Di tahap Source
kita akan menggunakan GithubSourceAction
untuk mengambil kode secara aman dari Github. Biasnya saya menggunakan AWS CodeCommit untuk contoh penyimpanan kode, tetapi kali ini saya akan menggunakan Github karena saya menyimpan kodenya di Github. Untuk mengkonfigurasi action ini, kita perlu mengambil konfigurasi yang sudah kita siapkan sebelumnya. Kita akan mengambil nama dan pemilik repositori Github yang sudah disimpan di Parameter Store dan token Github yang sudah disimpan di Secrets Manager. Artifak keluaran dari tahap ini adalah kode yang diambil dari Github.
private createSourceStage(stageName: string, output: codepipeline.Artifact): codepipeline.StageProps {
const secret = cdk.SecretValue.secretsManager('/myapp/dev/GITHUB_TOKEN');
const repo = ssm.StringParameter.valueForStringParameter(this, '/myapp/dev/GITHUB_REPO');
const owner = ssm.StringParameter.valueForStringParameter(this, '/myapp/dev/GITHUB_OWNER');
const githubAction = new codepipeline_actions.GitHubSourceAction({
actionName: 'Github_Source',
owner: owner,
repo: repo,
oauthToken: secret,
output: output,
});
return {
stageName: stageName,
actions: [githubAction],
};
}
Build Stage
Selanjutnya, tahap Build
akan menerima artifak berupa kode Github dan akan mengembalikan artifak keluaran untuk tahap Deploy
.
private createImageBuildStage(stageName: string, input: codepipeline.Artifact, output: codepipeline.Artifact): codepipeline.StageProps {
Kita membuat CodeBuild di kode berikut. CodeBuild akan menerima buildspec dan definisi environment. Karena kita akan menggunakan perintah docker di dalam proses build, kita perlu mengkonfigurasi parameter privilegedMode
sebagai true
. Kita juga perlu melewatkan URL repositori dan nama container ke environment variable agar bisa digunakan oleh CodeBuild untuk mengunggah docker image dan menulis artifak keluaran.
const project = new codebuild.PipelineProject(
this, 'Project', {
buildSpec: this.createBuildSpec(),
environment: {
buildImage: codebuild.LinuxBuildImage.STANDARD_2_0,
privileged: true,
},
environmentVariables: {
REPOSITORY_URI: {value: this.ecrRepo.repositoryUri},
CONTAINER_NAME: {value: this.containerName}
}
}
);
Baris berikut akan memberikan akses ke CodeBuild untuk mengirim atau mengambil docker image ke atau dari repositori ECR.
this.ecrRepo.grantPullPush(project.grantPrincipal);
Method createBuildSpec()
akan membuat definisi dari build specification untuk CodeBuild. Tapi secara alternatif, kita bisa membuat buildspec.yml
di direktori proyek.
createBuildSpec(): codebuild.BuildSpec {
Baris berikut akan mengkonfigurasi runtime dan melakukan instalasi paket NPM dan Composer. Untuknya kita membutuhkan runtime nodejs
dan php
.
install: {
'runtime-versions': {
'nodejs': '10',
'php': '7.3'
},
commands: [
'npm install',
'composer install',
],
},
Di baris berikut, kita akan mengkonfigurasi CodeBuild untuk mengakses ECR dan membuat nama tag untuk menandai versi docker image yang akan kita buat.
'aws --version',
'$(aws ecr get-login --region ${AWS_DEFAULT_REGION} --no-include-email | sed \'s|https://||\')',
'COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)',
'IMAGE_TAG=${COMMIT_HASH:=latest}'
Di baris berikut kita akan membangun Docker image, melakukan tagging, dan mengirimnya ke ECR.
'docker build -t $REPOSITORY_URI:latest .',
'docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG',
'docker push $REPOSITORY_URI:latest',
'docker push $REPOSITORY_URI:$IMAGE_TAG',
Akhirnya, baris terakhir akan membuat sebuah file bernama imagedefinitions.json
di proyek. Kita sebut file ini sebagai Image Definitions File. File ini berisi nilai JSON yang mengandung nama container dan URI dari repositori. File ini akan menjadi artifak keluaran dari tahap Build
dan dilewatkan ke Deploy
.
'printf "[{\\"name\\":\\"${CONTAINER_NAME}\\",\\"imageUri\\":\\"${REPOSITORY_URI}:latest\\"}]" > imagedefinitions.json'
]
}
},
artifacts: {
files: [
'imagedefinitions.json'
]
}
Deploy Stage
Tahap terakhir, Deploy
akan menerima artifak imagedefinitions.json
dan menggunakannya untuk mendeploy ke ECS menggunakan ECSDeployAction
.
createDeployStage(stageName: string, input: codepipeline.Artifact): codepipeline.StageProps {
const ecsDeployAction = new codepipeline_actions.EcsDeployAction({
actionName: 'ECSDeploy_Action',
input: input,
service: this.service,
});
return {
stageName: stageName,
actions: [ecsDeployAction],
}
}
Main Application
Aplikasi utama CDK sekarang akan memiliki WebStack
yang hanya memanggil ketiga construct.
const cluster = new Cluster(this, 'Cluster');
const webapp = new WebApp(this, 'WebApp', {
cluster: cluster
});
const pipeline = new Pipeline(this, 'Pipeline', {
webapp: webapp
})
Catatan: Jika Anda masih bereksperimen, jangan lupa eksekusi cdk destroy
untuk menghindari biaya yang berlebihan.
Ini saja untuk sekarang. Ini sudah lumayan panjang. Saya harap Anda dapat menikmati penjelasannya. Sekali lagi, tulisan ini adalah seri Membangun Aplikasi PHP/Yii2 Modern dengan AWS. Jika Anda punya ide menarik atau usul, silakan tulis di bagian komentar. Jumpa lagi di tulisan berikutnya.
Belajar Lebih Lanjut
Untuk belajar lebih lanjut tentang CI/CD di AWS, bisa kunjungi.