Implement DevOps to deploy IT Infrastructure - Terraform


Terraform to deploy your Infrastructure


Introduction:


 In this article, we will see how can we deploy our IT infrastructure using Terraform. Before we get into the procedure and steps, let’s first check the need of using Terraform for such task.

Terraform to deploy your infrastructureWhy do we need Terraform?

 I already mentioned in my earlier article, that Terraform can be blended with almost all providers – be it AWS, Azure, Google Cloud, VmWare etc. So, for obvious reason, it will give you more flexibility when you deploy your infrastructure with Terraform. May be you have chosen AWS for a specific part of your application. You can choose Azure or Google cloud for other part of your same application easily. You can manage your infrastructure from a single place or repository. So, Terraform give you the privilege to think for other cloud providers when your application or website is already in RUN mode.

Secondly, It centralize the infrastructural components. You don’t need to remember where you have deployed your A component or B component – which Availability Zone, Which VPC, which messaging queue and which cloud provider did you choose. Because, all of those are stored in a repository in the form of Terraform file.

Third, at any point if you want to go back to the previous step of deployment, it is possible as Terraform keep state file for each and every change.

In a lucid word, you are versioning your infrastructure in a single place that does not care about the cloud provider you’ve chosen. That’s the beauty or the main reason for opting Terraform to deploy your infrastructure.

I hope, it make sense now to use Terraform for deploying your IT infrastructure.

Create/Frame a commonly used case:


We will create a simple application in AWS using Terraform. We believe, it will assist many users who wants to use Terraform for deploying their infrastructure in AWS. For simplicity, we will restrict our discussion within AWS.

When you want to deploy any application to AWS, it is a mandate to make a planning prior to actual deployment. This will help you to formulate the Terraform code.

So, let's start.

I. Define provider


First, we need to define the provider. Because, Terraform needs to know where to connect and how to connect. Here, we will be defining for AWS.

There are 3 different ways to authenticate with AWS.
    • Static credentials.
    • Placing credentials in environmental variables
    • Using shared credential file.
We prefer to set it via shared credential file.

Create a file name called "provider.tf" and paste the below contents:

# Configure the AWS Provider
provider "aws" {
  region  = "us-east-1"
  shared_credentials_file = "~/.aws/credentials"
  profile = "demo"
}
data "aws_availability_zones" "available" {}

Be careful about the profile value. It should match with the value that you put in AWS credentials and config file.

II. Declare variables

Now, we will first declare the variable that we need. Create a file called tfvars.tf and insert the below content.

variable "public_key_path" {
  default = "/home/user/.ssh/id_rsa.pub"
}
variable "key_name" {
  default     = "terraform"
}
variable "aws_region" {
  description = "AWS region to launch servers."
  default     = "us-east-1"
}
# Amazon Linux
variable "aws_amis" {
  default = {
    us-east-1 = "ami-bb5642ac"
  }
}
variable "dnszonename" {
  default = "aws.techreader.net"
  description = "Internal DNS name"
}
variable "vpc_cidr" {
    default = "10.0.0.0/20"
  description = "VPC CIDR range"
}
variable "public_subnet_a" {
  default = "10.0.0.0/24"
  description = "Public subnet AZ A"
}
variable "public_subnet_b" {
  default = "10.0.4.0/24"
  description = "Public subnet AZ B"
}
variable "public_subnet_c" {
  default = "10.0.8.0/24"
  description = "Public subnet AZ C"
}
variable "private_subnet_a" {
  default = "10.0.1.0/24"
  description = "Private subnet AZ A"
}
variable "private_subnet_b" {
  default = "10.0.5.0/24"
  description = "Private subnet AZ B"
}
variable "private_subnet_c" {
  default = "10.0.9.0/24"
  description = "Private subnet AZ C"
} 

*user will be your ssh user where you generated ssh keys. If you have not done yet, you need to fork ssh-keygen -t rsa and follow the instructions.
 
III. Create VPC stuffs

In this part, we will be defining the basic network. So, we will create VPC, Subnets, Internet Gateway, Route tables, NAT gateways at this step.

Sample Terraform configuration (vpc.tf) will look like below:


# Create a VPC
resource "aws_vpc" "default" {
    cidr_block = "${var.vpc_cidr}"
    enable_dns_support = true
    enable_dns_hostnames = true
    tags = {
      Name = "VPC"
    }
}
# Create Public subnets
resource "aws_subnet" "PublicSubnetA" {
  vpc_id = "${aws_vpc.default.id}"
  cidr_block = "${var.public_subnet_a}"
  tags = {
        Name = "Public Subnet A"
  }
 availability_zone = "${data.aws_availability_zones.available.names[0]}"
}
resource "aws_subnet" "PublicSubnetB" {
  vpc_id = "${aws_vpc.default.id}"
  cidr_block = "${var.public_subnet_b}"
  tags = {
        Name = "Public Subnet B"
  }
 availability_zone = "${data.aws_availability_zones.available.names[1]}"
}
resource "aws_subnet" "PublicSubnetC" {
  vpc_id = "${aws_vpc.default.id}"
  cidr_block = "${var.public_subnet_c}"
  tags = {
        Name = "Public Subnet C"
  }
 availability_zone = "${data.aws_availability_zones.available.names[2]}"
}
# Create Pravate subnets
resource "aws_subnet" "PrivateSubnetA" {
  vpc_id = "${aws_vpc.default.id}"
  cidr_block = "${var.private_subnet_a}"
  tags = {
        Name = "Private Subnet A"
  }
 availability_zone = "${data.aws_availability_zones.available.names[0]}"
}
resource "aws_subnet" "PrivateSubnetB" {
  vpc_id = "${aws_vpc.default.id}"
  cidr_block = "${var.private_subnet_b}"
  tags = {
        Name = "Private Subnet B"
  }
 availability_zone = "${data.aws_availability_zones.available.names[1]}"
}
resource "aws_subnet" "PrivateSubnetC" {
  vpc_id = "${aws_vpc.default.id}"
  cidr_block = "${var.private_subnet_c}"
  tags = {
        Name = "Private Subnet C"
  }
 availability_zone = "${data.aws_availability_zones.available.names[2]}"
}
# Route table management
resource "aws_route_table_association" "PublicSubnetA" {
    subnet_id = "${aws_subnet.PublicSubnetA.id}"
    route_table_id = "${aws_route_table.public_route_a.id}"
}
resource "aws_route_table_association" "PublicSubnetB" {
    subnet_id = "${aws_subnet.PublicSubnetB.id}"
    route_table_id = "${aws_route_table.public_route_b.id}"
}
resource "aws_route_table_association" "PublicSubnetC" {
    subnet_id = "${aws_subnet.PublicSubnetC.id}"
    route_table_id = "${aws_route_table.public_route_c.id}"
}
resource "aws_route_table_association" "PrivateSubnetA" {
    subnet_id = "${aws_subnet.PrivateSubnetA.id}"
    route_table_id = "${aws_route_table.private_route_a.id}"
}
resource "aws_route_table_association" "PrivateSubnetB" {
    subnet_id = "${aws_subnet.PrivateSubnetB.id}"
    route_table_id = "${aws_route_table.private_route_b.id}"
}
resource "aws_route_table_association" "PrivateSubnetC" {
    subnet_id = "${aws_subnet.PrivateSubnetC.id}"
    route_table_id = "${aws_route_table.private_route_c.id}"
}
#Internet Gateway 
resource "aws_internet_gateway" "gw" {
   vpc_id = "${aws_vpc.default.id}"
    tags = {
        Name = "Internet Gateway"
    }
}
# NAT gateways
resource "aws_eip" "natgw_a" {
    vpc      = true
}
resource "aws_eip" "natgw_b" {
    vpc      = true
}
resource "aws_eip" "natgw_c" {
    vpc      = true
}
resource "aws_nat_gateway" "public_nat_a" {
    allocation_id = "${aws_eip.natgw_a.id}"
    subnet_id = "${aws_subnet.PublicSubnetA.id}"
    depends_on = ["aws_internet_gateway.gw"]
}
resource "aws_nat_gateway" "public_nat_b" {
    allocation_id = "${aws_eip.natgw_b.id}"
    subnet_id = "${aws_subnet.PublicSubnetB.id}"
    depends_on = ["aws_internet_gateway.gw"]
}
resource "aws_nat_gateway" "public_nat_c" {
    allocation_id = "${aws_eip.natgw_c.id}"
    subnet_id = "${aws_subnet.PublicSubnetC.id}"
    depends_on = ["aws_internet_gateway.gw"]
}
# Preparing ACL
resource "aws_network_acl" "all" {
   vpc_id = "${aws_vpc.default.id}"
    egress {
        protocol = "-1"
        rule_no = 2
        action = "allow"
        cidr_block =  "0.0.0.0/0"
        from_port = 0
        to_port = 0
    }
    ingress {
        protocol = "-1"
        rule_no = 1
        action = "allow"
        cidr_block =  "0.0.0.0/0"
        from_port = 0
        to_port = 0
    }
    tags = {
        Name = "open acl"
    }
}
# Associations
resource "aws_route_table" "public_route_a" {
  vpc_id = "${aws_vpc.default.id}"
  tags = {
      Name = "Public Route A"
  }
  route {
        cidr_block = "0.0.0.0/0"
        gateway_id = "${aws_internet_gateway.gw.id}"
    }
}
resource "aws_route_table" "public_route_b" {
  vpc_id = "${aws_vpc.default.id}"
  tags = {
      Name = "Public Route B"
  }
  route {
        cidr_block = "0.0.0.0/0"
        gateway_id = "${aws_internet_gateway.gw.id}"
    }
}
resource "aws_route_table" "public_route_c" {
  vpc_id = "${aws_vpc.default.id}"
  tags = {
      Name = "Public Route C"
  }
  route {
        cidr_block = "0.0.0.0/0"
        gateway_id = "${aws_internet_gateway.gw.id}"
    }
}
resource "aws_route_table" "private_route_a" {
  vpc_id = "${aws_vpc.default.id}"
  tags = {
      Name = "Private Route A"
  }
  route {
        cidr_block = "0.0.0.0/0"
        nat_gateway_id = "${aws_nat_gateway.public_nat_a.id}"
  }
}
resource "aws_route_table" "private_route_b" {
  vpc_id = "${aws_vpc.default.id}"
  tags = {
      Name = "Private Route B"
  }
  route {
        cidr_block = "0.0.0.0/0"
        nat_gateway_id = "${aws_nat_gateway.public_nat_b.id}"
  }
}
resource "aws_route_table" "private_route_c" {
  vpc_id = "${aws_vpc.default.id}"
  tags = {
      Name = "Private Route C"
  }
  route {
        cidr_block = "0.0.0.0/0"
        nat_gateway_id = "${aws_nat_gateway.public_nat_c.id}"
  }
}

IV. Create ec2 artifacts

In this section, we will create our desired ec2 instances and attach them to respective AZ

The Terraform config(ec2.tf) will look like below:

resource "aws_key_pair" "auth" {
  key_name   = "${var.key_name}"
  public_key = "${file(var.public_key_path)}"
}

resource "aws_launch_configuration" "autoscale_launch" {
  image_id = "${lookup(var.aws_amis, var.aws_region)}"
  instance_type = "t2.micro"
  security_groups = ["${aws_security_group.sec_web.id}"]
  key_name = "${aws_key_pair.auth.id}"
  user_data = <<-EOF
              #!/bin/bash
              sudo yum -y update
              sudo yum -y install nginx
              EOF
  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_autoscaling_group" "autoscale_group" {
  launch_configuration = "${aws_launch_configuration.autoscale_launch.id}"
  vpc_zone_identifier = ["${aws_subnet.PrivateSubnetA.id}","${aws_subnet.PrivateSubnetB.id}","${aws_subnet.PrivateSubnetC.id}"]
  load_balancers = ["${aws_lb.alb.name}"]
  min_size = 3
  max_size = 3
  tag {
    key = "Name"
    value = "autoscale"
    propagate_at_launch = true
  }
}
# Security Groups
resource "aws_security_group" "sec_web" {
  name        = "sec_web"
  description = "Used for autoscale group"
  vpc_id      = "${aws_vpc.default.id}"

  # HTTP access from anywhere
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  
  # outbound internet access
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_security_group" "sec_lb" {
  name = "sec_elb"
  vpc_id      = "${aws_vpc.default.id}"
  
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  
  ingress {
    from_port = 80
    to_port = 80
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_lb" "alb" {  
  name            = "alb"  
  subnets         = ["${aws_subnet.PublicSubnetA.id}","${aws_subnet.PublicSubnetB.id}","${aws_subnet.PublicSubnetC.id}"]
  security_groups = ["${aws_security_group.sec_lb.id}"]
  internal        = false 
  idle_timeout    = 60   
  tags = {    
    Name    = "alb"    
  }   
}
# ALB target definition
resource "aws_lb_target_group" "alb_target_group" {  
  name     = "alb-target-group"  
  port     = "80"  
  protocol = "HTTP"  
  vpc_id   = "${aws_vpc.default.id}"   
  tags = {    
    name = "alb_target_group"    
  }   
  stickiness {    
    type            = "lb_cookie"    
    cookie_duration = 1800    
    enabled         = true 
  }   
  health_check {    
    healthy_threshold   = 3    
    unhealthy_threshold = 10    
    timeout             = 5    
    interval            = 10    
    path                = "/"    
    port                = 80
  }
}

resource "aws_autoscaling_attachment" "alb_autoscale" {
  alb_target_group_arn   = "${aws_lb_target_group.alb_target_group.arn}"
  autoscaling_group_name = "${aws_autoscaling_group.autoscale_group.id}"
}

resource "aws_lb_listener" "alb_listener" {  
  load_balancer_arn = "${aws_lb.alb.arn}"  
  port              = 80  
  protocol          = "HTTP"
  
  default_action {    
    target_group_arn = "${aws_lb_target_group.alb_target_group.arn}"
    type             = "forward"  
  }
}

After creating the four Terraform files, you need to run below Terraform commands to create your desired AWS infrastructure:

>> terraform init

Sample output will look like:

Initializing modules...
Downloading terraform-aws-modules/vpc/aws 2.9.0 for vpc...
- vpc in .terraform/modules/vpc/terraform-aws-modules-terraform-aws-vpc-b51422b

Initializing the backend...

Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "aws" (terraform-providers/aws) 2.24.0...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.aws: version = "~> 2.24"

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

>> terraform validate

Success! The configuration is valid.

>> terraform plan

While forking the above command, you will find that all your required resources are provisioned. You will also see the number of artifacts that will be created, modified or destroyed.

Last few lines of the output will look like below:

Plan: 36 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.


>> terraform apply

This will indeed create all the resource in AWS.

>> (Optional) terraform destroy

This command will destroy all artifacts that you have created in AWS.

You are now done. You have now successfully deployed your simple infrastructure into AWS cloud using Terraform.

Note: The state file MUST be kept in a safe place, may be in github or s3 in case of AWS.

Takeout from this article:


So, let’s summarize once more what have you learned from this article:

We see the benefits of using Terraform while deploying the infrastructure.
We have learned how to create the planning before deploying Terraform.
We now know how to deploy infrastructure using Terraform.

Future scope:


We will add versioning of the Terraform codes in github.
We will also see how a infrastructure change request can be performed in a smooth way using a single click.

Conclusion:


I hope that this article will assist at least some of young Terraform users while deploying their IT infrastructure into AWS. Please feel free to comment or provide your feedback. Your feedback is extremely valuable to us. In case if you want to contact me, please use "Contact us" page.  

Comments