♾️

CDK Pipelinesを用いたCI/CDパイプラインの作成

2023/03/30に公開

概要

以下の記事の続きです。

https://zenn.dev/hikapoppin/articles/559cf40a50af7e

CDK Pipelinesを使ってCI/CDを実施しました。やや分かりづらいCDK Pipelinesの処理フローをハンズオンと共に説明します。

背景

CDK WorkshopでCDKの学習していると、CDK Pipelinesを使用してCI/CDのパイプラインの作成まできるようだったので、試してみました。

CDK Pipelinesとは

CDK Pipelineとは、CDKのCodePipelineを利用したCI/CDのワークフローを構築してくれる、CDK Constructです。こちらのConstructを使用することで、CI/CD周りの設定もIaC化することが出来ます。

CDK Pipelinesを使用したスタックをデプロイすると、以下のような処理フローが作成されます。

  • Source
    こちらの処理では、GitHubやCodeCommitなどのGitリポジトリに変更がpushされた際に、CDKのコードを取得してくる処理を行います。

  • Build
    こちらの処理では、取得したCDKのコードから、CloudFormationのスタックテンプレートが正常に生成されるかどうかをテストします。

  • UpdatePipeline
    パイプラインの変更を検出して、パイプラインの更新を行います。自分で自分を更新するので、self-mutateという中二病的なかっこいい名前がついています。

  • Assets
    CloudFormationのスタックテンプレートやその他のアーティファクトを作成して、S3にアップロードします。

  • Deploy
    S3から取得したアーティファクトをAWS環境上にデプロイします。

パイプラインの作成

今回はパイプラインの中でスタックをデプロイするようにするので、App層に記述していたBackendStackおよびFrontendStackを削除します。その代わりに、SpotipyPipelineStackを追加します。

spotipy.ts
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { SpotipyPipelineStack } from "../lib/pipeline-stack";

const pjPrefix = 'Spotipy';

const app = new cdk.App();

// For pipeline deployment
new SpotipyPipelineStack(app, `${pjPrefix}PipelineStack`, {});

SpotipyPipelineStackは以下のように定義します。

piepline-stack.ts
import * as cdk from 'aws-cdk-lib'
import * as codecommit from 'aws-cdk-lib/aws-codecommit';
import { Construct } from 'constructs'
import { CodeBuildStep, CodePipeline, CodePipelineSource } from 'aws-cdk-lib/pipelines';
import { SpotipyPipelineStage } from '../lib/pipeline-stage'

export class SpotipyPipelineStack extends cdk.Stack {
    constructor(scope: Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        //Creates CodeCommit repository called 'SpoityRepo'
        const repo = new codecommit.Repository(this, 'SpotipyRepo', {
            repositoryName: "SpotipyRepo"
        });

        const pipeline = new CodePipeline(this, 'Pipeline', {
            pipelineName: 'SpotipyPipeline',
            synth: new CodeBuildStep('SynthStep', {
                input: CodePipelineSource.codeCommit(repo, 'master'),
                installCommands: [
                    'npm install -g aws-cdk',
                    'npm install -g npm',
                ],
                commands: [
                    'mkdir -p lambda_layer',
                    'pip install -r requirements.txt -t ./lambda_layer/python/lib/python3.9/site-packages',
                    'npm ci',
                    'npm run build',
                    'npx cdk synth'
                ]
                },
            ),
        });

        //Adds deploy stage 
        const deploy = new SpotipyPipelineStage(this, 'Deploy');
        const deployStage = pipeline.addStage(deploy);

        //Adds test stage
        deployStage.addPost(
            new CodeBuildStep('TestAPIGatewayEndpoint', {
                projectName: 'TestAPIGatewayEndpoint',
                envFromCfnOutputs: {
                    ENDPOINT_URL: deploy.APIEndpoint
                },
                commands: [
                    'curl -X POST --data-urlencode "track_num=1" "${ENDPOINT_URL}sync"'
		    'curl -X POST --data-urlencode "track_num=1" "${ENDPOINT_URL}async"'
                ]
            })
        )
    }
}

以下の部分でCodeCommitのレポジトリを定義します。レポジトリ名はSpoityRepoとします。

//Creates CodeCommit repository called 'SpoityRepo'
const repo = new codecommit.Repository(this, 'SpotipyRepo', {
    repositoryName: "SpotipyRepo"
});

CIの部分では、SpoityRepoのmasterブランチへのpushをトリガーに処理が開始するようにします。InstallCommandsを以下のようにすることで、npmでCDKのモジュールのインストールとnpmのアップデートを行います。npmのアップデートを行っているのは、CodeBuildが古いバージョンのnpmを使用していて、npm ciコマンドを実行した際にエラーが出るためです。

installCommands: [
    'npm install -g npm',
    'npm install -g aws-cdk',
],

commandsの部分では、以下の処理を行います。

  • lambda_layerディレクトリの作成
  • Pythonのプログラムに必要なライブラリのインストール
  • npmで必要なパッケージのインストール
  • CDKのコードのビルド
  • CloudFormationのテンプレートの生成
commands: [
    'mkdir -p lambda_layer',
    'pip install -r requirements.txt -t ./lambda_layer/python/lib/python3.9/site-packages',
    'npm ci',
    'npm run build',
    'npx cdk synth'
]

なお、Pythonのライブラリのインストールには、requirements.txtを用いて行います。以下のように必要なライブラリを記載しておくことで、pipで一括インストールが出来ます。

async-timeout==4.0.2
boto3==1.26.23
botocore==1.29.23
certifi==2022.9.24
charset-normalizer==2.1.1
idna==3.4
jmespath==1.0.1
pycodestyle==2.10.0
python-dateutil==2.8.2
redis==4.4.0
requests==2.28.1
s3transfer==0.6.0
six==1.16.0
spotipy==2.21.0
tomli==2.0.1
urllib3==1.26.13

CDで行う処理は、以下のようなクラスを用いて別ファイル内に定義します。app.node.tryGetContext()から必要な値をとってきたり、デプロイするスタックを指定する部分は一緒ですが、クラス変数にFrontendStackのAPIEndpointを代入します。APIEndpointにはスタックのデプロイ時にAPIエンドポイントのURIが文字列として代入されます。この変数は後のAPIエンドポイントをテストする処理で使用します。

CDの処理内容を定義したクラスを用意したら、以下のようにして処理内容をパイプラインに追加することが出来ます。

//Adds deploy stage 
const deploy = new SpotipyPipelineStage(this, 'Deploy');
const deployStage = pipeline.addStage(deploy);

最後に、APIGatewayのエンドポイントのテストを行う処理も追加します。

//Adds test stage
deployStage.addPost(
    new CodeBuildStep('TestAPIGatewayEndpoint', {
        projectName: 'TestAPIGatewayEndpoint',
        envFromCfnOutputs: {
            ENDPOINT_URL: deploy.APIEndpoint
        },
        commands: [
            'curl -X POST --data-urlencode "track_num=1" "${ENDPOINT_URL}sync"',
	    'curl -X POST --data-urlencode "track_num=1" "${ENDPOINT_URL}async"'
        ]
    })
)

ENDPOINT_URL: deploy.APIEndpointとすることで、環境変数ENDPOINT_URLにAPIエンドポイントを埋め込みます。curlコマンドではこちらの環境変数を参照して処理を行います。

パイプラインのデプロイ

上記の準備が出来たら、cdk deployコマンドでパイプラインをデプロイします。なお、このデプロイを行う必要があるのは最初の一回のみです。二回目以降は、パイプラインの変更をcommit&pushすればUpdatePipelineの部分で自動で更新されます。

スタックをデプロイすると、CodeCommitのレポジトリと以下のようなパイプラインが作成されます。


初回はリポジトリに何も入っていいので、Sourceの段階で処理が失敗します。
パイプラインを作成したら、以下のコマンドでローカルの変更内容をコミットしておきます。

git add .
git commit -m "hogehoge"

今回は、HTTPS Git認証を用いてCodeCommitに接続します。なお、この操作を行うには事前にIAMでHTTPS Gitの認証情報を発行しておく必要があります。
認証情報を発行したら、以下のコマンドでリモートリポジトリを追加します。URLにCodeCommitのユーザー名とパスワードを埋め込むのがミソです。

git remote add origin https://"ユーザー名":"パスワード"@git-codecommit.us-east-1.amazonaws.com/v1/repos/SpotipyRepo

リモートリポジトリを追加したら、以下のコマンドでpushします。pushすると、パイプラインの処理が開始します。

git push --set-upstream origin master

今度は最後の処理まで成功しました!

Buildのフェーズはコードで定義した通りなので、それ以降の処理で何が行われてるか見てみましょう。UpdatePipelineのBuildspecを確認してみます。

{
  "version": "0.2",
  "phases": {
    "install": {
      "commands": [
        "npm install -g aws-cdk@2"
      ]
    },
    "build": {
      "commands": [
        "cdk -a . deploy SpotipyPipelineStack --require-approval=never --verbose"
      ]
    }
  }
}

いろいろオプションがついていますが、要するにパイプラインのスタックをcdk deployでデプロイしています。

AssetsのBuildspecも確認してみます。FileAsset1を見てみます。

{
  "version": "0.2",
  "phases": {
    "install": {
      "commands": [
        "npm install -g cdk-assets@2"
      ]
    },
    "build": {
      "commands": [
        "cdk-assets --path \"assembly-SpotipyPipelineStack-Deploy/SpotipyPipelineStackDeploySpotipyBackendStack16DCEC9E.assets.json\" --verbose publish \"796bd4da63bf70b119d38a693031420393e51d39067b4120bf236a007133de49:current_account-current_region\""
      ]
    }
  }
}

cdk-assetsというコマンドが出てきました。これは何かというと、CDKのアセットをAWS上にデプロイするためのコマンドです。

アセットとは、CDKのライブラリやアプリケーションにバンドルできるローカルファイルやディレクトリ、Dockerイメージのことです。今回の例でいうとLambda関数を構成するPythonのファイルやS3に格納するhtmlファイルが該当します。要するにデプロイに必要なアーティファクトのことですね。CDKはアセットを参照するようなアプリをデプロイする場合、最初にアセットを準備した上でS3またはECRにアップロードして、その後にスタックをデプロイします。CDKでは、公開されたアセットの場所をCloudFormationのパラメータとして関連するスタックに指定して、その情報を元にCDKのアプリ内でアセットの場所を参照出来るようにしています。

以上のようにAssetsの処理でアセットを準備できたら、後続の処理でCloudFormationがテンプレートのデプロイをしてくれます。なお、アセットはcdk.outフォルダに格納されます。

処理フローの図解

処理の流れがわかりづらいので、図で説明してみます。

まず、ローカルからパイプラインのスタックをデプロイすると、AWS環境上に以下のようにパイプラインが作成されます。

ソースコードの変更をpushすると、処理が開始します。

Buildでは、cdk synthでCloudFormationのテンプレートの生成をテストします。

Update Pipelineでは、パイプラインの変更を検出して、パイプラインのスタックのデプロイを行います。デプロイ後は、また最初から処理を行います。パイプラインに変更がない場合は何もしません。

Assetsでは、アセットとCloudFormationのテンプレートをS3にアップロードします。

最後にDeployで、CloudFormationがアセットとテンプレートをもとに、AWS上にリソースをデプロイします。

以上がCDK Pipelinesの処理フローです。

感想

パイプラインもコードで記述のは便利ですが、最初はself-mutateやアセットの処理部分で何をしているかが分かりづらかったです。CDK Pipelinesに限らずこういうお手軽に実装できるものほど、エラーが出た時の対応や細かい処理内容を追いづらいと感じている今日この頃です。

Discussion