Build AWS EC2 Instances, Security Groups using Terraform¶
Step-01: Introduction¶
Terraform Modules we will use¶
- terraform-aws-modules/vpc/aws
- terraform-aws-modules/security-group/aws
- terraform-aws-modules/ec2-instance/aws
Terraform New Concepts we will introduce¶
- aws_eip
- null_resource
- file provisioner
- remote-exec provisioner
- local-exec provisioner
- depends_on Meta-Argument
What are we going implement?¶
- Create VPC with 3-Tier Architecture (Web, App and DB) - Leverage code from previous section
- Create AWS Security Group Terraform Module and define HTTP port 80, 22 inbound rule for entire internet access
0.0.0.0/0 - Create Multiple EC2 Instances in VPC Private Subnets and install
- Create EC2 Instance in VPC Public Subnet
Bastion Host - Create Elastic IP for
Bastion HostEC2 Instance - Create
null_resourcewith following 3 Terraform Provisioners - File Provisioner
- Remote-exec Provisioner
- Local-exec Provisioner
Pre-requisite¶
- Copy your AWS EC2 Key pair
terraform-key.peminprivate-keyfolder - Folder name
local-exec-output-fileswherelocal-execprovisioner creates a file (creation-time provisioner)
Step-02: Copy all the VPC TF Config files from 06-02¶
- Copy the following TF Config files from 06-02 section which will create a 3-Tier VPC
- c1-versions.tf
- c2-generic-variables.tf
- c3-local-values.tf
- c4-01-vpc-variables.tf
- c4-02-vpc-module.tf
- c4-03-vpc-outputs.tf
- terraform.tfvars
- vpc.auto.tfvars
- private-key/terraform-key.pem
Step-03: Add app1-install.sh¶
- Add
app1-install.shin working directory#! /bin/bash # Instance Identity Metadata Reference - https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html sudo yum update -y sudo yum install -y httpd sudo systemctl enable httpd sudo service httpd start sudo echo '<h1>Welcome to StackSimplify - APP-1</h1>' | sudo tee /var/www/html/index.html sudo mkdir /var/www/html/app1 sudo echo '<!DOCTYPE html> <html> <body style="background-color:rgb(250, 210, 210);"> <h1>Welcome to Stack Simplify - APP-1</h1> <p>Terraform Demo</p> <p>Application Version: V1</p> </body></html>' | sudo tee /var/www/html/app1/index.html sudo curl http://169.254.169.254/latest/dynamic/instance-identity/document -o /var/www/html/app1/metadata.html
Step-04: Create Security Groups for Bastion Host and Private Subnet Hosts¶
Step-04-01: c5-01-securitygroup-variables.tf¶
- Place holder file for defining any Input Variables for EC2 Security Groups
Step-04-02: c5-03-securitygroup-bastionsg.tf¶
- SG Module Examples for Reference
# AWS EC2 Security Group Terraform Module # Security Group for Public Bastion Host module "public_bastion_sg" { source = "terraform-aws-modules/security-group/aws" version = "3.18.0" name = "public-bastion-sg" description = "Security group with SSH port open for everybody (IPv4 CIDR), egress ports are all world open" vpc_id = module.vpc.vpc_id # Ingress Rules & CIDR Block ingress_rules = ["ssh-tcp"] ingress_cidr_blocks = ["0.0.0.0/0"] # Egress Rule - all-all open egress_rules = ["all-all"] tags = local.common_tags }
Step-04-03: c5-04-securitygroup-privatesg.tf¶
# AWS EC2 Security Group Terraform Module
# Security Group for Private EC2 Instances
module "private_sg" {
source = "terraform-aws-modules/security-group/aws"
version = "3.18.0"
name = "private-sg"
description = "Security group with HTTP & SSH ports open for everybody (IPv4 CIDR), egress ports are all world open"
vpc_id = module.vpc.vpc_id
ingress_rules = ["ssh-tcp", "http-80-tcp"]
ingress_cidr_blocks = ["0.0.0.0/0"]
egress_rules = ["all-all"]
tags = local.common_tags
}
Step-04-04: c5-02-securitygroup-outputs.tf¶
- SG Module Examples for Reference
# Public Bastion Host Security Group Outputs output "public_bastion_sg_group_id" { description = "The ID of the security group" value = module.public_bastion_sg.this_security_group_id } output "public_bastion_sg_group_vpc_id" { description = "The VPC ID" value = module.public_bastion_sg.this_security_group_vpc_id } output "public_bastion_sg_group_name" { description = "The name of the security group" value = module.public_bastion_sg.this_security_group_name } # Private EC2 Instances Security Group Outputs output "private_sg_group_id" { description = "The ID of the security group" value = module.private_sg.this_security_group_id } output "private_sg_group_vpc_id" { description = "The VPC ID" value = module.private_sg.this_security_group_vpc_id } output "private_sg_group_name" { description = "The name of the security group" value = module.private_sg.this_security_group_name }
Step-05: c6-01-datasource-ami.tf¶
# Get latest AMI ID for Amazon Linux2 OS
data "aws_ami" "amzlinux2" {
most_recent = true
owners = [ "amazon" ]
filter {
name = "name"
values = [ "amzn2-ami-hvm-*-gp2" ]
}
filter {
name = "root-device-type"
values = [ "ebs" ]
}
filter {
name = "virtualization-type"
values = [ "hvm" ]
}
filter {
name = "architecture"
values = [ "x86_64" ]
}
}
Step-06: EC2 Instances¶
Step-06-01: c7-01-ec2instance-variables.tf¶
# AWS EC2 Instance Type
variable "instance_type" {
description = "EC2 Instance Type"
type = string
default = "t3.micro"
}
# AWS EC2 Instance Key Pair
variable "instance_keypair" {
description = "AWS EC2 Key pair that need to be associated with EC2 Instance"
type = string
default = "terraform-key"
}
Step-06-02: c7-03-ec2instance-bastion.tf¶
- Example EC2 Instance Module for Reference
# AWS EC2 Instance Terraform Module # Bastion Host - EC2 Instance that will be created in VPC Public Subnet module "ec2_public" { source = "terraform-aws-modules/ec2-instance/aws" version = "2.17.0" # insert the 10 required variables here name = "${var.environment}-BastionHost" ami = data.aws_ami.amzlinux2.id instance_type = var.instance_type key_name = var.instance_keypair subnet_id = module.vpc.public_subnets[0] vpc_security_group_ids = [module.public_bastion_sg.this_security_group_id] tags = local.common_tags }
Step-06-03: c7-04-ec2instance-private.tf¶
- Example EC2 Instance Module for Reference
# EC2 Instances that will be created in VPC Private Subnets module "ec2_private" { source = "terraform-aws-modules/ec2-instance/aws" version = "2.17.0" name = "${var.environment}-vm" ami = data.aws_ami.amzlinux2.id instance_type = var.instance_type user_data = file("${path.module}/apache-install.sh") key_name = var.instance_keypair #subnet_id = module.vpc.private_subnets[0] # Single Instance vpc_security_group_ids = [module.private_sg.this_security_group_id] instance_count = 3 subnet_ids = [ module.vpc.private_subnets[0], module.vpc.private_subnets[1], ] tags = local.common_tags }
Step-06-04: c7-02-ec2instance-outputs.tf¶
# AWS EC2 Instance Terraform Outputs
# Public EC2 Instances - Bastion Host
output "ec2_bastion_public_instance_ids" {
description = "List of IDs of instances"
value = module.ec2_public.id
}
output "ec2_bastion_public_ip" {
description = "List of Public ip address assigned to the instances"
value = module.ec2_public.public_ip
}
# Private EC2 Instances
output "ec2_private_instance_ids" {
description = "List of IDs of instances"
value = module.ec2_private.id
}
output "ec2_private_ip" {
description = "List of private ip address assigned to the instances"
value = module.ec2_private.private_ip
}
Step-07: EC2 Elastic IP for Bastion Host - c8-elasticip.tf¶
- learn about Terraform Resource Meta-Argument
depends_on
Step-08: c9-nullresource-provisioners.tf¶
Step-08-01: Define null resource in c1-versions.tf¶
- Learn about Terraform Null Resource
- Define null resource in c1-versions.tf in
terraform block
Step-08-02: Understand about Null Resource and Provisioners¶
- Learn about Terraform Null Resource
- Learn about Terraform File Provisioner
- Learn about Terraform Remote-Exec Provisioner
- Learn about Terraform Local-Exec Provisioner
# Create a Null Resource and Provisioners resource "null_resource" "name" { depends_on = [module.ec2_public ] # Connection Block for Provisioners to connect to EC2 Instance connection { type = "ssh" host = aws_eip.bastion_eip.public_ip user = "ec2-user" password = "" private_key = file("private-key/terraform-key.pem") } # Copies the terraform-key.pem file to /tmp/terraform-key.pem provisioner "file" { source = "private-key/terraform-key.pem" destination = "/tmp/terraform-key.pem" } # Using remote-exec provisioner fix the private key permissions on Bastion Host provisioner "remote-exec" { inline = [ "sudo chmod 400 /tmp/terraform-key.pem" ] } # local-exec provisioner (Creation-Time Provisioner - Triggered during Create Resource) provisioner "local-exec" { command = "echo VPC created on `date` and VPC ID: ${module.vpc.vpc_id} >> creation-time-vpc-id.txt" working_dir = "local-exec-output-files/" #on_failure = continue } ## Local Exec Provisioner: local-exec provisioner (Destroy-Time Provisioner - Triggered during deletion of Resource) provisioner "local-exec" { command = "echo Destroy time prov `date` >> destroy-time-prov.txt" working_dir = "local-exec-output-files/" when = destroy #on_failure = continue } }
Step-09: ec2instance.auto.tfvars¶
Step-10: Usage of depends_on Meta-Argument¶
Step-10-01: c7-04-ec2instance-private.tf¶
- We have put
depends_onso that EC2 Private Instances will not get created until all the resources of VPC module are created - why?
- VPC NAT Gateway should be created before EC2 Instances in private subnets because these private instances has a
userdatawhich will try to go outbound to download theHTTPDpackage using YUM to install the webserver - If Private EC2 Instances gets created first before VPC NAT Gateway provisioning of webserver in these EC2 Instances will fail.
Step-10-02: c8-elasticip.tf¶
- We have put
depends_onin Elastic IP resource. - This elastic ip resource will explicitly wait for till the bastion EC2 instance
module.ec2_publicis created. - This elastic ip resource will wait till all the VPC resources are created primarily the Internet Gateway IGW.
Step-10-03: c9-nullresource-provisioners.tf¶
- We have put
depends_onin Null Resource - This Null resource contains a file provisioner which will copy the
private-key/terraform-key.pemto Bastion Hostec2_public module created ec2 instance. - So we added explicit dependency in terraform to have this
null_resourcewait till respective EC2 instance is ready so file provisioner can copy theprivate-key/terraform-key.pemfile
Step-11: Execute Terraform Commands¶
# Terraform Initialize
terraform init
# Terraform Validate
terraform validate
# Terraform Plan
terraform plan
Observation:
1) Review Security Group resources
2) Review EC2 Instance resources
3) Review all other resources (vpc, elasticip)
# Terraform Apply
terraform apply -auto-approve
Observation:
1) VERY IMPORTANT: Primarily observe that first VPC NAT Gateway will be created and after that only module.ec2_private related EC2 Instance will be created
Step-12: Connect to Bastion EC2 Instance and Test¶
# Connect to Bastion EC2 Instance from local desktop
ssh -i private-key/terraform-key.pem ec2-user@<PUBLIC_IP_FOR_BASTION_HOST>
# Curl Test for Bastion EC2 Instance to Private EC2 Instances
curl http://<Private-Instance-1-Private-IP>
curl http://<Private-Instance-2-Private-IP>
# Connect to Private EC2 Instances from Bastion EC2 Instance
ssh -i /tmp/terraform-key.pem ec2-user@<Private-Instance-1-Private-IP>
cd /var/www/html
ls -lrta
Observation:
1) Should find index.html
2) Should find app1 folder
3) Should find app1/index.html file
4) Should find app1/metadata.html file
5) If required verify same for second instance too.
6) # Additionalyy To verify userdata passed to Instance
curl http://169.254.169.254/latest/user-data
# Additional Troubleshooting if any issues
# Connect to Private EC2 Instances from Bastion EC2 Instance
ssh -i /tmp/terraform-key.pem ec2-user@<Private-Instance-1-Private-IP>
cd /var/log
more cloud-init-output.log
Observation:
1) Verify the file cloud-init-output.log to see if any issues
2) This file (cloud-init-output.log) will show you if your httpd package got installed and all your userdata commands executed successfully or not
Step-13: Clean-Up¶
# Terraform Destroy
terraform destroy -auto-approve
# Clean-Up
rm -rf .terraform*
rm -rf terraform.tfstate*
🎉 New Course
Ultimate DevOps Real-World Project Implementation on AWS
$15.99
$84.99
81% OFF
MARCH2026
Enroll Now on Udemy →
🎉 Offer