Streamline Online Shop with GitHub Actions CICD

Streamline Online Shop with GitHub Actions CICD

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.

    • 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

      Note: There is no need to manually open the inbound rule from the AWS console as Terraform has already configured the instance to open port 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>
    
    • What it Does:

      Here's a concise explanation of your destroy.yml pipeline:

      • Trigger:

        • The workflow is manually triggered via workflow_dispatch.

        • It requires a confirmation input where you must type destroy to proceed.

      • Job 1: destroy-resources
        Destroys your application resources by:

        • Checking out the repository from the github-action branch.

        • Setting up Terraform and configuring AWS credentials.

        • Initializing Terraform in the terraform/terraform_resources directory.

        • Running terraform destroy to tear down application resources (e.g., EC2 instances) if confirmed.

        • Optionally performing cleanup of temporary files.

      • Job 2: destroy-backend
        Destroys your backend resources by:

        • Checking out the repository and setting up Terraform with AWS credentials.

        • Initializing Terraform in the terraform/terraform_backend directory and performing debug steps (listing S3 buckets and DynamoDB tables).

        • Importing existing backend resources (S3 bucket and DynamoDB table) if present.

        • Emptying the S3 bucket.

        • Running terraform destroy to remove the backend resources if confirmed.

      • Job 3: send-notification
        Sends an email notification by:

        • Determining the overall pipeline status based on the results of the destroy-resources and destroy-backend jobs.

        • Using an external email action to send a detailed summary of the workflow’s outcome, regardless of success or failure.

    • Manual Trigger:
      This workflow must be triggered manually. When you run it, you need to enter destroy as the input to start the deletion process.


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:

     git 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.