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.

CI/CD Pipeline

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.

Github Personal 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.

Parameter Store

Sama halnya kalau Anda mengakses konsol Secrets Manager, Anda dapat melihat akses rahasianya sudah tersimpan.

Secrets Manager

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.

New Pipeline

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.

Class Diagram

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.

Leave a comment

Leave a Reply

%d bloggers like this: