In this blog, we will set up a complete CI/CD pipeline for an online shop application using GitHub Actions. We’ll cover everything from forking the repository and setting up SSH keys to configuring GitHub secrets and triggering workflows that deploy and later destroy the infrastructure. This guide is designed to leave no stone unturned, so follow along carefully!
Repository Setup
Fork and Clone the Repository
Visit the Original Repository:
Go to the online_shop GitHub repository.Fork the Repository:
Click the "Fork" button to create your own copy in your GitHub account.
Open VsCode or any Code Editor
Open your preferred code editor (such as VSCode) to work with the code locally.Cloning the Repo
Clone your forked repository locally using the command below. Replace<YOUR_USERNAME>
with your GitHub username:git clone https://github.com/<YOUR_USERNAME>/online_shop
Switch to the "github-action" Branch
After cloning, navigate into the repository directory and switch to the branch that contains the CI/CD setup:cd online_shop git checkout github-action
SSH Key Setup in Terraform Resources
Before running our workflows, we need to set up a proper SSH key for secure communication with the EC2 instance.
Navigate to the Terraform Resources Directory
Change directory to the Terraform resources folder:cd terraform/terraform_resources
Delete the Existing Public Key
Remove the existing public key file namedgithub-action-key.pub
:rm github-action-key.pub
Generate a New SSH Key Pair
Run the following command to generate a new SSH key pair. When prompted, enter the namegithub-action-key
:ssh-keygen -t rsa -b 4096 -C "github-action-key" -f github-action-key
This creates:
github-action-key.pub
(public key)github-action-key
(private key)
that will be used later by GitHub Actions.
- Important Note:
If you choose a different name when runningssh-keygen
, then make sure to update the variable name in the Terraform variable file located at:
"terraform/terraform_resources/variable.tf"
Storing Secrets in Your GitHub Repository
What Are GitHub Secrets?
GitHub Secrets are encrypted environment variables that you can add to your repository. These secrets can be referenced in your GitHub Actions workflows without exposing the actual values in your code, logs, or configuration files. They’re essential for:
Authenticating with cloud providers (e.g., AWS).
Logging into container registries (e.g., Docker Hub).
Securely connecting to remote servers via SSH.
Step-by-Step Guide to Adding Secrets:
Navigate to Repository Settings:
Go to your repository on GitHub.
Click on Settings > Secrets and variables > Actions > New repository secret.
For each credential listed below, click on the New Repository secret button. You’ll be prompted to enter a name and the corresponding value. Here’s what you need to add:
AWS_ACCESS_KEY_ID
Description: The access key used by the AWS CLI and Terraform to authenticate your AWS account.
How to Obtain:
Log in to the AWS Management Console.
Navigate to IAM (Identity and Access Management).
Create a new user or use an existing one with the following permissions:
AdministratorAccess
AmazonDynamoDBFullAccess
AmazonS3FullAccess
Copy the Access Key ID provided.
Action: Name the secret
AWS_ACCESS_KEY_ID
and paste the copied key value.
AWS_SECRET_ACCESS_KEY
Description: The secret access key that pairs with the AWS access key to authenticate requests.
How to Obtain:
- This key is provided alongside the Access Key ID when setting up an IAM user.
Action: Name the secret
AWS_SECRET_ACCESS_KEY
and paste the secret key value.
DOCKER_USERNAME
Description: Your Docker Hub username, required for authenticating against Docker Hub.
Action: Name the secret
DOCKER_USERNAME
and enter your Docker Hub username.
DOCKER_PASSWORD
Description: A secure Docker Hub password in the form of a Personal Access Token (PAT) rather than your standard account password.
How to Generate a PAT:
Log in to Docker Hub.
Go to your account settings and navigate to the security or tokens section.
Generate a new token.
Action: Name the secret
DOCKER_PASSWORD
and paste the token.
EC2_SSH_PRIVATE_KEY
Description: The private SSH key used by GitHub Actions to securely connect to your EC2 instance.
How to Obtain:
Locate the private key file (
github-action-key
) generated earlier in theterraform/terraform_resources
directory.Open the file in a text editor and copy the entire content, ensuring no extra whitespace is added.
Action: Name the secret
EC2_SSH_PRIVATE_KEY
and paste the private key content.
AWS_DYNAMODB_TABLE
Description: The name of the DynamoDB table that Terraform will use as part of the remote backend.
How to Determine:
- Open the file
terraform/terraform_backend/variable.tf
and locate the variable that defines the DynamoDB table name.
- Open the file
Action: Name the secret
AWS_DYNAMODB_TABLE
and paste the table name value.
AWS_S3_BUCKET
Description: The name of the S3 bucket used by Terraform for storing the remote backend state.
How to Determine:
- Similar to the DynamoDB table, open
terraform/terraform_backend/variable.tf
to find the bucket name.
- Similar to the DynamoDB table, open
Action: Name the secret
AWS_S3_BUCKET
and paste the bucket name value.Tip: Ensure the bucket name is unique. You might create the table first and then delete it to verify that the bucket isn’t already in use.
MAIL_FROM :
Description: The sender email address used in your application for sending emails.
How to Obtain:
Use the email address that will send outgoing emails.
If using a third-party email provider (e.g., Gmail, Outlook, SMTP services), ensure the email is configured correctly.
Action:
- Name the secret MAIL_FROM and enter the sender's email address.
MAIL_USERNAME :
Description: The username used for authenticating the mail server (usually the email address or an account ID).
How to Obtain:
If using Gmail, it’s your full email address.
If using an SMTP provider (like SendGrid, AWS SES, or Mailgun), refer to their dashboard for the username.
Action:
- Name the secret MAIL_USERNAME and enter the appropriate username.
MAIL_PASSWORD :
Description: The password or API key used to authenticate the email service provider.
How to Obtain:
If using Gmail, generate an App Password:
Go to Google Account Security.
Enable 2-Step Verification (if not already enabled).
Under App Passwords, generate a new password for SMTP.
If using another SMTP service (e.g., SendGrid, AWS SES), generate an API key from their dashboard.
Action:
- Name the secret MAIL_PASSWORD and paste the password or API key.
Make Your Branch the Default Branch
Note:
Make your branch (the one with GitHub Actions workflows) the default branch in your GitHub settings. This is necessary because, otherwise, the workflow fordestroy.yml
may not show up (the exact reason is unclear, but this is a known workaround).
Understanding the GitHub Workflows
This repository contains two important workflows:
main.yml:
name: CI/CD Pipeline for Online Shop # Trigger the workflow on pushes to the 'github-action' branch. on: push: branches: - github-action jobs: ########################################################################### # Job 1: Configure Terraform Backend ########################################################################### terraform-backend: name: Configure Terraform Backend runs-on: ubuntu-latest steps: # Step 1: Checkout the repository. - name: Checkout Repository uses: actions/checkout@v3 # Step 2: Setup Terraform CLI. - name: Setup Terraform uses: hashicorp/setup-terraform@v3 with: terraform_version: latest # Step 3: Verify AWS CLI installation. - name: Check AWS CLI Version run: aws --version env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} # Step 4: Configure AWS Credentials. - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v2 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: eu-west-1 # Step 5: Test AWS Configuration by listing S3 buckets. - name: Testing Configuration run: aws s3 ls env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} # Step 6: Check if the S3 Bucket exists. - name: Check if S3 Bucket Exists id: check_bucket run: | if aws s3 ls "s3://${{ secrets.AWS_S3_BUCKET }}" 2>&1 | grep -q 'NoSuchBucket'; then echo "CREATE_BACKEND=true" >> $GITHUB_ENV else echo "CREATE_BACKEND=false" >> $GITHUB_ENV fi env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} # Step 7: Initialize the Terraform backend. - name: Initialize Backend run: terraform init working-directory: terraform/terraform_backend env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} # Step 8: Apply the Terraform backend configuration. - name: Apply Backend Configuration run: terraform apply --auto-approve -var="create_backend=$CREATE_BACKEND" working-directory: terraform/terraform_backend env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} ########################################################################### # Job 2: Provision Infrastructure Resources ########################################################################### terraform-resources: name: Provision Resources runs-on: ubuntu-latest needs: terraform-backend outputs: ec2_public_ip: ${{ steps.get-ec2-ip.outputs.ec2_ip }} steps: # Checkout the repository. - name: Checkout Repository uses: actions/checkout@v3 # Setup Terraform CLI. - name: Setup Terraform uses: hashicorp/setup-terraform@v3 with: terraform_version: latest # Verify AWS CLI installation. - name: Check AWS CLI Version run: aws --version env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} # Configure AWS Credentials. - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v2 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: eu-west-1 # Initialize Terraform using the remote backend. - name: Initialize Resources with Backend run: terraform init working-directory: terraform/terraform_resources env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} # Execute Terraform plan to review changes. - name: Terraform Plan run: terraform plan working-directory: terraform/terraform_resources env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} # Apply the Terraform changes. - name: Apply Terraform Changes run: terraform apply --auto-approve working-directory: terraform/terraform_resources env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} # Capture the EC2 instance's public IP from Terraform outputs. - name: Get EC2 Public IP id: get-ec2-ip run: echo "ec2_ip=$(terraform output -raw instance_public_ip)" >> $GITHUB_OUTPUT working-directory: terraform/terraform_resources ########################################################################### # Job 3: Build & Push Docker Image to DockerHub ########################################################################### docker: name: Build & Push Docker Image runs-on: ubuntu-latest needs: terraform-resources steps: # Checkout the repository. - name: Checkout Repository uses: actions/checkout@v3 # Log in to DockerHub using credentials stored in GitHub Secrets. - name: Log in to DockerHub run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin # Build the Docker image for the online shop. - name: Build Docker Image run: docker build -t ${{ secrets.DOCKER_USERNAME }}/online_shop:latest . # Push the built image to DockerHub. - name: Push Docker Image to DockerHub run: docker push ${{ secrets.DOCKER_USERNAME }}/online_shop:latest ########################################################################### # Job 4: Deploy the Application on EC2 Instance ########################################################################### deploy: name: Deploy on EC2 runs-on: ubuntu-latest needs: [terraform-resources, docker] steps: # Checkout the repository. - name: Checkout Repository uses: actions/checkout@v3 # Step 1: Update the system on the EC2 instance via SSH. - name: Update System env: EC2_PUBLIC_IP: ${{ needs.terraform-resources.outputs.ec2_public_ip }} SSH_PRIVATE_KEY: ${{ secrets.EC2_SSH_PRIVATE_KEY }} run: | echo "$SSH_PRIVATE_KEY" > private_key.pem chmod 600 private_key.pem ssh -o StrictHostKeyChecking=no -i private_key.pem ubuntu@$EC2_PUBLIC_IP "sudo apt update -y" # Step 2: Install Docker on the EC2 instance. - name: Install Docker env: EC2_PUBLIC_IP: ${{ needs.terraform-resources.outputs.ec2_public_ip }} SSH_PRIVATE_KEY: ${{ secrets.EC2_SSH_PRIVATE_KEY }} run: | echo "$SSH_PRIVATE_KEY" > private_key.pem chmod 600 private_key.pem ssh -o StrictHostKeyChecking=no -i private_key.pem ubuntu@$EC2_PUBLIC_IP "sudo apt install docker.io -y && sudo usermod -aG docker ubuntu" # Step 3: Deploy the Docker container on the EC2 instance. - name: Deploy Container env: EC2_PUBLIC_IP: ${{ needs.terraform-resources.outputs.ec2_public_ip }} SSH_PRIVATE_KEY: ${{ secrets.EC2_SSH_PRIVATE_KEY }} DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} run: | echo "$SSH_PRIVATE_KEY" > private_key.pem chmod 600 private_key.pem ssh -o StrictHostKeyChecking=no -i private_key.pem ubuntu@$EC2_PUBLIC_IP " sudo docker stop online_shop || true sudo docker rm online_shop || true sudo docker pull ${DOCKER_USERNAME}/online_shop:latest sudo docker run -d -p 3000:3000 --name online_shop ${DOCKER_USERNAME}/online_shop:latest " ########################################################################### # Job 5: Send Notification Email (NEW) ########################################################################### notify: name: Send Notification Email runs-on: ubuntu-latest needs: [terraform-backend, terraform-resources, docker, deploy] if: always() # Ensure this job runs regardless of previous outcomes steps: # Step to determine overall pipeline status - name: Determine overall pipeline status id: pipeline-status run: | # Check if all required jobs succeeded if [[ "${{ needs.terraform-backend.result }}" == "success" ]] \ && [[ "${{ needs.terraform-resources.result }}" == "success" ]] \ && [[ "${{ needs.docker.result }}" == "success" ]] \ && [[ "${{ needs.deploy.result }}" == "success" ]]; then echo "result=Success ✅" >> $GITHUB_OUTPUT else echo "result=Failed ❌" >> $GITHUB_OUTPUT fi # Step to send notification email - name: Send Email uses: hilarion5/send-mail@v1 with: smtp-server: smtp.gmail.com smtp-port: 465 smtp-secure: true from-email: ${{ secrets.MAIL_FROM }} to-email: <RECEIVER_EMAIL_ADDRESS>,<OHTER_RECEIVER_EMAIL_ADDRESS> # Add multiple email addresses separated by commas username: ${{ secrets.MAIL_USERNAME }} password: ${{ secrets.MAIL_PASSWORD }} subject: "CI/CD Pipeline Notification: ${{ github.workflow }} - ${{ steps.pipeline-status.outputs.result }}" body: "" html: | <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 8px; background-color: #ffffff;"> <h2 style="color: #24292e; text-align: center;">🚀 CI/CD Pipeline Notification</h2> <div style="background-color: #f6f8fa; padding: 16px; border-radius: 6px;"> <table style="width: 100%; border-collapse: collapse;"> <tr> <td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Workflow</strong></td> <td style="padding: 10px; border-bottom: 1px solid #ddd;">${{ github.workflow }}</td> </tr> <tr> <td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Triggered by</strong></td> <td style="padding: 10px; border-bottom: 1px solid #ddd;">${{ github.actor }}</td> </tr> <tr> <td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Repository</strong></td> <td style="padding: 10px; border-bottom: 1px solid #ddd;">${{ github.repository }}</td> </tr> <tr> <td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Run Details</strong></td> <td style="padding: 10px; border-bottom: 1px solid #ddd;"> <a href="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" style="color: #0366d6; text-decoration: none;">View Run #${{ github.run_id }}</a> </td> </tr> </table> </div> <h3 style="margin-top: 16px;">🛠 Job Statuses</h3> <table style="width: 100%; border-collapse: collapse; background-color: #fff;"> <tr style="background-color: #f6f8fa;"> <th style="padding: 10px; text-align: left;">Job</th> <th style="padding: 10px; text-align: center;">Status</th> </tr> <tr> <td style="padding: 10px;">Terraform Backend</td> <td style="padding: 10px; text-align: center; color: white; background-color: ${{ (needs.terraform-backend.result == 'success' && '#28a745') || '#d73a49' }}; border-radius: 4px;"> ${{ needs.terraform-backend.result }} </td> </tr> <tr> <td style="padding: 10px;">Terraform Resources</td> <td style="padding: 10px; text-align: center; color: white; background-color: ${{ (needs.terraform-resources.result == 'success' && '#28a745') || '#d73a49' }}; border-radius: 4px;"> ${{ needs.terraform-resources.result }} </td> </tr> <tr> <td style="padding: 10px;">Docker Build</td> <td style="padding: 10px; text-align: center; color: white; background-color: ${{ (needs.docker.result == 'success' && '#28a745') || '#d73a49' }}; border-radius: 4px;"> ${{ needs.docker.result }} </td> </tr> <tr> <td style="padding: 10px;">Deployment</td> <td style="padding: 10px; text-align: center; color: white; background-color: ${{ (needs.deploy.result == 'success' && '#28a745') || '#d73a49' }}; border-radius: 4px;"> ${{ needs.deploy.result }} </td> </tr> </table> <p style="color: #6a737d; font-size: 0.9em; margin-top: 20px; text-align: center;"> This email was sent automatically by <strong>GitHub Actions</strong>. </p> </div>
What it Does:
This workflow will:Trigger:
The workflow runs on any push to the github-action branch.Job 1: terraform-backend
Configures the remote Terraform backend by:Checking out the repo.
Setting up Terraform and verifying the AWS CLI.
Configuring AWS credentials.
Checking if the S3 bucket exists (to decide if backend creation is needed).
Initializing and applying the Terraform backend configuration.
Job 2: terraform-resources
Provisions your infrastructure by:Checking out the code and setting up Terraform.
Initializing Terraform with the remote backend.
Running Terraform plan and apply.
Capturing the EC2 instance’s public IP for later use.
Job 3: docker
Builds and pushes the Docker image for your online shop by:Logging into DockerHub.
Building the image.
Pushing it to your DockerHub repository.
Job 4: deploy
Deploys the application on the EC2 instance by:SSH-ing into the instance.
Updating the system and installing Docker.
Stopping any existing container, pulling the latest image, and running it.
Job 5: notify
Sends a notification email summarizing the pipeline results, regardless of success or failure, using an external action.
How to Access the Application:
Once the workflow completes, simply take the public IP of the instance and open it in your browser at: You can check on AWS console on eu-west-1 region for EC2 ip.
http://<Instance_Public_Ip>:3000
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.
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 enterdestroy
as the input to start the deletion process.
Making a Change and Pushing to Your Branch
To test the CI/CD pipeline:
Make a Change
Edit any file in your repository to trigger the workflow (this could be as simple as a comment change or an update to the code).Push Your Change
After making your changes, commit and push them to your branch:git add . git commit -m "Made a change to test CI/CD workflow" git push origin github-action
Observe the Workflow Trigger
Upon pushing, you will see that the workflow is automatically triggered on the push to your repository. Navigate to the "Actions" tab in GitHub to monitor the progress.Enjoy the Efficiency
With this setup, you’re all set to save time on every subsequent push!Email Notification on Pipeline Success:
Email Notification on Pipeline Failure:
Destroying All Infrastructure on a Single Click
Once you’re done testing or want to avoid incurring extra costs, you can quickly tear down the entire infrastructure with a single click.
Go to the Destroy All Infrastructure Workflow
In the GitHub "Actions" tab, locate thedestroy.yml
workflow.Run the Workflow Manually
Click on "Run workflow" and inputdestroy
when prompted, as the workflow must be triggered manually.Automatic Deletion
The workflow will automatically delete all the resources (EC2 instance, remote backend (S3 and DynamoDB), security groups, keys, etc.) within a couple of minutes.Pipeline Success Email Notification:
Pipeline Failure Email Notification:
Verification and Final Thoughts
After running your workflows, you can verify the deployed resources:
Check that your online shop application is accessible by navigating to
http://<Instance_Public_Ip>:3000
in your browser.Verify that all infrastructure components are in place during deployment.
EC2 :
S3 Bucket :
DynamoDB :
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.