diff --git a/README.md b/README.md index 6ae3cec3075..de140d138ed 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,7 @@ additional details on each available environment variable. | `ECS_DYNAMIC_HOST_PORT_RANGE` | `100-200` | This specifies the dynamic host port range that the agent uses to assign host ports from, for container ports mapping. If there are no available ports in the range for containers, including customer containers and Service Connect Agent containers (if Service Connect is enabled), service deployments would fail. | Defined by `/proc/sys/net/ipv4/ip_local_port_range` | `49152-65535` | | `ECS_TASK_PIDS_LIMIT` | `100` | Specifies the per-task pids limit cgroup setting for each task launched on the container instance. This setting maps to the pids.max cgroup setting at the ECS task level. See https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#pid. If unset, pids will be unlimited. Min value is 1 and max value is 4194304 (4*1024*1024) | `unset` | Not Supported on Windows | | `ECS_EBSTA_SUPPORTED` | `true` | Whether to use the container instance with EBS Task Attach support. This variable is set properly by ecs-init. Its value indicates if correct environment to support EBS volumes by instance has been set up or not. ECS only schedules EBSTA tasks if this feature is supported by the platform type. Check [EBS Volume considerations](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ebs-volumes.html#ebs-volume-considerations) for other EBS support details | `true` | Not Supported on Windows | +| `ECS_ENABLE_FIRELENS_ASYNC` | `true` | Whether the log driver connects to the Firelens container in the background. | `true` | `true` | Additionally, the following environment variable(s) can be used to configure the behavior of the ecs-init service. When using ECS-Init, all env variables, including the ECS Agent variables above, are read from path `/etc/ecs/ecs.config`: | Environment Variable Name | Example Value(s) | Description | Default value | diff --git a/agent/config/config.go b/agent/config/config.go index ccbafa79370..5afef0f9212 100644 --- a/agent/config/config.go +++ b/agent/config/config.go @@ -603,6 +603,7 @@ func environmentConfig() (Config, error) { WarmPoolsSupport: parseBooleanDefaultFalseConfig("ECS_WARM_POOLS_CHECK"), DynamicHostPortRange: parseDynamicHostPortRange("ECS_DYNAMIC_HOST_PORT_RANGE"), TaskPidsLimit: parseTaskPidsLimit(), + FirelensAsyncEnabled: parseBooleanDefaultTrueConfig("ECS_ENABLE_FIRELENS_ASYNC"), }, err } diff --git a/agent/config/config_test.go b/agent/config/config_test.go index d2e671dcc13..e8cd724166b 100644 --- a/agent/config/config_test.go +++ b/agent/config/config_test.go @@ -169,6 +169,7 @@ func TestEnvironmentConfig(t *testing.T) { setTestEnv("ECS_HOST_DATA_DIR", "/etc/ecs/") setTestEnv("ECS_CGROUP_CPU_PERIOD", "10ms") setTestEnv("ECS_VOLUME_PLUGIN_CAPABILITIES", "[\"efsAuth\"]") + setTestEnv("ECS_ENABLE_FIRELENS_ASYNC", "true") conf, err := environmentConfig() assert.NoError(t, err) @@ -224,6 +225,7 @@ func TestEnvironmentConfig(t *testing.T) { assert.True(t, conf.ShouldExcludeIPv6PortBinding.Enabled(), "Wrong value for ShouldExcludeIPv6PortBinding") assert.False(t, conf.WarmPoolsSupport.Enabled(), "Wrong value for WarmPoolsSupport") assert.Equal(t, "200-300", conf.DynamicHostPortRange) + assert.True(t, conf.FirelensAsyncEnabled.Enabled()) } func TestTrimWhitespaceWhenCreating(t *testing.T) { @@ -975,6 +977,47 @@ func TestExternalConfigMissingRegion(t *testing.T) { assert.Error(t, err) } +func TestFirelensAsyncEnabled(t *testing.T) { + testCases := []struct { + name string + envVarValue string + expectedResult bool + }{ + { + name: "default, env var not set", + envVarValue: "", + expectedResult: true, + }, + { + name: "explicitly disabled", + envVarValue: "false", + expectedResult: false, + }, + { + name: "explicitly enabled", + envVarValue: "true", + expectedResult: true, + }, + { + name: "env var has invalid value", + envVarValue: "foo", + expectedResult: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Setenv("AWS_DEFAULT_REGION", "us-west-2") + if tc.envVarValue != "" { + t.Setenv("ECS_ENABLE_FIRELENS_ASYNC", tc.envVarValue) + } + cfg, err := NewConfig(ec2.NewBlackholeEC2MetadataClient()) + assert.NoError(t, err) + assert.Equal(t, tc.expectedResult, cfg.FirelensAsyncEnabled.Enabled()) + }) + } +} + func setTestRegion() func() { return setTestEnv("AWS_DEFAULT_REGION", "us-west-2") } diff --git a/agent/config/config_unix.go b/agent/config/config_unix.go index c8ac2ecf9b5..959399217f7 100644 --- a/agent/config/config_unix.go +++ b/agent/config/config_unix.go @@ -121,6 +121,7 @@ func DefaultConfig() Config { CSIDriverSocketPath: defaultCSIDriverSocketPath, NodeStageTimeout: nodeStageTimeout, NodeUnstageTimeout: nodeUnstageTimeout, + FirelensAsyncEnabled: BooleanDefaultTrue{Value: ExplicitlyEnabled}, } } diff --git a/agent/config/config_windows.go b/agent/config/config_windows.go index 431f1b94784..fa0824c892a 100644 --- a/agent/config/config_windows.go +++ b/agent/config/config_windows.go @@ -166,6 +166,7 @@ func DefaultConfig() Config { CSIDriverSocketPath: defaultCSIDriverSocketPath, NodeStageTimeout: nodeStageTimeout, NodeUnstageTimeout: nodeUnstageTimeout, + FirelensAsyncEnabled: BooleanDefaultTrue{Value: ExplicitlyEnabled}, } } diff --git a/agent/config/parse.go b/agent/config/parse.go index 11059c83f23..5a8e46403cd 100644 --- a/agent/config/parse.go +++ b/agent/config/parse.go @@ -244,45 +244,45 @@ func parseAdditionalLocalRoutes(errs []error) ([]cniTypes.IPNet, []error) { } func parseBooleanDefaultFalseConfig(envVarName string) BooleanDefaultFalse { - boolDefaultFalseCofig := BooleanDefaultFalse{Value: NotSet} + boolDefaultFalseConfig := BooleanDefaultFalse{Value: NotSet} configString := strings.TrimSpace(os.Getenv(envVarName)) if configString == "" { // if intentionally not set, do not add warning log - return boolDefaultFalseCofig + return boolDefaultFalseConfig } res, err := strconv.ParseBool(configString) if err == nil { if res { - boolDefaultFalseCofig.Value = ExplicitlyEnabled + boolDefaultFalseConfig.Value = ExplicitlyEnabled } else { - boolDefaultFalseCofig.Value = ExplicitlyDisabled + boolDefaultFalseConfig.Value = ExplicitlyDisabled } } else { seelog.Warnf("Invalid format for \"%s\", expected a boolean. err %v", envVarName, err) } - return boolDefaultFalseCofig + return boolDefaultFalseConfig } func parseBooleanDefaultTrueConfig(envVarName string) BooleanDefaultTrue { - boolDefaultTrueCofig := BooleanDefaultTrue{Value: NotSet} + boolDefaultTrueConfig := BooleanDefaultTrue{Value: NotSet} configString := strings.TrimSpace(os.Getenv(envVarName)) if configString == "" { // if intentionally not set, do not add warning log - return boolDefaultTrueCofig + return boolDefaultTrueConfig } res, err := strconv.ParseBool(configString) if err == nil { if res { - boolDefaultTrueCofig.Value = ExplicitlyEnabled + boolDefaultTrueConfig.Value = ExplicitlyEnabled } else { - boolDefaultTrueCofig.Value = ExplicitlyDisabled + boolDefaultTrueConfig.Value = ExplicitlyDisabled } } else { seelog.Warnf("Invalid format for \"%s\", expected a boolean. err %v", envVarName, err) } - return boolDefaultTrueCofig + return boolDefaultTrueConfig } func parseTaskMetadataThrottles() (int, int) { diff --git a/agent/config/types.go b/agent/config/types.go index c54f28c869d..20cc9468d99 100644 --- a/agent/config/types.go +++ b/agent/config/types.go @@ -394,4 +394,8 @@ type Config struct { // NodeUnstageTimeout is the amount of time to wait for unstaging an EBS TA volume NodeUnstageTimeout time.Duration + + // FirelensAsyncEnabled specifies whether the agent should enable the async connect option of the + // fluentd log driver. Ref: https://docs.docker.com/engine/logging/drivers/fluentd/#fluentd-async + FirelensAsyncEnabled BooleanDefaultTrue } diff --git a/agent/engine/docker_task_engine.go b/agent/engine/docker_task_engine.go index 9a3a0494065..fec3a3a19a8 100644 --- a/agent/engine/docker_task_engine.go +++ b/agent/engine/docker_task_engine.go @@ -2081,7 +2081,8 @@ func (engine *DockerTaskEngine) createContainer(task *apitask.Task, container *a return metadata } -func getFirelensLogConfig(task *apitask.Task, container *apicontainer.Container, hostConfig *dockercontainer.HostConfig, cfg *config.Config) dockercontainer.LogConfig { +func getFirelensLogConfig(task *apitask.Task, container *apicontainer.Container, + hostConfig *dockercontainer.HostConfig, cfg *config.Config) dockercontainer.LogConfig { fields := strings.Split(task.Arn, "/") taskID := fields[len(fields)-1] tag := fmt.Sprintf(fluentTagDockerFormat, container.Name, taskID) @@ -2092,7 +2093,7 @@ func getFirelensLogConfig(task *apitask.Task, container *apicontainer.Container, logConfig.Config = make(map[string]string) logConfig.Config[logDriverTag] = tag logConfig.Config[logDriverFluentdAddress] = fluentd - logConfig.Config[logDriverAsyncConnect] = strconv.FormatBool(true) + logConfig.Config[logDriverAsyncConnect] = strconv.FormatBool(cfg.FirelensAsyncEnabled.Enabled()) logConfig.Config[logDriverSubSecondPrecision] = strconv.FormatBool(true) if bufferLimitExists { logConfig.Config[logDriverBufferLimit] = bufferLimit diff --git a/agent/engine/docker_task_engine_test.go b/agent/engine/docker_task_engine_test.go index a4f8de77202..8ff9264e6f7 100644 --- a/agent/engine/docker_task_engine_test.go +++ b/agent/engine/docker_task_engine_test.go @@ -5034,3 +5034,72 @@ func TestSetRegistryCredentials(t *testing.T) { }) } } + +func TestGetFirelensConfigWithAsyncEnabledConfigOption(t *testing.T) { + rawHostConfigInput := &dockercontainer.HostConfig{ + LogConfig: dockercontainer.LogConfig{ + Type: "awsfirelens", + Config: map[string]string{ + "log-driver-buffer-limit": "10000", + }, + }, + } + + rawHostConfig, err := json.Marshal(&rawHostConfigInput) + require.NoError(t, err) + hostConfig := func() *string { + s := string(rawHostConfig) + return &s + }() + + appContainer := &apicontainer.Container{ + Name: "app", + DockerConfig: apicontainer.DockerConfig{ + HostConfig: hostConfig, + }, + } + + firelensContainer := &apicontainer.Container{ + Name: "firelens", + FirelensConfig: &apicontainer.FirelensConfig{ + Type: "fluentbit", + }, + } + + task := &apitask.Task{ + Arn: "arn:aws:ecs:region:account-id:task/task-id", + Containers: []*apicontainer.Container{ + appContainer, + firelensContainer, + }, + } + + testCases := []struct { + name string + isFirelensAsyncEnabled string + }{ + { + name: "async enabled", + isFirelensAsyncEnabled: "true", + }, + { + name: "async disabled", + isFirelensAsyncEnabled: "false", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Initialize config with the appropriate FirelensAsyncEnabled value as per the test case. + asyncEnabled := config.BooleanDefaultTrue{Value: config.ExplicitlyDisabled} + if tc.isFirelensAsyncEnabled == "true" { + asyncEnabled = config.BooleanDefaultTrue{Value: config.ExplicitlyEnabled} + } + cfg := &config.Config{ + FirelensAsyncEnabled: asyncEnabled, + } + logConfig := getFirelensLogConfig(task, appContainer, rawHostConfigInput, cfg) + assert.Equal(t, tc.isFirelensAsyncEnabled, logConfig.Config[logDriverAsyncConnect]) + }) + } +}