LoginSignup
2
4

More than 3 years have passed since last update.

S3デプロイをCodePipeline+Lambdaでやる

Last updated at Posted at 2019-08-24

概要

 S3 + CloudFrontで静的コンテンツを配信して稼働しているWebサイトをよく見かけます。
LPサイトや自社のホームページなどに向いている構成です。
 当記事ではCodePipelineを使った静的サイトの自動デプロイについて解説していきます。

基本的な流れ

  1. Githubへプッシュ
  2. CodePipelineの開始
  3. 成果物の生成
  4. Lambdaの起動
  5. S3へのデプロイ
  6. キャッシュの削除

注意点

 CodePipelineではデフォルトでS3に配置するオプションがあるのですが、既存ファイルの削除には対応していません。
 そのため、S3に配置する前に一旦Lambdaを通すことで削除と配置の両方に対応する方法についてみていきます。

手順

Serverlessによる初期化

 LambdaのデプロイにServerlessFrameworkを使います。まずはプロジェクトを作りましょう。

serverless create --template aws-nodejs --path static-contents-to-s3

Serverlessデプロイ用ユーザー作成

 AWSコンソールよりIAMポリシーおよびIAMユーザーを作成します。
このユーザーはServerlessのデプロイ用ユーザーです。

ポリシー

Serverlessのデプロイ時に以下のアクセス権限が必要になりますので付与します。
CloudFormationCloudWatchEventBridgeIAMLambdaS3
なおフルアクセス+全てのリソースを対象としていますが、セキュリティを意識する場合は権限を絞りましょう。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "cloudformation:*",
                "iam:*",
                "lambda:*",
                "s3:*",
                "logs:*",
                "events:*"
            ],
            "Resource": "*"
        }
    ]
}

スクリーンショット 2019-08-24 9.11.39.png

IAMユーザー

プログラムによるアクセスでユーザーを作成します。
ポリシーに先ほど作成したものをアタッチします。
なお出力されるアクセスキーIDシークレットアクセスキーは後ほど使用するので控えておきます。

スクリーンショット 2019-08-24 9.13.29.png

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に記載することで関数と一緒にデプロイします。
なお、全てへのリソースへのアクセスを許容していますが、セキュリティを意識する場合は絞りましょう。

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 のような雰囲気でアップできるようにスクリプト化しておきます。

package.json
{
  "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で登録します。
リポジトリとブランチを指定して次に進みます。

スクリーンショット 2019-08-24 15.05.35.png

ビルドステージ

ビルドステージは使いませんのでスキップします。

スクリーンショット 2019-08-24 15.06.01.png

デプロイステージ

デプロイステージはLambdaを選択・・・と言いたいところですが
デフォルトでは選択できないようになっています。
ですので初めは適当なS3にデプロイする設定にしておきます。

スクリーンショット 2019-08-24 15.07.19.png

デプロイステージの編集

一旦作成したら「編集する」で再設定をおこないます。
アクションプロバイダーをAWS Lambdaとし、関数名は先ほどServerlessでアップしたものを指定します。

ユーザーパラメーターには{"bucket":"{バケット名}"}のようにデプロイ先のS3バケットを指定します。
この値がLambda関数に渡されるようになっています。

スクリーンショット 2019-08-24 15.12.02.png

キャッシュ削除ステージ

CloudFrontのキャッシュ削除用のステージを新しく作成します。

スクリーンショット 2019-08-24 17.37.27.png

ユーザーパラメーターにCloudFrontのDistributionIdを指定します。
{"DistributionId":"{自身のCloudFrontのID}"}

スクリーンショット 2019-08-24 17.38.30.png

ここまでできれば完成です。
Githubの自身で指定したブランチにプッシュすることで、一連の処理が実行されるようになります。

サンプルコード

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4