見出し画像

S3へのレプリケーションをトリガーにCloud Frontのキャッシュ削除を実行する

はじめに

こんにちは、SHIFT にて自動化アーキテクトとしてテスト自動化・DevOps 導入支援などをしていますKatayamaです。

フロントエンドのBuild成果物がDeploy環境にレプリケートされた事をトリガーに、Cloud Frontのキャッシュ削除を行うCDの構築を行ったので、それに関するTipsのようなものを書き残しておこうと思います。

※マルチベンダーなどの場合、開発資材のやり取り(コピー・複製=レプリケーション)が必要だが、そこにコミュニケーションコスト・作業コストをかけたくないという要望から始まったもの。

構成イメージ

何らかのトリガーでCode BuildがStartすると、S3にBuild成果物が保存され、それをトリガーにレプリケートが走る。 レプリケーション先の環境でレプリケート後に自動でDeploy(Cloud Frontのキャッシュ削除)を実行させる。

図で示すと以下のようになる。

※上記のCICDを構築したシステム側の構成は、よくあるS3×Cloud Frontで静的ホスティングしているもの。

Cloud Frontのキャッシュ削除

今回のDeployは、S3へのレプリケーションでindex.html・JavaScript・CSSなどがPUTされたら、Cloud Frontのファイルの無効化を行い、次にユーザがサイトにアクセスしたら最新のS3のファイルを見に行くようにさせる方式で行った。

Deploy自体は、レプリケーションでs3のBucketに新規でオブジェクトが作成されるのでそれをトリガーにLambda関数を実行し、以下のAWS CLIコマンドで行っている事と同じ事をAWS SDK(今回はJavaScript)で実行させる、という感じで行う。

aws cloudfront create-invalidation \
--distribution-id {distribution id} Paths={Quantity=1,Items=['/*']},CallerReference=UUID

※ Paths={Quantity=1,Items=['/*']},CallerReference=UUID の部分は複雑になるとJSONで書いた方がいい場合もあるが、その時は以下のようにJSONファイルからパラメータを読み込むようにすればいい。

aws cloudfront create-invalidation --distribution-id {distribution id} --paths file://paths.json
{
  "Paths": {
    "Quantity": 1,
    "Items": ["/*"]
  },
  "CallerReference": "UUID"
}

・参考:aws.cloudfront.create-invalidation
・参考:ファイルから AWS CLI パラメータをロードする

IAM

Lambda関数を作成したり、トリガーを設定するために必要な権限としては以下が必要になる。

 ・ iam:系:実行ロールの作成・付与をするのに必要
 ・ lambda:系:lambda関数を作成したり、トリガーを設定したりするのに必要
 ・ s3:系:s3のBucketにオブジェクトが追加されたら…というトリガーの設定をするのに必要

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "iam:PassRole",
                "iam:ListAttachedRolePolicies",
                "iam:PutRolePolicy",
                "iam:ListRolePolicies",
                "iam:GetRolePolicy"
            ],
            "Resource": "arn:aws:iam::xxxxxxxxxxxx:role/replication-deploy-lambda"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "iam:ListPolicies",
                "iam:ListRoles"
            ],
            "Resource": "*"
        }
    ]
}
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "lambda:CreateFunction",
                "lambda:UpdateFunctionCode",
                "lambda:AddPermission",
                "lambda:GetFunction",
                "lambda:GetPolicy"
            ],
            "Resource": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:replication-deploy-lambda"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "lambda:ListFunctions",
                "lambda:ListEventSourceMappings",
                "lambda:GetAccountSettings"
            ],
            "Resource": "*"
        }
    ]
}
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "s3:PutBucketNotification",
                "s3:GetBucketNotification",
                "s3:GetBucketLocation"
            ],
            "Resource": "arn:aws:s3:::replication-deploy-lambda"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "s3:ListAllMyBuckets",
            "Resource": "*"
        }
    ]
}

Lambda関数のトリガーを設定する

Amazon S3 イベント通知という仕組みを使い、S3にオブジェクトが追加されたタイミングでLambda関数をトリガーするように設定をする。

s3のバケットに対するイベントの PUT を設定し、場合によってはプレフィックス・サフィックスを指定する事で、特定のオブジェクトが追加された時のみLambdaがトリガーされるように設定する事もできる。

以下ではManagement Consoleでの設定と、AWS CLIコマンドでの設定の2パターンを見てみる。

Management Consoleでの設定のパターン

+トリガーを追加 から以下の画面が開くのでそこからトリガーにS3を設定し、イベントタイプなどを選択する。

※Management Consoleから設定する場合、 s3:GetBucketLocation (AmazonS3バケットが存在するリージョンを返す権限)がないと以下のようなエラーになり作成できないので注意(トリガーの一覧にS3のイベントが設定できているように見えるがエラーになる)。

AWS CLIコマンドでの設定のパターン

AWS CLIコマンドで行う場合は2Stepになる( aws lambda add-permission について何をしているのか?はLambda関数の権限で注意点を参照)。

aws lambda add-permission \
    --function-name replication-deploy-lambda \
    --principal s3.amazonaws.com \
    --statement-id replication-deploy-lambda-permission \
    --action "lambda:InvokeFunction" \
    --source-arn arn:aws:s3:::replication-deploy-lambda \
    --source-account xxxxxxxxxxxx

aws s3api put-bucket-notification-configuration \
    --bucket replication-deploy-lambda \
    --notification-configuration file://notification.json
{
    "LambdaFunctionConfigurations": [
        {
            "LambdaFunctionArn": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:replication-deploy-lambda",
            "Events": [
                "s3:ObjectCreated:Put"
            ]
        }
    ]
}

※ s3:ObjectCreated:Put の部分は自分がトリガーにしたいイベントを書くが、今回は PUT になる(アクション一覧はサポートされるイベントタイプを参照)。

※AWS CLIのリファレンスではSNSの設定が例になっているので TopicConfiguration であるが、今回はLambdaをトリガーする設定なので LambdaFunctionConfiguration で設定する事になる。

※今回は aws s3api put-bucket-notification-configuration というAPIでLambdaのトリガーを設定しているが、他にも aws lambda create-event-source-mapping というAPIもあり、これも同じようにイベントトリガーを設定するためのAPIだが、これはS3を設定できる仕様になっていないので今回は aws s3api put-bucket-notification-configuration を使っている。

※上記の「Management Consoleでの設定」では、 s3:GetBucketLocation がないとエラーになりトリガーの設定ができなかったが、AWS CLIの場合はなくても作成はできる。ただし、Management Consoleから設定されたトリガーを見るには s3:GetBucketLocatio の権限がないと以下のようになるので必要と思われる。

AWS CLIのリファレンスは以下

 ・ aws.lambdaadd-permission
 ・ aws.s3api.put-bucket-notification-configuration

・参考:【Tips】AWS-CLIでAWS LambdaのイベントソースにS3を設定する
・参考:PutBucketNotificationConfiguration Request Body

Lambda関数の権限で注意点

Lambdaには2つの権限設定があり、

・実行ロール
 Lambda関数がどのAWSサービスへアクセスできるのか?を制御するための権限
・リソースベースのポリシー
 どのAWSサービスならLambda関数へアクセスできるのか?を制御するための権限

この2つの設定をしないといけない。今回だとイベントのトリガーはs3なので以下のように設定する必要がある。

アクセス権限 > リソースベースのポリシー

以下ではLambda関数のリソースベースのポリシーを設定する方法を、Management Consoleから行う場合と、AWS CLIコマンドで行う場合とを見ていく。

Management Consoleでの設定のパターン

Management Consoleからやる分には、「トリガーを追加」でS3を指定すると勝手に「リソースベースのポリシー」は作成される。

AWS CLIコマンドでの設定のパターン

aws lambda add-permission \
    --function-name {関数名} \
    --principal s3.amazonaws.com \
    --statement-id {statement id(一意になれば何でもOK)} \
    --action "lambda:InvokeFunction" \
    --source-arn arn:aws:s3:::{バケット名} \
    --source-account {バケットがあるAWSアカウントID12桁}

・参考:AWS のサービスへのアクセス権を関数に付与する(翻訳が良くないので意訳すると、「AWSサービスからLambda関数を呼び出すアクセス権を付与する」)The simplest resource-based policy statement allows a service to invoke a function. (最も単純なリソースベースのポリシーステートメントにより、サービスは関数を呼び出すことができます。)と書かれているように、他のAWSサービスがLambda関数を呼び出すための設定が「リソースベースのポリシー」
・参考:リソースベースのポリシーの仕組み

Lambda関数を実装する

イベントトリガーの場合には、Lambda関数にEventオブジェクトが渡ってくるが、まずはS3へのオブジェクト追加(Amazon S3 イベント通知)時にどんなEventオブジェクトが渡ってくるのか?を見てみる。 その後、そのEventオブジェクトの中身からDeploy(CloudFront CreateInvalidation )を行うのに必要な情報を抜き出し実際にそれを実行するコードを考える。

Lambdaに渡ってくるEventオブジェクトの中身

以下のようなオブジェクトが渡ってくる。 ※実際に実装してみて後から分かった事として、Recordsと配列になっているが実際には配列の要素は1つであった(複数になる場合があればご指摘下さい)。

この中にどのBucketにPUTされたのか?という事が分かる情報(以下のJSONのキーの情報)があるので、これを使って次の項で実際にLambda関数を実装していく。

{
  "Records": [
    {
      "eventVersion": "2.1",
      "eventSource": "aws:s3",
      "awsRegion": "us-east-2",
      "eventTime": "2019-09-03T19:37:27.192Z",
      "eventName": "ObjectCreated:Put",
      "userIdentity": {
        "principalId": "AWS:AIDAINPONIXQXHT3IKHL2"
      },
      "requestParameters": {
        "sourceIPAddress": "205.255.255.255"
      },
      "responseElements": {
        "x-amz-request-id": "D82B88E5F771F645",
        "x-amz-id-2": "vlR7PnpV2Ce81l0PRw6jlUpck7Jo5ZsQjryTjKlc5aLWGVHPZLj5NeC6qMa0emYBDXOo6QBU0Wo="
      },
      "s3": {
        "s3SchemaVersion": "1.0",
        "configurationId": "828aa6fc-f7b5-4305-8584-487c791949c1",
        "bucket": {
          "name": "lambda-artifacts-deafc19498e3f2df",
          "ownerIdentity": {
            "principalId": "A3I5XTEXAMAI3E"
          },
          "arn": "arn:aws:s3:::lambda-artifacts-deafc19498e3f2df"
        },
        "object": {
          "key": "b21b84d653bb07b05b1e6b33684dc11b",
          "size": 1305107,
          "eTag": "b21b84d653bb07b05b1e6b33684dc11b",
          "sequencer": "0C0F6F405D6ED209E1"
        }
      }
    }
  ]
}

・参考:AWS Lambda を Amazon S3 に使用する

Lambda関数(index.handler)の実装

今回はNode.jsのLambda関数でDeployを実行させるので、実装としては以下のようした。

const { CloudFrontClient, CreateInvalidationCommand } = require("@aws-sdk/client-cloudfront");
const config = require("./config")
const client = new CloudFrontClient({ region: process.env.REGION });

exports.handler = async (event) => {
    try {
        const bucket = event.Records[0].s3.bucket.name;
        const object = event.Records[0].s3.object.key;
        const link = config.linking.filter(el => el.bucket === bucket)[0];

        console.log("object", object);
        console.log("link", link);

        if (!object.includes("index.html") || !link) {
            console.log("Not create invalidation because not applicable.", object);
            return "";
        }

        const input = {
            DistributionId: link.distributionId,
            InvalidationBatch: {
                CallerReference: String(Date.now()),
                Paths: {
                    Items: ["/*"],
                    Quantity: 1
                }
            }
        }
        const command = new CreateInvalidationCommand(input);
        const response = await client.send(command);

        console.log("status", response.$metadata.httpStatusCode);
        console.log("invalidation status", response.Invalidation.Status);

        return "";
    } catch (error) {
        return errorHandler(error);
    }
}

const errorHandler = (error) => {
    const obj = {};
    obj["status"] = 500;
    obj["message"] = error.message;
    obj["stack"] = error.stack;
    obj["result"] = "ng";
    if (error.$metadata) {
        obj["status"] = error.$metadata.httpStatusCode;
    }

    console.log("errorHandler", obj);
    return "";
}
exports.linking = [
    {
        bucket: "replication-deploy-lambda",
        distributionId: "xxxxxxxxxxxxxxxxx"
    }
]

※実装について何点か補足すると、

・event.Records[0].s3.bucket.name ・ event.Records[0].s3.object.key
 ここでLambda関数に渡ってくるEventオブジェクトの中身から必要な情報を抜き出している(Eventオブジェクトの中身についてはLambdaに渡ってくるEventオブジェクトの中身を項を参照)。

・config.linking.filter(・・・)
 Eventオブジェクトの中身を見れば分かるが、Cloud FrontのディストリビューションIDは当然ながらS3のPUTイベントには含まれていない。そこでどのBucketとどのディストリビューションが紐づいているか?を何らかの形で特定できるようにする必要があり、今回は config/index.js にその情報を持たせている。

・if (objName.includes("index.html") && link)
 Lambdaのトリガー設定時に「プレフィックス・サフィックス」を設定できるので、ここで敢えて条件分岐は不要かもしれないが念のために実装している。

・console.log()
 今回はS3がLambda関数のトリガーでありreturnされた結果を人が見る事ができないので、何が起きているか分かるように敢えてconsole.log()でlogに出力させるようにした。

・return "";
 これも今回S3がトリガーでreturnされた結果を人が見れないので "" (空文字)を返すようにした(何かJSONを返しても意味ないと判断した)。

・config.js
 これはjsonでもいい(jsだとコメントアウトができたりテストしやすかったのでjsにしたが、テスト側で書き換えられるので今思えばjsonでいい気がしている)。

・if (error.$metadata)
 AWS SDKのエラーでresponseがある場合、 $metadata の中に httpStatusCode の情報が入っているので、それがある時はそれを参照するようにしている。

・参考:CloudFront Client - AWS SDK for JavaScript v3
・参考:Class CreateInvalidationCommand

Lambda関数の実行ロールの設定

Lambda関数の権限で注意点で触れたが、Lambda関数の権限には2つあり、Lambda関数が他のAWSサービスに対して何らかの操作を行うには、実行ロールでその権限を設定する必要がある。

Management Consoleから関数を作成すると、以下のように勝手に実行ロールが作成されるが、その実行ロールに対し管理ポリシーやインラインポリシーで権限を付与する。

今回はCloud Frontの Create Invalidation が実行できればいいので以下のようなインラインポリシーを追加でアタッチするればOK。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "cloudfront:CreateInvalidation",
            "Resource": [
                 "arn:aws:cloudfront::xxxxxxxxxxxx:distribution/xxxxxxxxxx",
                 "arn:aws:cloudfront::xxxxxxxxxxxx:distribution/xxxxxxxxxx"
            ]
        }
    ]
}

まとめとして

2020年12月からレプリケーションがより使いやすくなったので今回扱ったような構成でのDeployもよくあるものになると思う。

補足 「Lambda関数のトリガーを設定する」について

上記のLambda関数のトリガーを設定するでは、Amazon S3 イベント通知の仕組みを使ってLambdaを実行するように設定したが、Cloud Trailのデータイベントを使ってCloud Watch EventでCloud Trailのイベント(AWS API Call via CloudTrail)を補足してLambdaを実行させる事も技術的には可能。

ただし、Cloud Trailのデータイベント記録は有料なのでそこだけ注意。

・参考:AWS CloudTrail の料金

__________________________________

執筆者プロフィール:Katayama Yuta
SaaS ERPパッケージベンダーにて開発を2年経験。 SHIFTでは、GUIテストの自動化やUnitテストの実装などテスト関係の案件に従事したり、最近ではDevOpsの一環でCICD導入支援をする案件にも従事。 座学で読み物を読むより、色々手を動かして試したり学んだりするのが好きなタイプ。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/