Skip to content

Commit 7a946f8

Browse files
KSDaemonmorford-brex
authored andcommitted
fix(snowflake-driver): Add support for IAM roles with IRSA for S3 export buckets (#10024)
* make keyId and secretKey optional for s3 * fix handling * lint * update docs * update update-all-snapshots-local * add driver test to gh workflow * address comments * revert docs changes * update validation logic * update comments * fix storage integaration name * Configure AWS credentials via IRSA * add permissions * code format * uppercase storage integration * Pass AWS envs to docker * add tests snapshot --------- Co-authored-by: Matthew Orford <[email protected]>
1 parent 1f968ab commit 7a946f8

File tree

8 files changed

+18158
-24
lines changed

8 files changed

+18158
-24
lines changed

.github/workflows/drivers-tests.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,10 @@ jobs:
204204

205205
tests:
206206
runs-on: ubuntu-24.04
207+
permissions:
208+
id-token: write # Needed for OIDC+AWS
209+
contents: read
210+
207211
timeout-minutes: 30
208212
needs: [latest-tag-sha, build]
209213
if: (needs['latest-tag-sha'].outputs.sha != github.sha)
@@ -225,6 +229,7 @@ jobs:
225229
snowflake
226230
snowflake-encrypted-pk
227231
snowflake-export-bucket-s3
232+
snowflake-export-bucket-s3-via-storage-integration-iam-roles
228233
snowflake-export-bucket-s3-prefix
229234
snowflake-export-bucket-azure
230235
snowflake-export-bucket-azure-prefix
@@ -259,6 +264,7 @@ jobs:
259264
- snowflake
260265
- snowflake-encrypted-pk
261266
- snowflake-export-bucket-s3
267+
- snowflake-export-bucket-s3-via-storage-integration-iam-roles
262268
- snowflake-export-bucket-s3-prefix
263269
- snowflake-export-bucket-azure
264270
- snowflake-export-bucket-azure-prefix
@@ -338,6 +344,15 @@ jobs:
338344
gunzip image.tar.gz
339345
docker load -i image.tar
340346
347+
- name: Configure AWS credentials via IRSA
348+
uses: aws-actions/configure-aws-credentials@v4
349+
with:
350+
role-to-assume: ${{ secrets.DRIVERS_TESTS_AWS_ROLE_ARN_FOR_SNOWFLAKE }}
351+
aws-region: us-west-1
352+
mask-aws-account-id: true
353+
if: |
354+
env.DRIVERS_TESTS_ATHENA_CUBEJS_AWS_KEY != '' && matrix.database == 'snowflake-export-bucket-s3-via-storage-integration-iam-roles'
355+
341356
- name: Run tests
342357
uses: nick-fields/retry@v3
343358
# It's enough to test for any one secret because they are set all at once or not set all

docs/pages/product/configuration/data-sources/snowflake.mdx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -133,15 +133,13 @@ Storage][google-cloud-storage] for export bucket functionality.
133133

134134
<InfoBox>
135135

136-
Ensure the AWS credentials are correctly configured in IAM to allow reads and
137-
writes to the export bucket in S3 if you are not using storage integration.
138-
If you are using storage integration then you still need to configure access keys
139-
for Cube Store to be able to read from the export bucket.
140-
It's possible to authenticate with IAM roles instead of access keys for Cube Store.
136+
Ensure proper IAM privileges are configured for S3 bucket reads and writes, using either
137+
storage integration or user credentials for Snowflake and either IAM roles/IRSA or user
138+
credentials for Cube Store, with mixed configurations supported.
141139

142140
</InfoBox>
143141

144-
Using IAM user credentials:
142+
Using IAM user credentials for both:
145143

146144
```dotenv
147145
CUBEJS_DB_EXPORT_BUCKET_TYPE=s3
@@ -151,8 +149,8 @@ CUBEJS_DB_EXPORT_BUCKET_AWS_SECRET=<AWS_SECRET>
151149
CUBEJS_DB_EXPORT_BUCKET_AWS_REGION=<AWS_REGION>
152150
```
153151

154-
[Using Storage Integration][snowflake-docs-aws-integration] to write to Export Bucket and
155-
then Access Keys to read from Cube Store:
152+
Using a [Storage Integration][snowflake-docs-aws-integration] to write to export buckets and
153+
user credentials to read from Cube Store:
156154

157155
```dotenv
158156
CUBEJS_DB_EXPORT_BUCKET_TYPE=s3
@@ -163,7 +161,8 @@ CUBEJS_DB_EXPORT_BUCKET_AWS_SECRET=<AWS_SECRET>
163161
CUBEJS_DB_EXPORT_BUCKET_AWS_REGION=<AWS_REGION>
164162
```
165163

166-
Using Storage Integration to write to export bocket and IAM role to read from Cube Store:
164+
Using a Storage Integration to write to export bucket and IAM role/IRSA to read from Cube Store:**
165+
167166
```dotenv
168167
CUBEJS_DB_EXPORT_BUCKET_TYPE=s3
169168
CUBEJS_DB_EXPORT_BUCKET=my.bucket.on.s3

packages/cubejs-snowflake-driver/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"lint:fix": "eslint --fix src/* --ext .ts"
2626
},
2727
"dependencies": {
28+
"@aws-sdk/client-s3": "^3.726.0",
2829
"@cubejs-backend/base-driver": "1.3.77",
2930
"@cubejs-backend/shared": "1.3.77",
3031
"date-fns-timezone": "^0.1.4",

packages/cubejs-snowflake-driver/src/SnowflakeDriver.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import { formatToTimeZone } from 'date-fns-timezone';
2424
import fs from 'fs/promises';
2525
import crypto from 'crypto';
26+
import { S3ClientConfig } from '@aws-sdk/client-s3';
2627
import { HydrationMap, HydrationStream } from './HydrationStream';
2728

2829
const SUPPORTED_BUCKET_TYPES = ['s3', 'gcs', 'azure'];
@@ -106,8 +107,8 @@ const SnowflakeToGenericType: Record<string, GenericDataBaseType> = {
106107
interface SnowflakeDriverExportAWS {
107108
bucketType: 's3',
108109
bucketName: string,
109-
keyId: string,
110-
secretKey: string,
110+
keyId?: string,
111+
secretKey?: string,
111112
region: string,
112113
integrationName?: string,
113114
}
@@ -328,14 +329,17 @@ export class SnowflakeDriver extends BaseDriver implements DriverInterface {
328329
if (bucketType === 's3') {
329330
// integrationName is optional for s3
330331
const integrationName = getEnv('dbExportIntegration', { dataSource });
332+
// keyId and secretKey are optional for s3 if IAM role is used
333+
const keyId = getEnv('dbExportBucketAwsKey', { dataSource });
334+
const secretKey = getEnv('dbExportBucketAwsSecret', { dataSource });
331335

332336
return {
333337
bucketType,
334338
bucketName: getEnv('dbExportBucket', { dataSource }),
335-
keyId: getEnv('dbExportBucketAwsKey', { dataSource }),
336-
secretKey: getEnv('dbExportBucketAwsSecret', { dataSource }),
337339
region: getEnv('dbExportBucketAwsRegion', { dataSource }),
338340
...(integrationName !== undefined && { integrationName }),
341+
...(keyId !== undefined && { keyId }),
342+
...(secretKey !== undefined && { secretKey }),
339343
};
340344
}
341345

@@ -387,6 +391,20 @@ export class SnowflakeDriver extends BaseDriver implements DriverInterface {
387391
);
388392
}
389393

394+
private getRequiredExportBucketKeys(
395+
exportBucket: SnowflakeDriverExportBucket,
396+
emptyKeys: string[]
397+
): string[] {
398+
if (exportBucket.bucketType === 's3') {
399+
const s3Config = exportBucket as SnowflakeDriverExportAWS;
400+
if (s3Config.integrationName) {
401+
return emptyKeys.filter(key => key !== 'keyId' && key !== 'secretKey');
402+
}
403+
}
404+
405+
return emptyKeys;
406+
}
407+
390408
protected getExportBucket(
391409
dataSource: string,
392410
): SnowflakeDriverExportBucket | undefined {
@@ -402,9 +420,11 @@ export class SnowflakeDriver extends BaseDriver implements DriverInterface {
402420

403421
const emptyKeys = Object.keys(exportBucket)
404422
.filter((key: string) => exportBucket[<keyof SnowflakeDriverExportBucket>key] === undefined);
405-
if (emptyKeys.length) {
423+
const keysToValidate = this.getRequiredExportBucketKeys(exportBucket, emptyKeys);
424+
425+
if (keysToValidate.length) {
406426
throw new Error(
407-
`Unsupported configuration exportBucket, some configuration keys are empty: ${emptyKeys.join(',')}`
427+
`Unsupported configuration exportBucket, some configuration keys are empty: ${keysToValidate.join(',')}`
408428
);
409429
}
410430

@@ -731,7 +751,7 @@ export class SnowflakeDriver extends BaseDriver implements DriverInterface {
731751
// Storage integration export flow takes precedence over direct auth if it is defined
732752
if (conf.integrationName) {
733753
optionsToExport.STORAGE_INTEGRATION = conf.integrationName;
734-
} else {
754+
} else if (conf.keyId && conf.secretKey) {
735755
optionsToExport.CREDENTIALS = `(AWS_KEY_ID = '${conf.keyId}' AWS_SECRET_KEY = '${conf.secretKey}')`;
736756
}
737757
} else if (bucketType === 'gcs') {
@@ -771,14 +791,18 @@ export class SnowflakeDriver extends BaseDriver implements DriverInterface {
771791
const { bucketName, path } = this.parseBucketUrl(this.config.exportBucket!.bucketName);
772792
const exportPrefix = path ? `${path}/${tableName}` : tableName;
773793

794+
const s3Config: S3ClientConfig = { region };
795+
if (keyId && secretKey) {
796+
// If access key and secret are provided, use them as credentials
797+
// Otherwise, let the SDK use the default credential chain (IRSA, instance profile, etc.)
798+
s3Config.credentials = {
799+
accessKeyId: keyId,
800+
secretAccessKey: secretKey,
801+
};
802+
}
803+
774804
return this.extractUnloadedFilesFromS3(
775-
{
776-
credentials: {
777-
accessKeyId: keyId,
778-
secretAccessKey: secretKey,
779-
},
780-
region,
781-
},
805+
s3Config,
782806
bucketName,
783807
exportPrefix,
784808
);

packages/cubejs-testing-drivers/fixtures/snowflake.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@
2222
}
2323
}
2424
},
25+
"export-bucket-s3-via-storage-integration-iam-roles": {
26+
"cube": {
27+
"environment": {
28+
"CUBEJS_DB_EXPORT_BUCKET_TYPE": "s3",
29+
"CUBEJS_DB_EXPORT_BUCKET": "snowflake-drivers-tests-preaggs",
30+
"CUBEJS_DB_EXPORT_BUCKET_AWS_REGION": "us-west-1",
31+
"CUBEJS_DB_EXPORT_INTEGRATION": "DRIVERS_TESTS_PREAGGS_S3",
32+
"AWS_REGION": "us-west-1",
33+
"AWS_ACCESS_KEY_ID": "${AWS_ACCESS_KEY_ID}",
34+
"AWS_SECRET_ACCESS_KEY": "${AWS_SECRET_ACCESS_KEY}",
35+
"AWS_SESSION_TOKEN": "${AWS_SESSION_TOKEN}",
36+
"AWS_DEFAULT_REGION": "${AWS_DEFAULT_REGION}"
37+
}
38+
}
39+
},
2540
"export-bucket-azure": {
2641
"cube": {
2742
"environment": {

packages/cubejs-testing-drivers/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"snowflake-full": "yarn test-driver -i dist/test/snowflake-full.test.js",
5050
"snowflake-encrypted-pk-full": "yarn test-driver -i dist/test/snowflake-encrypted-pk-full.test.js",
5151
"snowflake-export-bucket-s3-full": "yarn test-driver -i dist/test/snowflake-export-bucket-s3-full.test.js",
52+
"snowflake-export-bucket-s3-via-storage-integration-iam-roles-full": "yarn test-driver -i dist/test/snowflake-export-bucket-s3-via-storage-integration-iam-roles-full.test.js",
5253
"snowflake-export-bucket-s3-prefix-full": "yarn test-driver -i dist/test/snowflake-export-bucket-s3-prefix-full.test.js",
5354
"snowflake-export-bucket-azure-full": "yarn test-driver -i dist/test/snowflake-export-bucket-azure-full.test.js",
5455
"snowflake-export-bucket-azure-prefix-full": "yarn test-driver -i dist/test/snowflake-export-bucket-azure-prefix-full.test.js",
@@ -59,7 +60,7 @@
5960
"redshift-core": "yarn test-driver -i dist/test/redshift-core.test.js",
6061
"redshift-full": "yarn test-driver -i dist/test/redshift-full.test.js",
6162
"redshift-export-bucket-s3-full": "yarn test-driver -i dist/test/redshift-export-bucket-s3-full.test.js",
62-
"update-all-snapshots-local": "yarn run athena-export-bucket-s3-full --mode=local -u; yarn run bigquery-export-bucket-gcs-full --mode=local -u; yarn run clickhouse-full --mode=local -u; yarn run clickhouse-export-bucket-s3-full --mode=local -u; yarn run clickhouse-export-bucket-s3-prefix-full --mode=local -u; yarn run databricks-jdbc-export-bucket-azure-full --mode=local -u; yarn run databricks-jdbc-export-bucket-azure-prefix-full --mode=local -u; yarn run databricks-jdbc-export-bucket-gcs-full --mode=local -u; yarn run databricks-jdbc-export-bucket-gcs-prefix-full --mode=local -u; yarn run databricks-jdbc-export-bucket-s3-full --mode=local -u; yarn run databricks-jdbc-export-bucket-s3-prefix-full --mode=local -u; yarn run databricks-jdbc-full --mode=local -u; yarn run mssql-full --mode=local -u; yarn run mysql-full --mode=local -u; yarn run postgres-full --mode=local -u; yarn run redshift-export-bucket-s3-full --mode=local -u; yarn run redshift-full --mode=local -u; yarn run snowflake-encrypted-pk-full --mode=local -u; yarn run snowflake-export-bucket-azure-full --mode=local -u; yarn run snowflake-export-bucket-azure-prefix-full --mode=local -u; yarn run snowflake-export-bucket-azure-via-storage-integration-full --mode=local -u; yarn run snowflake-export-bucket-gcs-full --mode=local -u; yarn run snowflake-export-bucket-gcs-prefix-full --mode=local -u; yarn run snowflake-export-bucket-s3-full --mode=local -u; yarn run snowflake-export-bucket-s3-prefix-full --mode=local -u; yarn run snowflake-export-bucket-azure-prefix-full --mode=local -u; yarn run snowflake-export-bucket-azure-full --mode=local -u; yarn run snowflake-full --mode=local -u",
63+
"update-all-snapshots-local": "yarn run athena-export-bucket-s3-full --mode=local -u; yarn run bigquery-export-bucket-gcs-full --mode=local -u; yarn run clickhouse-full --mode=local -u; yarn run clickhouse-export-bucket-s3-full --mode=local -u; yarn run clickhouse-export-bucket-s3-prefix-full --mode=local -u; yarn run databricks-jdbc-export-bucket-azure-full --mode=local -u; yarn run databricks-jdbc-export-bucket-azure-prefix-full --mode=local -u; yarn run databricks-jdbc-export-bucket-gcs-full --mode=local -u; yarn run databricks-jdbc-export-bucket-gcs-prefix-full --mode=local -u; yarn run databricks-jdbc-export-bucket-s3-full --mode=local -u; yarn run databricks-jdbc-export-bucket-s3-prefix-full --mode=local -u; yarn run databricks-jdbc-full --mode=local -u; yarn run mssql-full --mode=local -u; yarn run mysql-full --mode=local -u; yarn run postgres-full --mode=local -u; yarn run redshift-export-bucket-s3-full --mode=local -u; yarn run redshift-full --mode=local -u; yarn run snowflake-encrypted-pk-full --mode=local -u; yarn run snowflake-export-bucket-azure-full --mode=local -u; yarn run snowflake-export-bucket-azure-prefix-full --mode=local -u; yarn run snowflake-export-bucket-azure-via-storage-integration-full --mode=local -u; yarn run snowflake-export-bucket-gcs-full --mode=local -u; yarn run snowflake-export-bucket-gcs-prefix-full --mode=local -u; yarn run snowflake-export-bucket-s3-full --mode=local -u; yarn run snowflake-export-bucket-s3-via-storage-integration-iam-roles-full --mode=local -u; yarn run snowflake-export-bucket-s3-prefix-full --mode=local -u; yarn run snowflake-export-bucket-azure-prefix-full --mode=local -u; yarn run snowflake-export-bucket-azure-full --mode=local -u; yarn run snowflake-full --mode=local -u",
6364
"tst": "clear && yarn tsc && yarn bigquery-core"
6465
},
6566
"files": [

0 commit comments

Comments
 (0)