Skip to content

feat: Add init and phonehome scripts #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,19 @@
# runner
Public components and supporting tools for the Nuon runner.
# Nuon Runner Public Components
Public components and supporting tools for the [Nuon runner](https://docs.nuon.co/concepts/runners).

## Scripts

The `scripts` directory contains scripts that are executed as part of the [runner's]((https://docs.nuon.co/concepts/runners)) bootstrapping processes, when other more standard mechanisms are not available.

### `scripts/aws/init.sh`

This script is set as the [UserData](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html) for runner EC2 instances, causing it to be run on exactly once on first boot.

The script installs docker and the cloudwatch agent, then sets up a systemd service that fetches the latest image of Nuon's runner and sets it to restart always.

Eventually, this script will be modified to offer more control over runner version.

### `scripts/aws/phonehome.py`

This script is triggered [as a lambda](https://docs.aws.amazon.com/lambda/latest/dg/services-cloudformation.html) on success or failure of Cloudformation stack creation. It tells the Nuon control plane that a new runner now exists, and reports information that is necessary for subsequent runner bootstrapping
and day-to-day operation (e.g. ARNs for IAM roles the runner needs to assume).
56 changes: 56 additions & 0 deletions scripts/aws/init.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!bin/bash

get_tag() {
local tag_name=$1
local instance_id=$(ec2-metadata -i | awk '{ print $2 }')

aws ec2 describe-tags \
--filters "Name=resource-id,Values=$instance_id" "Name=key,Values=$tag_name" \
--query 'Tags[0].Value' \
--output text
}

RUNNER_ID=$(get_tag "nuon_runner_id")
RUNNER_API_TOKEN=$(get_tag "nuon_runner_api_token")
RUNNER_API_URL=$(get_tag "nuon_runner_api_url")

yum install -y docker amazon-cloudwatch-agent
systemctl enable --now docker

# Set up things for the runner
useradd runner -G docker -c "" -d /opt/nuon/runner
chown -R runner:runner /opt/nuon/runner

cat << EOF > /opt/nuon/runner/env
RUNNER_ID=$RUNNER_ID
RUNNER_API_TOKEN=$RUNNER_API_TOKEN
RUNNER_API_URL=$RUNNER_API_URL
# FIXME(sdboyer) this hack must be fixed - userdata is only run on instance creation, and ip can change on each boot
HOST_IP=$(curl -s https://checkip.amazonaws.com)
EOF

# Create systemd unit file for runner
cat << EOF > /etc/systemd/system/nuon-runner.service
[Unit]
Description=Nuon Runner Service
After=docker.service
Requires=docker.service

[Service]
TimeoutStartSec=0
User=runner
ExecStartPre=-/usr/bin/docker exec %n stop
ExecStartPre=-/usr/bin/docker rm %n
ExecStartPre=/usr/bin/docker pull public.ecr.aws/p7e3r5y0/runner:latest
ExecStart=/usr/bin/docker run --rm --name %n -p 5000:5000 --detach --env-file /opt/nuon/runner/env public.ecr.aws/p7e3r5y0/runner:latest run
Restart=always
RestartSec=3

[Install]
WantedBy=default.target
EOF

# Just in case SELinux might be unhappy
/sbin/restorecon -v /etc/systemd/system/nuon-runner.service
systemctl daemon-reload
systemctl enable --now nuon-runner
35 changes: 35 additions & 0 deletions scripts/aws/phonehome.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import json

import urllib3
import cfnresponse

http = urllib3.PoolManager()


def lambda_handler(event, context):
props = event["ResourceProperties"]

# Start with all fields from the event
data = event.copy()
# Remove ResourceProperties since we'll flatten those separately
props = data.pop("ResourceProperties", None)

encoded_data = json.dumps(props).encode("utf-8")
url = props["url"]

try:
response = http.request(
"POST",
url,
body=encoded_data,
headers={"Content-Type": "application/json"},
)
print("Response: ", response.data)
cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
except Exception as e:
print("Error: ", str(e))
# It's OK if notifying Nuon fails on deletion
if event["RequestType"] in ["Create", "Update"]:
cfnresponse.send(event, context, cfnresponse.FAILED, {"Error": str(e)})
else:
cfnresponse.send(event, context, cfnresponse.SUCCESS, {})