Running an EC2 instance locally using LocalStack and AWS CLI

Learn how to run an EC2 instance using LocalStack's core cloud emulator and AWS CLI to facilitate development & testing right on your local machine!

ยท

8 min read

Running an EC2 instance locally using LocalStack and AWS CLI

EC2, or Elastic Compute Cloud, enables users to create and manage a virtual machine in the cloud. It's a pivotal service that marked the beginning of cloud computing and is often the first service users learn & explore in the AWS ecosystem. However, there are many horror tales about people leaving an EC2 instance running and receiving a wallet-busting bill. Now, the burning question: How can someone safely run an EC2 instance for some casual learning or testing without overstepping on the free tier and playing hide-and-seek with AWS billing?

LocalStack is a core cloud emulator that allows you to emulate various AWS services on your local machine without needing a real AWS account. LocalStack essentially operates within a Docker container, emulating or mocking different cloud services. This setup lets you connect your integrations (like Terraform, CDK, AWS CLI) to the running container for testing your application and infrastructure code without spending anything on infrastructure provisioning.

For learning, testing, and integration purposes, LocalStack supports running an emulated EC2 instance locally. This blog will walk you through launching a local EC2 instance, accessing the running instance, and deploying a basic Flask API using a user data shell script.

Docker Desktop on macOS and Windows does not make the Docker Bridge network accessible, preventing users from completing this tutorial. To ensure a smooth end-to-end experience, it is recommended to use Linux or GitHub Codespaces/GitPod.

Running Docker containers as EC2 instances

In the LocalStack community edition, you can mock EC2 APIs on your local machine. You send API requests to the mocked EC2 service running in LocalStack that mimics the real AWS service's interface and triggers a predefined behaviour. However, no actual instances (like virtual machines) are created. This setup often leads to testing errors because the mock implementation doesn't create real resources.

In LocalStack Pro, EC2 APIs utilize the Docker Engine backend to emulate EC2 instances. When you launch an EC2 instance locally, LocalStack sets up a Docker container recognized as an Amazon Machine Image (AMI). This enables users to log in to the instance, test their configurations, and conduct end-to-end integration tests on a local EC2 infrastructure.

Prerequisites

You can sign-up for LocalStack Hobby Plan to grab a LocalStack Auth Token and use advanced AWS APIs, such as EC2 Docker backend.

Create a Flask API

To illustrate an example, set up a basic Flask API with three routes: /, /get, and /post. Create a new file called app.py and add the following code.

from flask import Flask, request

app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, LocalStack!'

# GET route
@app.route('/get', methods=['GET'])
def get_example():
    return 'This is a GET request example.'

# POST route
@app.route('/post', methods=['POST'])
def post_example():
    data = request.json
    return f'This is a POST request example. Received data: {data}'

if __name__ == '__main__':
    app.run(port=8000, host="0.0.0.0")

Add this file to a fresh GitHub/GitLab repository, or use an existing template you can find at the following GitLab repository.

Start your LocalStack container

Launch the LocalStack container on your local machine using the specified command:

export LOCALSTACK_AUTH_TOKEN=...
localstack start

Once initiated, you'll receive a confirmation output indicating that the LocalStack container is up and running.

     __                     _______ __             __
    / /   ____  _________ _/ / ___// /_____ ______/ /__
   / /   / __ \/ ___/ __ `/ /\__ \/ __/ __ `/ ___/ //_/
  / /___/ /_/ / /__/ /_/ / /___/ / /_/ /_/ / /__/ ,<
 /_____/\____/\___/\__,_/_//____/\__/\__,_/\___/_/|_|

 ๐Ÿ’ป LocalStack CLI 3.5.0
 ๐Ÿ‘ค Profile: default

...
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ LocalStack Runtime Log (press CTRL-C to quit) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

LocalStack version: 3.5.1.dev20240712171301
LocalStack build date: 2024-07-14
LocalStack build git hash: 646be01
...

To confirm the startup of your LocalStack container with the Pro services, utilize the cURL command to query the /info endpoint.

curl http://localhost:4566/_localstack/info | jq
{
  "version": "3.5.1.dev20240712171301:646be01",
  "edition": "pro",
  "is_license_activated": true,
  ...
  "system": "linux",
  "is_docker": true,
  ...
}

Create an EC2 key pair

Before you make an EC2 instance, make a key pair. EC2 keeps the public key and shows the private key for you to save. To make a key pair using the awslocal CLI, use this command:

awslocal ec2 create-key-pair \
    --key-name my-key \
    --query 'KeyMaterial' \
    --output text | tee key.pem

This saves the key pair in a file named key.pem in the current directory. Apply the right permissions to the file with this command:

chmod 400 key.pem

Alternatively, you can bring in an existing key pair. If you have an SSH public key in your home directory under ~/.ssh/id_rsa.pub, run this command to import it:

awslocal ec2 import-key-pair \
    --key-name my-key \
    --public-key-material file://~/.ssh/id_rsa.pub

Add inbound roles

In LocalStack, networking features like subnets and VPCs are not emulated. LocalStack provides a default security group that manages the exposed ports for the EC2 instance. While users can create additional security groups, LocalStack focuses on the default security group.

By default, the SSH port 22 is open. To enable inbound traffic on the port 8000 for our Flask API, use this command to authorize the default security group:

awslocal ec2 authorize-security-group-ingress \
    --group-id default \
    --protocol tcp \
    --port 8000 \
    --cidr 0.0.0.0/0

The output will be:

{
    "Return": true,
    "SecurityGroupRules": [
        {
            "SecurityGroupRuleId": "sgr-84956433d958d012e",
            "GroupId": "sg-952c436416c2392b0",
            "GroupOwnerId": "000000000000",
            "IsEgress": false,
            "IpProtocol": "tcp",
            "FromPort": 8000,
            "ToPort": 8000,
            "CidrIpv4": "0.0.0.0/0",
            "Description": ""
        }
    ]
}

Retrieve the security group ID with this command:

sg_id=$(awslocal ec2 describe-security-groups | jq -r '.SecurityGroups[0].GroupId')
echo $sg_id

Run the EC2 Instance locally

Now, you can start and operate the EC2 instance on your local machine. Before executing the command, create a new file named user_script.sh and include the following content:

#!/bin/bash -xeu

apt update
apt install python3 -y
apt-get -y install python3-pip curl git
git clone https://gitlab.com/HarshCasper/flask-api-example.git
cd flask-api-example
pip3 install flask
python3 app.py

Utilize the above script as a user data shell script, enabling you to send specific instructions to an instance during launch.

Modify the repository URL if you want to specify your personal GitHub/GitLab repository for deployment. Include any extra commands necessary to set up the application correctly.

Now, directly initiate the EC2 instance by executing the following command:

awslocal ec2 run-instances \
  --image-id ami-ff0fea8310f3 \
  --count 1 \
  --instance-type t3.nano --key-name my-key \
  --security-group-ids $sg_id \
  --user-data file://./user_script.sh

In the command above, the instance type is specified as t2.nano, but it has no impact since LocalStack uses a ubuntu-20.04-focal-fossa Docker image emulated as an EC2 instance. This behaviour is set by the image ID ami-ff0fea8310f3. To use an Amazon Linux AMI instead, specify the image ID as ami-024f768332f0 (requiring adjustments to the user script).

Upon successful execution, the output will be retrieved.

{
    "Groups": [
        {
            "GroupName": "default",
            "GroupId": "sg-245f6a01"
        }
    ],
    "Instances": [
        {
            "AmiLaunchIndex": 0,
            "ImageId": "ami-ff0fea8310f3",
            "InstanceId": "i-42e830289e675885f",
            "InstanceType": "t3.nano",
            ...
            "VirtualizationType": "paravirtual"
        }
    ],
    "OwnerId": "000000000000",
    "ReservationId": "r-a8f48d6e"
}

You can also confirm that LocalStack has spun an additional Docker container to emulate a locally running EC2 instance:

docker ps
CONTAINER ID   IMAGE                                                      COMMAND                  CREATED          STATUS                          PORTS                                                                                                                                    NAMES
0c00863e8fdd   localstack-ec2/ubuntu-20.04-focal-fossa:ami-ff0fea8310f3   "sleep infinity"         4 seconds ago    Up 3 seconds                    0.0.0.0:22->22/tcp, 0.0.0.0:8000->8000/tcp                                                                                               localstack-ec2.i-320e3293f4029b596
3fc719173eb2   localstack/localstack-pro                                  "docker-entrypoint.sh"   18 minutes ago   Up 18 minutes (healthy)         127.0.0.1:443->443/tcp, 127.0.0.1:4510-4560->4510-4560/tcp, 0.0.0.0:53->53/tcp, 0.0.0.0:53->53/udp, 127.0.0.1:4566->4566/tcp, 5678/tcp   localstack-main

Logging into the EC2 instance

After launching the EC2 instance, check the LocalStack logs. In these logs, you can confirm that the EC2 instance is running successfully on your local machine.

localstack logs
...
Determined main container network: bridge
Determined main container target IP: 172.17.0.2
Instance i-42e830289e675885f will be accessible 
via SSH at: 127.0.0.1:22, 172.17.0.3:22
Instance i-42e830289e675885f port mappings (container -> host): 
{'8000/tcp': 8000, '22/tcp': 22}
AWS ec2.RunInstances => 200
...

In the logs above, verify that the instance is accessible via SSH at 127.0.0.1. Depending on your setup, this configuration might change. In this example, you can use the following command to log in to the EC2 instance:

ssh -i key.pem root@127.0.0.1

You'll be prompted to establish authenticity. After verification, you can log in to the instance.

The authenticity of host '127.0.0.1 (127.0.0.1)' can't be established.
ECDSA key fingerprint is SHA256:......
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '127.0.0.1' (ECDSA) to the list of known hosts.
root@6ef2a4c8d8ac:~#

In the local EC2 instance, you can execute various commands, similar to a real EC2 instance on the AWS cloud. You can also use cURL to confirm if your Flask API is operational:

root@6ef2a4c8d8ac:~# curl localhost:8000 
Hello, LocalStack!

Let's attempt to access the running Flask API outside of the EC2 instance.

Test the running Flask API

In the LocalStack logs, confirm that the port mappings from the container to the host are accessible on port 8000. Run the following command in a separate terminal tab to check if the Flask API is reachable:

curl localhost:8000
Hello, LocalStack!

Additionally, send GET and POST requests to test the active Flask API:

curl -X POST \
    -H "Content-Type: application/json" \
    -d '{"key": "value"}' \
    localhost:8000/post
This is a POST request example. Received data: {'key': 'value'}

curl http://localhost:8000/get
This is a GET request example.

To validate the execution of your user data shell script, use the following command in your active EC2 instance:

cat /var/log/cloud-init-output.log
...
+ python3 app.py
 * Serving Flask app 'app'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:8000
 * Running on http://172.17.0.3:8000
Press CTRL+C to quit
127.0.0.1 - - [15/Feb/2024 07:51:02] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [15/Feb/2024 07:51:06] "GET / HTTP/1.1" 200 -
...

Terminate the instance

To terminate the instance, stop the LocalStack container with the command:

localstack stop

However, if you need to examine the container filesystem for debugging later on you can configure EC2_REMOVE_CONTAINERS=0 while starting LocalStack. This configuration option controls whether Docker containers created during the process are removed at instance termination or when LocalStack shuts down.

Conclusion

You can create additional EC2 instances using Terraform, CDK, or Pulumi. For a visual user interface for EC2 instance creation, utilize the LocalStack Web Application. You can also set up EBS Block Devices, Instance Metadata Service, and Elastic Load Balancers for your locally running EC2 instances. We are actively enhancing our EC2 emulation features to facilitate faster local development and testing on your machine!

ย