# 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](https://docs.localstack.cloud/user-guide/integrations/terraform/), [CDK](https://docs.localstack.cloud/user-guide/integrations/aws-cdk/), [AWS CLI](https://docs.localstack.cloud/user-guide/integrations/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](https://github.com/features/codespaces)/[GitPod](https://www.gitpod.io/).

## Running Docker containers as EC2 instances

In the [LocalStack community edition](https://github.com/localstack/localstack), 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

* [LocalStack CLI](https://docs.localstack.cloud/getting-started/installation/#localstack-cli) with a [`LOCALSTACK_AUTH_TOKEN`](https://docs.localstack.cloud/getting-started/auth-token/)
    
* [Docker](https://docs.docker.com/get-docker/)
    
* [AWS CLI](https://docs.aws.amazon.com/cli/v1/userguide/cli-chap-install.html) with [`awslocal` wrapper](https://docs.localstack.cloud/user-guide/integrations/aws-cli/#localstack-aws-cli-awslocal)
    
* [jq](https://jqlang.github.io/jq/) & [cURL](https://curl.se/download.html)
    

> You can sign-up for [LocalStack Hobby Plan](https://app.localstack.cloud/pricing) 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`](http://app.py) and add the following code.

```bash
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](https://gitlab.com/HarshCasper/flask-api-example.git).

### Start your LocalStack container

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

```bash
export LOCALSTACK_AUTH_TOKEN=...
localstack start
```

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

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

 💻 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.

```bash
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](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html). 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:

```bash
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:

```bash
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`](http://rsa.pub), run this command to import it:

```bash
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](https://docs.aws.amazon.com/vpc/latest/userguide/configure-subnets.html) and [VPCs](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-vpc.html) 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:

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

The output will be:

```bash
{
    "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:

```bash
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:

```bash
#!/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:

```bash
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.

```bash
{
    "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:

```bash
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.

```bash
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:

```bash
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.

```bash
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:

```bash
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:

```bash
curl localhost:8000
Hello, LocalStack!
```

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

```bash
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:

```bash
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:

```bash
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](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance), [CDK](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Instance.html), or [Pulumi](https://www.pulumi.com/registry/packages/aws/api-docs/ec2/instance/). For a visual user interface for EC2 instance creation, utilize the [LocalStack Web Application](https://app.localstack.cloud/inst/default/resources/ec2). You can also set up [EBS Block Devices](https://docs.localstack.cloud/user-guide/aws/elastic-compute-cloud/#ebs-block-devices), [Instance Metadata Service](https://docs.localstack.cloud/user-guide/aws/elastic-compute-cloud/#instance-metadata-service), and [Elastic Load Balancers](https://docs.localstack.cloud/user-guide/aws/elastic-load-balancing/) for your locally running EC2 instances. We are actively enhancing our EC2 emulation features to facilitate faster local development and testing on your machine!
