こんにちは、小倉です。インフラ寄りの仕事が多いアプリケーションエンジニアです。
アプリのデプロイにはAWS CDKでCodePipelineを作成しています。
CodePipelineでは、ビルドIDなどを次のアクションで使いたい場合のために、アクション間で変数の受け渡しをすることができます。 また、AWS CDKを作成するときに間違えやすいところもあったため、そちらもご共有しようと思います。
なお、AWSコンソール上から変数を受け渡しする方法は、以前こちらでご紹介しています。 tech-blog.yayoi-kk.co.jp
今回の作成するCodePipeline
今回はDockerビルドするだけの単純なデプロイPipelineを作成していきます。 デプロイ元のソースにはS3バケットを使用し、バケット内のファイルが更新された時、ビルド処理が実行されるようにします。 Dockerビルドのタグには一意の文字列を設定し、後続のアクションに受け渡しします。
Pipelineを作成するCDKコード
前述のPipeline Stackを作成するCDKコードは以下になります。 TypeScriptで作成しています。
// lib/cdk_pipeline-stack.ts import { Stack, StackProps, RemovalPolicy } from "aws-cdk-lib"; import { Construct } from "constructs"; import * as s3 from "aws-cdk-lib/aws-s3"; import * as logs from "aws-cdk-lib/aws-logs"; import * as codebuild from "aws-cdk-lib/aws-codebuild"; import * as codepipeline from "aws-cdk-lib/aws-codepipeline"; import * as codepipeline_actions from "aws-cdk-lib/aws-codepipeline-actions"; export class CdkPipelineStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); const pipeline = new codepipeline.Pipeline(this, "MyFirstPipeline", { pipelineName: "environmentPipeline", crossAccountKeys: false, }); // Source const sourceBucket = new s3.Bucket(this, "sourceBucket", { bucketName: "(bucket name)", versioned: true, removalPolicy: RemovalPolicy.DESTROY, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, }); const sourceOutput = new codepipeline.Artifact(); const sourceAction = new codepipeline_actions.S3SourceAction({ actionName: "S3Source", bucket: sourceBucket, bucketKey: "s3Source.zip", output: sourceOutput, }); pipeline.addStage({ stageName: "Source", actions: [sourceAction], }); // Build const buildProject = new codebuild.PipelineProject(this, `buildProject`, { projectName: "buildProject", buildSpec: codebuild.BuildSpec.fromSourceFilename( "./build/buildspec.yml" ), logging: { cloudWatch: { logGroup: new logs.LogGroup(this, `buildProjectLogs`), }, }, environment: { buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_3, privileged: true, }, }); const buildAction = new codepipeline_actions.CodeBuildAction({ actionName: "buildAction", project: buildProject, input: sourceOutput, }); pipeline.addStage({ stageName: "Build", actions: [buildAction], }); // Deploy const deployProject = new codebuild.PipelineProject(this, `deployProject`, { projectName: "deployProject", buildSpec: codebuild.BuildSpec.fromSourceFilename( "./deploy/buildspec.yml" ), logging: { cloudWatch: { logGroup: new logs.LogGroup(this, `deployProjectLogs`), }, }, environment: { buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_3, privileged: true, }, }); const deployAction = new codepipeline_actions.CodeBuildAction({ actionName: "deployAction", project: deployProject, input: sourceOutput, environmentVariables: { EXPORTED_UUID: { value: buildAction.variable("EXPORTED_UUID"), }, }, }); pipeline.addStage({ stageName: "Deploy", actions: [deployAction], }); } }
Buildspec
Buildで実行されるBuildspec
# /build/buildspec.yml version: 0.2 env: exported-variables: - EXPORTED_UUID phases: pre_build: commands: - export EXPORTED_UUID=$(uuidgen) build: commands: - docker build -t test-image:${EXPORTED_UUID} -f ./Dockerfile .
Deployで実行されるBuildspec
# /deploy/buildspec.yml version: 0.2 phases: build: commands: - echo ${EXPORTED_UUID}
変数の受け渡し方法
CDKで変数を受け渡すポイントとしては、以下になります。
- 渡す側はBuildspecのexported-variablesに渡したい変数を指定する
- 受ける側はCDKのアクション定義で渡す側の変数定義を指定する
この例ではビルド時にUUIDでDockerイメージのタグ値を生成し、デプロイ時にタグ値を変数で受け取ってデプロイ対象イメージを指定しようとしています。
渡す側
exported-variables に EXPORTED_UUID
を指定します。
# /build/buildspec.yml version: 0.2 env: + exported-variables: + - EXPORTED_UUID phases: pre_build: commands: - export EXPORTED_UUID=$(uuidgen) build: commands: - docker build -t test-image:${EXPORTED_UUID} -f ./Dockerfile .
受ける側
受け渡したいアクションの CodeBuildAction に、buildAction.variable("変数名")
で設定します。
const buildAction = new codepipeline_actions.CodeBuildAction({ actionName: "buildAction", project: buildProject, input: sourceOutput, }); ~~ 省略 ~~ const deployAction = new codepipeline_actions.CodeBuildAction({ actionName: "deployAction", project: deployProject, input: sourceOutput, environmentVariables: { + EXPORTED_UUID: { + value: buildAction.variable("EXPORTED_UUID"), + }, }, });
CDKデプロイ
CDKコードをデプロイ後に、AWSコンソール上から確認してみます。
渡す側のアクションには変数の名前空間がセットされています。
受ける側のアクションには環境変数がセットされています。
また、Pipelineを実行したときのログを見てみると、UUIDが受け渡しできていることがわかります。
...
[Container] 2022/07/19 09:15:06 Phase context status code: Message:
[Container] 2022/07/19 09:15:06 Entering phase BUILD
[Container] 2022/07/19 09:15:06 Running command echo ${EXPORTED_UUID}
+ 576854ed-8750-4cc8-bc6b-6da942c9c92b
...
NGの例
環境変数は PipelineProject の environmentVariables
にも設定できますが、こちらでは環境変数を受け渡すことはできません。
// Deploy const deployProject = new codebuild.PipelineProject(this, `deployProject`, { projectName: "deployProject", buildSpec: codebuild.BuildSpec.fromSourceFilename( "./deploy/buildspec.yml" ), logging: { cloudWatch: { logGroup: new logs.LogGroup(this, `deployProjectLogs`), }, }, environment: { buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_3, privileged: true, + environmentVariables: { + EXPORTED_UUID: { + value: buildAction.variable("EXPORTED_UUID"), + }, + }, }, }); const deployAction = new codepipeline_actions.CodeBuildAction({ actionName: "deployAction", project: deployProject, input: sourceOutput, });
このCDKコードをデプロイすると、「変数の名前空間」は自動で設定され、
CodeBuildの環境変数にそれっぽく設定されてしまいます。
しかし、実行してたときのログは、以下のようにただの文字列としか認識していません。
...
[Container] 2022/07/19 09:08:03 Phase context status code: Message:
[Container] 2022/07/19 09:08:03 Entering phase BUILD
[Container] 2022/07/19 09:08:03 Running command echo ${EXPORTED_UUID}
+ #{Build_buildAction_NS.EXPORTED_UUID}
...
最後に
CDKで変数を受け渡すとき、注意ポイントをまとめると
- 渡す側はBuildspecファイルに書く
- 受ける側はCDKのCodeBuildActionの環境変数に書く
- 受ける側を誤ってCDKのPipelineProjectの環境変数に書くと変数展開されず期待通り動かない
私はNGの例のように、PipelineProject の environmentVariables
に設定していることになかなか気づけませんでした...
(そもそも、CodePipeline自体に環境変数設定できることさえ知りませんでした...)
これからAWS CDK で CodePipeline構築する方はお気をつけください。