将 AWS Marketplace for Containers Anywhere 产品与 License Manager 集成 - AWS Marketplace

本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。

将 AWS Marketplace for Containers Anywhere 产品与 License Manager 集成

按照以下说明将 AWS License Manager 与适用于 Amazon EKS Anywhere、Amazon ECS Anywhere、Amazon EC2 或本地基础设施的 AWS Marketplace for Containers Anywhere 产品集成。

有关 License Manager 与 AWS Marketplace 集成的一般信息,包括可用的许可模式,请参阅使用 AWS License Manager 的容器产品的合同定价。有关 AWS License Manager 的更多信息,请参阅《AWS License Manager 用户指南》和《AWS CLI 命令参考》中的 AWS License Manager 部分。

将 AWS Marketplace for Containers Anywhere 产品与 License Manager 集成

按照以下说明将您的 AWS Marketplace for Containers Anywhere 产品与 AWS License Manager 集成。

要将 AWS Marketplace for Containers Anywhere 产品与 License Manager 集成,请执行以下操作:
  1. 打开 Web 浏览器并登录 AWS Marketplace 管理门户

  2. 执行以下步骤,为您的容器产品创建产品 ID。您将在容器映像中使用此 ID,以便在后续步骤中进行许可证检查。

    1. 从菜单栏中,展开资产,然后选择容器

    2. 为您的产品输入面向客户的名称,然后选择创建。您以后可以更改此名称。

    3. 记下产品 ID。之后在创建或更新产品定价详情时会使用它。

      提示

      如果您丢失了产品 ID,可以从资产菜单中选择容器,在 AWS Marketplace 管理门户 中找到它。容器页面显示您的产品列表及其关联的产品 ID。

  3. 下载最新的公开 AWS SDK 并将其安装在您的容器应用程序中。您可以在用于在 AWS 上构建的工具中找到首选 AWS SDK 的安装说明。

    注意

    要从 Amazon EKS Anywhere 或非 AWS 提供的 Kubernetes 集群调用 License Manager API 操作,您必须使用支持的 AWS SDK。要查看支持的 AWS SDK 列表,请参阅使用支持的 AWS SDK

  4. 使用自定义凭证提供程序创建 AWS License Manager 客户端,以便它可以为部署在 AWS 和本地的容器应用程序提供凭证。有关自定义凭证提供程序的完整源代码 LicenseCredentialProvider,请参阅以下各部分:

    LicenseCredentialsProvider 通过添加 LicenseManagerTokenCredentialsProvider 扩展 AWS SDK 的默认凭证提供程序链,以供本地使用。这通过在本地环境中使用 License Manager OIDC 颁发的身份令牌来提供凭证。必须在应用程序类路径中包含 LicenseCredentialsProvider 的源代码。

    注意

    扩展 DefaultCredentialsProvider 允许同一个容器应用程序在 AWS 中运行和在本地环境中运行时获得凭证。如果容器应用程序已经使用自定义凭证提供程序链而不是默认凭证提供程序链,则也可以通过将 LicenseManagerTokenCredentialsProvider 添加到自定义链中来进行扩展。

    以下代码片段是使用 Java 创建 AWS License Manager 客户端的示例。

    LicenseManagerClientBuilder clientBuilder = LicenseManagerClient.builder().credentialsProvider(LicenseCredentialsProvider.create());
  5. 使用产品中每个付费容器映像中的 aws license-manager checkout-license 命令调用 CheckoutLicense API 操作。这将检查买家是否有权使用您的应用程序许可证。如果买家有权使用该应用程序,则 CheckoutLicense 成功并返回所请求的权利及其值。如果买家无权使用该应用程序,则 CheckoutLicense 会抛出异常。

    调用 CheckoutLicense API 操作时需要以下参数:

    • CheckoutType – 有效值为 PROVISIONALPERPETUAL

      • PERPETUAL – 当已签出的权利池中的权利数量将用尽时使用。

        示例:买家有权处理 500 GB 的数据。当他们继续处理数据时,会消耗 500 GB 池的数量并将其耗尽。

      • PROVISIONAL 用于浮动许可证权利,其中权利从池中签出,并在使用后返回。

        示例:某用户被授予应用程序 500 个并发用户的权利。当用户登录或注销时,用户会被消耗或返回到 500 个用户的池中。要了解有关浮动许可证权利的更多信息,请参阅使用 License Manager 管理浮动许可证特权

    • ClientToken – 区分大小写的唯一标识符。我们建议为每个唯一请求使用随机的 UUID。

    • Entitlements – 待签出的权利列表。

      • 对于特征权利,请按以下方式提供 NameUnit 属性。

        { "Name": "<Entitlement_Name>", "Unit": "None" }
      • 对于技术特权,请按以下方式提供 NameUnitCount 属性。

        { "Name": "<Entitlement_Name>", "Unit": "<Entitlement_Unit>", "Value": <Desired_Count> }
    • KeyFingerprint – 由 AWS Marketplace 颁发的许可证的密钥指纹是 aws:294406891311:AWS/Marketplace:issuer-fingerprint。使用此密钥指纹可确保许可证是由 AWS Marketplace 颁发的,而不是由不可靠的实体颁发。

    • ProductSKU – 在之前的步骤中在 AWS Marketplace 管理门户 上生成的产品 ID。

    以下代码段是通过 AWS CLI 使用 CheckoutLicense API 操作进行调用的示例。

    aws license-manager checkout-license \ --product-sku "2205b290-19e6-4c76-9eea-377d6bf71a47" \ --checkout-type "PROVISIONAL" \ --client-token "79464194dca9429698cc774587a603a1" \ --entitlements "Name=AWS::Marketplace::Usage/Drawdown/DataConsumption, Value=10, Unit=Gigabytes" \ --key-fingerprint "aws:294406891311:AWS/Marketplace:issuer-fingerprint"
    注意

    要查看许可证,容器应用程序需要出站网络访问权限才能使用 License Manager。部署在本地的应用程序可能会遇到出站网络访问不可靠或缓慢的情况。这些应用程序在调用 License Manager 时应包括够的重试次数。有关更多信息,请参阅针对本地部署集成 License Manager 的最佳实操

  6. 定期调用 CheckoutLicense API 操作,以确定由于在 AWS Marketplace 上进行续订、升级或取消而对客户的许可证进行的任何更改。调用频率取决于应用程序。我们建议每天检查一次许可证,以便在没有任何买家干预的情况下自动获取更改。

    部署在本地的应用程序可能具有不可靠的出站网络访问权限,无法定期检查许可证。在这种情况下,应用程序应使用缓存许可证以获得足够的弹性。有关更多信息,请参阅针对本地部署集成 License Manager 的最佳实操

  7. CheckoutLicense 调用与容器应用程序集成后,基于更改生成 Docker 容器映像的新版本。

  8. 更新应用程序的 Helm 图表以接受 Kubernetes 密钥作为可选输入,其中包含使用 License Manager API 访问许可证的配置。配置密钥将包含由 License Manager 颁发的身份令牌和一个 AWS Identity and Access Management 角色,上述自定义凭证提供程序将使用该角色来获取在本地部署容器应用程序时调用 License Manager API 的 AWS 凭证。另外,将 AWS 区域作为输入添加,默认值为 us-east-1

    在本地部署容器应用程序的买家可以通过容器产品的 AWS Marketplace 买家体验创建 Kubernetes 秘密。提供 Kubernetes 密钥名称作为 helm install 命令的输入。配置密钥采用以下格式配置。

    apiVersion: v1 kind: Secret metadata: name: aws-marketplace-license-config type: Opaque stringData: license_token: <token_value> // License Manager issued JWT token iam_role: <role_arn> // AWS Identity and Access Management role to assume with license token
  9. 更新 AWS License Manager 集成的容器映像的 Helm 图表中的应用程序部署模板,以包含以下内容:

    • 容器组 (pod) 的服务账户 – 在 Amazon EKS 上部署 Helm 需要服务账户。它用于通过在容器映像上为服务账号设置 IAM 角色来获得调用 License Manager API 操作的权限。有关服务账户的 IAM 角色的更多信息,请参阅服务账户的 IAM 角色

    • 本地部署的许可证访问权限 – 需要许可证配置密钥才能提供凭证和相应权限,以便调用 License Manager API 操作,在本地环境中进行 Helm 部署。买家将通过 AWS Marketplace 买家体验生成许可证密钥并将其提供给 Helm。

    以下代码片段是示例部署规范,其中包含服务帐号、许可证配置和映像拉取密钥。

    apiVersion: apps/v1 kind: Deployment metadata: name: example-app spec: replicas: 1 selector: matchLabels: app: example-app template: metadata: labels: app: example-app spec: // Service account for pod serviceAccountName: {{ .Values.serviceAccountName }} containers: - name: example-app image: example-app ports: - containerPort: 8001 // Add the following conditional attributes {{ - if .Values.awsmp.licenseConfigSecretName }} //Mount the license volume to the container image volumeMounts: - name: awsmp-product-license mountPath: "/var/run/secrets/product-license" //Add following environment variable to container for credential provider env: - name: AWS_WEB_IDENTITY_REFRESH_TOKEN_FILE value: "/var/run/secrets/product-license/license_token" - name: AWS_ROLE_ARN valueFrom: secretKeyRef: name: {{ .Values.aws.licenseConfigSecretName }} key: iam_role //Mount the license secret as a volume to the pod volumes: - name: awsmp-product-license secret: secretName: {{ .Values.aws.licenseConfigSecretName }} optional: true {{ - end }}
    注意

    许可证配置密钥是可选的。买家仅在本地部署时使用该值。对于 AWS 部署,部署规范必须包含 License Manager 集成映像的服务帐户。

  10. 通过执行以下各部分中的步骤,在本地和 Amazon EKS 上测试 License Manager 集成:

    1. 在本地测试 License Manager 集成

    2. 在 Amazon EKS 上测试 License Manager 集成

  11. 成功在 AWS 上和本地验证 License Manager 集成后,您可以按照 创建容器产品 中的步骤创建容器产品列表。

在本地测试 License Manager 集成

你可以使用 minikube 或任何其他设置来在本地测试任何 Kubernetes 集群上的 License Manager 集成。确保 Kubernetes 集群具有出站互联网访问权限以调用 License Manager API 操作。

要在本地测试 License Manager 集成,请执行以下操作:
  1. 使用所需权利在测试卖家账户中创建测试许可证。要设置测试许可证,请参阅《AWS License Manager API 参考》中的CreateLicense。或者,使用以下脚本创建测试许可证,然后为测试买家账户创建许可授权,以使用该许可证。以下脚本使用测试卖家账户凭证。

    read -p 'AWS Account for test buyer: ' TEST_BUYER_ACCOUNT_ID read -p 'License entitlements: ' ENTITLEMENTS # TEST_SELLER_ACCOUNT_ID="109876543210" # ENTITLEMENTS="{\"Name\": \"ByData\",\"MaxCount\": 1000,\"Overage\":true,\"Unit\": \"Gigabits\",\"AllowCheckIn\": true}" # Create License NOW=$(date +"%Y-%m-%dT00:00:00+00:00") PRODUCT_NAME="My awesome product" PRODUCT_SKU="c97b7825-44c4-4f42-b025-12baa4c171e0" LICENSE_BENEFICIARY=" arn:aws:iam::$TEST_BUYER_ACCOUNT_ID:root " LICENSE_ISSUER_NAME="test-seller" LICENSE_NAME="test-seller-license" CLIENT_TOKEN="b3920968-a94f-4547-af07-3dd232319367" CONSUMPTION_TTL=180 CONSUMPTION_RENEW_TYPE="None" HOME_REGION="us-east-1" LICENSE_ARN=$(aws license-manager create-license --license-name "$LICENSE_NAME" --product-name "$PRODUCT_NAME" --product-sku "$PRODUCT_SKU" --issuer Name="$LICENSE_ISSUER_NAME" --home-region "$HOME_REGION" --validity Begin="$NOW" --entitlements "$ENTITLEMENTS" --beneficiary "$LICENSE_BENEFICIARY" --consumption-configuration RenewType="$CONSUMPTION_RENEW_TYPE",ProvisionalConfiguration={MaxTimeToLiveInMinutes=$CONSUMPTION_TTL} --client-token "$CLIENT_TOKEN" | jq -r ".LicenseArn" ) echo "License arn: $LICENSE_ARN" # Create Grant GRANT_TOKEN="e9a14140-4fca-4219-8230-57511a6ea6" GRANT_NAME="test-grant" GRANT_ARN=$(aws license-manager create-grant --grant-name "$GRANT_NAME" --license-arn "$LICENSE_ARN" --principals "$LICENSE_BENEFICIARY" --home-region "$HOME_REGION" --client-token "$GRANT_TOKEN" --allowed-operations "CheckoutLicense" "CheckInLicense" "ExtendConsumptionLicense" "CreateToken" | jq -r ".GrantArn") echo "Grant arn: $GRANT_ARN"
  2. 采用之前定义的密钥格式,使用许可证令牌和 IAM 角色创建 Kubernetes 密钥。使用 License Manager CreateToken API 操作生成许可证令牌。然后,使用 IAM CreateRole API 操作创建具有权限和信任策略的 IAM 角色。请参阅以下示例脚本中的示例。以下脚本使用测试买家账户凭证。

    read -p 'AWS Account for test license: ' TEST_ACCOUNT_ID read -p 'License Arn' LICENSE_ARN # Create IAM Role ROLE_NAME="AWSLicenseManagerConsumptionTestRole" ROLE_DESCRIPTION="Role to test AWS License Manager integration on-prem" ROLE_POLICY_ARN="arn:aws:iam::aws:policy/service-role/AWSLicenseManagerConsumptionPolicy" ROLE_TRUST_POLICY="{\"Version\": \"2012-10-17\",\"Statement\": [{ \"Effect\":\"Allow\", \"Principal\": { \"Federated\": \"openid-license-manager.amazonaws.com\" }, \"Action\": \"sts:AssumeRoleWithWebIdentity\",\"Condition\": { \"ForAnyValue:StringLike\": { \"openid-license-manager.amazonaws.com:amr\": \"aws:license-manager:token-issuer-account-id:${TEST_ACCOUNT_ID}\" }}}]}" ROLE_SESSION_DURATION=3600 ROLE_ARN=$(aws iam create-role --role-name "$ROLE_NAME" --description "$ROLE_DESCRIPTION" --assume-role-policy-document "$ROLE_TRUST_POLICY" --max-session-duration $ROLE_SESSION_DURATION | jq ".Role" | jq -r ".Arn") aws iam attach-role-policy --role-name "$ROLE_NAME" --policy-arn "$ROLE_POLICY_ARN" echo "Role arn: $ROLE_ARN" # Create Token CLIENT_TOKEN="b3920968-a94f-4547-af07-3dd232319367" TOKEN=$(aws license-manager create-token --license-arn $LICENSE_ARN --role-arns $ROLE_ARN --client-token $CLIENT_TOKEN | jq '.Token') echo "License access token: $TOKEN"c
  3. 设置托管在 AWS 外的任何 Kubernetes 集群。使用它来测试容器应用程序是否可以从 AWS 之外的其他环境连接到 AWS License Manager API,以及自定义凭证提供程序是否已很好地集成到应用程序中。

  4. 将之前生成的许可证令牌和 IAM 角色部署到本地 Kubernetes 集群中。

    kubectl create secret generic "awsmp-license-access-config" \ --from-literal=license_token=${TOKEN} \ --from-literal=iam_role=${ROLE_ARN}
  5. 使用密钥名称作为输入通过 Helm 部署应用程序,并验证应用程序是否可以调用 License Manager API 操作来执行权利检查。有关 Helm 和部署规范的更改,请参阅将 AWS Marketplace for Containers Anywhere 产品与 License Manager 集成中的步骤 9。

在 Amazon EKS 上测试 License Manager 集成

您还可以在 Amazon EKS 上测试 License Manager 集成。测试以确保应用程序可以在没有许可证配置密钥的情况下调用 License Manager API 操作。此外,请确保服务账户可用于设置服务账户的 IAM 角色 (IRSA),并为应用程序提供相关凭证。

要在 Amazon EKS 上测试 License Manager 集成,请执行以下操作:
  1. 使用所需权利在测试卖家账户中创建测试许可证。参阅 CreateLicense API 参考设置测试许可证,或者,使用以下脚本进行创建,然后为测试买家账户创建许可授权,以使用该许可证。以下脚本使用测试卖家账户凭证。

    read -p 'AWS Account for test buyer: ' TEST_BUYER_ACCOUNT_ID read -p 'License entitlements: ' ENTITLEMENTS # TEST_SELLER_ACCOUNT_ID="109876543210" # ENTITLEMENTS="{\"Name\": \"ByData\",\"MaxCount\": 1000,\"Overage\": true,\"Unit\": \"Gigabits\",\"AllowCheckIn\": true}" # Create License NOW=$(date +"%Y-%m-%dT00:00:00+00:00") PRODUCT_NAME="My awesome product" PRODUCT_SKU="c97b7825-44c4-4f42-b025-12baa4c171e0" LICENSE_BENEFICIARY=" arn:aws:iam::$TEST_BUYER_ACCOUNT_ID:root " LICENSE_ISSUER_NAME="test-seller" LICENSE_NAME="test-seller-license" CLIENT_TOKEN="b3920968-a94f-4547-af07-3dd232319367" CONSUMPTION_TTL=180 CONSUMPTION_RENEW_TYPE="None" HOME_REGION="us-east-1" LICENSE_ARN=$(aws license-manager create-license --license-name "$LICENSE_NAME" --product-name "$PRODUCT_NAME" --product-sku "$PRODUCT_SKU" --issuer Name="$LICENSE_ISSUER_NAME" --home-region "$HOME_REGION" --validity Begin="$NOW" --entitlements "$ENTITLEMENTS" --beneficiary "$LICENSE_BENEFICIARY" --consumption-configuration RenewType="$CONSUMPTION_RENEW_TYPE",ProvisionalConfiguration={MaxTimeToLiveInMinutes=$CONSUMPTION_TTL} --client-token "$CLIENT_TOKEN" | jq -r ".LicenseArn" ) echo "License arn: $LICENSE_ARN" # Create Grant GRANT_TOKEN="e9a14140-4fca-4219-8230-57511a6ea6" GRANT_NAME="test-grant" GRANT_ARN=$(aws license-manager create-grant --grant-name "$GRANT_NAME" --license-arn "$LICENSE_ARN" --principals "$LICENSE_BENEFICIARY" --home-region "$HOME_REGION" --client-token "$GRANT_TOKEN" --allowed-operations "CheckoutLicense" "CheckInLicense" "ExtendConsumptionLicense" "CreateToken" | jq -r ".GrantArn") echo "Grant arn: $GRANT_ARN"
  2. 创建包含所需配置的测试 Amazon EKS 集群,或运行以下命令以使用默认配置。

    aws ec2 create-key-pair --region us-west-2 --key-name eks-key-pair
    eksctl create cluster \ --name awsmp-eks-test-example \ --region us-west-2 \ --with-oidc \ --ssh-access \ --ssh-public-key eks-key-pair
  3. 为现有集群创建服务账户并将其与 IAM 角色关联。以下命令将使用 AWSLicenseManagerConsumptionPolicy 创建 IAM 角色。然后,该命令将其附加到应在其中部署 License Manager 集成映像的 Amazon EKS 集群的 test_sa 服务账户。这样一来,服务帐号可以获得相应的凭证来调用 License Manager API 操作。

    eksctl create iamserviceaccount \ --name test_sa \ --namespace test_namespace \ --cluster awsmp-eks-test-example \ --attach-policy-arn "arn:aws:iam::aws:policy/service-role/AWSLicenseManagerConsumptionPolicy" \ --approve \ --override-existing-serviceaccounts
  4. 在服务账户中通过 Helm 部署应用程序,该账户通过前面的命令与 IAM 角色关联。验证应用程序是否可以调用 License Manager API 操作来执行权利检查。

使用 License Manager 管理浮动许可证特权

使用浮动许可证,当用户登录应用程序时,将从可用许可证池中消耗许可证。用户注销后,许可证将重新添加到可用许可证池中。

对于浮动许可证,应用程序使用 CheckoutLicense API 操作在使用资源时从权利池中签出权利。CheckoutLicense API 操作的响应包括许可证消耗令牌,该令牌是签出的唯一标识符。许可证消耗令牌可以对已签出的权利执行其他操作,例如将其签回许可证池或延长签出时间。

当资源不再使用时,应用程序会使用 CheckInLicense API 操作将权利签回池中。

aws license-manager check-in-license \ --license-consumption-token "f1603b3c1f574b7284db84a9e771ee12"

如果许可证未能签回池中,例如,如果应用程序在操作过程中崩溃,则权利将在 60 分钟后自动签回池中。因此,如果资源的使用时间超过 60 分钟,则最佳实操是将权利保留在资源池之外。为此,只要资源正在使用,就可以使用 ExtendLicenseConsumption API 操作。

aws license-manager extend-license-consumption \ --license-consumption-token "f1603b3c1f574b7284db84a9e771ee12"

针对本地部署集成 License Manager 的最佳实操

本地环境中的容器应用程序部署可能会遇到不可靠的出站网络访问。使用以下最佳实操来增加弹性,以避免因互联网连接不畅导致的潜在问题而导致买家的服务中断:

  • 充分重试 – 瞬时网络问题可能会使您的应用程序无法连接到 AWS License Manager。实现最长 30 分钟的重试,并关闭指数级回退。这可以帮助避免短期中断或网络问题。

  • 避免硬限制 – 部署在联网集群中的应用程序可以定期检查许可证,以确定由于升级或续订而导致的任何更改。由于出站访问不可靠,应用程序可能无法识别这些更改。应用程序应尽可能避免因无法通过 License Manager 检查许可证而中断向买家提供的服务。当许可证到期时,应用程序可能会依赖免费试用或开源体验,并且无法检查许可证是否有效。

  • 通知客户 - 使用缓存许可证时,许可证的任何更改(包括续订或升级)都不会自动反映在正在运行的工作负载上。通知您的客户(他们必须暂时允许对应用程序进行出站访问),以便应用程序可以更新其缓存的许可证。例如,通过应用程序本身或通过其文档通知客户。同样,当回退到较低的功能集时,请通知客户其权利已用尽或许可证已过期。然后,他们可以选择升级或续订。

LicenseManagerCredentialsProvider - Java 实现。

LicenseCredentialsProvider 通过添加 LicenseManagerTokenCredentialsProvider 扩展 AWS SDK 的默认凭证提供程序链,以供本地使用。

LicenseCredentialsProvider

package com.amazon.awsmp.license; import software.amazon.awssdk.auth.credentials.AwsCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.AwsCredentialsProviderChain; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.auth.credentials.internal.LazyAwsCredentialsProvider; import software.amazon.awssdk.utils.SdkAutoCloseable; public class LicenseCredentialsProvider implements AwsCredentialsProvider, SdkAutoCloseable { private static final LicenseCredentialsProvider CREDENTIALS_PROVIDER = new LicenseCredentialsProvider(); private final LazyAwsCredentialsProvider providerChain; private LicenseCredentialsProvider() { this.providerChain = createChain(); } public static LicenseCredentialsProvider create() { return CREDENTIALS_PROVIDER; } @Override public AwsCredentials resolveCredentials() { return this.providerChain.resolveCredentials(); } @Override public void close() { this.providerChain.close(); } private LazyAwsCredentialsProvider createChain() { return LazyAwsCredentialsProvider.create(() -> { AwsCredentialsProvider[] credentialsProviders = new AwsCredentialsProvider[]{ DefaultCredentialsProvider.create(), LicenseManagerTokenCredentialsProvider.create()}; return AwsCredentialsProviderChain.builder().reuseLastProviderEnabled(true) .credentialsProviders(credentialsProviders).build(); }); } }

LicenseManagerTokenCredentialsProvider

LicenseManagerTokenCredentialsProvider 通过在本地环境中使用 License Manager OIDC 颁发的身份令牌来提供凭证。必须在应用程序类路径中包含 LicenseCredentialsProvider 的源代码。

package com.amazon.awsmp.license; import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; import software.amazon.awssdk.auth.credentials.AwsCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.core.SdkSystemSetting; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.core.retry.RetryPolicyContext; import software.amazon.awssdk.core.retry.conditions.OrRetryCondition; import software.amazon.awssdk.core.retry.conditions.RetryCondition; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain; import software.amazon.awssdk.services.licensemanager.LicenseManagerClient; import software.amazon.awssdk.services.licensemanager.model.GetAccessTokenRequest; import software.amazon.awssdk.services.licensemanager.model.GetAccessTokenResponse; import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.auth.StsAssumeRoleWithWebIdentityCredentialsProvider; import software.amazon.awssdk.services.sts.model.AssumeRoleWithWebIdentityRequest; import software.amazon.awssdk.services.sts.model.IdpCommunicationErrorException; import software.amazon.awssdk.utils.IoUtils; import software.amazon.awssdk.utils.SdkAutoCloseable; import software.amazon.awssdk.utils.StringUtils; import software.amazon.awssdk.utils.SystemSetting; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.util.function.Supplier; public class LicenseManagerTokenCredentialsProvider implements AwsCredentialsProvider, SdkAutoCloseable { private final StsAssumeRoleWithWebIdentityCredentialsProvider credentialsProvider; private final RuntimeException loadException; private Path licenseAccessTokenFile; private String roleArn; private String roleSessionName; private StsClient stsClient; private LicenseManagerClient lmClient; public static LicenseManagerTokenCredentialsProvider create() { return new Builder().build(); } @Override public AwsCredentials resolveCredentials() { if (this.loadException != null) { throw this.loadException; } return this.credentialsProvider.resolveCredentials(); } @Override public void close() { IoUtils.closeQuietly(this.credentialsProvider, null); IoUtils.closeQuietly(this.stsClient, null); IoUtils.closeIfCloseable(this.lmClient, null); } private LicenseManagerTokenCredentialsProvider(Builder builder) { StsAssumeRoleWithWebIdentityCredentialsProvider credentialsProvider = null; RuntimeException loadException = null; try { this.licenseAccessTokenFile = Paths.get(StringUtils.trim(LicenseSystemSetting.AWS_WEB_IDENTITY_REFRESH_TOKEN_FILE.getStringValueOrThrow())); this.roleArn = SdkSystemSetting.AWS_ROLE_ARN.getStringValueOrThrow(); this.roleSessionName = SdkSystemSetting.AWS_ROLE_SESSION_NAME.getStringValue().orElse("aws-sdk-java-" + System.currentTimeMillis()); this.stsClient = builder.stsClient != null ? builder.stsClient : StsClientFactory.create(); this.lmClient = builder.lmClient != null ? builder.lmClient : LicenseManagerClientFactory.create(); AssumeRoleWithWebIdentityRequest request = AssumeRoleWithWebIdentityRequest.builder() .roleArn(this.roleArn).roleSessionName(this.roleSessionName).build(); Supplier<AssumeRoleWithWebIdentityRequest> supplier = new AssumeRoleRequestSupplier(request, this.licenseAccessTokenFile, this.lmClient); credentialsProvider = StsAssumeRoleWithWebIdentityCredentialsProvider.builder() .stsClient(this.stsClient).refreshRequest(supplier).build(); } catch (RuntimeException ex) { loadException = ex; } this.credentialsProvider = credentialsProvider; this.loadException = loadException; } public static final class Builder { private Path licenseAccessTokenFile; private String roleArn; private String roleSessionName; private StsClient stsClient; private LicenseManagerClient lmClient; public LicenseManagerTokenCredentialsProvider build() { return new LicenseManagerTokenCredentialsProvider(this); } public LicenseManagerTokenCredentialsProvider.Builder licenseAccessTokenFile(Path licenseAccessTokenFile) { this.licenseAccessTokenFile = licenseAccessTokenFile; return this; } public LicenseManagerTokenCredentialsProvider.Builder roleArn(String roleArn) { this.roleArn = roleArn; return this; } public LicenseManagerTokenCredentialsProvider.Builder roleSessionName(String roleSessionName) { this.roleSessionName = roleSessionName; return this; } public LicenseManagerTokenCredentialsProvider.Builder stsClient(StsClient stsClient) { this.stsClient = stsClient; return this; } public LicenseManagerTokenCredentialsProvider.Builder lmClient(LicenseManagerClient lmClient) { this.lmClient = lmClient; return this; } } private static final class AssumeRoleRequestSupplier implements Supplier { private final LicenseManagerClient lmClient; private final AssumeRoleWithWebIdentityRequest request; private final Path webIdentityRefreshTokenFile; AssumeRoleRequestSupplier(final AssumeRoleWithWebIdentityRequest request, final Path webIdentityRefreshTokenFile, final LicenseManagerClient lmClient) { this.lmClient = lmClient; this.request = request; this.webIdentityRefreshTokenFile = webIdentityRefreshTokenFile; } public AssumeRoleWithWebIdentityRequest get() { return this.request.toBuilder() .webIdentityToken(getIdentityToken()) .build(); } private String getIdentityToken() { return refreshIdToken(readRefreshToken(this.webIdentityRefreshTokenFile)); } private String readRefreshToken(Path file) { try (InputStream webIdentityRefreshTokenStream = Files.newInputStream(file)) { return IoUtils.toUtf8String(webIdentityRefreshTokenStream); } catch (IOException e) { throw new UncheckedIOException(e); } } private String refreshIdToken(String licenseRefreshToken) { final GetAccessTokenRequest request = GetAccessTokenRequest.builder() .token(licenseRefreshToken) .build(); GetAccessTokenResponse response = this.lmClient.getAccessToken(request); return response.accessToken(); } } private static final class LicenseManagerClientFactory { private static final Duration DEFAULT_API_TIMEOUT = Duration.ofSeconds(30); private static final Duration DEFAULT_API_ATTEMPT_TIMEOUT = Duration.ofSeconds(10); public static LicenseManagerClient create() { return getLicenseManagerClient(); } private static LicenseManagerClient getLicenseManagerClient() { ClientOverrideConfiguration configuration = ClientOverrideConfiguration.builder() .apiCallTimeout(DEFAULT_API_TIMEOUT) .apiCallAttemptTimeout(DEFAULT_API_ATTEMPT_TIMEOUT) .build(); LicenseManagerClient client = LicenseManagerClient.builder() .region(configureLicenseManagerRegion()) .credentialsProvider(AnonymousCredentialsProvider.create()) .overrideConfiguration(configuration).build(); return client; } private static Region configureLicenseManagerRegion() { Region defaultRegion = Region.US_EAST_1; Region region; try { region = (new DefaultAwsRegionProviderChain()).getRegion(); } catch (RuntimeException ex) { region = defaultRegion; } return region; } } private static final class StsClientFactory { private static final Duration DEFAULT_API_TIMEOUT = Duration.ofSeconds(30); private static final Duration DEFAULT_API_ATTEMPT_TIMEOUT = Duration.ofSeconds(10); public static StsClient create() { return getStsClient(); } private static StsClient getStsClient() { OrRetryCondition retryCondition = OrRetryCondition.create(new StsRetryCondition(), RetryCondition.defaultRetryCondition()); ClientOverrideConfiguration configuration = ClientOverrideConfiguration.builder() .apiCallTimeout(DEFAULT_API_TIMEOUT) .apiCallAttemptTimeout(DEFAULT_API_ATTEMPT_TIMEOUT) .retryPolicy(r -> r.retryCondition(retryCondition)) .build(); return StsClient.builder() .region(configureStsRegion()) .credentialsProvider(AnonymousCredentialsProvider.create()) .overrideConfiguration(configuration).build(); } private static Region configureStsRegion() { Region defaultRegion = Region.US_EAST_1; Region stsRegion; try { stsRegion = (new DefaultAwsRegionProviderChain()).getRegion(); } catch (RuntimeException ex) { stsRegion = defaultRegion; } return stsRegion; } private static final class StsRetryCondition implements RetryCondition { public boolean shouldRetry(RetryPolicyContext context) { return context.exception() instanceof IdpCommunicationErrorException; } } } private enum LicenseSystemSetting implements SystemSetting { AWS_WEB_IDENTITY_REFRESH_TOKEN_FILE("aws.webIdentityRefreshTokenFile"); private String systemProperty; private String defaultValue = null; LicenseSystemSetting(String systemProperty) { this.systemProperty = systemProperty; } @Override public String property() { return this.systemProperty; } @Override public String environmentVariable() { return this.name(); } @Override public String defaultValue() { return this.defaultValue; } } }

LicenseManagerCredentialsProvider - Golang 实现。

LicenseCredentialsProvider

LicenseCredentialsProvider 通过添加 LicenseManagerTokenCredentialsProvider 扩展 AWS SDK 的默认凭证提供程序链,以供本地使用。

package lib import ( "context" "fmt" "sync" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" ) // LicenseCredentialsProvider is the custom credential provider that can retrieve valid temporary aws credentials type LicenseCredentialsProvider struct { fallBackProvider aws.CredentialsProvider mux sync.RWMutex licenseCredentials aws.Credentials err error } // NewLicenseCredentialsProvider method will create a LicenseCredentialProvider Object which contains valid temporary aws credentials func NewLicenseCredentialsProvider() (*LicenseCredentialsProvider, error) { licenseCredentialProvider := &LicenseCredentialsProvider{} fallBackProvider, err := createCredentialProvider() if err != nil { return licenseCredentialProvider, fmt.Errorf("failed to create LicenseCredentialsProvider, %w", err) } licenseCredentialProvider.fallBackProvider = fallBackProvider return licenseCredentialProvider, nil } // Retrieve method will retrieve temporary aws credentials from the credential provider func (l *LicenseCredentialsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { l.mux.RLock() defer l.mux.RUnlock() l.licenseCredentials, l.err = l.fallBackProvider.Retrieve(ctx) return l.licenseCredentials, l.err } func createCredentialProvider() (aws.CredentialsProvider, error) { // LoadDefaultConfig will examine all "default" credential providers ctx := context.TODO() cfg, err := config.LoadDefaultConfig(ctx) if err != nil { return nil, fmt.Errorf("failed to create FallBackProvider, %w", err) } var useFallbackProvider bool if cfg.Credentials != nil { if _, err := cfg.Credentials.Retrieve(ctx); err != nil { // If the "default" credentials provider cannot retrieve credentials, enable fallback to customCredentialsProvider. useFallbackProvider = true } } else { useFallbackProvider = true } if useFallbackProvider { customProvider, err := newLicenseManagerTokenCredentialsProvider() if err != nil { return cfg.Credentials, fmt.Errorf("failed to create fallBackProvider, %w", err) } // wrap up customProvider with CredentialsCache to enable caching cfg.Credentials = aws.NewCredentialsCache(customProvider) } return cfg.Credentials, nil }

LicenseManagerTokenCredentialsProvider

LicenseManagerTokenCredentialsProvider 通过在本地环境中使用 License Manager OIDC 颁发的身份令牌来提供凭证。必须在应用程序类路径中包含 LicenseCredentialsProvider 的源代码。

package lib import ( "context" "fmt" "io/ioutil" "os" "sync" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/sts" ) const awsRefreshTokenFilePathEnvVar = "AWS_LICENSE_ACCESS_FILE" // licenseManagerTokenCredentialsProvider defines and contains StsAssumeRoleWithWebIdentityProvider type licenseManagerTokenCredentialsProvider struct { stsCredentialProvider *stsAssumeRoleWithWebIdentityProvider mux sync.RWMutex licenseCredentials aws.Credentials err error } // Retrieve method will retrieve credentials from credential provider. // Make this method public to make this provider satisfies CredentialProvider interface func (a *licenseManagerTokenCredentialsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { a.mux.RLock() defer a.mux.RUnlock() a.licenseCredentials, a.err = a.stsCredentialProvider.Retrieve(ctx) return a.licenseCredentials, a.err } // newLicenseManagerTokenCredentialsProvider will create and return a LicenseManagerTokenCredentialsProvider Object which wraps up stsAssumeRoleWithWebIdentityProvider func newLicenseManagerTokenCredentialsProvider() (*licenseManagerTokenCredentialsProvider, error) { // 1. Retrieve variables From yaml environment envConfig, err := config.NewEnvConfig() if err != nil { return &licenseManagerTokenCredentialsProvider{}, fmt.Errorf("failed to create LicenseManagerTokenCredentialsProvider, %w", err) } roleArn := envConfig.RoleARN var roleSessionName string if envConfig.RoleSessionName == "" { roleSessionName = fmt.Sprintf("aws-sdk-go-v2-%v", time.Now().UnixNano()) } else { roleSessionName = envConfig.RoleSessionName } tokenFilePath := os.Getenv(awsRefreshTokenFilePathEnvVar) b, err := ioutil.ReadFile(tokenFilePath) if err != nil { return &licenseManagerTokenCredentialsProvider{}, fmt.Errorf("failed to create LicenseManagerTokenCredentialsProvider, %w", err) } refreshToken := aws.String(string(b)) // 2. Create stsClient cfg, err := config.LoadDefaultConfig(context.TODO()) if err != nil { return &licenseManagerTokenCredentialsProvider{}, fmt.Errorf("failed to create LicenseManagerTokenCredentialsProvider, %w", err) } stsClient := sts.NewFromConfig(cfg, func(o *sts.Options) { o.Region = configureStsClientRegion(cfg.Region) o.Credentials = aws.AnonymousCredentials{} }) // 3. Configure StsAssumeRoleWithWebIdentityProvider stsCredentialProvider := newStsAssumeRoleWithWebIdentityProvider(stsClient, roleArn, roleSessionName, refreshToken) // 4. Build and return return &licenseManagerTokenCredentialsProvider{ stsCredentialProvider: stsCredentialProvider, }, nil } func configureStsClientRegion(configRegion string) string { defaultRegion := "us-east-1" if configRegion == "" { return defaultRegion } else { return configRegion } }