配置客户端端点 - 适用于 Go 的 AWS SDK v2

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

配置客户端端点

警告

端点解析是一个高级 SDK 主题。更改这些设置可能会破坏您的代码。默认设置应适用于生产环境中的大多数用户。

适用于 Go 的 AWS SDK 提供了配置用于服务的自定义端点的功能。大多数情况下,默认配置就足够了。配置自定义端点可以支持其他行为,例如使用服务的预发行版本。

自定义

SDK 中有两个“版本”的端点解析配置。

  • v2,于 2023 年第三季度发布,通过以下项进行配置:

    • EndpointResolverV2

    • BaseEndpoint

  • v1,与 SDK 一起发布,通过以下项进行配置:

    • EndpointResolver

建议使用 v1 端点解析的用户迁移到 v2,以访问更新的与端点相关的服务功能。

V2:EndpointResolverV2 + BaseEndpoint

在解析 v2 中,EndpointResolverV2 是实现端点解析的最终机制。对于您在 SDK 中发出的每个请求,都会调用解析器的 ResolveEndpoint 方法,这是工作流程的一部分。在发出请求时,将按原样使用解析器返回的 Endpoint 的主机名(但是,操作序列化器仍可附加到 HTTP 路径中)。

解析 v2 包括一个额外的客户端级别配置 BaseEndpoint,用于为您的服务实例指定“基本”主机名。此处设置的值并非最终值 - 进行最终解析时,该值最终会作为参数传递给客户端的 EndpointResolverV2(继续阅读,以获取有关 EndpointResolverV2 参数的更多信息)。然后,解析器实现有机会检查该值,并可能修改该值以确定最终端点。

例如,当您使用客户端(在其中指定了 BaseEndpoint)对给定存储桶执行 S3 GetObject 请求时,如果该存储桶与虚拟主机兼容(假设您尚未在客户端配置中禁用虚拟托管),则默认解析器会将该存储桶注入主机名中。

实际上,BaseEndpoint 很可能用于将您的客户端指向服务的开发或预览实例。

EndpointResolverV2 参数

每项服务都需要一组特定的输入,这些输入将传递给其解析函数,此函数在每个服务包中定义为 EndpointParameters

每项服务都包含以下基本参数(这些参数用于为 AWS 内部的一般端点解析提供便利):

名称 类型 描述
Region string 客户端所在的 AWS 区域
Endpoint string 在客户端配置中为 BaseEndpoint 设置的值
UseFips bool 是否在客户端配置中启用了 FIPS 端点
UseDualStack bool 是否在客户端配置中启用了双栈端点

服务可以指定解析所需的其他参数。例如,S3 的 EndpointParameters 包括存储桶名称以及几个特定于 S3 的功能设置(例如是否启用虚拟主机寻址)。

如果您要实现自己的 EndpointResolverV2,则永远不需要构造自己的 EndpointParameters 实例。SDK 将根据每次请求获取这些值,并将其传递给您的实现。

关于 Amazon S3 的说明

Amazon S3 是一项复杂的服务,其许多功能都是通过复杂的端点自定义建模的,例如存储桶虚拟托管、S3 MRAP 等。

因此,建议您在 S3 客户端中不要替换 EndpointResolverV2 实现。如果需要扩展其解析行为,例如通过向本地开发堆栈发送请求以及其他端点注意事项,建议您包装默认实现,使其作为备用方案回退到默认设置(如以下示例所示)。

示例

使用 BaseEndpoint

以下代码段演示了如何将 S3 客户端指向服务的本地实例,在本示例中,该实例托管在端口 8080 的环回设备上。

client := s3.NewFromConfig(cfg, func (o *svc.Options) { o.BaseEndpoint = aws.String("https://localhost:8080/") })

使用 EndpointResolverV2

以下代码段演示了如何使用 EndpointResolverV2 将自定义行为注入 S3 的端点解析中。

import ( "context" "net/url" "github.com/aws/aws-sdk-go-v2/service/s3" smithyendpoints "github.com/aws/smithy-go/endpoints" ) type resolverV2 struct { // you could inject additional application context here as well } func (*resolverV2) ResolveEndpoint(ctx context.Context, params s3.EndpointParameters) ( smithyendpoints.Endpoint, error, ) { if /* input params or caller context indicate we must route somewhere */ { u, err := url.Parse("https://custom.service.endpoint/") if err != nil { return smithyendpoints.Endpoint{}, err } return smithyendpoints.Endpoint{ URI: *u, }, nil } // delegate back to the default v2 resolver otherwise return s3.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params) } func main() { // load config... client := s3.NewFromConfig(cfg, func (o *s3.Options) { o.EndpointResolverV2 = &resolverV2{ // ... } }) }

使用两者

以下示例程序演示了 BaseEndpointEndpointResolverV2 之间的交互。这是一个高级使用案例:

import ( "context" "fmt" "log" "net/url" "github.com/aws/aws-sdk-go-v2" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" smithyendpoints "github.com/aws/smithy-go/endpoints" ) type resolverV2 struct {} func (*resolverV2) ResolveEndpoint(ctx context.Context, params s3.EndpointParameters) ( smithyendpoints.Endpoint, error, ) { // s3.Options.BaseEndpoint is accessible here: fmt.Printf("The endpoint provided in config is %s\n", *params.Endpoint) // fallback to default return s3.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params) } func main() { cfg, err := config.LoadDefaultConfig(context.Background()) if (err != nil) { log.Fatal(err) } client := s3.NewFromConfig(cfg, func (o *s3.Options) { o.BaseEndpoint = aws.String("https://endpoint.dev/") o.EndpointResolverV2 = &resolverV2{} }) // ignore the output, this is just for demonstration client.ListBuckets(context.Background(), nil) }

运行时,上面的程序会输出以下内容:

The endpoint provided in config is https://endpoint.dev/

V1:EndpointResolver

警告

保留端点解析 v1 以实现向后兼容,并将其与端点解析 v2 中的现代行为隔离开来。仅当调用方设置 EndpointResolver 字段时才会使用。

使用 v1 很可能会阻止您访问在 v2 解析发布时或之后引入的与端点相关的服务功能。有关如何升级的说明,请参阅“迁移”。

可以配置 EndpointResolver,以便为服务客户端提供自定义端点解析逻辑。您可以使用自定义端点解析器来覆盖服务对于所有端点的端点解析逻辑,也可以仅覆盖特定的区域端点。如果自定义解析器不希望解析请求的端点,则自定义端点解析器可以触发服务的端点解析逻辑以进行回退。EndpointResolverWithOptionsFunc 可用来轻松包装函数以满足 EndpointResolverWithOptions 接口的要求。

通过将用 WithEndpointResolverWithOptions 包装的解析器传递给 LoadDefaultConfig,可以轻松配置 EndpointResolver,从而能够在加载凭证时,以及在使用自定义端点解析器来配置生成的 aws.Config 时,覆盖端点。

端点解析器以字符串形式获得服务和区域,使解析器能够动态驱动其行为。每个服务客户端程序包都有一个导出的 ServiceID 常量,可用于确定哪个服务客户端正在调用您的端点解析器。

端点解析器可以使用 EndpointNotFoundError 哨兵错误值来触发对服务客户端默认解析逻辑的回退解析。这样,您就可以有选择地无缝覆盖一个或多个端点,而不必处理回退逻辑。

如果您的端点解析器实现返回除 EndpointNotFoundError 之外的错误,则端点解析将停止,并且服务操作会向您的应用程序返回错误。

示例

有回退

以下代码段演示了如何为 DynamoDB 覆盖单个服务端点,而其他端点会出现回退行为:

customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { if service == dynamodb.ServiceID && region == "us-west-2" { return aws.Endpoint{ PartitionID: "aws", URL: "https://test.us-west-2.amazonaws.com", SigningRegion: "us-west-2", }, nil } // returning EndpointNotFoundError will allow the service to fallback to it's default resolution return aws.Endpoint{}, &aws.EndpointNotFoundError{} }) cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithEndpointResolverWithOptions(customResolver))

没有回退

以下代码段演示了如何为 DynamoDB 覆盖单个服务端点,而其他端点不会出现回退行为:

customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { if service == dynamodb.ServiceID && region == "us-west-2" { return aws.Endpoint{ PartitionID: "aws", URL: "https://test.us-west-2.amazonaws.com", SigningRegion: "us-west-2", }, nil } return aws.Endpoint{}, fmt.Errorf("unknown endpoint requested") }) cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithEndpointResolverWithOptions(customResolver))

不可变端点

警告

将端点设置为不可变可能会使某些服务客户端功能无法正常运行,并可能导致未定义的行为。将端点定义为不可变时应谨慎行事。

某些服务客户端(例如 Amazon S3)可能会修改解析器为某些服务操作返回的端点。例如,Amazon S3 将通过更改已解析的端点来自动处理虚拟存储桶寻址。您可以通过将 HostnameImmutable 设置为 true,来防止 SDK 更改您的自定义端点。例如:

customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { if service == dynamodb.ServiceID && region == "us-west-2" { return aws.Endpoint{ PartitionID: "aws", URL: "https://test.us-west-2.amazonaws.com", SigningRegion: "us-west-2", HostnameImmutable: true, }, nil } return aws.Endpoint{}, fmt.Errorf("unknown endpoint requested") }) cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithEndpointResolverWithOptions(customResolver))

迁移

从端点解析的 v1 迁移到 v2 时,应遵循以下一般原则:

  • 返回一个将 HostnameImmutable 设置为 false端点大致等同于将 BaseEndpoint 设置为 v1 中最初返回的 URL 并将 EndpointResolverV2 保留为默认值。

  • 返回一个将 HostnameImmutable 设置为 true 的端点大致等同于实现 EndpointResolverV2,这会返回 v1 中最初返回的 URL。

    • 主要例外是使用建模端点前缀的操作。关于这方面的说明请见下文。

下面提供了这些案例的示例。

警告

V1 不可变端点和 V2 解析在行为上并不相同。例如,仍会为通过 v1 代码返回的不可变端点设置自定义功能(例如 S3 Object Lambda)的签名覆盖,但对于 v2 则不会这样做。

关于主机前缀的说明

有些操作使用主机前缀进行建模,这些前缀将附加到已解析端点的前面。此行为必须与 ResolveEndpointV2 的输出配合使用,因此主机前缀仍将应用于该结果。

您可以通过应用中间件来手动禁用端点主机前缀,请参阅“示例”部分。

示例

可变端点

以下代码示例演示了如何迁移一个返回可修改端点的基本 v1 端点解析器:

// v1 client := svc.NewFromConfig(cfg, func (o *svc.Options) { o.EndpointResolver = svc.EndpointResolverFromURL("https://custom.endpoint.api/") }) // v2 client := svc.NewFromConfig(cfg, func (o *svc.Options) { // the value of BaseEndpoint is passed to the default EndpointResolverV2 // implementation, which will handle routing for features such as S3 accelerate, // MRAP, etc. o.BaseEndpoint = aws.String("https://custom.endpoint.api/") })

不可变端点

// v1 client := svc.NewFromConfig(cfg, func (o *svc.Options) { o.EndpointResolver = svc.EndpointResolverFromURL("https://custom.endpoint.api/", func (e *aws.Endpoint) { e.HostnameImmutable = true }) }) // v2 import ( smithyendpoints "github.com/aws/smithy-go/endpoints" ) type staticResolver struct {} func (*staticResolver) ResolveEndpoint(ctx context.Context, params svc.EndpointParameters) ( smithyendpoints.Endpoint, error, ) { // This value will be used as-is when making the request. u, err := url.Parse("https://custom.endpoint.api/") if err != nil { return smithyendpoints.Endpoint{}, err } return smithyendpoints.Endpoint{ URI: *u, }, nil } client := svc.NewFromConfig(cfg, func (o *svc.Options) { o.EndpointResolverV2 = &staticResolver{} })

禁用主机前缀

import ( "context" "fmt" "net/url" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/<service>" smithyendpoints "github.com/aws/smithy-go/endpoints" "github.com/aws/smithy-go/middleware" smithyhttp "github.com/aws/smithy-go/transport/http" ) // disableEndpointPrefix applies the flag that will prevent any // operation-specific host prefix from being applied type disableEndpointPrefix struct{} func (disableEndpointPrefix) ID() string { return "disableEndpointPrefix" } func (disableEndpointPrefix) HandleInitialize( ctx context.Context, in middleware.InitializeInput, next middleware.InitializeHandler, ) (middleware.InitializeOutput, middleware.Metadata, error) { ctx = smithyhttp.SetHostnameImmutable(ctx, true) return next.HandleInitialize(ctx, in) } func addDisableEndpointPrefix(o *<service>.Options) { o.APIOptions = append(o.APIOptions, (func(stack *middleware.Stack) error { return stack.Initialize.Add(disableEndpointPrefix{}, middleware.After) })) } type staticResolver struct{} func (staticResolver) ResolveEndpoint(ctx context.Context, params <service>.EndpointParameters) ( smithyendpoints.Endpoint, error, ) { u, err := url.Parse("https://custom.endpoint.api/") if err != nil { return smithyendpoints.Endpoint{}, err } return smithyendpoints.Endpoint{URI: *u}, nil } func main() { cfg, err := config.LoadDefaultConfig(context.Background()) if err != nil { panic(err) } svc := <service>.NewFromConfig(cfg, func(o *<service>.Options) { o.EndpointResolverV2 = staticResolver{} }) _, err = svc.<Operation>(context.Background(), &<service>.<OperationInput>{ /* ... */ }, addDisableEndpointPrefix) if err != nil { panic(err) } }