DEV Community

Cover image for Building an Automated AWS Deployment Pipeline with Terraform and Bash
Kingsley Erhatiemwonmon
Kingsley Erhatiemwonmon

Posted on

Building an Automated AWS Deployment Pipeline with Terraform and Bash

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.

DevOps #AWS #Terraform #Bash #InfrastructureAsCode #CloudEngineering #Linux #Nginx #Git #Automation

Top comments (2)

Collapse
 
mealiclay01 profile image
Anas Rhimi

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?

Collapse
 
mcptokensaver profile image
MCP Token Saver

the numbers speak for themselves. 97% reduction is not marginal optimization.