paint-brush
Automate EC2 Deployments on AWS with Terraform Modulesby@omah
118 reads New Story

Automate EC2 Deployments on AWS with Terraform Modules

by IjayDecember 23rd, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

In cloud computing, managing infrastructure efficiently has now become an important part of modern infrastructure operations.
featured image - Automate EC2 Deployments on AWS with Terraform Modules
Ijay HackerNoon profile picture

In cloud computing, managing infrastructure efficiently has now become an important part of modern infrastructure operations. For instance, an IT professional who needs to spin up EC2 instances without knowing infrastructure as code has to manually go to the AWS management console. It might take several minutes to go through the interface, configure instance types, and set up security groups, etc. Now, imagine if there is a need to replicate the setup for multiple environments. There is a tendency to forget the configuration steps, leading to misconfiguration and inconsistencies in the infrastructure; this is where the power of automating with Terraform comes in.

Terraform is an infrastructure-as-code tool created by Hashicorp to write infrastructure configurations in declarative code. It helps for scalable and efficient deployments, and to manage your infrastructure programmatically. With Terraform, you can generate a consistent workflow to provision and manage all your resources in the infrastructure deployment lifecycle. Terraform can manage components such as storage, computing, networking, DNS entries, and the security of your applications.


Terraform has thousands of providers to manage several resources across different cloud platforms. You can find the providers in the Terraform Registry for platforms like Amazon Web Services (AWS), Azure, Google Cloud Platform, Helm, Kubernetes, etc.


A terraform workflow has three core stages;

  • Write: This defines resources across multiple cloud providers and services that you need to provision. For example, on AWS, you can create a configuration to deploy an application on an EC2 instance in a VPC network.
  • Plan: Terraform plan provides an execution plan that describes the infrastructure it will create, update, or destroy based on your configuration
  • Apply: Once the plan is approved, Terraform deploys the infrastructure in the right order respecting all necessary dependencies.


The key components of a Terraform Configuration are as follows;

  • Terraform Block: This is the configuration block where you specify the Terraform version and required providers for your project.
  • Terraform Providers: These are plugins that allow Terraform to interact with Cloud services, and external APIs. You declare these providers in your configuration file.
  • Terraform Resource: These are the building blocks of a Terraform configuration. These resources represent the required resource to be created or destroyed.


Why use Terraform Modules?

Before Terraform modules, cloud engineers typically wrote Terraform with monolithic configurations where all the resources were written in a single or few .tf or .tf.json files. As the need for complex infrastructure deployment grew, managing these setups became a difficult task due to code repetition. The need to create more modular, maintainable, and scalable infrastructure birthed the creation of Terraform modules.


Modules in Terraform allow you to organize all related resources into reusable packages by grouping them into specific .tf files. With Terraform modules, the problem of code repetition is addressed by adhering to the DRY (Don't Repeat Yourself) principle, allowing you to write code once and use it multiple times within your configuration. For example, instead of copying and pasting the same EC2 instance across multiple environments, you can define it as a module, and call it with specific variables in each environment.


A Terraform modules project should have the following;

  • Root Module: This is the main entry point of the Terraform project that contains resources in the primary working directory.
  • Child Modules: These are the self-contained configurations for specific resources and components, they are reusable and are called out by the root module using the module block.
  • Published Modules (optional): Terraform also has published modules that can be defined in your configuration. These modules can be gotten from either a public, or a private registry online. Terraforms makes it possible to publish modules that others can use, and also use modules that other people have published.


Since Terraform modules make programmatic infrastructure management easier, they are perfect for large-scale and complex infrastructure deployment. For instance, a VPC module can be reused if you need to deploy a VPC across several environments (such as development, staging, and production). This helps to save time and ensure that your code is consistent across different environments.


This article will show how to deploy an EC2 instance on a default VPC using Terraform modules. We will use this to demonstrate the power of Terraform modules and how they streamline workflows easily. Whether you are a newbie to Terraform, or an expert looking to streamline your infrastructure operations, this article is worth reading.


Prerequisites Before getting started, ensure you have:


Now that we understand the steps and we have gotten the prerequisites, we can start creating our EC2 instance on AWS using Terraform.

Step-by-Step Terraform Module for EC2 Instance

  1. Define the folder structure:

    Create a directory for the Terraform project and create the and the folders to look like what we have below;

terraform-ec2
  modules/
    ├── ec2/
    │   ├── main.tf
    │   ├── variables.tf
    │   └── outputs.tf
    ├── security_group/
    │   ├── main.tf
    │   ├── variables.tf
    │   └── outputs.tf
main.tf       # Root configuration to call modules
variables.tf
outputs.tf
terraform.tfvar


The directory structure above is a well-organized Terraform modules project. The modules folder contains reusable child modules that represent each infrastructure component.


modules/ec2/:

  • main.tf: Defines the resources required for the creation of the EC2 instance, such as instance type, AMI, and tags.
  • variables.tf: Specifies input variables to add parameters to the EC2 module (e.g., instance type or key pair).
  • outputs.tf: Exposes output values, such as instance IDs or public IPs, that can be accessed by the root module, or other modules.


modules/security_group/

  • main.tf: It defines security group resources, such as ingress and egress rules for inbound and outbound connections.
  • variables.tf: Declares input variables to customize security group rules for your ec2 instance.
  • outputs.tf: Provides output values, such as security group IDs, for use in other modules or the root module.


Root Files

  • main.tf: This is the file in the root directory that orchestrates the infrastructure by calling the child modules for deployment
  • variables.tf: Defines input variables for the root module to add parameters to the configurations, such as region or environment.
  • outputs.tf: Declares output values from the root module, often aggregating outputs from child modules (e.g., public IPs or security group IDs).


NB: file and folder names are only a standard for naming. You may give it any name.


  1. Provider configuration.

This allows Terraform to interact with Cloud Providers and other APIs. At the start of your infrastructure deployment, you must declare the providers your project requires so Terraform will install and use them.


The providers.tf file in your Terraform code is where you define the cloud provider to work with and the version.


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


  • Terraform {} block Specifies the provider required for the configuration. In our case, we are using the AWS provider.

  • Required_providers: Indicates the required plugins that Terraform needs to communicate with the Cloud Platform.

  • AWS: The name of the provider we are using

  • Source: The registry namespace where Terraform will locate the provider plugin. In our case, we will use the hashicorp/aws plugin maintained by Hashicorp. You can find other providers here

  • Version: Specifies the version of the provider plugin to use.


  1. Create the Security Group Module.

    This configuration uses the Default VPC, there will not be any need to create a module for the VPC. We would create a module for the Security Group configuration.


Go to your security group module in ./modules/security_groups

modules/
  └── security_group/
      ├── main.tf
      ├── variables.tf
      └── outputs.tf


In the security modules/security_group/main.tf file, create the security group configurations. The code snippet below defines a configuration code that creates an AWS security group and defines its inbound (ingress) rules and outbound rules (egress).



resource "aws_security_group" "this" {
  name        = var.name
  description = var.description
  vpc_id      = var.vpc_id

  tags = merge(
    {
      Name = var.name
    },
    var.tags
  )
}

resource "aws_security_group_rule" "inbound_rule" {
  for_each = var.ingress_rules

  security_group_id = aws_security_group.this.id
  type              = "ingress"
  from_port         = each.value.from_port
  to_port           = each.value.to_port
  protocol          = each.value.protocol
  cidr_blocks       = each.value.cidr_blocks
}

resource "aws_security_group_rule" "outbound_rule" {
  for_each = var.egress_rules

  security_group_id = aws_security_group.this.id
  type              = "egress"
  from_port         = each.value.from_port
  to_port           = each.value.to_port
  protocol          = each.value.protocol
  cidr_blocks       = each.value.cidr_blocks
}



  • resource "aws_security_group" "this" creates a security group resource in AWS. The identifier "this" is an internal label within the Terraform configuration, and it is used to reference this specific resource elsewhere in the infrastructure code

  • name: This is the name of the Security Group, passed as a variable var.name.

  • description: This is the description for the Security Group, also passed as a variable var.description.

  • vpc_id: It specifies the VPC where the Security Group will be created, defined by var.vpc_id.

  • Ingress rules define the type of traffic that is allowed into the resource. In this case, it allows SSH from port 22, and also allows HTTP (public web traffic) to access the web server.

  • Egress Rules specify the type of traffic allowed from the resource. (protocol = “-1”, CIDR 0.0.0.0/0) ensures that the instance can connect to the internet.


modules/security_group/variables.tf

variable "name" {
  description = "Name of the security group"
  type        = string
}

variable "description" {
  description = "Description of the security group"
  type        = string
  default     = "Managed by Terraform"
}

variable "vpc_id" {
  description = "The VPC ID where the security group will be created"
  type        = string
}

variable "ingress_rules" {
  description = "List of ingress rules"
  type = map(object({
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
  }))
  default = {}
}

variable "egress_rules" {
  description = "List of egress rules"
  type = map(object({
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
  }))
  default = {
    default = {
      from_port   = 0
      to_port     = 0
      protocol    = "-1" # All traffic
      cidr_blocks = ["0.0.0.0/0"]
    }
  }
}

variable "tags" {
  description = "Tags to apply to the security group"
  type        = map(string)
  default     = {}
}


The code snippet defines Terraform variables for the configuration of AWS Security groups. These variables make the security groups reusable across different deployment employments.


  • name specifies the name of the security group with a string type.
  • description provides the description of the security group.
  • vpc_id specifies the ID of the VPC where the security group will be created.


modules/security_group/outputs.tf



output "security_group_id" {
  description = "ID of the security group"
  value       = aws_security_group.this.id
}

output "security_group_arn" {
  description = "ARN of the security group"
  value       = aws_security_group.this.arn
}


This code snippet above defines outputs for the module to expose information about the created security group.

  1. Create the EC2 modules.


The next step in the project is creating the module for the EC2 instance. Inside the ec2 folder under the modules, define the main.tf file.


The code snippet below provisions the EC2 instance and configures it to run the Apache Webserver using a user data script. Here, the instance configuration is dynamic with values provided in a separate variables.tf file. The this in aws_instance "this" is simply a resource name used within Terraform.


modules/ec2/main.tf

resource "aws_instance" "this" {
  ami           = var.ami
  instance_type = var.instance_type
  subnet_id     = var.subnet_id
  key_name      = var.key_name

  user_data = <<-EOF
              #!/bin/bash
              sudo apt update -y
              sudo apt install -y apache2
              sudo systemctl start apache2
             sudo  systemctl enable apache2
              EOF

  tags = merge(
    {
      Name = var.name
    },
    var.tags
  )

  security_groups = var.security_groups
}


The code snippet above creates the ec2 instance and sets up an Apache web server using the user data script. The variables referenced in the main.tf are also defined in the variables.tf file.


modules/ec2/variables.tf

variable "ami" {
  description = "AMI ID for the EC2 instance"
  type        = string
}

variable "instance_type" {
  description = "Instance type for the EC2 instance"
  type        = string
  default     = "t2.micro"
}

variable "subnet_id" {
  description = "Subnet ID where we will deploy the EC2 instance"
  type        = string
}

variable "key_name" {
  description = "Key pair name for accessing the EC2 instance"
  type        = string
}

variable "name" {
  description = "Name tag for the EC2 instance"
  type        = string
}

variable "security_groups" {
  description = "List of security groups to associate with the EC2 instance"
  type        = list(string)
  default     = []
}

variable "tags" {
  description = "Tags to apply to the EC2 instance"
  type        = map(string)
  default     = {}
}


Next, we would also create the outputs.tf file to expose key information about the created security group.


modules/ec2/outputs.tf

output "ec2_instance_id" {
  description = "ID of the EC2 instance"
  value       = aws_instance.this.id
}

output "instance_public_ip" {
  description = "Public IP address of the EC2 instance"
  value       = aws_instance.this.public_ip
}

output "instance_private_ip" {
  description = "Private IP address of the EC2 instance"
  value       = aws_instance.this.private_ip
}


  1. Creating the root module configuration

Now that all the modules are properly set up, we will define the root modules that will handle the creation of our infrastructure.


main.tf


# Fetch default VPC
data "aws_vpc" "default_vpc" {
  default = true
}

data "aws_subnets" "default_subnets" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.default.id]
  }
}

# Fetch the first subnet in the default VPC
data "aws_subnet" "default_subnet" {
  id = tolist(data.aws_subnets.default.ids)[0]
}



# Security Group Module
module "security_group" {
  source      = "./modules/security_group"
  name        = var.sg_name
  description = var.sg_description
  vpc_id      = data.aws_vpc.default_vpc.id

  ingress_rules = var.sg_ingress_rules

  egress_rules = var.sg_egress_rules

  tags = var.sg_tags
}


# EC2 Module
module "ec2_instance" {
  source          = "./modules/ec2"
  ami             = var.ami
  instance_type   = var.instance_type
  subnet_id       = data.aws_subnet.default_subnet.id
  key_name        = var.key_name
  name            = var.ec2_name
  security_groups = [module.security_group.security_group_id]

  tags = var.ec2_tags
}

The code snippet references the default VPC and subnet. It also calls out the modules in the modules folder and assigns values to them.


The root variables file. defines the input variables for the root module.

variables.tf

variable "aws_region" {
  description = "AWS region to deploy resources"
  type        = string
  default     = "us-east-1"
}

# Security Group Variables
variable "sg_name" {
  description = "Name of the security group"
  type        = string
}

variable "sg_description" {
  description = "Description of the security group"
  type        = string
  default     = "Security group managed by Terraform"
}


variable "sg_ingress_rules" {
  description = "Ingress rules for the security group"
  type = map(object({
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
  }))
}

variable "sg_egress_rules" {
  description = "Egress rules for the security group"
  type = map(object({
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
  }))
  default = {
    default = {
      from_port   = 0
      to_port     = 0
      protocol    = "-1"
      cidr_blocks = ["0.0.0.0/0"]
    }
  }
}

variable "sg_tags" {
  description = "Tags for the security group"
  type        = map(string)
  default     = {}
}

# EC2 Instance Variables
variable "ami" {
  description = "AMI ID for the EC2 instance"
  type        = string
}

variable "instance_type" {
  description = "Instance type for the EC2 instance"
  type        = string
  default     = "t2.micro"
}


variable "key_name" {
  description = "Key pair name for accessing the EC2 instance"
  type        = string
}

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

variable "ec2_tags" {
  description = "Tags for the EC2 instance"
  type        = map(string)
  default     = {}
}


outputs.tf

output "security_group_id" {
  description = "ID of the Security Group"
  value       = module.security_group.security_group_id
}

output "ec2_instance_id" {
  description = "ID of the EC2 instance"
  value       = module.ec2_instance.instance_id
}

output "ec2_public_ip" {
  description = "Public IP of the EC2 instance"
  value       = module.ec2_instance.instance_public_ip
}


Finally, we need to create a file that defines the default values for the variables in the variables.tf file

.

terraform.tfvar

aws_region      = "us-east-1"
ami             = "ami-12345678"                # Replace with Ubuntu 22.04 AMI ID
instance_type   = "t2.micro"
key_name        = "my-key-pair"                 # Replace with your Key Pair name
ec2_name        = "ubuntu-web-server"
ec2_tags = {
  Environment = "dev"
  Project     = "TerraformDemo"
}

sg_name        = "web-server-sg"
sg_description = "Security group for web server"
sg_ingress_rules = {
  ssh = {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  http = {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
sg_tags = {
  Environment = "dev"
  Project     = "TerraformDemo"
}


6. Initialize Terraform

Run the following command below to initialize Terraform and download the necessary provider plugins

terraform init


  1. Run Terraform Plan

    Before you apply, It is essential to preview the changes that Terraform will make

    terraform plan
    


  1. Terraform Apply

    Next, we will run the command below to apply the configuration to create the EC2 instance on AWS;

    terraform apply
    


Now, verify by checking the EC2 console to see the running instance and visit the public IP Address of the EC2 instance to view the Apache home screen.

AWS EC2 Instance Running Successfully



Apache Server Running Successfully on AWS EC2 Instance


  1. Lastly, to destroy the instance and associated resources, run:
terraform destroy


Conclusion

In this article, we explained how Terraform modules help our infrastructure code become scalable and reusable, especially in complex infrastructure setups. We also wrapped it up by creating an ec2 instance using Terraform modules on AWS. We now understand the power of terraform modules and their usefulness in deploying reusable and scalable modules.