Skip to content

Instantly share code, notes, and snippets.

@clarkdave
Last active August 21, 2024 08:09
Show Gist options
  • Save clarkdave/fb5938cce0638da9a4624faacd0ea1f1 to your computer and use it in GitHub Desktop.
Save clarkdave/fb5938cce0638da9a4624faacd0ea1f1 to your computer and use it in GitHub Desktop.
ecs-interactive-console
#!/bin/bash -e
##
# Use this annotated script a base for launching an interactive console task on Amazon ECS
#
# more info: https://engineering.loyaltylion.com/running-an-interactive-console-on-amazon-ecs-c692f321b14d
#
# Requirements:
# - `jq` must be installed on both the client and server
##
# the ECS cluster in which we'll launch the console `default` or `staging`
cluster="$1"
# the name of the task definition to use when launching the console task
taskDefinition="$2"
# depending on your requirement, you may want to use an existing task definition and then use the
# `containerOverrides`, or you might have a completely separate task definition that is designed
# for running a console
#
# the override approach is reasonable, but note that you can't override most things; in particular
# you can't add overrides for cpu/mem constraints. Using the same task definition as another app
# may also make it harder to identify console tasks in ECS, although the `started-by` field does
# help there
#
# whichever approach you use, because ECS does not support interactive tasks yet, you'll need to
# start the container with a "dummy" sleep command; this will stop the container from exiting
# immediately allowing you to docker exec a console in later
overrides='{
"containerOverrides": [{
"name": "app",
"command": ["sleep", "infinity"]
}]
}'
echo '-> Starting task...'
# run the new task using the ECS scheduler. By default, ECS will place the task on any instance
# that has enough cpu/mem available. You can also specify placement constraints or a strategy here
#
# https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_RunTask.html
#
response=$(aws ecs run-task \
--cluster "$cluster" \
--task-definition "$taskDefinition" \
--overrides "$overrides" \
--started-by "user-console/$(whoami)")
taskArn=$(echo "$response" | jq -r '.tasks [0] .taskArn')
containerInstanceArn=$(echo "$response" | jq -r '.tasks [0] .containerInstanceArn')
# once the task has been placed, translate the containerInstanceArn into an EC2 instance from which
# we can find the public IP address so we can connect to the server
#
# note: in production, your ECS container instances are probably not exposed to the internet, so
# you'll need to adapt things from here (e.g. require VPN access, or go through an SSH gateway)
#
# https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_DescribeContainerInstances.html
# https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html
#
instanceId=$(aws ecs describe-container-instances \
--cluster "$cluster" \
--container-instances "$containerInstanceArn" \
| jq -r '.containerInstances [0] .ec2InstanceId')
instance=$(aws ec2 describe-instances \
--instance-ids "$instanceId" \
| jq '.Reservations [0] .Instances [0]')
instanceName=$(echo "$instance" | jq -r '.Tags[] | select(.Key == "Name") .Value')
instanceIp=$(echo "$instance" | jq -r '.PublicIpAddress')
# once we have the address of the container instance we then need the Docker container id
#
# ECS container instances all have a handy introspection API on port 51678 which can be used to
# translate a taskArn into a Docker container
#
# Even though the task have been placed by this point, it may take some time for the container to
# actually start, so we'll loop this until we have a container id
getDockerId() {
dockerId=$(ssh -qt "ec2-user@$instanceIp" \
"curl http://localhost:51678/v1/tasks?taskarn=$taskArn" \
| jq -r '.Containers [0] .DockerId')
}
echo "-> Waiting for container to start..."
getDockerId
while [ "$dockerId" == 'null' ] || [ -z "$dockerId" ]; do
getDockerId
sleep 1
done
echo "-> Loading console on instance $instanceName / $instanceIp ..."
# with the instance address and docker container, we now have all we need to launch into an
# interactive console using `ssh -t`, which launches a pseudo-tty that lasts until we close the
# session
#
# note: this example assumes an executable `./console` in the docker work dir; for Rails this would
# probably be `bundle exec rails console`
ssh -qt "ec2-user@$instanceIp" docker exec -ti "$dockerId" ./console
sleep 0.5
# after we close the tty we'll stop the task so it doesn't sit around wasting resources
echo -n '-> Stopping task...'
response=$(aws ecs stop-task \
--cluster "$cluster" \
--task "$taskArn" \
--reason 'user-console/stop')
echo -e '\r-> Stopping task... done'
@vikalpj
Copy link

vikalpj commented Jan 25, 2018

I updated this a little bit to use task family and find the latest task defination from that. :)

# the name of the task family to use when launching the console task
taskFamily="$2"

# Finding latest task defination from task family
taskDefinations=$(aws ecs list-task-definitions --family "$taskFamily" --sort DESC)
taskDefinition=$(echo $taskDefinations | jq -r '.taskDefinitionArns [0]' | cut -d '/' -f 2)

@zulhfreelancer
Copy link

Any idea how to make this works on Fargate?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment