Amazon Athenaのクエリを別アカウントのS3アクセスポイントに対して実行してみました

2024.04.11

初めに

Amazon S3にはアクセスポイントと呼ばれる機能が存在しています。

Amazon S3バケット側でアクセス権限を制御する場合はバケットポリシーで設定することが多いかと思いますがバケットポリシーはIAMユーザやロール等に設定するポリシーとは異なり単一のポリシーの指定のみしかできません。

この特性上単一のバケットに対してCloudFormationでバケットポリシーを複数定義するとデプロイに失敗する、もしくは上書きしてしまうという事象も発生します。

アクセス許可が増えるほど一つのポリシーがどんどん肥大化していくため、特定のアカウントやリソース群からのアクセスをまとめて削除したい場合のチェックも大変ですし見通しが悪く変更ミスのリスクも増えてきます。

S3アクセスポイントはバケットに結びつける外付けのエンドポイントのようなもので、バケット本体とは別に箇所にアクセスの受け口を作成しそれ毎にリソースベースのポリシーを定義することが可能でこれを解決することができます。

定義の分割例

アクセスポイントを利用しない場合アカウントAのみからアクセスを許可しているバケットに対してアカウントBからのアクセスを許可する場合CloudFormationの場合以下のように定義する必要があります。

  SampleBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties: 
      Bucket: example-com-bucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement: 
          - Sid: AccountAAccess
            Effect: "Allow"
            Principal: 
              AWS: arn:aws:iam::AAAAAAAAAAAAA:root
            Action: s3:GetObject
            Resource: "arn:aws:s3:::example-com-bucket/*
+         - Sid: AccountBAccess
+           Effect: "Allow"
+           Principal: 
+             AWS: arn:aws:iam::BBBBBBBBBBBB:root
+           Action: s3:GetObject
+           Resource: "arn:aws:s3:::example-com-bucket/*

新規に別のリソースを追加するのではなくバケットポリシーの定義としては同一のものを変更する形になるため、バケットポリシーが肥大化すれば肥大化するほど誤った追加削除処理の影響範囲も合わせて大きくなってきます。

シンプルに同一の許可をする場合であればループでも対応可能ですが場合によって書き込み処理を付与するのような例外処理が混ざり秘伝のタレとなりやすいのも一つのリスクです。

アクセスポイントを利用することでリソース定義自体を別にすることができますので上記のようにアカウントA・B両方を許可する場合以下のように追加できます。

  S3AccessPointA:
    Type: AWS::S3::AccessPoint
    Properties: 
      Bucket: example-com-bucket
      Name: example-com-bucket-ap-for-account-a
      Policy:
        Version: "2012-10-17"
        Statement: 
          - Sid: AccountAAccess
            Effect: "Allow"
            Principal: 
              AWS: arn:aws:iam::AAAAAAAAAAAAA:root
            Action: s3:GetObject
            Resource: "arn:aws:s3:::example-com-bucket/*
+ S3AccessPointB:
+   Type: AWS::S3::AccessPoint
+   Properties: 
+     Bucket: example-com-bucket
+     Name: example-com-bucket-ap-for-account-b
+     Policy:
+       Version: "2012-10-17"
+       Statement: 
+         - Sid: AccountBAccess
+           Effect: "Allow"
+           Principal: 
+             AWS: arn:aws:iam::BBBBBBBBBBBB:root
+           Action: s3:GetObject
+           Resource: "arn:aws:s3:::example-com-bucket/*

このようにすることで将来的に変更を行う場合にも変更セットを作成し確認することで意図しないリソースに対する変更がないかをチェックできますし、また作り次第ではスタックを別にすることで新規に追加する場合コードの変更ではなくスタックの追加・削除で対応できるため自動化しやすいという面も良さそうです。

今回試すこと

このアクセスポイントはAthenaでクエリをかける際の接続先としてもして可能ですので今回はこのアクセスポイントを経由してAthenaでクエリを行ってみます。

同一アカウントからアクセスを行うと操作元のIAMエンティティとリソースベースのポリシーどちらの許可で通っているのか分かりづらいため、両方の許可が必要なクロスアカウントアクセスで実施します。最初に記載した図のような形です。

データは以前より取得しているDMARCレポートを格納しているバケットがあるのでこちらに対してアクセスポイントを拡張しクエリをかけます。

接続先アカウントの作業

バケットを保持しているアカウント側の作業となります。

ポリシー次第では対向側のアカウントでアクセスポイントを作ることも可能ですが、サービスを提供している側が外部アカウントに対して別の口を用意するというケースを想定しアクセスポイントもこちらのアカウントで作成します。

アクセスポイントの作成

以下のようなテンプレートで作成します。TargetBucketNameには上記記載の記事で作成したバケット名、ExternalAccountIdには接続元となるアカウントIDを指定します。

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  TargetBucketName:
    Type: String
  ExternalAccountId:
    Type: String
Resources:
  S3AccessPoint:
    Type: AWS::S3::AccessPoint
    Properties:
      Bucket: !Ref TargetBucketName
      #NOTE: バケット名をNameに含めたいがAPの命名規則上"."の文字が使えずSplit&Join等で加工しないといけないので一旦ベタでわかりやすい命名を利用
      #      https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/creating-access-points.html?icmpid=docs_amazons3_console#access-points-names
      Name: !Sub dmarc-bucket-for-${ExternalAccountId}-ap
      Policy:
        Version: "2012-10-17"
        Statement:
          - Sid: AllowCrossAccountGet
            Effect: "Allow"
            Principal:
              AWS: !Ref ExternalAccountId
            Action:
              - s3:Get*
              - s3:List*
            Resource: 
              - !Sub "arn:aws:s3:${AWS::Region}:${AWS::AccountId}:accesspoint/dmarc-bucket-for-${ExternalAccountId}-ap"
              - !Sub "arn:aws:s3:${AWS::Region}:${AWS::AccountId}:accesspoint/dmarc-bucket-for-${ExternalAccountId}-ap/object/output/*"

注意点としてアクセスポイントを通じバケット内のリソースへのアクセスの許可の際にResoueceに指定する値としては{{アクセスポイントのARN}}/{{バケット内のパスプレフィックス}}ではなく間に/object/を挟んだ{{アクセスポイントのARN}}/object/{{バケット内のパスプレフィックス}}とする必要がある点です。以下のドキュメントに例がありますのでこちらも合わせてご参照ください。

作成するとマネジメントコンソールのS3のサービスの画面「アクセスポイント」から確認できます。

この情報のうち「アクセスポイントエイリアス」の値は後ほど使うので控えておきます。

バケットポリシー

アクセスポイント側で権限管理できるようになる...とは言ったもののアクセスポイント側に権限管理を委任するためのバケットポリシー自体は必要となります。

今回は以下のようなポリシーを追加してます。

{
    "Version": "2012-10-17",
    "Statement" : [{
        "Effect": "Allow",
        "Principal" : {
            "AWS": "*"
        },
        "Action" : [
            "s3:GetObject",
            "s3:ListBucket"
        ],
        "Resource" : [
             "arn:aws:s3:::rua.example.com-mail-stocker",
             "arn:aws:s3:::rua.example.com-mail-stocker/*"
        ],
        "Condition": {
            "StringEquals" : {
                "s3:DataAccessPointAccount" : "{{MyAccountID}}"
            }
        }
    }]
}

このポリシーは自身のアカウントのアクセスポイントを経由したs3:GetObjectおよびs3:ListBucketのアクセスを外部アカウントの操作含め全て許可するというものになり、この2操作に関しては同アカウントのアクセスポイント側に完全に任せてしまう設定となります。こうすることで以降都度都度バケット側のポリシーの変更が不要となります。

利用用途によっては完全に制御をアクセスポイント側に委任せずバケットポリシーと二重で管理しても良いですが、クロスアカウントであれば通常のバケットポリシーと同じようにIAMポリシー+リソースベースポリシー(アクセスポイント側)の制御がすでに入っている状態ですので必ずしもガチガチに制限をかけるものではないと個人的には考えています。

最初に記載したように都度バケットポリシーに追記する形をとると既存の設定の変更という形を取らざるを得ないので用途や運用負荷等を加味し必要に絞り込んでみてください。

とは言いつつも少なくとも今回は外部から書き込みはされたくないのでガードレールとしてs3:GetObjectおよびs3:ListBucketのみを許可しています。

考えようによってはアクセスポイントを噛ませることで重要度の高いデータの許可には多重の許可が必要といった安全機構にはなるのでしょうか?

接続元アカウント作業

アクセスポイント以降の準備ができましたので、実際にクエリをかける別アカウントの設定を行っていきます

接続元のIAMポリシーの追加

クロスアカウントアクセスの場合はリソース側のポリシーのみではなく接続元のIAMエンティティに対しても許可が必要となるため以下のようなポリシーを追加します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:ap-northeast-1:xxxxxxxxxxxx:accesspoint/dmarc-bucket-for-xxxxx-ap",
                "arn:aws:s3:ap-northeast-1:xxxxxxxxxxxx:accesspoint/dmarc-bucket-for-xxxxx-ap/object/output/*"
            ]
        }
    ]
}

テーブルの作成

S3バケットの場合LOCATIONとしてs3://{{バケット名}}/{{プレフィックス}}/を指定しますが、アクセスポイントの場合は先ほど作成したアクセスポイントエイリアスを利用しs3://{{アクセスポイントエイリアス}}/{{プレフィックス}}/のように記載します。
アクセスポイントポリシーの指定の際にはプレフィックスの前に/object/を挟む必要がありましたがこちらの指定では不要です。

データの関係上クエリが長いですで違いとしてはLOCATIONの指定がS3のアクセスポイント指定になっている部分以外はS3を直接参照する場合と違いはありません。

CREATE EXTERNAL TABLE IF NOT EXISTS dmarc_report_from_ap (
  feedback struct<
    report_metadata: struct<
      org_name: string,
      email: string,
      extra_contact_info: string,
      report_id: string,
      date_range: struct<
        begin: string,
        `end`: string
      >
    >,
    policy_published: struct<
      domain: string,
      adkim: string,
      aspf: string,
      p: string,
      sp: string,
      pct: string,
      np: string
    >,
    record: array<struct<
      `row`: struct<
        source_ip: string,
        `count`: string,
        policy_evaluated: struct<
          disposition: string,
          dkim: string,
          spf: string
        >
      >,
      identifiers: struct<
        header_from: string
      >,
      auth_results: struct<
        spf: struct<
          domain: string,
          result: string
        >,
        dkim: array<
          struct<
            domain: string,
            result: string,
            selector: string
          >
        >
      >>
    >
  >
 )
ROW FORMAT SERDE 'org.apache.hive.hcatalog.data.JsonSerDe'
WITH SERDEPROPERTIES ('paths'='feedback')
LOCATION 's3://dmarc-bucket-for-xxxxxxxxxx-s3alias/output/catcher/';

検索実行

上記のテーブルに対してSELECTを実行するとS3からのデータが得られていることが確認できます。

終わりに

S3アクセスポイントの利用例としてAthenaによる検索を行ってみました。

アクセスポイントを作ってさえしまえばクエリ実行側のアカウント側としてはResourceの許可先がバケットではなくアクセスポイントになったりと細かな違いはあるものの大枠としてはS3バケットに直接クエリをかける時と同じ感覚で操作可能で特別意識することがないのが嬉しいところです。

提供側のメリットが特に大きそうで、特別処理を組み込まなくても権限の追加削除がスタック単位で制御できるので自動化しやすいことに加え、権限のみではなくアクセスの口自体が削除時に消失するというのが個人的には良い感触です(エイリアスにはランダムな文字列?があるため再作成しても衝突しない限りは別名になる)。
とは言いつつも小規模な場合は逆に付随リソースが増え管理が煩雑になる可能性もあるのでアクセス元が大量にある、許可の増減が激しいような環境でなければ引き続きバケットポリシーを使っていくのが良さそうです。

CloudFormationであればベースのテンプレートを用意しておきアプリ側からCreateStack/DeleteStackを実行し自動的にアクセスポイントの追加削除、AWS APIの呼び出しのためStep Functionsと組み合わせて承認フローをコードレスで実装ということもできそうで面白そうなところではあります。