AWS→SnowflakeのWorkload Identity FederationをBashで実装して低レベルな処理を理解するの巻 – LayerX エンジニアブログ


この記事は、LayerX Tech Advent Calendar 2025 の 5日目の記事です。
tech.layerx.co.jp

こんにちは。バクラク事業部 BizOps部 データグループの@civitaspoです。

先日、SnowflakeでWorkload Identity Federation機能がリリースされました。Workload Identity Federation機能は、Amazon Web Services(以下、AWS)やGoogle Cloud、Microsoft Azureなどのクラウドプロバイダー上のワークロードが持つIdentityを使ってSnowflakeとOpenID Connect(以下、OIDC)を使った認証を行える機能です。

docs.snowflake.com
docs.snowflake.com

このWorkload Identity Federation機能は、上記に挙げたクラウドプロバイダーだけでなく、Snowflake が定義する形式の OIDC attestationを発行できるカスタム OIDC プロバイダーを利用することもできます。なお、Snowflake が認識するOIDC attestationの正式な仕様(JWT claim や署名方式など)は公開されていません。そのため、カスタム OIDC プロバイダーを使う場合は、実際に手を動かして Snowflake が accept するトークンを試行錯誤で探す必要があります。

現実的には、非常に難易度の高い話なのでSnowflakeが提供するSDKやCLIがサポートするカスタム OIDC プロバイダーのみが利用可能と考えるのが良いでしょう。2025/12/05 時点ではカスタム OIDC プロバイダーとしてGitHub Actionsのサポートは確認しています。

github.com

今回の記事では、このWorkload Identity Federation機能を低レイヤーから理解するため、AWSからSnowflakeへWorkload Identity Federationを使って認証し、セッショントークンを取得するところまでをBashで実装してみようと思います。

「最初に結論から」と言うには非常に暴力的ですが、Bashスクリプトを貼り付けます。以下のBashスクリプトを実行すると、AWS上の特定のロールへAssume Roleを行なったあと、Snowflake Workload Identity Federationを用いて認証を行い、セッショントークンを取得できます。


set -eo pipefail

usage() {
    cat <
Usage: $0 [options]

Options:
  --aws-role-arn :              AWS Role ARN to assume
  --aws-region :                  AWS Region
  --snowflake-account-identifier:             Snowflake Account Identifier (-)
  --snowflake-username:                       Snowflake Username
  -h, --help:                                 Show this help message and exit

EOF
}

while [[ $# -gt 0 ]]; do
    case $1 in
        --aws-role-arn)
            aws_role_arn="${2}"
            shift 2
            ;;
        --aws-region)
            aws_region="${2}"
            shift 2
            ;;
        --snowflake-account-identifier)
            snowflake_account_identifier="${2}"
            shift 2
            ;;
        --snowflake-username)
            snowflake_username="${2}"
            shift 2
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        -*|--*)
            echo "[ERROR] Unknown option: ${1}"
            usage
            exit 1
            ;;
        *)
            echo "[ERROR] Unknown argument: ${1}"
            usage
            exit 1
            ;;
    esac
done

for v in aws_role_arn aws_region snowflake_account_identifier snowflake_username; do
    if [[ -z "${!v}" ]]; then
        echo "[ERROR] '--${v//_/-}' option is not defined." >&2
        exit 1
    fi
done
if [[ ! "$aws_role_arn" =~ ^arn:aws:iam::[0-9]+:role/ ]]; then
    echo "[ERROR] Invalid aws_role_arn: $aws_role_arn" >&2
    exit 1
fi
if [[ ! "$aws_region" =~ ^[a-z]{2}-[a-z]+-[0-9]+$ ]]; then
    echo "[ERROR] Invalid aws_region: $aws_region" >&2
    exit 1
fi

for cmd in curl jq date aws openssl xxd; do
  if ! command -v $cmd &> /dev/null; then
    echo "$cmd command not found"
    exit 1
  fi
done


readonly AWS_ROLE_ARN="$aws_role_arn"
readonly AWS_REGION="$aws_region"
readonly SESSION_NAME="snowflake-wif-access-$(date +%s)"
readonly CREDENTIALS=$(aws sts assume-role --role-arn $AWS_ROLE_ARN --role-session-name $SESSION_NAME --region $AWS_REGION --output json)
readonly AWS_ACCESS_KEY_ID=$(echo $CREDENTIALS | jq -r '.Credentials.AccessKeyId')
readonly AWS_SECRET_ACCESS_KEY=$(echo $CREDENTIALS | jq -r '.Credentials.SecretAccessKey')
readonly AWS_SESSION_TOKEN=$(echo $CREDENTIALS | jq -r '.Credentials.SessionToken')

readonly X_AMZ_DATE=$(TZ=UTC date +"%Y%m%dT%H%M%SZ")
readonly X_AMZ_DATE_SHORT=$(echo $X_AMZ_DATE | cut -c 1-8) 
readonly CANONICAL_REQUEST_HOST="sts.${AWS_REGION}.amazonaws.com"
readonly CANONICAL_REQUEST_METHOD="POST"
readonly CANONICAL_REQUEST_URI="/"
readonly CANONICAL_REQUEST_QUERY="Action=GetCallerIdentity&Version=2011-06-15"
readonly SNOWFLAKE_AUDIENCE_HEADER_KEY="x-snowflake-audience"
readonly SNOWFLAKE_AUDIENCE_HEADER_VALUE="snowflakecomputing.com"
readonly SIGNED_HEADERS="host;x-amz-date;x-amz-security-token;${SNOWFLAKE_AUDIENCE_HEADER_KEY}"
readonly CANONICAL_REQUEST_HEADERS="\
host:${CANONICAL_REQUEST_HOST}
x-amz-date:${X_AMZ_DATE}
x-amz-security-token:${AWS_SESSION_TOKEN}
x-snowflake-audience:${SNOWFLAKE_AUDIENCE_HEADER_VALUE}
"
readonly EMPTY_PAYLOAD_HASH=$(printf "" | openssl dgst -binary -sha256 | xxd -p -c 256)
readonly CANONICAL_REQUEST="\
${CANONICAL_REQUEST_METHOD}
${CANONICAL_REQUEST_URI}
${CANONICAL_REQUEST_QUERY}
${CANONICAL_REQUEST_HEADERS}
${SIGNED_HEADERS}
${EMPTY_PAYLOAD_HASH}"
readonly CANONICAL_REQUEST_HASH=$(printf "$CANONICAL_REQUEST" | openssl dgst -binary -sha256 | xxd -p -c 256)
readonly STRING_TO_SIGN="\
AWS4-HMAC-SHA256
${X_AMZ_DATE}
${X_AMZ_DATE_SHORT}/${AWS_REGION}/sts/aws4_request
${CANONICAL_REQUEST_HASH}"
readonly K_SECRET=$(printf "AWS4$AWS_SECRET_ACCESS_KEY" | xxd -p -c 256)
readonly K_DATE=$(printf "$X_AMZ_DATE_SHORT"  | openssl dgst -binary -sha256 -mac HMAC -macopt "hexkey:${K_SECRET}"  2>/dev/null | xxd -p -c 256)
readonly K_REGION=$(printf "$AWS_REGION"      | openssl dgst -binary -sha256 -mac HMAC -macopt "hexkey:${K_DATE}"    2>/dev/null | xxd -p -c 256)
readonly K_SERVICE=$(printf "sts"             | openssl dgst -binary -sha256 -mac HMAC -macopt "hexkey:${K_REGION}"  2>/dev/null | xxd -p -c 256)
readonly K_SIGNING=$(printf "aws4_request"    | openssl dgst -binary -sha256 -mac HMAC -macopt "hexkey:${K_SERVICE}" 2>/dev/null | xxd -p -c 256)
readonly SIGNATURE=$(printf "$STRING_TO_SIGN" | openssl dgst -binary -sha256 -mac HMAC -macopt "hexkey:${K_SIGNING}" 2>/dev/null | xxd -p -c 256)
readonly AUTHORIZATION_HEADER_VALUE="AWS4-HMAC-SHA256 Credential=${AWS_ACCESS_KEY_ID}/${X_AMZ_DATE_SHORT}/${AWS_REGION}/sts/aws4_request, SignedHeaders=${SIGNED_HEADERS}, Signature=${SIGNATURE}"
readonly CREDENTIAL_VERIFICATION_URL="https://${CANONICAL_REQUEST_HOST}${CANONICAL_REQUEST_URI}?${CANONICAL_REQUEST_QUERY}"













readonly AWS_ATTESTATION_JSON="$(
  jq -nrc \
    --arg url "$CREDENTIAL_VERIFICATION_URL" \
    --arg authorization_header_value "$AUTHORIZATION_HEADER_VALUE" \
    --arg http_method "$CANONICAL_REQUEST_METHOD" \
    --arg host "$CANONICAL_REQUEST_HOST" \
    --arg x_amz_date "$X_AMZ_DATE" \
    --arg x_snowflake_audience "$SNOWFLAKE_AUDIENCE_HEADER_VALUE" \
    --arg x_amz_security_token "$AWS_SESSION_TOKEN" \
    '{
      url: $url,
      method: $http_method,
      headers: {
        "authorization": $authorization_header_value,
        "host": $host,
        "x-amz-date": $x_amz_date,
        "x-amz-security-token": $x_amz_security_token,
        "x-snowflake-audience": $x_snowflake_audience
      }
    }'
)"
readonly AWS_ATTESTATION_B64="$(printf "%s" "$AWS_ATTESTATION_JSON" | base64 | tr -d '\n')"


readonly SNOWFLAKE_LOGIN_URL="https://${snowflake_account_identifier}.snowflakecomputing.com/session/v1/login-request"
readonly SNOWFLAKE_LOGIN_REQUEST_BODY="$(
  jq -nrc \
    --arg snowflake_account_identifier "${snowflake_account_identifier}" \
    --arg snowflake_username "${snowflake_username}" \
    --arg token "${AWS_ATTESTATION_B64}" \
    '{
      data: {
        ACCOUNT_NAME: $snowflake_account_identifier,
        LOGIN_NAME: $snowflake_username,
        AUTHENTICATOR: "WORKLOAD_IDENTITY",
        PROVIDER: "AWS",
        TOKEN: $token
      }
    }'
)"

readonly SNOWFLAKE_LOGIN_RESPONSE_JSON="$(
  curl -sS -X POST \
    -H 'Content-Type: application/json' \
    -H 'Accept: application/snowflake' \
    -H 'User-Agent: BASH-WIF-CLIENT/0.0.1' \
    -d "${SNOWFLAKE_LOGIN_REQUEST_BODY}" \
    "$SNOWFLAKE_LOGIN_URL"
)"
readonly SNOWFLAKE_MASTER_TOKEN="$(echo "${SNOWFLAKE_LOGIN_RESPONSE_JSON}" | jq -r '.data.masterToken // empty')"
readonly SNOWFLAKE_SESSION_TOKEN="$(echo "${SNOWFLAKE_LOGIN_RESPONSE_JSON}" | jq -r '.data.token // empty')"
if [[ -z "${SNOWFLAKE_MASTER_TOKEN}" ]]; then
  echo "Failed to get a snowflake master token." >&2
  exit 1
fi
if [[ -z "${SNOWFLAKE_SESSION_TOKEN}" ]]; then
  echo "Failed to get a snowflake session token." >&2
  exit 1
fi

jq -ncr \
  --arg session_token "$SNOWFLAKE_SESSION_TOKEN" \
  --arg master_token "$SNOWFLAKE_MASTER_TOKEN" \
  '{
    session_token: $session_token,
    master_token: $master_token
  }'

詳しく説明していきます。

いきなりBashを貼りましたが、中身でやっていることをざっくり分解すると次の3ステップです。

  1. AWS STS に対して AssumeRole を実行
  2. AssumeRole で取得したTemporalなCredentialを使って、AWS STS の GetCallerIdentity の SigV4 署名付きリクエストを組み立て
  3. その署名付きリクエストを Snowflake が期待するattestation形式に変換し、 /session/v1/login-request に投げる

Snowflake の Workload Identity Federation のドキュメントにも書かれているとおり、Workload Identity Federation(以下、WIF) の基本的な流れは

  1. As a workload administrator, configure your service to use a native identity provider so that the provider can issue an attestation of your workload’s identity. This attestation is often, but not always, a JSON Web Token (JWT).
  2. As a Snowflake administrator, create a Snowflake service user for your workload. You set the properties of this user to values found in the attestation sent by the provider. For example, a user property might specify the name of an IAM role or the issuer URL of the provider.
  3. As a workload developer, configure your workload to use a Snowflake driver. Drivers send the attestation to Snowflake for verification.

ref. https://docs.snowflake.com/en/user-guide/workload-identity-federation#workflow-for-implementing-workload-identity-federation

となっています。AWS の場合、その「attestation」の中身が SigV4 で署名された GetCallerIdentity リクエストとなっています。この流れは、AWS から Google Cloud に対する Workload Identity Federation の流れとほとんど変わりません。1

docs.cloud.google.com

全体像の1と2は、ドキュメントに忠実に実装しただけです。

docs.aws.amazon.com

特筆して説明すべきなのは、この署名のタイミングで x-snowflake-audience ヘッダーをCanonical Request / Signed Headers 両方に乗せる必要がある点です。Snowflake の Python コネクタ実装を見ると、AWS WIF の attestation 生成において X-Snowflake-Audience ヘッダを付与し、それも SigV4 署名の対象に含めていることがわかります。

github.com

これを抜いてしまうと Snowflake 側から code=394703 message=The AWS STS request contained unacceptable headers. For instance, the “X-Amz-Date” headers value may be too old as a request is only valid for 15 minutes. というエラーが返ってきます。

実装で言うと以下の箇所です。ここまでの実装で、「AWS STS に投げられる、GetCallerIdentity のSigV4署名付きリクエスト」が作られているので、これをSnowflakeが期待するJSON形式に変換します。

readonly AWS_ATTESTATION_JSON="$(
  jq -nrc \
    --arg url "$CREDENTIAL_VERIFICATION_URL" \
    --arg authorization_header_value "$AUTHORIZATION_HEADER_VALUE" \
    --arg http_method "$CANONICAL_REQUEST_METHOD" \
    --arg host "$CANONICAL_REQUEST_HOST" \
    --arg x_amz_date "$X_AMZ_DATE" \
    --arg x_snowflake_audience "$SNOWFLAKE_AUDIENCE_HEADER_VALUE" \
    --arg x_amz_security_token "$AWS_SESSION_TOKEN" \
    '{
      url: $url,
      method: $http_method,
      headers: {
        "authorization": $authorization_header_value,
        "host": $host,
        "x-amz-date": $x_amz_date,
        "x-amz-security-token": $x_amz_security_token,
        "x-snowflake-audience": $x_snowflake_audience
      }
    }'
)"
readonly AWS_ATTESTATION_B64="$(printf "%s" "$AWS_ATTESTATION_JSON" | base64 | tr -d '\n')"

snowflake-connector-pythonの実装における、 create_aws_attestation(…) メソッドの結果を作っています。 url, method, headers をフィールドに持つJSONで、AWS STSに対して投げられるリクエストの詳細が格納されます。

github.com

ここまで準備したので、あとはSnowflakeに認証リクエストを投げるのみです。 /session/v1/login-request へ先ほど構築した AWS Attestation JSON を認証に必要なパラメータとともに投げ込みます。

readonly SNOWFLAKE_LOGIN_URL="https://${snowflake_account_identifier}.snowflakecomputing.com/session/v1/login-request"
readonly SNOWFLAKE_LOGIN_REQUEST_BODY="$(
  jq -nrc \
    --arg snowflake_account_identifier "${snowflake_account_identifier}" \
    --arg snowflake_username "${snowflake_username}" \
    --arg token "${AWS_ATTESTATION_B64}" \
    '{
      data: {
        ACCOUNT_NAME: $snowflake_account_identifier,
        LOGIN_NAME: $snowflake_username,
        AUTHENTICATOR: "WORKLOAD_IDENTITY",
        PROVIDER: "AWS",
        TOKEN: $token
      }
    }'
)"
readonly SNOWFLAKE_LOGIN_RESPONSE_JSON="$(
  curl -sS -X POST \
    -H 'Content-Type: application/json' \
    -H 'Accept: application/snowflake' \
    -H 'User-Agent: BASH-WIF-CLIENT/0.0.1' \
    -d "${SNOWFLAKE_LOGIN_REQUEST_BODY}" \
    "$SNOWFLAKE_LOGIN_URL"
)"

snowflake-connector-python における実装は以下になります。

github.com

Snowflake からのレスポンスは、ざっくり次のような JSON です。

{
  "data": {
    "masterToken": "XXXXXXXXXX",
    "token": "XXXXXXXXXX",
    "validityInSeconds": 3600,
    "displayUserName": "TEST_USER",
    "firstLogin": false,
    ...
  },
  "success": true
}

snowflake-connector-python の実装でも、この token と masterToken を認証後に格納しています。

github.com

本記事では、AWSからSnowflakeへWorkload Identity Federationを使って認証し、セッショントークンを取得するところまでをBashで実装してみることで、Workload Identity Federation機能における低レイヤーなアクセスを理解しました。SDKの中で行われているWorkload Identity Federationによる認証の実装を再実装してみることで、詳細な処理を追うことが出来ました。もしリクエスト時に認証エラーなどの問題が発生しても、原因切り分けもやりやすくなることでしょう。

LayerXでは、Snowflakeを活用したデータ基盤の構築と、その上でのAI/MLシステムの開発を進めています。Production-ReadyなAI開発をサポートするためのデータ基盤開発、時系列データ処理、リアルタイムデータパイプラインの構築などに興味がある方は、ぜひ一緒にチャレンジしましょう!

open.talentio.com
open.talentio.com
open.talentio.com




元の記事を確認する

関連記事