I recently completed a hands-on Cloud and DevOps project that combined AWS, Terraform, Bash, Linux, Git, and Nginx to automate the deployment of a web application.
The objective was to build a repeatable workflow that could provision the required AWS infrastructure, configure an Ubuntu server, deploy an application from GitHub, and verify that the web server was running successfully.
The result was a complete automated deployment workflow:
Terraform provisions the infrastructure. Bash configures the server. Git provides the application source. Nginx serves the website. Health checks verify the deployment.
By the end of the project, I had successfully:
Provisioned a complete AWS network using Terraform
Created a VPC and public subnet
Created and configured an Internet Gateway
Created a route table and route table association
Configured a security group for SSH and HTTP access
Dynamically selected an Ubuntu 24.04 AMI
Launched an Ubuntu EC2 instance using the existing react-key key pair
Used Terraform's templatefile() function to pass a Bash bootstrap script to the EC2 instance
Automatically installed Git and Nginx
Cloned a public GitHub repository
Deployed the application to /var/www/html
Configured ownership and file permissions
Enabled and started Nginx
Performed automated service and HTTP health checks
Added useful Terraform outputs for the deployment
Successfully deployed a live website on AWS
This project gave me practical experience building an automated workflow from cloud infrastructure provisioning to application delivery.
Project Structure
The project was organised with a clear separation between infrastructure code, server automation, and application files:
terraform-bash-automation/
│
├── .gitignore
├── .terraform.lock.hcl
├── README.md
├── main.tf
├── outputs.tf
├── providers.tf
├── variables.tf
│
├── app/
│ ├── index.html
│ └── style.css
│
└── scripts/
└── bootstrap.sh
main.tf
Contains the AWS infrastructure resources, including:
VPC
Public subnet
Internet Gateway
Route table
Route table association
Security group
EC2 instance
variables.tf
Defines reusable input variables such as:
AWS region
Instance type
Key pair name
SSH CIDR
Application repository URL
outputs.tf
Exposes useful information after deployment, including:
EC2 instance ID
Public IP address
VPC ID
Subnet ID
Website URL
scripts/bootstrap.sh
Contains the Bash automation responsible for configuring the Ubuntu server and deploying the application.
app/
Contains the web application files used by the project.
This structure kept the infrastructure, server configuration, and application files logically organised.
The Architecture
The project followed a clear division of responsibilities.
Terraform: Infrastructure Provisioning
Terraform was responsible for creating the AWS environment:
Terraform
│
├── VPC
├── Public Subnet
├── Internet Gateway
├── Route Table
├── Route Table Association
├── Security Group
└── EC2 Instance
Bash: Server Configuration and Deployment
Once the EC2 instance launched, the Bash bootstrap script configured the server:
EC2 Instance
│
▼
bootstrap.sh
│
├── Update Ubuntu
├── Install Git
├── Install Nginx
├── Clone Application Repository
├── Deploy Application Files
├── Configure Permissions
├── Start Nginx
└── Run Health Checks
The complete deployment flow was:
Terraform
│
▼
AWS Infrastructure
│
▼
Ubuntu EC2 Instance
│
▼
Bash Bootstrap Script
│
├── Git
├── Nginx
├── Application Deployment
└── Health Checks
│
▼
Live Website
Terraform answered:
Where should the application run?
Bash answered:
How should the server become ready to run it?
Provisioning the AWS Infrastructure
The first major achievement was provisioning the required AWS infrastructure entirely through Terraform.
The VPC used the CIDR block:
10.0.0.0/16
Inside the VPC, I created a public subnet:
10.0.1.0/24
The subnet was configured to automatically assign public IP addresses to instances launched within it.
I also created an Internet Gateway and a route table with a default route:
0.0.0.0/0 → Internet Gateway
The route table was associated with the public subnet, creating the network path required for the EC2 instance to communicate with the internet.
The resulting infrastructure was:
Instead of manually creating these resources through the AWS Console, the infrastructure was defined as code and provisioned through Terraform.
This made the environment repeatable and easier to manage.
Configuring Network Access
I created a security group for the web server.
The security group allowed:
SSH → Port 22
HTTP → Port 80
HTTP access was required so that users could access the website through the EC2 instance's public IP address.
SSH access was required for administrative access to the server.
For a production environment, SSH access should be restricted to trusted IP addresses or replaced with a more secure access method such as AWS Systems Manager Session Manager.
This project reinforced an important principle:
Infrastructure automation must consider both functionality and security.
Dynamically Selecting the Ubuntu AMI
Rather than hardcoding an AMI ID, I configured Terraform to dynamically find the latest matching Ubuntu 24.04 AMI.
This reduced dependence on a specific AMI ID and made the configuration more flexible.
The EC2 instance was configured with:
Operating System: Ubuntu 24.04
Instance Type: t2.micro
Key Pair: react-key
Region: us-east-1
Terraform then used the selected AMI when launching the instance.
Connecting Terraform to Bash
One of the most important parts of the project was connecting infrastructure provisioning with server configuration.
I used Terraform's templatefile() function to pass the Bash bootstrap script to the EC2 instance:
user_data = templatefile("${path.module}/scripts/bootstrap.sh", {
app_repo_url = var.app_repo_url
})
This created the following workflow:
Terraform creates EC2
│
▼
EC2 starts Ubuntu
│
▼
User data executes
│
▼
bootstrap.sh runs
│
▼
Server becomes application-ready
This meant the server could be automatically configured immediately after it was provisioned.
No manual package installation or repetitive server setup was required.
Automating Ubuntu Configuration with Bash
The Bash script began with:
!/bin/bash
set -euo pipefail
This provided stronger error handling:
-e stops execution when a command fails.
-u treats undefined variables as errors.
pipefail ensures failures inside pipelines are not silently ignored.
The script also logged its output to:
/var/log/bootstrap.log
This provided a useful record of the bootstrap process for troubleshooting.
The script then updated the Ubuntu system:
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y
It installed the required software:
apt-get install -y git nginx
This eliminated the need to manually connect to the server and install packages one by one.
Automatically Deploying the Application
The application source code was hosted in a public GitHub repository.
The Bash script cloned the repository:
git clone "$APP_REPO_URL" "$APP_DIR"
The application was cloned into:
/opt/mediplus
The application files were then deployed to the Nginx web root:
/var/www/html
The deployment flow was:
GitHub Repository
│
▼
git clone
│
▼
/opt/mediplus
│
▼
Copy application files
│
▼
/var/www/html
│
▼
Nginx
This automated the process of retrieving and deploying the application.
Configuring Ownership and Permissions
After deploying the files, the script configured ownership:
chown -R www-data:www-data "$WEB_ROOT"
It then applied separate permissions to directories and files:
find "$WEB_ROOT" -type d -exec chmod 755 {} \;
find "$WEB_ROOT" -type f -exec chmod 644 {} \;
This created a consistent permission structure for the deployed website.
The deployment process therefore did more than simply copy files to the web root. It also configured the files with appropriate ownership and permissions for the Nginx web server.
Starting Nginx and Verifying the Deployment
The script enabled Nginx to start automatically:
systemctl enable nginx
It then restarted the service:
systemctl restart nginx
The deployment also included automated health checks.
First, the script verified that Nginx was running:
systemctl is-active --quiet nginx
It then tested the local HTTP endpoint:
curl --fail --silent --show-error http://localhost
This was an important part of the automation.
The script did not simply install Nginx and assume the deployment was successful.
It verified that the service was active and responding.
Automation should verify the result of the work it performs.
Validating the Terraform Configuration
Before applying the infrastructure, I followed the Terraform workflow:
terraform fmt
This formatted the Terraform configuration.
I then validated the configuration:
terraform validate
Terraform returned:
Success! The configuration is valid.
Next, I reviewed the execution plan:
terraform plan
The plan showed:
Plan: 7 to add, 0 to change, 0 to destroy.
The seven resources were:
VPC
Public subnet
Internet Gateway
Route table
Route table association
Security group
EC2 instance
This allowed me to review the proposed infrastructure before applying the changes.
Successfully Provisioning the Infrastructure
After reviewing the plan, I ran:
terraform apply
Terraform successfully created the AWS infrastructure:
Apply complete! Resources: 7 added, 0 changed, 0 destroyed.
At this point, the infrastructure provisioning phase was complete.
Terraform had successfully created the environment required to host the application.
The EC2 instance was then automatically configured through the Bash bootstrap process.
Adding Useful Terraform Outputs
I added Terraform outputs to make important deployment information easier to retrieve.
Running:
terraform output
returned:
instance_id = "i-033bb416c07a6ce37"
instance_public_ip = "3.90.108.152"
subnet_id = "subnet-0c7c604e4595302aa"
vpc_id = "vpc-05ebdb39e31d9b674"
website_url = "http://3.90.108.152"
These outputs made it easy to retrieve key deployment details without manually searching through the AWS Console.
This was a small but valuable improvement to the usability of the Terraform project.
The Final Achievement
The final result was a live website deployed on AWS through an automated workflow.
The complete process was:
Terraform
│
▼
Create AWS Infrastructure
│
├── VPC
├── Public Subnet
├── Internet Gateway
├── Route Table
├── Security Group
└── EC2 Instance
│
▼
Bash Bootstrap
│
├── Update Ubuntu
├── Install Git
├── Install Nginx
├── Clone GitHub Repository
├── Deploy Application
├── Configure Permissions
├── Start Nginx
└── Run Health Checks
│
▼
Live Website
What I built was more than a simple EC2 deployment.
I created the infrastructure required to host the application, automated the configuration of the operating system, deployed the application from GitHub, configured the web server, and verified that the service was running successfully.
The result was a repeatable workflow that transformed a clean AWS environment into a working application server.
Key Lessons from the Project
Infrastructure as Code
Terraform allowed the AWS infrastructure to be defined declaratively and provisioned consistently.
Automation
Bash eliminated repetitive manual server configuration steps.
Separation of Responsibilities
Terraform managed the infrastructure.
Bash managed server configuration and application deployment.
This separation made the overall workflow easier to understand and maintain.
Verification
The deployment included service and HTTP health checks instead of assuming that successful commands automatically meant a successful deployment.
Security Awareness
The deployment worked, but production environments require careful consideration of network access, especially SSH exposure.
Restricting SSH access to trusted IP addresses or using a managed access solution such as AWS Systems Manager Session Manager would be more appropriate for a production environment.
Final Thoughts
This project was a practical step forward in my Cloud and DevOps journey.
I combined:
AWS
Terraform
Bash
Linux
Git
Nginx
Infrastructure as Code
Server automation
Application deployment
Health checks
The most important achievement was building a repeatable process that could take a clean AWS environment and automatically transform it into a working application server.
The final workflow can be summarised simply:
Terraform provisions the infrastructure.
Bash configures the server.
Git provides the application source.
Nginx serves the website.
Health checks verify the deployment.
This project gave me valuable hands-on experience with the complete path from cloud infrastructure to application delivery.
Top comments (2)
Nice end-to-end walkthrough — this is a clean template for anyone moving from manual EC2 setup to IaC. One thing I would add for production: move the state to an S3 backend with DynamoDB locking, since a local state file breaks the moment a second person runs apply. Also worth scoping that security group to your real SSH IP instead of 0.0.0.0/0. What is your plan for handling state when this grows past a single machine?
the numbers speak for themselves. 97% reduction is not marginal optimization.