見出し画像

AWS SAMで定期実行ジョブをデプロイする

こんにちは、こんにゃくざむらいです。ナビタイムジャパンでチャットボットのバックエンド開発をしています。
今回は、Azure Table Storageに蓄積されるチャットボットのログデータをAmazon S3に転送したときに初めてSAMデプロイするお話しです。
SAMデプロイは最初とっつきにくいですが、慣れるととても便利です。自分以外にもSAMデプロイに抵抗がある人は結構いると思うのでnote に残すことで同じような境遇の人の助けになれば良いなと思います。

クラウドサービスをまたぐ構成

SAMデプロイの話とは全く別の話なので、SAMデプロイだけ気になる方は読み飛ばしてください。

背景

ナビタイムジャパンでは様々なアプリをリリースしていますが、それぞれに「ご意見箱」というユーザーの意見投稿を受け付けるフォームがあります。ご意見箱のバックエンド側は一つの共通したシステムで一つの担当プロジェクトが意見を管理、分析しています。
ご意見箱は自由記述であるため、従来では一つ一つの意見を目で確認し「対応が必要かどうか」「どのプロジェクトが対応すべきか」などを判断しています。こちらの工数を減らすために私のプロジェクトではMicrosoft Bot Frameworkを用いたチャットボットの開発をしました。

チャットボットの画面

ボットのログに関する分析もご意見箱のプロジェクトが担当することになったのですが、そちらのプロジェクトはご意見分析にAmazon QuickSightを利用していました。
ボットを使用したユーザーのログはAzureサービスに蓄積しているため、このログを分析するためにはS3に転送しなければいけません。(AzureのBIツールを使えば済むという話もありますが、ご意見分析をするプロジェクトがもともとQuickSightを使用していて使い慣れているということや費用の関係上QuickSightを選定いたしました。)

AWSとAzureでお互いにデータ転送をするようなSDKなどが存在すれば楽だったのですが、2つのクラウドサービスを跨いで使うような事態は想定されていないのか見つかりませんでした。
そのためアクセスキーを用いたデータ転送をすることにしましたが、そこでも考えうる方法が2つありました。AWSのアクセスキーを使ってAzure Functionsでデータ転送をするか、Azureのアクセスキーを使ってAWS Lambdaでデータ転送をするかの二択です。

転送の流れ

どちらでも良かったのですが、私がまだAWSに疎いことと、当社全体ではAWSの利用率が高いということで、学習も兼ねてLambdaを使用する方法にしました。

構成

構成図

Azureのアクセスキーを使ってLambdaでデータ転送をします。ログは毎日蓄積されるため、ログの転送も毎日行う必要がありました。
ご意見分析をするときに一度だけ転送するという方法もありますが、ご意見分析をする頻度を考えると少し面倒でしたので、自動で毎日転送させる方式にしています。
Amazon EC2の選択肢について触れていませんでしたが、毎日一回の起動でなおかつ動作が軽い処理なのでEC2よりLambdaが適切でした。

毎日の定期実行のためにAmazon EventBridge Schedulerを用いています。またAzureのアクセスキーをAWS Systems Manager Parameter Storeに保存して参照できるようにしています。

AWS SAM デプロイについて

AWS CloudFormationを通じて、複数のリソースをで設定することでデプロイする方法です。(参考:AWS サーバーレスアプリケーションモデル - アマゾン ウェブ サービス)社内でも推奨されている方法で運用がかなり楽になります。
リソースひとつひとつに対して更新や削除などを行う手間が一度で済むので、扱うリソースが多い場合はCloudFormationを使ったデプロイの方が楽になります。YAMLファイルで設定したリソースがCloudFormationに紐づくため、YAMLファイルで一元管理することができます。

CloudFormationを使わない場合はAWS CLIのupdate-functionなどを使ってデプロイします。実行フォルダをzipで固めてアップロードする方法やdockerで実行イメージを作成してAmazon ECRにアップロードする方法があります。ただリソースの紐付けなどは行えないため、やはり複数のリソースを扱う際には向いていません。(参考:update-function-code — AWS CLI 1.27.151 Command Reference

SAMデプロイの話に戻ります。template.yamlというファイルに設定を記載します。今回の構成を作成するtemplate.yamlは以下の記述です。

Transform: AWS::Serverless-2016-10-31
Description: >
  Azure Table StorageからAWS S3に転送するためのLambda


# 引数として与えられるパラメータを設定する部分
Parameters:
  Env:
    Type: String
    AllowedValues:
      - dev
      - prod
    Default: dev

# 環境ごとに変数を設定する部分
Mappings:
  LambdaRoleMap:
    dev:
      lambdaRole: 'arn:aws:iam::xxxxxx'
    prod:
      lambdaRole: 'arn:aws:iam::xxxxxx'
  SchedulerRoleMap:
    dev:
      schedulerRole: 'arn:aws:iam::xxxxxx'
    prod:
      schedulerRole: 'arn:aws:iam::xxxxxx'
  ScheduleMap:
    dev:
      schedule: 'cron(*/5 * * * ? *)'
    prod:
      schedule: 'cron(0 8 * * ? *)'

# デプロイするリソースを設定する部分
Resources:
  # AWS Lambda
  TransferLogLambda: # リソースの名前
    Type: AWS::Serverless::Function
    Properties:
      PackageType: Zip
      CodeUri: src/
      Handler: main
      Runtime: go1.x
      Architectures:
        - x86_64
      FunctionName: 'transfer-log-lambda'
      Role: !FindInMap [LambdaRoleMap, !Ref Env, lambdaRole]
      Timeout: 180
      Events:
        Schedule:
          Type: ScheduleV2
          Properties:
            Name: !Sub 'transfer-log-lambda-${Env}-scheduler'
            ScheduleExpression: !FindInMap [ScheduleMap, !Ref Env, schedule]
            ScheduleExpressionTimezone: 'Asia/Tokyo'
            RoleArn: !FindInMap [SchedulerRoleMap, !Ref Env, schedulerRole]
      Environment:
        Variables:
          ENV: !Sub '${Env}'

ずらずらと何行も書いてあり初心者は見る気を無くしそうですね。(自分がそうでした…)上記のtemplate.yamlは検証環境と本番環境での変数の使い分けがあり、最低限の実装ではないので少し長いです。環境による変数の使い分けを除いた場合は以下になります。

Transform: AWS::Serverless-2016-10-31
Description: >
  Azure Table StorageからAWS S3に転送するためのLambda

# デプロイするリソースを設定する部分
Resources:
  # AWS Lambda
  TransferLogLambda: # リソースの名前
    Type: AWS::Serverless::Function
    Properties:
      PackageType: Zip
      CodeUri: src/
      Handler: main
      Runtime: go1.x
      Architectures:
        - x86_64
      FunctionName: 'transfer-log-lambda'
      Role: 'arn:aws:iam::xxxxxx'
      Timeout: 180
      Events:
        Schedule:
          Type: ScheduleV2
          Properties:
            Name: 'transfer-log-lambda-scheduler'
            ScheduleExpression: 'cron(0 8 * * ? *)'
            ScheduleExpressionTimezone: 'Asia/Tokyo'
            RoleArn: 'arn:aws:iam::xxxxxx'

少し短くなりました!
ただこれでも少し長い気がします。実体はもっと簡単です。上記のtemplate.yamlは全体の構成を全て含んでいますが、基本は以下の記述のみです。

Transform: AWS::Serverless-2016-10-31


Resources:
  set of resources

基本はこれだけです。これだけ見ると簡単ですね!
Resourcesの欄にはデプロイしたいリソースを追加していきます。今回はLambdaのみなのでResources以下にLambdaの記載をしていきます。

AWS Lambda

Resources:
  TransferLogLambda:  # リソースの名前
    Type: AWS::Serverless::Function
    Properties:
      PackageType: Zip
      CodeUri: src/  # エントリーポイントを含むフォルダ
      Handler: main  # エントリーポイント
      Runtime: go1.x
      Role: 'arn:aws:iam::xxxxxx'  # 必要であれば

Lambdaのみの場合こんなに少ないです。

PackageTypeはZipもしくはImageから選択できます。Imageを選択する場合あらかじめ構築済みコンテナをECRにアップロードする必要があります。今回はコードからデプロイするためZipを選んでいます。(PackageTypeのデフォルトはZipであるため実は指定する必要がありません。参考:AWS::Serverless::Function - AWS Serverless Application Model

Runtimeはいくつかの言語から選択できます。今回はGoを使うため、「go1.x」を選択しています。選択できるRuntimeについては公式を参考にしてください。(参考:Lambda runtimes - AWS Lambda

必要であればRoleをあらかじめ作成し設定してください。今回はS3へのアップロードをする権限が必要だったのでS3へのPut権限を付与したロールを設定しました。

AWS EventBridge Scheduler

Lambdaを定期実行するためにこちらのサービスを使用します。今回はSAMデプロイですので、GUIで新しく作る必要はないですが、あらかじめSchedulerのための実行ロールが必要となります。

どのような定期実行をするかはYAMLファイルのEventsで設定できます。さまざまな設定方法があります(参考:EventSource - AWS Serverless Application Model)が、今回はScheduleV2を用いて設定しました。
V2にした理由はTimezoneを使用したかったからです。標準ではUTCを基本としていて、9時間ずれた値で設定しなければならず、他者が見たときにわかりづらいです。ただScheduleV2の場合は実行ロールが必要になります。それぞれ一長一短だと思うので好きな方を採用してください。

以下は毎朝8時に起動させるための設定です。

Events:
  Schedule: # リソース名
    Type: ScheduleV2
    Properties:
      ScheduleExpression: 'cron(0 8 * * ? *)'
      ScheduleExpressionTimezone: 'Asia/Tokyo'
      RoleArn: 'arn:aws:iam::xxxxxx'

これもまた分割するとわかりやすいですね!
ScheduleExpressionで定期実行するタイミングを変更できます。Timezoneを使わない場合は9時間早い23時で定期実行を設定するとうまくいきます。詳しいcronの書き方については公式を参照してください。(参考:ルールのスケジュール式 - Amazon CloudWatch Events

cron具体例(上記リンクより引用)

今回の構成で使用したリソースの説明は以上です。以降はtemlate.yamlの特殊な記法についてです。

Mappingsについて

template.yamlではフォーマット文のようにして変数をそのまま文字として扱うことができます。

Mappings:
  MapName:
    keyname1:
      lambdaRole: 'arn:aws:iam::xxxxxx'
    keyname2:
      lambdaRole: 'arn:aws:iam::xxxxxx'

Resources:
  TransferLogLambda:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/ # エントリーポイントを含むフォルダ
      Handler: main # エントリーポイント
      Runtime: go1.x
      Role: !FindInMap [MapName, keyname1, lambdaRole]

このようにあらかじめ使いたい文字を変数として用意すれば後で参照することができます。このようにすれば可変部分をわかりやすく管理できます。(参考:Mappings - AWS CloudFormation

Parametersについて

template.yamlではデプロイ時にパラメータを設定することができます。

sam deploy --parameter-overrides ParameterKey=Env,ParameterValue=dev
Parameters:
  Env:
    Type: String
    AllowedValues:
      - dev
      - prod
    Default: dev

検証用と本番用を区別するための変数を設定しています。
ここでは作成するリソースの内容をtemplate.yamlに記述していますが、CloudFormation本体の設定ファイルはsamconfig.tomlに記載します。その場合はdeployする際に--config-envオプションを指定することで環境を使い分けることができます。(参考:AWS SAM CLI の設定ファイル - AWS Serverless Application Model

Refについて

Refを使用してParametersで設定した変数を参照できます。

Parameters:
  Env:
    Type: String
    AllowedValues:
      - dev
      - prod
    Default: dev

# 参照: !Ref Env

今回であれば検証環境か本番環境かを区別するためのEnvという変数を参照します。(参考:Ref - AWS CloudFormation

以上で説明は終わりです。これで定期実行を行うLambdaをSAMでデプロイできると思います。

あとがき

この記事を最後まで読んでいただきありがとうございます。
最後まで読んだ方ならお気づきかと思いますが大体のことは公式ドキュメントに記載してあります。困った時は公式ドキュメントを読みましょう!
ただ公式ドキュメントを検索しようとしても求めているドキュメントがうまく見つからない時があります。その場合はAWSドキュメントのトップページから探すと見つけやすいかもしれません。こちらで検索すればAWSのドキュメントのみを検索することができます。
ぜひご活用ください。