CodePipelineでアクション間での変数受け渡しをAWS CDKで構築する

こんにちは、小倉です。インフラ寄りの仕事が多いアプリケーションエンジニアです。

アプリのデプロイにはAWS CDKでCodePipelineを作成しています。

CodePipelineでは、ビルドIDなどを次のアクションで使いたい場合のために、アクション間で変数の受け渡しをすることができます。 また、AWS CDKを作成するときに間違えやすいところもあったため、そちらもご共有しようと思います。

なお、AWSコンソール上から変数を受け渡しする方法は、以前こちらでご紹介しています。 tech-blog.yayoi-kk.co.jp

今回の作成するCodePipeline

今回はDockerビルドするだけの単純なデプロイPipelineを作成していきます。 デプロイ元のソースにはS3バケットを使用し、バケット内のファイルが更新された時、ビルド処理が実行されるようにします。 Dockerビルドのタグには一意の文字列を設定し、後続のアクションに受け渡しします。 image.png

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構築する方はお気をつけください。

一緒に働く仲間を募集しています

herp.careers