# 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.](https://github.com/Amitabh-DevOps/online_shop)
        
    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:
    
    ```bash
    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:
    
    ```bash
    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:
    
    ```bash
    cd terraform/terraform_resources
    ```
    
2. **Delete the Existing Public Key**  
    Remove the existing public key file named `github-action-key.pub`:
    
    ```bash
    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`:
    
    ```bash
    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.
    
4. **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 &gt; Secrets and variables &gt; Actions &gt; 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.
        
    * **Also, verify that** `terraform/terraform_resources/terraform.tf` it contains the same Table name.
        
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.
        > 
        > **Also, verify that** `terraform/terraform_resources/terraform.tf` it contains the same Bucket name.
        
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.
        
9. **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.
        
10. **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.
        

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1739680705514/95606c75-ebb6-415d-9ca0-11655b43055e.png align="center")

---

## 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

> Make sure to add Original Receiver Email address in both YML file at `to-email: <RECEIVER_EMAIL_ADDRESS>,<OHTER_RECEIVER_EMAIL_ADDRESS>`

This repository contains two important workflows:

1. **main.yml:**
    
    ```yaml
    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: SMART BACKEND DETECTION - Check if S3 Bucket exists for Terraform backend
          # This prevents errors when trying to create resources that already exist
          - name: Check if S3 Bucket Exists
            id: check_bucket
            run: |
              echo "🔍 Checking if S3 bucket exists for Terraform backend..."
              
              # Try to list the S3 bucket contents
              # 2>&1 redirects stderr (error messages) to stdout so we can capture them
              # grep -q searches for 'NoSuchBucket' error message quietly (no output, just exit code)
              if aws s3 ls "s3://${{ secrets.AWS_S3_BUCKET }}" 2>&1 | grep -q 'NoSuchBucket'; then
                # BUCKET DOESN'T EXIST - Set CREATE_BACKEND=true
                # This tells Terraform: "Please create the S3 bucket and DynamoDB table"
                echo "CREATE_BACKEND=true" >> $GITHUB_ENV
                echo "🔧 S3 bucket not found. Will create backend resources."
              else
                # BUCKET EXISTS - Set CREATE_BACKEND=false  
                # This tells Terraform: "Skip creating backend resources, they already exist"
                echo "CREATE_BACKEND=false" >> $GITHUB_ENV
                echo "✅ S3 bucket found. Will use existing backend resources."
              fi
              
              echo "🎯 CREATE_BACKEND variable will be passed to Terraform as var.create_backend"
            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 Terraform backend configuration with conditional logic
          # The CREATE_BACKEND variable from Step 6 is automatically available here
          # Terraform will use it in: count = var.create_backend ? 1 : 0
          - name: Apply Backend Configuration
            run: |
              echo "🚀 Applying Terraform backend configuration..."
              echo "📝 CREATE_BACKEND value: $CREATE_BACKEND"
              echo "🎯 This will be passed to Terraform as var.create_backend"
              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 (Updated with SSH Action)
      ###########################################################################
      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 Action
          - name: Update System
            uses: appleboy/ssh-action@v0.1.10
            with:
              host: ${{ needs.terraform-resources.outputs.ec2_public_ip }}
              username: ubuntu
              key: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
              script: |
                sudo apt update -y
                echo "✅ System updated successfully"
    
          # Step 2: Install Docker on the EC2 instance
          - name: Install Docker
            uses: appleboy/ssh-action@v0.1.10
            with:
              host: ${{ needs.terraform-resources.outputs.ec2_public_ip }}
              username: ubuntu
              key: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
              script: |
                # Install Docker if not already installed
                if ! command -v docker &> /dev/null; then
                  echo "🔧 Installing Docker..."
                  sudo apt install docker.io -y
                  sudo usermod -aG docker ubuntu
                  echo "✅ Docker installed successfully"
                else
                  echo "✅ Docker already installed"
                fi
    
          # Step 3: Deploy the Docker container on the EC2 instance
          - name: Deploy Container
            uses: appleboy/ssh-action@v0.1.10
            with:
              host: ${{ needs.terraform-resources.outputs.ec2_public_ip }}
              username: ubuntu
              key: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
              script: |
                echo "🚀 Starting application deployment..."
                
                # Stop and remove existing container if it exists
                sudo docker stop online_shop || true
                sudo docker rm online_shop || true
                
                # Pull the latest image
                echo "📥 Pulling latest Docker image..."
                sudo docker pull ${{ secrets.DOCKER_USERNAME }}/online_shop:latest
                
                # Run the new container
                echo "🏃 Starting new container..."
                sudo docker run -d -p 3000:3000 --name online_shop ${{ secrets.DOCKER_USERNAME }}/online_shop:latest
                
                # Verify deployment
                sleep 5
                if sudo docker ps | grep -q online_shop; then
                  echo "✅ Application deployed successfully!"
                  echo "🌐 Application is running on http://$(curl -s ifconfig.me):3000"
                else
                  echo "❌ Deployment failed!"
                  exit 1
                fi
    
      ###########################################################################
      # 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 <mark>eu-west-1</mark> 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**
    
    ```yaml
    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
    
            # IMPORTANT: TF_VAR_create_backend="true" forces Terraform to recognize backend resources exist
            # Without this, Terraform thinks create_backend=false (default) → count=0 → "no resources to destroy"
            # With this, Terraform knows create_backend=true → count=1 → "found resources, destroying them"
            env:
              TF_VAR_create_backend: "true" # This is the magic line for proper cleanup!
              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
              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**  
    We are going to update the link to GitHub in Codebase and test whether the CI/CD actually works or not
    
    I am talking about this GitHub link in the NavBar:
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1751957591196/a2aac55c-c6aa-497a-82f2-aabcd5a5e7a5.png align="center")
    
2. Open any IDE, such as VS Code, or you can use GitHub as well.
    
3. Go to this file: `online_shop\src\components\Navbar.jsx`
    
4. Update this link with any of the links that you want to show:
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1751957725027/32d5fe21-9c4d-4a69-abac-ab6e27a75e3f.png align="center")
    
5. **Push Your Change**  
    After making your changes, commit and push them to your branch:
    
    ```bash
    git add .
    git commit -m "Made a change to test CI/CD workflow"
    git push origin github-action
    ```
    
6. **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.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1739682276746/d0c3f72d-7199-4897-9319-382366c9d437.png align="center")
    
7. After updating the commit and pushing the changes, when the workflow is completed, you can check by clicking on that **GitHub icon** in your deployed **online shop** application, which will redirect to the link that you have updated
    
8. If you are a developer, you can check by updating anything in the codebase. It is for those who do not actually know development
    
9. **Enjoy the Efficiency**  
    With this setup, you’re all set to save time on every subsequent push!
    
10. **<mark>Email Notification on Pipeline Success:</mark>**
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1739682347611/64465fc7-b18c-4db9-917c-67a3a7f2b9df.png align="center")
    
11. <mark>Email Notification on Pipeline Failure:</mark>
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1739682443624/cee80b48-df69-46aa-b073-070b3a80220c.png align="center")
    

---

## 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.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1739374357069/60d65c51-d0a1-470f-84fd-67b0f4f25ee7.png align="center")
    
* Verify that all infrastructure components are in place during deployment.
    
    * EC2 :
        
        ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1739374430202/1782d134-08d4-4a0d-95a3-7a272ecb5fce.png align="center")
        
    * S3 Bucket :
        
        ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1739374442937/4a803b48-4614-4c55-a4a3-4ada5a6cfe2b.png align="center")
        
    * DynamoDB :
        
        ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1739374460812/9760087c-4e98-4b52-955d-412e81763620.png align="center")
        
* When running the destroy workflow, ensure that all resources are properly deleted.
    

---

## Let’s verify using CLI

As our application is up and running, let’s try to connect the instance using cli and check whether the Docker container is running.

1. Go to `terraform/terraform_resources`
    
2. You can see both `Public and Private` key.
    
3. Copy the `Public Ip` For instance, from the **AWS Console.**
    
4. As we already know that, before connecting to the instance, we need to change the permission of the private key, so use the below command for that:
    
    ```bash
    chmod 400 github-action-key
    ```
    
5. Try to do `ssh` With that private key(which does not have an extension `.pub`)
    
    ```bash
    ssh -i github-action-key ubuntu@<Instance_Public_Ip>
    ```
    
6. Congrats, you have successfully connected to that **EC2 Instance** that is created using Terraform and where your application, an online shop, is running.
    
7. Now, run this command to check whether the Docker container is running or not
    
    1. For Docker images:
        
        ```bash
        docker images
        ```
        
    2. For Docker container:
        
        ```bash
        docker ps
        ```
        
        Copy the Docker container ID.
        
8. You will see both Docker image and Docker container for the **Online Shop**
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1751448282661/aa43013c-37c2-4a08-9a1d-cc00a12174e5.png align="center")
    
9. Let’s go inside that Docker container, use the below command:
    
    ```bash
    docker exec -it <Docker_Container_ID> sh
    ```
    
10. Let’s check the HTML page of the application, for example
    
    1. type `ls` Inside a Docker container, you will see `dist` directory
        
    2. Run this command: `cat dist/index.html` This will print the HTML page of the **Online Shop** as below\*\*.\*\*
        
        ```xml
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8"/>
            <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
            <link rel="icon" type="image/svg+xml" href="./OS.svg" />
            <title>Online Shop</title>
        
            <link rel="preconnect" href="https://fonts.googleapis.com">
            <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
            <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"
                  rel="stylesheet">
        
            <meta name="author" content="Md. Afzal Hassan Ehsani">
            <meta name="description"
                  content="Online Shop is a user-friendly e-commerce solution. Easily add products to your cart, checkout securely, and receive instant order confirmation. Enjoy a seamless shopping experience with Online Shop.">
            <meta name="keywords"
                  content="online shopping, shopping cart, e-commerce, checkout, purchase, products, cart, buy, order, store, online store, shopping, retail, e-commerce platform, ecommerce, online shopping, online store, online shopping, online shopping software, online shopping solutions, online shopping systems, online shopping tools">
          <script type="module" crossorigin src="./assets/index-BeB3a16X.js"></script>
          <link rel="stylesheet" crossorigin href="./assets/index-BKulh_YO.css">
        </head>
        <body>
        <div id="root"></div>
        </body>
        </html
        ```
        
    3. Through this, we have verified that our setup and application are working properly.
        
    4. If you want, you can do further debugging.
        

---

## 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.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1739682617874/a900bcc4-ca2e-4a72-8961-aa19d35970a1.png align="center")
    
4. <mark>Pipeline Success Email Notification:</mark>
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1739682505830/23e27337-6a0b-4417-b2ff-0ee9a8d2eb32.png align="center")
    
5. **Pipeline Failure Email Notification:**
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1739682566516/3e7cba32-f0a8-447f-af14-343c3f880928.png align="center")
    

---

### **<mark>Check out this Video to see how it works :</mark>**

%[https://youtu.be/VzDWK_EgJ-4] 

**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](https://linkedin.com/in/amitabh-devops).
