---
# System prepended metadata

title: AWS Lambda - Localstack 輔助開發工具
tags: [Tools, Localstack, AWS Lambda, Google Cloud Functions, Serverless]

---

{%hackmd xIp4k0sIS6SuPecdKUPMlw %}

[TOC]


---
> [name=Alvin] [time=Now]

# aws Lambda - Localstack 輔助開發工具

## 動機
![image](https://hackmd.io/_uploads/rJEw0DlQR.png)

開發 `Serverless` 應用程序時，傳統的開發方式可能會遇到一些挑戰。特別是當我們使用 `aws Lambda` 時，每次進行測試都需要部署到雲端環境，這樣的開發流程效率相對較低，且成本也相對較高。

因此，開發者通常會尋找一種在本地環境中測試 `Lambda` 函式的方法，以提高開發效率和降低成本。

## 目的
為了實現在 `localhost` 執行 `Lambda` 這一目標，選擇 `Localstack` 作為開發工具，它可以模擬 `aws` 雲端環境的大部分功能。同時，選擇了 `NestJS` 的 `serverless-offline` 插件，可以與 `Localstack` 無縫集成，使得在本地環境中測試和 `debug`，而無需實際部署到 `aws Lambda`(只是改成部署到本地端的 `Localstack` 工具上)。
通過上述方式，可以達到降低開發成本，提高開發效率，並更快地交付高質量的 `Serverless` 程式代碼。

## :memo: Environment

- [x] Mac OS
- [x] Docker
- [x] [Localstack](https://app.localstack.cloud/inst/default/status)
- [x] NestJS
- Serverless Plugins
  - [x] [serverless-localstack](https://www.npmjs.com/package/serverless-localstack) 
  - [x] [serverless-offline](https://www.npmjs.com/package/serverless-offline#run-modes) 
  - [ ] ~~[serverless-offline-sqs](https://www.npmjs.com/package/serverless-offline-sqs)~~
  

### Setup

- `docker_compose.yml`
``` yml=
version: "1.0"
services:
  localstack:
    hostname: mylocalstack
    container_name: "localstack-main"
    image: localstack/localstack:latest
    network_mode: "bridge"
    privileged: true
    ports:
      - "0.0.0.0:4566:4566"
      - "0.0.0.0:4510-4559:4510-4559"
    environment:
      - DEBUG=1
      - SERVICES=sqs, sns, lambda, iam, logs, cloudformation
      - DATA_DIR=/tmp/localstack/data
      - HOSTNAME=localstack
      - HOSTNAME_EXTERNAL=localstack
    volumes:
      - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
      - "/var/run/docker.sock:/var/run/docker.sock"
```
---

- `serverless.yml`
``` yml=
{
  "service": "serverless-setup",
  "provider": {
    "memorySize": 128, // 記憶體大小
    "timeout": 10,
    "name": "aws",
    "runtime": "nodejs20.x", // NodeJS 版號
    "region": "${self:custom.env.REGION}",
    "iam": { // 權限
      "role": {
        "statements": [
          {
            "Effect": "Allow",
            "Action": ["*"],
            "Resource": "*"
          },
        ]
      }
    },
    // "deploymentBucket": {
    //   "serverSideEncryption": "AES256",
    //   "name": "my-s3-bucket"
    // },
    "tracing": {
      "lambda": true
    }
  },
  "package": {
    "individually": true
  },
  "plugins": [ // 注入要使用的插件
    "serverless-localstack",
    "serverless-offline",
    // "serverless-lift",
    // "serverless-offline-sqs",
  ],
  "functions": {
    "tester": {
      "name": "tester",
      // "timeout": 10, // # optional, in seconds, default is 6
      "handler": `dist/apps/mailer/src/main.tester`,
      "environment": {
        "QUEUE_URL": "${self:custom.env.QUEUE_URL}"
      },
    },
    "sender": {
      "name": "sender",
      "maximumEventAge": 60,
      "maximumRetryAttempts": 0,
      // "timeout": 30, // # optional, in seconds, default is 6
      "reservedConcurrency": 1,
      "handler": `dist/apps/mailer/src/main.sender`,
      "url": true,
      "environment": {
        "QUEUE_URL": "${self:custom.env.QUEUE_URL}"
      },
      "events": [
        {
          "sqs": "arn:aws:sqs:${self:custom.env.REGION}:${self:custom.env.ACCOUNT_ID}:${self:custom.env.QUEUE_NAME}"
          // "sqs": {
          //   "arn": "arn:aws:sqs:${self:custom.env.REGION}:${self:custom.env.ACCOUNT_ID}:${self:custom.env.QUEUE_NAME}",
          //   "batchSize": 1,
          //   "maximumBatchingWindow": 1,
          //   "enabled": true,
          // },
          // "sqs": {
          //   "arn": {
          //     "Fn::GetAtt": [
          //       "${self:custom.env.QUEUE_NAME}",
          //       "Arn"
          //     ]
          //   }
          // },
          http: {
            method: 'GET',
            path: '/sender',
          },
        },
      ]
    },
  },
  "resources": {
    "Resources": {
      "LocalstackQueue": {
        "Type": "AWS::SQS::Queue",
        "Properties": {
          "QueueName": "${self:custom.env.QUEUE_NAME}",
          "VisibilityTimeout": 60,
          "MessageRetentionPeriod": 3600,
        }
      },
    },
  },
  "custom": {
    "env": {
      "QUEUE_PORT": "4566",
      "QUEUE_NAME": "LocalstackQueue",
      "ACCOUNT_ID": "000000000000",
      "REGION": "us-east-1",
      "QUEUE_URL": "http://host.docker.internal:${self:custom.env.QUEUE_PORT}/${self:custom.env.ACCOUNT_ID}/${self:custom.env.QUEUE_NAME}",
      // "QUEUE_URL": "https://localhost.localstack.cloud:${self:custom.env.QUEUE_PORT}/${self:custom.env.ACCOUNT_ID}/${self:custom.env.QUEUE_NAME}",
    },
    "localstack": {
      "host": "http://localhost/",
      "debug": true,
      "autostart": true,
      "compose_file": "./localstack/compose.yml",
      "stages": [
        "offline",
      ],
      "networks": [
        "host",
        "bridge",
      ],
      "lambda": {
        "mountCode": false
      },
      "docker": {
        "sudo": true,
      }
    },
    "serverless-offline": {
      // "reloadHandler": true,
      "terminateIdleLambdaTime": 50000,
      "useDocker": true,
      "useInProcess": true,
      "noAuth": true,
      "dockerNetwork": 'bridge',
      "httpPort": 3000,
      "host": "0.0.0.0",
      // "lambdaPort": 4566,
      // "host": "localhost",
      "dockerHostServicePath": `${__dirname}/../../`,
    },
    // "serverless-offline-sqs": {
    //   "autoCreate": true,
    //   "apiVersion": "2012-11-05",
    //   "endpoint": "http://0.0.0.0:4566",
    //   "region": "us-east-1",
    //   "accessKeyId": "root",
    //   "secretAccessKey": "root",
    //   "skipCacheInvalidation": false,
    // }
    }
}
```
---

- `handler.ts`
``` typescript=
import { HttpStatus } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Context, Handler } from 'aws-lambda';
import { AwsSqsService } from '@libs/aws-sqs';
import { MailWorkerModule } from './mailer.module';
import { MailWorkerService } from './mailer.service';
import { SQSEvent } from 'aws-lambda';

const { QUEUE_URL } = process.env;

export const sender: Handler = async (
  event: SQSEvent,
  context: Context,
  callback: any,
) => {
  const appContext =
    await NestFactory.createApplicationContext(MailWorkerModule);
  const awsSqsService = appContext.get(AwsSqsService);
  const mailWorkerService = appContext.get(MailWorkerService);

  const queueData = await awsSqsService.fetchMessageFromQueue(QUEUE_URL);

  const legitimateMessages =
    mailWorkerService.filterLegitimateMessage(queueData);
  console.log('legitimateMessages', legitimateMessages);
  await mailWorkerService.send(legitimateMessages);
  return {
    statusCode: HttpStatus.OK,
  };
  return callback(
    null,
    JSON.stringify({
      statusCode: HttpStatus.OK,
      body: {
        queueData,
        event,
      },
    }),
  );
};

export const tester = async function (event, context) {
  const appContext =
    await NestFactory.createApplicationContext(MailWorkerModule);
  const awsSqsService = appContext.get(AwsSqsService);

  const dataMeta = {
    date: new Date(),
    input: event,
  };

  await awsSqsService.sendMessage({
    QueueUrl: QUEUE_URL,
    MessageBody: JSON.stringify(dataMeta),
  });

  return {
    statusCode: HttpStatus.OK,
    body: JSON.stringify(dataMeta),
  };
};
```

### Command

- Run docker compose
  ``` shellscript
  docker compose -f "$path"/compose.yml/ up -d --force-recreate
  ```
> https://app.localstack.cloud/inst/default/status
> ![image](https://hackmd.io/_uploads/BJOd5NeQ0.png)


- Run serverless plugin
  ```shellscript
  yarn run build mailer && export SLS_DEBUG=* && APP=mailer NODE_ENV=dev IS_OFFLINE=true serverless mailer:offline start --stage=offline --noTimeout
  ```
  ![image](https://hackmd.io/_uploads/SyX4RVemC.png)



- Invoke function
  ``` shellscript
  APP=mailer NODE_ENV=dev IS_OFFLINE=true serverless mailer:invoke --stage=offline --function=tester -d '{}' -l
  ```
  ![image](https://hackmd.io/_uploads/B1Tp9OxXR.png)



:rocket: Refer:

> - [Docker Compose](https://docs.docker.com/compose/gettingstarted/)
> - [Localstack](https://www.localstack.cloud/)
> - [Localstack Dashboard](https://app.localstack.cloud/dashboard)
> - [plugin - serverless-localstack](https://www.npmjs.com/package/serverless-localstack)
> - [plugin - serverless-offline](https://www.npmjs.com/package/serverless-offline#run-modes)
> - [Serverless Document](https://www.serverless.com/framework/docs)
> - [aws Lambda - invoke cli](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/invoke.html)
