AWS CDKで作る CI/CDパイプライン
はじめに
今回、静的サイトをホスティングするための環境を構築することになりました。
環境構築にあたって、以下の記事を参考にCDKを用いてCI/CDを構築しました。
今回構築した構成
上記の記事に合ったCodeシリーズを用いたCI/CDに加え、静的サイトをホスティングするためのS3、CDNとしてCloudFront、ベーシック認証のためにCloudFront Functionを用いています。
これらのサービスを以下の図のような構成で作成しました。
dev、stg、prdの3環境分作成します。
一連の挙動としては以下の流れとなります。
- クライアントからCodeCommitのリモートリポジトリへPush、または各ブランチへマージする
- Push・マージ契機でCodePipelineが走り、CodeBuildが実行される
- CodeBuild実行後、CodeCommitの内容がCodeDeployによりS3へデプロイされる
- S3へデプロイされたリソースは、CloudFront経由で配信される
- 閲覧する際には予め、CloudFrontFuntionにて定められたベーシック認証が行われる
実際に作っていく
AWS CDKのインストール
AWS CDKコマンドをインストールします。
下記のコマンドでインストールして下さい。
npm install -g aws-cdk
プロジェクト作成
まずAWS CDKのプロジェクトを作成します。
本記事では、CodeCommitのリポジトリもCDKで作成するためCDKのコード自体を管理する場合は、別途リポジトリをご用意下さい。
任意のディレクトリを作成し、そのディレクトリ配下で下記コマンドを実行します。
cdk init app --language=typescript
appには任意のPJ名を入力してください。
今回はtypescriptを利用しますが、languageオプションで他言語を選択することも可能です。
上記、コマンドを実行して初期化後には下記のようなフォルダ構成ができています。
app
├ .git
├ .npmignore
└ bin
├ app.ts
├ cdk.json
├ jest.config.js
├ lib
└ app-stack.ts
├ node_modules
└ ...
├ package-lock.json
├ package.json
├ README.md
├ test
└ app.test.ts
├ tsconfig.json
AWSアカウント内で初めてCDKを利用する場合は、ブートストラップコマンドも実行しておいてください。
cdk bootstrap
各種モジュールをインストール
下記、コマンドを実行し使用する各モジュールをインストールします。
npm install @aws-cdk/aws-codecommit
npm install @aws-cdk/aws-codebuild
npm install @aws-cdk/aws-codepipeline
npm install @aws-cdk/aws-iam
npm install @aws-cdk/aws-s3
npm install @aws-cdk/aws-cloudfront
上記、コマンドを実行しても、package.json
に追記した後にnpm install
してもどちらでも大丈夫です。
* 上記の各モジュールのバージョンが揃っていないと後続の実際にコードを書く際に、エラーが出る場合があるので、エラーが出た際はバージョンを確認してみてください。
また、複数スタックを逐次デプロイするために下記のnpm-run-all
もインストールします。
npm install --save-dev npm-run-all
テンプレートの作成
ここからは実際に各サービスについて、コードを書いて定義していきます。
実際に定義するファイルは スタック毎にlib配下にtsファイルを作成していきます。
まずは、ClouFrontとS3の定義をしていきます。
lib/s3-stack.ts
import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';
import * as iam from '@aws-cdk/aws-iam';
import { StringParameter } from '@aws-cdk/aws-ssm';
import * as cloudfront from '@aws-cdk/aws-cloudfront';
export class s3Stack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
console.log("*****************S3Stack START*****************")
// プロジェクト名をcontextから取得
const projectName = this.node.tryGetContext('projectName');
console.log('ProjectName:' + projectName);
// 3環境分のS3バケット、CloudFrontを生成
["prd", "stg", "dev"].forEach(stage => {
// バケット名を設定
const bucketName= stage + '-' + projectName
// バケットを生成
const s3Bucket = new s3.Bucket(this, stage + '-pjBucket', {
bucketName: bucketName,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});
// OAIを設定
const oai = new cloudfront.OriginAccessIdentity(this, bucketName)
// バケットポリシーを生成
const bucketPolicy = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["s3:GetObject"],
principals: [
new iam.CanonicalUserPrincipal(
oai.cloudFrontOriginAccessIdentityS3CanonicalUserId
),
],
resources: [s3Bucket.bucketArn + "/*"],
})
s3Bucket.addToResourcePolicy(bucketPolicy)
// CloudFrontFunctionsの定義
if (stage == 'dev') {
const basicAuthFunction = new cloudfront.Function(
this,
stage + '-BasicAuthFunction',
{
functionName: bucketName + '-BasicAuth',
code: cloudfront.FunctionCode.fromFile({
filePath: "lambda/BasicAuth/dev-auth.js",
}),
}
);
this.createCloudFront(stage, s3Bucket, oai, basicAuthFunction)
} else if(stage == 'stg') {
const basicAuthFunction = new cloudfront.Function(
this,
stage + '-BasicAuthFunction',
{
functionName: bucketName + '-BasicAuth',
code: cloudfront.FunctionCode.fromFile({
filePath: "lambda/BasicAuth/stg-auth.js",
}),
}
);
this.createCloudFront(stage, s3Bucket, oai, basicAuthFunction)
} else {
const basicAuthFunction = new cloudfront.Function(
this,
stage + '-BasicAuthFunction',
{
functionName: bucketName + '-BasicAuth',
code: cloudfront.FunctionCode.fromFile({
filePath: "lambda/BasicAuth/prd-auth.js",
}),
}
);
this.createCloudFront(stage, s3Bucket, oai, basicAuthFunction)
}
// パラメータストアへS3BucketArnを登録
new StringParameter(this, stage + '-bucketArn', {
parameterName: bucketName + '-bucketArn',
stringValue: s3Bucket.bucketArn,
});
})
console.log("*****************S3Stack END*****************")
}
//****************************************************/
// CLoudFrontディストリビューションの作成
//****************************************************/
private createCloudFront(stage: string, s3Bucket: s3.Bucket, oai: cloudfront.OriginAccessIdentity, basicAuthFunction: cloudfront.Function) {
new cloudfront.CloudFrontWebDistribution(this, stage + '-Distribution', {
viewerCertificate: {
aliases: [],
props: {
cloudFrontDefaultCertificate: true,
},
},
priceClass: cloudfront.PriceClass.PRICE_CLASS_200,
originConfigs: [
{
s3OriginSource: {
s3BucketSource: s3Bucket,
originAccessIdentity: oai,
},
behaviors: [
{
isDefaultBehavior: true,
functionAssociations: [
{
eventType: cloudfront.FunctionEventType.VIEWER_REQUEST,
function: basicAuthFunction
},
],
minTtl: cdk.Duration.seconds(0),
maxTtl: cdk.Duration.days(365),
defaultTtl: cdk.Duration.days(1),
pathPattern: "*",
},
],
},
],
errorConfigurations: [
{
errorCode: 403,
responsePagePath: "/error_403.html",
responseCode: 200,
errorCachingMinTtl: 0,
},
{
errorCode: 404,
responsePagePath: "/error_404.html",
responseCode: 200,
errorCachingMinTtl: 0,
},
],
});
}
}
続いて、CI/CD部分の定義をapp-stack.tsに記載していきます。
lib/app-stack.ts
import * as cdk from '@aws-cdk/core';
import * as codecommit from '@aws-cdk/aws-codecommit';
import * as codebuild from '@aws-cdk/aws-codebuild';
import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as codePipeline_actions from '@aws-cdk/aws-codepipeline-actions';
import { StringParameter } from '@aws-cdk/aws-ssm';
import { Bucket, IBucket } from '@aws-cdk/aws-s3';
export class CiCdStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
console.log("*****************CI/CDStack START*****************")
// プロジェクト名をcontextから取得
const projectName = this.node.tryGetContext('projectName');
console.log('ProjectName:' + projectName);
// タグを作成
const tag: string = projectName;
// PJ名でリポジトリを作成する
const repoName = projectName
const repo = new codecommit.Repository(this, 'pjRepo', {
repositoryName: repoName,
description: 'repository'
});
["prd", "stg", "dev"].forEach(stage => {
// パラメータストアからS3のarnを取得する
const bucketArn = StringParameter.valueForStringParameter(this, stage + '-' + projectName + '-bucketArn');
console.log("bucketArn:" + bucketArn);
const targetBucket = Bucket.fromBucketArn(this, stage + 'BucketByArn', bucketArn);
console.log("targetBucket:" + targetBucket);
// プロジェクトを作成
const project = this.createProject(stage, targetBucket, tag)
// パイプラインを作成
const sourceOutput = new codepipeline.Artifact();
// 対象ブランチ(prd:main, dev:develop)
let branch;
if (stage == 'dev') {
branch = 'develop';
} else if (stage == 'stg') {
branch = 'staging';
} else {
branch = 'main'
}
new codepipeline.Pipeline(this, this.createId('Pipline', stage, tag), {
pipelineName: this.createName(stage, tag),
stages: [{
stageName: 'Source',
actions: [
this.createSourceAction(repo, branch, sourceOutput)
],
},
{
stageName: 'Build',
actions: [
this.createBuildAction(project, sourceOutput)
]
},
{
stageName: 'Deploy',
actions: [
this.createDeployAction(targetBucket, sourceOutput)
]
}
]
})
})
console.log("*****************CI/CDStack END*****************")
}
//**************************************************** */
// idの生成(tag + name + stage)
//**************************************************** */
private createId(name: string, stage: string, tag: string): string {
return tag + '-' + name + '-' + stage;
}
//**************************************************** */
// 名前の生成(stage + tag)
//**************************************************** */
private createName(stage: string, tag: string): string {
return stage + '-' + tag
}
//**************************************************** */
// プロジェクトの生成
//**************************************************** */
private createProject(stage: string, s3BucketName: IBucket, tag: string): codebuild.PipelineProject {
const project = new codebuild.PipelineProject(this, this.createId('Project', stage, tag), {
projectName: this.createName(stage, tag),
buildSpec: codebuild.BuildSpec.fromObject({
version: '0.2',
// ビルドプロジェクトで実行するコマンドを定義
phases: {
build: {
commands: [
'echo "*******Start Build*******"',
// 'npm build',
]
}
}
})
})
return project
}
//**************************************************** */
// 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: project,
input: sourceOutput,
outputs: [new codepipeline.Artifact()],
});
}
//**************************************************** */
// CodePipelineのデプロイアクションの生成
//**************************************************** */
private createDeployAction(targetBucket: IBucket, sourceOutput: codepipeline.Artifact) {
return new codePipeline_actions.S3DeployAction({
actionName: 'CodeDeploy',
bucket: targetBucket,
input: sourceOutput
});
}
}
s3-stack
で作成したS3バケットのarnをパラメータストアに登録し、app-stack
でパラメータストアから取得し、デプロイ先に設定しています。
続いてlib
配下で定義したスタックを実行ファイルに記載します。
bin/app.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { AppStack } from '../lib/app-stack';
import {s3Stack} from '../lib/s3-stack';
const app = new cdk.App();
new AppStack(app, 'AppStack');
new s3Stack(app, 's3Stack');
ベーシック認証コード作成
ベーシック認証を行うためのCloudFunctionのコードを作成していきます。
CloudFunctionのコードはs3-stack
内で定義した場所に配置して下さい。
今回の場合は、app/lambda/BasicAuth/
配下に環境毎にコードを用意します。
環境 | ファイル名 |
---|---|
dev | dev-auth.js |
stg | stg-auth.js |
prd | prd-auth.js |
今回はいずれの環境もベーシック認証の内容は同じものとし、いずれのユーザー名とパスワードを以下のようにします。
- ユーザー名:user
- パスワード:pass
ユーザー名とパスワードはbase64エンコーディングした文字列を記載します。
lambda/dev-auth.js
lambda/stg-auth.js
lambda/prd-auth.js
function handler(event) {
var request = event.request;
var headers = request.headers;
// echo -n user:pass | base64
var authString = "Basic dXNlcjpwYXNz";
if (
typeof headers.authorization === "undefined" ||
headers.authorization.value !== authString
) {
return {
statusCode: 401,
statusDescription: "Unauthorized",
headers: { "www-authenticate": { value: "Basic" } }
};
}
return request;
}
npmスクリプトの作成
スタックのビルドとデプロイを行うためのスクリプトを記載していきます。
今回の場合はS3・CloudFrontと、CI/CDの2スタックを順番にデプロイする必要があります。
(CI/CDスタックでデプロイ先のS3バケットのArnを取得しているため)
そこで、モジュールインストール時に追加したnpm-run-all
を利用します。
S3・CloudFrontと、CI/CDの2スタックをそれぞれデプロイするスクリプト、および、全てをデプロイするスクリプトの3種を記載します。
実際のコードが以下となります。
package.json
"deploy:ci/cd": "run-s build \"cdk deploy -- {1} --context projectName={2} --profile {3}\" --",
"deploy:s3": "run-s build \"cdk deploy -- {1} --context projectName={2} --profile {3}\" --",
"deploy:all": "run-s \"deploy:s3 {3} {1} {2}\" \"deploy:ci/cd {4} {1} {2}\" --"
プロジェクト名、awsプロファイル、各スタック名を引数として実行します。
実行例は以下の通りです。
npm run deploy:s3 {S3_StackName} {PJ_Name} {PROFILE}
npm run deploy:ci/cd {CI/CD_StackName} {PJ_Name} {PROFILE}
npm run deploy:all {PJ_Name} {PROFILE} {S3_StackName} {CI/CD_StackName}
おわりに
今回、S3やCloudFrontを用いたフロント側のCI/CDを作成しました。
また、今回作成したテンプレートにはまだ、下記のような課題が残っているので更新していきたいと思っています。
- CI/CDのステータスの通知
- CDK自体のテストコード
今回記載したテンプレートの全体像ははgithubに公開しています。
https://github.com/masahiro-sanya/front-pipeline
ご参考になればと思います。
その他、誤りや指摘事項があればコメント頂けると幸いです。
Author And Source
この問題について(AWS CDKで作る CI/CDパイプライン), 我々は、より多くの情報をここで見つけました https://qiita.com/s1m0n281/items/776bd22075b087a40659著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .