@@ -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"
@@ -97,6 +101,7 @@ const (
97101 logDriverFluentdAddress = "fluentd-address"
98102 dataLogDriverPath = "/data/firelens/"
99103 logDriverAsyncConnect = "fluentd-async-connect"
104+ logDriverAsync = "fluentd-async"
100105 logDriverSubSecondPrecision = "fluentd-sub-second-precision"
101106 logDriverBufferLimit = "fluentd-buffer-limit"
102107 dataLogDriverSocketPath = "/socket/fluent.sock"
@@ -1874,7 +1879,35 @@ func (engine *DockerTaskEngine) createContainer(task *apitask.Task, container *a
18741879 // Update the environment variables FLUENT_HOST and FLUENT_PORT depending on the supported network modes - bridge
18751880 // and awsvpc. For reference - https://docs.docker.com/config/containers/logging/fluentd/.
18761881 if hostConfig .LogConfig .Type == logDriverTypeFirelens {
1877- hostConfig .LogConfig = getFirelensLogConfig (task , container , hostConfig , engine .cfg )
1882+ // We need the Docker server version in order to generate the appropriate firelens log config
1883+ dockerServerVersion , err := engine .Version ()
1884+ if err != nil {
1885+ logger .Error ("Failed to determine Docker server version for Firelens log config generation" , logger.Fields {
1886+ field .TaskID : task .GetID (),
1887+ field .Container : container .Name ,
1888+ field .Error : err ,
1889+ })
1890+ return dockerapi.DockerContainerMetadata {
1891+ Error : dockerapi.CannotCreateContainerError {FromError : errors .Wrapf (versionErr ,
1892+ "failed to create container - container uses awsfirelens log driver and we failed to " +
1893+ "determine the Docker server version for Firelens log config generation" )},
1894+ }
1895+ }
1896+ logConfig , err := getFirelensLogConfig (task , container , hostConfig , engine .cfg , dockerServerVersion )
1897+ if err != nil {
1898+ logger .Error ("Failed to generate the Firelens log config" , logger.Fields {
1899+ field .TaskID : task .GetID (),
1900+ field .Container : container .Name ,
1901+ field .Error : err ,
1902+ })
1903+ return dockerapi.DockerContainerMetadata {
1904+ Error : dockerapi.CannotCreateContainerError {FromError : errors .Wrapf (err ,
1905+ "failed to create container - container uses awsfirelens log driver and we failed to " +
1906+ "generate the Firelens log config" )},
1907+ }
1908+ }
1909+ hostConfig .LogConfig = logConfig
1910+
18781911 if task .IsNetworkModeAWSVPC () {
18791912 container .MergeEnvironmentVariables (map [string ]string {
18801913 fluentNetworkHost : FluentAWSVPCHostValue ,
@@ -1916,29 +1949,33 @@ func (engine *DockerTaskEngine) createContainer(task *apitask.Task, container *a
19161949
19171950 // This is a short term solution only for specific regions
19181951 if hostConfig .LogConfig .Type == logDriverTypeAwslogs {
1919- region := engine .cfg .AWSRegion
1920- if _ , ok := unresolvedIsolatedRegions [region ]; ok {
1921- endpoint := ""
1922- dnsSuffix := ""
1923- partition , ok := ep .PartitionForRegion (ep .DefaultPartitions (), region )
1924- if ! ok {
1925- logger .Warn ("No partition resolved for region. Using AWS default" , logger.Fields {
1926- "region" : region ,
1927- "defaultDNSSuffix" : ep .AwsPartition ().DNSSuffix (),
1928- })
1929- dnsSuffix = ep .AwsPartition ().DNSSuffix ()
1930- } else {
1931- resolvedEndpoint , err := partition .EndpointFor ("logs" , region )
1932- if err == nil {
1933- 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 ()
19341966 } else {
1935- 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+ }
19361973 }
1974+ if endpoint == "" {
1975+ endpoint = fmt .Sprintf ("https://logs.%s.%s" , region , dnsSuffix )
1976+ }
1977+ hostConfig .LogConfig .Config [awsLogsEndpointKey ] = endpoint
19371978 }
1938- if endpoint == "" {
1939- endpoint = fmt .Sprintf ("https://logs.%s.%s" , region , dnsSuffix )
1940- }
1941- hostConfig .LogConfig .Config ["awslogs-endpoint" ] = endpoint
19421979 }
19431980 }
19441981
@@ -2108,29 +2145,64 @@ func (engine *DockerTaskEngine) createContainer(task *apitask.Task, container *a
21082145 return metadata
21092146}
21102147
2111- func getFirelensLogConfig (task * apitask.Task , container * apicontainer.Container ,
2112- hostConfig * dockercontainer.HostConfig , cfg * config.Config ) dockercontainer.LogConfig {
2148+ // getFirelensLogConfig generates the fluentd log driver's log config.
2149+ // Every container that wants to use Firelens for logging, gets one instance of the fluentd log driver associated to it.
2150+ func getFirelensLogConfig (task * apitask.Task ,
2151+ container * apicontainer.Container ,
2152+ hostConfig * dockercontainer.HostConfig ,
2153+ cfg * config.Config , dockerServerVersion string ) (dockercontainer.LogConfig , error ) {
2154+ var firelensConfig dockercontainer.LogConfig
2155+ // Set the log driver type
2156+ firelensConfig .Type = logDriverTypeFluentd
2157+ // Start from the existing container host config
2158+ hostConfigLogConfig := hostConfig .LogConfig
2159+ // Initialize a config to store the different log driver options
2160+ firelensConfig .Config = make (map [string ]string )
2161+ // Generate a tag based on the task ID
21132162 fields := strings .Split (task .Arn , "/" )
21142163 taskID := fields [len (fields )- 1 ]
21152164 tag := fmt .Sprintf (fluentTagDockerFormat , container .Name , taskID )
2165+ firelensConfig .Config [logDriverTag ] = tag
2166+ // Construct the fluent socket address
21162167 fluentd := socketPathPrefix + filepath .Join (cfg .DataDirOnHost , dataLogDriverPath , taskID , dataLogDriverSocketPath )
2117- logConfig := hostConfig .LogConfig
2118- bufferLimit , bufferLimitExists := logConfig .Config [apitask .FirelensLogDriverBufferLimitOption ]
2119- logConfig .Type = logDriverTypeFluentd
2120- logConfig .Config = make (map [string ]string )
2121- logConfig .Config [logDriverTag ] = tag
2122- logConfig .Config [logDriverFluentdAddress ] = fluentd
2123- logConfig .Config [logDriverAsyncConnect ] = strconv .FormatBool (cfg .FirelensAsyncEnabled .Enabled ())
2124- logConfig .Config [logDriverSubSecondPrecision ] = strconv .FormatBool (true )
2168+ firelensConfig .Config [logDriverFluentdAddress ] = fluentd
2169+ // Enable sub-second precision
2170+ firelensConfig .Config [logDriverSubSecondPrecision ] = strconv .FormatBool (true )
2171+ // Set the log driver buffer limit if passed via the task payload
2172+ bufferLimit , bufferLimitExists := hostConfigLogConfig .Config [apitask .FirelensLogDriverBufferLimitOption ]
21252173 if bufferLimitExists {
2126- logConfig .Config [logDriverBufferLimit ] = bufferLimit
2174+ firelensConfig .Config [logDriverBufferLimit ] = bufferLimit
2175+ }
2176+ // Determine whether to use the "fluentd-async" option or the legacy "fluentd-async-connect" option.
2177+ // "fluentd-async-connect" option was deprecated in Docker v20.10.0 and removed in v28.0.0.
2178+ // It was replaced with the "fluentd-async" option starting Docker v20.10.0.
2179+ // Docker v20.10.0 release notes: https://docs.docker.com/engine/release-notes/20.10/#logging-2
2180+ // Docker v28.0.0 release notes: https://docs.docker.com/engine/release-notes/28/#removed
2181+ // This change is not versioned and applies to all Docker client API versions.
2182+ // Hence, in order to preserve backwards-compatibility with Docker server versions older than v20.10.0,
2183+ // we need to continue using the older fluentd-async-connect option.
2184+ isAsyncCompatible , err := utils .Version (dockerServerVersion ).Matches (">=20.10.0" )
2185+ if err != nil {
2186+ logger .Error ("Unable to determine whether the Docker server version is at least 20.10.0" , logger.Fields {
2187+ field .TaskID : task .GetID (),
2188+ field .Container : container .Name ,
2189+ field .Error : err ,
2190+ })
2191+ return dockercontainer.LogConfig {}, errors .Wrapf (err ,
2192+ "unable to determine whether the Docker server version is at least 20.10.0" )
2193+ }
2194+ if isAsyncCompatible {
2195+ firelensConfig .Config [logDriverAsync ] = strconv .FormatBool (cfg .FirelensAsyncEnabled .Enabled ())
2196+ } else {
2197+ firelensConfig .Config [logDriverAsyncConnect ] = strconv .FormatBool (cfg .FirelensAsyncEnabled .Enabled ())
21272198 }
2199+
21282200 logger .Debug ("Applying firelens log config for container" , logger.Fields {
21292201 field .TaskID : task .GetID (),
21302202 field .Container : container .Name ,
2131- "config" : logConfig ,
2203+ "config" : firelensConfig ,
21322204 })
2133- return logConfig
2205+ return firelensConfig , nil
21342206}
21352207
21362208func (engine * DockerTaskEngine ) startContainer (task * apitask.Task , container * apicontainer.Container ) dockerapi.DockerContainerMetadata {
@@ -2882,3 +2954,104 @@ func (engine *DockerTaskEngine) getDockerID(task *apitask.Task, container *apico
28822954 }
28832955 return dockerContainer .DockerID , nil
28842956}
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