Automating AWS EC2 deployments with GitHub Actions and Systems Manager

This blog is hosted on an AWS EC2 instance; the code for it is stored in a GitHub repository. In order to update the code on the EC2 instance I previously would manually connect to the EC2 instance via SSH client, pull the code from the GitHub repository and then execute the necessary command to redeploy the code. While this got the job done, it always felt rather clunky and tedious. Last week I finally got around to automating the process. In today’s post I’ll be discussing the solution I came up with.

Essentially my approach involves integrating GitHub Actions with AWS Systems Manager to deploy the code on the EC2 instance on a push of the code to a branch of the GitHub repository. The GitHub Actions workflow consists of two steps, which are as follows:

  1. Configuring the AWS credentials
  2. Executing the deployment

The rest of this post will go into a bit more detail about each of these steps.

I’ll start by stubbing out the GitHub Actions workflow I’m using:

name: Deploy to EC2

on:
  push:
    branches:
      - master

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - name: Configure AWS credentials
    ...
    - name: Execute deployment script on EC2 instance
    ...

In the “name” section I specify a name for the workflow. In the “on” section I specify that I want the workflow to run on pushes to the master branch of the repository. Finally in the “jobs” section I outline the “deploy” process–“runs-on” specifies the runner for GitHub Actions to use; “steps” specifies the steps for GitHub Actions to execute.

To configure AWS credentials I use AWS’s official configure-aws-credentials action. This action needs to be configured with the following data:

  1. The IAM user’s access key ID
  2. The IAM user’s secret access key
  3. The region of the EC2 instance to which the code is being deployed

This presupposes of course that the IAM user and an EC2 instance already exist. In my case the latter did but the former didn’t so I just went ahead and created an IAM user to represent GitHub Actions. With these things in place the complete step ended up as follows:

- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v1
  with:
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    aws-region: us-east-1
(So as not to expose the AWS credentials publicly I store them as secrets in the repo’s Security settings.)
To execute the deployment process on the EC2 instance I use a custom action built around System Manager’s send-command function. The docs reveal the function to be highly configurable; the parameters that are relevant to my use case are as follows:
  1. document-name – The name of the Amazon Web Services Systems Manager document (SSM document) to run.
  2. targets – An array of search criteria that targets managed nodes using a key-value combination that you specify.
  3. parameters – The required and optional parameters specified in the document being run.
  4. timeout-seconds – If this time is reached and the command hasn’t already started running, it won’t run.

For document-name I specify “AWS-RunShellScript”–this is a shared resource available via Systems Manager Documents that enables Systems Manager to run a shell script.

For targets, I specify “instanceids” as “Key” and the instance ID of my EC2 instance as “Values.”

For parameters, I specify a string in the following form (where <command> represents a specific instruction to provide to the EC2 instance):

'commands=[
  "<command>"
]'

Finally for timeout-seconds, I specify a value of 600 (10 minutes).

With these things in place the complete step ended up as follows:

- name: Execute deployment script on EC2 instance
  run: |
    aws ssm send-command \
      --document-name "AWS-RunShellScript" \
      --targets "Key=instanceids,Values=${{ secrets.EC2_INSTANCE_ID }}" \
      --parameters 'commands=[
        "<command>"
      ]' \
      --timeout-seconds 600

(Similar to before I store the EC2 instance’s ID as a secret in the repo’s security settings so as not to expose it publicly.)

So this is pretty much it. A push of the code to the master branch of the repo now results in the code being deployed automatically to the EC2 instance via the GitHub Action. Handily the results of the execution are available in Command History under Systems Manager > Run Command.

Clicking into the detail of a command exposes further info such as output and error logging, plus the ability to re-run the command from Systems Manager itself.

All in all this was a fun little project that took a day or two of tinkering to get working.

Accessing an AWS EC2 instance via Session Manager

This blog is currently hosted on AWS EC2. Until recently I would always connect to my EC2 instance via SSH client. An alternative approach I learned of recently is to connect via Session Manager, a feature of AWS Systems Manager. A main benefit of Session Manager is that it removes the need to open inbound ports to the instance or manage SSH keys as Session Manager handles these security details for you.

Using Session Manager involves a few prerequisites, which can be reduced to the following three-step process:

  1. Provisioning the EC2 instance with the SSM* agent
  2. Provisioning the EC2 instance with an IAM role
  3. Restarting the SSM agent to detect the IAM role

* Simple Systems Manager

Detailed instructions follow. Note that these instructions are specific to Ubuntu 14.04, which I appreciate is quite outdated at time of writing. Steps 1 and 3 require you to be connected to an EC2 instance (for example via SSH client). Step 2 requires you to be logged in to the AWS Management Console.

Provisioning the EC2 instance with the SSM agent

The first main step toward connecting to an EC2 instance via Session Manager is to install the SSM agent on the EC2 instance. For my OS this involved running the following commands against the instance:

// Update the OS package index
sudo apt-get update

// Download the SSM agent package
wget https://s3.amazonaws.com/amazon-ssm-us-east-1/latest/debian_amd64/amazon-ssm-agent.deb

// Install the SSM agent package
sudo dpkg -i amazon-ssm-agent.deb

// Start the SSM agent
sudo start amazon-ssm-agent

// Verify the SSM agent status
sudo status amazon-ssm-agent

This last command should produce output like the following:

amazon-ssm-agent start/running, process 4180

Provisioning the EC2 instance with an IAM role

The second main step toward connecting to an EC2 instance via Session Manager is to provision the EC2 instance with an IAM role granting permission to Session Manager to connect to the instance. This step involves (1) creating the IAM role and (2) attaching the role to the instance.

Create an IAM role for the EC2 instance

From the AWS Management Console go to IAM. From the left nav click Roles and from the top-right click “Create Role.” You should be taken to a three-step wizard for creating an IAM role.

The first step is to select the trusted entity for the role. For “Trusted Entity Type” choose “AWS Service.” For “Use Case” choose EC2 as Service and “EC2 Role for AWS Systems Manager” as “Use Case.” You can then proceed to the next step of the wizard.

The second step is to add permissions to the role. All you should need to do for this step is to verify that the relevant policy (AmazonSSMManagedInstanceCore) is attached to the role, which the use case chosen in the previous step should take care of automatically. You can then proceed to the next step of the wizard.

The last step is to name, review, and create the role. Under “Role details” add a name and description for the role. Then create the role and verify that it was created successfully.

Attach the IAM role to the EC2 instance

Still in the AWS Management Console go to EC2. From the left nav click Instances and from the Instances pane select the relevant instance. From the Security submenu of the Actions menu select “Modify IAM Role.” From the “IAM role” menu select the role you created in the previous step. Then click “Update IAM role.”

From the Instances pane select the relevant instance again (assuming it’s not already selected). Verify that the role is attached to the instance–it should be listed under the “IAM Role” heading in the Details tab of the Instances pane.

Still in the Instances pane and with the relevant instance still selected, click Connect. From the “Connect to instance” page select the “Session Manager” tab. You should be presented with a page that resembles the following screenshot:

Note (a) the disabled Connect button and (b) the warning about the instance not being connected to Session Manager–both would be expected at this stage since it’s necessary to restart the SSM agent in order for the instance to detect the updated IAM role.

Restarting the SSM agent to detect the IAM role

The last main step, then, toward connecting to an EC2 instance via Session Manager is to restart the SSM agent in order for EC2 to detect the updated IAM role. For my OS this involved running the following command against the instance:

sudo restart amazon-ssm-agent

Back in the AWS Management Console refreshing the “Connect to instance” page should result in a page that resembles the following screenshot:

Note that (a) the warning about the instance not being connected to Session Manager has disappeared and (b) the Connect button has been enabled. If you go ahead and click the Connect button you should be presented with a browser-based terminal from which you can run commands against the EC2 instance.

Congratulations! You’ve now successfully connected to an EC2 instance via Session Manager.