CloudFormationでS3バケットに複数のバケットポリシーを割り当てようとした際の挙動

2023.05.23

初めに

S3バケットへのアクセスを制御する方法の1つとしてバケットポリシーがあり、これを割り当てることでバケット側からアクセスをIAMポリシーのような形で制御することができます。

CloudFormationでバケットポリシーを設定する場合はバケット側から指定するのではなくAWS::S3::BucketPolicyを作成しそちら側からバケットを指定する形となります。

指定方法の関係上CloudFormation上からは1つのバケットポリシーが割り当てるような設定ができそうですが、
PutBucketPolicyAPIやマネジメントコンソールの画面から1つのバケットには1つのバケットポリシーしか割り当てられなさそうです。

今回はCloudFormationで1つのバケットに対して複数のバケットポリシーを割り当てようとした場合どのような挙動となるのか見ていきます。

結論

  • タイミングが重なり同時に割り当てようとすると競合が発生しデプロイに失敗する
    • 明確に競合しているアクションが記されないため検証状況より推定
  • タイミングがズレている場合は上書きするケースとデプロイに失敗するケースがある
    • AWS::S3::BucketPolicyで生成したものは上書き可能で他のものはできない?
      • 全てのリソース種別を確認していないので確実なことは言えない

タイミングが重なった場合

以下のようなテンプレートでスタックを作成します。

AWSTemplateFormatVersion: 2010-09-09
Resources:
  Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub bucket-policy-test-${AWS::AccountId}
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - BucketKeyEnabled: True
            ServerSideEncryptionByDefault: 
              SSEAlgorithm: AES256
  BucketPolicy1:
    Type: AWS::S3::BucketPolicy
    Properties: 
      Bucket: !Ref Bucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: AccessFromCloudFront
            Effect: Allow
            Principal:
                Service: cloudfront.amazonaws.com
            Action: s3:GetObject
            Resource: !Sub arn:aws:s3:::${Bucket}/*
            Condition:
                StringEquals:
                    AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/XXXXXXXXX
  BucketPolicy2:
    Type: AWS::S3::BucketPolicy
    Properties: 
      Bucket: !Ref Buket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: AccessFromCloudFront
            Effect: Allow
            Principal:
                Service: cloudfront.amazonaws.com
            Action: s3:GetObject
            Resource: !Sub arn:aws:s3:::${Bucket}/*
            Condition:
                StringEquals:
                    AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/YYYYYYYYYY

※ バケットポリシーがCloudFrontからの許可なのは直近のブログ記事の再利用で深い意図はありません。

こちらを適用すると状況により(後述)競合が発生しスタック自体の生成が失敗します。

CloudTrailを見てもこの具体的に競合しているリクエストの情報というのは見れないため後の検証からおそらくとはなるのですが、バケットポリシー更新の特定のタイミングではロックのようなものがかかるようでタイミングが悪いとエラーとなります。

ややこしい点としては、同じテンプレートであっても発生する場合と発生しない点があるという点です。

今回の場合上記のエラーは一度バケットのみでスタックを作成した後、バケットポリシーを追加で生成した場合のみに発生しております。

バケットの作成を含め一気に新規のスタックとして作成をする場合以下のように生成が成功します。

一応今回試した中では0からデプロイした場合は必ず成功し、バケットが生成されている状態からデプロイした状態はエラーとなるといった形で一定の冪等性は見られました(試行回数が少ないため偏りの可能性有)。

明確に依存関係をつけた場合

改めてDependsOn属性を指定し明示的に依存関係を付けデプロイし適用されるバケットポリシーを見てみます。

  BucketPolicy1:
    Type: AWS::S3::BucketPolicy
    Properties: 
      ...
    DependsOn:
      - BucketPolicy2

先ほどエラーが発生したバケットのみが存在する状態からのバケットポリシーの追加という形でデプロイを行いましたがこの場合は成功となりました。

実際に割り当てられているポリシーを確認すると、後に作成されたBucketPolicy1がの内容が適用されています。

DependsOn属性を反対にBucketPolicy2側に付与した場合、今度は逆にBucketPolicy2の内容がバケットに割り当てられていました。

複数割り当て完了した場合は良い感じにマージされるわけではなく、後に適用される値で上書きされるということがわかリます。

また一括で生成せずに順々に割り当てた以下のようなケースも同様にデプロイに成功します。

  1. バケット本体定義のみでデプロイ
  2. BucketPolicy1を追加してデプロイ
  3. BucketPolicy2を追加してデプロイ

この場合最終的にBucketPolicy2の内容が適用されます。

上書きせず失敗するケースもある

ただこの上書きも必ずしも発生するわけではないようです。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-wafv2-loggingconfiguration.html
When you successfully enable logging using a PutLoggingConfiguration request, AWS WAF creates an additional role or policy that is required to write logs to the logging destination. For an Amazon CloudWatch Logs log group, AWS WAF creates a resource policy on the log group. For an Amazon S3 bucket, AWS WAF creates a bucket policy.

AWS::WAFv2::LoggingConfiguration等一部のリソースは生成時にバケットポリシーを合わせて設定するする仕様となっております。

例えばAWS::WAFv2::LoggingConfigurationを含む以下のテンプレートをデプロイします。

...
  WafLogBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub aws-waf-logs-policy-test-${AWS::AccountId}
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - BucketKeyEnabled: True
            ServerSideEncryptionByDefault: 
              SSEAlgorithm: AES256
  WafLogConfig:
    Type: AWS::WAFv2::LoggingConfiguration
    Properties: 
      LogDestinationConfigs: 
        - !GetAtt WafLogBucket.Arn
      ResourceArn: !GetAtt WAF.Arn
  WafLogBasePolicy:
    Type: AWS::S3::BucketPolicy
    Properties: 
      Bucket: !Ref WafLogBucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: AccessFromCloudFront
            Effect: Allow
            Principal:
                Service: cloudfront.amazonaws.com
            Action: s3:GetObject
            Resource: !Sub arn:aws:s3:::${WafLogBucket}/*
            Condition:
                StringEquals:
                    AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}

この場合は上書きではなくWafLogBasePolicyの設定に追記される形でポリシーが生成されます。

なお上書きではなく追記する仕様についてはバケットポリシーではなくLoggingConfigurationのAPI側の仕様によるものとなります。

先ほどの結果を確認する限りAWS::S3::BucketPolicyは直前の値を上書きするかのような挙動をとっていましたが、意図的にDependsOn属性でWafLogConfigWafLogBasePolicyの後に作成するとどうなるでしょうか。

この場合は上書きをするのではなく既存のバケットポリシーを検知し上書きをせずに失敗となるようです。

どうやらバケットポリシーリソースは一律で上書きするのではなく状況によっては上書きを実施せずにエラーを引き起こすようです。

終わりに

今回は小ネタ要素として複数のバケットポリシーを単一のバケットに対して指定を行った場合の挙動を見てみました。

実は最初のロックのような挙動のエラーと上書きの挙動だけを書こうと思っていたのですが、改めて試してみると思いもしない結果を生むこととなりました。

エラーとなる場合はあまり意識せずとも修正することになりそうですが、上書きしてしまう場合はテンプレートの分割や作業分担状況により意図せず設定を潰してしまう可能性がありそうです。

適切に分割できていればこのような現象自体に合うようなことは少ないかと思いますが1つのバケットを共用する等、特定のケースでは起き得そうですので頭の片隅に入れておくと良いかもしれません。