DuckDBでS3にアクセスする時の認証周りについて確認してみた

DuckDBでS3にアクセスする時の認証周りについて確認してみた

Clock Icon2025.03.13

こんにちは、なおにしです。

DuckDBを使用してS3バケットにアクセスする方法について複数パターンを検討する機会がありましたのでご紹介します。

はじめに

前回の記事でDuckDBを使用してS3に格納されているログに対してクエリを実行する方法をご紹介しました。
その際にDuckDBの拡張機能「httpfs」を使用しましたが、記載の方法の場合は.aws/credentialsに設定されているdefaultプロファイルが使用されます。

アクセスキーIDおよびシークレットアクセスキーが発行されており、かつそれをdefaultプロファイルとして設定している状況であれば特に不都合ありませんが、状況によっては以下のようなケースで対象のAWSアカウントにあるS3バケットにアクセスしたいこともあるかと思います。

  • configファイル内のdefaultプロファイル以外を指定したアクセス
  • IAM Identity CenterのSSO認証情報を使用したアクセス
  • 特定のAWSアカウントを経由したスイッチロールによるアクセス
  • インスタンスプロファイルを使用したアクセス

DuckDBのドキュメントには、CREDENTIAL_CHAINプロバイダーを使用するとAWS SDK によって提供されるメカニズムを使用して認証情報を自動的に取得できるとあり、CHAINとして設定可能な値は以下のとおりであると記載されています。

  • config
  • sts
  • sso
  • env
  • instance
  • process

それぞれの値にはリンクが貼られていますが、AWS SDK for C++のクラスリファレンスに飛ばされるだけで具体的な使用方法が分からなかったため、前述のケース毎に実際にアクセス可能か確認してみます。

やってみた

前提

以下の3アカウントにある各S3バケットに配置したファイルにアクセスします。

  • アカウントA:
    • ローカル端末の.aws/credentialsに記載の[default]プロファイルからアクセスキーID/シークレットアクセスキーを使用してアクセス可能
  • アカウントB:
    • ローカル端末の.aws/credentialsに記載の[account-b]プロファイルからアクセスキーID/シークレットアクセスキーを使用してアクセス可能
    • IAM Identity Center経由でのアクセスが可能
  • アカウントC:
    • ローカル端末の.aws/configに記載の[account-c]プロファイルからスイッチロールを使用してアクセス可能
      • [account-c]プロファイルのソースプロファイル(source_profile)にはアカウントAを指定

アカウントBとCまで分ける必要はないかとも思ったのですが、意図したCHAINでアクセスできているかを判別しやすくするために今回はそれぞれ分けてみました。

また、各アカウントの各S3バケットに配置したファイルは以下のように作成しました。

$ echo "アカウントA" | jq -R 'split(",") | {bucket: .[0]}' | gzip > accountA.gz
$ gzcat accountA.gz                                                                                   
{
  "bucket": "アカウントA"
}

configファイル内のdefaultプロファイル以外を指定したアクセス

以下のように特に何もパラメータを指定せずに「PROVIDER credential_chain」のみを指定した場合、ドキュメントに記載のとおりデフォルトのプロバイダーが指定されます。つまり、AWS CLIを設定している環境であれば.aws/credentialsの[default]プロファイルが適用されます。

D CREATE SECRET secret_a (
      TYPE s3,
      PROVIDER credential_chain
  );
┌─────────┐
│ Success │
│ boolean │
├─────────┤
│ true    │
└─────────┘
D SELECT *
    FROM read_json_auto('s3://duckdb-test-naonishi-a/accountA.gz');
┌─────────────┐
│   bucket    │
│   varchar   │
├─────────────┤
│ アカウントA │
└─────────────┘
D 

同様にドキュメントにあるとおり直接アクセスキーIDとシークレットアクセスキーを指定することで別のアカウントのS3バケットにアクセスすることも可能です。

D CREATE SECRET secret_b (
      TYPE s3,
      KEY_ID '(アクセスキーID)',
      SECRET '(シークレットアクセスキー)',
      REGION 'ap-northeast-1'
  );
┌─────────┐
│ Success │
│ boolean │
├─────────┤
│ true    │
└─────────┘
D 
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-a/accountA.gz');
HTTP Error:
HTTP GET error on 'https://duckdb-test-naonishi-a.s3.amazonaws.com/accountA.gz' (HTTP 403)
D 
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-b/accountB.gz');
┌─────────────┐
│   bucket    │
│   varchar   │
├─────────────┤
│ アカウントB │
└─────────────┘
D 

ですが、既に.aws/credentialsにプロファイルを記載している状態であれば、クエリでアクセスキーID/シークレットアクセスキーを都度指定するのではなくプロファイルを指定して実行できる方が楽なので確認したところ、GitHubの以下のやり取りでプロファイルの指定は可能であることが分かりました。(執筆時点ではドキュメントにはPROFILEパラメータの記載がありませんでした)

https://github.com/duckdb/duckdb/discussions/10696

実際にやってみたところ確かに機能しました。アカウントAのファイルにはアクセスできず、アカウントBのファイルを取得できています。

OKパターン
D CREATE SECRET secret_b (
        TYPE s3,
        PROVIDER credential_chain,
        PROFILE 'account-b'
  );
┌─────────┐
│ Success │
│ boolean │
├─────────┤
│ true    │
└─────────┘
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-a/accountA.gz');
HTTP Error:
HTTP GET error on 'https://duckdb-test-naonishi-a.s3.amazonaws.com/accountA.gz' (HTTP 403)
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-b/accountB.gz');
┌─────────────┐
│   bucket    │
│   varchar   │
├─────────────┤
│ アカウントB │
└─────────────┘
D

なお、環境変数AWS_DEFAULT_PROFILEを設定しても機能しませんでした。

$ export AWS_DEFAULT_PROFILE=account-c
$ env | grep AWS                            
AWS_DEFAULT_PROFILE=account-c

CHAINを指定しない場合、どういうわけか~/.aws/credentialsファイルも参照されずにアクセスできなくなりました。

NGパターン
D CREATE SECRET secret_b (
            TYPE s3,
            PROVIDER credential_chain
      );
100% ▕████████████████████████████████████████████████████████████▏ 
┌─────────┐
│ Success │
│ boolean │
├─────────┤
│ true    │
└─────────┘
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-a/accountA.gz');
HTTP Error:
HTTP GET error on 'https://duckdb-test-naonishi-a.s3.amazonaws.com/accountA.gz' (HTTP 400)
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-b/accountB.gz');
HTTP Error:
HTTP GET error on 'https://duckdb-test-naonishi-b.s3.amazonaws.com/accountB.gz' (HTTP 400)
D 

CHAINenvを指定しても同様です。

NGパターン
D CREATE SECRET secret_b (
            TYPE s3,
            PROVIDER credential_chain,
            CHAIN 'env'
      );
100% ▕████████████████████████████████████████████████████████████▏ 
┌─────────┐
│ Success │
│ boolean │
├─────────┤
│ true    │
└─────────┘
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-a/accountA.gz');
HTTP Error:
HTTP GET error on 'https://duckdb-test-naonishi-a.s3.amazonaws.com/accountA.gz' (HTTP 403)
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-b/accountB.gz');
HTTP Error:
HTTP GET error on 'https://duckdb-test-naonishi-b.s3.amazonaws.com/accountB.gz' (HTTP 403)
D 

IAM Identity CenterのSSO認証情報を使用したアクセス

IAM Identity CenterのSSO認証情報を使用して接続することも可能です。DuckDB内でSSOのURLを指定するパラメータ等はなかったため、AWS CLIで事前にログインしてプロファイルを生成する形になります。

IAM Identity Centerでアクセスキーを利用したログインを試行すると、以下のような解説画面が表示されます。

20250313_naonishi_duckdb-s3-authentication-methods_1

推奨に記載のとおりの方法で設定してみます。

$ aws configure sso
SSO session name (Recommended): duckdb-test
SSO start URL [None]: https://(インスタンス名).awsapps.com/start/#
SSO region [None]: ap-northeast-1
SSO registration scopes [sso:account:access]:
Attempting to automatically open the SSO authorization page in your default browser.
If the browser does not open or you wish to use a different device to authorize this request, open the following URL:

https://oidc.ap-northeast-1.amazonaws.com/authorize?response_type=code&client_id=iEXaWKNAuq2uGIq9YGNuAzFwLW5vcnRoZWFzdC0x&redirect_uri=httpxxxxxxxxxx&code_challenge_method=S256&scopes=sso%3Aaccount%3Aaccess&code_challenge=Ha3uxfj4yfqHDXqVZm-ckmd4PoaZ-quwYItoBHuUdlA
There are 4 AWS accounts available to you.
Using the account ID 123456789012
The only role available to you is: AdministratorAccess
Using the role name "AdministratorAccess"
CLI default client Region [ap-northeast-1]:
CLI default output format [json]:
CLI profile name [AdministratorAccess-123456789012]: sso-duckdb-test

To use this profile, specify the profile name using --profile, as shown:

aws s3 ls --profile sso-duckdb-test

$ tail ~/.aws/config
[profile sso-duckdb-test]
sso_session = duckdb-test
sso_account_id = 123456789012
sso_role_name = AdministratorAccess
region = ap-northeast-1
output = json
[sso-session duckdb-test]
sso_start_url = https://(インスタンス名).awsapps.com/start/#
sso_region = ap-northeast-1
sso_registration_scopes = sso:account:access

実際に設定する流れは上記のとおりです。これでプロファイルとして「sso-duckdb-test」が使用できるようになりました。こちらのプロファイルが利用できる期間、つまりセッションの有効期限はデフォルトで「8時間」です。詳細については以下の記事もご参照ください。

https://dev.classmethod.jp/articles/aws-cli-for-iam-identity-center-sso/

CHAINssoPROFILEに設定したプロファイルを指定したところ機能しました。

OKパターン
D CREATE SECRET secret_b (
            TYPE s3,
            PROVIDER credential_chain,
            CHAIN 'sso',
            PROFILE 'sso-duckdb-test'
      );
┌─────────┐
│ Success │
│ boolean │
├─────────┤
│ true    │
└─────────┘
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-a/accountA.gz');
HTTP Error:
HTTP GET error on 'https://duckdb-test-naonishi-a.s3.amazonaws.com/accountA.gz' (HTTP 403)
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-b/accountB.gz');
┌─────────────┐
│   bucket    │
│   varchar   │
├─────────────┤
│ アカウントB │
└─────────────┘
D 

なお、結局は.aws/configにプロファイルが追記されていますので、試しにPROFILEパラメータだけで対象のプロファイルを指定してみましたが機能しませんでした。

NGパターン
D CREATE SECRET secret_b (
        TYPE s3,
        PROVIDER credential_chain,
        PROFILE 'sso-duckdb-test'
    );
┌─────────┐
│ Success │
│ boolean │
├─────────┤
│ true    │
└─────────┘
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-a/accountA.gz');
HTTP Error:
HTTP GET error on 'https://duckdb-test-naonishi-a.s3.amazonaws.com/accountA.gz' (HTTP 403)
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-b/accountB.gz');
HTTP Error:
HTTP GET error on 'https://duckdb-test-naonishi-b.s3.amazonaws.com/accountB.gz' (HTTP 403)
D 

また、CHAINにssoだけ指定しても機能しませんでした。

NGパターン
D CREATE SECRET secret_b (
            TYPE s3,
            PROVIDER credential_chain,
            CHAIN 'sso'
      );
┌─────────┐
│ Success │
│ boolean │
├─────────┤
│ true    │
└─────────┘
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-a/accountA.gz');
HTTP Error:
HTTP GET error on 'https://duckdb-test-naonishi-a.s3.amazonaws.com/accountA.gz' (HTTP 403)
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-b/accountB.gz');
HTTP Error:
HTTP GET error on 'https://duckdb-test-naonishi-b.s3.amazonaws.com/accountB.gz' (HTTP 403)
D 

特定のAWSアカウントを経由したスイッチロールによるアクセス

本ケースで活用できるCHAINの値は以下の3つかと思いますので、順番に確認してみました。

  • sts
  • env
  • process

CHAINstsを指定した場合

アカウントCへのアクセスはスイッチロールによるものなので、内部的にはsts:AssumeRoleが実行されます。AssumeRoleといえば以下の記事をご参照ください。

https://dev.classmethod.jp/articles/iam-role-passrole-assumerole/

結果として得られる以下のクレデンシャルをDuckDBでそれぞれ指定可能です。

  • アクセスキーID
  • シークレットアクセスキー
  • セッショントークン

上記の値は具体的には以下のようにAWS CLIを実行することで取得可能です。なお、取得したクレデンシャルのデフォルトの有効期限は「1時間」です。IAM Identity Centerとは異なることにご注意ください。詳細は以下の記事もご参照ください。

https://dev.classmethod.jp/articles/understanding-iam-role-and-switch-role/

$ aws configure export-credentials --profile account-c
{
  "Version": 1,
  "AccessKeyId": "(アクセスキーID)",
  "SecretAccessKey": "(シークレットアクセスキー)",
  "SessionToken": "(トークン文字列)",
  "Expiration": "2025-03-13T19:10:19+00:00"
}

以下のように設定したところ、問題なくアカウントCからファイルを取得できました。

OKパターン
D CREATE SECRET secret_c (
            TYPE s3,
            PROVIDER credential_chain,
            CHAIN 'sts',
            KEY_ID '(アクセスキーID)',
            SECRET '(シークレットアクセスキー)',
            SESSION_TOKEN '(トークン文字列)'
      );
┌─────────┐
│ Success │
│ boolean │
├─────────┤
│ true    │
└─────────┘
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-a/accountA.gz');
HTTP Error:
HTTP GET error on 'https://duckdb-test-naonishi-a.s3.amazonaws.com/accountA.gz' (HTTP 403)
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-b/accountB.gz');
HTTP Error:
HTTP GET error on 'https://duckdb-test-naonishi-b.s3.amazonaws.com/accountB.gz' (HTTP 403)
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-c/accountC.gz');
┌─────────────┐
│   bucket    │
│   varchar   │
├─────────────┤
│ アカウントC │
└─────────────┘
D 

なお、上記についてはCHAINとしてstsを指定しなくても同様の挙動となっていました。SESSION_TOKENを使用しているからstsを指定しないと、というわけではないようです。
また、stsを指定した状態でPROFILEを指定したりも試しましたが、内部でAssumeRoleをしてくれるといった挙動もありませんでした。

CHAINenvを指定した場合

上記の方法では3つのクレデンシャルを個別に指定する必要がありますが、環境変数を使用すればより簡単にスイッチロール先にアクセス可能です。
先ほどのAWS CLIのコマンドconfigure export-credentialsは以下のように環境変数の形式で出力することができます。

$ aws configure export-credentials --format env --profile account-c
export AWS_ACCESS_KEY_ID=(アクセスキーID)
export AWS_SECRET_ACCESS_KEY=(シークレットアクセスキー)
export AWS_SESSION_TOKEN=(トークン文字列)
export AWS_CREDENTIAL_EXPIRATION=2025-03-13T19:10:19+00:00

このため、以下のように1度コマンドを打つだけで環境変数として設定可能です。

$ env | grep AWS
$ `aws configure export-credentials --format env --profile account-c`
$ env | grep AWS
AWS_ACCESS_KEY_ID=(アクセスキーID)
AWS_SECRET_ACCESS_KEY=(シークレットアクセスキー)
AWS_SESSION_TOKEN=(トークン文字列)
AWS_CREDENTIAL_EXPIRATION=2025-03-13T19:10:19+00:00

上記が適用された状態でCHAINenvを指定した場合、問題なく機能しました。

OKパターン
D CREATE SECRET secret_c (
            TYPE s3,
            PROVIDER credential_chain,
            CHAIN 'env'
      );
┌─────────┐
│ Success │
│ boolean │
├─────────┤
│ true    │
└─────────┘
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-a/accountA.gz');
HTTP Error:
HTTP GET error on 'https://duckdb-test-naonishi-a.s3.amazonaws.com/accountA.gz' (HTTP 403)
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-b/accountB.gz');
HTTP Error:
HTTP GET error on 'https://duckdb-test-naonishi-b.s3.amazonaws.com/accountB.gz' (HTTP 403)
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-c/accountC.gz');
┌─────────────┐
│   bucket    │
│   varchar   │
├─────────────┤
│ アカウントC │
└─────────────┘
D 

なお、環境変数が設定されていればCHAINenvを指定していなくても環境変数が優先されるようです。ですが、ドキュメントにはenv;configを指定する記載もありますので、明示しておいた方が良いかと思います。

OKパターン
D CREATE SECRET secret_c (
            TYPE s3,
            PROVIDER credential_chain
      );
┌─────────┐
│ Success │
│ boolean │
├─────────┤
│ true    │
└─────────┘
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-a/accountA.gz');
HTTP Error:
HTTP GET error on 'https://duckdb-test-naonishi-a.s3.amazonaws.com/accountA.gz' (HTTP 403)
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-b/accountB.gz');
HTTP Error:
HTTP GET error on 'https://duckdb-test-naonishi-b.s3.amazonaws.com/accountB.gz' (HTTP 403)
D SELECT *
      FROM read_json_auto('s3://duckdb-test-naonishi-c/accountC.gz');
┌─────────────┐
│   bucket    │
│   varchar   │
├─────────────┤
│ アカウントC │
└─────────────┘
D 

CHAINprocessを指定した場合

こちらについてはどのように使用すれば良いか結局分かりませんでした。COMMANDのようなパラメータも無さそうなので、どのようにクレデンシャルをDuckDBに渡せば良いのか分かりませんが、スイッチロールをしたいということであれば前述の方法が使えるのでprocessを無理に使う必要はないかと思います。

最初は以下の記事に記載されているTerraformと似たような挙動をイメージしていたので、.aws/configを同様に設定してみたりもしたのですが機能しませんでした。

https://dev.classmethod.jp/articles/terraform-mfa-assumerole-export-credentials/

インスタンスプロファイルを使用したアクセス

こちらは対象のS3バケットにアクセス可能なインスタンスプロファイルをアタッチしたEC2での操作となります。
アカウントAで起動したEC2にDuckDBをインストールして試したところ、問題なくファイルの中身を取得できました。

OKパターン
D CREATE SECRET secret_a (
              TYPE s3,
              PROVIDER credential_chain,
              CHAIN 'instance'
        );
┌─────────┐
│ Success │
│ boolean │
├─────────┤
│ true    │
└─────────┘
D SELECT *
        FROM read_json_auto('s3://duckdb-test-naonishi-a/accountA.gz');
┌─────────────┐
│   bucket    │
│   varchar   │
├─────────────┤
│ アカウントA │
└─────────────┘
D 

なお、CHAINprocessを指定しなくても問題なくS3バケットにアクセスできました。この挙動は以下の記事のようにCloudShellからアクセスした場合と同様です。

https://dev.classmethod.jp/articles/duckdb-s3-sql-from-aws-cloudshell/

CloudShellを使えばEC2を準備する必要もないためより手軽にDuckDBを使用できますが、記事にも記載されているとおりS3に格納されている対象ファイルが大きい場合などにEC2を準備しても良いかと思います。

S3の料金ページに記載のとおり以下に該当するデータ転送は無料のため、EC2の維持費はかかるものの、特にS3へのデータの読み書きが多い場合はローカル環境ではなくあえてEC2にDuckDBをインストールすることも有用かと思います。

Amazon S3 バケットから S3 バケットと同じ AWS リージョン内の任意の AWS のサービスに転送されたデータ (同じ AWS リージョン内の別のアカウントに転送されたデータを含む)。

まとめ

DuckDBからS3へのアクセス方法についてまとめてみました。本記事はあくまでCLIでduckdbコマンドを使用する前提のため、各種クライアントAPIからアクセスする際は別の工夫が必要になるかもしれません。

本記事がどなたかのお役に立てれば幸いです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.