Ends in
00
days
00
hrs
00
mins
00
secs
ENROLL NOW

🎁 Get 20% Off - Christmas Big Sale on All Practice Exams, Video Courses, and eBooks!

Building a Private React Application Infrastructure with Terraform

Home » Others » Building a Private React Application Infrastructure with Terraform

Building a Private React Application Infrastructure with Terraform

This is the first part of a series of blogs about the platform management of a React Application infrastructure using Terraform. In the early phases of a software development project, it is mandatory to have the application reviewed by security way before the Beta release to ensure that the app adheres to the standards of a secure web application and is able to protect the sensitive data it handles. Most of the time, DevOps Engineers are tasked to deploy the web application in a private domain to allow the initial load testing, penetration testing, and other security-related testing while keeping the application secure from external attacks.  

To be able to repeatedly deploy the infrastructure across different environments, we make use of an Infrastructure as Code tool, Terraform. When we use IaC, we are able to version the Infrastructure code and safely automate infrastructure tasks, including database changes. Learning Terraform is very easy, and is one of the reasons why it has remained one of the most popular Infrastructures as Code tools today.

The Architecture

S3 is only one of the ways to host a React Application. It is a managed object storage service, and when used to host a web application, we maintain some level of control over how deployment is done as opposed to an alternative solution like AWS Amplify. S3 is a suitable solution for simple web applications and is what we’ll be using in this demo.

Building a Private React Application Infrastructure with Terraform

AWS S3 buckets are publicly resolvable. It does not have a native “private website” feature, but the S3 policy can be written such that the objects are only accessed by certain resources we define. In the architecture above, a private API Gateway is defined and is allowed access to the S3 bucket by assuming an IAM role that is added to the S3 bucket policy. The API Gateway can then be exposed using a VPC endpoint. The VPC Endpoint IP is configured as a target of the Network Load Balancer, which is the route destination of a private domain defined in Route 53.

In the solution, we assume that the S3 bucket is built separately as part of the permanent architecture when the web application is released. We will partially discuss the required S3 policy to limit access to the S3 objects.

Steps

1. Create an IAM role for S3 access

As best practice, all public access to an S3 bucket should be blocked. This is the principle of least privilege access. We will only allow access to the resource that needs it, in this case, the IAM role that the API Gateway will assume.

Building a Private React Application Infrastructure with Terraform2

To assist in creating the S3 bucket policy statement, we can use the AWS Policy Generator provided here. We only need  s3:ListBucket and s3:GetObject to access the files, but as this role will be re-used for the CI/CD pipeline, we will also add s3:PutObject permissions to it.

{
            "Sid": "IAMRoleTemporaryAccess",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::123456789:role/react-app-iamrole"
            },
            "Action": [
                "s3:PutObject",
                "s3:ListBucket",
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::s3-reactapp-bucket/*",
                "arn:aws:s3:::s3-reactapp-bucket"
            ]
        }
}

In the code above, the S3 bucket named s3-reactapp-bucket is added with a bucket policy named IAMRoleTemporaryAccess. The policy allows the iam role react-app-iamrole in account ID 12345678 the following actions: PutObject, ListBucket, and GetObject.

To build the Terraform code for this specific part of the infrastructure, we can define a logical group named IAM. This module will contain all the required parts of the IAM role resource. The IAM module will be a reusable component across environments. The variables will vary across environments, so it makes sense to create separate vars.tf files for each environment.

Building a Private React Application Infrastructure with Terraform

The IAM Module

There are three parts to the IAM module: aws_iam_role, aws_iam_policy, and aws_iam_policy_attachment (where we attach the iam policy to the iam role).

Tutorials dojo strip

a. aws_iam_role

resource "aws_iam_role" "tutorialsdojo-iamrole" {
 name                = "${var.iamrolename}"
 assume_role_policy = jsonencode({
   Version = "2012-10-17"
   Statement = [
       {
         "Effect": "Allow",
         "Principal": {
             "Service": ["apigateway.amazonaws.com"]
         },
         "Action": "sts:AssumeRole"
       }
   ]
 })
}

Note: In the code above, the IAM role name is an input named iamrolename and takes the value of the variable from the vars.tf file in the same location. This strategy is done to allow the module to use a name passed to it, allowing it to be re-used across environments. 

b. aws_iam_policy
The aws_iam_policy contains the policy statement of the role. Since the IAM role is being built out, the resource defined can refer back to the terraform code of the previous resource (aws_iam_role.tutorialsdojo-iamrole.arn)

resource "aws_iam_policy" "tutorialsdojo-iampolicy" {
 name        = "${var.iampolicyname}"
 description = "Tutorials Dojo IAM Role Policy"


 policy = <<EOF
{
 "Version": "2012-10-17",
 "Statement": [
   {
     "Action": [
               "s3:GetBucketLocation",
               "s3:GetBucketTagging",
               "s3:GetBucketVersioning",
               "s3:GetBucketWebsite",
               "s3:GetObject",
               "s3:GetObjectAcl",
               "s3:ListBucket",
               "s3:ListBucketMultipartUploads",
               "s3:ListBucketVersions",
               "s3:PutBucketCORS",
               "s3:PutBucketTagging",
               "s3:PutObject",
               "s3:PutObjectAcl"
     ],
     "Effect": "Allow",
     "Resource": [
               "${aws_iam_role.tutorialsdojo-iamrole.arn}"
     ]
   }   
 ]
}
EOF
}

c. aws_iam_policy_attachment

Lastly, we can associate the IAM policy with the IAM role by using aws_iam_policy_attachment.

resource "aws_iam_policy_attachment" "tutorialsdojo-iampolicyattachment" {
 name       = "tutorialsdojo-iamrole-attachment"
 roles      = [aws_iam_role.tutorialsdojo-iamrole.name]
 policy_arn = aws_iam_policy.tutorialsdojo-iampolicy.arn
}

This completes the IAM role resource needed for the architecture.

Putting it Together

In the main.tf file under the environments folder, we can invoke the modules by the code below. Take note that all the required variable inputs of the module should also be provided when invoking the module.

module "tutorialsdojo-iamrole" {
   source                = "../../modules/iam"
   aws_profile           = var.aws_profile
   aws_region            = var.aws_region
   iamrolename           = var.iamrolename
 }

2. Create an ACM Certificate for the private domain

Since the ACM certificate rarely changes, we can skip creating a Terraform code for it and just request for a private certificate manually through the AWS management console. Note that the private certificate option will only be available for selection if you have already set up a private certificate authority in your AWS account.

Once available, you can get the certificate ARN of the private certificate and use it when building the Terraform code of the rest of the web application. Do note that a CNAME record has to be added in Route53 to prove to AWS that you own or control all the domain names that you request in ACM.

3. Build the Terraform Code of the Private Infrastructure

Folder Structure

To continue on the standard of folder structure, the rest of the application will be built out within the modules folder to allow code re-use when deploying the app to a different environment. Here, we logically grouped the modules into two: apigateway-internal and nlb.

Building a Private React Application Infrastructure with Terraform

Breaking down the apigateway-internal module

The first resource to build is the aws_api_gateway_rest_api resource. An API endpoint type can either be edge-optimized, regional, or private. Since the requirement for the web application is to be accessible in an internal network, we will configure the endpoint to be PRIVATE.

resource "aws_api_gateway_rest_api" "tutorialsdojo-restapi" {
 name        = "${var.apigwname}"
 description = "TutorialsDojo Bucket Private API Gateway"


 policy = "${data.aws_iam_policy_document.tutorialsdojo-resourcepolicy.json}"


 endpoint_configuration {
   types = ["PRIVATE"]
   vpc_endpoint_ids = [ "${var.apigwvpce}" ]
 }


 binary_media_types = [ "*/*" ]


 tags = local.common_tags
}

From the code above, we specify the policy to refer to a data document json code. This is a strategy to separate out code and keep the resources configurations clean and easy to follow. Below is the data code that is being referred to by the endpoint.

 data "aws_iam_policy_document" "tutorialsdojo-resourcepolicy" {
   statement {
   effect = "Deny"
   actions = [
     "execute-api:Invoke"
   ]
   principals {
     type        = "*"
     identifiers = ["*"]
   }
   resources = [
     "execute-api:/*",
   ]
   condition {
     test     = "StringNotEquals"
     variable = "aws:sourceVpc"


     values = [
       "${var.apigwvpcid}"
     ]
   }
 }
 statement {
   effect = "Allow"
   actions = [
     "execute-api:Invoke"
   ]
   principals {
     type        = "*"
     identifiers = ["*"]
   }
   resources = [
     "execute-api:/*",
   ]
 }
}

The root proxy is the root path for the web application and is a pass-through method in the api gateway module.

Building a Private React Application Infrastructure with Terraform

There are four resources to build out for the root proxy: aws_api_gateway_method, aws_api_gateway_integaration, aws_api_gateway_method_response, and aws_api_gateway_integration_response.

resource "aws_api_gateway_method" "tutorialsdojo-apigwmethod" {
} 
resource "aws_api_gateway_integration" "tutorialsdojo-rootintegration" {
}
resource "aws_api_gateway_method_response" "tutorialsdojo-rootmethodresp" {
}
resource "aws_api_gateway_integration_response" "tutorialsdojo-rootintegrationresp" {
}

Below is the expanded Terraform code of these four resources.

resource "aws_api_gateway_method" "tutorialsdojo-apigwmethod" {
 rest_api_id       = aws_api_gateway_rest_api.tutorialsdojo-restapi.id
 resource_id       = aws_api_gateway_rest_api.tutorialsdojo-restapi.root_resource_id
 http_method       = "GET"
 authorization     = "NONE"
 api_key_required  = "false"
}


resource "aws_api_gateway_integration" "tutorialsdojo-rootintegration" {
 rest_api_id             = aws_api_gateway_rest_api.tutorialsdojo-restapi.id
 resource_id             = aws_api_gateway_rest_api.tutorialsdojo-restapi.root_resource_id
 http_method             = aws_api_gateway_method.tutorialsdojo-apigwmethod.http_method
 integration_http_method = "GET"
 type                    = "AWS"
 uri                     = "arn:aws:apigateway:${var.aws_region}:s3:path/${var.s3name}${var.s3index}"
 credentials             = "${var.iamrole}"
}


resource "aws_api_gateway_method_response" "tutorialsdojo-rootmethodresp" {
 rest_api_id = "${aws_api_gateway_rest_api.tutorialsdojo-restapi.id}"
 resource_id = "${aws_api_gateway_rest_api.tutorialsdojo-restapi.root_resource_id}"
 http_method = "${aws_api_gateway_method.tutorialsdojo-apigwmethod.http_method}"
 status_code = "200"
 response_parameters = {
   "method.response.header.Content-Type" = false
   "method.response.header.Content-Length" = false
 }
}


resource "aws_api_gateway_integration_response" "tutorialsdojo-rootintegrationresp" {
 rest_api_id = "${aws_api_gateway_rest_api.tutorialsdojo-restapi.id}"
 resource_id = "${aws_api_gateway_rest_api.tutorialsdojo-restapi.root_resource_id}"
 http_method = "${aws_api_gateway_method.tutorialsdojo-apigwmethod.http_method}"
 status_code = "${aws_api_gateway_method_response.tutorialsdojo-rootmethodresp.status_code}"
 response_parameters = {
   "method.response.header.Content-Type" = "integration.response.header.Content-Type"
   "method.response.header.Content-Length" = "integration.response.header.Content-Length"
 }
 selection_pattern = ""
}

For the rest of the methods, we will follow the following resource structure.

resource "aws_api_gateway_resource" "tutorialsdojo-resource" {
}
resource "aws_api_gateway_method" "tutorialsdojo-apigwmethodall" {
}
resource "aws_api_gateway_integration" "tutorialsdojo-otherintegration" {
}
resource "aws_api_gateway_method_response" "tutorialsdojo-apigwmethodall200" {
}
resource "aws_api_gateway_method_response" "tutorialsdojo-apigwmethodall404" {
}
resource "aws_api_gateway_integration_response" "tutorialsdojo-otherintegrationresp200" {
}
resource "aws_api_gateway_integration_response" "tutorialsdojo-otherintegrationresp404" {
}

The expanded code is shown below:

resource "aws_api_gateway_resource" "tutorialsdojo-resource" {
 rest_api_id = aws_api_gateway_rest_api.tutorialsdojo-restapi.id
 parent_id   = aws_api_gateway_rest_api.tutorialsdojo-restapi.root_resource_id
 path_part   = "{proxy+}"
}


resource "aws_api_gateway_method" "tutorialsdojo-apigwmethodall" {
 rest_api_id   = aws_api_gateway_rest_api.tutorialsdojo-restapi.id
 resource_id   = aws_api_gateway_resource.tutorialsdojo-resource.id
 http_method   = "GET"
 authorization = "NONE"
 api_key_required = "false"
 request_parameters = {
   "method.request.path.proxy" = true
 }
}
resource "aws_api_gateway_integration" "tutorialsdojo-otherintegration" {
 rest_api_id             = "${aws_api_gateway_rest_api.tutorialsdojo-restapi.id}"
 resource_id             = "${aws_api_gateway_resource.tutorialsdojo-resource.id}"
 http_method             = "${aws_api_gateway_method.tutorialsdojo-apigwmethodall.http_method}"
 integration_http_method = "GET"
 type                    = "AWS"
 uri                     = "arn:aws:apigateway:${var.aws_region}:s3:path/${var.s3name}${var.s3index}"
 credentials             = "${var.iamrole}"


 request_parameters = {
   "integration.request.path.proxy" = "method.request.path.proxy"
 }
}


resource "aws_api_gateway_method_response" "tutorialsdojo-apigwmethodall200" {
 rest_api_id = "${aws_api_gateway_rest_api.tutorialsdojo-restapi.id}"
 resource_id = "${aws_api_gateway_resource.tutorialsdojo-resource.id}"
 http_method = "${aws_api_gateway_method.tutorialsdojo-apigwmethodall.http_method}"
 status_code = "200"
 response_parameters = {
   "method.response.header.Content-Type"   = false
   "method.response.header.Content-Length" = false
 }
}


resource "aws_api_gateway_method_response" "tutorialsdojo-apigwmethodall404" {
 rest_api_id = "${aws_api_gateway_rest_api.tutorialsdojo-restapi.id}"
 resource_id = "${aws_api_gateway_resource.tutorialsdojo-resource.id}"
 http_method = "${aws_api_gateway_method.tutorialsdojo-apigwmethodall.http_method}"
 status_code = "404"
}


resource "aws_api_gateway_integration_response" "tutorialsdojo-otherintegrationresp200" {
 rest_api_id = "${aws_api_gateway_rest_api.tutorialsdojo-restapi.id}"
 resource_id = "${aws_api_gateway_resource.tutorialsdojo-resource.id}"
 http_method = "${aws_api_gateway_method.tutorialsdojo-apigwmethodall.http_method}"
 status_code = "${aws_api_gateway_method_response.tutorialsdojo-apigwmethodall200.status_code}"
 response_parameters = {
   "method.response.header.Content-Type"   = "integration.response.header.Content-Type"
   "method.response.header.Content-Length" = "integration.response.header.Content-Length"
 }
 selection_pattern = ""
}


resource "aws_api_gateway_integration_response" "tutorialsdojo-otherintegrationresp404" {
 rest_api_id = "${aws_api_gateway_rest_api.tutorialsdojo-restapi.id}"
 resource_id = "${aws_api_gateway_resource.tutorialsdojo-resource.id}"
 http_method = "${aws_api_gateway_method.tutorialsdojo-apigwmethodall.http_method}"
 status_code = "${aws_api_gateway_method_response.tutorialsdojo-apigwmethodall404.status_code}"
 response_parameters = {
   "method.response.header.Content-Type"   = "integration.response.header.Content-Type"
   "method.response.header.Content-Length" = "integration.response.header.Content-Length"
 }
 selection_pattern = "404"
}

Breaking down the nlb module.

For the nlb module, we first build out the network load balancer resource and the target group.

resource "aws_lb" "tutorialsdojo-nlb" {
 name               = "${var.nlbname}"
 internal           = true
 load_balancer_type = "network"
 ip_address_type    = "ipv4"
 subnets            = ["${var.subnet0}","${var.subnet1}","${var.subnet2}"]


 enable_deletion_protection = false


 tags = local.common_tags
}


resource "aws_lb_target_group" "tutorialsdojo-nlbtg" {
 name        = "${var.nlbtg}"
 port        = 443
 protocol    = "TLS"
 vpc_id      = "${var.vpcid}"
 target_type = "ip"
 health_check {
   port     = 443
   protocol = "TCP"
 }
 tags = local.common_tags
}

The data can be separated as below:

data "aws_vpc_endpoint" "tutorialsdojo-apigwvpce" {
 vpc_id       = "${var.vpcid}"
 service_name = "com.amazonaws.us-east-1.execute-api"
}


data "aws_network_interface" "tutorialsdojo-vpceeni0" {
 id = "${var.eni0}"
}
data "aws_network_interface" "tutorialsdojo-vpceeni1" {
 id = "${var.eni1}"
}
data "aws_network_interface" "tutorialsdojo-vpceeni2" {
 id = "${var.eni2}"
}

Lastly, we build the load balancer target group attachments as well as the listener configuration of the load balancer.

resource "aws_lb_target_group_attachment" "tutorialsdojo-nlbattach0" {
 target_group_arn = "${aws_lb_target_group.tutorialsdojo-nlbtg.arn}"
 target_id        = "${data.aws_network_interface.tutorialsdojo-vpceeni0.private_ip}"
 port             = 443
}


resource "aws_lb_target_group_attachment" "tutorialsdojo-nlbattach1" {
 target_group_arn = "${aws_lb_target_group.tutorialsdojo-nlbtg.arn}"
 target_id        = "${data.aws_network_interface.tutorialsdojo-vpceeni1.private_ip}"
 port             = 443
}


resource "aws_lb_target_group_attachment" "tutorialsdojo-nlbattach2" {
 target_group_arn = "${aws_lb_target_group.tutorialsdojo-nlbtg.arn}"
 target_id        = "${data.aws_network_interface.tutorialsdojo-vpceeni2.private_ip}"
 port             = 443
}


resource "aws_lb_listener" "nlb-listener" {
 load_balancer_arn = "${aws_lb.tutorialsdojo-nlb.arn}"
 port              = "443"
 protocol          = "TLS"
 ssl_policy        = "${var.sslpolicy}"
 certificate_arn   = "${var.acmcertificatearn}"


 default_action {
   type             = "forward"
   target_group_arn = "${aws_lb_target_group.tutorialsdojo-nlbtg.arn}"
 }
}

Similar to before, the modules can be invoked from the main.tf file by adding its path in the source.

module "tutorialsdojo-nlb" {
 source                = "../../modules/nlb"
 aws_profile           = var.aws_profile
 aws_region            = var.aws_region
}


module "tutorialsdojo-apigatewayinternal" {
 source                = "../../modules/apigateway-internal"
 aws_profile           = var.aws_profile
 aws_region            = var.aws_region 
}

4. Add an A record pointing to the NLB record

Finally, we add an A record that points to the NLB endpoint to be able to access the web application via the private domain we defined.

Building a Private React Application Infrastructure with Terraform

Final Notes

As always, ensure that you destroy the infrastructure that you don’t need to avoid being charged unnecessarily. Building the infrastructure in Terraform allows you to easily do this by running terraform destroy, and it is a clean way to destroy all temporary resources created in AWS.

References:

https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-endpoint-types.html

Get 20% Off – Christmas Big Sale on All Practice Exams, Video Courses, and eBooks!

Tutorials Dojo portal

Learn AWS with our PlayCloud Hands-On Labs

Tutorials Dojo Exam Study Guide eBooks

tutorials dojo study guide eBook

FREE AWS Exam Readiness Digital Courses

FREE AWS, Azure, GCP Practice Test Samplers

Subscribe to our YouTube Channel

Tutorials Dojo YouTube Channel

Follow Us On Linkedin

Recent Posts

Written by: Kaye Alvarado

Kaye is a DevOps Engineer and the offshore lead of the API and Integration Management Team at Asurion. She is an AWS Community Builder, and a core member of AWSUG BuildHers+. She holds multiple AWS certifications, and volunteers to mentor others on DevOps skills training, and certification review sessions both inside and outside the company. On her free time, she creates comic strips about funny encounters in IT titled GIRLWHOCODES.

AWS, Azure, and GCP Certifications are consistently among the top-paying IT certifications in the world, considering that most companies have now shifted to the cloud. Earn over $150,000 per year with an AWS, Azure, or GCP certification!

Follow us on LinkedIn, YouTube, Facebook, or join our Slack study group. More importantly, answer as many practice exams as you can to help increase your chances of passing your certification exams on your first try!

View Our AWS, Azure, and GCP Exam Reviewers Check out our FREE courses

Our Community

~98%
passing rate
Around 95-98% of our students pass the AWS Certification exams after training with our courses.
200k+
students
Over 200k enrollees choose Tutorials Dojo in preparing for their AWS Certification exams.
~4.8
ratings
Our courses are highly rated by our enrollees from all over the world.

What our students say about us?