Setting Up Terraform with Remote Backend and EC2 Instance Deployment: A Beginner's Guide

Setting Up Terraform with Remote Backend and EC2 Instance Deployment: A Beginner's Guide

Introduction

Infrastructure as Code (IaC) is a crucial skill for modern DevOps practitioners, and Terraform is one of the most popular tools in this space. With Terraform, you can automate the provisioning of your cloud infrastructure using simple, declarative configuration files.

In this blog, we’ll:

  1. Understand the basics of Terraform and its building blocks.

  2. Learn about AWS services like S3 and DynamoDB used for state management.

  3. Implement a project to deploy an EC2 instance with Terraform, using a remote backend for state storage and locking.


What is Terraform?

Terraform is an open-source tool by HashiCorp that allows you to define, plan, and provision infrastructure in a consistent and predictable way. It supports multiple cloud providers like AWS, Azure, Google Cloud, and many more.

Key Features:

  • Infrastructure as Code (IaC): Write declarative configurations to define your infrastructure.

  • Idempotent: Terraform ensures the desired state is achieved, no matter how many times you apply it.

  • State Management: Tracks infrastructure changes through a state file, which can be stored remotely for collaboration.


Key Components of Terraform

1. Variables

Variables allow you to parameterize your Terraform configurations, making them reusable and dynamic.

Example:

variable "region" {
  default = "us-east-1"
}

This variable sets the AWS region, which you can reuse across your Terraform files.


2. Data Blocks

Data blocks are used to fetch information from your infrastructure. For example, you can query existing resources or fetch AMI IDs.

Example:

data "aws_ami" "latest" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*"]
  }
}

3. Outputs

Outputs allow you to display useful information after the infrastructure is provisioned, such as public IPs or resource IDs.

Example:

output "instance_public_ip" {
  description = "The public IP of the created EC2 instance"
  value       = aws_instance.web.public_ip
}

4. State management

Terraform maintains the state of your infrastructure in a .tfstate file. This file tracks the current state of resources and ensures consistency during updates.

  • State Locking: Prevents multiple users from modifying the state simultaneously.

  • Remote State: Stores the .tfstate file in a secure location like AWS S3 for better collaboration and versioning.


5. AWS S3 and DynamoDB

  • S3 (Simple Storage Service): We use S3 to store the Terraform state file, ensuring it's secure and accessible for collaboration.

  • DynamoDB: Acts as a locking mechanism to prevent multiple users from making concurrent changes to the state file.


Installing Terraform

Follow these steps to install Terraform.

1. Installing Terraform

To get started, you'll need to install Terraform on your system. Here's how you can do it:

For Windows:

  1. Download the Terraform binary from the official Terraform website.

  2. Extract the downloaded zip file and add its location to your system's PATH environment variable.

  3. Open a terminal and run:

     terraform --version
    

    This verifies the installation.

For Ubuntu:

  1. Update your package list:

     sudo apt-get update
    
  2. Install dependencies:

     sudo apt-get install -y gnupg software-properties-common
    
  3. Add HashiCorp's GPG key:

     curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
    
  4. Add the HashiCorp repository:

     echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
    
  5. Install Terraform:

     sudo apt-get update && sudo apt-get install terraform
    
  6. Verify the installation:

     terraform --version
    


Project Overview

We’ll implement the following:

  1. Set up a remote backend using S3 and DynamoDB for state storage and locking.

  2. Deploy an EC2 instance using Terraform.


Prerequisites

Before you begin, make sure you have the following setup:

  1. AWS Account: You should have an active AWS account with the necessary permissions to create resources like EC2, S3, and DynamoDB.

  2. AWS CLI: Install and configure the AWS CLI with your AWS credentials to authenticate Terraform with your AWS account.

     aws configure
    

    Ensure your credentials are set up properly with access key and secret key.

  3. EC2 Instance to Run Terraform: You need an EC2 instance with the necessary IAM permissions to create resources. Ensure the instance has:

    • IAM Role: Attach an IAM role with permissions to create S3 buckets, DynamoDB tables, and EC2 instances. Use a policy like AmazonEC2FullAccess, AmazonS3FullAccess, and AmazonDynamoDBFullAccess.

    • SSH Access: You will need SSH access to the EC2 instance for running the Terraform commands.


Project Structure

Here’s the directory structure for our project:

TERRAFORM_PRACTICE/
├── Terraform-backend/
│   ├── backend_infra.tf
│   ├── main.tf
│   ├── terraform.tf
│   ├── variable.tf
│   ├── terraform.tfstate
│   ├── terraform.tfstate.backup
├── Terraform-resources/
│   ├── ec2.tf
│   ├── main.tf
│   ├── output.tf
│   ├── variable.tf
│   ├── terraform.tfstate
│   ├── terraform.tfstate.backup
│   ├── terraform-key.pub

Step 1: Setting Up the Remote Backend

A remote backend ensures your Terraform state file is stored securely. This also enables collaboration and locks the state file during updates to avoid conflicts.

backend_infra.tf :

resource "aws_s3_bucket" "terraform_aws_s3_bucket" {
    bucket = var.aws_s3_bucket_name

    tags = {
        Name = var.aws_s3_bucket_name
    }
}

resource "aws_dynamodb_table" "terraform_aws_db" {
    name         = var.aws_dynamodb_table_name
    billing_mode = var.aws_db_billing_mode
    hash_key     = var.aws_db_hashkey

    attribute {
        name = "LockID"
        type = "S"
    }

    tags = {
        Name = var.aws_dynamodb_table_name
    }
}

terraform.tf :

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

variable.tf :

# AWS Region Variable
variable "aws_region" {
  description = "The AWS region where resources are deployed (e.g., us-east-1)."
  type        = string
  default     = "us-east-1"
}

# Remote Backend Variables
variable "aws_s3_bucket_name" {
  description = "The name of the S3 bucket for storing Terraform state files securely."
  type        = string
  default     = "terraform-s3-bucket-devops"
}

variable "aws_dynamodb_table_name" {
  description = "The name of the DynamoDB table used for state locking to ensure consistency and prevent race conditions."
  type        = string
  default     = "terraform-db"
}

variable "aws_db_billing_mode" {
  description = "The billing mode for the DynamoDB table (e.g., PAY_PER_REQUEST or PROVISIONED)."
  type        = string
  default     = "PAY_PER_REQUEST"
}

variable "aws_db_hashkey" {
  description = "The primary key attribute name used in the DynamoDB table for state locking (e.g., LockID)."
  type        = string
  default     = "LockID"
}

Run the following commands to initialize and apply the backend configuration:

Run one command at a time from below :

terraform init
terraform plan
terraform apply --auto-approve

note : - -auto-approve is for automatically giving permission to craete and destroy

  • terraform init :

  • terraform apply - -auto-approve :


Step 2: Deploying an EC2 Instance

With the remote backend in place, we can now deploy an EC2 instance.

ec2.tf :

# Fetch the AMI for the instance based on specified filters
data "aws_ami" "ubuntu" {
    owners      = [var.aws_ami_owners]
    most_recent = true

    filter {
        name   = "name"
        values = [var.aws_instance_os_type]
    }

    filter {
        name   = "state"
        values = ["available"]
    }
}

# Create an SSH key pair
resource "aws_key_pair" "terraform_key" {
    key_name   = var.aws_key_pair_name
    public_key = file(var.aws_key_pair_public_key)
}

# Get the default VPC for the region
resource "aws_default_vpc" "default" {}

# Create a security group
resource "aws_security_group" "terraform_sg" {
    name        = var.aws_sg_name
    description = var.aws_sg_description
    vpc_id      = aws_default_vpc.default.id

    ingress {
        description = "Allow access to SSH port 22"
        from_port   = 22
        to_port     = 22
        protocol    = var.ssh_protocol
        cidr_blocks = [var.ssh_cidr]
    }

    ingress {
        description = "Allow access to HTTP port 80"
        from_port   = 80
        to_port     = 80
        protocol    = var.http_protocol
        cidr_blocks = [var.http_cidr]
    }

    ingress {
        description = "Allow access to HTTPS port 443"
        from_port   = 443
        to_port     = 443
        protocol    = var.https_protocol
        cidr_blocks = [var.https_cidr]
    }

    egress {
        description = "Allow all outgoing traffic"
        from_port   = 0
        to_port     = 0
        protocol    = var.outgoing_protocol
        cidr_blocks = [var.outgoing_cidr]
    }

    tags = {
        Name = var.aws_sg_name
    }
}

# Create an EC2 instance
resource "aws_instance" "terraform_instance" {
    ami           = data.aws_ami.ubuntu.id
    instance_type = var.aws_instance_type
    key_name      = aws_key_pair.terraform_key.key_name
    security_groups = [aws_security_group.terraform_sg.name]

    root_block_device {
        volume_size = var.aws_instance_storage_size
        volume_type = var.aws_instance_volume_type
    }

    tags = {
        Name = var.aws_instance_name
    }
}

output.tf :

output "instance_private_ip" {
  value       = aws_instance.terraform_instance.private_ip
  description = "The private IP address of the main server instance."
}

output "instance_public_ip" {
  value       = aws_instance.terraform_instance.public_ip
  description = "The public IP address of the main server instance."
}

variable.tf :

# aws variable
variable "aws_region" {
  description = "The AWS region to deploy resources in."
  type = string
  default     = "us-east-1"
}

# EC2 instance variables
variable "aws_instance_type" {
  description = "Defines the type of EC2 instance to be created (e.g., t2.micro, t3.small)."
  type        = string
  default     = "t2.micro"
}

variable "aws_instance_name" {
  description = "Specifies the name tag for the EC2 instance, providing easy identification in AWS."
  type        = string
  default     = "terraform-server"
}

variable "aws_instance_os_type" {
  description = "Defines the operating system image filter for selecting an appropriate AMI (e.g., Ubuntu 20.04)."
  type        = string
  default     = "ubuntu/images/hvm-ssd/*amd64*"
}

variable "aws_ami_owners" {
  description = "The owner ID of the AMI to use. Default is Canonical (for Ubuntu AMIs)."
  type        = string
  default     = "099720109477"
}

variable "aws_instance_storage_size" {
  description = "Specifies the size of the root block storage for the EC2 instance, in GB."
  type        = number
  default     = 10
}

variable "aws_instance_volume_type" {
  description = "Defines the volume type for the EC2 instance (e.g., gp2, gp3, io1)."
  type        = string
  default     = "gp3"
}

# Key pair variables
variable "aws_key_pair_name" {
  description = "The name of the SSH key pair to use for accessing the EC2 instance."
  type        = string
  default     = "~/terraform-key"
}

variable "aws_key_pair_public_key" {
  description = "The path to the public key file for the SSH key pair."
  type        = string
  default     = "terraform-key.pub"
}

# Security group variables
variable "aws_sg_name" {
  description = "Defines the name of the AWS Security Group to be created."
  type        = string
  default     = "my_terraform_sg"
}

variable "aws_sg_description" {
  description = "Describes the purpose of the AWS Security Group being created."
  type        = string
  default     = "This security group allows SSH, HTTP, and HTTPS traffic."
}

variable "ssh_protocol" {
  description = "Specifies the protocol to use for SSH traffic (default: TCP)."
  type        = string
  default     = "tcp"
}

variable "http_protocol" {
  description = "Specifies the protocol to use for HTTP traffic (default: TCP)."
  type        = string
  default     = "tcp"
}

variable "https_protocol" {
  description = "Specifies the protocol to use for HTTPS traffic (default: TCP)."
  type        = string
  default     = "tcp"
}

variable "ssh_cidr" {
  description = "Defines the CIDR block to allow SSH access (default: open to all)."
  type        = string
  default     = "0.0.0.0/0"
}

variable "http_cidr" {
  description = "Defines the CIDR block to allow HTTP access (default: open to all)."
  type        = string
  default     = "0.0.0.0/0"
}

variable "https_cidr" {
  description = "Defines the CIDR block to allow HTTPS access (default: open to all)."
  type        = string
  default     = "0.0.0.0/0"
}

variable "outgoing_protocol" {
  description = "Specifies the protocol for all outgoing traffic (default: allow all)."
  type        = string
  default     = "-1"
}

variable "outgoing_cidr" {
  description = "Defines the CIDR block for all outgoing traffic (default: open to all)."
  type        = string
  default     = "0.0.0.0/0"
}

Run the following commands to deploy the EC2 instance:

Run one command at a time from below :

terraform init
terraform plan
terraform apply --auto-approve
  1. terraform init :

  2. terraform plan :

  3. terraform apply - -auto-approve :


Step 3: Full Terraform Code

For Terraform full code use this github repo : https://github.com/Amitabh-DevOps/Terraform-for-devops-my


Output images (Ec2, S3,DynamoDB) :

  1. Ec2 created :

  2. S3 created :

  3. DynamoDB created :


Conclusion

In this blog, you’ve learned:

  1. What Terraform is and its core concepts.

  2. How to set up a remote backend using AWS S3 and DynamoDB.

  3. How to deploy an EC2 instance using Terraform.

This foundational project demonstrates the power of IaC and Terraform. As you progress, you can extend this setup to include more complex infrastructure components like load balancers, databases, and networking configurations.

Feel free to use this project, modify it, and share your thoughts in the comments!