ちょっと話題の記事

AWS CDK(Cloud Development Kit )で、CodePipeline、CodeCommit、CodeBuildを使用した開発環境を作ってみました

2019.07.15

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

1 はじめに

CX事業本部の平内(SIN)です。

今回は、AWS CDKで、CodePipelineCodeCommitCodeBuildを使用した開発環境を作ってみました。デプロイされるのは、Lambdaファンクションのみです。

CodeCommitのリポジトリで、developブランチをコミットすると、dev環境のLambdaが更新され、masterブランチでprd環境が更新されるようになってます。

2 リポジトリ作成

最初に、CodeCommitでリポジトリを作成します。

スタックの中で作成することも可能ですが、その場合、スタックの削除でリポジトリも消えてしまうので、ちょっと運用上まずいかと思います。

$ aws codecommit create-repository --repository-name SampleRepo --repository-description "My sample repository"

3 AWS CDK

AWS CDKによる作業手順は、以下のとおりです。

(1) プロジェクト作成

作業用ディレクトリを作成して、その中で、initコマンドを実行します。

$ mkdir my-cicd;cd my-cicd
$ cdk init app --language=typescript

(2) モジュールインストール

使用するモジュールをインストールします。

$ npm install @aws-cdk/aws-lambda
$ npm install @aws-cdk/aws-codepipeline
$ npm install @aws-cdk/aws-codepipeline-actions
$ npm install @aws-cdk/aws-codebuild
$ npm install @aws-cdk/aws-iam
$ npm install @aws-cdk/aws-codecommit

(3) DMYプロジェクト

これは、スタックの中でLambda関数を新しく生成するのですが、そのCodeアセットのためのダミーです。(AWS CDKには、直接関係ないです)

$ mkdir dmy
$ echo "SAMPLE" > dmy/README.md

(4) コード作成

スタックのコードは、lib/my-cicd-stack.tsを編集します。(細部は、後述)

(5) ビルド・デプロイ

tscでビルドします。

$ npm run build

エラーがあれば、synthコマンドでもある程度のエラーは確認できます。

$ cdk synth

deployコマンドでデプロイします。

$ cdk deploy

4 コード

スタック作成のためのコードは、以下のとおりです。作業単位をメソッドとして定義できるので、非常に見通しが良いように思います。

devprdは、ステージ変数(stage)にして、forEachで回しています。

createId()は、スタック間でも一意である必要のあるID生成で、tagをその識別子としています。一方、cerateName()は、リソースの名前生成で、AWSコンソール上での視認性を高めるため、自動生成(スタック名+XXX)では無く、tag + stageで積極的に指定してみました。

lib/my-cicd-stack.ts

// 対象Lambda
// (develop => targetFunctionName_dev)
// (master => targetFunctionName_prd)
const targetFunctionName = "sample-function";
// リポジトリ名 repositoryName
const repositoryName = "SampleRepo"
// 識別するためのタグ
const tag = "my-cicd"; 

import cdk = require('@aws-cdk/core');
import * as lambda  from '@aws-cdk/aws-lambda';
import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions';
import * as codebuild from '@aws-cdk/aws-codebuild';
import * as iam from '@aws-cdk/aws-iam';
import * as codecommit from '@aws-cdk/aws-codecommit';


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

    // リポジトリの取得
    const repo = this.getRepository(repositoryName);

    // 2つのステージ(prd/dev)分のリソースを生成する
    ["prd","dev"].forEach( stage => {
      // デプロイ先のLambdaを仮に作成する
      // buildspec.ymlからのデプロイは、updateのみ許可する
      const targetFunction = this.createFunction(stage);

      // プロジェクトの生成
      const project = this.createProject(targetFunction, stage);

      // パイプラインの生成
      const sourceOutput = new codepipeline.Artifact();
      // 対象ブランチ(prd:master dev:developとなる)
      const branch = (stage=='dev')?'develop':'master';
      new codepipeline.Pipeline(this, this.createId('Pipeline',stage), {
        pipelineName: this.createName(stage),
        stages: [{
            stageName: 'Source',
            actions: [
              this.createSourceAction(repo, branch, sourceOutput)
            ],
          },
          {
            stageName: 'Build',
            actions: [
              this.createBuildAction(project, sourceOutput)
            ],
          },
        ],
      });
    })
  }

  //**************************************************** */
  // idの生成(tag + name + stage)
  //**************************************************** */
  private createId(name:string, stage: string): string {
    return tag + '-' + name + '-' + stage
  }

  //**************************************************** */
  // 名前の生成(tag + stage)
  //**************************************************** */
  private createName(stage: string): string {
    return tag + '_' + stage
  }

  //**************************************************** */
  // Lambdaファンクションの生成
  //**************************************************** */
  private createFunction(stage: string):lambda.Function {
    return new lambda.Function(this, this.createId('Target', stage), {
      functionName: targetFunctionName + '_' + stage,
      code: lambda.Code.asset('dmy'), // コードは仮のもの
      handler: 'index.handler',
      runtime: lambda.Runtime.NODEJS_10_X,
    });
  }

  //**************************************************** */
  // プロジェクトの生成
  //**************************************************** */
  private createProject(targetFunction:lambda.Function, stage: string):codebuild.PipelineProject {
    const project = new codebuild.PipelineProject(this, this.createId('Project',stage), {
      projectName: this.createName(stage),
      environment: {
        // 環境変数(関数名及び、ステージ)をbuildspec.ymlに送る
        environmentVariables: {
          FUNCTION_NAME: {
            type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
            value: targetFunction.functionName,
          },
          STAGE: {
            type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
            value: stage,
          }
        },
      },
    });
    // buildspc.ymlからLambdaをupdateするため、パーミッションを追加
    project.addToRolePolicy(new iam.PolicyStatement({
      resources: [targetFunction.functionArn],
      actions: ['lambda:UpdateFunctionCode',
                'lambda:UpdateFunctionConfiguration',] }
    ));
    return project;
  }

  //**************************************************** */
  // リポジトリの取得
  //**************************************************** */
  private getRepository(repositoryName:string):codecommit.Repository {

    return codecommit.Repository.fromRepositoryName(this, this.createId("repo",""), repositoryName) as codecommit.Repository;

    // 新規にリポジトリを作成する場合(注:スタックの削除でリポジトリも削除される)
    // return new codecommit.Repository(this, this.createId('Repository',"") ,{
    //   repositoryName: repositoryName,
    // });
  }

  //**************************************************** */
  // CodePipelineのソースアクション(CodeCommit)の生成
  //**************************************************** */
  private createSourceAction(repo:codecommit.Repository, branch: string, sourceOutput: codepipeline.Artifact): codepipeline_actions.CodeCommitSourceAction {
    return new codepipeline_actions.CodeCommitSourceAction ({
      actionName: 'CodeCommit',
      repository: repo,
      branch: branch, 
      output: sourceOutput,
    });
  }

  //**************************************************** */
  // CodePipelineのビルドアクション(CodeBuild) の生成
  //**************************************************** */
  private createBuildAction(project: codebuild.IProject, sourceOutput: codepipeline.Artifact) {
    return new codepipeline_actions.CodeBuildAction({
      actionName: 'CodeBuild',
      project,
      input: sourceOutput,
      outputs: [new codepipeline.Artifact()]
    });
  }
}

5 リソース

作成されているリソースを少し確認しておきます。

CodePipeline

CodeBuild

Lambda

ビルド実行の様子です。

6 buildspec.yml

こちらは、コミットされるコードのトップに置かれたbuildspec.ymlです。CodeBuildで実行され、LambdaをUpdateしています。

完全に個人的な趣味ですが、ここに、Lambdaの環境変数などのパラメータを集約することで、コード側で管理できるようにしています。

version: 0.2
env:
  variables:
    DESCRIPTION: sample function
    RUN_TIME: nodejs10.x
    MEMORY: 128
    TIMEOUT: 5
    HANDLER: index.handler
    ENV: TZ=Asia/Tokyo,NODE_ENV=   # NODE_ENV=$STAGE
    ZIP_FILE: /tmp/upload.zip
phases:
  pre_build:
    commands:
      - yarn global add typescript
      - yarn install
      -
  build:
    commands:
      - tsc
      - npm test
      - zip -r -q ${ZIP_FILE} * 
      - aws lambda update-function-code --function-name $FUNCTION_NAME --zip-file fileb://$ZIP_FILE --publish
      - aws lambda update-function-configuration --function-name $FUNCTION_NAME --environment Variables={$ENV$STAGE} --memory-size $MEMORY --runtime $RUN_TIME --description "$DESCRIPTION" --timeout $TIMEOUT --handler $HANDLER
  post_build:
    commands:
      - echo Deploy completed

7 最後に

すっかり、AWS CDKが気に入ってしまいました。恥ずかしながら、もともと、CloudFormationのテンプレートが得意なわけでは決して無いので、AWS CDKの生成するテンプレートはどうなの?と言われると正直返事に詰まります。

とりあえず、ベストプラクティスによる自動生成に頼りながら、順次、勉強したいと思います。