In this article, I will show how to use the ‚AWS systems manager‘ to run a setup script on an AWS EC2-instance in an automated fashion.

The actual setup steps are not important, but rather my take to tackle the following requirements:

  1. The setup script shall be implemented as an AWS Systems Manager (SSM) document
  2. The SSM document shall be set up via Infrastructure as Code (IaC) using terraform
  3. The setup script shall be checked into the GIT repository as a single file
  4. On the EC2-machine, the script shall not run as the root user.

Creating a terraform resource

This is straightforward as the AWS provider contains all necessary resources.

resource "aws_ssm_document" "example" {
  name          = "example_document"
  document_type = "Command"
  content = jsonencode({
    schemaVersion = "2.2",
    description   = "My example script",
    mainSteps = [
      {
        action = "aws:runShellScript",
        name   = "runShellScript"
        inputs = {
          runCommand = [
            "echo Hello, I am $(whoami)"
          ]
        }
      }
    ]
  })
}Code-Sprache: JavaScript (javascript)

This definition creates an SSM document that executes a one-liner on the remote machine. It will print „Hello, I am “ followed by the username of the user executing the script. You will see it will result in „Hello, I am root“ – showing us scripts are executed as the root user by default (Do you remember requirement 3?). You can also define multiple commands, line by line.

The SSM document can be tied to an EC2 instance with the terraform resource „aws_ssm_association“.

Referencing a script file to be managed via version control

Terraform provides a ‘file’ function to pass contents from a file. Let’s create an example that we call example.sh (I know, very creative). Here is the content

#!/bin/bash
echo Hello, I am $(whoami)Code-Sprache: JavaScript (javascript)

We can easily pass it to terraform like this:

resource "aws_ssm_document" "example" {
  name          = "example_document"
  document_type = "Command"
  content = jsonencode({
    schemaVersion = "2.2",
    description   = "My example script",
    mainSteps = [
      {
        action = "aws:runShellScript",
        name   = "runShellScript"
        inputs = {
          runCommand = [
            file("./alive.sh")
          ]
        }
      }
    ]
  })
}
Code-Sprache: JavaScript (javascript)

Running our script as non-root

Now it gets tricky. We already saw that our script is executed as root user. If we want commands to be run as non-root, we can use the Linux command ’su‘ (switch user) :

resource "aws_ssm_document" "example" {
  name          = "example_document"
  document_type = "Command"
  content = jsonencode({
    schemaVersion = "2.2",
    description   = "My example script",
    mainSteps = [
      {
        action = "aws:runShellScript",
        name   = "runShellScript"
        inputs = {
          runCommand = [
              "su -c 'echo Hello, I am $(whoami)' ssm-user"
          ]
        }
      }
    ]
  })
}
Code-Sprache: JavaScript (javascript)

The result will be „Hello, I am ssm-user“. The ssm-user is pre-configured on every EC2 instance, so is available out-of the box on your machine.

If we want to pass a file as shown above, it will not work, at least not for real-life multi-line scripts. The script file is expanded line by line from terraform, and any modification (for example adding su -c at the beginning will break the script file structure).

The answer is to use the heredoc functionality of bash to pass multi-line statements. We store the whole script in a bash variable called multi_line_statement temporarily (first three lines in the runCommand block), store the variable in a file on our remote machine  (three lines after that) and finally execute this as not-root (last two lines). Looks like this:

resource "aws_ssm_document" "example" {
  name          = "example_document"
  document_type = "Command"
  content = jsonencode({
    schemaVersion = "2.2",
    description   = "My example script",
    mainSteps = [
      {
        action = "aws:runShellScript",
        name   = "runShellScript"
        inputs = {
          runCommand = [
            "multiline_statement='",
            file("./example.sh"),
            "'",
            "cat << EOF > /tmp/example.sh",
            "$multiline_statement",
            "EOF",
            "chmod 777 /tmp/example.sh",
            "su -c /tmp/example.sh ssm-user"
          ]
        }
      }
    ]
  })
}Code-Sprache: JavaScript (javascript)

Works like a charm if you don’t use single quotes in your script.

Hey, something is not working

In case something is not working, you might want to know what is going on on your remote machine. SSM stores scripts that are executed here: /var/lib/amazon/ssm/{INSTANCE_ID}/document/orchestration/{COMMAND_ID}/runShellScript/_script.sh

Once logged in, you can re-run the script for testing.  Please note, this is all under ‚root‘ ownership, so you have to change the user to root or sudo to reach the file.


Über den Autor

Daniel Lohausen

Daniel Lohausen
IT-Organisationsberater und Transformationsmanager

Als Cloud-Architekt und Prozess-Coach vereint Daniel Lohausen zwei Kernelemente erfolgreicher Softwareentwicklung in einem ganzheitlichen Beratungsansatz. Er ist überzeugt, dass sich technische und prozedurale Innovationen positiv beeinflussen und man die besten Ergebnisse erzielt, wenn man bei beide Aspekte konstant weiter entwickelt.