Merge pull request #18818 from roidelapluie/roidelapluie/aws-sd-binary-size

discovery/aws: keep AWS SDK clients out of interfaces to shrink the binary
This commit is contained in:
Julien 2026-05-29 14:26:36 +02:00 committed by GitHub
commit 27925e446c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 213 additions and 16 deletions

View file

@ -150,6 +150,47 @@ type ec2Client interface {
DescribeInstances(ctx context.Context, params *ec2.DescribeInstancesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
}
// ec2ClientAdapter holds the EC2 API calls that AWS discovery actually uses as
// method-value closures over the concrete *ec2.Client.
//
// It exists purely to keep the binary small. The Go linker, once reflection
// (reflect.Value.Method/Call plus struct-field traversal, both reachable via
// the YAML/config machinery) is live, conservatively retains every exported
// method of any concrete type that is reachable through an interface — and a
// type stored as a field of an interface-boxed struct counts. *ec2.Client has
// ~470 operation methods; retaining all of them pulls in ~1,500 serializers and
// roughly 21 MB. By capturing only the needed methods as func values, the
// concrete *ec2.Client is hidden inside closure contexts (which reflection
// cannot traverse) and never appears as a field of a boxed type, so dead-code
// elimination drops the unused operations.
type ec2ClientAdapter struct {
describeAvailabilityZones func(ctx context.Context, params *ec2.DescribeAvailabilityZonesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeAvailabilityZonesOutput, error)
describeInstances func(ctx context.Context, params *ec2.DescribeInstancesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
describeNetworkInterfaces func(ctx context.Context, params *ec2.DescribeNetworkInterfacesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error)
}
// newEC2ClientAdapter wraps a concrete *ec2.Client, capturing only the API
// calls AWS discovery needs. See the ec2ClientAdapter doc comment for why.
func newEC2ClientAdapter(c *ec2.Client) ec2ClientAdapter {
return ec2ClientAdapter{
describeAvailabilityZones: c.DescribeAvailabilityZones,
describeInstances: c.DescribeInstances,
describeNetworkInterfaces: c.DescribeNetworkInterfaces,
}
}
func (a ec2ClientAdapter) DescribeAvailabilityZones(ctx context.Context, params *ec2.DescribeAvailabilityZonesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeAvailabilityZonesOutput, error) {
return a.describeAvailabilityZones(ctx, params, optFns...)
}
func (a ec2ClientAdapter) DescribeInstances(ctx context.Context, params *ec2.DescribeInstancesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
return a.describeInstances(ctx, params, optFns...)
}
func (a ec2ClientAdapter) DescribeNetworkInterfaces(ctx context.Context, params *ec2.DescribeNetworkInterfacesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error) {
return a.describeNetworkInterfaces(ctx, params, optFns...)
}
// EC2Discovery periodically performs EC2-SD requests. It implements
// the Discoverer interface.
type EC2Discovery struct {
@ -235,12 +276,12 @@ func (d *EC2Discovery) ec2Client(ctx context.Context) (ec2Client, error) {
cfg.Credentials = aws.NewCredentialsCache(assumeProvider)
}
d.ec2 = ec2.NewFromConfig(cfg, func(options *ec2.Options) {
d.ec2 = newEC2ClientAdapter(ec2.NewFromConfig(cfg, func(options *ec2.Options) {
if d.cfg.Endpoint != "" {
options.BaseEndpoint = &d.cfg.Endpoint
}
options.HTTPClient = httpClient
})
}))
return d.ec2, nil
}

View file

@ -162,6 +162,60 @@ type ecsClient interface {
DescribeContainerInstances(context.Context, *ecs.DescribeContainerInstancesInput, ...func(*ecs.Options)) (*ecs.DescribeContainerInstancesOutput, error)
}
// ecsClientAdapter captures only the ECS API calls AWS discovery uses as
// method-value closures, keeping the concrete *ecs.Client out of any
// interface-boxed struct field. See ec2ClientAdapter for the full rationale:
// this stops the linker from retaining the entire ECS API surface (~2 MB).
type ecsClientAdapter struct {
listClusters func(context.Context, *ecs.ListClustersInput, ...func(*ecs.Options)) (*ecs.ListClustersOutput, error)
describeClusters func(context.Context, *ecs.DescribeClustersInput, ...func(*ecs.Options)) (*ecs.DescribeClustersOutput, error)
listServices func(context.Context, *ecs.ListServicesInput, ...func(*ecs.Options)) (*ecs.ListServicesOutput, error)
describeServices func(context.Context, *ecs.DescribeServicesInput, ...func(*ecs.Options)) (*ecs.DescribeServicesOutput, error)
listTasks func(context.Context, *ecs.ListTasksInput, ...func(*ecs.Options)) (*ecs.ListTasksOutput, error)
describeTasks func(context.Context, *ecs.DescribeTasksInput, ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error)
describeContainerInstances func(context.Context, *ecs.DescribeContainerInstancesInput, ...func(*ecs.Options)) (*ecs.DescribeContainerInstancesOutput, error)
}
func newECSClientAdapter(c *ecs.Client) ecsClientAdapter {
return ecsClientAdapter{
listClusters: c.ListClusters,
describeClusters: c.DescribeClusters,
listServices: c.ListServices,
describeServices: c.DescribeServices,
listTasks: c.ListTasks,
describeTasks: c.DescribeTasks,
describeContainerInstances: c.DescribeContainerInstances,
}
}
func (a ecsClientAdapter) ListClusters(ctx context.Context, params *ecs.ListClustersInput, optFns ...func(*ecs.Options)) (*ecs.ListClustersOutput, error) {
return a.listClusters(ctx, params, optFns...)
}
func (a ecsClientAdapter) DescribeClusters(ctx context.Context, params *ecs.DescribeClustersInput, optFns ...func(*ecs.Options)) (*ecs.DescribeClustersOutput, error) {
return a.describeClusters(ctx, params, optFns...)
}
func (a ecsClientAdapter) ListServices(ctx context.Context, params *ecs.ListServicesInput, optFns ...func(*ecs.Options)) (*ecs.ListServicesOutput, error) {
return a.listServices(ctx, params, optFns...)
}
func (a ecsClientAdapter) DescribeServices(ctx context.Context, params *ecs.DescribeServicesInput, optFns ...func(*ecs.Options)) (*ecs.DescribeServicesOutput, error) {
return a.describeServices(ctx, params, optFns...)
}
func (a ecsClientAdapter) ListTasks(ctx context.Context, params *ecs.ListTasksInput, optFns ...func(*ecs.Options)) (*ecs.ListTasksOutput, error) {
return a.listTasks(ctx, params, optFns...)
}
func (a ecsClientAdapter) DescribeTasks(ctx context.Context, params *ecs.DescribeTasksInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) {
return a.describeTasks(ctx, params, optFns...)
}
func (a ecsClientAdapter) DescribeContainerInstances(ctx context.Context, params *ecs.DescribeContainerInstancesInput, optFns ...func(*ecs.Options)) (*ecs.DescribeContainerInstancesOutput, error) {
return a.describeContainerInstances(ctx, params, optFns...)
}
type ecsEC2Client interface {
DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
DescribeNetworkInterfaces(context.Context, *ec2.DescribeNetworkInterfacesInput, ...func(*ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error)
@ -250,16 +304,16 @@ func (d *ECSDiscovery) initEcsClient(ctx context.Context) error {
cfg.Credentials = aws.NewCredentialsCache(assumeProvider)
}
d.ecs = ecs.NewFromConfig(cfg, func(options *ecs.Options) {
d.ecs = newECSClientAdapter(ecs.NewFromConfig(cfg, func(options *ecs.Options) {
if d.cfg.Endpoint != "" {
options.BaseEndpoint = &d.cfg.Endpoint
}
options.HTTPClient = client
})
}))
d.ec2 = ec2.NewFromConfig(cfg, func(options *ec2.Options) {
d.ec2 = newEC2ClientAdapter(ec2.NewFromConfig(cfg, func(options *ec2.Options) {
options.HTTPClient = client
})
}))
// Test credentials by making a simple API call
testCtx, cancel := context.WithTimeout(ctx, 10*time.Second)

View file

@ -246,6 +246,37 @@ type elasticacheClient interface {
ListTagsForResource(ctx context.Context, params *elasticache.ListTagsForResourceInput, optFns ...func(*elasticache.Options)) (*elasticache.ListTagsForResourceOutput, error)
}
// elasticacheClientAdapter captures only the ElastiCache API calls AWS
// discovery uses as method-value closures, keeping the concrete
// *elasticache.Client out of any interface-boxed struct field. See
// ec2ClientAdapter for the full rationale: this stops the linker from retaining
// the entire ElastiCache API surface (~2.5 MB).
type elasticacheClientAdapter struct {
describeServerlessCaches func(ctx context.Context, params *elasticache.DescribeServerlessCachesInput, optFns ...func(*elasticache.Options)) (*elasticache.DescribeServerlessCachesOutput, error)
describeCacheClusters func(ctx context.Context, params *elasticache.DescribeCacheClustersInput, optFns ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error)
listTagsForResource func(ctx context.Context, params *elasticache.ListTagsForResourceInput, optFns ...func(*elasticache.Options)) (*elasticache.ListTagsForResourceOutput, error)
}
func newElastiCacheClientAdapter(c *elasticache.Client) elasticacheClientAdapter {
return elasticacheClientAdapter{
describeServerlessCaches: c.DescribeServerlessCaches,
describeCacheClusters: c.DescribeCacheClusters,
listTagsForResource: c.ListTagsForResource,
}
}
func (a elasticacheClientAdapter) DescribeServerlessCaches(ctx context.Context, params *elasticache.DescribeServerlessCachesInput, optFns ...func(*elasticache.Options)) (*elasticache.DescribeServerlessCachesOutput, error) {
return a.describeServerlessCaches(ctx, params, optFns...)
}
func (a elasticacheClientAdapter) DescribeCacheClusters(ctx context.Context, params *elasticache.DescribeCacheClustersInput, optFns ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error) {
return a.describeCacheClusters(ctx, params, optFns...)
}
func (a elasticacheClientAdapter) ListTagsForResource(ctx context.Context, params *elasticache.ListTagsForResourceInput, optFns ...func(*elasticache.Options)) (*elasticache.ListTagsForResourceOutput, error) {
return a.listTagsForResource(ctx, params, optFns...)
}
// ElasticacheDiscovery periodically performs Elasticache-SD requests.
// It implements the Discoverer interface.
type ElasticacheDiscovery struct {
@ -328,12 +359,12 @@ func (d *ElasticacheDiscovery) initElasticacheClient(ctx context.Context) error
cfg.Credentials = aws.NewCredentialsCache(assumeProvider)
}
d.elasticacheClient = elasticache.NewFromConfig(cfg, func(options *elasticache.Options) {
d.elasticacheClient = newElastiCacheClientAdapter(elasticache.NewFromConfig(cfg, func(options *elasticache.Options) {
if d.cfg.Endpoint != "" {
options.BaseEndpoint = &d.cfg.Endpoint
}
options.HTTPClient = client
})
}))
// Test credentials by making a simple API call
testCtx, cancel := context.WithTimeout(ctx, 10*time.Second)

View file

@ -119,12 +119,29 @@ func (c *LightsailSDConfig) UnmarshalYAML(unmarshal func(any) error) error {
return c.HTTPClientConfig.Validate()
}
// lightsailClientAdapter captures only the Lightsail API calls AWS discovery
// uses as method-value closures, keeping the concrete *lightsail.Client out of
// any interface-boxed struct field. See ec2ClientAdapter for the full
// rationale: this stops the linker from retaining the entire Lightsail API
// surface (~3.4 MB).
type lightsailClientAdapter struct {
getInstances func(ctx context.Context, params *lightsail.GetInstancesInput, optFns ...func(*lightsail.Options)) (*lightsail.GetInstancesOutput, error)
}
func newLightsailClientAdapter(c *lightsail.Client) *lightsailClientAdapter {
return &lightsailClientAdapter{getInstances: c.GetInstances}
}
func (a *lightsailClientAdapter) GetInstances(ctx context.Context, params *lightsail.GetInstancesInput, optFns ...func(*lightsail.Options)) (*lightsail.GetInstancesOutput, error) {
return a.getInstances(ctx, params, optFns...)
}
// LightsailDiscovery periodically performs Lightsail-SD requests. It implements
// the Discoverer interface.
type LightsailDiscovery struct {
*refresh.Discovery
cfg *LightsailSDConfig
lightsail *lightsail.Client
lightsail *lightsailClientAdapter
}
// NewLightsailDiscovery returns a new LightsailDiscovery which periodically refreshes its targets.
@ -154,7 +171,7 @@ func NewLightsailDiscovery(conf *LightsailSDConfig, opts discovery.DiscovererOpt
return d, nil
}
func (d *LightsailDiscovery) lightsailClient(ctx context.Context) (*lightsail.Client, error) {
func (d *LightsailDiscovery) lightsailClient(ctx context.Context) (*lightsailClientAdapter, error) {
if d.lightsail != nil {
return d.lightsail, nil
}
@ -198,12 +215,12 @@ func (d *LightsailDiscovery) lightsailClient(ctx context.Context) (*lightsail.Cl
cfg.Credentials = aws.NewCredentialsCache(assumeProvider)
}
d.lightsail = lightsail.NewFromConfig(cfg, func(options *lightsail.Options) {
d.lightsail = newLightsailClientAdapter(lightsail.NewFromConfig(cfg, func(options *lightsail.Options) {
if d.cfg.Endpoint != "" {
options.BaseEndpoint = &d.cfg.Endpoint
}
options.HTTPClient = httpClient
})
}))
return d.lightsail, nil
}

View file

@ -158,6 +158,36 @@ type mskClient interface {
ListNodes(context.Context, *kafka.ListNodesInput, ...func(*kafka.Options)) (*kafka.ListNodesOutput, error)
}
// mskClientAdapter captures only the MSK (Kafka) API calls AWS discovery uses
// as method-value closures, keeping the concrete *kafka.Client out of any
// interface-boxed struct field. See ec2ClientAdapter for the full rationale:
// this stops the linker from retaining the entire MSK API surface (~1.4 MB).
type mskClientAdapter struct {
describeClusterV2 func(context.Context, *kafka.DescribeClusterV2Input, ...func(*kafka.Options)) (*kafka.DescribeClusterV2Output, error)
listClustersV2 func(context.Context, *kafka.ListClustersV2Input, ...func(*kafka.Options)) (*kafka.ListClustersV2Output, error)
listNodes func(context.Context, *kafka.ListNodesInput, ...func(*kafka.Options)) (*kafka.ListNodesOutput, error)
}
func newMSKClientAdapter(c *kafka.Client) mskClientAdapter {
return mskClientAdapter{
describeClusterV2: c.DescribeClusterV2,
listClustersV2: c.ListClustersV2,
listNodes: c.ListNodes,
}
}
func (a mskClientAdapter) DescribeClusterV2(ctx context.Context, params *kafka.DescribeClusterV2Input, optFns ...func(*kafka.Options)) (*kafka.DescribeClusterV2Output, error) {
return a.describeClusterV2(ctx, params, optFns...)
}
func (a mskClientAdapter) ListClustersV2(ctx context.Context, params *kafka.ListClustersV2Input, optFns ...func(*kafka.Options)) (*kafka.ListClustersV2Output, error) {
return a.listClustersV2(ctx, params, optFns...)
}
func (a mskClientAdapter) ListNodes(ctx context.Context, params *kafka.ListNodesInput, optFns ...func(*kafka.Options)) (*kafka.ListNodesOutput, error) {
return a.listNodes(ctx, params, optFns...)
}
// MSKDiscovery periodically performs MSK-SD requests. It implements
// the Discoverer interface.
type MSKDiscovery struct {
@ -240,12 +270,12 @@ func (d *MSKDiscovery) initMskClient(ctx context.Context) error {
cfg.Credentials = aws.NewCredentialsCache(assumeProvider)
}
d.msk = kafka.NewFromConfig(cfg, func(options *kafka.Options) {
d.msk = newMSKClientAdapter(kafka.NewFromConfig(cfg, func(options *kafka.Options) {
if d.cfg.Endpoint != "" {
options.BaseEndpoint = &d.cfg.Endpoint
}
options.HTTPClient = client
})
}))
// Test credentials by making a simple API call
testCtx, cancel := context.WithTimeout(ctx, 10*time.Second)

View file

@ -276,6 +276,30 @@ type rdsClient interface {
DescribeDBInstances(context.Context, *rds.DescribeDBInstancesInput, ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error)
}
// rdsClientAdapter captures only the RDS API calls AWS discovery uses as
// method-value closures, keeping the concrete *rds.Client out of any
// interface-boxed struct field. See ec2ClientAdapter for the full rationale:
// this stops the linker from retaining the entire RDS API surface (~5 MB).
type rdsClientAdapter struct {
describeDBClusters func(context.Context, *rds.DescribeDBClustersInput, ...func(*rds.Options)) (*rds.DescribeDBClustersOutput, error)
describeDBInstances func(context.Context, *rds.DescribeDBInstancesInput, ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error)
}
func newRDSClientAdapter(c *rds.Client) rdsClientAdapter {
return rdsClientAdapter{
describeDBClusters: c.DescribeDBClusters,
describeDBInstances: c.DescribeDBInstances,
}
}
func (a rdsClientAdapter) DescribeDBClusters(ctx context.Context, params *rds.DescribeDBClustersInput, optFns ...func(*rds.Options)) (*rds.DescribeDBClustersOutput, error) {
return a.describeDBClusters(ctx, params, optFns...)
}
func (a rdsClientAdapter) DescribeDBInstances(ctx context.Context, params *rds.DescribeDBInstancesInput, optFns ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error) {
return a.describeDBInstances(ctx, params, optFns...)
}
// RDSDiscovery periodically performs RDS-SD requests. It implements
// the Discoverer interface.
type RDSDiscovery struct {
@ -358,12 +382,12 @@ func (d *RDSDiscovery) initRdsClient(ctx context.Context) error {
cfg.Credentials = aws.NewCredentialsCache(assumeProvider)
}
d.rds = rds.NewFromConfig(cfg, func(options *rds.Options) {
d.rds = newRDSClientAdapter(rds.NewFromConfig(cfg, func(options *rds.Options) {
if d.cfg.Endpoint != "" {
options.BaseEndpoint = &d.cfg.Endpoint
}
options.HTTPClient = client
})
}))
// Test credentials by making a simple API call
testCtx, cancel := context.WithTimeout(ctx, 10*time.Second)