この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
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をコピーします。
(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のデプロイが目的だとしても、他にもソース元が変化する可能性はあると思います。しかし、上記の例を参考にすれば、なんとかなりそうな予感がします。