DuckDBでS3にアクセスする時の認証周りについて確認してみた
こんにちは、なおにしです。
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パラメータの記載がありませんでした)
実際にやってみたところ確かに機能しました。アカウントAのファイルにはアクセスできず、アカウントBのファイルを取得できています。
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
ファイルも参照されずにアクセスできなくなりました。
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
CHAIN
にenv
を指定しても同様です。
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でアクセスキーを利用したログインを試行すると、以下のような解説画面が表示されます。
推奨に記載のとおりの方法で設定してみます。
$ 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時間」です。詳細については以下の記事もご参照ください。
CHAIN
にsso
、PROFILE
に設定したプロファイルを指定したところ機能しました。
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パラメータだけで対象のプロファイルを指定してみましたが機能しませんでした。
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
だけ指定しても機能しませんでした。
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
CHAIN
にsts
を指定した場合
アカウントCへのアクセスはスイッチロールによるものなので、内部的にはsts:AssumeRole
が実行されます。AssumeRoleといえば以下の記事をご参照ください。
結果として得られる以下のクレデンシャルをDuckDBでそれぞれ指定可能です。
- アクセスキーID
- シークレットアクセスキー
- セッショントークン
上記の値は具体的には以下のようにAWS CLIを実行することで取得可能です。なお、取得したクレデンシャルのデフォルトの有効期限は「1時間」です。IAM Identity Centerとは異なることにご注意ください。詳細は以下の記事もご参照ください。
$ aws configure export-credentials --profile account-c
{
"Version": 1,
"AccessKeyId": "(アクセスキーID)",
"SecretAccessKey": "(シークレットアクセスキー)",
"SessionToken": "(トークン文字列)",
"Expiration": "2025-03-13T19:10:19+00:00"
}
以下のように設定したところ、問題なくアカウントCからファイルを取得できました。
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をしてくれるといった挙動もありませんでした。
CHAIN
にenv
を指定した場合
上記の方法では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
上記が適用された状態でCHAIN
にenv
を指定した場合、問題なく機能しました。
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
なお、環境変数が設定されていればCHAIN
にenv
を指定していなくても環境変数が優先されるようです。ですが、ドキュメントにはenv;config
を指定する記載もありますので、明示しておいた方が良いかと思います。
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
CHAIN
にprocess
を指定した場合
こちらについてはどのように使用すれば良いか結局分かりませんでした。COMMAND
のようなパラメータも無さそうなので、どのようにクレデンシャルをDuckDBに渡せば良いのか分かりませんが、スイッチロールをしたいということであれば前述の方法が使えるのでprocess
を無理に使う必要はないかと思います。
最初は以下の記事に記載されているTerraformと似たような挙動をイメージしていたので、.aws/config
を同様に設定してみたりもしたのですが機能しませんでした。
インスタンスプロファイルを使用したアクセス
こちらは対象のS3バケットにアクセス可能なインスタンスプロファイルをアタッチしたEC2での操作となります。
アカウントAで起動したEC2にDuckDBをインストールして試したところ、問題なくファイルの中身を取得できました。
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
なお、CHAIN
にprocess
を指定しなくても問題なくS3バケットにアクセスできました。この挙動は以下の記事のようにCloudShellからアクセスした場合と同様です。
CloudShellを使えばEC2を準備する必要もないためより手軽にDuckDBを使用できますが、記事にも記載されているとおりS3に格納されている対象ファイルが大きい場合などにEC2を準備しても良いかと思います。
S3の料金ページに記載のとおり以下に該当するデータ転送は無料のため、EC2の維持費はかかるものの、特にS3へのデータの読み書きが多い場合はローカル環境ではなくあえてEC2にDuckDBをインストールすることも有用かと思います。
Amazon S3 バケットから S3 バケットと同じ AWS リージョン内の任意の AWS のサービスに転送されたデータ (同じ AWS リージョン内の別のアカウントに転送されたデータを含む)。
まとめ
DuckDBからS3へのアクセス方法についてまとめてみました。本記事はあくまでCLIでduckdbコマンドを使用する前提のため、各種クライアントAPIからアクセスする際は別の工夫が必要になるかもしれません。
本記事がどなたかのお役に立てれば幸いです。