min read

How to deploy Localstack with Pulumi

Intro

If you are developing a SaaS application and you are using Pulumi to create your infrastructure, you might want to test your services during development locally, which is where Localstack comes into play.

Localstack is a Docker container that contains reference implementations of many AWS services for demo purposes and allows you to emulate AWS services locally.

Localstack with Pulumi

When using Pulumi to spin up Amazon AWS services as your application´s infrastructure, you probably want to do that also locally so your application can be more or less started on your Laptop before you move on to the live environment that costs you actual money every hour.

For that purpose we are first creating a ComponentResource to encapsulate all depending cloud resources in one class.

import {ComponentResource} from "@pulumi/pulumi";

export class LocalStack extends ComponentResource {

    constructor(name: string, opts: ComponentResourceOptions, provider: Provider) {
        super("project:infrastructure:LocalStack", name, {}, opts);
    }
    
}

As each AWS service from Localstack is exposed over a separate port, we need to generate two lists of port definitions based on a range of ports given, one for our Deployment and another one for our Service. As both required objects differ, we need two separate schemas.

    const portRangeStart = 4567;
    const portRangeEnd = 4599;
    
    const deploymentPorts = [];
    const servicePorts = [];
        
    for (let port = portRangeStart; port <= portRangeEnd; port++) {
        deploymentPorts.push({containerPort: port, hostPort: port});
        servicePorts.push({name: `${port}`, port: port, targetPort: port});
    }

Putting all together we are getting a lean Pulumi CustomResource wraps our Localstack deployment.

import {ComponentResource, ComponentResourceOptions} from "@pulumi/pulumi";
import {Service} from "@pulumi/kubernetes/core/v1";
import {Deployment} from "@pulumi/kubernetes/apps/v1";
import {Provider} from "@pulumi/kubernetes";


export class LocalStack extends ComponentResource {

    constructor(name: string, opts: ComponentResourceOptions, provider: Provider) {
        super("project:infrastructure:LocalStack", name, {}, opts);


        const appName = `${name}-localstack`;
        const appLabels = {app: appName};

        const portRangeStart = 4567;
        const portRangeEnd = 4599;


        const deploymentPorts = [];
        const servicePorts = [];

        for (let port = portRangeStart; port <= portRangeEnd; port++) {
            // @ts-ignore
            deploymentPorts.push({containerPort: port, hostPort: port});
            // @ts-ignore
            servicePorts.push({name: `${port}`, port: port, targetPort: port});
        }

        const localstackDeployment = new Deployment(appName, {
            metadata: {
                namespace: 'default'
            },
            spec: {
                replicas: 1,
                selector: {matchLabels: appLabels},
                template: {
                    metadata: {labels: appLabels},
                    spec: {
                        containers: [
                            {
                                name: appName,
                                image: 'localstack/localstack',
                                ports: deploymentPorts,
                                 env: [
                                        {
                                            name: 'SERVICES',
                                            value: 'apigateway,lambda,iam,sts,s3,dynamodb'
                                        },
                                        {
                                            name: 'LAMBDA_EXECUTOR',
                                            value: 'docker-reuse'
                                        },
                                        {
                                            name: 'PORT_WEB_UI',
                                            value: '8080'
                                        }
                                    ]
                            }
                        ]
                    }
                }
            }
        }, {parent: this, provider: provider})

        new Service(appName, {
            metadata: {
                labels: localstackDeployment.spec.template.metadata.labels,
                namespace: 'default'
            },
            spec: {
                selector: appLabels,
                ports: servicePorts,
            }
        }, {parent: this, provider: provider})
    }


}

The next step is now to tell pulumi/aws to connect to your Localstack deployment instead of the real AWS backend. In order to do so, we need to change all endpoints of the individual services manually by using this example https://github.com/demiban/pulumi-localstack-lambda-example .

First we create a JSON object that contains all AWS service endpoints and save that to localstack-endpoints.json

{
  "APIGateway": "http://localhost:4567",
  "CloudFormation": "http://localhost:4581",
  "CloudWatch":"http://localhost:4582",
  "CloudWatchLogs":"http://localhost:4586",
  "DynamoDB": "http://localhost:4569",
  "DynamoDBStreams": "http://localhost:4570",
  "Elasticsearch": "http://localhost:4571",
  "ES": "http://localhost:4578",
  "Firehose": "http://localhost:4573",
  "IAM": "http://localhost:4593",
  "Kinesis": "http://localhost:4568",
  "KMS": "http://localhost:4584",
  "Lambda": "http://localhost:4574",
  "Redshift": "http://localhost:4577",
  "Route53": "http://localhost:4580",
  "StepFunctions": "http://localhost:4585",
  "S3": "http://localhost:4572",
  "SES": "http://localhost:4579",
  "SNS": "http://localhost:4575",
  "SQS": "http://localhost:4576",
  "SSM": "http://localhost:4583",
  "STS": "http://localhost:4592"
}

Usage of the Localstack component

We can now create a new Localstack instance, so the Docker container will be started and load our localstack-endpoints.js file that contains our endpoint configurations. With that we are overriding the AWS provider configuration to tell Pulumi not to contact the AWS Cloud but our local "Fake AWS" instead.

const endpoints = require("./localstack/localstack-endpoints.json");

const REGION = aws.EUWest1Region;


let localstack = new LocalStack("pulumi-aws-localstack", {}, provider);

awsProvider = new aws.Provider("localstack", {
    skipCredentialsValidation: true,
    skipMetadataApiCheck: true,
    s3ForcePathStyle: true,
    accessKey: "mockAccessKey",
    secretKey: "mockSecretKey",
    region: REGION,
    endpoints: [{
        apigateway: endpoints.APIGateway,
        cloudformation: endpoints.CloudFormation,
        cloudwatch: endpoints.CloudWatch,
        cloudwatchlogs: endpoints.CloudWatchLogs,
        dynamodb: endpoints.DynamoDB,
        es: endpoints.ES,
        firehose: endpoints.Firehose,
        iam: endpoints.IAM,
        kinesis: endpoints.Kinesis,
        kms: endpoints.KMS,
        lambda: endpoints.Lambda,
        route53: endpoints.Route53,
        redshift: endpoints.Redshift,
        s3: endpoints.S3,
        ses: endpoints.SES,
        sns: endpoints.SNS,
        sqs: endpoints.SQS,
        ssm: endpoints.SSM,
        sts: endpoints.STS,
    }],
})

With that you can emulate AWS locally with Localstack and test your services in your AWS development environment on your machine.