Terraform Project Documentation

Introduction

This document provides a step-by-step guide to setting up a Terraform project to deploy a sample "Hello World" application on an EC2 instance. The project uses S3 for storing Terraform state files and DynamoDB for state locking.

Prerequisites

Before you begin, ensure you have the following:

  • AWS CLI installed and configured with the appropriate permissions.

  • Terraform installed on your local machine.

  • An AWS account with access to create S3 buckets, DynamoDB tables, and EC2 instances.

Project Structure

Here is the overview of the project structure

terraform-project/
├── modules/
│   ├── ec2_instance/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── s3_bucket/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   └── dynamodb_table/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
├── main.tf
├── providers.tf
├── backend.tf
├── variables.tf
├── outputs.tf
└── workspaces/
    ├── dev.tfvars
    └── prod.tfvars

Configuration Files

Providers

The providers.tf file specifies the providers required for the project. In this case, we use the AWS provider for deploying infrastructure.

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

# AWS Provider
provider "aws" {
  region = var.aws_region
}

Variables

The variables.tf file defines the variables used in the project.

#Main Files Variables

variable "aws_region" {
  description = "aws region"
  type = string
}

# EC2 Instance Module Variables

variable "ec2_ami_id" {
  description = "AMI Id for EC2 instance"
  type = string
}

variable "ec2_instance_type" {
  description = "Instance type for EC2 instance"
  type = string
}

variable "ec2_instance_name" {
  description = "Name of the EC2 instance"
  type = string
}

variable "vpc_id" {
  description = "VPC Id"
  type = string
}

variable "ec2_security_group_name" {
  description = "Name of the Security group for EC2"
  type = string
}

variable "ec2_private_key" {
  description = "Path for the EC2 pem file"
  type = string
}

# S3 Module Variables

variable "s3_bucket_name" {
  description = "Name of the s3 bucket"
  type = string
}


# Dynamodb Module Variables

variable "aws_dynamodb_table_name" {
    description = "Name of the dynamodb table"
    type = string
}

Resources

The main.tf file defines the resources, including modules for EC2, S3, and DynamoDB.

# Define Locals for using dynamic variables with the variable terraform.workspace ( dev/prod )
locals {
  ec2_instance_name         = "${terraform.workspace}-${var.ec2_instance_name}"
  # s3_bucket_name            = "${terraform.workspace}-${var.s3_bucket_name}"
  # aws_dynamodb_table_name   = "${terraform.workspace}-${var.aws_dynamodb_table_name}"
  ec2_security_group_name   = "${terraform.workspace}-${var.ec2_security_group_name}"
}

# Defining module for EC2 module
module "ec2_instance_module" {
  source = "./modules/ec2-instance"
  ec2_ami_id = var.ec2_ami_id
  ec2_instance_name = local.ec2_instance_name
  ec2_instance_type = var.ec2_instance_type
  ec2_private_key = var.ec2_private_key
  vpc_id = var.vpc_id
  ec2_security_group_name = local.ec2_security_group_name
}

# Defining module for S3 Bucket
module "s3_bucket_module" {
  source = "./modules/s3-bucket"
  s3_bucket_name = var.s3_bucket_name
}

# Deniging module for DynamoDB
module "dynamodb_table_terraform_lock_module" {
  source = "./modules/dynamo-db"
  aws_dynamodb_table_name = var.aws_dynamodb_table_name
}

Modules

S3 Bucket Module

main.tf in modules/s3_bucket:

resource "aws_s3_bucket" "aws_s3_bucket" {
  bucket = var.s3_bucket_name
}

variables.tf in modules/s3_bucket:

variable "s3_bucket_name" {
  description = "Name of the s3 bucket"
  type = string
}

outputs.tf in modules/s3_bucket:

output "aws-s3-s3_bucket_name" {
  value = aws_s3_bucket.aws_s3_bucket.bucket
}

DynamoDB Table Module

main.tf in modules/dynamodb_table:

resource "aws_dynamodb_table" "aws_terraform_lock_dynamodb_table" {
  name = var.aws_dynamodb_table_name
  hash_key = "LockID"
  billing_mode = "PAY_PER_REQUEST"

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

  tags = {
    "Name" = "Dev-Table"
  }
}

variables.tf in modules/dynamodb_table:

variable "aws_dynamodb_table_name" {
    description = "Name of the dynamodb table"
    type = string
}

outputs.tf in modules/dynamodb_table:

output "aws_dynamodb_table_name" {
    description = "Name of the dynamodb table"
    value = aws_dynamodb_table.aws_terraform_lock_dynamodb_table.name
}

EC2 Instance Module

main.tf in modules/ec2_instance:

resource "aws_security_group" "web-app-security-group" {
  name = var.ec2_security_group_name
  vpc_id = var.vpc_id
}

resource "aws_vpc_security_group_ingress_rule" "allow_tls_ipv4_22" {
  security_group_id = aws_security_group.web-app-security-group.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 22
  ip_protocol       = "tcp"
  to_port           = 22
}

resource "aws_vpc_security_group_ingress_rule" "allow_tls_ipv4_80" {
  security_group_id = aws_security_group.web-app-security-group.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 80
  ip_protocol       = "tcp"
  to_port           = 80
}

resource "aws_vpc_security_group_ingress_rule" "allow_tls_ipv4_all" {
  security_group_id = aws_security_group.web-app-security-group.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1"
}

resource "aws_vpc_security_group_egress_rule" "allow_all_traffic_ipv4" {
  security_group_id = aws_security_group.web-app-security-group.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1" # semantically equivalent to all ports
}


resource "aws_instance" "aws_ec2_instance" {
  ami = var.ec2_ami_id
  instance_type = var.ec2_instance_type
  key_name = "first-instance-key-pair"
  vpc_security_group_ids = [ aws_security_group.web-app-security-group.id ]

  tags = {
    Name = var.ec2_instance_name
  }

  provisioner "remote-exec" {
    inline = [ 
        "sudo yum update -y",
        "sudo yum install -y httpd",
        "sudo systemctl start httpd",
        "sudo systemctl enable httpd",
        "echo '<h1>Hello, World!</h1>' | sudo tee /var/www/html/index.html"
     ]

    connection {
        user = "ec2-user"
        host = self.public_ip
        private_key = file(var.ec2_private_key)
    }
  }
}

remote-exec Provisioner:

The remote-exec provisioner is used to run scripts or commands on a remote machine over SSH or WinRM connections. It's often used to configure or install software on provisioned instances.

In the above example, the remote-exec provisioner connects to the AWS EC2 instance using SSH and runs a series of commands to update the package repositories, install Apache HTTP Server, and start the HTTP server.

variables.tf in modules/ec2_instance:

variable "ec2_ami_id" {
  description = "AMI Id for EC2 instance"
  type = string
}

variable "ec2_instance_type" {
  description = "Instance type for EC2 instance"
  type = string
}

variable "ec2_instance_name" {
  description = "Name of the EC2 instance"
  type = string
}

variable "ec2_private_key" {
  description = "EC2 Private Key Path"
  type = string
}

variable "ec2_security_group_name" {
  description = "Name of the Security group for EC2"
  type = string
}

variable "vpc_id" {
  description = "VPC Id"
  type = string
}

outputs.tf in modules/ec2_instance:

output "aws_ec2_instance_public_ip" {
  description = "AWS EC2 instance public ip"
  value = aws_instance.aws_ec2_instance.public_ip
}

Backend Configuration

The backend.tf file configures the backend for storing the Terraform state in an S3 bucket and using DynamoDB for state locking.

terraform {
  backend "s3" {
    bucket         = "web-terraform-backend-bucket"
    key            = "terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "web-terraform-lock-table"
  }
}

Workspaces

Terraform workspaces allow you to manage multiple environments. The dev.tfvars and prod.tfvars files contain variable values specific to the dev and prod environments.

dev.tfvars in workspaces/:

# Main Variables

aws_region = "us-east-1"

# EC2 Module Instance Variables

ec2_ami_id = "ami-00beae93a2d981137"
ec2_instance_type = "t2.micro"
ec2_instance_name = "Web-App"
ec2_private_key = "./private-keys/first-instance-key-pair.rsa"
vpc_id = "vpc-09c2d21a970b5104a"
ec2_security_group_name = "Web-App-Security-Group"

# S3 Module Variables

s3_bucket_name = "web-terraform-backend-bucket"

# Dynamodb Module Variables

aws_dynamodb_table_name = "web-terraform-lock-table"

Note: Create a prod.tfvars under workspaces folder and define values which should pass when deploying infrastructure.

Deploying the Project

Step 1: Initialize Terraform

Open your terminal and navigate to your project directory. Run the following command to initialize Terraform, which will download the necessary provider plugins and set up the backend configuration.

terraform init

Step 2: Select or Create a Workspace

Terraform workspaces allow you to manage different environments (e.g., development, production). Create and switch to the desired workspace using the following commands:

terraform workspace new dev
terraform workspace select dev

Step 3: Plan the Configuration

Run the following command to plan the Terraform configuration. This command will show the resources defined in your configuration files how many resources is going to add or how many resources going to update or resources count to destroy.

terraform plan -var-file="./workspaces/dev.tfvars"

Step 3: Apply the Configuration

Run the following command to apply the Terraform configuration. This command will create the resources defined in your configuration files.

terraform apply -var-file="./workspaces/dev.tfvars"

When prompted to confirm the action, type yes and press Enter.

Deployed Infrastructure in AWS

Accessing the Application

Step 1: Get the Public IP of the EC2 Instance

After the deployment is complete, you can get the public IP address of the EC2 instance.

Step 2: Open the Web Application

Open your web browser and navigate to http://<public_ip>, replacing <public_ip> with the actual public IP address obtained in the previous step. You should see the "Hello, World!" message.

Conclusion

This document provided a comprehensive guide to setting up a Terraform project to deploy a "Hello World" application on an EC2 instance, using S3 for state storage and DynamoDB for state locking. Adjust the configurations as needed for your specific use case. By following these steps, you should be able to replicate and understand the setup and deployment process for your Terraform project.