🔥

【CDK】クロスアカウントでCloudFrontのキャッシュをクリアする構成

2024/04/23に公開

概要

今回はCodePiplineの最後で「他のAWSアカウントのCloudFrontのキャッシュを削除する」方法について紹介します。
だいぶニッチな構成なので、そもそも前提が当てはまらない方が大半だと思いますが、自分の作業メモも兼ねて試行錯誤の記録として残します。

前提

以下2つのAWSアカウントを持っているものとします。その配下にそれぞれリソースを持っています。

  • Aアカウント(ソース管理)
    • CodeCommit
    • CodeBuild
    • CodeDeploy
    • CodePipeline
  • Bアカウント(SPAの配信)
    • CloudFront
    • S3

見てわかる通り、Aアカウントはソース管理およびCI/CDによる配信関連のリソースを、そしてBアカウントはCI/CDの成果物の配信のための役割を持っています。
アプリケーションはシンプルにReactを用いたSPAを想定します。
今回の目的は最初に書いた通り「CodePipelineが走ったあとにCloudFrontのキャッシュをクリアする」です。

これらが同じアカウントに存在する場合、比較的簡単に実現できます。
以下の記事にもあるように、「CloudFrontのキャッシュをクリアするLambda」を用意して、CodePiplineのステージとして配置するだけです。
https://qiita.com/SunagaItsuki/items/c7b2af6a6fc9c24a046c

しかし今回はCodePipelineCloudFrontが別々のアカウントに存在します。
その場合、少しだけ手間がかかったので、その紆余曲折を紹介していきます。

うまくいかなかったケース

まず初めに失敗したケースから紹介していきます。
考え方として「CloudFrontのキャッシュをクリアするLambda」をA,Bどちらのアカウントで用意していくか、を中心にしたためこのようなアプローチになりました。

①LambdaをBアカウントに作るケース

Bアカウント。つまりCloudFrontがある方のアカウントでLambdaを作成していくパターンです。
手順としては

  • BアカウントでLambdaを作成
  • そのLambdaのリソースベースのポリシーを修正し、Aアカウントからの呼び出しを許可する
  • AアカウントのCodePipelineの最後でLambdaを呼び出す

となります。クロスアカウントでの操作は何度か経験したため、最初はこれでいけるかなと思ってましたがダメでした。
Aアカウント側のCDKのデプロイ時に以下のエラーが出ました。

Stack name must match the regular expression: /^[A-Za-z][A-Za-z0-9-]*$/, got '${Token[TOKEN.xxx]}-support-yyyyyyyyyyyyy'

クロスアカウントまわりのパーミッションでダメになるかと思いきやスタック名の規則エラーが出ています。
正直、このエラーの原因がはっきりしないまま他の方法で取り掛かってしまったので、もしかしたら何らか改善すればこのやり方でいける可能性もあります。

②LambdaをAアカウントに作るケース

Aアカウント。つまりCodePipelineがある方のアカウントでLambdaを作成するパターンです。
この場合、外部のアカウントのCloudFrontのキャッシュ削除権限をどうやってLambdaに持たせるか、がポイントだと思うので

  • BアカウントでCloudFrontのキャッシュ削除権限を持ったロールを作成
  • AアカウントでLambdaを作成し、実行ロールとして上記のロールを割り当てる
  • AアカウントのCodePipelineの最後でLambdaを呼び出す

の手順としました。
ロールだけクロスアカウントで持たせるイメージです。
この方法はもともとダメそうだなという感覚だったのですが、やはりそうでした。
こちらもAアカウント側のCDKのデプロイ時に以下のエラーが出ました。

Cross-account pass role is not allowed.

クロスアカウントでのPassRoleはできないよ、とのこと。
スイッチロール的なノリで使おうとしましたが、仕様的にNGそうなので断念しました。

うまくいったケース

次にうまくいったケースです。

③LambdaをA,Bに1つずつ作り、invokeする形で対応する

  • BアカウントでCloudFrontのキャッシュを削除するLambdaを作成
  • Aアカウントで上記のLambdaを呼び出すLambdaを作成、CodePipelineの最後で呼び出す

の手順になります。
少し冗長ですが、アカウントをまたいだinvokeは可能なことを元から知っていたのでこの方法に頼りました。
一部ですがCDKのコードを紹介します

BアカウントのLambdaとCDK

Lambda
// lambda/deleteCloudFrontCacheFunc/index.ts
import {
  CloudFrontClient,
  CreateInvalidationCommand
} from '@aws-sdk/client-cloudfront';
// CloudFrontを操作するためのClientを宣言
const client = new CloudFrontClient({
  region: 'us-east-1'
});
export async function handler(event: any): Promise<any> {
  try {
    // 環境変数からCloudFrontのIDを取得
    const distributionId = process.env.DISTRIBUTION_ID;
    // キャッシュ削除処理
    await client.send(new CreateInvalidationCommand({
      DistributionId: distributionId,
      InvalidationBatch: {
        CallerReference: `invalidate-${new Date().getTime()}`,
        Paths: {
          Quantity: 1,
          Items: ['/*']
        }
      }
    });
  } catch (error) {
    console.error('Error :', error);
  }
}
CDK
// キャッシュ削除用のLambdaを宣言
const func = new NodejsFunction(this, 'Func', {
  entry: 'lambda/deleteCloudFrontCacheFunc/index.ts',
  runtime: Runtime.NODEJS_18_X,
  environment: {
    DISTRIBUTION_ID: "xxxxxx"
  },
});
// CloudFrontのキャッシュ削除権限を追加
func.addToRolePolicy(
  new iam.PolicyStatement({
    actions: ['cloudfront:CreateInvalidation'],
    resources: ['CloudFrontのARN']
  })
);
// Aアカウントからの呼び出しを許可
func.addPermission('CrossAccountPermission', {
  principal: new iam.AccountPrincipal("AアカウントのID"),
  action: 'lambda:InvokeFunction'
});

AアカウントのLambdaとCDK

Lambda
// lambda/invoker/index.ts
import { Handler } from 'aws-lambda';
import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda';

// Lambda操作のためのClientを宣言
const client = new LambdaClient({
  region: 'ap-northeast-1'
});

export const handler: Handler = async (event) => {
  try {
    // 環境変数から呼び出したいLambdaのARNを取得
    const targetLambdaArn = process.env.TARGET_LAMBDA_ARN;
    await client.send(
      new InvokeCommand({
        FunctionName: targetLambdaArn,
        InvocationType: 'Event'
      })
    );
  } catch (error) {
    console.error('Error :', error);
  }
};
CDK
// Lambdaを宣言
const invoker = new NodejsFunction(
  this,
  'invoker',
  {
    entry: 'lambda/invoker/index.ts',
    runtime: Runtime.NODEJS_18_X,
    environment: {
      TARGET_LAMBDA_ARN: "xxxxxxxxxxxxxxx"
    }
  }
);
// invokeの実行権限をつける
invoker.addToRolePolicy(
  new iam.PolicyStatement({
    actions: ['lambda:InvokeFunction'],
    resources: [cdk.Fn.sub("xxxxxxxxxxxxxxx")]
  })
);

// CodePipeline宣言
const pipeline = new Pipeline(/* ...略...*/);

// 最後にLambdaを呼び出すステージを追加
pipeline.addStage({
  stageName: 'InvokeLambda',
  actions: [
    new LambdaInvokeAction({
      actionName: 'invokeLambda',
      lambda: invoker
    })
  ]
});

これでパイプラインを動かした最後で、他のAWSアカウントのCloudFrontのキャッシュを削除する構成が出来上がりました。

まとめ

冒頭でも書いた通り「他のAWSアカウントのCloudFrontのキャッシュを削除する」方法について紹介しました。
正直なところ途中の注意書きにもある通り、ベストプラクティスでない可能性もあります。というかそうじゃないかと疑っています。

これ以外の構成で同じことができた方がいましたら、コメントいただけると大変喜びます!

Discussion