こんにちは。エンジニアの遠藤です。

何度かブログでも紹介していますが、ミルディアでは、 serverlese framework を使った案件を多く扱っています。

私もプロジェクトの立ち上げから何度か参加しており、その度にセットアップをしてきました。
また、プロジェクトを進めていると、色々不便な点が見つかってきます。

そうした過去の失敗や経験を踏まえ、プロジェクトのテンプレートを用意することにしました。

テンプレートは gihutb に公開しています。
https://github.com/milldea/milldea-project-templates/tree/main/serverless-python

公開の背景

元々は社内の private なリポジトリに用意して、内部で使いまわせれば良いかなという程度だったのですが、代表にレビューを依頼したところ以下のようなご意見をいただき、公開することにしました。

俺たちが serverless を始めたときって、本当にシステムまるっと参考にできるものがなくて、困った、不安だった記憶があって。
それが洗練されているかどうかはさておき、こういう形で動くんだ、ってのはすぐ公開する価値があるかなと思いました。

つまり、世界貢献だと。

数年前、 Angular の開発をしている Google のとエンジニアと会話する機会がありました。
確か、 angular.js が大ブームになった後、色々と課題が見つかり、それらを解決するために Angular2 が絶賛開発中といった時期だったと思います。

私はこんな質問をしました。
「どうしてお金にならない OSS の開発をしているのですか?」

回答はこうでした。
「我々は Web の世界を綺麗にしたいのです。綺麗なフレームワークを使って、綺麗なコードが Web に広まるべきだと考えています」

(かっっっっこいいいいいいい!)

いや、まぁ、これは建前で、 Google 検索しやすくなるとか、ビジネス的にも色々理由はあったのでしょうが、とても痺れた記憶があります。

今回の代表の発言も個人的にはそれと同じくらいの衝撃を受けました。

前置きが長くなりましたが、そんなわけで少しでも誰かの役に立てばと、テンプレートを公開することにいたしました。

概要

このプロジェクトは、 serverless framework で python を実行するサンプルになっています。
構築が完了すると、 API Gateway + Lambda が serverless local で実行できる状態になります。

また、 localstack により、dynamodb, s3, secretsmanager などの各種サービスも同時に起動します。

合わせて、dynamoDB, secretsmanager のセットアップ、アクセスするコードのサンプルも用意しています。

基本的には Readme を見てもらえればやることは書いてあるのですが、具体的に、どこでどんなコードが動いているのかを解説しておきます。

動かないな、とかちょっと違う用途に使いたいな、といった時に少しでも参考になればと思います。
またなぜその構成にしたのか、なども可能な範囲で触れておきます。

なお、 localstack は Community 版を使っています。
Pro 版との違いは公式を参照ください。
https://localstack.cloud/features/

実行環境

MacBook Pro Apple M1 Pro
macOS Ventura 13.2.1

プロジェクトの clone

このあたりは特に説明するまでもないですが、まずは local にプロジェクトを clone します。

$ git clone git@github.com:milldea/milldea-project-templates.git
$ cd milldea-project-templates/serverless-python

Docker

このプロジェクトテンプレートは Docker(docker-compose)を使っています。
理由としては、 node や python のバージョンを確実に統一したかったからです。

一度、 serverless framework のバージョンが異なる状態で deploy されたことがありました。
生成するリソース名が変わるという仕様変更があり、本番デプロイ中に各種リソースが再生成されまくってしまい、サービス提供が止まったり、ログが消えたりといった大惨事に遭遇したため、それ依頼 Docker を採用することにしました。

もっというと、 local からデプロイするなよというご指摘もあり、現在では CodeBuild や CodeDeploy などを使って、変な環境依存がないようにしています。

それならば、 local に Docker は不要ではないかという話もありそうですが、開発中にハマった時など、あなたの node のバージョンはいくつだ?OSはなんだ?という不毛なやり取りが減るので、それなりにメリットはあると考えています。

なので、1人プロジェクトなら anyenv だけで事足りるかもしれません。

docker-compose

起動する前に、それぞれの定義ファイルの中身を確認してみます。

本テンプレートでは、 docker-compose を使用し、 serverless framework を起動するコンテナと、 localstack を起動するコンテナを定義しています。
docker-compose.yml

version: "3"
services:
  sls:
    build: .
    container_name: sls_template
    tty: true
    volumes:
      - .:/opt/app
    ports:
      - "3000:3000"
    environment:
      - DEFAULT_REGION=ap-northeast-1
      - LOCALSTACK_HOST=localstack
  localstack:
    image: localstack/localstack:latest
    container_name: localstack_template
    ports:
      - "4566:4566"
    environment:
      - DEBUG=true
      - DEFAULT_REGION=ap-northeast-1
      - HOSTNAME=localstack
      - LOCALSTACK_HOSTNAME=localstack
      - DATA_DIR=${DATA_DIR- }
  dynamodb_admin:
    image: aaronshaf/dynamodb-admin
    tty: true
    container_name: dynamodb_admin
    ports:
      - "8001:8001"
    depends_on:
      - localstack
    environment:
      DYNAMO_ENDPOINT: http://localstack:4566
      AWS_REGION: ap-northeast-1
      AWS_ACCESS_KEY_ID: dummy
      AWS_SECRET_ACCESS_KEY: dummy

sls

container_name を sls_template のままにしておくと、別のプロジェクトでテンプレートを作成した時に重複してしまうので、 sls_your_project_name など、名前を変えておくことをお勧めします。

port の 3000 を定義しているのは、ホストマシンから localhost:3000 で API にアクセスできるようにするためです。

これは、例えば Web 開発をしていて、ブラウザからアクセスする API がある時など、ホストマシンからアクセスする際などに必要となります。

environment は awslocal-cli の設定値を定義しています。
詳しくは後述しますが、この定義をしておくことで、 aws 関連のコマンドを実行する際に、毎回 region や endpoint を指定する必要がなくなります。

localstack

localstack 側は、 4566 ポートを開放してこちらもホストマシンからアクセスできるようにしています。
他に特筆するような定義はしておりません。
以前はサービスごとに port を定義していましたが、今は 4566 ポートですべてのサービスにアクセスできるので、この定義のみで問題ないと思います。

dynamodb_admin

dynamodb 操作を GUI から実行できます。
environment.DYNAMO_ENDPOINT に localstack のエンドポイントを指定することで、 localstack の用意した dynamodb にアクセスできます。
このブログでは、 aws-cli のコマンド操作を紹介していますが、苦手な方もいると思うので、 GUI も合わせて docker-compose にまとめてみました。

そもそも DynamoDB を使わない、という方はこの定義ごと削除いただいて構いません。

Dockerfile

Dockerfile

ベースイメージは、 amazonlinux:2 を使っています。
これは Lambda のランタイムに合わせた設定になっています。(2023/04/20 現在)
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-runtimes.html

他の操作としては yum で serverless framework を実行するために必要なパッケージのインストールと、 anyenv を使って node, python, aws-cli をインストールしています。

どのプロジェクトでも必要になると思われる最低限のセットアップを行っています。

Docker の起動

プロジェクトルートで、以下のコマンドを実行すると、定義した3つのコンテナが起動します。

$ docker-compose up -d --build
[+] Running 4/4
 ⠿ Network serverless-python_default  Created               0.0s
 ⠿ Container sls_template             Started               3.3s
 ⠿ Container localstack_template      Started               0.6s
 ⠿ Container dynamodb_admin           Started               3.1s

Docker の停止

参考までに起動した docker は stop または down することができますが、基本的には stop がお勧めです。
down するとコンテナ内でセットアップした内容や、作成したテーブルなどの情報が全て失われてしまいます。
変なライブラリを入れてしまって、環境が動かなくなった時などは down → up しますが、基本的には stop → start が良いですね。

stop

$ docker-compose stop         
[+] Running 3/3
 ⠿ Container sls_template         Stopped                  10.3s
 ⠿ Container dynamodb_admin       Stopped                  10.3s
 ⠿ Container localstack_template  Stopped                   1.6s

start

$ docker-compose start
[+] Running 3/3
 ⠿ Container sls_template         Started                   2.8s
 ⠿ Container localstack_template  Started                   0.3s
 ⠿ Container dynamodb_admin       Started                   2.4s

down

$ docker-compose down 
[+] Running 4/4
 ⠿ Container sls_template             Removed              10.2s
 ⠿ Container dynamodb_admin           Removed              10.3s
 ⠿ Container localstack_template      Removed               2.0s
 ⠿ Network serverless-python_default  Removed               0.1s

初期セットアップ

Docker を起動したらコンテナに入ります。

$ docker-compose exec sls bash --login

上記の sls の部分は docker-compose.yml の service に該当しますので変更した場合は、上記のコマンドも適宜変更します。

コンテナに入ったらそのまま shell を実行してください。

$ sh init.sh

ファイルの中身を見てみます。

#!/bin/bash
rm -rf .serverless

yarn init -y && yarn install
pip install --upgrade pip
pip install -r requirements.txt

sls requirements install
sls plugin install -n serverless-python-requirements
sls plugin install -n serverless-offline
sls plugin install -n serverless-prune-plugin
sls plugin install -n serverless-localstack

yarn, pip, sls plugin のそれぞれのインストールをしています。
地味に毎回これを実行していくのが面倒なので、ひとまとめにしています。
このあたりも、別のメンバーが環境構築がうまくいかない、といった時に、一つ一つ実行したかを確認しなくても良くなるので、少し効率が上がるかなと思っています。

ただ、ライブラリを追加した時には、こちらもメンテしていく必要があるので、一長一短ではあります。

続けて、 aws-cli の設定もしておきます。
これは、ホストマシンでもコンテナ内でも、どちらも設定しておくと便利です。
aws-cli を使う際に必要になりますので、 aws-cli を実行する環境では必ず設定しておきましょう。

なお、コンテナ内にはすでに aws-cli がインストールされていますが、ホストマシンにインストールされていない場合は、以下の記事などを参考にインストールをしてください。

https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/getting-started-install.html

$ aws configure
AWS Access Key ID [None]: dummy
AWS Secret Access Key [None]: dummy
Default region name [None]: ap-northeast-1
Default output format [None]: json

ここまで実行できれば、一通り local の環境構築が完了です。

Serverless Framework

ここまでの流れがうまくできていれば、 serverless-offline が起動できるはずです。

bash-4.2# sls offline start
Running "serverless" from node_modules
Using serverless-localstack

Starting Offline at stage local (ap-northeast-1)

Offline [http for lambda] listening on http://0.0.0.0:3002
Function names exposed for local invocation by aws-sdk:
           * hello: serverless-python-local-hello
           * setup_dynamodb: serverless-python-local-setup_dynamodb
           * get_secrets: serverless-python-local-get_secrets

   ┌───────────────────────────────────────────────────────────────────────┐
   │                                                                       │
   │   GET | http://0.0.0.0:3000/local/hello                               │
   │   POST | http://0.0.0.0:3000/2015-03-31/functions/hello/invocations   │
   │                                                                       │
   └───────────────────────────────────────────────────────────────────────┘

Server ready: http://0.0.0.0:3000 🚀

上記のような状態になれば、起動成功です。
ホストマシンから、 curl で Lambda を叩いてみましょう。

$ curl localhost:3000/local/hello | jq 
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1700  100  1700    0     0  50197      0 --:--:-- --:--:-- --:--:-- 58620
{
  "message": "Go Serverless v3.0! Your function executed successfully!",
  "input": {
    "body": null,
    "headers": {
      "Host": "localhost:3000",
      "User-Agent": "curl/7.86.0",
      "Accept": "*/*"
    },
    "httpMethod": "GET",
    ...(省略)...
  }
}

上記のような json が返ってくれば、 serverless-offline の起動も成功です。

DynamoDB

テーブルの作成

続けて、 DynamoDB テーブルを local に用意します。
serverless-offline は停止しても問題ありません。

DynamoDB 自体は localstack により起動しているはずなので、確認してみましょう。

コンテナ内からは以下のコマンドで確認できます。

bash-4.2# awslocal dynamodb list-tables 
{
    "TableNames": []
}

空ですが、レスポンスがあることを確認できます。

awslocal というコマンドは、 aws-cli をラップしてオプションの --endpoint-url=http://localhost:4566 を付けなくても叩けるようにしてくれるライブラリです。
このライブラリは requirements.txtawscli-local==0.20として記載しており、初期セットアップでインストールされるようになっています。
また、 docker-compose の項目でも説明しましたが、 environment に必要な定義もしているため、 host や region を新たに設定する必要はありません。

もし、 awscli-local を使わない場合は、以下のコマンドになります。

bash-4.2# aws --endpoint-url http://localstack:4566 dynamodb list-tables 
{
    "TableNames": []
}

詳細は公式をご確認ください。
https://github.com/localstack/awscli-local

DynamoDB が動いていることを確認できたら、テーブルを作成していきます。

テーブルの作成は2通りで実行することができます。
1つは直接、 aws-cli で作成する方法です。

$ awslocal dynamodb create-table --table-name sample --attribute-definitions \
        AttributeName=user_id,AttributeType=S \
        AttributeName=user_name,AttributeType=S \
    --key-schema AttributeName=user_id,KeyType=HASH \
     AttributeName=user_name,KeyType=RANGE \
    --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1

もう一つは、 serverless framework のデプロイ先を localstack にして、 resources に定義したリソースを作成してもらう方法です。

$ sls deploy

簡単なのは後者ですが、個人的には aws-cli で作成する方法をお勧めしたいです。
本筋からは逸れてしまうため、理由は最後に記載します。

では、テーブルの作成が完了したか確認してみましょう。
以下のようなレスポンスが返ってくれば成功です。

bash-4.2# awslocal dynamodb list-tables 
{
    "TableNames": [
        "sample"
    ]
}

Lambda からアクセス

続けて、 Lambda から DynamoDB にアクセスしてみます。

以下の function は DynamoDB にレコードを Put して、インサートしたものを Get しています。

def setup_dynamodb(event, context):
    sample_table = DynamoDB(event).sample_table()
    sample_id = "00001"
    sample_name = "milldea_test"
    item = {
        "user_id": sample_id,
        "user_name": sample_name,
        "other": "sample",
    }
    sample_table.put_item(Item=item)
    response_item = sample_table.get_item(
            Key={
                "user_id": sample_id,
                "user_name": sample_name
            }
        )
    response = {
        "statusCode": 200,
        "body": response_item["Item"]
    }
    return response

Lambad は sls invoke コマンドで実行することができます。
実際に実行してみて、以下のレスポンスが受け取れれば成功です。

bash-4.2# sls invoke local -f setup_dynamodb
Running "serverless" from node_modules
Using serverless-localstack
{
    "statusCode": 200,
    "body": {
        "user_id": "00001",
        "user_name": "milldea_test",
        "other": "sample"
    }
}

dynamodb-admin

冒頭で紹介した GUI からもアクセスできます。
ここまでの手順が完了していると、以下の URL からテーブルの一覧、レコードなどが参照できます。

http://localhost:8001

----------2023-04-21-8.48.47

もちろん、こちらからテーブルを作成しても問題ありません。

環境変数の region を統一しないと作成したテーブルが出てこないので、注意してください。

SecretsManager

Secrets Key の作成

続けて、 SecretsManager の構築も見てみましょう。
やっていることは DynamoDB とほとんど同じです。

なぜ、デフォルトで SecretsManager を入れているかというと、古いプロジェクトでは認証情報が結構ベタで残っていたりするのですよね。
というのも環境構築の初期は、やはり動くものが早く見たくて、この辺りが杜撰になりがちだと思っています。
そこで、一旦環境変数とかに入れておいて、後で直そうとか思っていたものがそのままなぜか本番運用まで進んでしまった、といったケースがあります。

なので、面倒だけど、ここだけはやっておいた方が良いもの、ということで SecretsManager をピックアップしています。

セキュアな情報を持たないシステムなんて、ほとんどないですからね。

まず、登録する Key 情報を用意します。

$ echo "{\"hoge\": \"fuga\"}" >> key.json

うっかり、このファイルを git にコミットしないように気をつけましょう。

ファイルを用意したら同じく aws-cli で登録します。
成功すると以下のようなレスポンスを受け取れます。

bash-4.2# awslocal secretsmanager create-secret --name local-key --secret-string file://key.json
{
    "ARN": "arn:aws:secretsmanager:ap-northeast-1:000000000000:secret:local-key-IgSDpF",
    "Name": "local-key",
    "VersionId": "b0dc56d0-cc8b-4581-97dc-941eb2e9860e"
}

Lambda からアクセス

こちらも同様に Lambda からアクセスしてみましょう。
以下のレスポンスが返ってくるはずです。

bash-4.2# sls invoke local -f get_secrets
Running "serverless" from node_modules
Using serverless-localstack
{
    "statusCode": 200,
    "body": "{\"secret\": {\"hoge\": \"fuga\"}}"
}

他のサービスも同じように aws-cli を使ってリソースの作成ができますのでプロジェクトに必要なものを随時追加していきましょう。
Pro 版じゃないと使えないものもあるので、ご注意ください。

Localstack

docker-compose を起動すれば、基本的にそれ以上実行することはありません。
ただ、正常に動いているか、ログをどうやって確認すれば良いか、などの方法は知っておいた方が便利です。

状態確認

ホストマシンから、以下のコマンドを実行することで、 localstack のヘルスチェックが可能です。

$ curl localhost:4566/_localstack/health | jq

レスポンスは以下のような内容になっています。

{
  "features": {
    "initScripts": "initialized"
  },
  "services": {
    "acm": "available",
    "apigateway": "available",
    "cloudformation": "available",
    "cloudwatch": "available",
    "config": "available",
    "dynamodb": "available",
    "dynamodbstreams": "available",
    "ec2": "available",
    "es": "available",
    "events": "available",
    "firehose": "available",
    "iam": "available",
    "kinesis": "available",
    "kms": "available",
    "lambda": "available",
    "logs": "available",
    "opensearch": "available",
    "redshift": "available",
    "resource-groups": "available",
    "resourcegroupstaggingapi": "available",
    "route53": "available",
    "route53resolver": "available",
    "s3": "available",
    "s3control": "available",
    "secretsmanager": "available",
    "ses": "available",
    "sns": "available",
    "sqs": "available",
    "ssm": "available",
    "stepfunctions": "available",
    "sts": "available",
    "support": "available",
    "swf": "available",
    "transcribe": "available"
  },
  "version": "1.1.1.dev"
}

稼働しているサービスが一覧できます。

ログの確認

$ docker-compose logs -f localstack

ログがつらつらの流れるので、追うのが少し大変ですが、
デプロイがうまくいかない場合は確認する必要があります。

pro 版を使えというエラーが出たりします。

localstack_main  | 2023-04-20T01:26:09.433  WARN --- [ Thread-3281] l.u.c.template_deployer    : Unexpected resource type ApiGatewayV2::Api when resolving references of resource HttpApi: {"Type": "AWS::ApiGatewayV2::Api", "LogicalResourceId": "HttpApi", "Properties": {"Name": "local-serverless-python", "ProtocolType": "HTTP"}, "PhysicalResourceId": null}. To find out if ApiGatewayV2::Api is supported in LocalStack Pro, please check out our docs at https://docs.localstack.cloud/aws/cloudformation

こちらも同じですね。

localstack_main  | 2023-04-20T01:29:34.687  INFO --- [   asgi_gw_8] l.aws.handlers.service     : API action 'DescribeRepositories' for service 'ecr' not yet implemented or pro feature - check https://docs.localstack.cloud/aws/feature-coverage for further information

留意点

localstack へ deploy するか、 aws-cli でコードを実行するのとどちらが良いかですが、個人的には、 Community 版を使っている限りは、あまり localstack へのデプロイはお勧めできません。

Community 版ではできないことが多く、それがどこに依存しているかパッと分からなかったりします。
エラーにも出ていましたが、 ecr の環境などは使っているつもりはないのに、怒られました。
また、 API Gateway V2 が使えなかったりするのですが、その時の deploy 側のエラーは以下のような内容だったりします。

Error:
CREATE_FAILED: serverless-python-local (AWS::CloudFormation::Stack)
undefined

docker-compose のログを見に行けば Pro 版じゃないとダメだ、と分かるのですが、個人的にこれはかなり不親切なログだなと思っています。
また、急に CloudFormation の状態がおかしくなり、最初から環境を作り直す、といったことが頻繁に発生する印象です。
加えて、数分ではありますが、デプロイにはそこそこ時間がかかるので、これも開発効率を考えるとあまり良い選択とは思えていません。

そういった諸々の点を考慮しても、 localstack と serverless framework をあまり密に連携させた開発環境の運用はちょっと危険かなと感じています。

最後に

このテンプレートプロジェクトはまだ出来たてほやほやなので、定期的にメンテナンスして使いやすいものに仕上げていきたいなと思っています。
また、 serverless だけでなく、他のフレームワークでも同じようにテンプレートを用意して開発をより効率的にスタートできるような環境を用意していきたいです。

皆様からの issues や PR もお待ちしておりますので、アップデートにご協力いただけたらとても嬉しいです。