

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

# AWS Marketplace 使用 License Manager 集成 for Container
<a name="container-anywhere-license-manager-integration"></a>

作为 AWS Marketplace 卖家，您可以 AWS License Manager 与 AWS Marketplace 适用于亚马逊 EKS Anywhere、Amazon ECS Anywhere、Amazon EC2 或本地基础设施的 for Containers Anywhere 产品集成。以下部分提供了此集成的说明。

有关 License Manager 与集成的一般信息 AWS Marketplace，包括可用的许可模式，请参阅[集装箱产品的合同定价 AWS License Manager](container-license-manager-integration.md)。有关 AWS License Manager的更多信息，请参阅《[AWS License Manager 用户指南](https://docs.aws.amazon.com/license-manager/latest/userguide/license-manager.html)》和《AWS CLI 命令参考》**中的 [AWS License Manager](https://docs.aws.amazon.com/cli/latest/reference/license-manager/index.html) 部分。

**Topics**
+ [将 “容器随处 AWS Marketplace 可用” 产品与 License Manager 集成](#containers-anywhere-integrate-with-LM)
+ [在本地测试 License Manager 集成](#container-testing-LM-integration-locally)
+ [在 Amazon EKS 上测试 License Manager 集成](#container-testing-LM-integration-EKS)
+ [使用 License Manager 管理浮动许可证特权](#container-LM-floating-license)
+ [针对本地部署集成 License Manager 的最佳实操](#container-LM-best-practices-on-prem)
+ [`LicenseManagerCredentialsProvider` - Java 实现。](#container-license-manager-cred-provider-java)
+ [`LicenseManagerCredentialsProvider` - `Golang` 实现。](#container-license-manager-cred-provider-golang)

## 将 “容器随处 AWS Marketplace 可用” 产品与 License Manager 集成
<a name="containers-anywhere-integrate-with-LM"></a>

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

**将 for Container AWS Marketplace s Anywhere 产品与 License M**

1. 打开 Web 浏览器并登录 [AWS Marketplace 管理门户](https://aws.amazon.com/marketplace/management/)。

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

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

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

   1. 记下**产品 ID**。之后在创建或更新产品定价详情时会使用它。
**提示**  
如果您丢失了产品 ID，则可以从 “**资产**” 菜单中选择 “容器”，在 “**容器**” 中找到它。 AWS Marketplace 管理门户 **容**器页面显示您的产品及其关联产品的列表 IDs。

1. 下载最新的公共 AWS SDK，然后将其安装到您的容器应用程序中。您可以在 [AWS 上构建的 AWS 工具中找到首选软件开发工具包的](https://aws.amazon.com/tools/)安装说明。
**注意**  
要从 Amazon EKS Anywhere 或未提供的 Kubernetes 集群调用 License Manager API 操作 AWS，您必须使用支持的软件开发工具包。 AWS 要查看支持的列表 AWS SDKs，请参阅[使用支持的 AWS SDK](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts-minimum-sdk.html)。

1. 使用自定义凭证提供程序创建 AWS License Manager 客户端，以便它可以为部署在 AWS 本地和本地的容器应用程序提供凭证。有关自定义凭证提供程序的完整源代码 `LicenseCredentialProvider`，请参阅以下各部分：
   + [`LicenseManagerCredentialsProvider` - Java 实现。](#container-license-manager-cred-provider-java)
   + [`LicenseManagerCredentialsProvider` - `Golang` 实现。](#container-license-manager-cred-provider-golang)

    `LicenseCredentialsProvider`通过添加`LicenseManagerTokenCredentialsProvider`扩展 AWS SDK 的默认凭证提供程序链，以供本地使用。这通过在本地环境中使用 License Manager OIDC 颁发的身份令牌来提供凭证。必须在应用程序类路径中包含 `LicenseCredentialsProvider` 的源代码。
**注意**  
扩展`DefaultCredentialsProvider`允许同一个容器应用程序在本地环境中运行 AWS 和在本地环境中运行时获得证书。如果容器应用程序已经使用自定义凭证提供程序链而不是默认凭证提供程序链，则也可以通过将 `LicenseManagerTokenCredentialsProvider` 添加到自定义链中来进行扩展。

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

   ```
   LicenseManagerClientBuilder clientBuilder = LicenseManagerClient.builder().credentialsProvider(LicenseCredentialsProvider.create());
   ```

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

   调用 `CheckoutLicense` API 操作时需要以下参数：
   + `CheckoutType` – 有效值为 `PROVISIONAL` 或 `PERPETUAL`。
     + `PERPETUAL` – 当已签出的权利池中的权利数量将用尽时使用。

       示例：买家有权处理 500 GB 的数据。当他们继续处理数据时，会消耗 500 GB 池的数量并将其耗尽。
     + `PROVISIONAL` 用于浮动许可证权利，其中权利从池中签出，并在使用后返回。

       示例：某用户被授予应用程序 500 个并发用户的权利。当用户登录或注销时，用户会被消耗或返回到 500 个用户的池中。要了解有关浮动许可证权利的更多信息，请参阅[使用 License Manager 管理浮动许可证特权](#container-LM-floating-license)。
   + `ClientToken` – 区分大小写的唯一标识符。我们建议为每个唯一请求使用随机的 UUID。
   + `Entitlements` – 待签出的权利列表。
     + 对于特征权利，请按以下方式提供 `Name` 和 `Unit` 属性。

       ```
       {
         "Name": "<Entitlement_Name>",
         "Unit": "None"
       }
       ```
     + 对于技术特权，请按以下方式提供 `Name`、`Unit` 和 `Count` 属性。

       ```
       {
         "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 的最佳实操](#container-LM-best-practices-on-prem)。

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

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

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

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

   在本地部署容器应用程序的买家可以通过容器产品的购买者体验创建 Kubernetes 秘密。 AWS Marketplace 提供 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
   ```

1. 更新集成的容器镜像的 Helm 图表中的应用程序部署模板， AWS License Manager 使其包含以下内容：
   + 容器组 (pod) 的服务账户 – 在 Amazon EKS 上部署 Helm 需要服务账户。它用于通过在容器映像上为服务账号设置 IAM 角色来获得调用 License Manager API 操作的权限。有关服务账户的 IAM 角色的更多信息，请参阅[服务账户的 IAM 角色](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html)。
   + 本地部署的许可证访问权限 – 需要许可证配置密钥才能提供凭证和相应权限，以便调用 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 集成映像的服务帐户。

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

   1. [在本地测试 License Manager 集成](#container-testing-LM-integration-locally)

   1. [在 Amazon EKS 上测试 License Manager 集成](#container-testing-LM-integration-EKS)

1. 成功验证本地 AWS 和本地的 License Manager 集成后，您可以按照中的步骤创建容器产品清单[概述：创建容器产品](container-product-getting-started.md#create-container-product)。

## 在本地测试 License Manager 集成
<a name="container-testing-LM-integration-locally"></a>

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

**要在本地测试 License Manager 集成，请执行以下操作：**

1. 使用所需权利在测试卖家账户中创建测试许可证。要设置测试许可证，请参阅 *AWS License Manager API 参考[CreateLicense](https://docs.aws.amazon.com/license-manager/latest/APIReference/API_CreateLicense.html)*中的。或者，使用以下脚本创建测试许可证，然后为测试买家账户创建许可授权，以使用该许可证。以下脚本使用测试卖家账户凭证。

   ```
   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"
   ```

1. 采用之前定义的密钥格式，使用许可证令牌和 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
   ```

1. 设置托管在外部的任何 Kubernetes 集群。 AWS使用它来测试容器应用程序是否可以从其他环境连接到 AWS License Manager API， AWS 以及自定义凭证提供程序是否已很好地集成到应用程序中。

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

   ```
   kubectl create secret generic "awsmp-license-access-config" \
   --from-literal=license_token=${TOKEN} \
   --from-literal=iam_role=${ROLE_ARN}
   ```

1. 使用密钥名称作为输入通过 Helm 部署应用程序，并验证应用程序是否可以调用 License Manager API 操作来执行权利检查。有关 Helm 和部署规范的更改，请参阅[将 “容器随处 AWS Marketplace 可用” 产品与 License Manager 集成](#containers-anywhere-integrate-with-LM)中的步骤 9。

## 在 Amazon EKS 上测试 License Manager 集成
<a name="container-testing-LM-integration-EKS"></a>

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

**要在 Amazon EKS 上测试 License Manager 集成，请执行以下操作：**

1. 使用所需权利在测试卖家账户中创建测试许可证。请参阅 [CreateLicense API 参考](https://docs.aws.amazon.com/license-manager/latest/APIReference/API_CreateLicense.html)来设置您的测试许可证，或者使用以下脚本创建测试许可证，然后向测试买家账户创建使用许可证的许可授权。以下脚本使用测试卖家账户凭证。

   ```
   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"
   ```

1. 创建包含所需配置的测试 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
   ```

1. 为现有集群创建服务账户并将其与 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
   ```

1. 在服务账户中通过 Helm 部署应用程序，该账户通过前面的命令与 IAM 角色关联。验证应用程序是否可以调用 License Manager API 操作来执行权利检查。

## 使用 License Manager 管理浮动许可证特权
<a name="container-LM-floating-license"></a>

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

对于浮动许可证，应用程序使用 `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 的最佳实操
<a name="container-LM-best-practices-on-prem"></a>

本地环境中的容器应用程序部署可能会遇到不可靠的出站网络访问。使用以下最佳实操来增加弹性，以避免因互联网连接不畅导致的潜在问题而导致买家的服务中断：
+ **充分重试** — 临时网络问题可能会使您的应用程序无法连接到。 AWS License Manager实现最长 30 分钟的重试，并关闭指数级回退。这可以帮助避免短期中断或网络问题。
+ **避免硬限制** – 部署在联网集群中的应用程序可以定期检查许可证，以确定由于升级或续订而导致的任何更改。由于出站访问不可靠，应用程序可能无法识别这些更改。应用程序应尽可能避免因无法通过 License Manager 检查许可证而中断向买家提供的服务。当许可证到期时，应用程序可能会依赖免费试用或开源体验，并且无法检查许可证是否有效。
+ **通知客户** - 使用缓存许可证时，许可证的任何更改（包括续订或升级）都不会自动反映在正在运行的工作负载上。通知您的客户（他们必须暂时允许对应用程序进行出站访问），以便应用程序可以更新其缓存的许可证。例如，通过应用程序本身或通过其文档通知客户。同样，当回退到较低的功能集时，请通知客户其权利已用尽或许可证已过期。然后，他们可以选择升级或续订。

## `LicenseManagerCredentialsProvider` - Java 实现。
<a name="container-license-manager-cred-provider-java"></a>

`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` 实现。
<a name="container-license-manager-cred-provider-golang"></a>

**`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
	}
}
```