Skip to content

Commit dd2c8f5

Browse files
authored
Merge pull request #26 from sustainable-computing-io/auto-terminate
feat: add auto termination function
2 parents bc3dd37 + 46563a6 commit dd2c8f5

File tree

3 files changed

+121
-0
lines changed

3 files changed

+121
-0
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ jobs:
111111
| spot_inastance_only | (Optional) If true, only create a spot instance. | "true" |
112112
| create_s3_bucket | (Optional) If true, create a S3 bucket to store the model. | "false" |
113113
| bucket_name | (Optional) The name of the S3 bucket to store the model. | The bucket name is the same as the repository name with time date stamp. |
114+
| auto_terminate_hours | (Optional) The number of hours after which the instance should be terminated. | 6 |
114115
115116
## Outputs
116117
@@ -120,3 +121,4 @@ jobs:
120121
| `runner_name` | GitHub self hosted runner name |
121122
| `instance_ip` | AWS EC2 instance IP |
122123
| `bucket_name` | AWS S3 bucket name |
124+
| `termination_time` | The time when the instance was terminated |

action.yml

+5
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ inputs:
5252
bucket_name:
5353
description: 'The name of the S3 bucket (if created) to use during terminate action'
5454
required: false
55+
auto_terminate_hours:
56+
description: 'The number of hours to wait before terminating the instance'
57+
required: false
5558

5659
# Define your outputs here.
5760
outputs:
@@ -63,6 +66,8 @@ outputs:
6366
description: The name of the self hosted runner
6467
bucket_name:
6568
description: The name of the S3 bucket (if created)
69+
termination_time:
70+
description: The time the instance will be automatically terminated (if enabled)
6671

6772
runs:
6873
using: docker

entrypoint.sh

+114
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ CREATE_S3_BUCKET="${INPUT_CREATE_S3_BUCKET:-false}" # Wehther to create a S3 buc
2525
BUCKET_NAME="${INPUT_BUCKET_NAME:-}" # Name of the S3 bucket
2626
INSTANCE_ID="${INPUT_INSTANCE_ID:-}" # ID of the created instance
2727
KEY_NAME_OPT="" # Option to pass to the AWS CLI to specify the key pair
28+
AUTO_TERMINATE_HOURS="${INPUT_AUTO_TERMINATE_HOURS:-6}" # Number of hours after which to terminate the instance (0 means no auto-termination)
2829
[ "$DEBUG" == "true" ] && set -x
2930

3031
# get the organization name from the github repo
@@ -106,6 +107,81 @@ prep_create() {
106107
fi
107108
}
108109

110+
schedule_termination() {
111+
local instance_id=$1
112+
local hours=$2
113+
114+
if [ "$hours" -gt 0 ]; then
115+
# Calculate termination time in UTC
116+
local termination_time
117+
termination_time=$(date -u -d "+${hours} hours" +"%Y-%m-%dT%H:%M:%SZ")
118+
119+
# Create an AWS CloudWatch event rule to terminate the instance
120+
local rule_name="terminate-${instance_id}"
121+
122+
# Create CloudWatch event rule
123+
aws events put-rule \
124+
--name "$rule_name" \
125+
--schedule-expression "at(${termination_time})" \
126+
--state ENABLED \
127+
--region "${REGION}"
128+
129+
# Create IAM role for CloudWatch to terminate EC2 instances if it doesn't exist
130+
local role_name="AutoTerminateEC2Role"
131+
if ! aws iam get-role --role-name "$role_name" 2>/dev/null; then
132+
aws iam create-role \
133+
--role-name "$role_name" \
134+
--assume-role-policy-document '{
135+
"Version": "2012-10-17",
136+
"Statement": [{
137+
"Effect": "Allow",
138+
"Principal": {
139+
"Service": "events.amazonaws.com"
140+
},
141+
"Action": "sts:AssumeRole"
142+
}]
143+
}'
144+
145+
# Attach policy to allow terminating EC2 instances
146+
aws iam put-role-policy \
147+
--role-name "$role_name" \
148+
--policy-name "TerminateEC2Policy" \
149+
--policy-document '{
150+
"Version": "2012-10-17",
151+
"Statement": [{
152+
"Effect": "Allow",
153+
"Action": "ec2:TerminateInstances",
154+
"Resource": "*"
155+
}]
156+
}'
157+
fi
158+
159+
# Create target for the rule
160+
aws events put-targets \
161+
--rule "$rule_name" \
162+
--targets "[{
163+
\"Id\": \"TerminateInstance\",
164+
\"Arn\": \"arn:aws:events:${REGION}:${AWS_ACCOUNT_ID}:rule/${rule_name}\",
165+
\"RoleArn\": \"arn:aws:iam::${AWS_ACCOUNT_ID}:role/${role_name}\",
166+
\"Input\": \"{\\\"instance_ids\\\": [\\\"${instance_id}\\\"]}\",
167+
\"RunCommandParameters\": {
168+
\"RunCommand\": [
169+
\"aws\",
170+
\"ec2\",
171+
\"terminate-instances\",
172+
\"--instance-ids\",
173+
\"${instance_id}\",
174+
\"--region\",
175+
\"${REGION}\"
176+
]
177+
}
178+
}]"
179+
180+
debug "Instance $instance_id scheduled for termination at $termination_time UTC"
181+
fi
182+
}
183+
184+
109185
# create the user data script
110186
create_uesr_data () {
111187
# Encode user data so it can be passed as an argument to the AWS CLI
@@ -179,6 +255,35 @@ delete_s3_bucket () {
179255
}
180256

181257
terminate_instance () {
258+
local rule_name="terminate-${instance_id}"
259+
local role_name="AutoTerminateEC2Role"
260+
261+
# Remove CloudWatch Event Rule and Targets if they exist
262+
if aws events list-rules --name-prefix "$rule_name" --region "${REGION}" 2>/dev/null | grep -q "$rule_name"; then
263+
debug "Removing CloudWatch Event targets for rule $rule_name"
264+
aws events remove-targets --rule "$rule_name" --ids "TerminateInstance" --region "${REGION}"
265+
266+
debug "Deleting CloudWatch Event rule $rule_name"
267+
aws events delete-rule --name "$rule_name" --region "${REGION}"
268+
fi
269+
270+
# Check if the IAM role is being used by other rules before deleting
271+
if aws iam get-role --role-name "$role_name" 2>/dev/null; then
272+
# List all rules to check if the role is still in use
273+
local rules_using_role
274+
rules_using_role=$(aws events list-rules --region "${REGION}" --query 'Rules[?RoleArn!=`null`]' --output text)
275+
276+
if [ -z "$rules_using_role" ]; then
277+
debug "Removing IAM role policy"
278+
aws iam delete-role-policy --role-name "$role_name" --policy-name "TerminateEC2Policy"
279+
280+
debug "Deleting IAM role $role_name"
281+
aws iam delete-role --role-name "$role_name"
282+
else
283+
debug "IAM role $role_name is still in use by other rules, skipping deletion"
284+
fi
285+
fi
286+
182287
# Terminate instance
183288
aws ec2 terminate-instances --instance-ids "$INSTANCE_ID" --region "${REGION}"
184289
# Delete S3 bucket
@@ -230,6 +335,12 @@ create_runner () {
230335
exit 1
231336
fi
232337
fi
338+
339+
# Schedule auto-termination if enabled
340+
if [ "$AUTO_TERMINATE_HOURS" -gt 0 ]; then
341+
schedule_termination "$INSTANCE_ID" "$AUTO_TERMINATE_HOURS"
342+
fi
343+
233344
rm user_data.sh
234345
# Wait for instance to become ready
235346
aws ec2 wait instance-status-ok --instance-ids "$INSTANCE_ID" --region "${REGION}"
@@ -249,6 +360,9 @@ create_runner () {
249360
echo "runner_name=$RUNNER_NAME" >> $GITHUB_OUTPUT
250361
echo "instance_ip=$INSTANCE_IP" >> $GITHUB_OUTPUT
251362
echo "bucket_name=$BUCKET_NAME" >> $GITHUB_OUTPUT
363+
if [ "$AUTO_TERMINATE_HOURS" -gt 0 ]; then
364+
echo "termination_time=$(date -u -d "+${AUTO_TERMINATE_HOURS} hours" +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_OUTPUT
365+
fi
252366
}
253367

254368
list_runner () {

0 commit comments

Comments
 (0)