diff --git a/apigw-fargate-terraform/Network-ALB.tf b/apigw-fargate-terraform/Network-ALB.tf new file mode 100644 index 000000000..6b9d6a06f --- /dev/null +++ b/apigw-fargate-terraform/Network-ALB.tf @@ -0,0 +1,63 @@ +# Network-ALB.tf + +################################################################# +# Network - AWS Application Load Balancer +################################################################# +# Creating the internal AWS Application Load Balancer on private subnets +resource "aws_alb" "MyApp-ALB" { + name = "MyApp-ALB" + internal = true + subnets = [aws_subnet.MySubnet-privateA.id, aws_subnet.MySubnet-privateB.id] + security_groups = [aws_security_group.MyApp-ALB_SG.id] +} + +# Defining ALB target group with health check of 5 seconds timeout +resource "aws_alb_target_group" "MyApp-ALB_TG" { + name = "MyApp-ALB-TG" + port = 80 + protocol = "HTTP" + vpc_id = aws_vpc.MyVPC-VPC.id + target_type = "ip" + + health_check { + healthy_threshold = "3" + interval = "30" + protocol = "HTTP" + matcher = "200" + timeout = "5" + path = "/" + unhealthy_threshold = "2" + } +} + +# Setting up internal ALB security group allowing only traffic from 80 TCP port +resource "aws_security_group" "MyApp-ALB_SG" { + name = "MyApp-ALB-SG" + vpc_id = aws_vpc.MyVPC-VPC.id + + ingress { + protocol = "tcp" + from_port = 80 + to_port = 80 + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + protocol = "-1" + from_port = 0 + to_port = 0 + cidr_blocks = ["0.0.0.0/0"] + } +} + +# Redirecting all traffic from the ALB to the ECS Cluster target group +resource "aws_alb_listener" "MyApp-ALB_Listener" { + load_balancer_arn = aws_alb.MyApp-ALB.id + port = 80 + protocol = "HTTP" + + default_action { + target_group_arn = aws_alb_target_group.MyApp-ALB_TG.id + type = "forward" + } +} diff --git a/apigw-fargate-terraform/Network-VPC.tf b/apigw-fargate-terraform/Network-VPC.tf new file mode 100644 index 000000000..9d40b0185 --- /dev/null +++ b/apigw-fargate-terraform/Network-VPC.tf @@ -0,0 +1,189 @@ +# Network-ALB.tf + +################################################################# +# Network - Virtual Private Cloud (VPC) +################################################################# +# Creating the VPC with "10.0.0.0/16" CIDR range with feched data info +resource "aws_vpc" "MyVPC-VPC" { + cidr_block = "10.0.0.0/16" + + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = "MyVPC-VPC" + } +} + +# Creating 2 private subnets in each different AZ +resource "aws_subnet" "MySubnet-privateA" { + cidr_block = cidrsubnet(aws_vpc.MyVPC-VPC.cidr_block, 8, 1) + availability_zone = data.aws_availability_zones.available.names[1] + vpc_id = aws_vpc.MyVPC-VPC.id + + tags = { + Name = "MySubnet-privateA" + } +} + +resource "aws_subnet" "MySubnet-privateB" { + cidr_block = cidrsubnet(aws_vpc.MyVPC-VPC.cidr_block, 8, 2) + availability_zone = data.aws_availability_zones.available.names[2] + vpc_id = aws_vpc.MyVPC-VPC.id + + tags = { + Name = "MySubnet-privateB" + } +} + +# Creating 2 public subnets in each different AZ +resource "aws_subnet" "MySubnet-publicA" { + cidr_block = cidrsubnet(aws_vpc.MyVPC-VPC.cidr_block, 8, 3) + availability_zone = data.aws_availability_zones.available.names[1] + vpc_id = aws_vpc.MyVPC-VPC.id + map_public_ip_on_launch = true + + tags = { + Name = "MySubnet-publicA" + } +} + +resource "aws_subnet" "MySubnet-publicB" { + cidr_block = cidrsubnet(aws_vpc.MyVPC-VPC.cidr_block, 8, 4) + availability_zone = data.aws_availability_zones.available.names[2] + vpc_id = aws_vpc.MyVPC-VPC.id + map_public_ip_on_launch = true + + tags = { + Name = "MySubnet-publicB" + } +} + +# Internet Gateway for the public subnet +resource "aws_internet_gateway" "MyIGW-IG" { + vpc_id = aws_vpc.MyVPC-VPC.id + + tags = { + Name = "MyIGW-IG" + } + +} + +# Routing the public subnet traffic through the IGW +resource "aws_route" "internet_access" { + route_table_id = aws_vpc.MyVPC-VPC.main_route_table_id + destination_cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.MyIGW-IG.id +} + +# Creating one NAT Gateway in each AZ with an Elastic IP for each private subnet to get internet connectivity +resource "aws_eip" "MyEIP-EIPA" { + depends_on = [aws_internet_gateway.MyIGW-IG] + + tags = { + Name = "MyEIP-EIPA" + } +} + +resource "aws_eip" "MyEIP-EIPB" { + depends_on = [aws_internet_gateway.MyIGW-IG] + + tags = { + Name = "MyEIP-EIPB" + } +} + +resource "aws_nat_gateway" "MyNATGW-NATGWA" { + subnet_id = element(aws_subnet.MySubnet-publicA.*.id, 1) + allocation_id = element(aws_eip.MyEIP-EIPA.*.id, 1) + + tags = { + Name = "MyNATGW-NATGWA" + } +} + +resource "aws_nat_gateway" "MyNATGW-NATGWB" { + subnet_id = element(aws_subnet.MySubnet-publicB.*.id, 2) + allocation_id = element(aws_eip.MyEIP-EIPB.*.id, 2) + + tags = { + Name = "MyNATGW-NATGWB" + } + +} + +# Routing public subnets, making non-local traffic pass through Internet Gateway to the internet +resource "aws_route_table" "MyRouteTable-publicA" { + vpc_id = aws_vpc.MyVPC-VPC.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.MyIGW-IG.id + } + + tags = { + Name = "MyRouteTable-publicA" + } +} + +resource "aws_route_table" "MyRouteTable-publicB" { + vpc_id = aws_vpc.MyVPC-VPC.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.MyIGW-IG.id + } + + tags = { + Name = "MyRouteTable-publicB" + } +} + +# Routing private subnets, making non-local traffic pass through each NAT gateway to the internet +resource "aws_route_table" "MyRouteTable-privateA" { + vpc_id = aws_vpc.MyVPC-VPC.id + + route { + cidr_block = "0.0.0.0/0" + nat_gateway_id = element(aws_nat_gateway.MyNATGW-NATGWA.*.id, 1) + } + + tags = { + Name = "MyRouteTable-privateA" + } + +} + +resource "aws_route_table" "MyRouteTable-privateB" { + vpc_id = aws_vpc.MyVPC-VPC.id + + route { + cidr_block = "0.0.0.0/0" + nat_gateway_id = element(aws_nat_gateway.MyNATGW-NATGWB.*.id, 2) + } + + tags = { + Name = "MyRouteTable-privateB" + } +} + +# Associating the previously created routes to the public and private subnets (so they don't default to the main MyVPC-VPC route table) +resource "aws_route_table_association" "privateA" { + subnet_id = element(aws_subnet.MySubnet-privateA.*.id, 1) + route_table_id = element(aws_route_table.MyRouteTable-privateA.*.id, 1) +} + +resource "aws_route_table_association" "privateB" { + subnet_id = element(aws_subnet.MySubnet-privateB.*.id, 2) + route_table_id = element(aws_route_table.MyRouteTable-privateB.*.id, 2) +} + +resource "aws_route_table_association" "publicA" { + subnet_id = element(aws_subnet.MySubnet-publicA.*.id, 1) + route_table_id = element(aws_route_table.MyRouteTable-publicA.*.id, 1) +} + +resource "aws_route_table_association" "publicB" { + subnet_id = element(aws_subnet.MySubnet-publicB.*.id, 2) + route_table_id = element(aws_route_table.MyRouteTable-publicB.*.id, 2) +} diff --git a/apigw-fargate-terraform/README.md b/apigw-fargate-terraform/README.md new file mode 100644 index 000000000..8c1014abc --- /dev/null +++ b/apigw-fargate-terraform/README.md @@ -0,0 +1,115 @@ +# Amazon API Gateway to AWS Fargate + +This project contains a terraform template for deploying an AWS Fargate service running on an Amazon Elastic Container Service (ECS) cluster with a private Application Load Balancer in-front. The Application Load Balanced Fargate Service is integrated with Amazon API Gateway HTTP API to expose the endpoint. This template uses public standard nginx image without having to pre-push the image to Amazon Elastic Container Registry (ECR) or another container library. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/apigw-fargate-terraform. + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli?in=terraform/aws-get-started) installed + + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ```bash + git clone https://github.com/aws-samples/serverless-patterns + ``` +2. Change directory to the pattern directory: + ```bash + cd serverless-patterns/apigw-fargate-terraform + ``` +3. From the command line, initialize terraform to download and install the providers defined in the configuration: + ``` + terraform init + ``` +4. From the command line, apply the configuration in the main.tf file: + ``` + terraform apply + ``` +5. During the prompts: + * Enter yes +6. Note the outputs from the deployment process. These contain the resource names and/or URLs which are used for testing. + +## How it works + +- The VPC and subnets are created +- The ECS cluster is created +- The Task Definitions are created +- The API Gateway Integration, Route, and VPC Link are created +- The Fargate Service is created + +## Testing + +Retrieve the API Gateway URL from the `cdk deploy` output. Example of the output is: + +``` +APIGatewayUrl = "https://abcd123efg.execute-api.eu-west-1.amazonaws.com" +``` + +For reference: + +```bash +Outputs: +APIGatewayUrl = "https://abcd123efg.execute-api.eu-west-1.amazonaws.com" +MyFargateServiceLoadBalancer = "internal-MyApp-ALB-XXXXX.eu-west-1.elb.amazonaws.com" +MyFargateServiceServiceURL = "http://internal-MyApp-ALB-XXXXX.eu-west-1.elb.amazonaws.com" +``` + +The API Gateway allows a GET request to `/`. To call it, run the following: + +```bash +curl --location --request GET '' +# Example +curl --location --request GET 'https://abcd123efg.execute-api.ap-southeast-2.amazonaws.com/' +``` + +Running the request above should produce the following output: + +```bash +~ % curl --location --request GET 'https://abcd123efg.execute-api.ap-southeast-2.amazonaws.com/' + + + +Welcome to nginx! +(...) +```` + +## Cleanup + +1. Change directory to the pattern directory: + ``` + cd apigw-fargate-terraform + ``` +2. Delete all created resources by terraform + ```bash + terraform destroy + ``` +3. During the prompts: + * Enter yes +4. Confirm all created resources has been deleted + ```bash + terraform show + ``` +5. Navigate to ECR in the AWS console and delete the container images created + +## Documentation and useful references + +- [GitHub issue where one of the contributors provided an example with a pre-built image and any type of request](https://github.com/aws/aws-cdk/issues/8066) +- [CDK documentation for ApplicationLoadBalancedFargateService](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ecs-patterns.ApplicationLoadBalancedFargateService.html) +- [CDK documentation for APIGatewayv2 CfnIntegration](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigatewayv2.CfnIntegration.html) +- [CDK documentation for APIGatewayv2 CfnRoute](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigatewayv2.CfnRoute.html) +- [CDK documentation for APIGatewayv2 HttpApi](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigatewayv2.HttpApi.html) +- [CDK documentation for CDK Core CfnResource](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.CfnResource.html) + +--- + +Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 + diff --git a/apigw-fargate-terraform/example-pattern.json b/apigw-fargate-terraform/example-pattern.json new file mode 100644 index 000000000..60825af1d --- /dev/null +++ b/apigw-fargate-terraform/example-pattern.json @@ -0,0 +1,60 @@ +{ + "title": "API Gateway to AWS Fargate", + "description": "Create an API Gateway endpoint to access an AWS Fargate service.", + "language": "terraform", + "level": "200", + "framework": "Terraform", + "introBox": { + "headline": "Create an API Gateway endpoint to access an AWS Fargate service.", + "text": [ + "This project contains a terraform template for deploying an AWS Fargate service running on an Amazon Elastic Container Service (ECS) cluster with a private Application Load Balancer in-front. The Application Load Balanced Fargate Service is integrated with Amazon API Gateway HTTP API to expose the endpoint.", + "This template uses a custom image without having to pre-push the image to Amazon Elastic Container Registry (ECR) or another container library. This makes use of the in-built ecs.ContainerImage.fromAsset method. The custom image has a base route / to output a message." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-fargate-terraform", + "templateURL": "serverless-patterns/apigw-fargate-terraform", + "projectFolder": "apigw-fargate-terraform", + "templateFile": "main.tf" + } + }, + "resources": { + "bullets": [ + { + "text": "AWS Fargate", + "link": "https://aws.amazon.com/fargate" + }, + { + "text": "Building Happy Little APIs - Episode 1", + "link": "https://www.youtube.com/watch?v=CEHC9crd2VU" + } + ] + }, + "deploy": { + "text": [ + "terraform init", + "terraform apply" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "terraform destroy" + ] + }, + "authors": [ + { + "name": "Oriol Matavacas", + "image": "https://togithub.s3.eu-west-1.amazonaws.com/Oriol.jpg", + "bio": "Oriol Matavacas is a Sr. Solutions Architect at AWS based in Barcelona. Oriol primarily supporting customers on the journey to the Cloud. He enjoys building new solutions with scalability, availability and easy to maintain by using serverless.", + "linkedin": "https://www.linkedin.com/in/oriol-matavacas-rodriguez-b165868a", + "twitter": "" + } + ] +} + diff --git a/apigw-fargate-terraform/main.tf b/apigw-fargate-terraform/main.tf new file mode 100644 index 000000000..c370929cf --- /dev/null +++ b/apigw-fargate-terraform/main.tf @@ -0,0 +1,32 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 3.27" + } + } + + required_version = ">= 0.14.9" +} + +# Fetching current Account ID, AWS region and AZs +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} +data "aws_availability_zones" "available" {} + +################################################################# +# Outputs +################################################################# +output "APIGatewayUrl" { + value = "${aws_apigatewayv2_api.MyApp-APIGatewayHTTP.api_endpoint}" + description = "API Gateway URL to access the GET endpoint" +} + +output "MyFargateServiceLoadBalancer" { + value = "${aws_alb.MyApp-ALB.dns_name}" +} + +output "MyFargateServiceServiceURL" { + value = "http://${aws_alb.MyApp-ALB.dns_name}" +} +