diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml new file mode 100644 index 0000000..9bd787b --- /dev/null +++ b/.github/workflows/action.yml @@ -0,0 +1,23 @@ +name: 'Runner Starter' +description: 'Turn on/off a self-hosted runner' +inputs: + instance_id: + description: 'ID of the EC2 instance to start OR the name of the scaling group to scale down' + required: true + action: + description: 'Define if I want to start or stop the runner' + required: true + aws_default_region: + description: 'AWS region to use' + required: true + +runs: + using: "composite" + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Start/Stop Runner Instance + shell: bash + run: | + ${GITHUB_ACTION_PATH}/instance_start_stop.bash --instance-id=${{ inputs.instance_id }} --action=${{ inputs.action }} diff --git a/.github/workflows/instance_start_stop.bash b/.github/workflows/instance_start_stop.bash new file mode 100755 index 0000000..3e1e23a --- /dev/null +++ b/.github/workflows/instance_start_stop.bash @@ -0,0 +1,103 @@ +#!/bin/bash + +# Function to print messages in a fancy way +function printMessage { + local message="$1" + local type="$2" + local length=${#message} + local line=$(printf "%-${length}s" | tr ' ' '-') + echo "" + case "$type" in + "info") + echo -e "\033[1;34m$line\033[0m" + echo -e "\033[1;34m$message\033[0m" + echo -e "\033[1;34m$line\033[0m" + ;; + "success") + echo -e "\033[1;32m$line\033[0m" + echo -e "\033[1;32m$message\033[0m" + echo -e "\033[1;32m$line\033[0m" + ;; + "error") + echo -e "\033[1;31m$line\033[0m" + echo -e "\033[1;31m$message\033[0m" + echo -e "\033[1;31m$line\033[0m" + ;; + *) + echo -e "\033[1;34m$line\033[0m" + echo -e "\033[1;34m$message\033[0m" + echo -e "\033[1;34m$line\033[0m" + ;; + esac + echo "" +} + +# Parse command line arguments +while [ $# -gt 0 ]; do + case "$1" in + --instance-id=*) + INSTANCE_ID="${1#*=}" + ;; + --action=*) + ACTION="${1#*=}" + ;; + *) + printMessage "Invalid argument: $1" "error" + exit 1 + ;; + esac + shift +done + +# Check if instance ID is null +if [ -z "$INSTANCE_ID" ]; then + printMessage "--instance-id=XX-XXX is required" "error" + exit 1 +fi + +# Check if action is null +if [ -z "$ACTION" ]; then + printMessage "--action=start|stop is required" "error" + exit 1 +fi + +# Check if action is valid +if [ "$ACTION" != "start" ] && [ "$ACTION" != "stop" ]; then + printMessage "Invalid action: $ACTION" "error" + exit 1 +fi + +# Check if instance is stopped or started +INSTANCE_STATE=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" --query "Reservations[].Instances[].State.Name" --output text) + +if [ "$ACTION" = "start" ]; then + if [ "$INSTANCE_STATE" = "stopped" ]; then + INSTANCE_NAME=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" --query "Reservations[].Instances[].Tags[?Key=='Name'].Value" --output text) + printMessage "Starting instance $INSTANCE_NAME ($INSTANCE_ID)..." "info" + aws ec2 start-instances --instance-ids "$INSTANCE_ID" >/dev/null + printMessage "Waiting for instance $INSTANCE_ID to start..." "info" + aws ec2 wait instance-running --instance-ids "$INSTANCE_ID" + printMessage "Instance $INSTANCE_ID is now running" "success" + elif [ "$INSTANCE_STATE" = "running" ]; then + INSTANCE_NAME=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" --query "Reservations[].Instances[].Tags[?Key=='Name'].Value" --output text) + printMessage "Instance $INSTANCE_NAME ($INSTANCE_ID) is already running" "success" + else + printMessage "Instance $INSTANCE_ID is in an unknown state: $INSTANCE_STATE" "error" + exit 1 + fi +elif [ "$ACTION" = "stop" ]; then + if [ "$INSTANCE_STATE" = "running" ]; then + INSTANCE_NAME=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" --query "Reservations[].Instances[].Tags[?Key=='Name'].Value" --output text) + printMessage "Stopping instance $INSTANCE_NAME ($INSTANCE_ID)..." "info" + aws ec2 stop-instances --instance-ids "$INSTANCE_ID" >/dev/null + printMessage "Waiting for instance $INSTANCE_ID to stop..." "info" + aws ec2 wait instance-stopped --instance-ids "$INSTANCE_ID" + printMessage "Instance $INSTANCE_ID is now stopped" "success" + elif [ "$INSTANCE_STATE" = "stopped" ]; then + INSTANCE_NAME=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" --query "Reservations[].Instances[].Tags[?Key=='Name'].Value" --output text) + printMessage "Instance $INSTANCE_NAME ($INSTANCE_ID) is already stopped" "success" + else + printMessage "Instance $INSTANCE_ID is in an unknown state: $INSTANCE_STATE" "error" + exit 1 + fi +fi diff --git a/README.md b/README.md index ff1dda4..8f37f14 100644 --- a/README.md +++ b/README.md @@ -10,3 +10,31 @@ The runner is kept active every 2 days (to not be removed as a Github Runner) an ## Installation TODO + +## Usage + +```yaml + steps: + # Configure AWS Credentials + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-west-2 + role-to-assume: $ARN_IAM_ROLE + + - name: Start Runner + uses: nodesource/aws-eco-runner@v1 + with: + instance_id: $INSTANCE_ID + action: 'start' + aws_default_region: 'us-west-2' + + ... + + - name: Stop Runner + uses: nodesource/aws-eco-runner@v1 + with: + instance_id: $INSTANCE_ID + action: 'stop' + aws_default_region: 'us-west-2' +``` diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..a151d70 --- /dev/null +++ b/action.yml @@ -0,0 +1,18 @@ +name: 'aws-eco-runner' +description: 'AWS Eco Runner is a solution designed to optimize the cost of using GitHub Actions runners on AWS. By automating the activation and deactivation of the instance, it ensures that you only incur costs when necessary' +author: 'NodeSource' + +runs: + using: 'node20' + main: '.github/workflows/action.yml' + +inputs: + instance_id: + description: 'ID of the EC2 instance to start OR the name of the scaling group to scale down' + required: true + action: + description: 'Define if I want to start or stop the runner' + required: true + aws_default_region: + description: 'AWS region to use' + required: true