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. \n
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;
\ The key components of a Terraform Configuration are as follows;
\
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;
\ 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 InstanceDefine the folder structure:
Create a directory for the Terraform project and create the and the folders to look like what we have below;
\ 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/:
\ modules/security_group/
\ Root Files
\ NB: file and folder names are only a standard for naming. You may give it any name.
\
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.
\
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.
\
\ 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.
\ 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 }\
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\
Run Terraform Plan
Before you apply, It is essential to preview the changes that Terraform will make
\
Terraform Apply
Next, we will run the command below to apply the configuration to create the EC2 instance on AWS;
\ 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.
\ \
\
\
ConclusionIn 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.
\n
All Rights Reserved. Copyright , Central Coast Communications, Inc.