サーバーレスアプリケーションの勉強について一区切りな予定。作ったのがちょっと前(3月)で記憶が怪しいところがあり大した内容は書けてないような気がする。。
- 前回:Lambdaを使ったHTTPとSSL証明書のヘルスチェックプログラムをServerless Frameworkからデプロイしたときの備忘録(serverless-layers)
- 前々回:Serverless FrameworkでPythonのコードをAWSLambdaにデプロイする
- 前々々回:シャニマスのノウハウブックを管理するツール作った(Nuxt3+Serverless Framework)
今回はこれまでの内容に加えて、Lambda関数をtypescriptで書いてみました。
また、せっかくなので今自分の中であると便利そうなものを考えて、「AWSのEC2とLightsailのインスタンスについてWebUIで作成したスケジュールで起動/停止を行う」というサーバーレスアプリケーションを開発してみました。
作ったものについて
主に「EC2とLightsailを指定時間で起動/停止する設定を作成するWebUI」と「作成したスケジュールを元に動作するサーバーレスアプリケーション」の2本を作成しました。
EC2とLightsailを指定時間で起動/停止する設定を作成するWebUIについて
これはWebUIにアクセスしたときの画面です。
- リソースIDはAWSのアクセス情報を元にEC2のインスタンスIDとLightsailのインスタンス名がプルダウン表示されます。インスタンスを選択すると隣のリソース情報欄に選択中のインスタンスの情報が表示されます
- イベントの種類はStartとStopが選択可能なプルダウンです
- イベント時刻で関数を実行したい時刻(00:00~23:59)を設定します
- 曜日設定で関数を実行したい曜日(日曜~土曜)を設定します
- 「行追加」ボタンクリックで入力行を末尾に追加します
- 「保存」ボタンクリックで現在の入力を保存します(保存先はローカルストレージ)
- 「反映」ボタンクリックで現在の入力を元に、Serverless Frameworkを使用したデプロイで必要となるschedule.ymlとevent.jsonを生成します(ymlとjsonについては後述)
- 「設定」ボタンクリックで設定欄を右サイドに表示します
- 「Lambdaから設定を復元する」をクリックするとAWSにデプロイ済みのLambda関数から入力情報を復元します(後述)
- 別のPCから環境を構築して作業するときを想定した機能です
- 「event.jsonから設定を復元する」
- 反映ボタンをクリックした先でevent.jsonをダウンロードできるようにしているので、そのjsonファイルをアップロードして復元することもできるようにしました
- 「Lambdaから設定を復元する」をクリックするとAWSにデプロイ済みのLambda関数から入力情報を復元します(後述)
WebUIの開発についてフレームワークはNuxt3を使用しました。以前作成したシャニマスのノウハウブックを管理するツールのリポジトリをコピーしてるのでレイアウトが似ています。
また、今回はEC2やLightsailのインスタンス情報を取得するのにAWS SDK for JavaScript v2(以後aws-sdk v2)ではなくAWS SDK for JavaScript v3(以後aws-sdk v3)を使用しています。
これまでaws-sdk v3はあまり使ったことが無かったのですが、機能単位でパッケージが分割されており必要なものだけインストールすれば良くなっている点、ドキュメントが軽量で読みやすくなっている点が気に入りました。
import { DescribeInstancesCommand, EC2Client } from "@aws-sdk/client-ec2"; const ec2 = new EC2Client({ region: 'ap-northeast-1', }); export default defineEventHandler(async (_event) => { const result = await ec2.send(new DescribeInstancesCommand({})); return result.Reservations; });
// aws-sdk v2は今回インストールしてないのでこのコード動作確認してません。。 const AWS = require('aws-sdk'); AWS.config.update({ region: 'ap-northeast-1' }); const ec2 = new AWS.EC2(); export default defineEventHandler(async (_event) => { const result = await ec2.describeInstances({}).promise(); return result.Reservations; });
- aws-sdk v3の方は使いたい機能のコマンドクラスをインストールしたパッケージからnamed importする
- 今回はaws-sdk v3についてclient-ec2、client-lightsail、client-eventbridgeくらいしか使用しませんでしたが、いずれも機能名+Clientというクラスのインスタンスを作成しsendでコマンドクラスのオブジェクトを渡して実行するみたい
- コマンドクラスの名称も今回使った範囲ではaws-sdk v2の頃のメソッド名を「アッパーキャメル化+Command」という命名規則だったので特に分かりにくいといったことはなかった
作成したスケジュールを元に動作するサーバーレスアプリケーションについて
ソースはこちら → https://github.com/imo-tikuwa/serverless-aws-instance-scheduler-serverless-submodule
- WebUIの方のコードは勉強中のコメントが多々含まれているため非公開ですが、サブモジュールとして開発することでこちらだけパブリックなリポジトリとして公開することができました
- 動かすにはWebUIの方の「反映」ボタンよりconfigディレクトリに生成されるschedule.ymlとevent.jsonが必要です
service: ${env:SERVERLESS_SERVICE_NAME} frameworkVersion: "3" provider: name: aws runtime: nodejs18.x region: ${env:AWS_DEFAULT_REGION} # 関数実行時に必要となる権限について列挙(関数の実行ロールに追加される) iamRoleStatements: - Effect: "Allow" Action: - "ec2:DescribeInstances" - "ec2:StartInstances" - "ec2:StopInstances" - "lightsail:GetInstances" - "lightsail:StartInstance" - "lightsail:StopInstance" Resource: "*" package: patterns: - "!**" - "index.js" functions: RunSchedules: handler: index.handler # 関数名についてサービス名と一致させる name: ${self:service} description: AWSのリソース(EC2,Lightsail)を指定時間で起動/停止するスケジューラ environment: TZ: ${env:TZ} events: ${file(./config/schedule.yml)} # 以下の設定は適宜修正してください timeout: 300 memorySize: 512 ephemeralStorageSize: 512 plugins: - serverless-layers - serverless-plugin-typescript custom: serverless-layers: layersDeploymentBucket: ${self:service}-layers-${aws:accountId} compatibleRuntimes: ["nodejs18.x"]
こちらは上記リポジトリのserverless.ymlの全文になります。
これまでと変わった点はほとんどありません。typescriptを使用した開発をおこなっているためserverless-plugin-typescriptプラグインの指定があるくらいかと思います。
関数内のeventsでインクルードしているschedule.ymlが先ほど書いたWebUIの方で生成するファイルになります。
- schedule: rate: - cron(55 4 ? * 2,3,4,5,6,7 *) - cron(5 5 ? * 2,3,4,5,6,7 *) - cron(31 22 ? * 7 *) - cron(26 23 ? * 7 *) - cron(10 13 ? * 2,3 *) input: ${file(./config/event.json)}
こちらはWebUIで生成したschedule.ymlの中身です。
scheduleは上記serverless.ymlにおいて関数のスケジュールとしてインクルードされるため、この設定がLambda関数に紐づくEventBridgeとしてデプロイされます(以下の画像参照)。
また、schedule.ymlの中で更にevent.jsonというファイルを読み込んでいますが、これもWebUIの方で生成するファイルになります。
あと、${file()}を使用したインクルードはserverless.ymlから見たパスを書く必要があります。
{ "schedules": [ { "resourceType": "ec2", "resourceId": "i-0c8eaa96159ec5189", "eventType": "start", "eventHour": 13, "eventMinute": 55, "weekdays": [ 1, 2, 3, 4, 5, 6 ], "holiday": 1, "memo": "ほげ" }, { "resourceType": "ec2", "resourceId": "i-0c8eaa96159ec5189", "eventType": "stop", "eventHour": 14, "eventMinute": 5, "weekdays": [ 1, 2, 3, 4, 5, 6 ], "holiday": 1, "memo": "ほげほげ" }, ~~~省略~~~ ] }
こちらはWebUIで生成されたevent.jsonです。
schedule.ymlのinputにて読み込むことでLambda関数に紐づく各EventBridgeの入力定数となります。
入力定数はLambda関数のhandler()に対しての入力となります。
以下のようにEventBridgeのターゲットタブから確認がすることができます。
※WebUIの「Lambdaから設定を復元する」ボタンの機能はこの入力定数をaws-sdk v3を通じて取得することで復元しています。
動かすのに必要なもの
- Docker(docker-compose)
- 以下のポリシーを持つIAMアカウント
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "s3:*", "s3-object-lambda:*", "iam:*", "lambda:*", "events:*", "logs:CreateLogGroup", "logs:CreateLogStream", "logs:DeleteLogGroup", "logs:DeleteLogStream", "logs:TagResource", "cloudformation:DescribeStackResource", "cloudformation:CreateChangeSet", "cloudformation:DeleteChangeSet", "cloudformation:DescribeStacks", "cloudformation:DescribeStackEvents", "cloudformation:GetTemplate", "cloudformation:DeleteStack", "cloudformation:DescribeChangeSet", "cloudformation:ExecuteChangeSet", "cloudformation:ValidateTemplate", "cloudformation:ListStackResources", "ec2:DescribeInstances", "ec2:StartInstances", "ec2:StopInstances" "lightsail:GetInstances", "lightsail:StartInstance", "lightsail:StopInstance", ], "Resource": "*" } ] }
詳しくはこちら → https://github.com/imo-tikuwa/serverless-aws-instance-scheduler-docker
このツールの活用方法
- EC2で立てたテスト環境についてデイタイムの間だけ起動する
- 1日1回だけ呼び出したいcronがある
のような用途で使えるかなと思います。
後者の方は正直それ自体のサーバーレスアプリケーション化を検討するのも良いと思います(この記事書いてて思った)。
構成図
こんな感じになりました(サブモジュールを表現するのが難しい)
関数の実行結果の確認方法
関数の実行内容についてコンソール出力を行っているため、実行結果はCloudWatchのログより確認することができます。
備考、まとめ、メモ
- Serverless FrameworkでデプロイするLambda関数についてserverless-plugin-typescriptというプラグインを使用してTypescriptで書いてみました。いつもよりはjavascript上の型を意識したコードが書けたんじゃないかなと思います。
- 設定を作成するWebUIは機密性の高い情報を扱うためパブリックな環境へのデプロイは行いませんでした。代わりとしてDockerを使用した動作環境を各自のローカルに構築できるようにし、その環境内からServerless Frameworkを使用してデプロイも可能としてみました。
- s3、s3-object-lambda、iam、lambda、eventsについてすべてのIAMポリシーを必要とする設定となってしまっている状況については、公開する前に直せればいいなーと思ってましたが結局やらなかった。。;;
- 開発していたのは3月頃だけど、その後いろいろ別のことやっててこの記事書いてる現在が5月後半のため若干記憶が怪しいところがあります;o;
- 検証用に作成した空のインスタンスについて放置していたことが原因でAWSの請求が少しだけ高くなってしまった;o;
参考リンク
- Serverless Frameworkのaws-nodejs-typescriptのテンプレートを詳しく見る
- ServerlessでデプロイするLambda関数をtypescriptで書く方法を調べていたときに読んだページ。こちらの記事に書いてある通りaws-nodejs-typescriptテンプレートは今回のアプリケーション作成のユースケースには一致しなかったので使いませんでした
- Serverless Framework: Plugins
- 上のブログ記事を読んだ後、じゃあどうしようとググったときに読んだページ
- 今回作成したサーバーレスアプリケーションのpackage.jsonに含まれているserverless-plugin-typescriptプラグインについて紹介されています
- こちらのプラグインを使用し、Serverless Frameworkの”aws-nodejs”テンプレートをベースとし、index.jsをindex.tsに書き換えることで無事に開発を進めることができました
- File: README AWS SDK for JavaScript
- aws-sdk v2のドキュメント。サイドバーの使い勝手(特に検索機能)がv3と比べていまいち。。
- AWS SDK for JavaScript v3
- aws-sdk v3のドキュメント。v2と比べてすごく読みやすい。軽い。