スポンサーリンク

Lambdaを使ったHTTPとSSL証明書のヘルスチェックプログラムをServerless Frameworkからデプロイしたときの備忘録(serverless-layers)

今回もServerless Frameworkに関する話題。

前回 → Serverless FrameworkでPythonのコードをAWSLambdaにデプロイする
前々回 → シャニマスのノウハウブックを管理するツール作った(Nuxt3+Serverless Framework)

今回はserverless-layersというプラグインを使ってnode_modulesディレクトリをレイヤー化し、複数のLambda関数で使いまわすといった構成を作ったときの備忘録になります。

Serverless Frameworkを導入するリポジトリの概要

  • 2つのLambda関数が存在する
    • どちらもindex.jsの中にハードコートされたURLの配列に対してaxiosでリクエストを送る
    • http-status-200-healthcheck/index.js
      • HTTPステータスをチェックする
      • レスポンスステータスとして200番が返ってきたら正常と見なす
    • certificate-healthcheck/index.js
      • SSL証明書の有効期限をチェックする
      • レスポンスからSSL証明書の情報を読み取り、有効期限が1週間以上先だったら正常と見なす
    • 異常なステータスを検知したときSNSで自分のメールアドレスにメール通知を行う
  • ↑のLambda関数を含むリポジトリについて手動でAWSにデプロイしていた頃の自分用の覚え書きが大量に含まれていることや、ハードコートしてるURLの配列などがあるためにプライベートリポジトリとなっています。

serverless.ymlや環境の構成について

※SNS_TOPIC_ARN環境変数は↑のリポジトリの概要に書いたメール通知を行うのに必要なものになります。Serverless Frameworkを導入するのに必要なものではないので注意。

service: lambda-healthcheck-functions

frameworkVersion: '3'

provider:
  name: aws
  runtime: nodejs16.x
  region: ap-northeast-1
  # 手動で作成したLambda関数と名前を被らないようにする
  stage: new
  # Lambda関数内でメール通知が必要な場合、完了処理としてAWSSNSを使用したメール通知を行う
  # ※SNSはserverlessの中に仕組化はしておらず、各index.jsの中でaws-sdkを介してのメール通知を行っている
  # そのため、serverless frameworkが自動生成するIAMロールに以下のロールを明示する必要がある
  iamRoleStatements:
    - Effect: "Allow"
      Action: "sns:Publish"
      Resource: ${env:SNS_TOPIC_ARN}

# パッケージング対象について全てを除外
package:
  individually: true
  exclude:
    - "**/*"

functions:
  # SSL証明書のヘルスチェック
  certificate-healthcheck:
    handler: certificate-healthcheck/index.handler
    timeout: 180
    environment:
      SNS_TOPIC_ARN: ${env:SNS_TOPIC_ARN}
    # 1日1回日本時間で午前3時に実行
    events:
      - schedule: cron(0 18 * * ? *)
    # パッケージング対象についてindex.jsのみを指定
    package:
      include:
        - "certificate-healthcheck/index.js"

  # HTTPステータスのヘルスチェック
  http-status-200-healthcheck:
    handler: http-status-200-healthcheck/index.handler
    timeout: 180
    environment:
      SNS_TOPIC_ARN: ${env:SNS_TOPIC_ARN}
    # 2時間毎に実行
    events:
      - schedule: cron(0 1-23/2 * * ? *)
    # パッケージング対象についてindex.jsのみを指定
    package:
      include:
        - "http-status-200-healthcheck/index.js"

plugins:
  - serverless-layers

custom:
  serverless-layers:
    # レイヤー化したいnode_modules/のzipをアップするS3バケット名を指定
    layersDeploymentBucket: ${self:service}-layers-${aws:accountId}
    compatibleRuntimes: ['nodejs16.x']
services:
  app:
    build: ./docker/app
    volumes:
      # Lambda関数を含むディレクトリ
      - ./src:/src:cached
    tty: true
    environment:
      AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID
      AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY
      SNS_TOPIC_ARN: $SNS_TOPIC_ARN
COMPOSE_PATH_SEPARATOR=:
COMPOSE_FILE=docker-compose.yml

# serverless frameworkのデプロイが行えるユーザーのアクセスキー/シークレットアクセスキー
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=

# SNS通知用のARN
SNS_TOPIC_ARN=
  •  タイムアウト設定、実行時間設定、レイヤーの互換性のあるランタイム情報などは、手動で構築していた頃同じとなるようserverless.ymlで定義
    • タイムアウトは各関数内でtimeout: で設定するだけ。
    • 実行時間はevents: でcron式を書くだけでEventBridge(CloudWatch Event)を作成してくれてとても便利
      • デプロイユーザーの許可ポリシー設定せずにデプロイしてみたら、EventBridgeのPutRuleが必要(events:PutRule)とエラーが出ました
      • 今回はEventBridgeのアクションは全部許可しました(events:*)
      • serverless removeからLambda関数を削除するときとかevents:DeleteRuleが必要になるんじゃないかなぁと思っている(試していない)
{
  "name": "lambda-healthcheck-functions",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^0.27.2",
    "moment": "^2.29.4"
  }
  "devDependencies": {
    "prettier": "^2.8.4",
    "serverless": "^3.27.0",
    "serverless-layers": "^2.6.1"
  }
}
{
  "name": "certificate-healthcheck",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "author": "imo-tikuwa",
  "license": "ISC",
  "dependencies": {
    "aws-sdk": "^2.1213.0"
  }
}
{
  "name": "http-status-200-healthcheck",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "author": "imo-tikuwa",
  "license": "ISC",
  "dependencies": {
    "aws-sdk": "^2.1213.0"
  }
}
  • serverless.ymlがあるディレクトリのpackage.jsonのdependenciesにレイヤー化したいパッケージを指定
    • 別のディレクトリにあるpackage.jsonを使いたい場合、dependenciesPathというオプションでserverless.ymlからの相対パスを指定することで設定可能
  • /src/certificate-healthcheck/package.json と /src/http-status-200-healthcheck/package.json はそれぞれの関数内で使用するパッケージを管理します
    • 今回はLambda(Node16.xランタイム)でデフォルトで使用可能となっているaws-sdk(v2)を開発環境用にインストールしています

Severless Framework導入の前後の構成図

Serverless Frameworkを導入したリポジトリについて前後のイメージ図を作成してみました。

↑Serverless Framework導入前の構成図

↑Serverless Framework導入後の構成図

  • 導入前、導入後ともにLambdaのコード自体はほぼ同一
    • 導入前、レイヤー内に含まれるライブラリ(axios、momentとか)をインポートするのに/opt/node_modules/axios としていたのを /opt/nodejs/node_modules/axios のように修正する必要があった
      • 2023/03/18追記 これ全くの間違いでした。。
        レイヤーに含めるパッケージ特に工夫せずにいつも通りのインポートの記述でよかった

        // ↓NG
        const axios = require("/opt/nodejs/node_modules/axios");
        const moment = require("/opt/nodejs/node_modules/moment");
        
        // ↓OK
        const axios = require("axios");
        const moment = require("moment");

        ローカルでの開発時はserverless-layersプラグインを導入した状態で

        serverless invoke localから実行することで、Lambdaで動かすときと同じような感じでレイヤー内のnode_modulesからインポートが行われる模様
    • serverless-layersプラグインを使用してレイヤー化する場合、nodejs/node_modulesというディレクトリ構成のzipが作成され、S3にアップ⇒レイヤー化される模様
  • SNS自体はserverless.ymlから何かを設定するわけではなく、手動で構築した頃のものをそのまま使用
    • そもそもserverlessの設定でメール通知可能な状態まで持っていけるのか不明
    • serverlessから構築しているわけではないため、Serverless Frameworkがデプロイ時にLambda関数に対して自動で設定する実行ロールにsns:Publishが不足する
      • 口述してるiamRoleStatementsの設定によって解決できた

iamRoleStatementsについて

    デプロイ時にLambda関数と合わせて作成されるIAMロールにロールを追加したいときに使う紐づくIAMロールはLambda関数の設定タブ>アクセス権限>実行ロールより確認可能

iamRoleStatementsを設定しなかった場合以下のようなIAMロールとなるはず。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:CreateLogStream",
                "logs:CreateLogGroup"
            ],
            "Resource": [
                "arn:aws:logs:ap-northeast-1:@@@@@@@@@@@@:log-group:/aws/lambda/[ServerlessFrameworkのservice名]-[ServerlessFrameworkのstage名]*:*"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:ap-northeast-1:@@@@@@@@@@@@:log-group:/aws/lambda/[ServerlessFrameworkのservice名]-[ServerlessFrameworkのstage名]*:*:*"
            ],
            "Effect": "Allow"
        }
    ]
}

今回はSNSの送信(Publish)ポリシーが必要で、対象のリソースとなるSNSのARNは環境変数として開発環境内に存在する状態なので以下のようになりました。

provider:
  iamRoleStatements:
    - Effect: "Allow"
      Action: "sns:Publish"
      Resource: ${env:SNS_TOPIC_ARN}

上記の設定を行った状態でデプロイしたところ↓に添付する画像のようにLambda関数の実行ロールに指定のアクションに関する定義が追加されることが確認できました。

その他、今回は試してはいないですが、以下のような感じでロール自体を別途作成することもできるみたい?(ChatGPTに聞いた)。

# 以下、動作確認できてないです。。
role:
  name: ${self:service}-sns-role
  statements:
    - Effect: Allow
      Action:
        - sns:Publish
      Resource: ${env:SNS_TOPIC_ARN}

パッケージ対象の設定について

最初はserverless.ymlのパッケージ対象の設定について以下のような記述をしていました。

# パッケージング対象について一旦全てを除外し、それぞれの関数をインクルード
package:
  exclude:
    - "**/*"
  include:
    - "certificate-healthcheck/index.js"
    - "http-status-200-healthcheck/index.js"

上の設定でデプロイした場合、それぞれのLambda関数画面でcertificate-healthcheck/index.jsとhttp-status-200-healthcheck/index.jsがエクスプローラ上に含まれてしまいました。。

これは、参考サイト内の情報を参考に関数ごとにインクルードの設定を記載することで解決しました。
抜粋すると以下のような感じ

# パッケージング対象について一旦全てを除外
package:
  # ↓関数ごとにzipを作成する
  individually: true
  # 除外設定はこのままでOK
  exclude:
    - "**/*"

functions:
  # SSL証明書のヘルスチェック
  certificate-healthcheck:
    handler: certificate-healthcheck/index.handler
    package:
      include:
        # ↓SSL証明書のヘルスチェック関数のみをパッケージ対象とする
        - "certificate-healthcheck/index.js"

  # HTTPステータスのヘルスチェック
  http-status-200-healthcheck:
    handler: http-status-200-healthcheck/index.handler
    package:
      include:
        # ↓HTTPステータスのヘルスチェック関数のみをパッケージ対象とする
        - "http-status-200-healthcheck/index.js"

デプロイした関数やレイヤーの確認

Lambda関数

関数名はServerless Frameworkの [サービス名] + [stage名] + [関数名] となる模様


Lambdaレイヤー

レイヤー名は [サービス名] + [stage名] + “nodejs-default”となった


以下はSSL証明書のヘルスチェック関数の方の概要

関数にレイヤーが紐づいていること、EventBridgeのトリガーが紐づいていることが確認できた


同関数の設定タブ

serverless.ymlで関数内に設定した環境変数が適切に設定されていることが確認できた


EventBridgeのトリガー

defaultのイベントバスにevents:で設定したcron式がそのまま登録されていることを確認

反省点

  • 各Lambda関数にハードコートしてしまってるチェック対象のURLについて関数のトリガー(EventBridge)の入力定数を活用して外から渡すようにしてあげるとより使い勝手の良いプログラムになりそう

参考サイト

タイトルとURLをコピーしました