FURYU Tech Blog - フリュー株式会社

フリュー株式会社の開発者が技術情報を発信するブログです。

RemoteOutputsのおかげで好きなリージョンCloudFrontにWebACLを紐付けれる話

突然ですが、S3に保存している静的ファイルをCloudFrontでキャッシュし、HTTPSでのリクエストを可能にしたい気持ちに駆られることはよくあると思います。
そしてその構成をコード管理したいため、CDKで作成したいということもよくあると思います。

実はリージョンに制約があったりして、実装する上で躓いたところがあるのでまとめました。

※筆者の環境

* Mac OS 12
* Typescript 4.8.4
* node 18.10.0
* CDK CLI 2.44.0

[前段]つまづいた流れ

弊社のサービスは基本的に日本国内のユーザーが多いです。
そのためAWSリソースは全て日本国内のリージョン(ap-northeast-1/ap-northeast-3など)に作成するのが適切です。
特にS3なんかはコンテンツの提供速度に関わるため、サービス提供するエリアになるべく近いリージョンに作成するのが一般的ですよね。

ということで全て国内のリージョンを指定してcdk deployすると下記の様に怒られます。

3:24:09 PM | CREATE_FAILED        | AWS::WAFv2::WebACL | webacl
Resource handler returned message: "Error reason: The scope is not valid., field: SCOPE_VALUE, parameter: CLOUDFRONT (Service: Wafv2, Status Code: 400, Request ID: XXXXX, Extended Request ID: null)" (RequestToken: XXXXX,
HandlerErrorCode: InvalidRequest)

〜中略〜

Stack Deployments Failed: Error: The stack named web-acl-stack failed creation, it may need to be manually deleted from the AWS console: ROLLBACK_COMPLETE: Resource handler returned message: "Error reason: The scope is not valid., field: SCOPE_VALUE, parameter: CLOUDFRONT (Service: Wafv2, Status Code: 400, Request ID: XXXXX, Extended Request ID: null)" (RequestToken: XXXXX, HandlerErrorCode: InvalidRequest)

ここで落ち着いてドキュメントを見ると、WebACLをCloudFrontに当てるにはus-east-1(バージニア北部)のリージョンに作る必要があるとのことです。
参考: 公式ドキュメント

ということでWebACLをバージニア北部に作成し、そのスタックをCloudFrontから参照させます。

すると今度は下記の様に「別のリージョンにあるスタックは参照できません」と怒られます。

Error: Stack "contents-storage-stack" cannot consume a cross reference from stack "web-acl-stack". Cross stack references are only supported for stacks deployed to the same environment or between nested stacks and their parent stack

ここで「WebACLをCloudFrontに当てるには関連するもの全部us-east-1(バージニア北部)に作るの?!」となるのですが、RemoteOutputsさえあればそこまでしなくて済みます。

※AWSコンソール上で作成していれば特に悩まなくて済む問題かと思いますが、今回はCDKで実装したい…どうしても…。

[本題]RemoteOutputsで解決する

流れ

  1. WebACLをus-east-1に作成
  2. 一旦CfnOutputでそのWebACLのArnを出力する(★)
  3. WebACLのスタックのRemoteOutputsを作成する
  4. そのRemoteOutputsから(★)で作成したOutputの値を取得し、他スタックから参照可能な変数に入れてやる
  5. CloudFrontを含むスタックにRemoteOutputsのスタックを渡し、WebACLのArnを参照させる

いざ実装

まずはWebACLの作成です。
ただus-east-1に作成しているだけなので、スタック定義的には特別なことはありません。

export class WebAclStack extends cdk.Stack {
    readonly webAcl: CfnWebACL;

    constructor(scope: Construct, id: string, props: Props) {
        super(scope, id, props);

        this.webAcl = new CfnWebACL(this, 'webacl', {〜ここはお好きに〜});
    }
}

次はいざRemoteOutputsの作成です。
これだけで1スタックつくることにしましたが、実装量はごくわずかです。
※この辺のスタックの分け方は各プロジェクトでよしなにすれば良いと思います。

export class RemoteOutputStack extends cdk.Stack {
    readonly webAclArn: string;

    constructor(scope: cdk.App, id: string, props: Props) {
        super(scope, id, props);

        this.addDependency(props.webAclStack);
        const outputs = new RemoteOutputs(this, 'webaclRemoteOutput', { stack: props.webAclStack });
        this.webAclArn = outputs.get(`${props.remoteOutputId}`);
    }
}

export interface Props extends StackProps {
    readonly webAclStack: WebAclStack;
    readonly remoteOutputId: string;
}

最後はCloudFrontとS3の作成です。
こちらもただ作成するだけで、WebACLのArnをPropsとして外から受け取っている以外は特別なことはありません。

export class ContentsStorageStack extends cdk.Stack {

    constructor(scope: Construct, id: string, props: Props) {
        super(scope, id, props);

        const contentsBucket = new Bucket(this, 'contentsBucket', {
            〜ここはお好きに〜
        });
        
        new Distribution(this, 'distribution', {
            enabled: true,
            webAclId: props.webAclArn,  // ここが大事
            defaultBehavior: {
                origin: new S3Origin(this.contentsBucket),
                〜その他はお好きに〜
            }

            〜その他はお好きに〜
        });
    }
}

export interface Props extends StackProps {
    readonly projectName: string;
    readonly webAclArn: string;
}

※余談:ちなみにwebAclIdというフィールド名ですが、渡すべきはArnです。参考

ここまで用意したら、これらを組み合わせていきます。

const virginiaEnv: cdk.Environment = {
    account: '123456789012',
    region: 'us-east-1'
};
const tokyoEnv: cdk.Environment = {
    account: '123456789012',
    region: 'ap-northeast-1'
};

// WebAclをus-east-1につくる
const webAclStack = new WebAclStack(app, 'webaclStack', {
    env: virginiaEnv  // ここ注意
});

// WebAclのOutputを追加し、それを使ってap-northeast-1でRemoteOutputする
const remoteOutputId: string = 'webAclArn';
new cdk.CfnOutput(webAclStack, remoteOutputId, { value: webAclStack.webAcl.attrArn });

const remoteOutputStack = new RemoteOutputStack(app, 'remoteOutput', {
    env: tokyoEnv,  // ここ注意
    webAclStack: contentsStorageWebAclStack,
    remoteOutputId: remoteOutputId
});

// RemoteOutputと同じリージョンでCloudFront等残りのものをつくる
new ContentsStorageStack(app, 'storageStack', {
    env: tokyoEnv,  // ここ注意
    webAclArn: remoteOutputStack.webAclArn  // Arnはただの文字列なので渡せる
});

StackのコンストラクタにはStackPropsを渡すことができ、この中にEnvironment型のenvというフィールドがあります。
ここにリージョン情報を指定してやることで、スタックごとにリソースをどこに作成するかを分けることができます。
※上記の例ではStackPropsを継承したPropsを定義しているので例示のコードだけでは分かりづらいかもしれません。

まとめ

web上で検索するとRemoteOutputsが誕生する以前の記事もヒットするため、 はじめは「WebACLを用意するには全部バージニア北部につくらないといけない?なんてこと…」と勘違いしたのですが、RemoteOutputsのおかげで無事回避できました。

確かに公式ドキュメントをよく読むと、「WebACLはバージニア北部に作ってね」と書いていますが「CloudFrontも同じリージョンに作ってね」とまでは書かれていません。

実装の段にも書きましたが、1スタック増えるとしても全てのリソースをバージニア北部に作るよりは遥かに良いと思います。
なるべく近いエッジロケーションでキャッシュして、コンテンツの提供速度を上げたいためのCloudFrontなのに、実装都合で遠くのリージョンに配置するのでは恩恵が少ないですから…。

ということで以上、RemoteOutputsに救われた話でした。