概要
S3 + CloudFrontで静的コンテンツを配信して稼働しているWebサイトをよく見かけます。
LPサイトや自社のホームページなどに向いている構成です。
当記事ではCodePipelineを使った静的サイトの自動デプロイについて解説していきます。
基本的な流れ
- Githubへプッシュ
- CodePipelineの開始
- 成果物の生成
- Lambdaの起動
- S3へのデプロイ
- キャッシュの削除
注意点
CodePipelineではデフォルトでS3に配置するオプションがあるのですが、既存ファイルの削除には対応していません。
そのため、S3に配置する前に一旦Lambdaを通すことで削除と配置の両方に対応する方法についてみていきます。
手順
Serverlessによる初期化
LambdaのデプロイにServerlessFrameworkを使います。まずはプロジェクトを作りましょう。
serverless create --template aws-nodejs --path static-contents-to-s3
Serverlessデプロイ用ユーザー作成
AWSコンソールよりIAMポリシーおよびIAMユーザーを作成します。
このユーザーはServerlessのデプロイ用ユーザーです。
ポリシー
Serverlessのデプロイ時に以下のアクセス権限が必要になりますので付与します。
CloudFormation
、CloudWatch
、EventBridge
、IAM
、Lambda
、S3
なおフルアクセス+全てのリソースを対象としていますが、セキュリティを意識する場合は権限を絞りましょう。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"cloudformation:*",
"iam:*",
"lambda:*",
"s3:*",
"logs:*",
"events:*"
],
"Resource": "*"
}
]
}
IAMユーザー
プログラムによるアクセス
でユーザーを作成します。
ポリシーに先ほど作成したものをアタッチします。
なお出力されるアクセスキーID
とシークレットアクセスキー
は後ほど使用するので控えておきます。
Lambda関数作成
関数の実行ロール
Lambda関数ですが、まずは実行ロールの権限についてみていきます。
関数がCodePipelineから呼ばれる場合、以下のポリシーが必要になります。(公式ドキュメントより)
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"logs:*"
],
"Effect": "Allow",
"Resource": "arn:aws:logs:*:*:*"
},
{
"Action": [
"codepipeline:PutJobSuccessResult",
"codepipeline:PutJobFailureResult"
],
"Effect": "Allow",
"Resource": "*"
}
]
}
そして以下のポリシーも合わせて設定します。
- 成果物取得元S3バケットへのアクセス権限(Get)
- デプロイ先のS3バケットへのアクセス権限(Put, Delete, List)
- CloudFrontキャッシュ削除権限
これら実行ロールに付与するポリシーはserverless.yml
に記載することで関数と一緒にデプロイします。
なお、全てへのリソースへのアクセスを許容していますが、セキュリティを意識する場合は絞りましょう。
(一部抜粋)
# you can add statements to the Lambda function's IAM Role here
iamRoleStatements:
- Effect: 'Allow'
Action:
- 'logs:*'
Resource:
- 'arn:aws:logs:::*:*:*'
- Effect: 'Allow'
Action:
- 'codepipeline:PutJobSuccessResult'
- 'codepipeline:PutJobFailureResult'
Resource:
- '*'
- Effect: 'Allow'
Action:
- 's3:GetObject'
- 's3:PutObject'
- 's3:DeleteObject'
Resource:
- 'arn:aws:s3:::*/*'
- Effect: 'Allow'
Action:
- 's3:ListBucket'
Resource:
- 'arn:aws:s3:::*'
- Effect: 'Allow'
Action:
- 'cloudfront:CreateInvalidation'
Resource:
- '*'
AWSへのアクセス情報
.env
を作成し、先ほど作成したIAMユーザーのアクセスキーIDおよびシークレットアクセスキーを記載しておきます。
後ほど、Docker上で実行する際に環境変数として読み込ませるようにするためです。
AWS_ACCESS_KEY_ID={Your AWS Access Key ID}
AWS_SECRET_ACCESS_KEY={Your AWS Secret Access Key}
(例)
$ docker run -it --rm -v $PWD:/app --env-file .env -w /app node:10 /bin/bash
必要パッケージ
serverless
はLambdaのアップで必要になるので入れておきます。
ついでにyarn run deploy -- --stage dev
のような雰囲気でアップできるようにスクリプト化しておきます。
{
"name": "static-contents-to-s3",
"scripts": {
"deploy": "node_modules/.bin/sls deploy -v",
"remove": "node_modules/.bin/sls remove -v"
},
"dependencies": {
"mime-types": "^2.1.24",
"node-zip": "^1.1.1"
},
"devDependencies": {
"aws-sdk": "^2.517.0",
"serverless": "^1.50.0"
}
}
関数の実装
以下にサンプルを置いていますので参照してください。
https://github.com/flatnyat/static-contents-to-s3
デプロイ用関数
CodePipelineから呼ばれるLambdaにはevent["CodePipeline.job"]
に各種パラメータが渡されることに注意してください。
また、関数の完了時にはCodePipelineへ状態を通知することも必要です。
以下の部分はCodePipelineからユーザーが明示的に指定するもので後ほど設定します。
const userData = JSON.parse(data.actionConfiguration.configuration.UserParameters);
'use strict';
const Aws = require('aws-sdk');
const NodeZip = require('node-zip');
const MimeTypes = require('mime-types');
const Pipeline = require('./pipeline');
/** S3 deploy */
module.exports.handle = async (event, context) => {
console.log(JSON.stringify(event));
try {
const data = event["CodePipeline.job"].data;
// 成果物を取得して解凍
const artifactBucket = data.inputArtifacts[0].location.s3Location.bucketName;
const artifactKey = data.inputArtifacts[0].location.s3Location.objectKey;
const S3 = new Aws.S3({ region: 'ap-northeast-1' });
const object = await S3.getObject({ Bucket: artifactBucket, Key: artifactKey }).promise();
const zip = new NodeZip(object.Body, { base64: false, checkCRC32: true });
const files = Object.keys(zip.files);
console.log(files);
// 一旦バケットの中身をリセット
const userData = JSON.parse(data.actionConfiguration.configuration.UserParameters);
const objects = await S3.listObjects({ Bucket: userData.bucket, MaxKeys: 1000 }).promise();
await Promise.all(objects.Contents.map((content) => {
return S3.deleteObject({ Bucket: userData.bucket, Key: content.Key }).promise();
}));
// デプロイ
await Promise.all(files.map((i) => {
const f = zip.files[i];
if (! f.name.startsWith(".")) {
return S3.putObject({
Bucket: userData.bucket,
Key : f.name,
Body : Buffer.from(f.asBinary(), 'binary'),
ContentType: MimeTypes.lookup(f.name) || 'application/octet-stream',
}).promise();
}
}));
// CodePipelineへ通知
await Pipeline.putJobSuccess(event["CodePipeline.job"].id, "", context);
} catch (ex) {
await Pipeline.putJobFailure(event["CodePipeline.job"].id, ex, context);
}
};
CloudFrontキャッシュ削除用関数
DistributionId
は後ほどCodePipelineからユーザー指定パラメータとして設定します。
'use strict';
const Aws = require('aws-sdk');
const Pipeline = require('./pipeline');
/** CloudFront cache delete */
module.exports.handle = async (event, context) => {
console.log(JSON.stringify(event));
try {
const data = JSON.parse(event["CodePipeline.job"].data.actionConfiguration.configuration.UserParameters);
const cloudfront = new Aws.CloudFront({});
await cloudfront.createInvalidation({
DistributionId: data.DistributionId,
InvalidationBatch: {
CallerReference: String(new Date().getTime()),
Paths: {
Quantity: 1,
Items: ['/*']
}
}
}).promise();
await Pipeline.putJobSuccess(event["CodePipeline.job"].id, "", context);
} catch (ex) {
await Pipeline.putJobFailure(event["CodePipeline.job"].id, ex, context);
}
};
CodePipelineへの結果通知
'use strict';
const Aws = require('aws-sdk');
const codePipeline = new Aws.CodePipeline();
exports.putJobSuccess = (async(jobId, message, context) => {
try {
await codePipeline.putJobSuccessResult({
jobId: jobId,
}).promise();
context.succeed(message);
} catch (ex) {
context.fail(ex);
}
});
exports.putJobFailure = (async(jobId, message, context) => {
await codePipeline.putJobFailureResult({
jobId: jobId,
failureDetails: {
message: JSON.stringify(message),
type: 'JobFailed',
externalExecutionId: context.invokeid
}
}).promise();
context.fail(message);
});
関数デプロイ
CodePipelineを作る前に関数をアップしておきましょう。
当記事の例では以下のコマンドでできるようになっています。
$ docker run -it --rm -v $PWD:/app --env-file .env -w /app node:10 /bin/bash
$ yarn run deploy -- --stage dev
CodePipeline作成
AWSコンソールからCodePipelineを作成します。
ソースステージ
ソースプロバイダーをGithubで登録します。
リポジトリとブランチを指定して次に進みます。
ビルドステージ
ビルドステージは使いませんのでスキップします。
デプロイステージ
デプロイステージはLambdaを選択・・・と言いたいところですが
デフォルトでは選択できないようになっています。
ですので初めは適当なS3にデプロイする設定にしておきます。
デプロイステージの編集
一旦作成したら「編集する」で再設定をおこないます。
アクションプロバイダーをAWS Lambda
とし、関数名は先ほどServerlessでアップしたものを指定します。
ユーザーパラメーターには{"bucket":"{バケット名}"}
のようにデプロイ先のS3バケットを指定します。
この値がLambda関数に渡されるようになっています。
キャッシュ削除ステージ
CloudFrontのキャッシュ削除用のステージを新しく作成します。
ユーザーパラメーターにCloudFrontのDistributionIdを指定します。
{"DistributionId":"{自身のCloudFrontのID}"}
ここまでできれば完成です。
Githubの自身で指定したブランチにプッシュすることで、一連の処理が実行されるようになります。