こんにちは、 https://boxil.jp を作っている徳田(haze_it_ac)です。
先月に今風?な構成のAPIを業務で作ったので、その紹介をしようと思います。
作るもの・要件
外部のAPIを叩くためのアプリケーションです。
BOXILのAPIサーバから今回作るAPIを叩き、そこから外のAPIを叩いて情報を取得したり、処理をしたりするものです。
現時点ではBOXILのみで使われていますが、それ以外からも使用されることを予定・想定しているため、BOXILとは別の基盤で作成しどこからでも実行できるように構築する必要があります。
なお、今回のサンプルリポジトリは以下になります。ソースコードと合わせて読んでみてください。
全体構成 概要
AWS CDKを中心に据えた、AWS Lambdaで実行されるアプリケーションです。
実行環境
Webアプリケーションサーバとしての構成は必要なかったため、サーバレスのAWS Lambdaを使用しています。
他のアプリケーションからリクエストを受け取るためにAPI Gatewayを使って受け取ります。
言語はJavaScriptです。(AWS CDKによりTypeScriptをそのままDeployすればよしなにやってくれるので、実際に書くのはTypeScriptのみです)
CI/CD
実行環境としてGitHub Actionsを、CDにはAWS CDKを使っています。
CIはESLintを流しています。テストはちゃんと書いていません🙃
ローカル開発
変更したコードをその場で即座に動作確認をしたいので、ローカルで実行環境を用意します。
AWS SAM CLIを使うことで、ローカルにLambda相当の環境を作ることができて便利です。
npm run build
で吐き出されたJavaScriptが実行されます。
構成要素の紹介
AWS CDKについて
AWS Cloud Development Kit 、通称CDKは、AWSが用意している開発ツール、フレームワークです。
「コードを書いて cdk deploy
を実行するとよしなにデプロイしてくれるマン」で、裏はCloud Formationで構成されています。
今回はAWS Lambda, API Gatewayをデプロイするために使いましたが、ECS、DynamoDB、その他諸々のデプロイにも使えます。便利。
GitHub Actions
去年の5月に一般公開されたGitHubの中に埋め込まれたCI/CD実行環境です。
1リポジトリにつき20並列まで動くので速いのが特徴。便利。
AWS SAM CLI
CLIでLambda Functionsを実行できるやつ。今回はローカルで開発する際にこれを使います。
template.yaml を作成し、 local start-api -t template.yaml
で実行できます。
動かす際には npm run build
を忘れないようにしましょう。
実装説明
ディレクトリ構成
cdk init app --language=typescript
によって大まかな構成が出来上がります。
BranchとDeployフロー
特定のbranchへのpushをフックに、GitHub ActionsのJobを走らせています。
.github/workflows/development.yml
[development.yml] name: development on: push: branches: - develop ... - name: CDK Deploy if: contains(github.event_name, 'push') run: cdk deploy --require-approval never env: ENV: development AWS_DEFAULT_REGION: 'ap-northeast-1' AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
今回のリポジトリの場合、 develop
へのpush (merge)でstaging環境へ、 master
へのpush (merge) でproduction環境へリリースする流れとなります。
デプロイ時に使用するシークレット情報は、GitHubのSecret設定で環境変数を設定できます。
困ったことと解決方法
Lambdaのnode_modules配置場所の問題
AWS CDKやAWS SAMでは、デプロイする先のディレクトリ(今回の場合は /src
)を指定して、そのソースコードをLambdaに送るという動きをします。
そのため、該当のディレクトリに /node_modules
を配置する必要がありますが、 cdk deploy
等の処理を実行する場所は cdk.json
等のあるリポジトリのルートディレクトリである必要があり、そこにも同一の node_modules
が必要となります。
一般的にはLambda Layersを使用するところな気がしますが、CDKでうまく使用する方法が軽く調べても出てこなかった(調査力が足りないだけかもしれない)ため、シンボリックリンクを使い解決しました。
/package.json
-> /src/package.json
,
/package-lock.json
-> /src/package-lock.json
,
/node_modules
-> /src/node_modules
のシンボリックリンクを作成することで、実態は /src
にありつつ、各コマンドは正常に実行できるようにしています。
もっと良い解決方法があれば教えてほしいです... 🥺
コールドスタート問題
Lambdaはホットスタンバイとなっている状態のリソースがあればそこから実行されますが、暫く実行されないとスタンバイ状態が切れ、Lambda環境が起動されてから実行される、所謂コールドスタートとなります。
CDKでデプロイしたLambda Functionsは起動にかなり時間が掛かるらしく、API Gatewayのtimeout設定の最大値である30秒を超えることが多発し、問題となりました。
その対処としてAWSから Provisioned Concurrency
という機能が提供されており、指定した個数ぶんのリソースが常に起動されている状態を実現することができます。
CDKではLambdaのリソース状態等を指定するCDK Stackの中で記述します。
// コールドスタート対応 // https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Version.html#provisionedconcurrentexecutions sampleLambda.currentVersion.addAlias('VERSION_NAME', { provisionedConcurrentExecutions: 2 });
これで解決!...すると思いきや、それでも結構な割合でtimeoutが発生していたので、結局CloudWatch Eventsを使って、一分に一回空の呼び出しを行って、常にリソースを起動させる対応も並行して実施しています。
これでいまのところは問題なく運用できています。
複数アプリケーションから実行されるようになったら根本的な解決を考えないといけなくなりそうです。困った。
権限について
CDK Deployを実行するユーザにはかなり強めの権限が必要になります。
必要な権限を調べつつ行いましたが、最終的にはAWS Lambda, IAM, API Gateway, Cloud Formationへのフルアクセスを設定することになりました。
Lambdaの環境変数を暗号化したりする必要がある場合には、更にKMSのアクセスが、他のリソースも使う場合はそれらのアクセスも必要になるかと思います。
当たり前の話ではありますが、暗号化キーやシークレットを外に公開しないよう注意しましょう。
ログについて
ログはCloudWatchに保管されるようにしています。
特に何も設定していない状態だと、「呼び出したアプリケーション側のリクエストがどのログなのかがわからない」という問題が発生します。所謂分散トレースの話ですね。
その対処として、
サンプルアプリケーション上では行っていませんが、実際のコードでは、Request Parameterに request_id
というパラメータを必須で入力するように設定し、ログを検索できるようにしています。
検索にはCloudWatch Log Insightsを使用しています。
filter @message like /sample_app_111/
のように指定すると検索できます。
参考
Developers.ioの複数の記事を参考にして作っています。いつもお世話になっています!ありがとうございます
ESLintの設定は大半をバトルプログラマー柴田さんの記事をほぼそのまま使用しています 🙏
解説は以上です。
たまに詰まり所はありますが、便利で楽しいです。
是非皆さんも試してみてください!