Building Terraform based Multi-Tier Architecture from CLI
NARAYANAN PALANI ??????
Platform Engineering Lead | AWS & Google Cloud Certified Architect | Cloud Solutions Expert | Driving Innovation in Retail, Commercial & Investment Banking | CI/CD | DevOps | Cloud Transformation
Gone are days when we deploy and reboot services-all that manual and time consuming. Infrastructure as Code (IaC) has taken a leap with Terraform and written this article to share those insights of building AWS based multi-tier arch in short span of time via command line terminal!
Terraform is an Infrastructure as Code (IaC) tool that allows you to define, manage, and provision AWS infrastructure using simple, human-readable configuration files. It improves AWS deployments by automating and streamlining the process, ensuring consistency and reducing manual effort.
With Terraform, you can declare the desired state of your infrastructure, such as EC2 instances, VPCs, or S3 buckets, in a configuration file. Terraform then handles creating, updating, or deleting resources to match that state. This eliminates repetitive manual tasks, minimizes errors, and provides a clear version history of infrastructure changes. It also allows you to replicate environments easily, scale resources efficiently, and integrate deployment processes into CI/CD pipelines.
Let us see how this deployment works in few minutes in this article.
Navigating to AWS Console to create a user for this deployment using IaC:
Once created, newly submitted user is available in the IAM portal now:
Now click on terraform-user and select Security Credential Section to create an access key:
Once access key created , store it somewhere safe to use it back in the CLI.
Now it is time to navigate to CLI to run good couple of commands:
Install Git and clone repository:
sudo yum update -y
sudo yum install git -y
git --version
git clone https://github.com/pluralsight-cloud/multi-tier-terraform.git
cd multi-tier-terraform
4. Run the Install Script and use AWS configure
chmod +x install.sh
./install.sh
after the script is completed, run these commands to install git lfs
sudo yum install epel-release -y
sudo yum install git-lfs -y
git lfs install
git lfs pull
aws configure
6. Initialize, Plan and Apply the Terraform code
terraform init
terraform plan
[cloud_user@ip-10-0-0-253 multi-tier-terraform]$ terraform plan
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# module.ec2_instances.aws_instance.main[0] will be created
+ resource "aws_instance" "main" {
+ ami = "ami-06b21ccaeff8cd686"
+ arn = (known after apply)
+ associate_public_ip_address = (known after apply)
+ availability_zone = (known after apply)
+ cpu_core_count = (known after apply)
+ cpu_threads_per_core = (known after apply)
+ disable_api_stop = (known after apply)
+ disable_api_termination = (known after apply)
+ ebs_optimized = (known after apply)
+ get_password_data = false
+ host_id = (known after apply)
+ host_resource_group_arn = (known after apply)
+ iam_instance_profile = (known after apply)
+ id = (known after apply)
+ instance_initiated_shutdown_behavior = (known after apply)
+ instance_lifecycle = (known after apply)
+ instance_state = (known after apply)
+ instance_type = "t3.micro"
+ ipv6_address_count = (known after apply)
+ ipv6_addresses = (known after apply)
+ key_name = (known after apply)
+ monitoring = (known after apply)
+ outpost_arn = (known after apply)
+ password_data = (known after apply)
+ placement_group = (known after apply)
+ placement_partition_number = (known after apply)
+ primary_network_interface_id = (known after apply)
+ private_dns = (known after apply)
+ private_ip = (known after apply)
+ public_dns = (known after apply)
+ public_ip = (known after apply)
+ secondary_private_ips = (known after apply)
+ security_groups = (known after apply)
+ source_dest_check = true
+ spot_instance_request_id = (known after apply)
+ subnet_id = (known after apply)
+ tags = {
+ "Name" = "ps-ec2-instance-1"
}
+ tags_all = {
+ "Name" = "ps-ec2-instance-1"
}
+ tenancy = (known after apply)
+ user_data = "4ad2948ebf59b8de7fe61f98235b28871e815879"
+ user_data_base64 = (known after apply)
+ user_data_replace_on_change = false
+ vpc_security_group_ids = (known after apply)
}
# module.ec2_instances.aws_instance.main[1] will be created
+ resource "aws_instance" "main" {
+ ami = "ami-06b21ccaeff8cd686"
+ arn = (known after apply)
+ associate_public_ip_address = (known after apply)
+ availability_zone = (known after apply)
+ cpu_core_count = (known after apply)
+ cpu_threads_per_core = (known after apply)
+ disable_api_stop = (known after apply)
+ disable_api_termination = (known after apply)
+ ebs_optimized = (known after apply)
+ get_password_data = false
+ host_id = (known after apply)
+ host_resource_group_arn = (known after apply)
+ iam_instance_profile = (known after apply)
+ id = (known after apply)
+ instance_initiated_shutdown_behavior = (known after apply)
+ instance_lifecycle = (known after apply)
+ instance_state = (known after apply)
+ instance_type = "t3.micro"
+ ipv6_address_count = (known after apply)
+ ipv6_addresses = (known after apply)
+ key_name = (known after apply)
+ monitoring = (known after apply)
+ outpost_arn = (known after apply)
+ password_data = (known after apply)
+ placement_group = (known after apply)
+ placement_partition_number = (known after apply)
+ primary_network_interface_id = (known after apply)
+ private_dns = (known after apply)
+ private_ip = (known after apply)
+ public_dns = (known after apply)
+ public_ip = (known after apply)
+ secondary_private_ips = (known after apply)
+ security_groups = (known after apply)
+ source_dest_check = true
+ spot_instance_request_id = (known after apply)
+ subnet_id = (known after apply)
+ tags = {
+ "Name" = "ps-ec2-instance-2"
}
+ tags_all = {
+ "Name" = "ps-ec2-instance-2"
}
+ tenancy = (known after apply)
+ user_data = "4ad2948ebf59b8de7fe61f98235b28871e815879"
+ user_data_base64 = (known after apply)
+ user_data_replace_on_change = false
+ vpc_security_group_ids = (known after apply)
}
# module.internet_gateway.aws_internet_gateway.main will be created
+ resource "aws_internet_gateway" "main" {
+ arn = (known after apply)
+ id = (known after apply)
+ owner_id = (known after apply)
+ tags = {
+ "Name" = "ps-internet-gateway"
}
+ tags_all = {
+ "Name" = "ps-internet-gateway"
}
+ vpc_id = (known after apply)
}
# module.load_balancer.aws_lb.application_lb will be created
+ resource "aws_lb" "application_lb" {
+ arn = (known after apply)
+ arn_suffix = (known after apply)
+ client_keep_alive = 3600
+ desync_mitigation_mode = "defensive"
+ dns_name = (known after apply)
+ drop_invalid_header_fields = false
+ enable_deletion_protection = false
+ enable_http2 = true
+ enable_tls_version_and_cipher_suite_headers = false
+ enable_waf_fail_open = false
+ enable_xff_client_port = false
+ enforce_security_group_inbound_rules_on_private_link_traffic = (known after apply)
+ id = (known after apply)
+ idle_timeout = 60
+ internal = false
+ ip_address_type = (known after apply)
+ load_balancer_type = "application"
+ name = "ps-application-lb"
+ name_prefix = (known after apply)
+ preserve_host_header = false
+ security_groups = (known after apply)
+ subnets = (known after apply)
+ tags = {
+ "Name" = "ps-application-lb"
}
+ tags_all = {
+ "Name" = "ps-application-lb"
}
+ vpc_id = (known after apply)
+ xff_header_processing_mode = "append"
+ zone_id = (known after apply)
}
# module.load_balancer_listener.aws_lb_listener.alb_listener will be created
+ resource "aws_lb_listener" "alb_listener" {
+ arn = (known after apply)
+ id = (known after apply)
+ load_balancer_arn = (known after apply)
+ port = 80
+ protocol = "HTTP"
+ ssl_policy = (known after apply)
+ tags_all = (known after apply)
+ default_action {
+ order = (known after apply)
+ target_group_arn = (known after apply)
+ type = "forward"
}
}
# module.load_balancer_listener.aws_lb_target_group.ec2_tg will be created
+ resource "aws_lb_target_group" "ec2_tg" {
+ arn = (known after apply)
+ arn_suffix = (known after apply)
+ connection_termination = (known after apply)
+ deregistration_delay = "300"
+ id = (known after apply)
+ ip_address_type = (known after apply)
+ lambda_multi_value_headers_enabled = false
+ load_balancer_arns = (known after apply)
+ load_balancing_algorithm_type = (known after apply)
+ load_balancing_anomaly_mitigation = (known after apply)
+ load_balancing_cross_zone_enabled = (known after apply)
+ name = "ps-ec2-target-group"
+ name_prefix = (known after apply)
+ port = 80
+ preserve_client_ip = (known after apply)
+ protocol = "HTTP"
+ protocol_version = (known after apply)
+ proxy_protocol_v2 = false
+ slow_start = 0
+ tags = {
+ "Name" = "ps-ec2-target-group"
}
+ tags_all = {
+ "Name" = "ps-ec2-target-group"
}
+ target_type = "instance"
+ vpc_id = (known after apply)
+ health_check {
+ enabled = true
+ healthy_threshold = 2
+ interval = 30
+ matcher = (known after apply)
+ path = "/"
+ port = "traffic-port"
+ protocol = "HTTP"
+ timeout = 5
+ unhealthy_threshold = 2
}
}
# module.rds_instance.aws_db_instance.mysql_db will be created
+ resource "aws_db_instance" "mysql_db" {
+ address = (known after apply)
+ allocated_storage = 20
+ apply_immediately = false
+ arn = (known after apply)
+ auto_minor_version_upgrade = true
+ availability_zone = (known after apply)
+ backup_retention_period = (known after apply)
+ backup_target = (known after apply)
+ backup_window = (known after apply)
+ ca_cert_identifier = (known after apply)
+ character_set_name = (known after apply)
+ copy_tags_to_snapshot = false
+ db_name = (known after apply)
+ db_subnet_group_name = "ps-db-subnet-group"
+ dedicated_log_volume = false
+ delete_automated_backups = true
+ domain_fqdn = (known after apply)
+ endpoint = (known after apply)
+ engine = "mysql"
+ engine_lifecycle_support = (known after apply)
+ engine_version = (known after apply)
+ engine_version_actual = (known after apply)
+ hosted_zone_id = (known after apply)
+ id = (known after apply)
+ identifier = (known after apply)
+ identifier_prefix = (known after apply)
+ instance_class = "db.t3.micro"
+ iops = (known after apply)
+ kms_key_id = (known after apply)
+ latest_restorable_time = (known after apply)
+ license_model = (known after apply)
+ listener_endpoint = (known after apply)
+ maintenance_window = (known after apply)
+ master_user_secret = (known after apply)
+ master_user_secret_kms_key_id = (known after apply)
+ monitoring_interval = 0
+ monitoring_role_arn = (known after apply)
+ multi_az = (known after apply)
+ nchar_character_set_name = (known after apply)
+ network_type = (known after apply)
+ option_group_name = (known after apply)
+ parameter_group_name = (known after apply)
+ password = (sensitive value)
+ performance_insights_enabled = false
+ performance_insights_kms_key_id = (known after apply)
+ performance_insights_retention_period = (known after apply)
+ port = (known after apply)
+ publicly_accessible = false
+ replica_mode = (known after apply)
+ replicas = (known after apply)
+ resource_id = (known after apply)
+ skip_final_snapshot = true
+ snapshot_identifier = (known after apply)
+ status = (known after apply)
+ storage_throughput = (known after apply)
+ storage_type = "gp2"
+ tags = {
+ "Name" = "ps-mysql-db"
}
+ tags_all = {
+ "Name" = "ps-mysql-db"
}
+ timezone = (known after apply)
+ username = "admin"
+ vpc_security_group_ids = (known after apply)
}
# module.route_table.aws_route_table.public will be created
+ resource "aws_route_table" "public" {
+ arn = (known after apply)
+ id = (known after apply)
+ owner_id = (known after apply)
+ propagating_vgws = (known after apply)
+ route = [
+ {
+ carrier_gateway_id = ""
+ cidr_block = "0.0.0.0/0"
+ core_network_arn = ""
+ destination_prefix_list_id = ""
+ egress_only_gateway_id = ""
+ gateway_id = (known after apply)
+ ipv6_cidr_block = ""
+ local_gateway_id = ""
+ nat_gateway_id = ""
+ network_interface_id = ""
+ transit_gateway_id = ""
+ vpc_endpoint_id = ""
+ vpc_peering_connection_id = ""
},
]
+ tags = {
+ "Name" = "ps-public-route-table"
}
+ tags_all = {
+ "Name" = "ps-public-route-table"
}
+ vpc_id = (known after apply)
}
# module.security_groups.aws_security_group.db_sg will be created
+ resource "aws_security_group" "db_sg" {
+ arn = (known after apply)
+ description = "Managed by Terraform"
+ egress = [
+ {
+ cidr_blocks = [
+ "0.0.0.0/0",
]
+ description = ""
+ from_port = 0
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = "-1"
+ security_groups = []
+ self = false
+ to_port = 0
},
]
+ id = (known after apply)
+ ingress = [
+ {
+ cidr_blocks = [
+ "10.0.5.0/24",
]
+ description = ""
+ from_port = 3306
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = "tcp"
+ security_groups = []
+ self = false
+ to_port = 3306
},
]
+ name = (known after apply)
+ name_prefix = (known after apply)
+ owner_id = (known after apply)
+ revoke_rules_on_delete = false
+ tags = {
+ "Name" = "ps-db-security-group"
}
+ tags_all = {
+ "Name" = "ps-db-security-group"
}
+ vpc_id = (known after apply)
}
# module.security_groups.aws_security_group.ec2_sg will be created
+ resource "aws_security_group" "ec2_sg" {
+ arn = (known after apply)
+ description = "Managed by Terraform"
+ egress = [
+ {
+ cidr_blocks = [
+ "0.0.0.0/0",
]
+ description = ""
+ from_port = 0
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = "-1"
+ security_groups = []
+ self = false
+ to_port = 0
},
]
+ id = (known after apply)
+ ingress = [
+ {
+ cidr_blocks = [
+ "0.0.0.0/0",
]
+ description = ""
+ from_port = 22
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = "tcp"
+ security_groups = []
+ self = false
+ to_port = 22
},
+ {
+ cidr_blocks = [
+ "0.0.0.0/0",
]
+ description = ""
+ from_port = 80
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = "tcp"
+ security_groups = []
+ self = false
+ to_port = 80
},
]
+ name = (known after apply)
+ name_prefix = (known after apply)
+ owner_id = (known after apply)
+ revoke_rules_on_delete = false
+ tags = {
+ "Name" = "ps-ec2-security-group"
}
+ tags_all = {
+ "Name" = "ps-ec2-security-group"
}
+ vpc_id = (known after apply)
}
# module.subnets.aws_db_subnet_group.ps_db_subnet_group will be created
+ resource "aws_db_subnet_group" "ps_db_subnet_group" {
+ arn = (known after apply)
+ description = "Managed by Terraform"
+ id = (known after apply)
+ name = "ps-db-subnet-group"
+ name_prefix = (known after apply)
+ subnet_ids = (known after apply)
+ supported_network_types = (known after apply)
+ tags = {
+ "Name" = "ps-db-subnet-group"
}
+ tags_all = {
+ "Name" = "ps-db-subnet-group"
}
+ vpc_id = (known after apply)
}
# module.subnets.aws_subnet.private_1 will be created
+ resource "aws_subnet" "private_1" {
+ arn = (known after apply)
+ assign_ipv6_address_on_creation = false
+ availability_zone = "us-east-1a"
+ availability_zone_id = (known after apply)
+ cidr_block = "10.0.5.0/24"
+ enable_dns64 = false
+ enable_resource_name_dns_a_record_on_launch = false
+ enable_resource_name_dns_aaaa_record_on_launch = false
+ id = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ ipv6_native = false
+ map_public_ip_on_launch = false
+ owner_id = (known after apply)
+ private_dns_hostname_type_on_launch = (known after apply)
+ tags = {
+ "Name" = "ps-private-subnet-1"
}
+ tags_all = {
+ "Name" = "ps-private-subnet-1"
}
+ vpc_id = (known after apply)
}
# module.subnets.aws_subnet.private_2 will be created
+ resource "aws_subnet" "private_2" {
+ arn = (known after apply)
+ assign_ipv6_address_on_creation = false
+ availability_zone = "us-east-1b"
+ availability_zone_id = (known after apply)
+ cidr_block = "10.0.6.0/24"
+ enable_dns64 = false
+ enable_resource_name_dns_a_record_on_launch = false
+ enable_resource_name_dns_aaaa_record_on_launch = false
+ id = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ ipv6_native = false
+ map_public_ip_on_launch = false
+ owner_id = (known after apply)
+ private_dns_hostname_type_on_launch = (known after apply)
+ tags = {
+ "Name" = "ps-private-subnet-2"
}
+ tags_all = {
+ "Name" = "ps-private-subnet-2"
}
+ vpc_id = (known after apply)
}
# module.subnets.aws_subnet.public_1 will be created
+ resource "aws_subnet" "public_1" {
+ arn = (known after apply)
+ assign_ipv6_address_on_creation = false
+ availability_zone = "us-east-1a"
+ availability_zone_id = (known after apply)
+ cidr_block = "10.0.1.0/24"
+ enable_dns64 = false
+ enable_resource_name_dns_a_record_on_launch = false
+ enable_resource_name_dns_aaaa_record_on_launch = false
+ id = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ ipv6_native = false
+ map_public_ip_on_launch = false
+ owner_id = (known after apply)
+ private_dns_hostname_type_on_launch = (known after apply)
+ tags = {
+ "Name" = "ps-public-subnet-1"
}
+ tags_all = {
+ "Name" = "ps-public-subnet-1"
}
+ vpc_id = (known after apply)
}
# module.subnets.aws_subnet.public_2 will be created
+ resource "aws_subnet" "public_2" {
+ arn = (known after apply)
+ assign_ipv6_address_on_creation = false
+ availability_zone = "us-east-1b"
+ availability_zone_id = (known after apply)
+ cidr_block = "10.0.2.0/24"
+ enable_dns64 = false
+ enable_resource_name_dns_a_record_on_launch = false
+ enable_resource_name_dns_aaaa_record_on_launch = false
+ id = (known after apply)
+ ipv6_cidr_block_association_id = (known after apply)
+ ipv6_native = false
+ map_public_ip_on_launch = false
+ owner_id = (known after apply)
+ private_dns_hostname_type_on_launch = (known after apply)
+ tags = {
+ "Name" = "ps-public-subnet-2"
}
+ tags_all = {
+ "Name" = "ps-public-subnet-2"
}
+ vpc_id = (known after apply)
}
# module.vpc.aws_vpc.main will be created
+ resource "aws_vpc" "main" {
+ arn = (known after apply)
+ cidr_block = "10.0.0.0/16"
+ default_network_acl_id = (known after apply)
+ default_route_table_id = (known after apply)
+ default_security_group_id = (known after apply)
+ dhcp_options_id = (known after apply)
+ enable_dns_hostnames = (known after apply)
+ enable_dns_support = true
+ enable_network_address_usage_metrics = (known after apply)
+ id = (known after apply)
+ instance_tenancy = "default"
+ ipv6_association_id = (known after apply)
+ ipv6_cidr_block = (known after apply)
+ ipv6_cidr_block_network_border_group = (known after apply)
+ main_route_table_id = (known after apply)
+ owner_id = (known after apply)
+ tags = {
+ "Name" = "ps-main-vpc"
}
+ tags_all = {
+ "Name" = "ps-main-vpc"
}
}
Plan: 16 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ db_sg_id = (known after apply)
+ ec2_instance_ids = [
+ (known after apply),
+ (known after apply),
]
+ ec2_sg_id = (known after apply)
+ internet_gateway_id = (known after apply)
+ load_balancer_arn = (known after apply)
+ load_balancer_listener_arn = (known after apply)
+ public_subnet_1_id = (known after apply)
+ public_subnet_2_id = (known after apply)
+ rds_instance_id = (known after apply)
+ route_table_id = (known after apply)
+ vpc_id = (known after apply)
─────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.
[cloud_user@ip-10-0-0-253 multi-tier-terraform]$
Once reviewed the plan, execute the infrastructure deployment by,
terraform apply
Summary
This entire set of new infrastructure has been built within few minutes with the help of Terraform in CLI:
Terraform is efficient for building AWS infrastructure because it automates and standardizes the process through declarative configurations. Instead of executing individual commands manually, you define your infrastructure as code, and Terraform handles the provisioning. This approach saves time and reduces errors.
For example, in real use cases, Terraform can quickly spin up an entire application environment, including EC2 instances, RDS databases, and S3 buckets, with a single command. If you need to replicate environments across regions for high availability or disaster recovery, Terraform makes it seamless by reusing the same configuration. It’s also invaluable for teams, as it ensures consistent environments during development, testing, and production.
Like this article? Subscribe to Engineering Leadership , Digital Accessibility, Digital Payments Hub and Motivation newsletters to enjoy reading useful articles. Press SHARE and REPOST button to help sharing the content with your network.
Recently came across a very interesting success of Fabien Perronnet Escoffier for clearing 12+ certifications hence congrats to him-follow his latest Q&A at: Link