📘

ControlTower[CT.S3.PR.1] が有効化されている環境でAmplify Sandboxを実行する

に公開

AWS Amplify Gen2のサンドボックス環境を構築しようとしたところ、Control Towerのコントロールによってデプロイが失敗する問題に遭遇しました。本記事では、この問題の原因と技術的な対応方法について詳しく解説します。

はじめに

AWS Amplify Gen2は、フロントエンド開発者がバックエンドリソースを簡単に構築できるフレームワークです。特にサンドボックス機能は、ファイルの変更を検知して自動的にバックエンドを更新してくれる非常に便利な機能です。

しかし、セキュリティが強化された企業環境では、Control Towerのコントロールによって、Amplifyが自動生成するリソースの作成が制限される場合があります。
本記事では、Control Tower環境でAmplify Sandboxを利用する際に発生する、CT.S3.PR.1のアクション抑止エラーの背景と技術的対応を解説します。

発生した問題

Amplify Gen2のサンドボックスをデプロイしようとしたところ、CloudFormationで以下のエラーが発生しました。

The following hook(s) failed: [AWS::ControlTower::Hook]

CloudFormationの詳細画面を確認すると、より具体的なエラーメッセージが表示されています。

AWS::ControlTower::Hook

Hook failed with message: ValidationError [CT.S3.PR.1]: 
Require an Amazon S3 bucket to have block public access settings configured 
[FIX]: The parameters 'BlockPublicAcls', 'BlockPublicPolicy', 'IgnorePublicAcls', 
'RestrictPublicBuckets' must be set to true under the bucket-level 
'PublicAccessBlockConfiguration'. 

コントロール(ガードレール)を違反したエラー

このエラーは、Control Towerのコントロール[CT.S3.PR.1]に違反していることを示しています。

Control Towerのコントロールの仕組み

Control Towerのコントロールは、AWSアカウントのセキュリティとコンプライアンスを確保するための制御機能です。
CloudFormation(IaC)を使用したリソース作成時のチェックがAWSより提供されています。
これらのフックは、リソース作成のタイミングで判定処理を行い、不適切なリソースの作成を防止しています。

CT.S3.PR.1の具体的な要件

CT.S3.PR.1の具体的な要件は以下に記載があります。
https://docs.aws.amazon.com/controltower/latest/controlreference/s3-rules.html#ct-s3-pr-1-description

このルールでは、S3バケットのPublicAccessBlockConfigurationに以下の4つの項目すべてをtrueに設定することが求められています。

  • BlockPublicAcls
  • BlockPublicPolicy
  • IgnorePublicAcls
  • RestrictPublicBuckets

Remediation for rule failure
The parameters BlockPublicAcls, BlockPublicPolicy, IgnorePublicAcls, RestrictPublicBuckets must be set to true under the bucket-level PublicAccessBlockConfiguration.

Amplify Sandboxの構成と生成リソース

現在のAmplify構成

今回作成するAmplifyプロジェクトのbackend.tsは以下の構成です。

backend.ts
/**
 * @see https://docs.amplify.aws/react/build-a-backend/ to add storage, functions, and more
 */
const backend = defineBackend({
  myFirstFunction,
  data,
});

このコードは、Lambda関数(myFirstFunction)とAppSync API(data)を使用したシンプルなAPI連携を実現しています。
公式ドキュメントでfunctionを使用するチュートリアルで提示されている物ですね。

Amplifyが生成するCloudFormationスタック構造

Amplify Gen2はCDKベースでリソースを作成、管理しており、生成されるリソースは.amplify\artifacts\cdk.outディレクトリに出力されます。
メインのファイルを確認すると、以下のようなNested Stack構造になっていることが分かります。

cdk.out
{
  "Resources": {
    "function1351588B": {
      "Type": "AWS::CloudFormation::Stack",
      // Lambda関数に関連するスタック
    },
    "data7552DF31": {
      "Type": "AWS::CloudFormation::Stack",
      // AppSyncに関連するスタック
    }
  }
}

ここまでの検証の結果、functionのみをデプロイした場合はエラーが発生しないことが確認できたため、問題のS3バケットはdataスタック内に存在すると推測されます。

原因分析

問題のS3バケットの特定

dataスタックのCloudFormationテンプレートを詳しく調査した結果、以下の2つのS3バケットがPublicAccessBlockConfigurationの設定なしで作成されている様です。

1. AmplifyCodegenAssetsBucket

cdk.out
"amplifyDataAmplifyCodegenAssetsAmplifyCodegenAssetsBucket9CCB4ACA": {
  "Type": "AWS::S3::Bucket",
  "Properties": {
    "CorsConfiguration": {
      "CorsRules": [...]
    },
    "Tags": [
      {
        "Key": "amplify:friendly-name",
        "Value": "amplifyData"
      }
    ]
  }
}

このバケットは、Amplifyのコード生成アセットを格納するために使用されます。

2. ModelIntrospectionSchemaBucket

cdk.out
"modelIntrospectionSchemaBucketF566B665": {
  "Type": "AWS::S3::Bucket",
  "Properties": {
    "Tags": [
      {
        "Key": "amplify:deployment-type",
        "Value": "sandbox"
      }
    ]
  }
}

このバケットは、AppSyncモデルのイントロスペクション情報を一時格納するサンドボックス専用のバケットです。

どちらのバケットもPublicAccessBlockConfigurationが設定されていないため、CT.S3.PR.1のルールに違反しています。
原因となるBucketが特定できました!

対策① — CDKのエスケープハッチ

エスケープハッチの概要

Amplifyのbackendオブジェクトは、CDKのL1/L2コンストラクトへのアクセスを提供しています。この仕組みは「エスケープハッチ」と呼ばれ、Amplifyが自動生成するリソースをカスタマイズすることができます。

参考:Overriding resources

実装方法

backend.tsに以下のコードを追加して、S3バケットにPublicAccessBlockConfigurationを設定します。
全てのS3に対して、同様の設定を適用したいので、findAll()を使用してCfnBucketすべてに対して同様の設定を行っていきます。

backend.ts
import { defineBackend } from '@aws-amplify/backend';
import { myFirstFunction } from './my-first-function/resource';
import { data } from './data/resource';
import * as s3 from 'aws-cdk-lib/aws-s3';

const backend = defineBackend({
  myFirstFunction,
  data,
});

// すべてのS3バケットにPublicAccessBlockConfigurationを適用
for (const child of backend.data.node.findAll()) {
  if (child instanceof s3.CfnBucket) {
    console.log("Applying setting to bucket::", child.node.path);
    child.addPropertyOverride("PublicAccessBlockConfiguration", {
      BlockPublicAcls: true,
      BlockPublicPolicy: true,
      IgnorePublicAcls: true,
      RestrictPublicBuckets: true,
    });
  }
}

部分的な成功と課題

この修正により、AmplifyCodegenAssetsBucketは正常にデプロイできるようになりました。しかし、ModelIntrospectionSchemaBucketは依然としてエラーが発生します。

詳細は不明ですが、ModelIntrospectionSchemaBucketbackend.dataのスコープ範囲外で作成されているため、エスケープハッチでは修正できないのではないかと推察します。

対策② — Amplify内部モジュールの一時修正

修正対象の特定

ModelIntrospectionSchemaBucketを作成している箇所を調査したところ、@aws-amplify/backend-dataモジュールのfactory.jsで以下のように定義されていることが分かりました。

factory.js
const modelIntrospectionSchemaBucket = new Bucket(scope, 'modelIntrospectionSchemaBucket', {
    enforceSSL: true,
    autoDeleteObjects: true,
    removalPolicy: RemovalPolicy.DESTROY
});

修正内容

L2コンストラクトレベルでblockPublicAccessプロパティを追加します。

factory.js
const modelIntrospectionSchemaBucket = new Bucket(scope, 'modelIntrospectionSchemaBucket', {
    enforceSSL: true,
    autoDeleteObjects: true,
    removalPolicy: RemovalPolicy.DESTROY,
+   blockPublicAccess: BlockPublicAccess.BLOCK_ALL  // この行を追加
});

また、import文も修正します。

factory.js
- import { Bucket } from 'aws-cdk-lib/aws-s3'; // 修正前
+ import { Bucket, BlockPublicAccess } from 'aws-cdk-lib/aws-s3'; // 修正後

patch-packageによる恒久化と共有

モジュールの直接修正は一時的な対応であり、npm installを実行すると元に戻ってしまいます。そこで、patch-packageを使用して修正を恒久化し、チームで共有できるようにします。

これらの修正は、バグではない挙動だと思いますので、GitHub Issueを出す物でもないかなとは思います。

1. patch-packageのセットアップ

package.jsonにpostinstallスクリプトを追加します。

package.json
{
  "scripts": {
    "postinstall": "patch-package"
  }
}

必要なパッケージをインストールします。

npm install patch-package postinstall-postinstall --save-dev

2. パッチファイルの作成

モジュールの修正後、以下のコマンドでパッチファイルを作成します。

npx patch-package @aws-amplify/backend-data

実行が完了し、パッチの作成が完了しました。

PS C:\workspace_tame\tame-manual-web> npx patch-package @aws-amplify/backend-data
patch-package 8.0.1
 Creating temporary folder
 Installing @aws-amplify/backend-data@1.6.2 with npm
 Diffing your files with clean files
 Created file patches/@aws-amplify+backend-data+1.6.2.patch

3. パッチファイルの内容

生成されたパッチファイル(patches/@aws-amplify+backend-data+1.6.2.patch)は以下です。

diff --git a/node_modules/@aws-amplify/backend-data/lib/factory.js b/node_modules/@aws-amplify/backend-data/lib/factory.js
index 1518de0..f03a60d 100644
--- a/node_modules/@aws-amplify/backend-data/lib/factory.js
+++ b/node_modules/@aws-amplify/backend-data/lib/factory.js
@@ -9,7 +9,7 @@ import { AmplifyError, AmplifyUserError, CDKContextKey, TagName, } from '@aws-am
 import { Aspects, RemovalPolicy, Tags } from 'aws-cdk-lib';
 import { convertJsResolverDefinition } from './convert_js_resolvers.js';
 import { AppSyncPolicyGenerator } from './app_sync_policy_generator.js';
-import { Bucket } from 'aws-cdk-lib/aws-s3';
+import { Bucket, BlockPublicAccess } from 'aws-cdk-lib/aws-s3';
 import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment';
 import { convertLoggingOptionsToCDK } from './logging_options_parser.js';
 const modelIntrospectionSchemaKey = 'modelIntrospectionSchema.json';
@@ -192,6 +192,7 @@ class DataGenerator {
             enforceSSL: true,
             autoDeleteObjects: true,
             removalPolicy: RemovalPolicy.DESTROY,
+            blockPublicAccess: BlockPublicAccess.BLOCK_ALL
         });
         new BucketDeployment(scope, 'modelIntrospectionSchemaBucketDeployment', {
             // See https://github.com/aws-amplify/amplify-category-api/pull/1939

このパッチファイルをGitにコミットすることで、チームメンバーがnpm installを実行した際に自動的に修正が適用されるはずです。

動作確認

すべての修正を適用した後、CloudFormationテンプレートを確認すると、PublicAccessBlockConfigurationが正しく設定されていることが確認できました!

cdk.out
"modelIntrospectionSchemaBucketF566B665": {
  "Type": "AWS::S3::Bucket",
  "Properties": {
    "PublicAccessBlockConfiguration": {
      "BlockPublicAcls": true,
      "BlockPublicPolicy": true,
      "IgnorePublicAcls": true,
      "RestrictPublicBuckets": true
    },
    // ... その他のプロパティ
  }
}

最後に、サンドボックスを起動してみます。

> npx ampx sandbox
19:46:34 Backend synthesized in 3.03 seconds
19:46:36 Built and published assets
19:46:39 Updated AWS::AppSync::ApiKey data/amplifyData/GraphQLAPI/DefaultApiKey
19:46:39 Deployment completed in 2.693 seconds
19:46:39 AppSync API endpoint = [endpoint]
19:46:39 [Sandbox] Watching for file changes...
19:46:39 File written: amplify_outputs.json

無事にデプロイが成功しました!

⚠️注意事項

パッチを適用して、Amplifyライブラリ内部のコード修正を行っていますが、これらはAWS公式でサポートされていない暫定的な対応方法です。以下のリスクもありますので、実施する際は分かったうえで行ってください!

  1. Amplifyのアップデート時の影響

    • モジュールのバージョンアップ時にパッチが適用できなくなる可能性があります
    • パッチの内容とアップデート内容が競合する場合があります
  2. 動作保証がない

    • 公式サポート外の修正のため、予期しない動作をする可能性があります
    • 将来的にAmplify側で別の対応が実装される可能性があります

実施する場合は自己検証のうえ、チーム内で適切にパッチ管理を行ってください。

まとめ

Control Towerの[CT.S3.PR.1]コントロールが有効化された環境でAmplify Gen2のサンドボックスを実行するには、以下の2つの対応が必要でした!

  1. CDKエスケープハッチの使用

    • backend.tsでCDKのL1コンストラクトにアクセス
    • S3バケットにPublicAccessBlockConfigurationを追加
  2. patch-packageによるモジュール修正

    • Amplifyモジュール内で直接作成されるバケットへの対応
    • 修正をパッチファイルとして管理・共有

これらの対応により、セキュリティ要件を満たしながらAmplifyの便利な機能を活用できます。
ただし、暫定的な対応ですので、臨機応変に設定を確認する必要があると思います。

将来的には、Amplify側でControl Tower環境への対応が実装されることを期待しています。
それまでは、本記事の方法を参考に、セキュリティとコンプライアンスを確保しながら、効率的な開発環境を構築してみてください!

Discussion