見出し画像

【AWS SAM】 Lambda + Step Functions + EventBridge

こんにちは、 iwanaga です。
表題のコードを垂れ流します。
サムネイルは以下から拝借しています

簡単な経緯

  • このブログは、3 名の怠惰さんが休日を充実させるべく、何かしらのアウトプットを持ち回りで行う趣味ブログである

    • 持ち回りの担当回をサボった場合は、ペナルティとして他の怠惰さんに金銭を支払うというルール付き

  • 3 名は怠惰なので、自分の担当日を覚えておくのが面倒、考えるのも面倒

  • なので、リマインドの仕組みが必要

    • メールだと見ないので Slack でメンションが欲しい

  • ペナルティも覚えておくのが面倒なので、ゆくゆくはシステムで全てを管理したい

…ということで、まずは担当日に通知するシステムを構築することになりました。

なんで SAM?

  • 最優先は金額が安いこと

    • アプリケーションは従量課金の Lambda

    • DB も従量課金の DynamoDB

  • サーバレス構成になりそう

    • 関数ローカル実行やロールスニペットも提供されている SAM を採用

      • SAM が想定するユースケースに合致していれば短いコードでいい感じに IaC を実現できる

      • CloudFormation の記述も共存可能なので、定義できないリソースがあるという懸念もない

      • あと単純に技術的興味

構成図

  • AWS リソース

AWS リソース
  • StepFunction 定義

StepFunction 定義

SAM テンプレート

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  SAM Template for weekly_report

Parameters:
  DynamoTableName:
    Type: String
  NotifyWeeklyReportSlackUrl:
    Type: String

Globals:
  Function:
    Timeout: 10
    Environment:
      Variables:
        NOTIFY_WEEKLY_REPORT_SLACK_URL: !Ref NotifyWeeklyReportSlackUrl
        DYNAMO_DB_TABLE: !Ref DynamoTableName

Resources:
  #####################################
  # 次週報を書くユーザ取得
  #####################################
  GetNextWeeklyReportUserFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: build/lambda
      Handler: get-next-weekly-report-user.handler
      Runtime: nodejs12.x
      FunctionName: nippo-get-next-weekly-report-user
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref DynamoTableName
  #####################################
  # 週報提出者にリマインド
  #####################################
  RemindWeeklyReportSlackFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: build/lambda
      Handler: remind-weekly-report-slack.handler
      Runtime: nodejs12.x
      FunctionName: nippo-remind-weekly-report-slack
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref DynamoTableName
  #####################################
  # 最後に週報書いたユーザ番号を更新
  #####################################
  UpdateLastWeeklyReporterIndexFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: build/lambda
      Handler: update-last-weekly-reporter-index.handler
      Runtime: nodejs12.x
      FunctionName: nippo-update-last-weekly-reporter-index
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref DynamoTableName

  #####################################
  # State Machine Remind Weekly Report to Slack
  #####################################
  RemindWeeklyReportToSlack:
    Type: AWS::Serverless::StateMachine
    Properties:
      Name: remind-weekly-report-to-slack-state-machine
      DefinitionUri: infrastructure/remind-weekly-report-job.asl.json
      DefinitionSubstitutions:
        NippoGetNextWeeklyReportUserArn: !GetAtt GetNextWeeklyReportUserFunction.Arn
        NippoRemindWeeklyReportSlackArn: !GetAtt RemindWeeklyReportSlackFunction.Arn
        NippoUpdateLastWeeklyReporterIndexArn: !GetAtt UpdateLastWeeklyReporterIndexFunction.Arn
      Role: !GetAtt StateMachineRemindWeeklyReportToSlackRole.Arn
      Logging:
        Level: ALL
        IncludeExecutionData: True
        Destinations:
          - CloudWatchLogsLogGroup:
              LogGroupArn: !GetAtt StateMachineRemindWeeklyReportToSlackLogGroup.Arn
      Events:
        ScheduledEvent:
          Type: Schedule
          Properties:
            Name: remind-weekly-report-to-slack-scheduled-event
            Enabled: true
            Schedule: cron(0 0 ? * SAT-SUN *)

  #####################################
  # State Machine Remind Weekly Report to Slack Log Group
  #####################################
  StateMachineRemindWeeklyReportToSlackLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: remind-weekly-report-to-slack-logs
  StateMachineRemindWeeklyReportToSlackRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - states.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /service-role/
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess # 妥協 https://docs.aws.amazon.com/step-functions/latest/dg/cw-logs.html#cloudwatch-iam-policy
      Policies:
        - PolicyName: lambda-scoped-access
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: lambda:InvokeFunction
                Resource:
                  - !GetAtt GetNextWeeklyReportUserFunction.Arn
                  - !GetAtt RemindWeeklyReportSlackFunction.Arn
                  - !GetAtt UpdateLastWeeklyReporterIndexFunction.Arn

補足: ブログ投稿のことを便宜的に「週報」「WeeklyReport」という概念で呼称しています。

起動コマンド

- name: Sam Package
  run: |
    sam package --template-file template.yaml \
    --output-template-file packaged-template.yaml \
    --s3-bucket nippo-scheduled-task-sam
- name: Sam Deploy
  run: |
    echo y | sam deploy --no-fail-on-empty-changeset \
    --stack-name nippo-scheduled-task \
    --template-file packaged-template.yaml \
    --parameter-overrides \
    DynamoTableName=${{ secrets.NIPPO_TABLE }} \
    NotifyWeeklyReportSlackUrl=${{secrets.NOTIFY_WEEKLY_REPORT_SLACK_URL}} \
    --capabilities CAPABILITY_IAM \
    --role-arn ${{secrets.SAM_EXECUTION_ROLE_ARN}}

簡単な解説

Transform

SAM を使うことを明示します。

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

Parameters

コマンドラインから受け取るパラメタを定義します。
!Ref DynamoTableName みたいな感じで参照できます。

Parameters:
  DynamoTableName:
    Type: String

Globals

テンプレート内の全リソースに適用する設定を行います(デフォルトの設定のようなイメージ)。
今回の例では、Function (Lambda 関数) に適用する設定を行います。Timeout 10 秒と環境変数を設定しています。

Globals:
  Function:
    Timeout: 10
    Environment:
      Variables:
        NOTIFY_WEEKLY_REPORT_SLACK_URL: !Ref NotifyWeeklyReportSlackUrl
        DYNAMO_DB_TABLE: !Ref DynamoTableName

Lambda 関数

以下の定義リファレンスに従っています(雑)。

  GetNextWeeklyReportUserFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: build/lambda
      Handler: get-next-weekly-report-user.handler
      Runtime: nodejs12.x
      FunctionName: nippo-get-next-weekly-report-user
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref DynamoTableName

StepFunctions

  • DefinitionUri

    • 定義は別ファイル(remind-weekly-report-job.asl.json)で持っているのでそれを指定

  • DefinitionSubstitutions

    • 該当定義ファイルのプレースホルダーを埋めます。このブロックに書いておけば勝手に埋めてくれるので便利。

    • 定義ファイル側で ${NippoGetNextWeeklyReportUserArn} のように定義しておくことでまるっと埋めてくれる

  • Events

    • StateMachine の実行トリガーを設定できる

      • StateMachine 側から実行トリガーを指定するのは若干変な感じはするが…

    • ここに書いておけば、別定義することなく EventBridge リソースが作られる…!! これが非常に便利。

    • ただし、CloudFormation ほどの柔軟な設定はできないので、SAM で対応できない設定をする場合は CloudFormation リソース定義に切り替える必要がありそう

    • 注意: StateMachine のリソース名と Events のリソース名の合計が長すぎるとデプロイエラーになる。文字数制限があるっぽい。

RemindWeeklyReportToSlack:
    Type: AWS::Serverless::StateMachine
    Properties:
      Name: remind-weekly-report-to-slack-state-machine
      DefinitionUri: infrastructure/remind-weekly-report-job.asl.json
      DefinitionSubstitutions:
        NippoGetNextWeeklyReportUserArn: !GetAtt GetNextWeeklyReportUserFunction.Arn
        NippoRemindWeeklyReportSlackArn: !GetAtt RemindWeeklyReportSlackFunction.Arn
        NippoUpdateLastWeeklyReporterIndexArn: !GetAtt UpdateLastWeeklyReporterIndexFunction.Arn
      Role: !GetAtt StateMachineRemindWeeklyReportToSlackRole.Arn
      Logging:
        Level: ALL
        IncludeExecutionData: True
        Destinations:
          - CloudWatchLogsLogGroup:
              LogGroupArn: !GetAtt StateMachineRemindWeeklyReportToSlackLogGroup.Arn
      Events:
        ScheduledEvent:
          Type: Schedule
          Properties:
            Name: remind-weekly-report-to-slack-scheduled-event
            Enabled: true
            Schedule: cron(0 0 ? * SAT-SUN *)

ロググループと IAM ロール

CloudFormation の記述に従っているのであまり書くことはなさそう

起動コマンド

デプロイコマンド実行は、ベストプラクティスに則り、 IAM ユーザに IAM ロールを引き受けさせる形で実行します。
IaC を実現すると、広範囲のアクセス権限が必要になり、ユーザとして持たせておくと、その鍵管理をどうするか問題が発生するからです。
secrets は、GitHub Actions に設定した secrets を使用する想定です。
具体的なコマンドについては、この辺を参考に…

まとめ

SAM テンプレートを雑にみてきました。
AWS の公式ドキュメントは探すのも読むのも大変でしたが、SAM のおかげでパッと構築することができました。
今後は、EventBridge 複数起動しても大丈夫なようにする、ペナルティの計測などを実装していければ良いかなと思います。
他にも構築したシステムがあるので、それも今後記事にできればと思っています。

もっとこうした方が良いよ的な知見、お待ちしております。
では、ごきげんよう。

いいなと思ったら応援しよう!