This repository is a practical and automated study of AWS infrastructure provisioning using Terraform, Packer, and Shell Script. The goal is to create a scalable, secure, and maintainable environment, focusing on best practices for real-world projects and AWS certification preparation.
The project simulates a scalable web application scenario, with multiple environments (dev, prod, staging), automated deployment of static content via EFS, and a secure bastion host lifecycle. The main flow is:
- Custom AMI build with Packer (NGINX, dependencies, etc).
- Infrastructure provisioning (VPC, EFS, ALB, ASG, Bastion) via Terraform.
- Centralized content update on EFS, instantly reflected on all ASG instances.
- Full automation via the
run.shscript, orchestrating all steps, including bastion host lifecycle.
- Modular VPC: Public/private subnets, routing, segmented security groups.
- EFS: Shared storage for web content, mounted on all ASG instances.
- ALB & ASG: Load balancing and auto-scaling.
- Ephemeral Bastion Host: Created on demand for admin operations (e.g., EFS updates), automatically destroyed after use.
- Automation via
run.sh: A single entry point for build, deploy, content update, and teardown.
The project is structured into modules to promote modularity, reusability, and scalability. Environment variables and secrets will be used to manage sensitive and environment-specific configurations.
To support multiple environments (such as dev, prod, staging), the project uses a folder structure for .tfvars files:
.
├── src/
│ └── index.html # Example file that can be updated on EFS
│
├── packer/
│ ├── ami-templates/
│ │ ├── nginx-webserver/
│ │ │ ├── build.pkr.hcl
│ │ │ ├── nginx-ami.pkr.hcl
│ │ │ ├── source.pkr.hcl
│ │ │ └── variables.pkr.hcl
│ │ │
│ │ └── another-app-worker/
│ │ ├── ...
│ │
│ ├── envs/
│ │ ├── dev/
│ │ │ └── dev.pkrvars.hcl
│ │ ├── prod/
│ │ │ └── prod.pkrvars.h│cl
│ │ └── staging/
│ │ └── staging.pkrvars.hcl
│ │
│ ├── README.md
│ └── README.pt-br.md
│
├── infra/
│ ├── main.tf
│ ├── variables.tf
│ ├── provider.tf
│ ├── outputs.tf
│ ├── backend.tf
│ │
│ ├── modules/
│ │ ├── vpc/ # Virtual Private Cloud Module
│ │ ├── efs/ # Elastic File System Module
│ │ ├── alb/ # Application Load Balancer Module
│ │ └── bhc/ # Bastion Host Controller Module
│ │
│ └── envs/
│ ├── dev/
│ │ |── terraform.tfvars
│ │ └── ...
│ ├── prod/
│ │ └── terraform.tfvars
│ └── staging/
│ └── terraform.tfvars
│
├── scripts/
│ ├── efs_actions.sh
│ ├── packer_actions.sh
│ ├── terraform_actions.sh
│ └── utils.sh
│
├── .env
├── .env.example
├── .gitignore
├── README.md
├── README.pt-br.md
├── run.sh
Each environment folder (dev, prod, staging, etc.) contains a terraform.tfvars file with specific configurations for that environment, such as VPC CIDR blocks, AMI IDs, and instance types.
The infrastructure provisioned by this project covers the following components:
Responsible for creating the virtual network in AWS, including:
- VPC (Virtual Private Cloud): Isolated network for your infrastructure.
- First Network:
10.0.0.0/16- Public Subnet AZa:
10.0.1.0/24 - Private Subnet AZa:
10.0.2.0/24 - Public Subnet AZb:
10.0.3.0/24 - Private Subnet AZb:
10.0.4.0/24
- Public Subnet AZa:
- Second Network (for multi-region peering):
10.1.0.0/16> Optional- Public Subnet AZa:
10.1.1.0/24 - Private Subnet AZa:
10.1.2.0/24 - Public Subnet AZb:
10.1.3.0/24 - Private Subnet AZb:
10.1.4.0/24
- Public Subnet AZa:
- First Network:
- Subnets: Divided into public and private subnets across different Availability Zones (AZa, AZb).
- Route Tables:
- A public route table for internet inbound and outbound traffic.
- A private route table for internal traffic and AWS service access.
- Internet Gateway (IGW): Enables communication between the VPC and the internet.
- The IGW will be associated with the public route table.
- Security Groups:
- Public: Allowing HTTP access (port 80) from any IP (
0.0.0.0/0). - Private: Allowing SSH access (port 22) only from internal VPC subnets.
- Bastion: Specific Security Group for the bastion host, allowing SSH from controlled IPs and EFS access.
- EFS: Security Group for EFS, allowing NFS traffic from application instances and the bastion host.
- Public: Allowing HTTP access (port 80) from any IP (
Responsible for provisioning Amazon Elastic File System (EFS), a distributed and scalable file system.
- EFS File System: A centralized point for storing data that can be shared across multiple EC2 instances. This is essential for applications that require common and persistent storage, such as web servers serving static content. Auto Scaling Group instances will mount this EFS, ensuring all application replicas access the same files.
- Mount Targets: Access points within the private subnets of the VPC, allowing EC2 instances to mount EFS.
- Security Group: As mentioned in the VPC, a dedicated SG for EFS to control NFS access.
Responsible for configuring load balancing and auto-scaling for your web application.
- Application Load Balancer (ALB): Distributes inbound HTTP/HTTPS traffic among application instances.
- Target Group: Groups the EC2 instances that receive traffic from the ALB.
- Launch Template: Defines the configurations for the EC2 instances to be launched, including instance type, AMI, SSH key, and
user_datascript for mounting EFS. - Auto Scaling Group (ASG): Ensures a specific number of EC2 instances are always running, scaling up or down automatically based on demand, providing high availability and resilience.
Responsible for provisioning a secure and temporary bastion host.
- EC2 Bastion Host Instance: A virtual machine that can be created and destroyed on demand. It serves as a secure access point to the private network, allowing management operations (such as updating files on EFS) to be performed without exposing application instances directly to the internet.
- Ephemeral Configuration: The bastion host is configured with all necessary resources (Amazon Linux 2 based AMI, automatic EFS mounting at
/mnt/efs, and appropriate IAM permissions via instance profile). - Access Control: The bastion's Security Group is strictly configured to allow SSH access only from trusted IPs and NFS access to EFS. The use of SSM (AWS Systems Manager) is prioritized for access and command execution, eliminating the need to publicly open SSH ports.
- Cost and Security Optimization: Being temporary and activated on demand, the bastion host minimizes costs and reduces the attack surface, as it is not active 24/7.
This project adopts a robust approach for managing machine images and application content:
- Optimized AMI with Packer: We use Packer to build custom machine images (AMIs). This AMI pre-installs essential services like NGINX,
amazon-efs-utils, and other dependencies, in addition to ensuring system packages are updated. Instead of installing everything in theuser_dataof each new instance, the AMI comes ready, which speeds up instance boot time and makes them more consistent and secure, especially for instances in private subnets that do not have direct internet access. - EFS as a Distributed File Server: Amazon EFS is employed as a fully managed network file system (NFS). This means web content (HTML, CSS, JS, images) is stored in a single, centralized source of truth on EFS. When a file is updated on EFS (e.g., via the bastion host), this change is immediately reflected across all EC2 instances in the Auto Scaling Group that are mounting the same EFS. This eliminates the need to individually synchronize files on each server, simplifying content deployment and ensuring consistency.
- Bastion Host for Secure Operations: Updating content on EFS or other management tasks are performed securely through the temporary bastion host. This bastion is created with appropriate Security Groups and IAM profiles, ensuring that only necessary traffic and permissions are granted during the operation's lifespan. This keeps your application instances in private subnets, protected from direct access.
AWS resources will follow a consistent naming pattern:
$username.$region.$resource-name.$name.$environment
$username: Your username or identifier.$region: The AWS region where the resource is being provisioned (e.g.,us-east-1).$resource-name: The type of resource (e.g.,vpc,subnet,alb).$name: A descriptive name for the resource.$environment: The environment where the resource is being provisioned (e.g.,dev).
Example: andresinho20049.us-east-1.vpc.my-vpc.dev
All provisioned resources will include the following tags for better organization and traceability:
environment:$env(Ex:dev,prod,staging)project:$project(Project name, ex:terraform-study)region:$region(AWS Region)
To manage Terraform state securely and collaboratively, an S3 backend with DynamoDB for state locking will be used. Additionally, workspaces will be employed to isolate environments (development, production, etc.).
To switch or create workspaces, use:
terraform workspace select $ENVIRONMENT || terraform workspace new $ENVIRONMENTThis ensures that the Terraform state is stored separately for each environment (e.g., dev, prod).
- Terraform CLI installed.
- Packer CLI installed.
- AWS CLI configured with credentials.
- S3 bucket configured for state backend.
- DynamoDB table configured for state locking.
This project offers two main ways to interact with the infrastructure: by executing commands manually (for greater control and debugging) or by using the run.sh script (for automation and convenience).
Regardless of the chosen approach, the initial steps are the same.
-
Clone the Repository:
git clone https://github.com/andresinho20049/terraform-aws-with-autoscaling-course cd terraform-aws-with-autoscaling-course -
Rename the
.env.examplefile to.env: This file will contain the environment variables necessary for the Terraform backend and other global configurations.cp .env.example .env
Remember to replace the example values with your own.
-
Load Environment Variables: Before running any Packer, Terraform, or
run.shcommands, load the variables from the.envfile into your shell session.source .env
Follow these steps if you prefer to execute Packer, Terraform, and AWS CLI commands manually for greater control and debugging.
👀 See Example
- Navigate to the Packer directory:
cd packer/ami-templates/nginx-webserver/ - Initialize Packer:
packer init . - Build the AMI with Packer:
packer build \ -var-file="../../envs/$ENVIRONMENT/$ENVIRONMENT.pkrvars.hcl" .
-
Navigate back to the
infradirectory:cd ../../../infra -
Initialize Terraform: This command configures the S3 backend for Terraform state management.
terraform init \ -backend-config="bucket=$TF_BACKEND_BUCKET" \ -backend-config="key=$TF_BACKEND_KEY" \ -backend-config="region=$TF_BACKEND_REGION" \ -backend-config="dynamodb_table=$TF_AWS_LOCK_DYNAMODB_TABLE" -
Select or Create Workspace: Define the environment for which you want to provision the infrastructure. Make sure the value of
$ENVIRONMENTmatches one of the folders inenvs/.terraform workspace select $ENVIRONMENT || terraform workspace new $ENVIRONMENT
-
Plan Infrastructure: This command generates an execution plan, showing which resources will be created, modified, or destroyed. It uses the specific
.tfvarsfile for the selected environment.mkdir -p plan terraform plan \ -var-file="./envs/$ENVIRONMENT/terraform.tfvars" \ -var="account_username=$USERNAME" \ -var="project=$PROJECT_NAME" \ -var="key_name=$SSH_KEY_NAME" \ -var="create_bastion_host=false" \ -out="./plan/$ENVIRONMENT.plan"Note: The
-var="create_bastion_host=false"ensures that the bastion host is not created by default during the main infrastructureapply. -
Apply Infrastructure: Execute the generated plan to provision resources in AWS.
terraform apply "./plan/$ENVIRONMENT.plan"
-
Create the Bastion Host: Navigate to the
infradirectory and apply Terraform to create the bastion.cd infra # If you're not already in the infra directory terraform plan \ -var-file="./envs/$ENVIRONMENT/terraform.tfvars" \ -var="account_username=$USERNAME" \ -var="project=$PROJECT_NAME" \ -var="key_name=$SSH_KEY_NAME" \ -var="create_bastion_host=true" \ -out="./plan/$ENVIRONMENT.bastion.plan" terraform apply "./plan/$ENVIRONMENT.bastion.plan"
Get the Bastion Instance ID:
terraform output -raw bastion_instance_id # Example output: i-0abcdef1234567890Save this ID, you'll need it.
For those who want to understand or execute the process of updating a file on EFS manually, without using the run.sh script, follow the detailed steps below. This method uses a temporary S3 bucket as an intermediary for file transfer, ensuring security and efficiency through AWS Systems Manager (SSM).
We assume the bastion host is already running and that the EFS is mounted at /mnt/efs on your instances, with your website content in /mnt/efs/<PROJECT_NAME>/html/.
-
Upload Local File to a Temporary S3 Bucket Before updating EFS, let's upload the file to a temporary S3 bucket.
LOCAL_FILE="./src/index.html" # Change this to your local file path # Crie um nome para o bucket S3 temporário e uma chave única para o arquivo S3_TEMP_BUCKET="${USERNAME}.${TF_BACKEND_REGION}.s3.bhc-temp.${ENVIRONMENT}" S3_KEY="efs-temp/$(basename "$LOCAL_FILE")-$(date +%s)" aws s3 cp "$LOCAL_FILE" "s3://$S3_TEMP_BUCKET/$S3_KEY" --region "$AWS_REGION"
-
Move the File from S3 to EFS on the Bastion Host and Adjust Permissions
Now, use
aws ssm send-commandto run commands on the bastion host. These commands will download the file from S3, move it to the correct EFS directory, and adjust its permissions and ownership.EFS_RELATIVE_PATH="html/index.html" # Adjust this to your EFS file path # Full EFS file path EFS_MOUNT_POINT_ON_EC2="/mnt/efs" EFS_TARGET_FULL_PATH="${EFS_MOUNT_POINT_ON_EC2}/${PROJECT_NAME}/${EFS_RELATIVE_PATH}" LOCAL_FILE_BASENAME="$(basename "$LOCAL_FILE")" # Construct the remote command string, escaping internal double quotes for JSON. REMOTE_COMMANDS="sudo mkdir -p \\\"$(dirname "$EFS_TARGET_FULL_PATH")\\\"; \\ aws s3 cp \\\"s3://$s3_temp_bucket/$s3_key\\\" \\\"/tmp/$(basename "$local_file_full_path")\\\"; \\ sudo mv \\\"/tmp/$(basename "$local_file_full_path")\\\" \\\"$EFS_TARGET_FULL_PATH\\\"; \\ aws s3 rm \\\"s3://$s3_temp_bucket/$s3_key\\\"" aws ssm send-command \ --instance-ids "$BASTION_ID" \ --document-name "AWS-RunShellScript" \ --parameters "commands=[\"$REMOTE_COMMANDS\"]" \ --region "$region" \ --output text
-
Trigger an Instance Refresh on the Auto Scaling Group (Crucial for Deployment)
For instances in your Auto Scaling Group to start serving the updated content, you need to trigger an instance refresh. This ensures that new instances (with the latest EFS content, since it is a shared file system) are launched and old ones are gradually removed.
cd infra ASG_NAME=$(terraform output -raw asg_name) # Certifique-se de que este output existe cd .. echo "Iniciando Instance Refresh para o Auto Scaling Group: $ASG_NAME" aws autoscaling start-instance-refresh \ --auto-scaling-group-name "$ASG_NAME" \ --region "$AWS_REGION" \ --preferences '{"MinHealthyPercentage": 50, "InstanceWarmup": 180}' if [ $? -ne 0 ]; then echo "Aviso: Falha ao iniciar o instance refresh para o ASG '$ASG_NAME'. Verifique o console AWS para detalhes." else echo "Instance refresh iniciado com sucesso para '$ASG_NAME'. Novas instâncias serão provisionadas para servir o conteúdo atualizado." fi
-
Destroy the Bastion Host:
When you no longer need the bastion host, remove it by applying Terraform with
create_bastion_hostset tofalse. This avoids tearing down your entire infrastructure.cd infra # If you're not already in the infra directory terraform plan \ -var-file="./envs/$ENVIRONMENT/terraform.tfvars" \ -var="account_username=$USERNAME" \ -var="project=$PROJECT_NAME" \ -var="key_name=$SSH_KEY_NAME" \ -var="create_bastion_host=false" \ -out="./plan/$ENVIRONMENT.destroy_bastion.plan" terraform apply "./plan/$ENVIRONMENT.destroy_bastion.plan"
- Navigate to the
infradirectory:cd infra - Destroy the infrastructure:
terraform destroy \ -var-file="./envs/$ENVIRONMENT/terraform.tfvars" \ -var="account_username=$USERNAME" \ -var="project=$PROJECT_NAME" \ -var="key_name=$SSH_KEY_NAME" \ -var="create_bastion_host=false" # Ensures the bastion (if exists) is considered for destruction
The run.sh script centralizes and automates operations, making them simpler and less error-prone.
👀 See Example
-
Grant Execute Permissions to Scripts
Ensure that the main script and helper scripts have execute permissions.
chmod +x run.sh scripts/*.sh -
Full provisioning (build AMI + infrastructure):
./run.sh apply
-
Update content on EFS (reflected on all instances):
./run.sh update-efs-file src/index.html html/index.html # Or for entire directories: ./run.sh update-efs-file src/ html/The script creates the bastion if needed, uploads securely via temporary S3, executes remote commands via SSM, and destroys the bastion at the end.
-
Destroy infrastructure:
./run.sh destroy
- Secure bastion lifecycle: No open SSH ports, uses SSM, destroys host after use.
- End-to-end automation: From AMI build to content deploy, all via a single script.
- Multi-environment: Clear separation via workspaces and
.tfvarsfiles. - Idempotency & consistency: Content updates are reflected on all instances without manual deploys.
- Ready for multi-region & peering: Network structure prepared for expansion.
Developed by Andresinho20049
Project: AWS Infrastructure with Terraform – Study & Automation
Description:
This project offers a practical and automated study of AWS infrastructure provisioning using Terraform, Packer, and Shell Script. It creates a scalable, secure, and easy-to-maintain environment, focusing on best practices and preparing for AWS certifications. It simulates a web application with multiple environments (dev, prod, staging), including the creation of a custom AMI with Packer, provisioning of VPC, EFS, ALB, and ASG via Terraform, and full automation via the run.sh script.