{"id":4500,"date":"2025-05-03T16:29:17","date_gmt":"2025-05-03T16:29:17","guid":{"rendered":"https:\/\/geekmungus.co.uk\/?p=4500"},"modified":"2025-05-03T16:30:54","modified_gmt":"2025-05-03T16:30:54","slug":"example-website-workload-amazon-elastic-container-service-ecs-with-application-load-balancer-and-dns-route53-including-nat-gateway","status":"publish","type":"post","link":"https:\/\/geekmungus.co.uk\/?p=4500","title":{"rendered":"Example Website Workload &#8211; Amazon Elastic Container Service (ECS) with Application Load Balancer and DNS (Route53), including NAT Gateway"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">The Amazon Elastic Container Service (ECS), specifically Fargate allows you to run containers on an ad-hoc basis.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The following guide assumes you have already deployed an Amazon Elastic Container Registry (ECR), built your image, pushed it to the ECR and have obtained the Repository URI.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The guide also uses local Terraform state, please see the documentation on storing state remotely if that is what you require.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"ExampleWebsiteWorkloadAmazonElasticContainerService(ECS)withApplicationLoadBalancerandDNS(Route53),includingNATGateway-Repository\">Repository<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">The example Terraform definitions can be found within Gitlab. Two example deployments are given below:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Amazon ECS, Application Load Balancer (ALB), DNS (Route53) and Certificate &#8211;\u00a0<a href=\"https:\/\/github.com\/tristanhself\/general\/tree\/master\/aws\/ecs-alb-dns-custom\">https:\/\/github.com\/tristanhself\/general\/tree\/master\/aws\/ecs-alb-dns-custom<\/a>\u00a0&#8211; Each container has a public IP address, which is not ideal nor necessary in this case.<\/li>\n\n\n\n<li>Amazon ECS, Application Load Balancer (ALB), DNS (Route53), Certificate and NAT Gateways &#8211;<a href=\"https:\/\/github.com\/tristanhself\/general\/tree\/master\/aws\/ecs-alb-dns-custom-nat\">https:\/\/github.com\/tristanhself\/general\/tree\/master\/aws\/ecs-alb-dns-custom-nat<\/a>\u00a0&#8211; Each container uses only a private IP address, to ensure the container (Task) can therefore reach the ECR, it must be provided with a NAT Gateway for Internet access.<\/li>\n<\/ul>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"ExampleWebsiteWorkloadAmazonElasticContainerService(ECS)withApplicationLoadBalancerandDNS(Route53),includingNATGateway-Step1-UpdateTerraformVariables\">Step 1 &#8211; Update Terraform Variables<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">There are three variables to update within the&nbsp;<strong>variables.tf<\/strong>&nbsp;file:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">*&nbsp;<strong>route53zone&nbsp;<\/strong>&#8211; The AWS Route53 Hosted Zone ID which you can obtain from the AWS console, obviously you need to have the domain already present as an AWS Route53 Hosted Zone.<br>*&nbsp;<strong>domainname&nbsp;<\/strong>&#8211; The domain name (excluding sub-domain) that your application will be deployed at, this assumes you already have the domain in an AWS Route53 Hosted Zone.<br>*&nbsp;<strong>ecrimageuri&nbsp;<\/strong>&#8211; The Amazon Elastic Container Registry (ECR) URI which you can obtain from the AWS console for the image you wish to deploy from the repository.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Update the variables by updating the &#8220;default&#8221; value within the&nbsp;<strong>variables.tf<\/strong>&nbsp;file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"ExampleWebsiteWorkloadAmazonElasticContainerService(ECS)withApplicationLoadBalancerandDNS(Route53),includingNATGateway-Step2-DeployTerraform\">Step 2 &#8211; Deploy Terraform<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Deploy the environment with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform apply &#91;--auto-approve]<\/code><\/pre>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"ExampleWebsiteWorkloadAmazonElasticContainerService(ECS)withApplicationLoadBalancerandDNS(Route53),includingNATGateway-Step3-VerifyBehaviour\">Step 3 &#8211; Verify Behaviour<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Once deployed, the Terraform provides some Outputs. however all you need is to access your application at the domain name you entered, e.g.&nbsp;<a href=\"https:\/\/www.mydomain.com\/\">https:\/\/www.mydomain.com<\/a>&nbsp;or&nbsp;<a href=\"https:\/\/mydomain.com\/\">https:\/\/mydomain.com<\/a>, the ALB automatically redirects from HTTP to HTTPS if required.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Perform whatever tests you require.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"ExampleWebsiteWorkloadAmazonElasticContainerService(ECS)withApplicationLoadBalancerandDNS(Route53),includingNATGateway-Step4-Clean-Up\">Step 4 &#8211; Clean-Up<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">To clean up, you need remove your ECS cluster, ALB by running a Terraform destroy, bear in mind that the image(s) stored in the Amazon Elastic Container Registry (ECR) are not removed, if you&#8217;ve deployed the ECR separately, you&#8217;ll need to remove this separately.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As we created using Terraform you can remove with Terraform thusly:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform destroy &#91;--auto-approve]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">As you can see it is fairly simple to deploy a resilient container based workload and publish it to the Internet, obviously there is lots more that can be done to improve this deployment, but this provides you with the basis from which to build.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"ExampleWebsiteWorkloadAmazonElasticContainerService(ECS)withApplicationLoadBalancerandDNS(Route53),includingNATGateway-TerraformandConfigurationExplaination\">Terraform and Configuration Explaination<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Let&#8217;s deep dive into each of the sections of the configuration, so we can understand what is being done at each stage. We&#8217;ll not include the Terraform provider within the explaination as it is hoped you&#8217;re aware of this and how they work already.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"ExampleWebsiteWorkloadAmazonElasticContainerService(ECS)withApplicationLoadBalancerandDNS(Route53),includingNATGateway-main.tf-VPC,Subnets,IGW,RouteTable(s)andSecurityGroup\">main.tf &#8211; VPC, Subnets, IGW, Route Table(s) and Security Group<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The &#8220;main.tf&#8221; file contains the creation of the VPC, followed by creation of two Subnets A and B which are created as &#8220;private subnets&#8221;. The VPC then has an Internet Gateway created, with a RouteTable and default Routes attached, so the VPC and the subnets have a direct Internet connection.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"ExampleWebsiteWorkloadAmazonElasticContainerService(ECS)withApplicationLoadBalancerandDNS(Route53),includingNATGateway-ecs.tf-AWSECSCluster,TaskRoles,TaskDefinitionandServiceDefinition\">ecs.tf &#8211; AWS ECS Cluster, Task Roles, Task Definition and Service Definition<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Creates the AWS ECS Cluster, Task Roles (i.e. the permissions of ECS and the Tasks), Task Definitions (i.e. deploys the containers) and defines the Service (through which the application is accessed.)<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ExampleWebsiteWorkloadAmazonElasticContainerService(ECS)withApplicationLoadBalancerandDNS(Route53),includingNATGateway-AWSECSExecutionRoles\">AWS ECS Execution Roles<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The Execution Role is used by the ECS Agent to make AWS API calls on the behalf of the user.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We first define the role with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>resource \"aws_iam_role\" \"ecsTaskExecutionRole\" {\n  name               = \"ecsTaskExecutionRole\"\n  assume_role_policy = \"${data.aws_iam_policy_document.assume_role_policy.json}\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">We then create an Assume Role Policy, which defines who (or what if the principal is a service) can use the role we just created.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>data \"aws_iam_policy_document\" \"assume_role_policy\" {\n  statement {\n    actions = &#91;\"sts:AssumeRole\"]\n \n    principals {\n      type        = \"Service\"\n      identifiers = &#91;\"ecs-tasks.amazonaws.com\"]\n    }\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Now we have defined the role and who\/what can use the role we need to define what the role can actually do, rather than specifying the Role Action Policy manually, we are using a pre-existing Policy with the necessary actions we need: ecr:GetAuthorizationToken, ecr:BatchCheckLayerAvailability, ecr:GetDownloadUrlForLayer, ecr:BatchGetImage, logs:CreateLogStream, logs:PutLogEvents. And one already exists for this purpose: AmazonECSTaskExecutionRolePolicy.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>resource \"aws_iam_role_policy_attachment\" \"ecsTaskExecutionRole_policy\" {\n  role       = \"${aws_iam_role.ecsTaskExecutionRole.name}\"\n  policy_arn = \"arn:aws:iam::aws:policy\/service-role\/AmazonECSTaskExecutionRolePolicy\"\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ExampleWebsiteWorkloadAmazonElasticContainerService(ECS)withApplicationLoadBalancerandDNS(Route53),includingNATGateway-AWSECSTaskRoles\">AWS ECS Task Roles<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Task Role grants permissions specifically to the tasks, e.g. if a running task needs to access another AWS resource, e.g. a database. We don&#8217;t need a task role for this use case, the tasks (containers) don&#8217;t need to access anything else.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ExampleWebsiteWorkloadAmazonElasticContainerService(ECS)withApplicationLoadBalancerandDNS(Route53),includingNATGateway-AWSECSCluster,ECSTaskDefinitionandECSServiceDefinition\">AWS ECS Cluster, ECS Task Definition and ECS Service Definition<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">We now need to define the AWS ECS cluster.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>resource \"aws_ecs_cluster\" \"example\" {\n  name = \"fargate-cluster\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Once defined we can now define the ECS Task, within this we are specifying the size of the containers, and also which image we are wanting to use for the task, here we specify the Repository URI where the image resides.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>resource \"aws_ecs_task_definition\" \"example\" {\n  family                   = \"fargate-task\"\n  execution_role_arn       = aws_iam_role.ecsTaskExecutionRole.arn # Execution Role is used by the ECS Agent to make AWS API calls on the behalf of the user.\n  # task_role_arn          = aws_iam_role.ecs_task_role.arn # Task Role grants permissions specifically to the tasks, e.g. if a running task needs to access another AWS resource.\n  network_mode             = \"awsvpc\"\n  requires_compatibilities = &#91;\"FARGATE\"]\n  cpu                      = \"256\" # Adjust based on your needs\n  memory                   = \"512\" # Adjust based on your needs\n \n  container_definitions = jsonencode(&#91;{\n    name      = \"my-container\"\n    #image     = \"nginx\" # Replace with your container image\n    image     = var.ecrimageuri\n    essential = true\n    portMappings = &#91;{\n      containerPort = 80\n      hostPort      = 80\n      protocol      = \"tcp\"\n    }]\n  }])\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Finally we define the ECS Service Definition, this dectates how many containers are deployed, which subnets they will be deployed to, along with the security group to restrict access to them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It also binds the Load Balancer Target Group to the ECS Service, so the Load Balancer will direct requests to the running container ports.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>resource \"aws_ecs_service\" \"example\" {\n  name            = \"fargate-service\"\n  cluster         = aws_ecs_cluster.example.id\n  task_definition = aws_ecs_task_definition.example.arn\n  desired_count   = 2\n  launch_type     = \"FARGATE\"\n \n  load_balancer {\n    target_group_arn = aws_lb_target_group.ecs_target_group.arn\n    container_name   = \"my-container\"\n    container_port   = 80\n  }\n \n  network_configuration {\n    subnets          = &#91;aws_subnet.subnet_a.id, aws_subnet.subnet_b.id]\n    security_groups  = &#91;aws_security_group.ecs_sg.id]\n    assign_public_ip = false\n  }\n \n  depends_on = &#91;\n    aws_lb_listener.http\n  ]\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"ExampleWebsiteWorkloadAmazonElasticContainerService(ECS)withApplicationLoadBalancerandDNS(Route53),includingNATGateway-alb.tf-ApplicationLoadBalancer\">alb.tf &#8211; Application Load Balancer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We define the Application Load Balancer (ALB), which consists of these main components:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">* Application Load Balancer (ALB) Definition &#8211; The actual definition of the load balancer, which includes the security group that restricts access to it.<br>* Application Load Balancer (ALB) Target Group &#8211; The definition of the target group which links the listeners, i.e. the things that the end user will hit when connecting and the, in this case containers that the connections will be redirected to.<br>* Application Load Balancer (ALB) Listener (HTTP) &#8211; The definition of the listener that the end user will hit, this is the HTTP one which redirects to HTTPS.<br>* Application Load Balancer (ALB) Listener (HTTPS) &#8211; The definition of the listener that the end user will hit, this is the HTTPS one which has a certificate binding and directs the connections to the Target Group, which in turn goes to the containers running in AWS ECS.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Application Load Balancer (ALB) Definition\nresource \"aws_lb\" \"app_lb\" {\n  name               = \"fargate-alb\"\n  internal           = false\n  load_balancer_type = \"application\"\n  security_groups    = &#91;aws_security_group.ecs_sg.id]\n  subnets            = &#91;aws_subnet.subnet_a.id, aws_subnet.subnet_b.id]\n \n  enable_deletion_protection       = false\n  enable_cross_zone_load_balancing = true\n \n  tags = {\n    Name = \"fargate-alb\"\n  }\n}\n \n# ALB Target Group\nresource \"aws_lb_target_group\" \"ecs_target_group\" {\n  name     = \"ecs-target-group\"\n  port     = 80\n  protocol = \"HTTP\"\n  vpc_id   = aws_vpc.main.id\n  target_type = \"ip\"\n \n  health_check {\n    path                = \"\/\"\n    interval            = 30\n    timeout             = 5\n    healthy_threshold   = 3\n    unhealthy_threshold = 3\n    matcher             = \"200\"\n  }\n \n  tags = {\n    Name = \"ecs-target-group\"\n  }\n}\n \n# ALB Listener (HTTP) with Redirect to HTTPS\nresource \"aws_lb_listener\" \"http\" {\n  load_balancer_arn = aws_lb.app_lb.arn\n  port              = 80\n  protocol          = \"HTTP\"\n \n  default_action {\n    type = \"redirect\"\n    redirect {\n      port = 443\n      protocol = \"HTTPS\"\n      status_code = \"HTTP_301\"\n    }\n  }\n}\n \n# ALB Listener (HTTPS)\nresource \"aws_lb_listener\" \"https\" {\n  load_balancer_arn = aws_lb.app_lb.arn\n  port              = 443\n  protocol          = \"HTTPS\"\n  certificate_arn = aws_acm_certificate.example.arn\n \n  default_action {\n    type             = \"forward\"\n    target_group_arn = aws_lb_target_group.ecs_target_group.arn\n  }\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"ExampleWebsiteWorkloadAmazonElasticContainerService(ECS)withApplicationLoadBalancerandDNS(Route53),includingNATGateway-dnscert.tf-DNSRecordandcertificate\">dnscert.tf &#8211; DNS Record and certificate<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">These define the DNS records automatically, which includes defining the following:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"http:\/\/www.domain.com\/\">www.domain.com<\/a>\u00a0CNAME record which points at the Application Load Balancer (ALB).<\/li>\n\n\n\n<li><a href=\"http:\/\/domain.com\/\">domain.com<\/a>\u00a0A record which is a special Apex A record which points at the Application Load Balancer (ALB).<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The Terraform also creates the Amazon Certificate Manager (ACM) certificate automatically, it also inserts the validation record into the DNS zone automatically, so it is ready to be assigned to the load balancer.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The Amazon Elastic Container Service (ECS), specifically Fargate allows you to run containers on an ad-hoc basis.&nbsp; The following guide assumes you have already deployed an Amazon Elastic Container Registry (ECR), built your image, pushed it to the ECR and have obtained the Repository URI. The guide also uses local Terraform state, please see the &#8230; <a title=\"Example Website Workload &#8211; Amazon Elastic Container Service (ECS) with Application Load Balancer and DNS (Route53), including NAT Gateway\" class=\"read-more\" href=\"https:\/\/geekmungus.co.uk\/?p=4500\" aria-label=\"Read more about Example Website Workload &#8211; Amazon Elastic Container Service (ECS) with Application Load Balancer and DNS (Route53), including NAT Gateway\">Read more<\/a><\/p>\n","protected":false},"author":4,"featured_media":4496,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[28,46],"tags":[],"class_list":["post-4500","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-aws","category-docker"],"_links":{"self":[{"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=\/wp\/v2\/posts\/4500","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=4500"}],"version-history":[{"count":2,"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=\/wp\/v2\/posts\/4500\/revisions"}],"predecessor-version":[{"id":4502,"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=\/wp\/v2\/posts\/4500\/revisions\/4502"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=\/wp\/v2\/media\/4496"}],"wp:attachment":[{"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=4500"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=4500"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=4500"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}