Building a Resilient and Scalable AWS Infrastructure with Terraform

Building a Resilient and Scalable AWS Infrastructure with Terraform

Introduction

If you have read my last blog ,you must have understood how lengthy is it to create a vpc manually, In this tutorial I was learning terraform ( An IAC tool,Infrastructure as a code ) . Whatever you want just write in the way of code, Write one command and you are ready to go.

In today's cloud-centric landscape, deploying a robust and scalable infrastructure is crucial for the success of any application. This blog post will guide you through the process of building a resilient AWS infrastructure using Terraform, focusing on security, scalability, and maintainability.

Small Talk about terraform

For Applying or creating any infrastucture using terraform we write different files with a .tf extension. We write our desired idea and apply commands like terraform init , terraform plan ,terraform apply or terraform destroy sometimes.

I. Introduction to Terraform

A. Definition

  1. Infrastructure as Code (IaC):

    • Manage and provision infrastructure through code.
  2. Terraform:

    • Open-source IaC tool by HashiCorp.

    • Declarative configuration language.

B. Key Concepts

  1. Providers:

    • Plugins for interacting with infrastructure providers.
  2. Resources:

    • Declarative definitions of infrastructure components.
  3. State:

    • Maintains the current state of deployed infrastructure.

II. How Terraform Works

A. Configuration Files

  1. File Types:

    • .tf files containing Terraform configurations.

B. Executing Commands

  1. terraform init:

    • Initializes working directory.
  2. terraform plan:

    • Previews changes.
  3. terraform apply:

    • Applies changes.
  4. terraform destroy:

    • Destroys resources.

C. State Management

  1. Terraform State:

    • JSON file tracking infrastructure mapping.

D. Modules

  1. Reusable Modules:

    • Encapsulate and organize configurations.

E. Remote Backends

  1. Remote State Storage:

    • Stores state files remotely.

F. Variables and Outputs

  1. variables.tf:

    • Defines input variables.
  2. outputs.tf:

    • Defines output values.

Prerequisites

Before diving into the tutorial, ensure you have the following prerequisites:

  • An AWS account with the necessary permissions.

  • Terraform installed on your local machine.

  • Basic knowledge of networking concepts.

Infrastructure Overview

Our infrastructure will consist of several key components:

  • Virtual Private Cloud (VPC): Isolated network environment in the AWS cloud.

  • Security Groups: Define inbound and outbound traffic rules for EC2 instances.

  • NAT Gateway: Facilitates outbound internet traffic from private subnets.

  • Application Servers: Instances running your application.

  • Load Balancer: Distributes incoming traffic across multiple application servers.

  • Internet Gateways: Allow communication between your VPC and the internet.

  • Route Tables: Define how traffic should flow within the VPC.

Project Structure

To keep our Terraform project organized, we'll structure it as follows:

  1. main.tf: Define the AWS provider and basic resources.

  2. variables.tf: Declare variables to parameterize our configuration.

  3. network.tf: Create VPC, subnets, route tables, and internet gateways.

  4. security_groups.tf: Define security groups for the border and app servers.

  5. nat_gateway.tf: Provision NAT Gateway for outbound traffic from private subnets.

  6. app_servers.tf: Deploy our application servers.

  7. load_balancer.tf: Configure an Elastic Load Balancer (ELB) for distributing traffic.

  8. outputs.tf: Define outputs to display relevant information after provisioning.Setting Up the VPC

Let's start by defining our VPC in aws-vpc.tf

# aws-vpc.tf
/* Setup our aws provider */
provider "aws" {

  access_key = var.access_key
  secret_key = var.secret_key
  region     = var.region
}

/* Define our vpc */
resource "aws_vpc" "default" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  tags = {
    Name = "automated"
  }
}

This sets up the foundational networking components for our infrastructure.

Security Groups

In security_groups.tf, define security groups for the border and app servers.

/* Default security group */
resource "aws_security_group" "default" {
  name        = "default-automated-vpc"
  description = "Default security group that allows inbound and outbound traffic from all instances in the VPC"
  vpc_id      = aws_vpc.default.id

  ingress {
    from_port = "0"
    to_port   = "0"
    protocol  = "-1"
    self      = true
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "default"
  }
}

/* Security group for the nat server */
resource "aws_security_group" "nat" {
  name        = "nat-automated-vpc"
  description = "Security group for nat instances that allows SSH and VPN traffic from internet"
  vpc_id      = aws_vpc.default.id

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

  ingress {
    from_port   = 1194
    to_port     = 1194
    protocol    = "udp"
    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 = "nat"
  }
}

/* Security group for the web */
resource "aws_security_group" "web" {
  name        = "web-automated-vpc"
  description = "Security group for web that allows web traffic from internet"
  vpc_id      = aws_vpc.default.id

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

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

  tags = {
    Name = "web"
  }
}

Security groups ensure that traffic flows securely within your infrastructure.

NAT Gateway

To enable outbound internet traffic from private subnets, let's set up a NAT Gateway in nat_gateway.tf.

# nat_gateway.tf

/* NAT/VPN server */
resource "aws_instance" "nat" {
  ami               = var.amis[var.region]
  instance_type     = "t2.micro"
  subnet_id         = aws_subnet.public.id
  security_groups   = [aws_security_group.default.id, aws_security_group.nat.id]
  key_name          = aws_key_pair.deployer.key_name
  associate_public_ip_address = true
  source_dest_check = false
  tags = {
    Name = "nat"
  }
  connection {
    host        = coalesce(self.public_ip, self.private_ip)
    type        = "ssh"
    user        = "ubuntu"
    private_key = file("~/.ssh/id_rsa")
  }

  /* To allow LAN nodes with private IP addresses to communicate with external public networks,
     configure the firewall for IP masquerading, which masks requests from LAN nodes with the
     IP address of the firewall's external device (in this case, eth0):
  */

  provisioner "remote-exec" {
    inline = [
      "sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE",
      "echo 1 | sudo tee /proc/sys/net/ipv4/conf/all/forwarding",
      "sudo apt-get update",
      "sudo apt-get -y install apt-transport-https ca-certificates curl gnupg-agent software-properties-common",
      "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -",
      "sudo add-apt-repository 'deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable'",
      "sudo apt-get update",
      "sudo apt-get -y install docker-ce docker-ce-cli containerd.io",
      "sudo usermod -aG docker ubuntu",
      "sudo apt-get -y install vim",
      "sudo mkdir -p /etc/openvpn",
      "sudo docker run --name ovpn-data -v /etc/openvpn busybox",
      "sudo docker run --volumes-from ovpn-data --rm kylemanna/openvpn ovpn_genconfig -p ${var.vpc_cidr} -u udp://${aws_instance.nat.public_ip}",
    ]
  }
}

This ensures that your application servers in private subnets can communicate with the internet.

App Servers

Now, deploy your application servers in app_servers.tf.

# app_servers.tf
/* App servers */
resource "aws_instance" "app" {
  count             = 2
  ami               = var.amis[var.region]
  instance_type     = "t2.micro"
  subnet_id         = aws_subnet.private.id
  security_groups   = [aws_security_group.default.id]
  key_name          = aws_key_pair.deployer.key_name
  source_dest_check = true
  user_data         = file("cloud-config/app.yml")
  tags = {
    Name = "automated-app-${count.index}"
  }
}

/* Load balancer */
resource "aws_elb" "app" {
  name            = "automated-vpc-elb"
  subnets         = [aws_subnet.public.id]
  security_groups = [aws_security_group.default.id, aws_security_group.web.id]
  listener {
    instance_port     = 80
    instance_protocol = "http"
    lb_port           = 80
    lb_protocol       = "http"
  }

  health_check {
    healthy_threshold   = 2
    unhealthy_threshold = 2
    timeout             = 5
    target              = "HTTP:80/"
    interval            = 15
  }

  instances = aws_instance.app.*.id
}

Specify the details of your application servers, including AMI, instance type, key pair, and security group.

Outputs

In outputs.tf, define what information you want to display after provisioning.

# outputs.tf

output "natIP" {
  value = aws_instance.nat.public_ip
}

output "appservers" {
  value = join(",", aws_instance.app.*.private_ip)
}

output "elbHostname" {
  value = aws_elb.app.dns_name
}

These outputs help you quickly access relevant information about your infrastructure.

Conclusion

Congratulations! We've successfully built a resilient and scalable AWS infrastructure using Terraform. This Infrastructure as Code approach ensures consistency, repeatability, and easy maintenance of your environment.
There will be more blogs regarding DevOps practices like , share and subcribe to our newsletter.