Build AWS Web Application Layer With Terraform | Infra-As-Code

Build AWS Web Application Layer With Terraform | Infra-As-Code

Welcome back to the series of Deploying On AWS Cloud Using Terraform 👨🏻‍💻. This terraform usecase is created for DevOps - Infrastructure Automation on AWS Project. Based on our previous learnings from the last 10 blog series we are going to create a real-time basic web application tier to have some hands-on and understand how its implementation works in real-time.

If you are a beginner for Terraform and want to start your journey towards infra-as-code developer as part of your DevOps role, buckle up 🚴‍♂️ and let's get started and understand core Terraform concepts by implementing it...🎬

❗️❗️Pre-Requisite❗️❗️

1️⃣ Deploying On AWS Cloud Using Terraform Series Pre-Requisites

2️⃣ Terraform Naming Conventions & Best Practices

🔎Basic Terraform Configurations🔍

As part of the basic configuration we are going to set up 3 terraform files

  1. Providers File:- Terraform relies on plugins called "providers" to interact with cloud providers, SaaS providers, and other APIs.
    Providers are distributed separately from Terraform itself, and each provider has its own release cadence and version numbers.
    The Terraform Registry is the main directory of publicly available Terraform providers, and hosts providers for most major infrastructure platforms. Each provider has its own documentation, describing its resource types and their arguments.
    We would be using AWS Provider for our terraform series. Make sure to refer Terraform AWS documentation for up-to-date information.
    Provider documentation in the Registry is versioned; you can use the version menu in the header to change which version you're viewing.
provider "aws" {
  region                  = "var.AWS_REGION"
  shared_credentials_file = "<Your AWS Credentials File path>"
}
  1. Variables File:- Terraform variables lets us customize aspects of Terraform modules without altering the module's own source code. This allows us to share modules across different Terraform configurations, reusing the same data at multiple places.
    When you declare variables in the root terraform module of your configuration, you can set their values using CLI options and environment variables. When you declare them in child modules, the calling module should pass values in the module block.
variable "AWS_REGION" {
  default = "us-east-1"
}
#-------------------------Variables For Autoscaling---------------------
variable "instance_type" {
  type    = string
  default = "t2.micro"
}
variable "autoscaling_group_min_size" {
  type    = number
  default = 2
}
variable "autoscaling_group_max_size" {
  type    = number
  default = 3
}
variable "aws_key_pair" {
  type    = string
  default = "<File Path>"
}
#-------------------------Data Block to fetch subnet ids---------------------
data "aws_subnet_ids" "GetSubnet_Ids" {
  vpc_id = aws_vpc.CustomVPC.id
  filter {
    name   = "tag:Type"
    values = ["Public"]
  }
}
  1. Versions File:- It's always a best practice to maintain a version file where you specific version based on which your stack is testing and live on production.
terraform {
  required_version = ">= 0.12"
}

🎨 Diagrammatic Representation 🎨

image.png

👨🏻‍💻Configure Virtual Private Cloud👨🏻‍💻

Now as we have configured basic files for providers, variables and versions let's move ahead and start coding our VPC file.

🔳 Resources

AWS_VPC:- This resource is used to launch a private VPC in the configured AWS account.

🔳 Arguments

cidr_block- This IPv4 CIDR block for the VPC is an optional argument. CIDR can be explicitly set or it can be derived from IPAM using ipv4_netmask_length.
enable_dns_support - This is an optional argument that denotes a boolean flag to enable/disable DNS support in the VPC with the default value as true.
enable_dns_hostnames - This is an optional argument that denotes a boolean flag to enable/disable DNS hostnames in the VPC with the default value as true.
tags:- One of the most important property used in all resources. Always make sure to attach tags for all your resources.

#---------------------Create Custom VPC----------------------
resource "aws_vpc" "CustomVPC" {
  cidr_block           = "10.0.0.0/16"
  instance_tenancy     = "default"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "CustomVPC"
  }
}

👨🏻‍💻Configure Internet Gateway & Associate Them👨🏻‍💻

Now let's go ahead and configure our internet gateway resource and associate that internet gateway to the VPC we have created above.

🔳 Resources

aws_internet_gateway:- This resource is used to launch a private VPC in the configured AWS account.

🔳 Arguments

vpc_id- This VPC id argument refers to the id value of the VPC which has been created so the internet gateway can get associated with it.
tags:- One of the most important property used in all resources. Always make sure to attach tags for all your resources.

#---------------------Create IGW And associate with VPC----------------------
resource "aws_internet_gateway" "IGW" {
  vpc_id = aws_vpc.CustomVPC.id

  tags = {
    Name = "IGW"
  }
}

👨🏻‍💻Configure Public Subnet👨🏻‍💻

As a part of this code block, we are going to create 2 subnets in 2 different availability zone as part of this region.

🔳 Resource

aws_subnet:- This resource is used to launch a subnet associated with a vpc and tied up with the availability zone. This subnet is nothing but a segregation of the IP address range of your VPC.

🔳 Arguments

vpc_id:- Refers to the id of a VPC to which it would be associated to utilize its CIDR IP range.
cidr_block- This IPv4 CIDR block for the VPC is an optional argument. CIDR can be explicitly set or it can be derived from IPAM using ipv4_netmask_length.
availability_zone - This is an optional argument to mention AZ for the subnet for a specific region under which it is launched.
map_public_ip_on_launch - This is an optional argument that indicates instances launched into the subnet should be assigned a public IP address.
tags:- One of the most important property used in all resources. Always make sure to attach tags for all your resources.

resource "aws_subnet" "PublicSubnet1" {
  vpc_id                  = aws_vpc.CustomVPC.id
  cidr_block              = "10.0.0.0/18"
  availability_zone       = "us-east-1a"
  map_public_ip_on_launch = true

  tags = {
    Name = "PublicSubnet1"
    Type = "Public"
  }
}
resource "aws_subnet" "PublicSubnet2" {
  vpc_id                  = aws_vpc.CustomVPC.id
  cidr_block              = "10.0.64.0/18"
  availability_zone       = "us-east-1b"
  map_public_ip_on_launch = true

  tags = {
    Name = "PublicSubnet2"
    Type = "Public"
  }
}

👨🏻‍💻Configure Public Route Table & Associate👨🏻‍💻

Let's first focus on the code to create a custom route table and add an internet gateway route to that table.

🔳 Resource

aws_route_table:- This resource is to define create route tables and define routes within them.

🔳 Arguments

vpc_id:- Refers to the id of a VPC to which it would be associated.
route:- This is an optional argument which to define a list of routes to be added to the route table we are creating. ✦ tags:- One of the most important property used in all resources. Always make sure to attach tags for all your resources.

#---------------------Create Custom Public route table----------------------
resource "aws_route_table" "PublicRouteTable" {
  vpc_id = aws_vpc.CustomVPC.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.IGW.id
  }
  tags = {
    Name = "PublicRouteTable"
  }
}

As you can see above we have created our custom route table and also added routes to that custom route table. Now we have to associate this route table to our subnets to make these subnets public.

🔳 Resource

aws_route_table_association:- This resource is used to create an association between the route table and a subnet/internet gateway.

🔳 Arguments

subnet_id:- Refers to id of a subnet to which it needs to be associated.
route_table_id:- Refers to route table id to which we have to associate our subnet.

#---------Create route table association with public route table--------
resource "aws_route_table_association" "PublicSubnetRouteTableAssociation1" {
  subnet_id      = aws_subnet.PublicSubnet1.id
  route_table_id = aws_route_table.PublicRouteTable.id
}
resource "aws_route_table_association" "PublicSubnetRouteTableAssociation2" {
  subnet_id      = aws_subnet.PublicSubnet2.id
  route_table_id = aws_route_table.PublicRouteTable.id
}

👨🏻‍💻Configure Security Group For Load Balancer👨🏻‍💻

The method acts as a virtual firewall to control your inbound and outbound traffic flowing to your EC2 instances inside a subnet.

🔳 Resource

aws_security_group:- This resource is define traffic inbound and outbound rules on the subnet level.

🔳 Arguments

name:- This is an optional argument to define the name of the security group.
description:- This is an optional argument to mention details about the security group that we are creating.
vpc_id:- This is a mandatory argument and refers to the id of a VPC to which it would be associated.
tags:- One of the most important property used in all resources. Always make sure to attach tags for all your resources. EGRESS & INGRESS are processed in attribute-as-blocks mode.

#---------------------Create Security Group For Load Balancer----------------
resource "aws_security_group" "elb_sg" {
  name        = "allow_http_elb"
  description = "Allow http inbound traffic for elb"
  vpc_id      = aws_vpc.CustomVPC.id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = {
    Name = "terraform-elb-security-group"
  }
}

👨🏻‍💻Configure Target Group👨🏻‍💻

🔳 Resource

aws_lb_target_group:- This resource group resources for use so that it can be associated with load balancers.

🔳 Arguments

name:- This is an optional argument to define the name of the target group.
port:- This is a mandatory argument to mention the port on which targets receive traffic unless overridden when registering a specific target.
vpc_id:- This is a mandatory argument and refers to the id of a VPC to which it would be associated.
protocol:- This is a mandatory argument as our target type is "instance". Protocol to use for routing traffic to the targets. Should be one of "TCP", "TLS", "UDP", "TCP_UDP", "HTTP" or "HTTPS".
target_type:- This is an optional argument with target types as an instance, IP, and lambda.

#---------------------Create Target Group----------------------
resource "aws_lb_target_group" "CustomTG" {
  name        = "CustomTG"
  port        = 80
  protocol    = "HTTP"
  vpc_id      = aws_vpc.CustomVPC.id
  target_type = "instance"
}

👨🏻‍💻Configure Load Balancer & Listener👨🏻‍💻

🔳 Resource

aws_lb:- This resource is used to create a load balancer that helps us distribute our traffic.

🔳 Arguments

name:- This is an optional argument to define the name of the Load Balancer.
subnets:- This is an optional argument to mention which load balancer will be part of which subnets.
security_groups:- This is an optional argument to mention which controls your inbound and outbound traffic flowing.
tags:- One of the most important property used in all resources. Always make sure to attach tags for all your resources.

resource "aws_alb" "CustomELB" {
  name            = "CustomELB"
  security_groups = [aws_security_group.elb_sg.id]
  subnets         = data.aws_subnet_ids.GetSubnet_Ids.ids
  tags = {
    Name = "CustomELB"
  }
}

Let's now create a new load balancer listener which will be configured to accept HTTP client connections.

🔳 Resource

aws_lb_listener:- This resource is used to create a load balancer listener which helps us to check for connection requests, using the protocol and port that you configure.

🔳 Arguments

load_balancer_arn:- This is a mandatory argument to define arn of the Load Balancer by using arn attribute.
port:- This is an optional argument to mention the port on which targets receive traffic.
protocol:- This is an optional argument as our target type is "instance". Protocol to use for routing traffic to the targets. Should be one of "TCP", "TLS", "UDP", "TCP_UDP", "HTTP" or "HTTPS".
✦ **default_action **:- This is a mandatory argument to define the type of routing for this listener.

#---------------------Create Load Balancer Listener----------------------
resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_alb.CustomELB.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type = "forward"
    forward {
      target_group {
        arn = aws_lb_target_group.CustomTG.arn
      }
      stickiness {
        enabled  = true
        duration = 28800
      }
    }
  }
}

👨🏻‍💻Configure Security Group For Launch Configuration

The method which acts as a virtual firewall to control your inbound and outbound traffic flowing in and out.

🔳 Resource

aws_security_group:- This resource is define traffic inbound and outbound rules on the subnet level.

🔳 Arguments

name:- This is an optional argument to define the name of the security group.
description:- This is an optional argument to mention details about the security group that we are creating.
✦ **vpc_id **:- This is a mandatory argument and refers to the id of a VPC to which it would be associated.
tags:- One of the most important property used in all resources. Always make sure to attach tags for all your resources.
EGRESS & INGRESS are processed in attribute-as-blocks mode.

#---------------------Create Security Group For EC2----------------------
resource "aws_security_group" "ec2_sg" {
  name        = "allow_http_elb"
  description = "Allow http inbound traffic for elb"
  vpc_id      = aws_vpc.CustomVPC.id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = ["${aws_security_group.elb_sg.id}"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = {
    Name = "terraform-elb-security-group"
  }
}

👨🏻‍💻Deploy Launch Config and AutoScaling group👨🏻‍💻

Before creating the Autoscaling group let's first define the launch configuration for EC2 instances that will be used by autoscaling later.

🔳 Resource

aws_launch_configuration:- This resource is create a launch configuration for EC2 instances that we are going to deploy as part of our autoscaling group.

🔳 Arguments

name_prefix:- This is an optional argument to create a unique name beginning with the specified prefix.
image_id:- This is an mandatory argument to mention image_id id based on which EC2 instance will be launched.
instance_type:- This is a mandatory argument to mention instance type for the EC2 instances like t2.small, t2.micro etc.
key_name:- This is an optional argument to define to enable ssh connection to your EC2 instance.
security_groups:- This is an optional argument to mention which controls your inbound and outbound traffic flowing to your EC2 instances inside a subnet.
user_data :- This is an optional argument to provide commands or scripts to be executed during the launch of the EC2 instance.
Lifecycle:- Lifecycle is a nested block that can appear within a resource block.
create_before_destroy:- when Terraform must change a resource argument that cannot be updated in place due to remote API limitations, Terraform will instead destroy the existing object and then create a new replacement object with the newly configured arguments.

resource "aws_launch_configuration" "webteir_launch_config" {
  name_prefix                 = "webteir"
  image_id                    = "ami-0742b4e673072066f"
  instance_type               = var.instance_type
  key_name                    = var.aws_key_pair
  security_groups             = ["${aws_security_group.ec2_sg.id}"]
  associate_public_ip_address = true
  user_data                   = <<EOF
    #! /bin/bash
    sudo su
    sudo yum update
    sudo yum install -y httpd
    sudo chkconfig httpd on
    sudo service httpd start
    echo "<h1>Deployed EC2 Using ASG</h1>" | sudo tee /var/www/html/index.html
    EOF
  lifecycle {
    create_before_destroy = true
  }
}

🔳 Resource

aws_autoscaling_group:- This resource group resources for use so that it can be associated with load balancers.

🔳 Arguments

launch_configuration:- This is an optional argument to mention the name of the launch configuration to be used.
min_size:- This is a mandatory argument to define the minimum size of the Autoscaling group.
max_size:-This is a mandatory argument to define the maximum size of the Autoscaling group.
target_group_arns:- This is an optional argument to define the target group arn to which EC2 can register.
vpc_zone_identifier:- This is an optional argument to define a list of subnet IDs to launch resources in.
tags:- One of the most important property used in all resources. Always make sure to attach tags for all your resources.

resource "aws_autoscaling_group" "autoscaling_group_webteir" {
  launch_configuration = aws_launch_configuration.webteir_launch_config.id
  min_size             = var.autoscaling_group_min_size
  max_size             = var.autoscaling_group_max_size
  target_group_arns    = ["${aws_lb_target_group.CustomTG.arn}"]
  vpc_zone_identifier  = data.aws_subnet_ids.GetSubnet_Ids.ids

  tag {
    key                 = "Name"
    value               = "autoscaling-group-webteir"
    propagate_at_launch = true
  }
}

🔳 Output File

Output values make information about your infrastructure available on the command line, and can expose information for other Terraform configurations to use. Output values are similar to return values in programming languages.

output "vpc_id" {
  value       = aws_vpc.CustomVPC.id
  description = "This is vpc id."
}
output "enable_dns_support" {
  value       = aws_vpc.CustomVPC.enable_dns_support
  description = "Check whether dns support is enabled for VPC."
}
output "enable_dns_hostnames" {
  value       = aws_vpc.CustomVPC.enable_dns_hostnames
  description = "Check whether dns hostname is enabled for VPC."
}
output "aws_internet_gateway_id" {
  value       = aws_internet_gateway.IGW.id
  description = "Internet gateway id."
}
output "igw_aws_account" {
  value       = aws_internet_gateway.IGW.owner_id
  description = "AWS Account id to which internet gateway is associated."
}
output "PublicSubnet1" {
  value       = aws_subnet.PublicSubnet1.id
  description = "This is first public subnet id."
}
output "PublicSubnet2" {
  value       = aws_subnet.PublicSubnet2.id
  description = "This is first second subnet id."
}
output "PublicRouteTable" {
  value       = aws_route_table.PublicRouteTable.id
  description = "List custom public route table id."
}
output "CustomTG" {
  value       = aws_lb_target_group.CustomTG.id
  description = "This is Target Group id."
}
output "CustomELB" {
  value       = aws_alb.CustomELB.id
  description = "This is load balancer ID."
}
output "elb_sg" {
  value       = aws_security_group.elb_sg.id
  description = "This is Security Group ID for load balancer."
}
output "asg_sg" {
  value       = aws_security_group.ec2_sg.id
  description = "This is Security Group ID for Ec2."
}
output "aws_launch_configuration" {
  value       = aws_launch_configuration.webteir_launch_config.id
  description = "This is ASG Launch Configuration ID."
}
output "autoscaling_group_dev" {
  value       = aws_autoscaling_group.autoscaling_group_webteir.id
  description = "This is ASG ID."
}

🔊To view the entire GitHub code click here

1️⃣ The terraform fmt command is used to rewrite Terraform configuration files to a canonical format and style👨‍💻.

terraform fmt

2️⃣ Initialize the working directory by running the command below. The initialization includes installing the plugins and providers necessary to work with resources. 👨‍💻

terraform init

3️⃣ Create an execution plan based on your Terraform configurations. 👨‍💻

terraform plan

4️⃣ Execute the execution plan that the terraform plan command proposed. 👨‍💻

terraform apply -auto-approve

👁‍🗨👁‍🗨 YouTube Tutorial 📽

[Coming Soon]

ELB (2).png

destroy.png

❗️❗️Important Documentation To Be Viewed❗️❗️

⛔️ Hashicorp Terraform
⛔️ AWS CLI
⛔️ Hashicorp Terraform Extension Guide
⛔️ Terraform Autocomplete Extension Guide
⛔️ AWS VPC
⛔️ AWS Internet Gateway
⛔️ AWS Subnet
⛔️ AWS Route Table
⛔️ AWS Route Table Association
⛔️ AWS Security Group
⛔️ AWS Target Group
⛔️ AWS Target Group Attachment
⛔️ Terraform Length Function
⛔️ AWS Load Balancer
⛔️ AWS Load Balancer Listener
⛔️ AWS Launch Configuration
⛔️ AWS Autoscaling Group
⛔️ Lifecycle Meta-Argument

🥁🥁 Conclusion 🥁🥁

In this blog, I have demonstrated the provisioning of the web application layer on AWS using Terraform following Terraform's best practices. I have also referenced what arguments and documentation we are going to use so that while you are writing the code it would be easy for you to understand terraform official documentation.

📢 Stay tuned for my next blog.....

🎊**So, did you find my content helpful? If you did or like my other content, feel free to buy me a coffee. Thanks. **🎊

👨🏻‍💻Terraform Github Repository👨🏻‍💻

Did you find this article valuable?

Support Dheeraj Choudhary by becoming a sponsor. Any amount is appreciated!