Author: Amitabh Soni
In this blog, we will set up a complete CI/CD pipeline for an online shop application using GitHub Actions. We’ll cover everything from forking the repository and setting up SSH keys to configuring GitHub secrets and triggering workflows that deploy and later destroy the infrastructure. This guide is designed to leave no stone unturned, so follow along carefully!
Repository Setup
- Fork and Clone the Repository
- Visit the Original Repository:
Go to the online_shop GitHub repository. - Fork the Repository:
Click the “Fork” button to create your own copy in your GitHub account.
- Visit the Original Repository:
- Open VsCode or any Code Editor
Open your preferred code editor (such as VSCode) to work with the code locally. - Cloning the Repo
Clone your forked repository locally using the command below. Replace<YOUR_USERNAME>
with your GitHub username:git clone https://github.com/<YOUR_USERNAME>/online_shop
- Switch to the “github-action” Branch
After cloning, navigate into the repository directory and switch to the branch that contains the CI/CD setup:cd online_shop git checkout github-action
SSH Key Setup in Terraform Resources
Before running our workflows, we need to set up a proper SSH key for secure communication with the EC2 instance.
- Navigate to the Terraform Resources Directory
Change directory to the Terraform resources folder:cd terraform/terraform_resources
- Delete the Existing Public Key
Remove the existing public key file namedgithub-action-key.pub
:rm github-action-key.pub
- Generate a New SSH Key Pair
Run the following command to generate a new SSH key pair. When prompted, enter the namegithub-action-key
:ssh-keygen -t rsa -b 4096 -C "github-action-key" -f github-action-key
This creates:github-action-key.pub
(public key)github-action-key
(private key)
that will be used later by GitHub Actions.
- Important Note:
If you choose a different name when runningssh-keygen
, then make sure to update the variable name in the Terraform variable file located at:"terraform/terraform_resources/variable.tf"
Storing Secrets in Your GitHub Repository
What Are GitHub Secrets?
GitHub Secrets are encrypted environment variables that you can add to your repository. These secrets can be referenced in your GitHub Actions workflows without exposing the actual values in your code, logs, or configuration files. They’re essential for:
- Authenticating with cloud providers (e.g., AWS).
- Logging into container registries (e.g., Docker Hub).
- Securely connecting to remote servers via SSH.
Step-by-Step Guide to Adding Secrets:
- Navigate to Repository Settings:
- Go to your repository on GitHub.
- Click on Settings > Secrets and variables > Actions > New repository secret.
For each credential listed below, click on the New Repository secret button. You’ll be prompted to enter a name and the corresponding value. Here’s what you need to add:
- AWS_ACCESS_KEY_ID
- Description: The access key used by the AWS CLI and Terraform to authenticate your AWS account.
- How to Obtain:
- Log in to the AWS Management Console.
- Navigate to IAM (Identity and Access Management).
- Create a new user or use an existing one with the following permissions:
AdministratorAccess
AmazonDynamoDBFullAccess
AmazonS3FullAccess
- Copy the Access Key ID provided.
- Action: Name the secret
AWS_ACCESS_KEY_ID
and paste the copied key value.
- AWS_SECRET_ACCESS_KEY
- Description: The secret access key that pairs with the AWS access key to authenticate requests.
- How to Obtain:
- This key is provided alongside the Access Key ID when setting up an IAM user.
- Action: Name the secret
AWS_SECRET_ACCESS_KEY
and paste the secret key value.
- DOCKER_USERNAME
- Description: Your Docker Hub username, required for authenticating against Docker Hub.
- Action: Name the secret
DOCKER_USERNAME
and enter your Docker Hub username.
- DOCKER_PASSWORD
- Description: A secure Docker Hub password in the form of a Personal Access Token (PAT) rather than your standard account password.
- How to Generate a PAT:
- Log in to Docker Hub.
- Go to your account settings and navigate to the security or tokens section.
- Generate a new token.
- Action: Name the secret
DOCKER_PASSWORD
and paste the token.
- EC2_SSH_PRIVATE_KEY
- Description: The private SSH key used by GitHub Actions to securely connect to your EC2 instance.
- How to Obtain:
- Locate the private key file (
github-action-key
) generated earlier in theterraform/terraform_resources
directory. - Open the file in a text editor and copy the entire content, ensuring no extra whitespace is added.
- Locate the private key file (
- Action: Name the secret
EC2_SSH_PRIVATE_KEY
and paste the private key content.
- AWS_DYNAMODB_TABLE
- Description: The name of the DynamoDB table that Terraform will use as part of the remote backend.
- How to Determine:
- Open the file
terraform/terraform_backend/variable.tf
and locate the variable that defines the DynamoDB table name.
- Open the file
- Action: Name the secret
AWS_DYNAMODB_TABLE
and paste the table name value.
- AWS_S3_BUCKET
- Description: The name of the S3 bucket used by Terraform for storing the remote backend state.
- How to Determine:
- Similar to the DynamoDB table, open
terraform/terraform_backend/variable.tf
to find the bucket name.
- Similar to the DynamoDB table, open
- Action: Name the secret
AWS_S3_BUCKET
and paste the bucket name value.Tip: Ensure the bucket name is unique. You might create the table first and then delete it to verify that the bucket isn’t already in use.
- MAIL_FROM :Description: The sender email address used in your application for sending emails.How to Obtain:
- Use the email address that will send outgoing emails.
- If using a third-party email provider (e.g., Gmail, Outlook, SMTP services), ensure the email is configured correctly.
Action:
- Name the secret MAIL_FROM and enter the sender’s email address.
- MAIL_USERNAME :Description: The username used for authenticating the mail server (usually the email address or an account ID).How to Obtain:
- If using Gmail, it’s your full email address.
- If using an SMTP provider (like SendGrid, AWS SES, or Mailgun), refer to their dashboard for the username.
Action:
- Name the secret MAIL_USERNAME and enter the appropriate username.
- MAIL_PASSWORD :Description: The password or API key used to authenticate the email service provider.How to Obtain:
- If using Gmail, generate an App Password:
- Go to Google Account Security.
- Enable 2-Step Verification (if not already enabled).
- Under App Passwords, generate a new password for SMTP.
- If using another SMTP service (e.g., SendGrid, AWS SES), generate an API key from their dashboard.
- If using Gmail, generate an App Password:
Action:
- Name the secret MAIL_PASSWORD and paste the password or API key.

Make Your Branch the Default Branch
Note:
Make your branch (the one with GitHub Actions workflows) the default branch in your GitHub settings. This is necessary because, otherwise, the workflow fordestroy.yml
may not show up (the exact reason is unclear, but this is a known workaround).
Understanding the GitHub Workflows
This repository contains two important workflows:
- main.yml:
name: CI/CD Pipeline for Online Shop
# Trigger the workflow on pushes to the 'github-action' branch.
on:
push:
branches:
- github-action
jobs:
###########################################################################
# Job 1: Configure Terraform Backend
###########################################################################
terraform-backend:
name: Configure Terraform Backend
runs-on: ubuntu-latest
steps:
# Step 1: Checkout the repository.
- name: Checkout Repository
uses: actions/checkout@v3
# Step 2: Setup Terraform CLI.
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: latest
# Step 3: Verify AWS CLI installation.
- name: Check AWS CLI Version
run: aws --version
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# Step 4: Configure AWS Credentials.
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
# Step 5: Test AWS Configuration by listing S3 buckets.
- name: Testing Configuration
run: aws s3 ls
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# Step 6: Check if the S3 Bucket exists.
- name: Check if S3 Bucket Exists
id: check_bucket
run: |
if aws s3 ls "s3://${{ secrets.AWS_S3_BUCKET }}" 2>&1 | grep -q 'NoSuchBucket'; then
echo "CREATE_BACKEND=true" >> $GITHUB_ENV
else
echo "CREATE_BACKEND=false" >> $GITHUB_ENV
fi
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# Step 7: Initialize the Terraform backend.
- name: Initialize Backend
run: terraform init
working-directory: terraform/terraform_backend
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# Step 8: Apply the Terraform backend configuration.
- name: Apply Backend Configuration
run: terraform apply --auto-approve -var="create_backend=$CREATE_BACKEND"
working-directory: terraform/terraform_backend
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
###########################################################################
# Job 2: Provision Infrastructure Resources
###########################################################################
terraform-resources:
name: Provision Resources
runs-on: ubuntu-latest
needs: terraform-backend
outputs:
ec2_public_ip: ${{ steps.get-ec2-ip.outputs.ec2_ip }}
steps:
# Checkout the repository.
- name: Checkout Repository
uses: actions/checkout@v3
# Setup Terraform CLI.
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: latest
# Verify AWS CLI installation.
- name: Check AWS CLI Version
run: aws --version
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# Configure AWS Credentials.
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
# Initialize Terraform using the remote backend.
- name: Initialize Resources with Backend
run: terraform init
working-directory: terraform/terraform_resources
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# Execute Terraform plan to review changes.
- name: Terraform Plan
run: terraform plan
working-directory: terraform/terraform_resources
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# Apply the Terraform changes.
- name: Apply Terraform Changes
run: terraform apply --auto-approve
working-directory: terraform/terraform_resources
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# Capture the EC2 instance's public IP from Terraform outputs.
- name: Get EC2 Public IP
id: get-ec2-ip
run: echo "ec2_ip=$(terraform output -raw instance_public_ip)" >> $GITHUB_OUTPUT
working-directory: terraform/terraform_resources
###########################################################################
# Job 3: Build & Push Docker Image to DockerHub
###########################################################################
docker:
name: Build & Push Docker Image
runs-on: ubuntu-latest
needs: terraform-resources
steps:
# Checkout the repository.
- name: Checkout Repository
uses: actions/checkout@v3
# Log in to DockerHub using credentials stored in GitHub Secrets.
- name: Log in to DockerHub
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
# Build the Docker image for the online shop.
- name: Build Docker Image
run: docker build -t ${{ secrets.DOCKER_USERNAME }}/online_shop:latest .
# Push the built image to DockerHub.
- name: Push Docker Image to DockerHub
run: docker push ${{ secrets.DOCKER_USERNAME }}/online_shop:latest
###########################################################################
# Job 4: Deploy the Application on EC2 Instance
###########################################################################
deploy:
name: Deploy on EC2
runs-on: ubuntu-latest
needs: [terraform-resources, docker]
steps:
# Checkout the repository.
- name: Checkout Repository
uses: actions/checkout@v3
# Step 1: Update the system on the EC2 instance via SSH.
- name: Update System
env:
EC2_PUBLIC_IP: ${{ needs.terraform-resources.outputs.ec2_public_ip }}
SSH_PRIVATE_KEY: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
run: |
echo "$SSH_PRIVATE_KEY" > private_key.pem
chmod 600 private_key.pem
ssh -o StrictHostKeyChecking=no -i private_key.pem ubuntu@$EC2_PUBLIC_IP "sudo apt update -y"
# Step 2: Install Docker on the EC2 instance.
- name: Install Docker
env:
EC2_PUBLIC_IP: ${{ needs.terraform-resources.outputs.ec2_public_ip }}
SSH_PRIVATE_KEY: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
run: |
echo "$SSH_PRIVATE_KEY" > private_key.pem
chmod 600 private_key.pem
ssh -o StrictHostKeyChecking=no -i private_key.pem ubuntu@$EC2_PUBLIC_IP "sudo apt install docker.io -y && sudo usermod -aG docker ubuntu"
# Step 3: Deploy the Docker container on the EC2 instance.
- name: Deploy Container
env:
EC2_PUBLIC_IP: ${{ needs.terraform-resources.outputs.ec2_public_ip }}
SSH_PRIVATE_KEY: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: |
echo "$SSH_PRIVATE_KEY" > private_key.pem
chmod 600 private_key.pem
ssh -o StrictHostKeyChecking=no -i private_key.pem ubuntu@$EC2_PUBLIC_IP "
sudo docker stop online_shop || true
sudo docker rm online_shop || true
sudo docker pull ${DOCKER_USERNAME}/online_shop:latest
sudo docker run -d -p 3000:3000 --name online_shop ${DOCKER_USERNAME}/online_shop:latest
"
###########################################################################
# Job 5: Send Notification Email (NEW)
###########################################################################
notify:
name: Send Notification Email
runs-on: ubuntu-latest
needs: [terraform-backend, terraform-resources, docker, deploy]
if: always() # Ensure this job runs regardless of previous outcomes
steps:
# Step to determine overall pipeline status
- name: Determine overall pipeline status
id: pipeline-status
run: |
# Check if all required jobs succeeded
if [[ "${{ needs.terraform-backend.result }}" == "success" ]] \
&& [[ "${{ needs.terraform-resources.result }}" == "success" ]] \
&& [[ "${{ needs.docker.result }}" == "success" ]] \
&& [[ "${{ needs.deploy.result }}" == "success" ]]; then
echo "result=Success ✅" >> $GITHUB_OUTPUT
else
echo "result=Failed ❌" >> $GITHUB_OUTPUT
fi
# Step to send notification email
- name: Send Email
uses: hilarion5/send-mail@v1
with:
smtp-server: smtp.gmail.com
smtp-port: 465
smtp-secure: true
from-email: ${{ secrets.MAIL_FROM }}
to-email: <RECEIVER_EMAIL_ADDRESS>,<OHTER_RECEIVER_EMAIL_ADDRESS> # Add multiple email addresses separated by commas
username: ${{ secrets.MAIL_USERNAME }}
password: ${{ secrets.MAIL_PASSWORD }}
subject: "CI/CD Pipeline Notification: ${{ github.workflow }} - ${{ steps.pipeline-status.outputs.result }}"
body: ""
html: |
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 8px; background-color: #ffffff;">
<h2 style="color: #24292e; text-align: center;">🚀 CI/CD Pipeline Notification</h2>
<div style="background-color: #f6f8fa; padding: 16px; border-radius: 6px;">
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Workflow</strong></td>
<td style="padding: 10px; border-bottom: 1px solid #ddd;">${{ github.workflow }}</td>
</tr>
<tr>
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Triggered by</strong></td>
<td style="padding: 10px; border-bottom: 1px solid #ddd;">${{ github.actor }}</td>
</tr>
<tr>
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Repository</strong></td>
<td style="padding: 10px; border-bottom: 1px solid #ddd;">${{ github.repository }}</td>
</tr>
<tr>
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Run Details</strong></td>
<td style="padding: 10px; border-bottom: 1px solid #ddd;">
<a href="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" style="color: #0366d6; text-decoration: none;">View Run #${{ github.run_id }}</a>
</td>
</tr>
</table>
</div>
<h3 style="margin-top: 16px;">🛠 Job Statuses</h3>
<table style="width: 100%; border-collapse: collapse; background-color: #fff;">
<tr style="background-color: #f6f8fa;">
<th style="padding: 10px; text-align: left;">Job</th>
<th style="padding: 10px; text-align: center;">Status</th>
</tr>
<tr>
<td style="padding: 10px;">Terraform Backend</td>
<td style="padding: 10px; text-align: center; color: white; background-color:
${{ (needs.terraform-backend.result == 'success' && '#28a745') || '#d73a49' }}; border-radius: 4px;">
${{ needs.terraform-backend.result }}
</td>
</tr>
<tr>
<td style="padding: 10px;">Terraform Resources</td>
<td style="padding: 10px; text-align: center; color: white; background-color:
${{ (needs.terraform-resources.result == 'success' && '#28a745') || '#d73a49' }}; border-radius: 4px;">
${{ needs.terraform-resources.result }}
</td>
</tr>
<tr>
<td style="padding: 10px;">Docker Build</td>
<td style="padding: 10px; text-align: center; color: white; background-color:
${{ (needs.docker.result == 'success' && '#28a745') || '#d73a49' }}; border-radius: 4px;">
${{ needs.docker.result }}
</td>
</tr>
<tr>
<td style="padding: 10px;">Deployment</td>
<td style="padding: 10px; text-align: center; color: white; background-color:
${{ (needs.deploy.result == 'success' && '#28a745') || '#d73a49' }}; border-radius: 4px;">
${{ needs.deploy.result }}
</td>
</tr>
</table>
<p style="color: #6a737d; font-size: 0.9em; margin-top: 20px; text-align: center;">
This email was sent automatically by <strong>GitHub Actions</strong>.
</p>
</div>
What it Does:
This workflow will:
- Trigger:
The workflow runs on any push to the github-action branch. - Job 1: terraform-backend
Configures the remote Terraform backend by:- Checking out the repo.
- Setting up Terraform and verifying the AWS CLI.
- Configuring AWS credentials.
- Checking if the S3 bucket exists (to decide if backend creation is needed).
- Initializing and applying the Terraform backend configuration.
- Job 2: terraform-resources
Provisions your infrastructure by:- Checking out the code and setting up Terraform.
- Initializing Terraform with the remote backend.
- Running Terraform plan and apply.
- Capturing the EC2 instance’s public IP for later use.
- Job 3: docker
Builds and pushes the Docker image for your online shop by:- Logging into DockerHub.
- Building the image.
- Pushing it to your DockerHub repository.
- Job 4: deploy
Deploys the application on the EC2 instance by:- SSH-ing into the instance.
- Updating the system and installing Docker.
- Stopping any existing container, pulling the latest image, and running it.
- Job 5: notify
Sends a notification email summarizing the pipeline results, regardless of success or failure, using an external action.
- How to Access the Application:
Once the workflow completes, simply take the public IP of the instance and open it in your browser at: You can check on AWS console on eu-west-1 region for EC2 ip.http://<Instance_Public_Ip>:3000
- destroy.yml
name: Destroy All Infrastructure
on:
workflow_dispatch:
inputs:
confirm:
description: "Type 'destroy' to confirm"
required: true
jobs:
destroy-resources:
name: Destroy Application Resources
runs-on: ubuntu-latest
steps:
#####################################################################
# Step 1: Checkout, Setup Terraform, and Configure AWS Credentials
#####################################################################
- name: Checkout Repository
uses: actions/checkout@v3
with:
ref: github-action
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: latest
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
#####################################################################
# Step 2: Initialize Terraform (Application Resources)
#####################################################################
- name: Initialize Terraform (Application Resources)
run: terraform init
working-directory: terraform/terraform_resources
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: eu-west-1
# TF_LOG: DEBUG
#####################################################################
# Step 3: Destroy Application Resources (only if confirmed)
#####################################################################
- name: Destroy Application Resources
if: ${{ github.event.inputs.confirm == 'destroy' }}
run: terraform destroy --auto-approve
working-directory: terraform/terraform_resources
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: eu-west-1
# TF_LOG: DEBUG
#####################################################################
# (Optional) Post-Destruction Cleanup Step
# Add any cleanup commands if needed here.
#####################################################################
- name: Cleanup Temporary Files (if any)
if: ${{ github.event.inputs.confirm == 'destroy' }}
run: echo "No temporary files to cleanup."
destroy-backend:
name: Destroy Backend Resources
runs-on: ubuntu-latest
needs: destroy-resources
steps:
#####################################################################
# Step 1: Checkout, Setup Terraform, and Configure AWS Credentials
#####################################################################
- name: Checkout Repository
uses: actions/checkout@v3
with:
ref: github-action
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: latest
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
#####################################################################
# Step 2: Initialize Terraform (Backend Resources) and Debug Connectivity
#####################################################################
- name: Initialize Terraform (Backend Resources)
run: terraform init
working-directory: terraform/terraform_backend
env:
TF_VAR_create_backend: "true"
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: eu-west-1
# TF_LOG: DEBUG
# Debug: List available S3 buckets to verify connectivity.
- name: Debug - List S3 Buckets
run: aws s3 ls
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: eu-west-1
# Debug: List available DynamoDB tables to verify connectivity.
- name: Debug - List DynamoDB Tables
run: aws dynamodb list-tables
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: eu-west-1
#####################################################################
# Step 3: Import, Empty, and Destroy Backend Resources (only if confirmed)
#####################################################################
- name: Import S3 Bucket (if exists)
if: ${{ github.event.inputs.confirm == 'destroy' }}
continue-on-error: true
run: terraform import 'aws_s3_bucket.terraform_aws_s3_bucket[0]' ${{ secrets.AWS_S3_BUCKET }}
working-directory: terraform/terraform_backend
env:
TF_VAR_create_backend: "true"
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: eu-west-1
# TF_LOG: DEBUG
- name: Import DynamoDB Table (if exists)
if: ${{ github.event.inputs.confirm == 'destroy' }}
continue-on-error: true
run: terraform import 'aws_dynamodb_table.terraform_aws_db[0]' ${{ secrets.AWS_DYNAMODB_TABLE }}
working-directory: terraform/terraform_backend
env:
TF_VAR_create_backend: "true"
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: eu-west-1
# TF_LOG: DEBUG
- name: Empty S3 Bucket
if: ${{ github.event.inputs.confirm == 'destroy' }}
run: aws s3 rm s3://${{ secrets.AWS_S3_BUCKET }} --recursive
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: eu-west-1
- name: Destroy Backend Resources
if: ${{ github.event.inputs.confirm == 'destroy' }}
run: terraform destroy --auto-approve
working-directory: terraform/terraform_backend
env:
TF_VAR_create_backend: "true"
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: eu-west-1
# TF_LOG: DEBUG
send-notification:
name: Send Email Notification
runs-on: ubuntu-latest
needs: [destroy-resources, destroy-backend]
if: always() # Runs regardless of previous job outcomes
steps:
# 1) Determine overall pipeline status
- name: Determine Pipeline Status
id: pipeline-status
run: |
# We'll check the results of the two jobs we "need"
# and create an output variable "result" accordingly.
if [ "${{ needs.destroy-resources.result }}" = "success" ] && [ "${{ needs.destroy-backend.result }}" = "success" ]; then
echo "result=Success ✅" >> $GITHUB_OUTPUT
else
echo "result=Failed ❌" >> $GITHUB_OUTPUT
fi
- name: Send Email
uses: hilarion5/send-mail@v1
with:
smtp-server: smtp.gmail.com
smtp-port: 465
smtp-secure: true
from-email: ${{ secrets.MAIL_FROM }}
to-email: <RECEIVER_EMAIL_ADDRESS>,<OHTER_RECEIVER_EMAIL_ADDRESS> # Add multiple email addresses separated by commas
username: ${{ secrets.MAIL_USERNAME }}
password: ${{ secrets.MAIL_PASSWORD }}
subject: "Destroy Workflow Notification: ${{ github.workflow }} - ${{ steps.pipeline-status.outputs.result }}"
body: ""
html: |
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 8px; background-color: #ffffff;">
<h2 style="color: #24292e; text-align: center;">🔥 Destroy Workflow Notification</h2>
<div style="background-color: #f6f8fa; padding: 16px; border-radius: 6px;">
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Workflow</strong></td>
<td style="padding: 10px; border-bottom: 1px solid #ddd;">${{ github.workflow }}</td>
</tr>
<tr>
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Triggered by</strong></td>
<td style="padding: 10px; border-bottom: 1px solid #ddd;">${{ github.actor }}</td>
</tr>
<tr>
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Repository</strong></td>
<td style="padding: 10px; border-bottom: 1px solid #ddd;">${{ github.repository }}</td>
</tr>
<tr>
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Run Details</strong></td>
<td style="padding: 10px; border-bottom: 1px solid #ddd;">
<a href="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" style="color: #0366d6; text-decoration: none;">
View Run #${{ github.run_id }}
</a>
</td>
</tr>
</table>
</div>
<h3 style="margin-top: 16px;">🛠 Job Statuses</h3>
<table style="width: 100%; border-collapse: collapse; background-color: #fff;">
<tr style="background-color: #f6f8fa;">
<th style="padding: 10px; text-align: left;">Job</th>
<th style="padding: 10px; text-align: center;">Status</th>
</tr>
<tr>
<td style="padding: 10px;">Destroy Application Resources</td>
<td style="padding: 10px; text-align: center; color: white; background-color: ${{ needs.destroy-resources.result == 'success' && '#28a745' || '#d73a49' }}; border-radius: 4px;">
${{ needs.destroy-resources.result }}
</td>
</tr>
<tr>
<td style="padding: 10px;">Destroy Backend Resources</td>
<td style="padding: 10px; text-align: center; color: white; background-color: ${{ needs.destroy-backend.result == 'success' && '#28a745' || '#d73a49' }}; border-radius: 4px;">
${{ needs.destroy-backend.result }}
</td>
</tr>
</table>
<p style="color: #6a737d; font-size: 0.9em; margin-top: 20px; text-align: center;">
This email was sent automatically by <strong>GitHub Actions</strong>.
</p>
</div>
Making a Change and Pushing to Your Branch
To test the CI/CD pipeline:
- Make a Change
Edit any file in your repository to trigger the workflow (this could be as simple as a comment change or an update to the code). - Push Your Change
After making your changes, commit and push them to your branch:CopyCopygit add .
git commit -m "Made a change to test CI/CD workflow"
git push origin github-action
- Observe the Workflow Trigger
Upon pushing, you will see that the workflow is automatically triggered on the push to your repository. Navigate to the “Actions” tab in GitHub to monitor the progress. - Enjoy the Efficiency
With this setup, you’re all set to save time on every subsequent push! - Email Notification on Pipeline Success:
- Email Notification on Pipeline Failure:
Destroying All Infrastructure on a Single Click
Once you’re done testing or want to avoid incurring extra costs, you can quickly tear down the entire infrastructure with a single click.
- Go to the Destroy All Infrastructure Workflow
In the GitHub “Actions” tab, locate thedestroy.yml
workflow. - Run the Workflow Manually
Click on “Run workflow” and inputdestroy
when prompted, as the workflow must be triggered manually. - Automatic Deletion
The workflow will automatically delete all the resources (EC2 instance, remote backend (S3 and DynamoDB), security groups, keys, etc.) within a couple of minutes. - Pipeline Success Email Notification:
- Pipeline Failure Email Notification:
Verification and Final Thoughts
After running your workflows, you can verify the deployed resources:
- Check that your online shop application is accessible by navigating to
http://<Instance_Public_Ip>:3000
in your browser. - Verify that all infrastructure components are in place during deployment.
- EC2 :
- S3 Bucket :
- DynamoDB :
- EC2 :
- When running the destroy workflow, ensure that all resources are properly deleted.
Check out this Video to see how it works :
Good luck, try it out, and happy coding!
By following this detailed guide, you have successfully set up a CI/CD pipeline that automates both the deployment and teardown of your online shop application. Every single step—from forking the repository to generating SSH keys, setting GitHub secrets, and finally running the workflows—is covered to ensure a seamless experience.
You can reach out to me on LinkedIn.
Author: Amitabh Soni