CLOVER🍀

That was when it all began.

Serverless Frameworkの設定にAWS Systems Manager Parameter Storeを使ってみる(LocalStack利用)

これは、なにをしたくて書いたもの?

Severless Frameworkのserverless.ymlAWS Systems Manager Parameter Storeの値を参照できるみたいなので、試してみることに
しました。

合わせて、AWS Lambda関数内からAWS Systems Manager Parameter Storeを参照するようにもしてみましょう。

Serverless Frameworkの設定ファイルからAWS Systems Manager Parameter Storeの値を参照する

Serverless Frameworkの設定ファイル(serverless.yml)からAWS Systems Manager Parameter Storeの値を参照する方法は、
以下に記載があります。

Variables / Reference Variables using the SSM Parameter Store

こんな感じで${ssm:[パラメーター名]}で参照します。

service: ${ssm:/path/to/service/id}-service
provider:
  name: aws
functions:
  hello:
    name: ${ssm:/path/to/service/myParam}-hello
    handler: handler.hello

指定したデータの型がSecureStringStringListのようなものの場合は、自動的に展開されるようです。
SecureStringであれば自動で復号され、その挙動を抑制することもできるようです。

Variables / Reference Variables using the SSM Parameter Store / Resolution of non plain string types

今回は扱いませんが、AWS Secrets Managerから値を参照することもできるようです。指定方法は${ssm:〜}となっており、
AWS Systems Manager Parameter Storeを使う時と同じようです。

Variables / Reference Variables using AWS Secrets Manager

AWS CloudFormationでも同様にAWS Systems Manager Parameter StoreやAWS Secrets Managerから値を参照することができます。

動的な参照を使用してテンプレート値を指定する - AWS CloudFormation

AWS CloudFormationの場合はAWS Systems Managerの値を参照する場合はssmssm-secureAWS Secrets Managerの場合は
secretsmanagerと使い分けが必要なようです。

説明はこれくらいにして、実際に使っていってみましょう。

言語はNode.js+TypeScript、環境はLocalStackを使います。AWS Systems Manager Parameter Storeに格納する情報として、
データベースアクセスも含めることにしましょう。こちらにはMySQLを使用します。

環境

今回の環境は、こちらです。

LocalStack。

$ python3 -V
Python 3.10.6


$ localstack --version
2.1.0

起動。

$ LAMBDA_EXECUTOR=docker-reuse localstack start

AWS CLI

$ awslocal --version
aws-cli/2.12.3 Python/3.11.4 Linux/5.15.0-75-generic exe/x86_64.ubuntu.22 prompt/off

Terraform。Serverless Framework管理外のものは、Terraformで構築することにします。

$ terraform version
Terraform v1.5.1
on linux_amd64

MySQL

 MySQL  localhost:3306 ssl  SQL > select version();
+-----------+
| version() |
+-----------+
| 8.0.33    |
+-----------+
1 row in set (0.0002 sec)

MySQLは172.17.0.3で動作しているものとします。

Terraformで環境を構築する

まずはTerraformで環境を構築していきます。

AWS Lambda関数ではAmazon SNSをサブスクライブすることにして、このトピックを作成することにします。
それからMySQLのデータベースやユーザーもTerraformで作成し、Amazon SNSのARNやMySQLへの接続情報を
AWS Systems Manager Parameter Storeに格納することにしましょう。

つまり、こういうTerraform構成ファイルを作成しました。

main.tf

terraform {
  required_version = "1.5.1"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.5.0"
    }

    mysql = {
      source  = "bangau1/mysql"
      version = "1.10.4"
    }
  }
}

provider "aws" {
  access_key                  = "mock_access_key"
  region                      = "us-east-1"
  secret_key                  = "mock_secret_key"
  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true

  endpoints {
    sns = "http://localhost:4566"
    ssm = "http://localhost:4566"
  }
}

provider "mysql" {
  endpoint = "172.17.0.3:3306"
  username = "root"
  password = "password"
}

resource "mysql_database" "practice" {
  name                  = "practice"
  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_0900_bin"
}

resource "mysql_user" "user" {
  user               = "kazuhira"
  host               = "%"
  plaintext_password = "password"
}

resource "mysql_grant" "grant" {
  user       = mysql_user.user.user
  host       = mysql_user.user.host
  database   = mysql_database.practice.name
  privileges = ["ALL PRIVILEGES"]
}

resource "aws_sns_topic" "my_topic" {
  name = "my-topic"
}

resource "aws_ssm_parameter" "my_topic_arn" {
  name  = "my-topic-arn"
  type  = "SecureString"
  value = aws_sns_topic.my_topic.arn
}

resource "aws_ssm_parameter" "mysql_host" {
  name  = "mysql-host"
  type  = "SecureString"
  value = "172.17.0.3"
}

resource "aws_ssm_parameter" "mysql_port" {
  name  = "mysql-port"
  type  = "SecureString"
  value = "3306"
}

resource "aws_ssm_parameter" "mysql_database" {
  name  = "mysql-database"
  type  = "SecureString"
  value = mysql_database.practice.name
}

resource "aws_ssm_parameter" "mysql_user" {
  name  = "mysql-user"
  type  = "SecureString"
  value = mysql_user.user.user
}

resource "aws_ssm_parameter" "mysql_password" {
  name  = "mysql-password"
  type  = "SecureString"
  value = "password"
}

terraform initして、terraform applyして構築完了。

$ terraform init
$ terraform apply

確認。

$ awslocal ssm describe-parameters --query Parameters[].Name
[
    "mysql-password",
    "mysql-port",
    "mysql-database",
    "mysql-host",
    "mysql-user",
    "my-topic-arn"
]

Serverless Frameworkを使ってAWS Lambda関数を作成する

続いては、Serverless Frameworkを使ってAWS Lambda関数を作成していきます。

Node.jsプロジェクトを作成し、TypeScriptやServerless Frameworkを依存関係に追加

$ npm init -y
$ npm i -D typescript
$ npm i -D serverless

aws-nodejsテンプレートを使って、サービスを作成。

$ npx serverless create --template aws-nodejs

Serverless Framework Commands - AWS Lambda - Create

生成されたserverless.ymlAWS Lambda関数。

serverless.yml

# Welcome to Serverless!
#
# This file is the main config file for your service.
# It's very minimal at this point and uses default values.
# You can always add more config options for more control.
# We've included some commented out config examples here.
# Just uncomment any of them to get that config option.
#
# For full config options, check the docs:
#    docs.serverless.com
#
# Happy Coding!

service: serverless-ssm-parameter-store
# app and org for use with dashboard.serverless.com
#app: your-app-name
#org: your-org-name

# You can pin your service to only deploy with a specific Serverless version
# Check out our docs for more details
frameworkVersion: "3"

provider:
  name: aws
  runtime: nodejs18.x

# you can overwrite defaults here
#  stage: dev
#  region: us-east-1

# you can add statements to the Lambda function's IAM Role here
#  iam:
#    role:
#      statements:
#        - Effect: "Allow"
#          Action:
#            - "s3:ListBucket"
#          Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ]  }
#        - Effect: "Allow"
#          Action:
#            - "s3:PutObject"
#          Resource:
#            Fn::Join:
#              - ""
#              - - "arn:aws:s3:::"
#                - "Ref" : "ServerlessDeploymentBucket"
#                - "/*"

# you can define service wide environment variables here
#  environment:
#    variable1: value1

# you can add packaging information here
#package:
#  patterns:
#    - '!exclude-me.js'
#    - '!exclude-me-dir/**'
#    - include-me.js
#    - include-me-dir/**

functions:
  hello:
    handler: handler.hello
#    The following are a few example events you can configure
#    NOTE: Please make sure to change your handler code to work with those events
#    Check the event documentation for details
#    events:
#      - httpApi:
#          path: /users/create
#          method: get
#      - websocket: $connect
#      - s3: ${env:BUCKET}
#      - schedule: rate(10 minutes)
#      - sns: greeter-topic
#      - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
#      - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx
#      - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx
#      - iot:
#          sql: "SELECT * FROM 'some_topic'"
#      - cloudwatchEvent:
#          event:
#            source:
#              - "aws.ec2"
#            detail-type:
#              - "EC2 Instance State-change Notification"
#            detail:
#              state:
#                - pending
#      - cloudwatchLog: '/aws/lambda/hello'
#      - cognitoUserPool:
#          pool: MyUserPool
#          trigger: PreSignUp
#      - alb:
#          listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/
#          priority: 1
#          conditions:
#            host: example.com
#            path: /hello

#    Define function environment variables here
#    environment:
#      variable2: value2

# you can add CloudFormation resource templates here
#resources:
#  Resources:
#    NewResource:
#      Type: AWS::S3::Bucket
#      Properties:
#        BucketName: my-new-bucket
#  Outputs:
#     NewOutput:
#       Description: "Description for the output"
#       Value: "Some output value"

handler.js

'use strict';

module.exports.hello = async (event) => {
  return {
    statusCode: 200,
    body: JSON.stringify(
      {
        message: 'Go Serverless v1.0! Your function executed successfully!',
        input: event,
      },
      null,
      2
    ),
  };

  // Use this code if you don't use the http event with the LAMBDA-PROXY integration
  // return { message: 'Go Serverless v1.0! Your function executed successfully!', event };
};

AWS Lambda関数の雛形は、不要なので削除しておきます。

$ rm handler.js

続いて、Prettierや型宣言を追加。

$ npm i -D prettier
$ npm i -D @types/node@v18
$ npm i -D @types/aws-lambda

Serverless Frameworkのesbuildプラグインや、LocalStack向けのプラグインも追加。

$ npm i -D serverless-esbuild esbuild
$ npm i -D serverless-localstack

AWS Systems Manager Parameter StoreにアクセスするためにSSMClientを追加し、MySQLにアクセスするためにmysql2
加えます。

$ npm i @aws-sdk/client-ssm
$ npm i mysql2

あとはテスト用にJest、esbuild-jestを追加。

$ npm i -D jest @types/jest
$ npm i -D esbuild-jest

各種設定ファイルも載せておきます。

依存関係。

  "devDependencies": {
    "@types/aws-lambda": "^8.10.119",
    "@types/jest": "^29.5.2",
    "@types/node": "^18.16.18",
    "esbuild": "^0.17.19",
    "esbuild-jest": "^0.5.0",
    "jest": "^29.5.0",
    "prettier": "^2.8.8",
    "serverless": "^3.32.2",
    "serverless-esbuild": "^1.45.1",
    "serverless-localstack": "^1.1.1",
    "typescript": "^5.1.3"
  },
  "dependencies": {
    "@aws-sdk/client-ssm": "^3.359.0",
    "mysql2": "^3.4.1"
  }

scripts

  "scripts": {
    "typecheck": "tsc --project .",
    "typecheck:watch": "tsc --project . --watch",
    "test": "jest",
    "format": "prettier --write ./**/*.{js,ts}"
  },

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "moduleResolution": "node",
    "lib": ["esnext"],
    "noEmit": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": ["./**/*.ts"],
  "exclude": [
    "node_modules/**/*",
    ".serverless/**/*"
  ]
}

tsconfig.typecheck.json

{
  "extends": "./tsconfig",
  "compilerOptions": {
    "baseUrl": "./",
    "noEmit": true
  }
}

.prettierrc.json

{
  "singleQuote": true,
  "printWidth": 120
}

jest.config.js

module.exports = {
  testEnvironment: 'node',
  transform: {
    '^.+\\.tsx?$': 'esbuild-jest',
  },
};

作成したAWS Lambda関数。

subscribe-sns/handler.ts

import { GetParameterCommand, GetParameterCommandInput, SSMClient } from '@aws-sdk/client-ssm';
import { SNSEvent } from 'aws-lambda';
import { randomUUID } from 'crypto';
import mysql from 'mysql2/promise';

function getSsmEndpoint(): string {
  if (process.env['LOCALSTACK_HOSTNAME'] !== undefined) {
    return `http://${process.env['LOCALSTACK_HOSTNAME']}:4566`;
  } else {
    return 'http://localhost:4566';
  }
}

const ssmClient = new SSMClient({
  credentials: {
    accessKeyId: 'test',
    secretAccessKey: 'test',
  },
  region: 'us-east-1',
  endpoint: getSsmEndpoint(),
});

async function getParameter(name: string): Promise<string> {
  const input: GetParameterCommandInput = {
    Name: name,
    WithDecryption: true,
  };

  const output = await ssmClient.send(new GetParameterCommand(input));

  return output.Parameter?.Value!;
}

export const handler = async (event: SNSEvent): Promise<void> => {
  const connection = await mysql.createConnection({
    host: await getParameter('mysql-host'),
    port: parseInt(await getParameter('mysql-port'), 10),
    database: await getParameter('mysql-database'),
    user: await getParameter('mysql-user'),
    password: await getParameter('mysql-password'),
  });

  try {
    await connection.execute(`
create table if not exists subscribe_log(
  id varchar(36),
  message_id varchar(36),
  message text,
  registered_datetime datetime,
  primary key(id)
)
`);

    await connection.beginTransaction();

    for (const record of event.Records) {
      await connection.query(
        `
insert into
  subscribe_log(id, message_id, message, registered_datetime)
values(?, ?, ?, now())
`,
        [randomUUID(), record.Sns.MessageId, record.Sns.Message]
      );

      console.info(
        `[${new Date().toISOString()}] messageId = [${record.Sns.MessageId}], message = [${record.Sns.Message}]`
      );
    }

    await connection.commit();
  } catch (e) {
    await connection.rollback();

    console.log(e);
  } finally {
    await connection.end();
  }
};

Amazon SNSをサブスクライブして、メッセージなどをMySQLへ保存するAWS Lambda関数です。保存先のテーブルは、実行時に
存在していなければ作成します。

MySQLへの接続情報は、AWS Systems Manager Parameter Storeから取得するようにしています。

function getSsmEndpoint(): string {
  if (process.env['LOCALSTACK_HOSTNAME'] !== undefined) {
    return `http://${process.env['LOCALSTACK_HOSTNAME']}:4566`;
  } else {
    return 'http://localhost:4566';
  }
}

const ssmClient = new SSMClient({
  credentials: {
    accessKeyId: 'test',
    secretAccessKey: 'test',
  },
  region: 'us-east-1',
  endpoint: getSsmEndpoint(),
});

async function getParameter(name: string): Promise<string> {
  const input: GetParameterCommandInput = {
    Name: name,
    WithDecryption: true,
  };

  const output = await ssmClient.send(new GetParameterCommand(input));

  return output.Parameter?.Value!;
}

export const handler = async (event: SNSEvent): Promise<void> => {
  const connection = await mysql.createConnection({
    host: await getParameter('mysql-host'),
    port: parseInt(await getParameter('mysql-port'), 10),
    database: await getParameter('mysql-database'),
    user: await getParameter('mysql-user'),
    password: await getParameter('mysql-password'),
  });

エンドポイントは、LocalStack内で動作させている時は環境変数LOCALSTACK_HOSTNAMEを参照するようにして、それ以外は
LocalStackへlocalhostでアクセスできるものとして指定します。

Serverless Framworkの設定ファイル。

serverless.yml

service: serverless-ssm-parameter-store

frameworkVersion: "3"

provider:
  name: aws
  runtime: nodejs18.x
  stage: dev
  region: us-east-1

package:
  individually: true

functions:
  subscribeSns:
    handler: subscribe-sns/handler.handler
    events:
      - sns: ${ssm:my-topic-arn}

custom:
  esbuild:
    bundle: true
    target: node18
    platform: node

plugins:
  - serverless-esbuild
  - serverless-localstack

Amazon SNSのARNは、AWS Systems Manager Parameter Storeから取得するようにしています。

functions:
  subscribeSns:
    handler: subscribe-sns/handler.handler
    events:
      - sns: ${ssm:my-topic-arn}

すでに存在するAmazon SNSトピックをサブスクライブするには、ARNを指定すればよさそうなので。

SNS / Using a pre-existing topic

デプロイ。

$ npx serverless deploy

Amazon SNSへメッセージを送ってみます。

$ TOPIC_ARN=$(awslocal sns list-topics --query Topics[0].TopicArn --output text)
$ awslocal sns publish --topic-arn $TOPIC_ARN --message 'test'
{
    "MessageId": "8db1b998-edcb-4842-ad9b-ac3c00e805d6"
}

ログを確認。

$ awslocal logs tail /aws/lambda/serverless-ssm-parameter-store-dev-subscribeSns
2023-06-28T15:08:34.035000+00:00 2023/06/28/[$LATEST]1c7fb102c5cf2e155b632cf2550a65a4 START RequestId: dce667e0-67ff-4219-b996-2eaca751a07a Version: $LATEST
2023-06-28T15:08:34.042000+00:00 2023/06/28/[$LATEST]1c7fb102c5cf2e155b632cf2550a65a4 2023-06-28T15:08:33.997Z  dce667e0-67ff-4219-b996-2eaca751a07a    INFO    [2023-06-28T15:08:33.996Z] messageId = [8db1b998-edcb-4842-ad9b-ac3c00e805d6], message = [test]
2023-06-28T15:08:34.049000+00:00 2023/06/28/[$LATEST]1c7fb102c5cf2e155b632cf2550a65a4 END RequestId: dce667e0-67ff-4219-b996-2eaca751a07a
2023-06-28T15:08:34.056000+00:00 2023/06/28/[$LATEST]1c7fb102c5cf2e155b632cf2550a65a4 REPORT RequestId: dce667e0-67ff-4219-b996-2eaca751a07a    Duration: 466.38 ms     Billed Duration: 467 ms   Memory Size: 1024 MB    Max Memory Used: 1024 MB

動作していますね。

MySQL側も確認してみましょう。

 MySQL  localhost:3306 ssl  practice  SQL > select * from subscribe_log;
+--------------------------------------+--------------------------------------+---------+---------------------+
| id                                   | message_id                           | message | registered_datetime |
+--------------------------------------+--------------------------------------+---------+---------------------+
| 9d35aecd-ffed-4c3c-a0a9-7bc49e295f2f | 8db1b998-edcb-4842-ad9b-ac3c00e805d6 | test    | 2023-06-28 15:08:33 |
+--------------------------------------+--------------------------------------+---------+---------------------+
1 row in set (0.0011 sec)

こちらもOKですね。

というわけで、AWS Systems Manager Parameter Storeに格納した値をserverless.ymlで参照できることを確認できました。
ついでに、AWS Lambda関数内からAWS Systems Manager Parameter Storeへもアクセスしておいたわけですが。

serverless generate-eventも使ってみましょう。

ローカル呼び出し確認。

$ npx serverless generate-event -t aws:sns -b 'Hello Serverless generate-event and invoke local' | npx serverless invoke local --function subscribeSns
Using serverless-localstack
serverless-localstack: Reconfigured endpoints
[2023-06-28T15:13:00.228Z] messageId = [52ed5e3d-5fgf-56bf-923d-0e5c3b503c2a], message = [Hello Serverless generate-event and invoke local]

デプロイした関数の呼び出し。

$ npx serverless generate-event -t aws:sns -b 'Hello Serverless generate-event and invoke' | npx serverless invoke --function subscribeSns
Using serverless-localstack
serverless-localstack: Reconfigured endpoints
null

データは両方とも入っています。

 MySQL  localhost:3306 ssl  practice  SQL > select * from subscribe_log;
+--------------------------------------+--------------------------------------+--------------------------------------------------+---------------------+
| id                                   | message_id                           | message                                          | registered_datetime |
+--------------------------------------+--------------------------------------+--------------------------------------------------+---------------------+
| 5dfafd57-f8ce-413c-9a12-4e6cc2dd0c3a | 52ed5e3d-5fgf-56bf-923d-0e5c3b503c2a | Hello Serverless generate-event and invoke       | 2023-06-28 15:13:45 |
| 9d35aecd-ffed-4c3c-a0a9-7bc49e295f2f | 8db1b998-edcb-4842-ad9b-ac3c00e805d6 | test                                             | 2023-06-28 15:08:33 |
| aa817921-0053-4970-aece-57788fad47a7 | 52ed5e3d-5fgf-56bf-923d-0e5c3b503c2a | Hello Serverless generate-event and invoke local | 2023-06-28 15:13:00 |
+--------------------------------------+--------------------------------------+--------------------------------------------------+---------------------+
3 rows in set (0.0009 sec)

テストを書く

オマケ的に、最後にテストを書いてみましょう。

以下でテストデータを作成。

$ npx serverless generate-event -t aws:sns -b '{"message": "Hello World!!"}' | jq
{
  "Records": [
    {
      "EventSource": "aws:sns",
      "EventVersion": "1.0",
      "EventSubscriptionArn": "arn:aws:sns:us-east-1:123456789:service-1474781718017-1:fdaa4474-f0ff-4777-b1c4-79b96f5a504f",
      "Sns": {
        "Type": "Notification",
        "MessageId": "52ed5e3d-5fgf-56bf-923d-0e5c3b503c2a",
        "TopicArn": "arn:aws:sns:us-east-1:123456789:service-1474781718017-1",
        "Subject": "",
        "Message": "{\"message\": \"Hello World!!\"}",
        "Timestamp": "2016-09-25T05:37:51.150Z",
        "SignatureVersion": "1",
        "Signature": "V5QL/dhow62Thr9PXYsoHA7bOsDFkLdWZVd8D6LyptA6mrq0Mvldvj/XNtai3VaPp84G3bD2nQbiuwYbYpu9u9uHZ3PFMAxIcugV0dkOGWmYgKxSjPApItIoAgZyeH0HzcXHPEUXXO5dVT987jZ4eelD4hYLqBwgulSsECO9UDCdCS0frexiBHRGoLbWpX+2Nf2AJAL+olEEAAgxfiPEJ6J1ArzfvTFZXdd4XLAbrQe+4OeYD2dw39GBzGXQZemWDKf4d52kk+SwXY1ngaR4UfExQ10lDpKyfBVkSwroaq0pzbWFaxT2xrKIr4sk2s78BsPk0NBi55xA4k1E4tr9Pg==",
        "SigningCertUrl": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-b95095beb82e8f6a0e6b3aafc7f4149a.pem",
        "UnsubscribeUrl": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:123456789:service-1474781718017-1:fdaa4474-f0ff-4777-b1c4-79b96f5a504f",
        "MessageAttributes": {}
      }
    }
  ]
}

こちらを少し加工して、テストでのAWS Lambda関数の呼び出し時のメッセージにします。

subscribe-sns/handler.test.ts

import { SNSEvent } from 'aws-lambda';
import { handler } from './handler';
import mysql from 'mysql2/promise';
import { Connection } from 'mysql2/promise';

let connection: Connection;

beforeAll(async () => {
  connection = await mysql.createConnection({
    host: '172.17.0.3',
    port: 3306,
    database: 'practice',
    user: 'kazuhira',
    password: 'password',
  });
});

afterAll(async () => {
  await connection.end();
});

beforeEach(async () => {
  const [rows] = await connection.query(
    `
select
  count(1) as count
from
  information_schema.tables
where
  table_schema = ?
  and table_name = ?
`,
    ['practice', 'subscribe_log']
  );

  if ((rows as any[])[0].count > 0) {
    await connection.query(`truncate table subscribe_log`);
  }
});

test('subscribe SNS event', async () => {
  const payload: SNSEvent = JSON.parse(`
{
  "Records": [
    {
      "EventSource": "aws:sns",
      "EventVersion": "1.0",
      "EventSubscriptionArn": "arn:aws:sns:us-east-1:123456789:service-1474781718017-1:fdaa4474-f0ff-4777-b1c4-79b96f5a504f",
      "Sns": {
        "Type": "Notification",
        "MessageId": "52ed5e3d-5fgf-56bf-923d-0e5c3b503c2a",
        "TopicArn": "arn:aws:sns:us-east-1:123456789:service-1474781718017-1",
        "Subject": "",
        "Message": "{\\"message\\": \\"Hello World!!\\"}",
        "Timestamp": "2016-09-25T05:37:51.150Z",
        "SignatureVersion": "1",
        "Signature": "V5QL/dhow62Thr9PXYsoHA7bOsDFkLdWZVd8D6LyptA6mrq0Mvldvj/XNtai3VaPp84G3bD2nQbiuwYbYpu9u9uHZ3PFMAxIcugV0dkOGWmYgKxSjPApItIoAgZyeH0HzcXHPEUXXO5dVT987jZ4eelD4hYLqBwgulSsECO9UDCdCS0frexiBHRGoLbWpX+2Nf2AJAL+olEEAAgxfiPEJ6J1ArzfvTFZXdd4XLAbrQe+4OeYD2dw39GBzGXQZemWDKf4d52kk+SwXY1ngaR4UfExQ10lDpKyfBVkSwroaq0pzbWFaxT2xrKIr4sk2s78BsPk0NBi55xA4k1E4tr9Pg==",
        "SigningCertUrl": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-b95095beb82e8f6a0e6b3aafc7f4149a.pem",
        "UnsubscribeUrl": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:123456789:service-1474781718017-1:fdaa4474-f0ff-4777-b1c4-79b96f5a504f",
        "MessageAttributes": {}
      }
    }
  ]
}
`) as SNSEvent;

  await handler(payload);

  const [counts] = await connection.query('select count(1) as count from subscribe_log');

  expect(counts).toStrictEqual([{ count: 1 }]);

  const [rows] = await connection.query(`select id, message_id, message, registered_datetime from subscribe_log`);

  expect(rows).toHaveLength(1);
  expect((rows as any[])[0]).toHaveProperty('message', '{"message": "Hello World!!"}');
});

データは、テストごとにtruncate tableするようにしています。

確認。

$ npm run test

> serverless-ssm-parameter-store@1.0.0 test
> jest

  console.info
    [2023-06-28T15:17:04.959Z] messageId = [52ed5e3d-5fgf-56bf-923d-0e5c3b503c2a], message = [{"message": "Hello World!!"}]

      at handler (subscribe-sns/handler.ts:88:15)

 PASS  subscribe-sns/handler.test.ts
  ✓ subscribe SNS event (524 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.291 s, estimated 2 s
Ran all test suites.

まとめ

Serverless Frameworkで、AWS Systems Manager Parameter Storeから値を参照するようにしてみました。

割とあっさり使えて便利ですね。AWS Lambda関数でこういうハードコードしたくない値かつ簡単に見えないようにしたい値の管理には
こうやってserverless.ymlで参照するか、AWS Lambda関数内の処理そのもので参照することになると思うので、押さえておこうと
思います。