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

  1. Fork and Clone the Repository
    1. Visit the Original Repository:
      Go to the online_shop GitHub repository.
    2. Fork the Repository:
      Click the “Fork” button to create your own copy in your GitHub account.
  2. Open VsCode or any Code Editor
    Open your preferred code editor (such as VSCode) to work with the code locally.
  3. 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
  4. 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.

  1. Navigate to the Terraform Resources Directory
    Change directory to the Terraform resources folder:cd terraform/terraform_resources
  2. Delete the Existing Public Key
    Remove the existing public key file named github-action-key.pub:rm github-action-key.pub
  3. Generate a New SSH Key Pair
    Run the following command to generate a new SSH key pair. When prompted, enter the name github-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.

  1. Important Note:
    If you choose a different name when running ssh-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:

  1. 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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 the terraform/terraform_resources directory.
      • Open the file in a text editor and copy the entire content, ensuring no extra whitespace is added.
    • Action: Name the secret EC2_SSH_PRIVATE_KEY and paste the private key content.
  6. 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.
    • Action: Name the secret AWS_DYNAMODB_TABLE and paste the table name value.
  7. 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.
    • 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.
  8. MAIL_FROM :Description: The sender email address used in your application for sending emails.How to Obtain:
    1. Use the email address that will send outgoing emails.
    2. 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.
  1. MAIL_USERNAME :Description: The username used for authenticating the mail server (usually the email address or an account ID).How to Obtain:
    1. If using Gmail, it’s your full email address.
    2. 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.
  1. MAIL_PASSWORD :Description: The password or API key used to authenticate the email service provider.How to Obtain:
    1. 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.
    2. If using another SMTP service (e.g., SendGrid, AWS SES), generate an API key from their dashboard.

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 for destroy.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:

  1. 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.
  1. 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
  2. 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:

  1. 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).
  2. 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
  3. 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.
  4. Enjoy the Efficiency
    With this setup, you’re all set to save time on every subsequent push!
  5. Email Notification on Pipeline Success:
  6. 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.

  1. Go to the Destroy All Infrastructure Workflow
    In the GitHub “Actions” tab, locate the destroy.yml workflow.
  2. Run the Workflow Manually
    Click on “Run workflow” and input destroy when prompted, as the workflow must be triggered manually.
  3. 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.
  4. Pipeline Success Email Notification:
  5. 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 :
  • 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

Categorized in:

Blog,