~/post/unleashing-the-power-of-localstack
Published on

Unleashing the Power of LocalStack for Faster AWS Development

6 mins read
Authors
  • Name
    Twitter

AWS is the world’s leading cloud vendor, offering 200+ services operating in 26 regions globally. Beyond the fundamental services proposed by any cloud provider, AWS has a consistent, coherent, and complete environment to innovate, create and develop next-gen applications. This includes APIs, SDKs, CLI tools, etc.

Image having your own local AWS environment in a single Docker Container?

The promise is clear: Run AWS services on your local computer. Interested? Let’s get started!

Table of Contents

What’s LocalStack?

To get started with AWS, you have to create a new AWS account and receive a free tier of $300 for 12 months to practice AWS services.

However, even if you are a seasoned AWS user, you may prefer to have some essential AWS services locally for development, testing, and iteration without the need for an AWS account. This is where LocalStack comes in.

"LocalStack is a cloud service emulator that runs in a single docker container on your local machine. It allows you to run, test, and debug your AWS applications without a connection to a remote cloud provider".

As stated on their Getting Started page, LocalStack supports a number of fundamental AWS services including AWS Lambda, S3, DynamoDB, SQS, and many more.

For even more features and advanced APIs, there is also a Pro version of LocalStack. A comprehensive list of supported APIs can be found on their ☑️ Feature Coverage page.

With this understanding of LocalStack, Let’s delve into what makes it so powerful.

Under the hood

In Software Engineering, when designing API, you should model it first. API Modelling helps you to:

  • Identify the users/actors of your API
  • Establish better communication practices
  • Draw out common understanding (ubiquitous language in DDD)
  • Answer the question of why the API exists.

Let’s take the example of SQS, a message queueing service allowing different software components to exchange messages safely. If we take a look at the service API definition, we can see this:

Let’s take the example of SQS, a message queueing service allowing different software components to exchange messages safely. If we take a look at the service API definition, we can see this:

{
	"CreateQueue":{
	  "name": "CreateQueue",
	  "HTTP": {
	    "method": "POST",
	    "requestUri": "/"
	  },
	  "input":{"shape": "CreateQueueRequest"},
	  "output":{
	    "shape": "CreateQueueResult",
	    "resultWrapper": "CreateQueueResult"
	  },
	  "errors":[
	    {"shape": "QueueDeletedRecently"},
	    {"shape": "QueueNameExists"}
	  ],
	  "documentation": "Creates a new standard or FIFO queue. You can pass one or more attributes in the request ..."
	},
	...
	...
	"CreateQueueRequest":{
		"type": "structure",
    "required":["QueueName"],
		"members": {
			"QueueName":{
                "shape": "String",
                "documentation": "The name of the new queue. The following limits apply to this name: ..."
            },
			"Attributes": {
				"shape": "QueueAttributeMap",
				"documentation": "A map of attributes with their corresponding values...",
				"locationName": "Attribute"
			},
			"tags":{
                "shape": "TagMap",
                "documentation": "Add cost allocation tags to the specified Amazon SQS queue. For an overview, ...",
                "locationName": "Tag"
            }
		}
	}
}

This snippet corresponds to the CreateQueue endpoint of the SQS service. The API takes 03 parameters: QueueName, Attributes, and Tags.

We can find the equivalent implementation in the localstack repo:

@handler("CreateQueue")
def create_queue(
    self,
    context: RequestContext,
    queue_name: String,
    attributes: QueueAttributeMap = None,
    tags: TagMap = None,
) -> CreateQueueResult:
    raise NotImplementedError

...
...

class CreateQueueRequest(ServiceRequest):
    QueueName: String
    Attributes: Optional[QueueAttributeMap]
    tags: Optional[TagMap]

As explained in this article, LocalStack’s mission since day one is to keep parity with AWS. This means, whenever you make an AWS API call to LocalStack’s cloud emulator, it behaves the same way AWS would.

Based on the AWS service protocol definition, the LocalStack Team built a framework named AWS Server Framework (ASF). This latter generates server-side stubs for services and all their supported operations. The API definitions come from the python SDK discussed above. All service requests are then routed to their respective server-side implementation through ASF, which implements the AWS protocol in a generalized way.

There are two levels of Emulation:

  • CRUD: The service accepts requests and returns proper (potentially static) responses. No additional business logic besides storing entities.
  • Emulated: The service imitates the functionality, including synchronous and asynchronous business logic operating on service entities.

Let’s practice!

There are various ways to start and manage your LocalStack instance. As described in the docs, you can use LocalStack CLI, LocalStack Cockpit, Docker, Docker-Compose, or Helm.

In our example, I will use the CLI. This requires Python 3.7 at least.

Install LocalStack with this command:

$ pip install localstack

Then, run it with the command:

$ localstack start -d

The command above starts LocalStack inside a container.

Alternatively, you can directly run LocalStack from the official image with the following command:

$ docker run --rm -it -p 4566:4566 -p 4510-4559:4510-4559 localstack/localstack

After a few seconds, you should see this:

Image description

To have a smooth experience, I recommend you to install the following tools:

# a thin wrapper around the aws cli command that runs the command directly against LocalStack
$ pip install awscli-local

# a thin wrapper around the terraform command line client
$ pip install terraform-local

Additionally, you should install the Docker Desktop LocalStack Extension.

Image description

Once the installation is done, you should see this:

Image description

You can also query the status of respective services on LocalStack with the CLI:

$ localstack status services
Image description

In my previous article, I introduced you to AWS CDK + Terraform (CDKTF) and set up a basic s3 bucket.

Unfortunately, there is no wrapper for CDKTF yet. I will create a basic bucket from python.

import boto3
from pprint import pprint
import glob

client = boto3.resource("s3", endpoint_url="http://s3.localhost.localstack.cloud:4566")

# create a new bucket
blogBucket = client.Bucket('blog.abdelfare.me')

response = blogBucket.create()
pprint(response)

for filename in glob.iglob(f'posts/*'):

	# upload blog article from the "posts" directory
	client.upload_file(filename, filename)

There we go, our bucket was created and you can check it with the following commands:

$ awslocal s3 ls s3://blog.abdelfare.me --recursive
Image description

Your development environment is now ready for cloud development with AWS.

LocalStack is ephemeral in nature and will not persist any data across restarts. You can destroy your local infrastructure with the command:

$ localstack stop

Conclusion

To sum up, LocalStack provides a convenient and practical solution for developing with AWS Cloud. With its ability to emulate some services and provide basic CRUD operations for others, it offers a smooth experience that eliminates any surprises when deploying to a real AWS account.

However, it’s worth noting that LocalStack should only be used for development and initial testing. For validating the performance, reliability, and availability of your system, you should use your real AWS account.