Terraform Project 1: Deploying AWS resources

ยท

13 min read

Terraform Project 1: Deploying AWS resources

Contents:

  1. Introduction

  2. What is Terraform and why it is the easiest configuration management tool?

  3. Creating root modules

  4. Creating chile modules

  5. Steps for execution

  6. AWS services console output

Introduction:

In this project, we will deploy the AWS services like VPC, SUBNET, NIC, Security Group and EC2 instances using Terraform as a configuration management tool. Also, we will see the concept of the Root and Child Module which is being used.

Terraform:

Terraform is an open-source infrastructure as code (IaC) tool developed by HashiCorp. It allows you to define and manage your infrastructure resources in a declarative manner. With Terraform, you can describe your infrastructure requirements using a simple and human-readable language, called HashiCorp Configuration Language (HCL), or optionally, JSON.

The main goal of Terraform is to enable you to provision and manage infrastructure resources across various cloud providers (such as AWS, Azure, Google Cloud, etc.) as well as on-premises systems. It follows the principle of infrastructure as code, where you define your infrastructure resources in a text file, version control it, and treat it like any other code artifact.

Here are some key reasons why Terraform is commonly used as a DevOps tool:

  1. Infrastructure as Code (IaC): Terraform allows you to define your infrastructure as code, enabling you to version control, review, and collaborate on infrastructure changes, just like you would with application code. This approach brings automation, reproducibility, and consistency to your infrastructure provisioning process.

  2. Multi-Cloud and Hybrid Cloud Support: Terraform provides a consistent workflow to manage resources across multiple cloud providers and on-premises systems. This allows you to adopt a multi-cloud or hybrid cloud strategy, leveraging the best features and services from different providers without being locked into a single platform.

  3. Declarative Syntax: With its declarative syntax, Terraform allows you to define the desired state of your infrastructure. It automatically figures out the execution plan to reach that desired state, taking care of resource dependencies and order of operations. This simplifies infrastructure management and reduces the risk of configuration drift.

  4. Infrastructure Lifecycle Management: Terraform manages the entire lifecycle of your infrastructure resources, including provisioning, updating, and destroying. It keeps track of the current state of your infrastructure and performs the necessary changes to bring it in line with your desired state. This helps with maintaining consistency and simplifies resource management.

  5. Infrastructure Orchestration: Terraform provides the ability to orchestrate complex infrastructure setups by defining dependencies between resources, creating resource graphs, and applying changes in a controlled manner. This allows you to manage complex infrastructure deployments, such as multi-tier applications, networking, security groups, and more.

  6. Community and Ecosystem: Terraform has a large and active community, which means there is a wealth of shared knowledge, best practices, and community-contributed modules available. The ecosystem around Terraform includes a vast collection of pre-built modules and plugins that can be leveraged to accelerate infrastructure provisioning and configuration.

Overall, Terraform simplifies and automates the process of infrastructure provisioning and management, bringing agility, scalability, and consistency to your infrastructure-as-code practices in a multi-cloud or hybrid cloud environment.

Root and Child Modules in Terraform:

In Terraform, root modules and child modules are concepts used to organize and modularize your infrastructure code. Let's explore each of them:

  1. Root Modules: A root module is the top-level module in your Terraform configuration. It represents the entry point of your infrastructure code and can contain one or more child modules. The root module typically defines the overall infrastructure architecture, global settings, and variables that are shared across multiple child modules.

The root module is usually represented by the main.tf file in your Terraform project and is responsible for initializing the provider configuration, declaring variables, and defining resources that are not specific to any particular module.

  1. Child Modules: Child modules are reusable components that encapsulate a set of resources and configurations for a specific infrastructure component or functionality. They provide a way to break down your infrastructure code into smaller, manageable pieces, promoting reusability and modularity.

Child modules are typically defined in separate directories, each containing its own set of Terraform configuration files (such as main.tf, variables.tf, outputs.tf). These modules can be referenced and used within the root module or other child modules.

Child modules allow you to abstract complex configurations into self-contained units, making it easier to maintain, test, and reuse infrastructure code across different projects or environments.

Using root and child modules in Terraform offers several benefits, including:

  • Modularity: Modules allow you to break down your infrastructure code into smaller, reusable components, making it easier to manage and maintain.

  • Code Organization: Modules provide a logical structure to your Terraform project, promoting organization and readability.

  • Reusability: By creating modular components, you can reuse them across different projects, and environments, or even share them with the community.

  • Encapsulation: Modules encapsulate resources and configurations, making it easier to manage their state and apply changes.

  • Dependency Management: Modules can define input and output variables, enabling dependency management and establishing connections between different components of your infrastructure.

When using root and child modules, it's important to define clear interfaces (input variables and output values) between them to ensure proper communication and avoid tight coupling. Additionally, leveraging module best practices and following naming conventions can enhance the maintainability and scalability of your Terraform projects.

The File Structure should look like this:

Root Modules

Main.tf file content

#initial

module "vpc" {
  source = "./modules/aws_vpc"
  vpc_cidr = var.vpc_cidr
  vpc_tag = var.vpc_tag
}

module "subnet" {
  source = "./modules/aws_subnet"
  vpc_id = module.vpc.vpc_id
  subnet_cidr = var.subnet_cidr
  subnet_name = var.subnet_name
}

module "sg" {
  source = "./modules/aws_sg"
  sg_name = var.sg_name
  sg_description = var.sg_description
  vpc_id = module.vpc.vpc_id
}

module "nic" {
  source = "./modules/aws_nic"
  subnet_id = module.subnet.subnet_id
  private_ips = var.private_ips
  nic_name = var.nic_name
}


module "instance" {
  source = "./modules/aws_instance"
  instance_ami  = var.instance_ami
  instance_type = var.instance_type
  nic_id = module.nic.nic_id
  instance_name = var.instance_name
}

This Terraform code is using modules to provision resources in an AWS environment. Each module represents a specific resource or a set of related resources. Let's go through each module and explain its purpose:

  1. vpc: This module is defined using the aws_vpc module located in the "./modules/aws_vpc" directory. It creates a Virtual Private Cloud (VPC) in AWS. The module expects two input variables: vpc_cidr (the CIDR block for the VPC) and vpc_tag (tags to assign to the VPC).

  2. subnet: This module is defined using the aws_subnet module located in the "./modules/aws_subnet" directory. It creates a subnet within the VPC created by the vpc module. The module expects three input variables: vpc_id (the ID of the VPC), subnet_cidr (the CIDR block for the subnet), and subnet_name (the name to assign to the subnet).

  3. sg: This module is defined using the aws_sg module located in the "./modules/aws_sg" directory. It creates a security group within the VPC. The module expects three input variables: sg_name (the name to assign to the security group), sg_description (a description of the security group), and vpc_id (the ID of the VPC).

  4. nic: This module is defined using the aws_nic module located in the "./modules/aws_nic" directory. It creates a network interface within the subnet created by the subnet module. The module expects four input variables: subnet_id (the ID of the subnet), private_ips (a list of private IP addresses to assign to the network interface), nic_name (the name to assign to the network interface).

  5. instance: This module is defined using the aws_instance module located in the "./modules/aws_instance" directory. It creates an EC2 instance within the VPC, using the specified Amazon Machine Image (AMI) and instance type. The module expects four input variables: instance_ami (the ID of the AMI to use), instance_type (the type of EC2 instance to launch), nic_id (the ID of the network interface created by the nic module), and instance_name (the name to assign to the EC2 instance).

Variable.tf file content

#initial

variable "vpc_cidr" {
  type = string
  default = "172.16.0.0/16"

}

variable "vpc_tag" {
  type = map(any)
  default = {
     "Name" = "my_vpc"
}
}

variable "subnet_cidr" {
  type = string
  default = "172.16.10.0/24"
}

variable "subnet_name" {
  type = map(any)
  default = {
    "Name" = "my_subnet"
  }
}

variable "sg_name" {

  type = map(any)
  default = {
    "Name" = "allow_tls"
  }
}

variable "sg_description" {
  type = string
  default = "allow_tls inbound traffic"
}

variable "nic_name" {
 type = map(any)
 default = {
   "Name" = "my_nic"
 }

}

variable "private_ips" {
  type = list(string)
  default = [ "172.16.10.100" ]
}

variable "instance_ami" {
    type = string
    default = "ami-03a0c45ebc70f98ea"

}

variable "instance_type" {
    type = string
    default = "t2.micro"

}

variable "instance_name" {
  type = map(any)
  default = {
    "Name" = "Prod-Server"
  }
}

The variable.tf file contains variable declarations for the Terraform code. These variables allow you to customize the configurations of your infrastructure when running the Terraform scripts. Let's go through each variable and explain its purpose:

  1. vpc_cidr: This variable is of type string and represents the CIDR block for the VPC. It has a default value of "172.16.0.0/16".

  2. vpc_tag: This variable is of type map(any) and represents the tags to assign to the VPC. It is a map with a single key-value pair, where the key is "Name" and the value is "my_vpc".

  3. subnet_cidr: This variable is of type string and represents the CIDR block for the subnet. It has a default value of "172.16.10.0/24".

  4. subnet_name: This variable is of type map(any) and represents the name to assign to the subnet. It is a map with a single key-value pair, where the key is "Name" and the value is "my_subnet".

  5. sg_name: This variable is of type map(any) and represents the name to assign to the security group. It is a map with a single key-value pair, where the key is "Name" and the value is "allow_tls".

  6. sg_description: This variable is of type string and represents the description of the security group. It has a default value of "allow_tls inbound traffic".

  7. nic_name: This variable is of type map(any) and represents the name to assign to the network interface. It is a map with a single key-value pair, where the key is "Name" and the value is "my_nic".

  8. private_ips: This variable is of type list(string) and represents a list of private IP addresses to assign to the network interface. It has a default value of ["172.16.10.100"].

  9. instance_ami: This variable is of type string and represents the ID of the Amazon Machine Image (AMI) to use for the EC2 instance. It has a default value of "ami-03a0c45ebc70f98ea".

  10. instance_type: This variable is of type string and represents the type of the EC2 instance to launch. It has a default value of "t2.micro".

instance_name: This variable is of type map(any) and represents the name to assign to the EC2 instance. It is a map with a single key-value pair, where the key is "Name" and the value is "Prod-Server".

Provider.tf contents:

#initial

provider "aws" {
    # profile = "myaws" #####using the default profile at the moment#####
    region = "us-east-2"
}
  • provider: This block specifies the provider configuration for AWS.

  • "aws": This is the name of the provider, indicating that we are configuring AWS.

  • region: This parameter specifies the AWS region to operate in. In this case, the region is set to "us-east-2", which corresponds to the US East (Ohio) region.

The provider configuration is essential because it allows Terraform to authenticate and connect to your AWS account using your credentials. By specifying the region, you define the target AWS region where the resources will be provisioned.

Note that there is a commented line # profile = "myaws". This line is currently commented out, but it can be uncommented and set to the appropriate AWS profile name if you are using named profiles in your AWS credentials file. By specifying a profile, you can separate different sets of AWS credentials for different environments or purposes.

By including this provider configuration, you enable Terraform to manage AWS resources within the specified region using the configured AWS credentials.

Child Modules

AWS Modules

  1. AWS_VPC

main.tf file contents

#inital

resource "aws_vpc" "my_vpc" {
  cidr_block = var.vpc_cidr

  tags = var.vpc_tag
}

variable.tf file contents

#inital

variable "vpc_cidr" {
  type = string
  default = "172.16.0.0/16"

}

variable "vpc_tag" {
  type = map(any)
  default = {
     "Name" = "my_vpc"
}
}

output.tf file contents

#inital

output "vpc_id" {
  value = aws_vpc.my_vpc.id
}
  1. AWS_SUBNET

main.tf file contents

#inital

resource "aws_subnet" "my_subnet" {
  vpc_id = var.vpc_id
  cidr_block = var.subnet_cidr

  tags = var.subnet_name
}

variable.tf file contents

#inital

variable "vpc_id" {
  type = string
  #as of now we don't have the vpc id
}

variable "subnet_cidr" {
  type = string
  default = "172.16.10.0/24"
}

variable "subnet_name" {
  type = map(any)
  default = {
    "Name" = "my_subnet"
  }
}

output.tf file contents

#inital

output "subnet_id" {
  value = aws_subnet.my_subnet.id
}
  1. AWS_SG

main.tf file contents

#inital

resource "aws_security_group" "my_sg" {
  name = "allow_tls"
  description = var.sg_description
  vpc_id = var.vpc_id

   ingress {
    description = "TLS from VPC"
    from_port = 443
    to_port = 443
    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 = var.sg_name
}

variable.tf file contents

#inital

variable "vpc_id" {
  type = string
  # as we don't have vpc id
}

variable "sg_name" {

  type = map(any)
  default = {
    "Name" = "allow_tls"
  }
}

variable "sg_description" {
  type = string
  default = "allow_tls inbound traffic"
}
  1. AWS_NIC

main.tf file contents

#inital

resource "aws_network_interface" "name" {
  subnet_id = var.subnet_id
  private_ips = var.private_ips

  tags = var.nic_name
}

variable.tf file contents

#inital

variable "subnet_id" {
  type = string
  # as we don't have a subnet id
}

variable "nic_name" {
 type = map(any)
default = {
  "Name" = "my_nic"
}

}

variable "private_ips" {
  type = list(string)
  default = [ "172.16.10.100" ]
}

output.tf file contents

#inital

output "nic_id" {
  value = aws_network_interface.name.id
}
  1. AWS_EC2_Instance

main.tf file contents

#inital

resource "aws_network_interface" "name" {
  subnet_id = var.subnet_id
  private_ips = var.private_ips

  tags = var.nic_name
}

variable.tf file contents

#inital

variable "subnet_id" {
  type = string
  # as we don't have a subnet id
}

variable "nic_name" {
 type = map(any)
default = {
  "Name" = "my_nic"
}

}

variable "private_ips" {
  type = list(string)
  default = [ "172.16.10.100" ]
}

output.tf file contents

#inital

output "nic_id" {
  value = aws_network_interface.name.id
}

Steps to execute:

  1. Clone the git hub repo from https://github.com/kunalrepo/Terraform_withAWS.git into your local machine.

  2. Go to the root main.tf file and open an integrated terminal from your favourite code editor, here we have used VS code for simplicity.

  3. First, configure the AWS CLI in the integrated terminal by using the command aws configure and provide the access key id and secret key generated for an IAM user/role with sufficient privileges/permissions to deploy services on the AWS console. Keep the region as specified in the provider.tf file "us-east-2".

  4. Execute the command terraform init to initialize the Terraform project in the directory. It prepares the working directory for Terraform and also checks the vulnerability in the code.

  5. Execute the command terraform plan -out=tfplan.out to examine the Terraform configuration and determine what actions need to be taken to achieve the desired state of the infrastructure. Also saving the plan in the tfplan.out file as the best practices.

  6. Execute the command terraform apply tfplan.out to apply the changes specified in the Terraform configuration to create, modify, or delete resources in the infrastructure. It takes the execution plan generated by terraform plan in the output file tfplan.out and performs the necessary actions to achieve the desired state.

AWS Console Output

my_vpc created in AWS console through terraform:

my_subnet created in AWS console through terraform:

security group created in AWS console through terraform:

Network ACL's created in AWS console through terraform:

Prod-server ec2 instance created in AWS console through Terraform:

NOTE: Please use terraform destroy to remove all the infrastructure created to deploy AWS services otherwise it may cost unnecessary billing to your AWS account.

Reference: Please refer to the Hashicorp documentation for any syntax related information.

ย