Skip to content

Commit e50abf0

Browse files
authored
IPv6-only support for awslogs logging driver (#4577)
1 parent 1a770f8 commit e50abf0

130 files changed

Lines changed: 63017 additions & 23 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

agent/config/ipcompatibility/ipcompatibility.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,16 @@ func NewIPCompatibility(ipv4Compatible, ipv6Compatible bool) IPCompatibility {
2424
return IPCompatibility{ipv4Compatible: ipv4Compatible, ipv6Compatible: ipv6Compatible}
2525
}
2626

27-
// Returns an IPv4-only IPCompatibility instance.
27+
// Returns an IPv4-only IPCompatibility value.
2828
func NewIPv4OnlyCompatibility() IPCompatibility {
2929
return NewIPCompatibility(true, false)
3030
}
3131

32+
// Returns an IPv6-only IPCompatibility value.
33+
func NewIPv6OnlyCompatibility() IPCompatibility {
34+
return NewIPCompatibility(false, true)
35+
}
36+
3237
// IsIPv4Compatible returns the current IPv4 compatibility status.
3338
func (ic *IPCompatibility) IsIPv4Compatible() bool {
3439
return ic.ipv4Compatible

agent/config/ipcompatibility/ipcompatibility_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ func TestIPv4OnlyCompatibility(t *testing.T) {
4444
assert.False(t, c.IsIPv6Compatible())
4545
}
4646

47+
func TestIPv6OnlyCompatibility(t *testing.T) {
48+
c := NewIPv6OnlyCompatibility()
49+
assert.True(t, c.IsIPv6Compatible())
50+
assert.False(t, c.IsIPv4Compatible())
51+
}
52+
4753
func TestIsIPv6Only(t *testing.T) {
4854
tests := []struct {
4955
name string

agent/engine/docker_task_engine.go

Lines changed: 129 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ import (
5656
"github.com/aws/amazon-ecs-agent/ecs-agent/logger/field"
5757
"github.com/aws/amazon-ecs-agent/ecs-agent/utils/retry"
5858
"github.com/aws/amazon-ecs-agent/ecs-agent/utils/ttime"
59+
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
5960
"github.com/aws/aws-sdk-go/aws"
6061
ep "github.com/aws/aws-sdk-go/aws/endpoints"
62+
"github.com/aws/smithy-go/ptr"
6163
"github.com/docker/docker/api/types"
6264
dockercontainer "github.com/docker/docker/api/types/container"
6365
"github.com/docker/docker/api/types/registry"
@@ -89,6 +91,8 @@ const (
8991
logDriverTypeFirelens = "awsfirelens"
9092
logDriverTypeFluentd = "fluentd"
9193
logDriverTypeAwslogs = "awslogs"
94+
awsLogsEndpointKey = "awslogs-endpoint"
95+
awsLogsRegionKey = "awslogs-region"
9296
logDriverTag = "tag"
9397
logDriverMode = "mode"
9498
logDriverBufferSize = "max-buffer-size"
@@ -1945,29 +1949,33 @@ func (engine *DockerTaskEngine) createContainer(task *apitask.Task, container *a
19451949

19461950
// This is a short term solution only for specific regions
19471951
if hostConfig.LogConfig.Type == logDriverTypeAwslogs {
1948-
region := engine.cfg.AWSRegion
1949-
if _, ok := unresolvedIsolatedRegions[region]; ok {
1950-
endpoint := ""
1951-
dnsSuffix := ""
1952-
partition, ok := ep.PartitionForRegion(ep.DefaultPartitions(), region)
1953-
if !ok {
1954-
logger.Warn("No partition resolved for region. Using AWS default", logger.Fields{
1955-
"region": region,
1956-
"defaultDNSSuffix": ep.AwsPartition().DNSSuffix(),
1957-
})
1958-
dnsSuffix = ep.AwsPartition().DNSSuffix()
1959-
} else {
1960-
resolvedEndpoint, err := partition.EndpointFor("logs", region)
1961-
if err == nil {
1962-
endpoint = resolvedEndpoint.URL
1952+
if engine.cfg.InstanceIPCompatibility.IsIPv6Only() {
1953+
engine.setAWSLogsDualStackEndpoint(task, container, hostConfig)
1954+
} else {
1955+
region := engine.cfg.AWSRegion
1956+
if _, ok := unresolvedIsolatedRegions[region]; ok {
1957+
endpoint := ""
1958+
dnsSuffix := ""
1959+
partition, ok := ep.PartitionForRegion(ep.DefaultPartitions(), region)
1960+
if !ok {
1961+
logger.Warn("No partition resolved for region. Using AWS default", logger.Fields{
1962+
"region": region,
1963+
"defaultDNSSuffix": ep.AwsPartition().DNSSuffix(),
1964+
})
1965+
dnsSuffix = ep.AwsPartition().DNSSuffix()
19631966
} else {
1964-
dnsSuffix = partition.DNSSuffix()
1967+
resolvedEndpoint, err := partition.EndpointFor("logs", region)
1968+
if err == nil {
1969+
endpoint = resolvedEndpoint.URL
1970+
} else {
1971+
dnsSuffix = partition.DNSSuffix()
1972+
}
19651973
}
1974+
if endpoint == "" {
1975+
endpoint = fmt.Sprintf("https://logs.%s.%s", region, dnsSuffix)
1976+
}
1977+
hostConfig.LogConfig.Config[awsLogsEndpointKey] = endpoint
19661978
}
1967-
if endpoint == "" {
1968-
endpoint = fmt.Sprintf("https://logs.%s.%s", region, dnsSuffix)
1969-
}
1970-
hostConfig.LogConfig.Config["awslogs-endpoint"] = endpoint
19711979
}
19721980
}
19731981

@@ -2946,3 +2954,104 @@ func (engine *DockerTaskEngine) getDockerID(task *apitask.Task, container *apico
29462954
}
29472955
return dockerContainer.DockerID, nil
29482956
}
2957+
2958+
// Sets CloudWatch Logs dual stack endpoint as "awslogs-endpoint" option in the logging config.
2959+
// This is needed because awslogs driver that we consume from Docker does not support
2960+
// an option to enable dual stack endpoints, so customers have no way to enable dual stack endpoints
2961+
// that are needed in an IPv6-only environment.
2962+
func (engine *DockerTaskEngine) setAWSLogsDualStackEndpoint(
2963+
task *apitask.Task, container *apicontainer.Container, hostConfig *dockercontainer.HostConfig,
2964+
) {
2965+
// Helper function to populate common logger.Fields
2966+
withAdditionalLoggerFields := func(additionalFields logger.Fields) logger.Fields {
2967+
fields := logger.Fields{field.TaskARN: task.Arn, field.ContainerName: container.Name}
2968+
for k, v := range additionalFields {
2969+
fields[k] = v
2970+
}
2971+
return fields
2972+
}
2973+
2974+
// Do nothing if endpoint is already set
2975+
if hostConfig.LogConfig.Config[awsLogsEndpointKey] != "" {
2976+
logger.Info(
2977+
fmt.Sprintf(
2978+
"%s is already set in awslogs config, skip resolving dual stack CloudWatch Logs endpoint",
2979+
awsLogsEndpointKey),
2980+
withAdditionalLoggerFields(logger.Fields{}),
2981+
)
2982+
return
2983+
}
2984+
2985+
// Region is required to resolve endpoint
2986+
region := hostConfig.LogConfig.Config[awsLogsRegionKey]
2987+
if region == "" {
2988+
logger.Warn(
2989+
fmt.Sprintf(
2990+
"%s not found in awslogs config, skip resolving dual stack CloudWatch Logs endpoint",
2991+
awsLogsRegionKey),
2992+
withAdditionalLoggerFields(logger.Fields{}),
2993+
)
2994+
return
2995+
}
2996+
2997+
// Docker versions older than 18.09.0 do not support awslogs-endpoint
2998+
// option. So, skip endpoint resolution for those Docker versions.
2999+
dockerVersion, err := engine.Version()
3000+
if err != nil {
3001+
logger.Error("Failed to get Docker engine version. Skip resolving dual stack CloudWatch Logs endpoint.",
3002+
withAdditionalLoggerFields(logger.Fields{field.Error: err}))
3003+
return
3004+
}
3005+
const thresholdVersion = "18.09.0"
3006+
dockerVersionIsCompatible, err := utils.Version(dockerVersion).Matches(">=" + thresholdVersion)
3007+
if err != nil {
3008+
logger.Error("Failed to determine if docker version is high enough",
3009+
withAdditionalLoggerFields(logger.Fields{
3010+
field.Error: err,
3011+
field.DockerVersion: dockerVersion,
3012+
"thresholdVersion": thresholdVersion,
3013+
}))
3014+
return
3015+
}
3016+
if !dockerVersionIsCompatible {
3017+
logger.Warn(
3018+
fmt.Sprintf(
3019+
"Docker version does not support %s option. Skip resolving dual stack CloudWatch Logs endpoint.",
3020+
awsLogsEndpointKey),
3021+
withAdditionalLoggerFields(logger.Fields{
3022+
field.DockerVersion: dockerVersion,
3023+
"thresholdVersion": thresholdVersion,
3024+
}),
3025+
)
3026+
return
3027+
}
3028+
3029+
// Resolve the endpoint
3030+
endpoint, err := getAWSLogsDualStackEndpoint(region)
3031+
if err != nil {
3032+
logger.Error(
3033+
"Failed to get CloudWatch Logs dual stack endpoint. Skipping setting it.",
3034+
withAdditionalLoggerFields(logger.Fields{field.Region: region, field.Error: err}))
3035+
return
3036+
}
3037+
3038+
logger.Info("Resolved CloudWatch Logs dual stack endpoint",
3039+
withAdditionalLoggerFields(logger.Fields{
3040+
field.Endpoint: endpoint,
3041+
field.Region: region,
3042+
}))
3043+
hostConfig.LogConfig.Config[awsLogsEndpointKey] = endpoint
3044+
}
3045+
3046+
// Returns CloudWatch Logs dual stack endpoint for the given region.
3047+
func getAWSLogsDualStackEndpoint(region string) (string, error) {
3048+
endpoint, err := cloudwatchlogs.NewDefaultEndpointResolverV2().ResolveEndpoint(context.TODO(),
3049+
cloudwatchlogs.EndpointParameters{
3050+
UseDualStack: ptr.Bool(true),
3051+
Region: ptr.String(region),
3052+
})
3053+
if err != nil {
3054+
return "", fmt.Errorf("failed to resolve dual stack CloudWatch Logs endpoint for region '%s': %w", region, err)
3055+
}
3056+
return endpoint.URI.String(), nil
3057+
}

0 commit comments

Comments
 (0)