Skip to content

ECS - network mode HOST doesn't work (fails healthcheck)

0

I have this CDK code. As shown, the container task fails to deploy, due to failed ALB healthcheck. It deploys fine if I change the NetworkMode to AWSVPC (and uncomment related lines), but then my task doesn't have Internet access. I really want to keep HOST mode in the hopes that I can use the Hosts's Public IP and avoid adding a NAT. I also tried BRIDGE mode and the result was similar. What's missing to get HOST mode working properly?

package main

import (
	"fmt"

	"github.com/aws/aws-cdk-go/awscdk/v2"
	"github.com/aws/aws-cdk-go/awscdk/v2/awsautoscaling"
	"github.com/aws/aws-cdk-go/awscdk/v2/awscodepipeline"
	"github.com/aws/aws-cdk-go/awscdk/v2/awscodepipelineactions"
	"github.com/aws/aws-cdk-go/awscdk/v2/awsec2"
	"github.com/aws/aws-cdk-go/awscdk/v2/awsecr"
	"github.com/aws/aws-cdk-go/awscdk/v2/awsecs"
	"github.com/aws/aws-cdk-go/awscdk/v2/awselasticloadbalancingv2"
	"github.com/aws/aws-cdk-go/awscdk/v2/awsiam"
	"github.com/aws/aws-cdk-go/awscdk/v2/awslambda"
	"github.com/aws/aws-cdk-go/awscdk/v2/awslogs"

	"github.com/aws/jsii-runtime-go"
)

const TARGET_CONTAINER_NAME = "target"

const PREVIEW_CONTAINER_NAME = "preview"

const containerMemoryMIB = 1800

func instanceRequirements() *awsautoscaling.CfnAutoScalingGroup_InstanceRequirementsProperty {
	return &awsautoscaling.CfnAutoScalingGroup_InstanceRequirementsProperty{
		MemoryMiB: &awsautoscaling.CfnAutoScalingGroup_MemoryMiBRequestProperty{
			Min: jsii.Number(containerMemoryMIB),
			Max: jsii.Number(containerMemoryMIB * 3),
		},
		VCpuCount: &awsautoscaling.CfnAutoScalingGroup_VCpuCountRequestProperty{
			Min: jsii.Number(1),
			Max: jsii.Number(4),
		},
		BurstablePerformance: jsii.String("included"),
	}
}

type ContainerProps struct {
	awscdk.StackProps
	MonostackProps Route66MonoStackProps
	Network        *Route66Network
	Database       ListingDataInfo
	EcrRepo        awsecr.IRepository
	ContainerName  string
	Alb            awselasticloadbalancingv2.IApplicationLoadBalancer
	AlbSg          awsec2.ISecurityGroup
	Listener       awselasticloadbalancingv2.IApplicationListener
}

type ContainerInfo struct {
	Cluster          awsecs.ICluster
	Service          awsecs.IBaseService
	TaskDefinition   awsecs.ITaskDefinition
	AutoScalingGroup awsautoscaling.IAutoScalingGroup
}

type EcsContainerPlatform struct {
	preview *ContainerInfo
	target  *ContainerInfo
}

func (platform *EcsContainerPlatform) NewOriginPair(
	props *Route66MonoStackProps,
	infraStack awscdk.Stack,
	originStack awscdk.Stack,
	network *Route66Network,
	db *ListingDataInfo,
) HttpOriginPair {

	vpc := network.Vpc
	alb := awselasticloadbalancingv2.NewApplicationLoadBalancer(infraStack, jsii.String("LoadBalancer"), &awselasticloadbalancingv2.ApplicationLoadBalancerProps{
		Vpc:              vpc,
		InternetFacing:   jsii.Bool(true),
		LoadBalancerName: jsii.String(props.Branch + "-alb"),
	})

	albSecurityGroup := awsec2.NewSecurityGroup(infraStack, jsii.String("LoadBalancerSecurityGroup"), &awsec2.SecurityGroupProps{
		Vpc:              vpc,
		AllowAllOutbound: jsii.Bool(true),
		Description:      jsii.String("Security group for Load Balancer"),
	})
	// inbound
	albSecurityGroup.AddIngressRule(
		awsec2.Peer_AnyIpv4(),
		awsec2.Port_TcpRange(jsii.Number(9080), jsii.Number(9081)),
		jsii.String("Allow HTTP traffic"),
		nil,
	)
	// allow ipv6 too
	albSecurityGroup.AddIngressRule(
		awsec2.Peer_AnyIpv6(),
		awsec2.Port_TcpRange(jsii.Number(9080), jsii.Number(9081)),
		jsii.String("Allow HTTP traffic"),
		nil,
	)
	listener9080 := alb.AddListener(jsii.String("Listener9080"), &awselasticloadbalancingv2.BaseApplicationListenerProps{
		Port:     jsii.Number(9080),
		Protocol: awselasticloadbalancingv2.ApplicationProtocol_HTTP,
	})
	listener9081 := alb.AddListener(jsii.String("Listener9081"), &awselasticloadbalancingv2.BaseApplicationListenerProps{
		Port:     jsii.Number(9081),
		Protocol: awselasticloadbalancingv2.ApplicationProtocol_HTTP,
	})

	platform.target = addContainerStack(infraStack, originStack, &ContainerProps{
		ContainerName:  TARGET_CONTAINER_NAME, // must match in build-image.py
		MonostackProps: *props,
		Network:        network,
		Database:       *db,
		AlbSg:          albSecurityGroup,
		Alb:            alb,
		Listener:       listener9080,
	})

	platform.preview = addContainerStack(infraStack, originStack, &ContainerProps{
		ContainerName:  PREVIEW_CONTAINER_NAME, // must match in build-image.py
		MonostackProps: *props,
		Network:        network,
		Database:       *db,
		AlbSg:          albSecurityGroup,
		Alb:            alb,
		Listener:       listener9081,
	})

	return HttpOriginPair{
		TargetHostname:  *alb.LoadBalancerDnsName(),
		TargetPort:      9080,
		PreviewHostname: *alb.LoadBalancerDnsName(),
		PreviewPort:     9081,
	}
}

func (ecs *EcsContainerPlatform) Deploy(
	infraStack awscdk.Stack,
	originStack awscdk.Stack,
	opsStack awscdk.Stack,
	props Route66MonoStackProps,
) {

	activationLambda, activationOptions := ecsActivation(opsStack, ecs.preview)

	instanceRole := awsiam.NewRole(infraStack, jsii.String("InstanceRole"), &awsiam.RoleProps{
		AssumedBy: awsiam.NewServicePrincipal(jsii.String("ec2.amazonaws.com"), nil),
	})
	// make admin
	instanceRole.AddManagedPolicy(awsiam.ManagedPolicy_FromAwsManagedPolicyName(jsii.String("AdministratorAccess")))

	codePipeline, dockerOutput := addCiBuildPipeline(opsStack, props, activationOptions)

	ecsDeploymentSteps(codePipeline, dockerOutput, ecs.target, ecs.preview, activationLambda)
}

func addContainerStack(infraStack awscdk.Stack, originStack awscdk.Stack, props *ContainerProps) *ContainerInfo {

	vpc := props.Network.Vpc
	containerName := props.ContainerName
	containerPort := jsii.Number(80)

	logGroup := awslogs.NewLogGroup(infraStack, jsii.String(containerName+"Log"), &awslogs.LogGroupProps{
		LogGroupName: func() *string {
			if props.ContainerName == TARGET_CONTAINER_NAME {
				return jsii.String(props.MonostackProps.LogGroup)
			} else {
				return jsii.String(props.MonostackProps.LogGroup + "-PREVIEW")
			}
		}(),
		Retention:     awslogs.RetentionDays_INFINITE,
		RemovalPolicy: awscdk.RemovalPolicy_DESTROY,
	})
	logGroupName := jsii.String(*logGroup.LogGroupName())

	taskSg := awsec2.NewSecurityGroup(infraStack, jsii.String(containerName+"TaskSg"), &awsec2.SecurityGroupProps{
		Vpc:              vpc,
		AllowAllOutbound: jsii.Bool(true),
		Description:      jsii.String("Security group for ECS tasks"),
	})

	taskSg.AddIngressRule(
		props.AlbSg,
		awsec2.Port_Tcp(jsii.Number(*containerPort)),
		jsii.String("Allow traffic from ALB to Task"),
		nil,
	)

	hostSg := awsec2.NewSecurityGroup(infraStack, jsii.String(containerName+"HostSG"), &awsec2.SecurityGroupProps{
		Vpc:              vpc,
		AllowAllOutbound: jsii.Bool(true),
		Description:      jsii.String("Security group for ECS EC2 hosts"),
	})

	hostSg.AddIngressRule(
		props.AlbSg,
		awsec2.Port_AllTraffic(),
		jsii.String("Allow traffic from ALB to Host"),
		nil,
	)

	clusterName := props.ContainerName + "Cluster"

	// Create execution role for ECS tasks
	executionRole := awsiam.NewRole(infraStack, jsii.String(containerName+"ExeRole"), &awsiam.RoleProps{
		AssumedBy: awsiam.NewServicePrincipal(jsii.String("ecs-tasks.amazonaws.com"), nil),
	})

	executionRole.AddManagedPolicy(awsiam.ManagedPolicy_FromAwsManagedPolicyName(jsii.String("service-role/AmazonECSTaskExecutionRolePolicy")))
	// add admin for now
	executionRole.AddManagedPolicy(awsiam.ManagedPolicy_FromAwsManagedPolicyName(jsii.String("AdministratorAccess")))

	taskRole := awsiam.NewRole(infraStack, jsii.String(containerName+"TaskRole"), &awsiam.RoleProps{
		AssumedBy: awsiam.NewServicePrincipal(jsii.String("ecs-tasks.amazonaws.com"), nil),
	})

	taskRole.AddManagedPolicy(awsiam.ManagedPolicy_FromAwsManagedPolicyName(jsii.String("service-role/AmazonECSTaskExecutionRolePolicy")))
	// add admin for now
	taskRole.AddManagedPolicy(awsiam.ManagedPolicy_FromAwsManagedPolicyName(jsii.String("AdministratorAccess")))

	var taskDefinition awsecs.TaskDefinition
	var service awsecs.BaseService

	// EC2 Spot configuration
	instanceRole := awsiam.NewRole(infraStack, jsii.String(containerName+"InstRole"), &awsiam.RoleProps{
		AssumedBy: awsiam.NewServicePrincipal(jsii.String("ec2.amazonaws.com"), nil),
	})
	instanceRole.AddManagedPolicy(awsiam.ManagedPolicy_FromAwsManagedPolicyName(jsii.String("AmazonSSMManagedInstanceCore")))
	instanceRole.AddManagedPolicy(awsiam.ManagedPolicy_FromAwsManagedPolicyName(jsii.String("service-role/AmazonEC2ContainerServiceforEC2Role")))

	instanceProfile := awsiam.NewInstanceProfile(infraStack, jsii.String(containerName+"Profile"), &awsiam.InstanceProfileProps{
		Role: instanceRole,
	})

	userData := awsec2.UserData_Custom(jsii.String(fmt.Sprintf(`#!/bin/bash
echo "ECS_CLUSTER=%s" >> /etc/ecs/ecs.config
echo "ECS_ENABLE_SPOT_INSTANCE_DRAINING=true" >> /etc/ecs/ecs.config
`, clusterName)))

	asg := awsautoscaling.NewAutoScalingGroup(infraStack, jsii.String(containerName+"Asg"), &awsautoscaling.AutoScalingGroupProps{
		AutoScalingGroupName: jsii.String(props.MonostackProps.Branch + "-" + containerName + "-asg"),
		Vpc:                  props.Network.Vpc,
		MinCapacity:          jsii.Number(1),
		MaxCapacity:          jsii.Number(3),
		DesiredCapacity:      jsii.Number(1),
		CapacityRebalance:    jsii.Bool(true),
		//Setting 'associatePublicIpAddress' must not be set when 'launchTemplate' or 'mixedInstancesPolicy' is set
		//AssociatePublicIpAddress: jsii.Bool(true),
		VpcSubnets: &awsec2.SubnetSelection{
			SubnetType: awsec2.SubnetType_PUBLIC,
		},
		MixedInstancesPolicy: &awsautoscaling.MixedInstancesPolicy{
			InstancesDistribution: &awsautoscaling.InstancesDistribution{
				SpotAllocationStrategy:              awsautoscaling.SpotAllocationStrategy_LOWEST_PRICE,
				OnDemandAllocationStrategy:          awsautoscaling.OnDemandAllocationStrategy_LOWEST_PRICE,
				OnDemandBaseCapacity:                jsii.Number(0),
				OnDemandPercentageAboveBaseCapacity: jsii.Number(0),
				SpotInstancePools:                   jsii.Number(2),
			},
			LaunchTemplateOverrides: &[]*awsautoscaling.LaunchTemplateOverrides{{
				InstanceRequirements: instanceRequirements(),
			}},
			LaunchTemplate: awsec2.NewLaunchTemplate(infraStack, jsii.String(containerName+"LT"), &awsec2.LaunchTemplateProps{
				InstanceProfile: instanceProfile,
				MachineImage: awsecs.EcsOptimizedImage_AmazonLinux2(
					props.MonostackProps.HardwareType,
					&awsecs.EcsOptimizedImageOptions{},
				),
				UserData:                 userData,
				RequireImdsv2:            jsii.Bool(true),
				AssociatePublicIpAddress: jsii.Bool(true),
				SecurityGroup:            hostSg,
			}),
		},
	})

	spotCapacityProvider := awsecs.NewAsgCapacityProvider(infraStack, jsii.String(containerName+"CapacityProvider"), &awsecs.AsgCapacityProviderProps{
		AutoScalingGroup:                   asg,
		EnableManagedTerminationProtection: jsii.Bool(false),
		EnableManagedScaling:               jsii.Bool(true),
		TargetCapacityPercent:              jsii.Number(100),
		SpotInstanceDraining:               jsii.Bool(true),
	})

	cluster := awsecs.NewCluster(infraStack, jsii.String(containerName+"Cluster"), &awsecs.ClusterProps{
		Vpc:         vpc,
		ClusterName: jsii.String(clusterName),
	})
	//cluster.Node().AddDependency(spotCapacityProvider)

	cluster.AddAsgCapacityProvider(spotCapacityProvider, &awsecs.AddAutoScalingGroupCapacityOptions{})

	taskDefinition = awsecs.NewEc2TaskDefinition(originStack, jsii.String(containerName+"TaskDef"), &awsecs.Ec2TaskDefinitionProps{
		NetworkMode:   awsecs.NetworkMode_HOST,
		ExecutionRole: executionRole,
		TaskRole:      taskRole,
	})

	service = awsecs.NewEc2Service(originStack, jsii.String(containerName+"Service"), &awsecs.Ec2ServiceProps{
		ServiceName:    jsii.String(props.MonostackProps.Branch + "-" + containerName + "-service"),
		Cluster:        cluster,
		TaskDefinition: taskDefinition,
		//vpcSubnets, securityGroup(s) and assignPublicIp can only be used in AwsVpc networking mode
		// SecurityGroups: &[]awsec2.ISecurityGroup{taskSg},
		// VpcSubnets: &awsec2.SubnetSelection{
		// 	SubnetType: awsec2.SubnetType_PUBLIC,
		// },
		// FARGATE ONLY:
		// AssignPublicIp:         jsii.Bool(true),
		DesiredCount:           jsii.Number(1),
		HealthCheckGracePeriod: awscdk.Duration_Seconds(jsii.Number(60)),
		// CircuitBreaker: &awsecs.DeploymentCircuitBreaker{
		// 	Enable:   jsii.Bool(props.ContainerName == TARGET_CONTAINER_NAME),
		// 	Rollback: jsii.Bool(props.ContainerName == TARGET_CONTAINER_NAME),
		// },
		MaxHealthyPercent: jsii.Number(200),
		MinHealthyPercent: jsii.Number(100),
		CapacityProviderStrategies: &[]*awsecs.CapacityProviderStrategy{
			{
				CapacityProvider: spotCapacityProvider.CapacityProviderName(),
				Weight:           jsii.Number(1),
				Base:             jsii.Number(1),
			},
		},
		PlacementStrategies: &[]awsecs.PlacementStrategy{
			// 1. Spread across AZs first
			awsecs.PlacementStrategy_SpreadAcross(
				jsii.String("attribute:ecs.availability-zone")),

			// 2. Binpack memory within AZ
			awsecs.PlacementStrategy_PackedByMemory(),
		},
	})

	configPath := props.MonostackProps.CtcConfigPath

	env := &map[string]*string{
		"ctc_config_path":       jsii.String(configPath),
		"DB_USERNAME_PGSQL":     jsii.String(props.Database.Username),
		"DB_HOSTNAME_PGSQL":     props.Database.Hostname.StringValue(),
		"DB_PASSWORD_PGSQL":     props.Database.Password.StringValue(),
		"DYNAMODB_REGION":       jsii.String(props.MonostackProps.DynamodbRegion),
		"DYNAMODB_TABLE_PREFIX": jsii.String(props.MonostackProps.DynamodbTablePrefix),
	}

	if logGroupName != nil {
		(*env)["LOG_GROUP"] = logGroupName
	}

	// Then use the secret in your container definition
	taskDefinition.AddContainer(jsii.String(containerName), &awsecs.ContainerDefinitionOptions{
		ContainerName: jsii.String(containerName),
		Image: awsecs.ContainerImage_FromRegistry(
			jsii.String("public.ecr.aws/nginx/nginx:alpine"), nil),
		//Cpu:                  jsii.Number(1024),
		MemoryLimitMiB:       jsii.Number(containerMemoryMIB),
		MemoryReservationMiB: jsii.Number(containerMemoryMIB),
		PortMappings: &[]*awsecs.PortMapping{{
			ContainerPort: containerPort,
			Protocol:      awsecs.Protocol_TCP,
			HostPort:      containerPort,
		}},
		Environment: env,
		// Doesn't work with ElasticBeanstalk, so for consistency, don't use it in ECS:
		// Secrets: &map[string]awsecs.Secret{
		// 	"DB_PASSWORD_PGSQL": awsecs.Secret_FromSsmParameter(
		// 		awsssm.StringParameter_FromSecureStringParameterAttributes(
		// 			stack,
		// 			jsii.String("DB_PASSWORD_PGSQL"+containerName),
		// 			&awsssm.SecureStringParameterAttributes{
		// 				ParameterName: jsii.String("/route66-ctc-web/pgsql-db-password-readonly"),
		// 				Version:       nil,
		// 			},
		// 		),
		// 	),
		// },
		Logging: awsecs.LogDrivers_AwsLogs(&awsecs.AwsLogDriverProps{
			StreamPrefix: jsii.Str
3 Answers
0
Accepted Answer

Eventually resolved, the problem was here:

	alb := awselasticloadbalancingv2.NewApplicationLoadBalancer(infraStack, jsii.String("LoadBalancer"), &awselasticloadbalancingv2.ApplicationLoadBalancerProps{
		Vpc:              vpc,
		InternetFacing:   jsii.Bool(true),
		LoadBalancerName: jsii.String(props.Branch + "-alb"),
	})

	albSecurityGroup := awsec2.NewSecurityGroup(infraStack, jsii.String("LoadBalancerSecurityGroup"), &awsec2.SecurityGroupProps{
		Vpc:              vpc,
		AllowAllOutbound: jsii.Bool(true),
		Description:      jsii.String("Security group for Load Balancer"),
	})

Root cause: The albSecurityGroup had to be added to the alb.

answered a year ago

0

The code got truncated, here's the rest

		Logging: awsecs.LogDrivers_AwsLogs(&awsecs.AwsLogDriverProps{
			StreamPrefix: jsii.String("/ecs/route66"),
			LogGroup:     logGroup,
		}),
		HealthCheck: &awsecs.HealthCheck{
			Command:     jsii.Strings("CMD-SHELL", "true"),
			Interval:    awscdk.Duration_Seconds(jsii.Number(30)),
			Timeout:     awscdk.Duration_Seconds(jsii.Number(15)),
			StartPeriod: awscdk.Duration_Seconds(jsii.Number(15)),
			Retries:     jsii.Number(3),
		},
	})

	props.Listener.AddTargets(jsii.String("ECS"), &awselasticloadbalancingv2.AddApplicationTargetsProps{
		Port:    containerPort,
		Targets: &[]awselasticloadbalancingv2.IApplicationLoadBalancerTarget{service},
		HealthCheck: &awselasticloadbalancingv2.HealthCheck{
			Path:             jsii.String("/"),
			HealthyHttpCodes: jsii.String("200,300"),
			Interval:         awscdk.Duration_Seconds(jsii.Number(10)),
			Timeout:          awscdk.Duration_Seconds(jsii.Number(5)),
		},
		SlowStart:           awscdk.Duration_Seconds(jsii.Number(60)),
		DeregistrationDelay: awscdk.Duration_Seconds(jsii.Number(30)),
		TargetGroupName:     jsii.String(props.MonostackProps.Branch + "-" + containerName),
	})

	return &ContainerInfo{
		Cluster:          cluster,
		Service:          service,
		AutoScalingGroup: asg,
		TaskDefinition:   taskDefinition,
	}
}

func ecsActivation(stack awscdk.Stack, previewContainer *ContainerInfo) (awslambda.Function, *awscodepipeline.StageOptions) {

	activationLambda := awslambda.NewFunction(stack, jsii.String("ActivationLambda"), &awslambda.FunctionProps{
		Runtime: awslambda.Runtime_PYTHON_3_12(),
		Handler: jsii.String("index.handler"),
		Code: awslambda.Code_FromInline(jsii.String(`
import boto3
import os

ecs = boto3.client('ecs')
autoscaling = boto3.client('autoscaling')
codepipeline = boto3.client('codepipeline')

def handler(event, context):
	job_id = event['CodePipeline.job']['id']
	cluster_name = os.environ['CLUSTER_NAME']
	service_name = os.environ['SERVICE_NAME']
	asg_name = os.environ['ASG_NAME']
	action = event['CodePipeline.job']['data']['actionConfiguration']['configuration']['UserParameters']  # Get action from event data

	if action == 'activate':
		desired_count = 1
		desired_capacity = 1
	else:  # action == 'deactivate'
		desired_count = 0
		desired_capacity = 0

	ecs_params = {
		'cluster': cluster_name,
		'service': service_name,
		'desiredCount': desired_count,
	}

	asg_params = {
		'AutoScalingGroupName': asg_name,
		'DesiredCapacity': desired_capacity,
		'MinSize': desired_capacity,
	}

	try:
		autoscaling.update_auto_scaling_group(**asg_params)
		print(f'Auto Scaling Group set to desired capacity: {desired_capacity}')
	except Exception as e:
		print(f'Error updating Auto Scaling Group: {e}')
		codepipeline.put_job_failure_result(jobId=job_id, failureDetails={'message': str(e), 'type': 'ConfigurationError', 'externalExecutionId': context.aws_request_id})
		return

	try:
		ecs.update_service(**ecs_params)
		print(f'Staging service set to desired count: {desired_count}')
		codepipeline.put_job_success_result(jobId=job_id)
		return {
			'statusCode': 200,
			'body': f'Staging service set to desired count: {desired_count}'
		}
	except Exception as e:
		print(f'Error updating staging service: {e}')
		codepipeline.put_job_failure_result(jobId=job_id, failureDetails={'message': str(e), 'type': 'ConfigurationError', 'externalExecutionId': context.aws_request_id})
		raise e
	`)),
		MemorySize: jsii.Number(128),
		Timeout:    awscdk.Duration_Seconds(jsii.Number(30)),
		Environment: &map[string]*string{
			"CLUSTER_NAME": jsii.String(*previewContainer.Cluster.ClusterName()),
			"SERVICE_NAME": jsii.String(*previewContainer.Service.ServiceName()),
			"ASG_NAME":     jsii.String(*previewContainer.AutoScalingGroup.AutoScalingGroupName()),
		},
	})

	activationLambda.AddToRolePolicy(awsiam.NewPolicyStatement(&awsiam.PolicyStatementProps{
		Actions:   jsii.Strings("ecs:UpdateService"),
		Resources: jsii.Strings(*previewContainer.Service.ServiceArn()), // Modify with your actual ARN
	}))

	activationLambda.AddToRolePolicy(awsiam.NewPolicyStatement(&awsiam.PolicyStatementProps{
		Actions:   jsii.Strings("autoscaling:UpdateAutoScalingGroup"),
		Resources: jsii.Strings(*previewContainer.AutoScalingGroup.AutoScalingGroupArn()), //  ARN
	}))

	return activationLambda, &awscodepipeline.StageOptions{
		StageName: jsii.String("ActivatePreview"),
		Actions: &[]awscodepipeline.IAction{
			awscodepipelineactions.NewLambdaInvokeAction(&awscodepipelineactions.LambdaInvokeActionProps{
				ActionName:           jsii.String("ActivatePreviewAction"),
				Lambda:               activationLambda,
				UserParametersString: jsii.String("activate"),
			}),
		},
	}
}

func ecsDeploymentSteps(codePipeline awscodepipeline.Pipeline, dockerOutput awscodepipeline.Artifact, targetContainer *ContainerInfo, previewContainer *ContainerInfo, activationLambda awslambda.Function) {
	// Deploy staging container in a separate stage so it runs independently
	_ = codePipeline.AddStage(&awscodepipeline.StageOptions{
		StageName: jsii.String("DeployPreview"),
		Actions: &[]awscodepipeline.IAction{
			awscodepipelineactions.NewEcsDeployAction(&awscodepipelineactions.EcsDeployActionProps{
				ActionName:        jsii.String("DeployPreview"),
				Service:           previewContainer.Service,
				DeploymentTimeout: awscdk.Duration_Minutes(jsii.Number(15)),
				Role:              codePipeline.Role(),
				ImageFile:         awscodepipeline.ArtifactPath_ArtifactPath(dockerOutput.ArtifactName(), jsii.String("imagedefinitions-preview.json")),
			}),
		},
	})

	// Deploy main container after manual approval in a separate stage
	_ = codePipeline.AddStage(&awscodepipeline.StageOptions{
		StageName: jsii.String("Approval"),
		Actions: &[]awscodepipeline.IAction{
			awscodepipelineactions.NewManualApprovalAction(&awscodepipelineactions.ManualApprovalActionProps{
				ActionName: jsii.String("ManualApproval"),
			}),
		},
	})
	_ = codePipeline.AddStage(&awscodepipeline.StageOptions{
		StageName: jsii.String("DeployTarget"),
		Actions: &[]awscodepipeline.IAction{
			awscodepipelineactions.NewEcsDeployAction(&awscodepipelineactions.EcsDeployActionProps{
				ActionName:        jsii.String("DeployTarget"),
				Service:           targetContainer.Service,
				DeploymentTimeout: awscdk.Duration_Minutes(jsii.Number(15)),
				Role:              codePipeline.Role(),
				ImageFile:         awscodepipeline.ArtifactPath_ArtifactPath(dockerOutput.ArtifactName(), jsii.String("imagedefinitions-target.json")),
			}),
		},
	})

	_ = codePipeline.AddStage(&awscodepipeline.StageOptions{
		StageName: jsii.String("DeactivatePreview"),
		Actions: &[]awscodepipeline.IAction{
			awscodepipelineactions.NewLambdaInvokeAction(&awscodepipelineactions.LambdaInvokeActionProps{
				ActionName:           jsii.String("DeactivatePreviewAction"),
				Lambda:               activationLambda,
				UserParametersString: jsii.String("deactivate"),
			}),
		},
	})

}

answered 2 years ago

-2

The issue you're experiencing with the HOST network mode failing health checks is likely due to a few factors:

  1. Security Group Configuration: When using HOST mode, the container uses the EC2 instance's network interface directly. Make sure the EC2 instance's security group allows inbound traffic from the Application Load Balancer (ALB) on the container port.

  2. Health Check Configuration: Ensure that the health check path and port in your ALB target group settings match the container's exposed endpoint.

  3. Container Port Mapping: In HOST mode, the HostPort should match the ContainerPort. Make sure your task definition correctly maps these ports.

  4. EC2 Instance Networking: Verify that the EC2 instances are in public subnets if you need internet access without a NAT gateway.

To resolve these issues:

  1. Modify the host security group (hostSg) to allow inbound traffic from the ALB security group on the container port:

    hostSg.AddIngressRule( props.AlbSg, awsec2.Port_Tcp(containerPort), jsii.String("Allow traffic from ALB to Host"), nil, )

  2. Ensure your ALB target group's health check settings are correct for your application.

  3. In the task definition, make sure the HostPort matches the ContainerPort:

    PortMappings: &[]*awsecs.PortMapping{{ ContainerPort: containerPort, Protocol: awsecs.Protocol_TCP, HostPort: containerPort, }},

  4. Configure your Auto Scaling Group to launch instances in public subnets:

    VpcSubnets: &awsec2.SubnetSelection{ SubnetType: awsec2.SubnetType_PUBLIC, },

By making these adjustments, you should be able to get the HOST network mode working properly with your ECS tasks while maintaining internet access without requiring a NAT gateway.
Sources
Host mode - Amazon Elastic Container Service
Map Amazon ECS container ports to the EC2 instance network interface - Amazon Elastic Container Service
Amazon ECS task networking options for the EC2 launch type - Amazon Elastic Container Service
Choosing a network mode - Amazon Elastic Container Service

answered 2 years ago

EXPERT

reviewed 2 years ago

  • This is a poor answer by the AI Agent -- all four of the suggested changes are already implemented correctly in the code.

You are not logged in. Log in to post an answer.

A good answer clearly answers the question and provides constructive feedback and encourages professional growth in the question asker.