[AWS CDK] CodePipelineのソース元を色々指定してみました(CodeCommitとか、Githubとか、S3 Bucketとか、BacklogのGitとか)

2019.08.14

1 はじめに

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

CodePipelineでは、各種のソースが利用可能です。 今回は、AWS CDK(AWS Cloud Development Kit)で、CodePipelineのソース元を色々変えて記述要領を確認してみました。

2 サンプル

最初に、本記事の基準となるサンプルコードです。

更新されたコードでLambdaを更新します。

#!/usr/bin/env node
import cdk = require('@aws-cdk/core');
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 codecommit from '@aws-cdk/aws-codecommit';
import * as iam from '@aws-cdk/aws-iam';

//**************************************************** */
// buildspec.yamの中から、functionNameに対してdeployされる想定
const stage = "dev"; // "stg","prd"
const functionName = stage + '-myFunction'
//**************************************************** */

export class CdkCodePipelineSampleStack extends cdk.Stack {
    constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
      super(scope, id, props);
   
        //**************************************************** */
        // 1. プロジェクトの生成
        //**************************************************** */
        const project = new codebuild.PipelineProject(this, 'project', {
            projectName: 'myProject-' + stage,
            environment: {
                // 環境変数(関数名及び、ステージ)をbuildspec.ymlに送るってデプロイする
                environmentVariables: {
                    FUNCTION_NAME: {
                        type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
                        value: functionName,
                    },
                    STAGE: {
                        type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
                        value: stage,
                    }
                },
            }
        });
        // buildspc.ymlからLambdaをupdateするため、パーミッションを追加
        project.addToRolePolicy(new iam.PolicyStatement({
            resources: [`arn:aws:lambda:${this.region}:${this.account}:function:${functionName}`],
            actions: ['lambda:UpdateFunctionCode',
                      'lambda:UpdateFunctionConfiguration',] }
        ));

        // パイプラインの生成
        const sourceOutput = new codepipeline.Artifact();
        //**************************************************** */
        // 2. ソースアクションの生成
        //**************************************************** */
        const repositoryName = 'CdkCodePipelineSample';
        const branch = 'develop'; // 'release','master';

        const repo = codecommit.Repository.fromRepositoryName(this, "repo", repositoryName) as codecommit.Repository;
        const sourceAction = new codepipeline_actions.CodeCommitSourceAction ({
            actionName: 'CodeCommit',
            repository: repo,
            branch: branch,
            output: sourceOutput,
        });

        //**************************************************** */
        // 3. ビルドアクションの生成
        //**************************************************** */
        const buildAction = new codepipeline_actions.CodeBuildAction({
            actionName: 'CodeBuild',
            project,
            input: sourceOutput,
            outputs: [new codepipeline.Artifact()]
        });

        //**************************************************** */
        // 4. パイプラインの生成
        //**************************************************** */
        new codepipeline.Pipeline(this, 'pipeline', {
            pipelineName: 'myPipeline-' + stage,
            stages: [
                {
                    stageName: 'Source',
                    actions: [
                        sourceAction
                    ],
                },
                {
                    stageName: 'Build',
                    actions: [
                        buildAction
                    ],
                }
            ]
        })
    }
}

const app = new cdk.App();
new CdkCodePipelineSampleStack(app, 'CdkCodePipelineSampleStack');

デプロイは、buildspec.ymlでaws lambda update-function-codeされています。

buildspec.yml

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:
      - cd src
      - 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

3 Code Commit

上記の基準サンプルは、ソース元が、Code Commitになっています。 codepipeline_actions.CodeCommitSourceAction()で、リポジトリ名とブランチ名を指定するだけで。

//**************************************************** */
// 2. ソースアクションの生成
//**************************************************** */
const repositoryName = 'myRepository';
const branch = 'develop'; // 'release','master';

const repo = codecommit.Repository.fromRepositoryName(this, "repo", repositoryName) as codecommit.Repository;
const sourceAction = new codepipeline_actions.CodeCommitSourceAction ({
    actionName: 'CodeCommit',
    repository: repo,
    branch: branch,
    output: sourceOutput,
});

4 Github

ソース元がGitHubになる場合は、codepipeline_actions.GitHubSourceAction()を使用します。

パラメータには、リポジトリ名、ブランチ名の他に、GiutHubのオーナー名及び、認証トークンが必要になります。

import * as secretsmanager from '@aws-cdk/aws-secretsmanager';

// ・・・略・・・

//**************************************************** */
// 2. ソースアクションの生成
//**************************************************** */
const repositoryName = 'CdkCodePipelineSample';
const owner = 'HIRAUCHI';
const oauthToken = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
const branch = 'develop'; // 'release','master';

const sourceAction = new codepipeline_actions.GitHubSourceAction ({
    actionName: 'Github',
    owner: owner,
    repo: repositoryName,
    branch: branch,
    oauthToken: cdk.SecretValue.plainText(oauthToken),
    trigger: codepipeline_actions.GitHubTrigger.POLL // 'WEBHOOK', 'NONE'
    output: sourceOutput,
});

なお、triggerでWebhookも指定可能です。

trigger: codepipeline_actions.GitHubTrigger.WEBHOOK

5 S3のバケット

codepipeline_actions.S3SourceAction()で、S3バケットをソース元に指定できます。バケットには、バージョニングの設定が必須です。

import * as s3 from '@aws-cdk/aws-s3';

// ・・・略・・・

//**************************************************** */
// 2. ソースアクションの生成
//**************************************************** */
const sourceBucket = new s3.Bucket(this, 'sourceBucket', {
    bucketName: 'source-bucket-' + this.account,
    versioned: true, // バージョニングが必須
});
const sourceAction = new codepipeline_actions.S3SourceAction({
  actionName: 'S3',
  bucket: sourceBucket,
  bucketKey: 'upload.zip',
  output: sourceOutput,
});

6 ECR(参考)

CodePipelineでは、ECRをソース元にすることも可能ですが、今回は、Lambdaのデプロイがサンプルとなっているため、参考にコード例 のみ紹介されて頂きます。

import * as ecr from '@aws-cdk/aws-ecr';

// ・・・略・・・

//**************************************************** */
// 2. ソースアクションの生成
//**************************************************** */
const repositoryName = 'CdkCodePipelineSample';

const sourceAction = new codepipeline_actions.EcrSourceAction ({
    actionName: 'ECR',
    repository: repositoryName,
    imageTag: imageTag,
    output: sourceOutput,
});

7 BacklogのGit

BacklogのGitでは、更新時にWebhookを仕掛けることができます。このWebhookでLambdaを起動し、対象のコードをcloneしてS3にアップロードします。S3にアップロードされた後は、上記の「S3のバケット」の仕組みをそのまま使用します。

(1) Webhook

最初に、BacklogのGitでWebフックを設定します。 指定するURLは、CDKのdeployで最後に表示される、API GatewayのEndpointをコピーします。

参考:Git Webフック

(2) Lambda

API Gatewayの後ろに配置するLambdaでは、BacklogのGitから送られてきた情報に基づいて、コードをgit cloneし、圧縮して、 S3バケットにアップロードしています。

珍しくPythonで書いているのですが、理由は以下のとおりです。

  • BacklogのGitは、httpsでダウンロードできない
  • GitコマンドをLambda上で軽易に使えるライブラリがpythonに用意されている(porcelain)
  • Lambda上でzip圧縮を行えるライブラリがpythonに用意されている(shutil)

JavaScriptでLambdaからgit cloneするのは、予想以上にハードルが高かったです・・・

import json
import urllib.parse
import os
import tempfile
import shutil
import boto3
from dulwich import porcelain

BUCKET_NAME = os.environ['BUCKET_NAME']
ZIP_FILE_NAME = os.environ['ZIP_FILE_NAME']
USER = os.environ['USER']
PASS = os.environ['PASS']
REPOSITORY = os.environ['REPOSITORY']
BLANCH = os.environ['BLANCH']

def lambda_handler(event, context):
    
    payloadStr = urllib.parse.unquote(event["body"][8:]) # event["body"]  payload="xxxxxx"
    
    payload = json.loads(payloadStr) 
    repository = payload["repository"]["name"]
    url = payload["repository"]["url"]
    branch = payload["ref"][11:] # refs/heads/master"

    print(f"repository:{repository} branch:{branch} uri:{url}")

    if repository == REPOSITORY and branch == BLANCH:
        # gitパスの生成
        site = urllib.parse.urlparse(url)
        userStr = urllib.parse.quote(USER)
        passStr = urllib.parse.quote(PASS)
        uri = site.scheme +"://" + userStr + ":" + passStr +"@" + site.netloc + site.path + ".git"
        
        # 作業ディレクトリの生成
        tmpDir  = tempfile.mkdtemp()
        
        # clone/zip/upload
        try:
            porcelain.clone(uri, tmpDir)
            print("git clone success")
            
            zipFileName = tmpDir+ '/' + os.path.splitext(ZIP_FILE_NAME)[0]
            shutil.make_archive(zipFileName, 'zip', tmpDir )
            print("zip success")
            
            s3 = boto3.client('s3')
            s3.upload_file(zipFileName + '.zip', BUCKET_NAME, ZIP_FILE_NAME)
            print("s3 upload success")
        except Exception as e:
            print("ERROR" +  e)
        
        # 後始末
        shutil.rmtree(tmpDir)

(3) AWS CDK

最後に、S3をソース元としたCodePipelineと、Webhook用のAPI GatewayとLambdaを追加したAWS CDKの全コードです。

#!/usr/bin/env node
import cdk = require('@aws-cdk/core');
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 codecommit from '@aws-cdk/aws-codecommit';
import * as lambda from '@aws-cdk/aws-lambda';
import * as iam from '@aws-cdk/aws-iam';
import * as s3 from '@aws-cdk/aws-s3';
import * as apigateway from '@aws-cdk/aws-apigateway';

//**************************************************** */
// buildspec.yamの中から、functionNameに対してdeployされる想定
const stage = "dev"; // "stg","prd"
const functionName = stage + '-myFunction'
//**************************************************** */

// BacklogのGit情報
const repositoryName = 'test-project';
const branch = 'develop';
const user = 'user@example.jp'; 
const pass = 'xxxxxx'; 

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

        const bucketName = 'source-bucket-' + this.account;
        const zipFileName = 'upload.zip';

        //**************************************************** */
        // プロジェクトの生成
        //**************************************************** */
        const project = new codebuild.PipelineProject(this, 'project', {
            projectName: 'myProject-' + stage,
            environment: {
                // 環境変数(関数名及び、ステージ)をbuildspec.ymlに送るってデプロイする
                environmentVariables: {
                    FUNCTION_NAME: {
                        type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
                        value: functionName,
                    },
                    STAGE: {
                        type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
                        value: stage,
                    }
                },
            }
        });
        // buildspc.ymlからLambdaをupdateするため、パーミッションを追加
        project.addToRolePolicy(new iam.PolicyStatement({
            resources: [`arn:aws:lambda:${this.region}:${this.account}:function:${functionName}`],
            actions: ['lambda:UpdateFunctionCode',
                      'lambda:UpdateFunctionConfiguration',] }
        ));

        // パイプラインの生成
        const sourceOutput = new codepipeline.Artifact();

        //**************************************************** */
        // ソースアクションの生成
        //**************************************************** */
        const sourceBucket = new s3.Bucket(this, 'sourceBucket', {
            bucketName: bucketName,
            versioned: true, // バージョニングが必須
        });
        const sourceAction = new codepipeline_actions.S3SourceAction({
            actionName: 'S3',
            bucket: sourceBucket,
            bucketKey: zipFileName,
            output: sourceOutput,
        });
        
        //**************************************************** */
        // ビルドアクションの生成
        //**************************************************** */
        const buildAction = new codepipeline_actions.CodeBuildAction({
            actionName: 'CodeBuild',
            project,
            input: sourceOutput,
            outputs: [new codepipeline.Artifact()]
        });

        //**************************************************** */
        // パイプラインの生成
        //**************************************************** */
        new codepipeline.Pipeline(this, 'pipeline', {
            pipelineName: 'myPipeline-' + stage,
            stages: [
                {
                    stageName: 'Source',
                    actions: [
                        sourceAction
                    ],
                },
                {
                    stageName: 'Build',
                    actions: [
                        buildAction
                    ],
                }
            ]
        })

        //**************************************************** */
        // Webhook用の Lambda
        //**************************************************** */
        const getFunction = new lambda.Function(this, "get-function",{
            functionName: this.stackName + "-getSourceFromBacklog",
            code: lambda.Code.asset("lambda"),
            handler:"lambda_function.lambda_handler",
            runtime: lambda.Runtime.PYTHON_3_7,
            timeout: cdk.Duration.seconds(120),
            environment: {
                "BUCKET_NAME": bucketName,
                "ZIP_FILE_NAME": zipFileName,
                "REPOSITORY": repositoryName,
                "BLANCH": branch,
                "USER": user, 
                "PASS": pass, 
            }
        })
        getFunction.addToRolePolicy(new iam.PolicyStatement({
            resources: [`arn:aws:s3:::${bucketName}/${zipFileName}`],
            actions: ['s3:putObject'] }
        ));

        //**************************************************** */
        // Webhook用の API Gateway
        //**************************************************** */
        const api = new apigateway.RestApi(this, "api");
        const lambdaIntegration = new apigateway.LambdaIntegration(getFunction);
        api.root.addMethod("POST",lambdaIntegration);
        
    }
}

const app = new cdk.App();
new CdkCodePipelineWithBacklogStack(app, 'CdkCodePipelineWithBacklogStack');

8 最後に

今回、CodePipelineのソース元を色々記述してみました。

Lambdaのデプロイが目的だとしても、他にもソース元が変化する可能性はあると思います。しかし、上記の例を参考にすれば、なんとかなりそうな予感がします。