Continuous Integration and Continuous Deployment (CI/CD) pipelines are essential for modern software delivery processes. They ensure that code is built, tested, and deployed automatically, reducing the chance of manual errors and speeding up the release process.

In this blog, we'll explore how to set up a CI/CD pipeline using GitHub Actions to deploy to an AWS EC2 instance through AWS Systems Manager (SSM) without SSH to the instance.

Prerequistes:

AWS SSM(System Manager) configuration should be done for instances to be connected with SSM command. Refer below blog for SSM configuration.

Credits : Vishnu Prasad

Key Technologies

  • GitHub Actions: Automates workflows like CI/CD within GitHub repositories.
  • AWS SSM (Systems Manager): Provides a secure way to execute commands on EC2 instances without needing direct SSH access.

Step 1: Create an user in IAM with SSM access

  1. Create a policy with the given permissions to SendCommand, ListCommandInvocations.
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "ssm:StartSession",
                "ssm:SendCommand",
                "ssm:ListCommandInvocations"
            ],
            "Resource": "*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "ssm:ResumeSession",
                "ssm:TerminateSession"
            ],
            "Resource": "arn:aws:ssm:*:*:session/${aws:userid}-*"
        },
        {
            "Sid": "VisualEditor2",
            "Effect": "Allow",
            "Action": [
                "ssm:StartSession",
                "ssm:SendCommand",
                "ssm:ListCommandInvocations"
            ],
            "Resource": "arn:aws:ssm:<region>:<account_no>:document/SSM-SessionManagerRunShell"
        }
    ]
}

2. Attach the policy to the user.

3. Create access key and secret key for the user which will be used in the aws configuration in github actions.

Step 2: Create github actions workflow in your repo

  1. Create a step in jobs to configure the aws access and secret key in the pipeline.

variables : AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY


jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - 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: your-region  

2. Create a step to execute the command in the instance using SSM SendCommand. In the send command change the parameters commands according to your needs to execute the commands in the instance.

Variable : AWS_INSTANCE_ID

- name: Execute commands via SSM
        id : execute_command
        run: |
          COMMAND_ID=$(aws ssm send-command \
            --instance-ids ${{ secrets.AWS_INSTANCE_ID }} \
            --document-name "AWS-RunShellScript" \
            --parameters 'commands=["echo "Commands to be executed" ",
                                    "whoami"]' \
            --comment "Deploying comment" \
            --query "Command.CommandId" \
            --output text)

          echo "Command ID: $COMMAND_ID"
          echo "::set-output name=command_id::$COMMAND_ID"

3. Use ListCommandInvocations to get the output of the SendCommand execution using the Command_ID from execution step.

Here, we are getting the command_id value from the execute_command step and listing the execution output.

Variable : AWS_INSTANCE_ID

- name: Output execution via SSM
        id : output_execution
        run: |
          COMMAND_ID=${{ steps.execute_command.outputs.command_id }}
          aws ssm list-command-invocations \
            --details \
            --instance-id ${{ secrets.AWS_INSTANCE_ID }} \
            --query "CommandInvocations[].CommandPlugins[].{Status:Status,Output:Output}" \
            --command-id "$COMMAND_ID" \
            --output text

Step 3: Create Secrets in github repository for the variables used

In your github repository, Go to setting. In the security, Secrets and Variables → actions

Create the variables used in the gtihub actions workflow with exact same name and give the appropriate value.

AWS_ACCESS_KEY_ID — user access key value AWS_SECRET_ACCESS_KEY — user secret access key value AWS_INSTANCE_ID — EC2 instance id

Sample workflow file

Example workflow to deploy to the EC2 instance using AWS SSM when branch is merged to staging.

name: Deploy to using github actions

on:
  pull_request:
    branches:
      - staging
    types:
      - closed  # Trigger when the PR is closed (merged)

jobs:
  deploy:
    if: github.event.pull_request.merged == true  # Only run if the PR was merged
    runs-on: ubuntu-latest

    steps:
      - 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: ap-south-1  

      - name: Execute commands via SSM
        id : execute_command
        run: |
          COMMAND_ID=$(aws ssm send-command \
            --instance-ids ${{ secrets.AWS_INSTANCE_ID }} \
            --document-name "AWS-RunShellScript" \
            --parameters 'commands=["
              runuser -l ubuntu git pull origin staging",
              "sudo systemctl restart gunicorn.service && echo Gunicorn restarted successfully || echo Gunicorn failed to restart"]' \
            --comment "Deploying to Staging" \
            --query "Command.CommandId" \
            --output text)

          echo "Command ID: $COMMAND_ID"
          echo "::set-output name=command_id::$COMMAND_ID"    
      
      - name: Output execution via SSM
        id : output_execution
        run: |
          sleep 15s
          COMMAND_ID=${{ steps.execute_command.outputs.command_id }}
          aws ssm list-command-invocations \
            --details \
            --instance-id ${{ secrets.AWS_INSTANCE_ID }} \
            --query "CommandInvocations[].CommandPlugins[].{Status:Status,Output:Output}" \
            --command-id "$COMMAND_ID" \
            --output text

This pipeline is an efficient way to automate the deployment of your application to AWS without manually connecting to EC2 instances. By using AWS SSM, you increase the security of your deployment process by eliminating the need for SSH access, while GitHub Actions makes the automation of the pipeline seamless.

Enhance the pipeline by adding notification job to notify about the deploy job status in slack using slackapi/[email protected] action.