LocalStack - Mock AWS in local development
Tags: webdev, cloud, backend, aws
Published: 2023-10-11 04:42:48

Most of us are familiar with Amazon Web Services (AWS) and have probably used their cloud computing services at some point in our careers. AWS is a go-to solution for many developers when it comes to building and deploying applications on the cloud.

However, working with AWS can come with its challenges, particularly when it comes to testing and development. While AWS offers a wide range of powerful services and tools, navigating through the vast array of options and configuring them correctly can be complex and costly. As AWS provides a pay-as-you-go pricing model, the costs can quickly escalate if resources are not managed effectively while developing and testing. Herein lies the role of LocalStack.

![AWS Services](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e0b4c7vk61sn9yulles8.png)


## What is LocalStack?

LocalStack is an open-source project that provides a fully functional local AWS cloud stack. It allows developers to test and develop their applications offline without needing an internet connection or any cloud resources. LocalStack allows developers to emulate the behavior of AWS services such as S3, Lambda, DynamoDB, and more, all within a local environment, eliminating the need for an AWS account.

## Features and Capabilities

- **Simulation of entire AWS Services:** LocalStack enables developers to simulate the entire AWS ecosystem within its local environment.
- **Offline Development:** With LocalStack, developers can work offline without relying on an internet connection or AWS account.
- **Cost Savings:** By simulating AWS services locally, LocalStack eliminates the costs associated with running resources in the cloud.
- **Easy Setup and Configuration:** LocalStack simplifies the setup process with a user-friendly command-line interface (CLI) and APIs.
- **Realistic Testing Scenarios:** By emulating the actual behavior of AWS services, LocalStack allows developers to create realistic testing scenarios. This early identification and resolution of potential issues lead to the development of more robust and reliable applications.

![Flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s6j2shse2wfsrwqhoeel.jpg)

## Installation & Setup

There are several ways to install LocalStack on our local machine.

- **[LocalStack CLI](https://docs.localstack.cloud/getting-started/installation/#localstack-cli)**
- **[LocalStack Cockpit](https://docs.localstack.cloud/getting-started/installation/#localstack-cockpit)**
- **[Docker](https://docs.localstack.cloud/getting-started/installation/#docker)**
- **[Docker-Compose](https://docs.localstack.cloud/getting-started/installation/#docker-compose)**
- **[Helm](https://docs.localstack.cloud/getting-started/installation/#helm)**

We will use the `Docker-Compose` method in this article. Docker and docker-compose should be installed on our machine as a prerequisite.

- Create a `docker-compose.yml` file and paste the following contents.
  ```yaml
    version: "3.8"
    
    services:
      localstack:
        image: localstack/localstack
        ports:
          - "127.0.0.1:4566:4566"
          - "127.0.0.1:4510-4559:4510-4559"
        environment:
          - DEBUG=${DEBUG-}
          - DOCKER_HOST=unix:///var/run/docker.sock
        volumes:
          - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
          - "/var/run/docker.sock:/var/run/docker.sock"
  ```
   
- Start the container by running `docker-compose up`.

- LocalStack should be started at [`http://localhost:4566`](http://localhost:4566/).

- To check it’s running properly, check the health by visiting [`https://localhost:4566/health`](https://localhost:4566/health).
    
    ![health](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x312xeiz9wojsas74yq7.png)

## Limitation

Before continuing the article, we should also know about the limitations of LocalStack. One of the major limitations of LocalStack is that the data are not persistent in the free/community version. Meaning, that every time we stop LocalStack, the data including the configuration we ran via command, the buckets, and the databases, all will be removed and it will start fresh on the next start.

## Usage

We are going to create a Lambda function and configure a trigger for Amazon S3 to demonstrate how LocalStack can be used for AWS mocking. When a new image is uploaded to the S3 bucket, the Lambda function should create a thumbnail of that image in the same bucket.

### Install AWS CLI

As we are mocking AWS, the official AWS CLI should be installed on our machine. If it’s not installed, follow [this guide](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) to install it first. After installing it, we will need to configure it as a first-time user by running `aws configure`. Set any value as *Access Key ID* and *Secret Access Key* and set a *Default Region* (e.g. ap-northeast-2). If you use your original Access key and secret, it’s okay too, as LocalStack will ignore it ultimately.

#### How will the AWS CLI communicate with LocalStack?

By default, when we run a command in AWS CLI, it will try to communicate with AWS API. But here, we want to communicate with LocalStack instead AWS API. 

When running any AWS command, if we specify the endpoint, it will send the requests to that endpoint instead of the default endpoint.

For example, let’s run a command `aws s3api list-buckets --endpoint-url=http://localhost:4566`. 

![Bucket List](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tp6msmncrg6guph4gkbr.png)

It displays the bucket list from LocalStack and does not throw any error even though we used dummy data as the AWS configuration.

#### How will the AWS SDK communicate with LocalStack?

We can also use LocalStack in our AWS SDKs. We just have to point to the endpoint while initilizting the SDK. Here’s an example of JavaScript `aws-sdk`:

```jsx
const AWS = require('aws-sdk');

const s3 = new AWS.S3({
  endpoint: 'http://localhost:4566', // Only needed for localstack
  s3ForcePathStyle: true, // Only needed for localstack
});
```

Then we can use the **`s3`** object to interact with the S3 service locally through LocalStack.

### Create the Lambda function

- Open a new terminal and create a directory by running `mkdir lambda-src`.
- Open the directory by running `cd lambda-src`.

- Create a new file `package.json` and paste the following code:
    
    ```json
    {
      "main": "index.js",
      "dependencies": {
        "aws-sdk": "^2.1376.0",
        "sharp": "^0.32.1"
      }
    }
    ```
    
- Run `npm install` to install the dependencies.
- Create a new file `index.js` and paste the following code:
    
    ```jsx
    const AWS = require('aws-sdk');
    const sharp = require('sharp');
    
    const WIDTH = 64;
    
    const s3 = new AWS.S3({
      endpoint: `http://${process.env.LOCALSTACK_HOSTNAME}:${process.env.EDGE_PORT}`, // Only needed for localstack
      s3ForcePathStyle: true, // Only needed for localstack
    });
    
    exports.handler = async (event) => {
      try {
        const srcBucket = event.Records[0].s3.bucket.name;
        const srcKey = decodeURIComponent(
          event.Records[0].s3.object.key.replace(/\+/g, ' ')
        );
    
        if (srcKey.includes(`${WIDTH}x${WIDTH}`)) {
          console.info('Skipping processed image.');
          return;
        }
    
        const destinationKey = srcKey.replace(
          /(\.[^.]*)?$/,
          `-${WIDTH}x${WIDTH}$1`
        ); // output will be name-200x200.png
    
        const typeMatch = srcKey.match(/\.([^.]*)$/);
        if (!typeMatch) {
          console.error('Could not determine the image type.');
          return;
        }
    
        const imageType = typeMatch[1].toLowerCase();
        if (imageType != 'jpg' && imageType != 'png' && imageType != 'jpeg') {
          console.error(`Unsupported image type: ${imageType}`);
          return;
        }
    
        const params = {
          Bucket: srcBucket,
          Key: srcKey,
        };
    
        const originalImage = await s3.getObject(params).promise();
    
        const buffer = await sharp(originalImage.Body).resize(WIDTH).toBuffer();
    
        const destinationParams = {
          Bucket: srcBucket,
          Key: destinationKey,
          Body: buffer,
          ContentType: 'image',
        };
    
        await s3.putObject(destinationParams).promise();
    
        console.log('Successfully resized ' + srcBucket + '/' + srcKey +
            ' and uploaded to ' + srcBucket + '/' + destinationKey
        );
      } catch (error) {
        console.error(error);
        return;
      }
    };
    ```
    
- Then zip the directory. If you have `zip` installed, you can run:
    
    ```bash
    zip -r function.zip .
    ```
    
- By running `cd ..` return to the root directory.
- Create the Lambda function by running:
    
    ```bash
    aws --endpoint-url=http://localhost:4566 \
    lambda create-function --function-name CreateThumbnail \
    --zip-file fileb://lambda-src/function.zip --handler index.handler --runtime nodejs16.x \
    --role arn:aws:iam::000000000000:role/lambda-s3-role
    ```
    
- Create a S3 bucket:
    
    ```bash
    aws --endpoint-url=http://localhost:4566 s3 mb s3://avatar-bucket
    ```
    
- Create a file `notification-configuration.json`:
    
    ```json
    {
      "LambdaFunctionConfigurations": [
        {
          "LambdaFunctionArn": "arn:aws:lambda:ap-northeast-2:000000000000:function:CreateThumbnail",
          "Events": ["s3:ObjectCreated:*"]
        }
      ]
    }
    ```
    
- Configure Amazon S3 to publish events:
    
    ```bash
    aws --endpoint-url=http://localhost:4566 \
    s3api put-bucket-notification-configuration --bucket avatar-bucket \
    --notification-configuration file://notification-configuration.json
    ```
    
- Copy an image to the directory and upload the image to the S3 bucket:
    
    ```bash
    aws --endpoint-url=http://localhost:4566 \
    s3api put-object --bucket avatar-bucket \
    --key test-image.png --body=test-image.png
    ```
    
- Check the log:
    
    ```bash
    aws --endpoint-url=http://localhost:4566 logs tail '/aws/lambda/CreateThumbnail' --follow
    ```
    
- To update the code:
    
    ```bash
    aws --endpoint-url=http://localhost:4566 lambda update-function-code --function-name CreateThumbnail --zip-file fileb://lambda-src/function.zip
    ```
    

Now, a thumbnail will be created in the same bucket whenever a new image is uploaded. Once our development and testing phase is complete, proceed to replicate these actions in the production environment. Do remember to remove the `endpoint` and `s3ForcePathStyle` from the code as they are only needed to communicate with LocalStack.

## References

- https://localstack.cloud
- https://docs.localstack.cloud
- Cover image from https://localstack.cloud
- AWS Services image from https://allcode.com/top-aws-services