SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

AWS CDKとGitHub ActionsでLambdaで動くAPIをTypeScriptで作る

 こんにちは、 https://boxil.jp を作っている徳田(haze_it_ac)です。
先月に今風?な構成のAPIを業務で作ったので、その紹介をしようと思います。

作るもの・要件

f:id:hazeblog:20201020134724p:plain
雑な図

外部のAPIを叩くためのアプリケーションです。
BOXILのAPIサーバから今回作るAPIを叩き、そこから外のAPIを叩いて情報を取得したり、処理をしたりするものです。
現時点ではBOXILのみで使われていますが、それ以外からも使用されることを予定・想定しているため、BOXILとは別の基盤で作成しどこからでも実行できるように構築する必要があります。

なお、今回のサンプルリポジトリは以下になります。ソースコードと合わせて読んでみてください。

github.com

全体構成 概要

f:id:hazeblog:20201019214907p:plain
雑な構成図

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.amazon.com

AWS Cloud Development Kit 、通称CDKは、AWSが用意している開発ツール、フレームワークです。
「コードを書いて cdk deploy を実行するとよしなにデプロイしてくれるマン」で、裏はCloud Formationで構成されています。
今回はAWS Lambda, API Gatewayをデプロイするために使いましたが、ECS、DynamoDB、その他諸々のデプロイにも使えます。便利。

GitHub Actions

github.co.jp

去年の5月に一般公開されたGitHubの中に埋め込まれたCI/CD実行環境です。
1リポジトリにつき20並列まで動くので速いのが特徴。便利。

AWS SAM CLI

github.com

CLIでLambda Functionsを実行できるやつ。今回はローカルで開発する際にこれを使います。

template.yaml を作成し、 local start-api -t template.yaml で実行できます。
動かす際には npm run build を忘れないようにしましょう。

実装説明

ディレクトリ構成

cdk init app --language=typescript によって大まかな構成が出来上がります。

f:id:hazeblog:20201021110035p:plain

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設定で環境変数を設定できます。

f:id:hazeblog:20201020210317p:plain
設定画面

困ったことと解決方法

Lambdaのnode_modules配置場所の問題

AWS CDKやAWS SAMでは、デプロイする先のディレクトリ(今回の場合は /src)を指定して、そのソースコードをLambdaに送るという動きをします。
そのため、該当のディレクトリに /node_modules を配置する必要がありますが、 cdk deploy 等の処理を実行する場所は cdk.json 等のあるリポジトリのルートディレクトリである必要があり、そこにも同一の node_modules が必要となります。

一般的にはLambda Layersを使用するところな気がしますが、CDKでうまく使用する方法が軽く調べても出てこなかった(調査力が足りないだけかもしれない)ため、シンボリックリンクを使い解決しました。

f:id:hazeblog:20201020205116p:plain
https://github.com/haze-it/github-action-and-aws-cdk-sample/blob/develop/package.json

/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秒を超えることが多発し、問題となりました。

docs.aws.amazon.com

その対処として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
    });

github-action-and-aws-cdk-sample/index.ts at master · haze-it/github-action-and-aws-cdk-sample · GitHub

これで解決!...すると思いきや、それでも結構な割合でtimeoutが発生していたので、結局CloudWatch Eventsを使って、一分に一回空の呼び出しを行って、常にリソースを起動させる対応も並行して実施しています。

f:id:hazeblog:20201021111716p:plain
CloudWatch -> イベント -> ルール から設定ができます

これでいまのところは問題なく運用できています。
複数アプリケーションから実行されるようになったら根本的な解決を考えないといけなくなりそうです。困った。

権限について

CDK Deployを実行するユーザにはかなり強めの権限が必要になります。
必要な権限を調べつつ行いましたが、最終的にはAWS Lambda, IAM, API Gateway, Cloud Formationへのフルアクセスを設定することになりました。
Lambdaの環境変数を暗号化したりする必要がある場合には、更にKMSのアクセスが、他のリソースも使う場合はそれらのアクセスも必要になるかと思います。

当たり前の話ではありますが、暗号化キーやシークレットを外に公開しないよう注意しましょう。

ログについて

ログはCloudWatchに保管されるようにしています。
特に何も設定していない状態だと、「呼び出したアプリケーション側のリクエストがどのログなのかがわからない」という問題が発生します。所謂分散トレースの話ですね。

その対処として、
サンプルアプリケーション上では行っていませんが、実際のコードでは、Request Parameterに request_id というパラメータを必須で入力するように設定し、ログを検索できるようにしています。

f:id:hazeblog:20201021114920p:plain
パラメータ指定とログ出力のイメージ

検索にはCloudWatch Log Insightsを使用しています。
filter @message like /sample_app_111/ のように指定すると検索できます。

docs.aws.amazon.com

参考

Developers.ioの複数の記事を参考にして作っています。いつもお世話になっています!ありがとうございます

dev.classmethod.jp

dev.classmethod.jp

ESLintの設定は大半をバトルプログラマー柴田さんの記事をほぼそのまま使用しています 🙏

bps-tomoya.hateblo.jp


解説は以上です。
たまに詰まり所はありますが、便利で楽しいです。
是非皆さんも試してみてください!

[宣伝]

smartcamp.co.jp

スマートキャンプではエンジニアを募集しています。

興味があればお気軽にご連絡ください!