Localstack is a Python module which is used to launch locally many popular AWS services like S3, DynamoDB, Kinesis, etc.

If your micro-service application uses AWS services, before running integration tests, you can deploy Localstack along with your application to mock these AWS services. This would help you to save a lot of money if these integration tests run on a regular basis.

In this demo, we will deploy Localstack to Minikube (a local Kubernetes cluster) using Helm.

Prerequisites Link to heading

  • Helm
  • Kubectl
  • Minikube
  • AWS CLI

1. Launch Minikube Link to heading

Make sure Minikube launched successfully and you can connect to the cluster using kubectl

$ minikube start
😄  minikube v1.28.0 on Darwin 12.3.1
✨  Using the docker driver based on existing profile
👍  Starting control plane node minikube in cluster minikube
🚜  Pulling base image ...
🔄  Restarting existing docker container for "minikube" ...
🐳  Preparing Kubernetes v1.25.3 on Docker 20.10.20 ...
🔎  Verifying Kubernetes components...
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🌟  Enabled addons: storage-provisioner, default-storageclass
🏄  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default

$ kubectl get pods -n kube-system
NAME                               READY   STATUS    RESTARTS      AGE
coredns-565d847f94-hcwt9           1/1     Running   1 (14h ago)   15h
etcd-minikube                      1/1     Running   1 (82m ago)   15h
kube-apiserver-minikube            1/1     Running   1 (82m ago)   15h
kube-controller-manager-minikube   1/1     Running   1 (14h ago)   15h
kube-proxy-hjrld                   1/1     Running   1 (82m ago)   15h
kube-scheduler-minikube            1/1     Running   1 (82m ago)   15h
storage-provisioner                1/1     Running   2 (82m ago)   15h

2. Create Localstack Helm chart Link to heading

Create a localstack directory as following

.
└── localstack
    ├── Chart.yaml
    ├── templates
    │   ├── _helpers.tpl
    │   ├── deployment.yaml
    │   ├── pvc.yaml
    │   └── service.yaml
    └── values.yaml

2 directories, 6 files

Chart.yaml

apiVersion: v1
description: Localstack chart
name: localstack
version: 0.0.0-dev0+0.local

values.yaml

k8s:
  serviceType: ClusterIP
  pvcEnabled: false

pvc:
  size: 1Gi

localstack:
  tag: 1.3.1
  debugEnabled: 0
  eagerServiceLoadingEnabled: 1
  activeServices: s3
  service:
    name: edge
    port: 4566

resources:
  memLimit: 2Gi
  cpuLimit: 1000m

deploy:
  startupProbe:
    enabled: true
    failureThreshold: 10
  livenessProbe:
    enabled: true
    periodSeconds: 60

_helpers.tpl

{{/*
Expand the name of the chart.
*/}}
{{- define "localstack.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "localstack.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}

{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "localstack.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Common labels
*/}}
{{- define "localstack.labels" -}}
helm.sh/chart: {{ include "localstack.chart" . }}
{{ include "localstack.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{/*
Selector labels
*/}}
{{- define "localstack.selectorLabels" -}}
app.kubernetes.io/name: {{ include "localstack.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{/*
Create the name of the service account to use
*/}}
{{- define "localstack.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "localstack.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ template "localstack.name" . }}
  labels:
    app: {{ template "localstack.name" . }}
    chart: {{ .Chart.Name }}-{{ .Chart.Version }}
    release: {{ .Release.Name }}
    heritage: {{ .Release.Service }}
spec:
  strategy:
    rollingUpdate:
      maxUnavailable: 0
    type: RollingUpdate
  selector:
    matchLabels:
      app: {{ template "localstack.name" . }}
      release: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app: {{ template "localstack.name" . }}
        release: {{ .Release.Name }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: 'localstack/localstack:{{ .Values.localstack.tag }}'
          env:
            - name: EAGER_SERVICE_LOADING
              value: {{ .Values.localstack.eagerServiceLoadingEnabled | quote}}
            - name: SERVICES
              value: {{ .Values.localstack.activeServices }}
            - name: HOSTNAME_EXTERNAL
              value: '{{ template "localstack.name" . }}.{{ .Release.Namespace }}'
            - name: DEBUG
              value: {{ .Values.localstack.debugEnabled | quote}}
          ports:
            - name: {{ .Values.localstack.service.name }}
              containerPort: {{ .Values.localstack.service.port }}
              protocol: TCP
          resources:
            requests:
              memory: {{ .Values.resources.memLimit }}
              cpu: {{ .Values.resources.cpuLimit }}
            limits:
              memory: {{ .Values.resources.memLimit }}
              cpu: {{ .Values.resources.cpuLimit }}
          volumeMounts:
            - mountPath: /var/lib/localstack
              name: data
          {{- if .Values.deploy.startupProbe.enabled }}
          startupProbe:
            httpGet:
              scheme: HTTP
              path: /_localstack/health
              port: 4566
            failureThreshold: {{ .Values.deploy.startupProbe.failureThreshold }}
          {{- end }}
          {{- if .Values.deploy.livenessProbe.enabled }}
          livenessProbe:
            httpGet:
              scheme: HTTP
              path: /_localstack/health
              port: 4566
            periodSeconds: {{ .Values.deploy.livenessProbe.periodSeconds }}
          {{- end }}
      volumes:
        - name: data
      {{- if .Values.k8s.pvcEnabled }}
          persistentVolumeClaim:
            claimName: {{ template "localstack.name" . }}
      {{- else }}
          emptyDir: {}
      {{- end }}

pvc.yaml

{{ if .Values.k8s.pvcEnabled -}}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: {{ template "localstack.name" . }}
  labels:
    app: {{ template "localstack.name" . }}
    chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    release: {{ .Release.Name }}
    heritage: {{ .Release.Service }}
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: {{ .Values.pvc.size }}
{{- end }}

service.yaml

apiVersion: v1
kind: Service
metadata:
  name: {{ template "localstack.name" . }}
  labels:
    app: {{ template "localstack.name" . }}
    chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    release: {{ .Release.Name }}
    heritage: {{ .Release.Service }}
spec:
  selector:
    app: {{ template "localstack.name" . }}
    release: {{ .Release.Name }}
  type: {{ .Values.k8s.serviceType }}
  ports:
    - name: {{ .Values.localstack.service.name }}
      port: {{ .Values.localstack.service.port }}

3. (Optional) Build a Helm archive Link to heading

You can build a Helm archive and push this archive to your artifact repository if you have a CI/CD pipeline to manage different versions of Localstack.

$ helm package localstack --version 1.0.0-dev1 --destination .

4. Deploy Localstack chart to Kubernetes cluster Link to heading

We may want to set k8s.serviceType to NodePort to override the default ClusterIP value. This will help us to access Localstack from the outside of the Kubernetes cluster.

$ helm install localstack localstack --set k8s.serviceType=NodePort

Make sure the deployment looks good

$ kubectl get all
NAME                            READY   STATUS    RESTARTS      AGE
pod/localstack-547d6564-4bbrx   1/1     Running   1 (19m ago)   70m

NAME                 TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)          AGE
service/kubernetes   ClusterIP   10.96.0.1     <none>        443/TCP          15h
service/localstack   NodePort    10.98.19.12   <none>        4566:30674/TCP   70m

NAME                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/localstack   1/1     1            1           70m

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/localstack-547d6564   1         1         1       70m

5. Get your Localstack endpoint Link to heading

Since we’re running a local Kubernetes cluster, you cannot directly connect to Localstack using the format <NODE_IP>:<NODE_PORT>. We have to ask Minikube to set up the service to have a Localstack endpoint

$ minikube service localstack --url
http://127.0.0.1:62262

6. Validation Link to heading

Now with Localstack is running and its services are ready, we can run aws CLI with --endpoint-url pointing to the Localstack endpoint. For the region, you can use any dummy value.

$ aws --region=us-east-1 \
    --endpoint-url=http://127.0.0.1:62262 \
    s3api create-bucket --bucket my-bucket
{
    "Location": "/my-bucket"
}

$ aws --region=us-east-1 \
    --endpoint-url=http://127.0.0.1:62647 \
    s3 ls
2021-08-31 15:20:31 my-bucket

If you face authentication error, just need to export a dummy AWS credentials and try to create/list S3 buckets again

export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"