🐈

AWS SAM CLI で PostgreSQL を利用する Lambda ローカル開発環境

2024/02/06に公開

はじめに

AWS Lambda は手軽に関数を実装、公開できて便利なサービスですが、ちょっと手の込んだことをしようとすると Lambda の特殊な環境の影響で面倒になってしまうことがあったりします。
例えば PostgreSQL を使おうとした場合、通常の Lambda 環境には PostgreSQL 用のパッケージが入っていないので Lambda Layer というミドルウェア層が必要になります。
今回は AWS SAM CLI を用いて PostgreSQL を利用する関数をローカルで開発するための環境構築を行ったので、その方法を共有します。

今回話すこと

  • ローカル開発環境の構築

今回話さないこと

  • デプロイについて
  • アプリケーションについて

ローカル開発環境

今回は ruby の関数を実装したかったので AWS の公式が公開している serveless-sinatra-sample をフォークして環境を構築しました。
また、PostgreSQL は Docker を利用しています。
開発環境構築ができているプロジェクトはこのリポジトリで公開しています。

ファイル構成

.
├── .aws-sam/build/  # sam でビルドを実行した際に出力されるディレクトリ
│   └── PGLayer/lib  # Lamda Layer のビルド結果ディレクトリ、git clone してすぐ使えるように gitignore していない
├── app/  # アプリケーションコード、今回は省略
├── bin/  # 開発時のコマンド
│   ├── build.sh
│   ├── bundle.sh
│   ├── dev.sh
│   └── init.sh
├── container/
│   ├── app/
│   │   ├── Dockerfile  # build, bundle はこのイメージを利用して行う
│   │   └── bundle.sh
│   └── db/
│       ├── data  # コンテナとの DB データバインド用
│       └── init.sql
├── pg_layer/
│   └── Makefile  # Lambda Layer のビルドソース
├── env.json  # sam local での環境変数定義ファイル
└── template.yaml  # CloudFormation の定義ファイル、sam コマンドからも参照される

ビルド用 Docker

この構成では bundle install と sam build を Docker コンテナで行っています。
Dockerfile では AWS が公開している ruby ランタイムのビルド用イメージをベースに PostgreSQL に必要になるパッケージを追加でインストールしています。
https://github.com/fucso/sinatra-lambda-demo/blob/env/docker/container/app/Dockerfile

bundle install

bin/bundle.sh コマンドで上記のコンテナで bundle install が実行されるようになっています。
インストールした gem を参照するのは bundle install を実行するコンテナではなく sam cli が暗黙的に立ち上げる Lambda ランタイムです。
そのため bin/bundle.sh から container/app/bundle.sh を呼び出して bundle config でインストールした gem の配置ディレクトリをホストとマウントしている /vendor/bundle ディレクトリに変更しています。

Lambda Layer

前述の通り Lambda から PostgreSQL を利用するためには Lambda Layer が必要です。
ローカルの開発環境では sam bulid で .aws-sam/build に書き出した Lambda Layer 用のバイナリファイルが格納されたディレクトリを参照するようにしています。

sam build

bin/build.sh コマンドでビルドを実行します。
ビルドの際は先ほど説明した PostgreSQL 関連のパッケージをインストールしてあるビルド用の Docker イメージを利用します。

Makefile

Lambda Layer の作成は Makefile をもとに行います。
ここでは Docker 内の /usr/lib64 ディレクトリにある PostgreSQL 関連のバイナリパッケージを Lambda Layer 用のディレクトリにコピーしています。
Lambda Layer はここでコピーしたファイルを持ってビルドされます。

template.yaml

template.yaml は CloudFormation の定義ファイルで sam コマンドからも参照されます。
sam build で Lambda Layer のビルドを行うためにこのファイルに Lambda Layer に関する定義を追加しています。

...
Parameters:
   PGLayerContentUri:
     Type: String
     Default: 'pg_layer/'
...
  PGLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      CompatibleRuntimes:
        - ruby3.2
      ContentUri: !Ref PGLayerContentUri
    Metadata:
      BuildMethod: makefile
...

ContentUri は Lambda Layer へのパスです。
S3 パスを設定したりもできますが、ローカルパスを設定した場合は sam build 時に BuildMethod に記載の方法でビルドが行われます。
今回はビルド時と環境立ち上げ時で異なるパスを設定したかったのでパラメーターで設定して変更可能にしています。

環境立ち上げ

bin/dev.sh コマンドでローカル開発環境を立ち上げます。
このコマンド一つで PostgreSQL Docker と sam api の両方が立ち上がります。
コマンド実行後のコンソールには sam api のログが表示され、Ctrl + C で Docker と sam api の両方が終了します。

Docker

普通の docker run コマンドです。
DB データの永続化のためにちょっと追加の処理をしています。

db_dir="$(pwd)/container/db"
data_dir="${db_dir}/data"
if [ ! -d "$data_dir" ]; then
    echo "Creating PostgreSQL data directory..."
    mkdir -p "$data_dir"
fi

docker run \
  --name sinatra-lambda-db \
  -postgrese POSTGRES_USER=sinatra-lambda-demo \
  -e POSTGRES_PASSWORD=password \
  -e POSTGRES_DB=sinatra-lambda-demo \
  -v "${data_dir}":/var/lib/postgresql/data \
  -v "${db_dir}/init":/docker-entrypoint-initdb.d \
  -p 5432:5432 \
  -d --rm \

sam api

sam local start-api コマンドでローカルの sam api を立ち上げます。

sam local start-api \
  --env-vars env.json \
  --template template.yaml \
  --parameter-overrides PGLayerContentUri=".aws-sam/build/PGLayer" &
SAM_PID=$!
echo "SAM Local started with PID $SAM_PID"

wait $SAM_PID

--env-vars env.json

env.json に定義した環境変数を template.yaml に渡しています。
デフォルトでは未設定で問題ない環境変数であっても template.yaml の Environment で環境変数名を定義しておかないと反映されません。

Environment:
  Variables:
    DB_USER: dummy
    DB_PASSWORD: dummy
    DB_NAME: dummy
    DB_HOST: dummy

env.json は以下のフォーマットです。

{
  [ResourceName]: {
    [Key]: [Value]
  }
}

--parameter-overrides PGLayerContentUri=".aws-sam/build/PGLayer"

template.yaml でパラメーターとして設定した値をオーバーライドするオプションです。
ここでは Lambda Layer のパスを sam build で書き出したディレクトリに差し替えています。

Ctrl + C での終了

あまり本題と関係ありませんが trap SIGINT でプロセスの終了を検知して Docker と sam api の両方を終了させています。

function cleanup {
  echo "Stopping SAM Local..."
  if kill -0 $SAM_PID > /dev/null 2>&1; then
    kill -SIGTERM $SAM_PID
    # Wait for the process to exit
    wait $SAM_PID
  fi

  echo "Stopping PostgreSQL container..."
  docker stop sinatra-lambda-db

  exit 0
}

trap cleanup SIGINT

今後の課題

この記事としてはテーマがローカル開発環境の構築なのでここで終了ですが、当然開発したらデプロイも必要になってきます。
今回記載した内容をベースに Lambda へのデプロイも同じリポジトリで実装を試みたのですが、どうしても解決できないエラーに躓いてしまいそこで止まってしまっています。
AWS のサポートに聞いてみたり、拙い英語で issue を立ち上げたりしてみたりはしているので、解決できたらデプロイ編の記事も書こうかと思います。

Discussion