Try for Free →
Niels AndriesseCo-Founder
Patrick PijnappelCo-Founder

Programmatically Managing AWS EC2 Instances: A Deep Dive

July 10th, 2023
While you're using Superluminal, you're allocated an EC2 instance on AWS. Code generated by Superluminal is executed inside a Docker container on the EC2 instance assigned to you. This is critical for data security, but managing compute instances while ensuring cost-effectiveness and particularly performance isn't straightforward. In this article, we'll walk through the peculiarities that come with managing EC2 instances programmatically.Let me start by pointing out that AWS maintains a number of SDKs to programmatically manage EC2 instances. However, as of the time of writing this article, many of the SDKs such as the Swift and Rust SDKs are marked as experimental. The Superluminal back-end is built using Swift, and we didn't want to build on an experimental SDK, so we explored other options.We tried using boto3, the AWS Python SDK. It's stable and it's possible to call Python code from Swift using PythonKit. However, we quickly found that we didn't want to rely on PythonKit either, because calling Python code from Swift is error-prone and experimental.So, we decided to go with the most mature and well-maintained option: the AWS CLI. Using the CLI from Swift is relatively straightforward with the Swift-native Process API. A simple implementation would look something like this:
func runEC2Command(with arguments: [String]) throws -> String? {
    let task = Process()
    task.executableURL = URL(fileURLWithPath: $PATH_TO_AWS_CLI)
    task.arguments = [ "ec2" ] + arguments
    let outputPipe = Pipe()
    task.standardOutput = outputPipe
    task.standardError = outputPipe
    task.qualityOfService = .userInitiated
    let _ = try task.run()
    task.waitUntilExit()
    let output = given(try outputPipe.fileHandleForReading.readToEnd()) { String(data: $0, encoding: .utf8) }
    return output
}
Starting an EC2 instance programmaticallyCreating an AMIIf you want your EC2 instance to start pre-configured, for example with some of your own code running on it, you'll want to first create an AMI (Amazon Machine Image) on AWS. You do this by right-clicking an EC2 instance you have running that's configured the way you want it and clicking "Create Image" under "Images and Templates". It'll take a few minutes for the AMI to be created. In the meantime, don't shut down your EC2 instance.Starting an EC2 instance programmatically using the AWS CLIOnce you have your AMI set up and it's marked as ready on AWS, you can programmatically start up an EC2 instance from the template you just created by running:
aws ec2 run-instances 
    --image-id $AMI_ID
    --instance-type $INSTANCE_TYPE
    --count 1
    --key-name $AWS_KEY
    --security-group-ids $SECURITY_GROUP_ID
    --subnet_id $SUBNET_ID
    --tag-specifications ResourceType=instance,Tags=[{Key=Name,Value=$INSTANCE_NAME}]
$AMI_ID is the ID of the AMI you just created, which you can find on AWS under the AMIs section. $INSTANCE_TYPE is the instance type you want to start up, so for example "t2.micro". $AWS_KEY is the name of your key pair. $SECURITY_GROUP_ID and $SUBNET_ID depend on your configuration, and $INSTANCE_NAME is the name you want to give your new instance. There are many other options, but in most cases you won't need those.Waiting until the instance is runningIt can take a while for an EC2 instance to reach the "ready" state. You can track the state of the instance by polling it every few seconds using:
aws ec2 describe-instances --instance-ids $INSTANCE_ID
Where $INSTANCE_ID is the ID of the instance you just started as returned by the run-instances call earlier.Waiting until the instance is reachableWhen an instance's state as described by describe-instances is "ready", this doesn't mean it's necessarily reachable yet. When you try to connect to it via web socket, or if you try to make an HTTP request to it, this'll likely initially still fail. You can tell when it's reachable by pinging it every few seconds until the ping comes back successfully. A simple way to do this is to set up a POST /ping route on your instance that does nothing except return a 200 and to then hit that endpoint every few seconds until it returns a successful response. Once you get a 200 from this endpoint instead of a timeout, you'll know your instance is ready for further interaction.InteractionGreat! Now that you have programmatically created an instance from an AMI, waited for it to be ready and then waited for it to be reachable, you can interact with it using your preferred protocol. We use a combination of HTTP requests and web socket messages at Superluminal, whichever is more appropriate for a particular task.A final piece of advice before we get into further lifecycle management is to run any generated or user-supplied code inside a Docker container on the instance. You don't want arbitrary code executing directly on your compute instance as it could access system resources, files on disk, and so on.Lifecycle managementThroughout its lifetime, an EC2 instance will typically cycle through the following states:
  • pending: The instance is starting up.
  • running: The instance is running and (potentially) ready for interaction.
  • stopping: The instance is stopping.
  • stopped: The instance is stopped.
  • shutting-down: The instance is in the process of terminating.
  • terminated: The instance is terminated.
You'll need to handle each of these to robustly (and cost-effectively) manage your compute instances. Again, the way to get the current state of a given instance is using:
aws ec2 describe-instances --instance-ids $INSTANCE_ID
Stopping an instance is straightforward using the following command:
aws ec2 stop-instances --instance-ids $INSTANCE_ID
And similarly, terminating an instance is done using:
aws ec2 terminate-instances --instance-ids $INSTANCE_ID
Wrapping UpIf you're building in Python, you should use the boto3 library mentioned earlier. But if you're using a language for which there is no mature AWS SDK, programmatically interacting with the AWS CLI might be a good alternative. And although there's more to programmatic instance management through the AWS CLI than what's described in this article, this article should make for a good starting point!