Lambda+Glue+Step Functionsの構成をServerless FrameworkとAWS SAMのそれぞれでデプロイしてみた

2024.04.02

データアナリティクス事業本部のueharaです。

今回は、Lambda+Glue+Step Functionsの構成をServerless FrameworkAWS SAMのそれぞれでデプロイしてみたいと思います。

はじめに

2023年の10月に、Serverless FrameworkがV.4から有料化されることが発表されました。

これまでAWS上でETL処理を行うリソースのデプロイにServerless Frameworkを多用していたのですが、有料化に伴い別の手段も検討したく、本記事ではAWS Serverless Application Model (AWS SAM)を利用したいと思います。

今回デプロイを検証するのは、比較的軽量なETLでありがちな以下の構成になります。

S3の特定のパスにファイルがPutされたことをEventBridgeで検知し、Step Functionsを起動します。

Step FunctionsのワークフローとしてはLambda→Glueと処理が進むような流れを考えます。

※S3は既に構築済みのものとし、Serverless Framework及びSAMでのデプロイは今回は行いません。

Serverless Frameworkでのデプロイ

まずはServerless Frameworkによるデプロイを行います。

フォルダは構成は以下の通りです。

.
├── glue_scripts
│   └── test_glue.py
├── handler
│   └── test_func.py
├── package.json
└── serverless.yml

ファイルの用意

test_glue.py

test_glue.pyにはGlueで実行するスクリプトを記載します。

今回は検証なので一定時間Sleepするだけの処理を記載することにします。

test_glue.py

import sys
import time


def main(argv):
    print("start")
    # sleep 5 minutes
    time.sleep(300)
    print("end")
    

main(sys.argv)

こちらのファイルをGlueから参照できるようにするため任意のS3バケットにアップロードしておきます。

私は s3://cm-da-uehara/glue-scripts/test_glue.py としてアップロードしました。

test_func.py

test_func.py にはLambdaで実行するスクリプトを記載します。

Glueのスクリプトと同様に、単純に一定時間Sleepするだけの処理を記載することにします。

test_func.py

import time


def lambda_handler(event, context):
    print("start")
    # sleep 1 minute
    time.sleep(60)
    print("end")

    return {"message": "success"}

package.json

package.jsonに今回のデプロイに関連するリソースの依存関係を記載しておきます。

package.json

{
    "dependencies": {
        "serverless": "^3.19.0"
    },
    "devDependencies": {
        "serverless-python-requirements": "^5.4.0",
        "serverless-step-functions": "^3.20.1"
    },
    "name": "uehara-sls-test"
}

Serverless Frameworkを用いたStep Functionsのデプロイにはserverless-step-functionsプラグインが必要になるため、そちらも記載をしています。

serverless.yml

Serverless Frameworkでのデプロイにおいて一番の肝になる serverless.yml は以下の通りに記載します。

serverless.yml

service: uehara-test-app-sls
frameworkVersion: '3'
configValidationMode: error
plugins:
  - serverless-step-functions

provider:
  name: aws
  runtime: python3.9
  stackName: ${self:service}
  stage: ${env:ENV, 'dev'}
  region: ap-northeast-1
  deploymentBucket:
    name: cm-da-uehara
  timeout: 180 # 180 seconds
  memorySize: 128

package:
  individually: true
  patterns:
    - '!handler/**'
    - '!.git/**'
    - '!.gitignore'
    - '!.serverless'
    - '!.serverless/**'
    - '!package.json'
    - '!package-lock.json'
    - '!serverless.yml'
    - '!yarn.lock'
    - '!node_modules'
    - '!node_modules/**'
    - '!__pycache__'

functions:
  # Lambda
  MyLambdaFunction:
    handler: handler/test_func.lambda_handler
    name: "uehara-sls-test-lambda"
    role: MyLambdaFunctionRole
    package:
      patterns:
        - 'handler/test_func.py'

resources:
  Resources:
    # Lambda Role
    MyLambdaFunctionRole:
      Type: AWS::IAM::Role
      Properties:
        RoleName: "uehara-sls-test-lambda-role"
        AssumeRolePolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Action: sts:AssumeRole
              Principal:
                Service:
                  - lambda.amazonaws.com
        ManagedPolicyArns:
          - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
    # Glue Job
    MyGlueJob:
      Type: AWS::Glue::Job
      Properties:
        Role: !GetAtt GlueJobRole.Arn
        GlueVersion: '3.0'
        Name: uehara-sls-test-glueJob
        DefaultArguments:
          "library-set": "analytics"
        Command:
          Name: pythonshell
          ScriptLocation: "s3://cm-da-uehara/glue-scripts/test_glue.py"
          PythonVersion: "3.9"
        ExecutionProperty:
          MaxConcurrentRuns: 3
        MaxCapacity: 0.0625
        MaxRetries: 0
    # Glue Job Role
    GlueJobRole:
      Type: AWS::IAM::Role
      Properties:
        RoleName: "uehara-sls-test-glueJob-role"
        AssumeRolePolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Effect: Allow
              Principal:
                Service: glue.amazonaws.com
              Action: "sts:AssumeRole"
        ManagedPolicyArns:
          - arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole
          - arn:aws:iam::aws:policy/AmazonS3FullAccess
    # Step Functions Role
    MyStepFunctionsRole:
      Type: AWS::IAM::Role
      Properties:
        RoleName: "uehara-sls-test-sf-role"
        AssumeRolePolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - states.ap-northeast-1.amazonaws.com
              Action: sts:AssumeRole
        ManagedPolicyArns:
          - arn:aws:iam::aws:policy/service-role/AWSLambdaRole
          - arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole
    # EventBridge Role
    MyEventBridgeRole:
      Type: AWS::IAM::Role
      Properties:
        AssumeRolePolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - events.amazonaws.com
              Action: sts:AssumeRole
        Policies:
          - PolicyName: EventBridgePolicy
            PolicyDocument:
              Version: '2012-10-17'
              Statement:
                - Effect: Allow
                  Action:
                    - "states:StartExecution"
                  Resource:
                    - "arn:aws:states:*:*:stateMachine:$uehara-sls-test-sf"
        RoleName: uehara-sls-test-eventbridge-role

stepFunctions:
  # Step Functions
  stateMachines:
    MyStepFunctions:
      name: "uehara-sls-test-sf"
      role: !GetAtt MyStepFunctionsRole.Arn
      definition:
        Comment: "Test Step Functions"
        StartAt: InvokeLambda
        States:
          InvokeLambda:
            Type: Task
            Resource: !GetAtt MyLambdaFunction.Arn
            Next: InvokeGlueJob
          InvokeGlueJob:
            Type: Task
            Resource: "arn:aws:states:::glue:startJobRun.sync"
            Parameters:
              JobName: !Ref MyGlueJob
            End: true
      events:
        - cloudwatchEvent:
            name: "uehara-sls-test-sf-event"
            iamRole: !GetAtt MyEventBridgeRole.Arn
            event:
              source:
                - "aws.s3"
              detail-type:
                - "Object Created"
              detail:
                bucket:
                  name:
                    - "cm-da-uehara"
                object:
                  key:
                    - prefix: 'tmp/'

特徴として、Lambda関数についてはfunctionsセクションに、Step FunctionsについてはstepFunctionsセクションに記載を行います。

GlueのScriptLocationについては、ご自身でアップロードしたGlueのスクリプトを保管しているS3のURIを指定して下さい。

Step Functionsの起動トリガーについて、cm-da-ueharaというS3バケットにオブジェクトキーのPrefixにtmp/がついたものがPutされたら起動するというものになっています。

デプロイ

以下コマンドでデプロイを行います。

$ sls deploy --verbose

デプロイが完了すると、以下のようにStep Functionsが実行できるようになっているかと思います。

実行

指定したS3バケットのtmp/に適当なデータをPutしてStep Functionsを起動すると、Lambda→Glueの順に処理が実施されます。

それぞれの処理はただ単にSleepをしているだけなので、問題なく終了するかと思います。

AWS SAMでのデプロイ

次に、SAMで同じことをしてみます。

フォルダは構成は以下の通りです。

.
├── glue_scripts
│   └── test_glue.py
├── handler
│   └── test_func.py
├── samconfig.toml
└── template.yaml

ファイルの用意

【注】 test_glue.pytest_func.pyはServerless Frameworkで記載した内容と同じのため、ここでは割愛します。

samconfig.toml

samconfig.tomlはAWS SAM CLIの設定ファイルになります。

記載方法についてはこちらのドキュメントに記載がありますので、そちらを参考頂ければと思います。

今回は以下のように設定してみました。

samconfig.toml

version = 0.1

[default]
region = "ap-northeast-1"

[default.build.parameters]
debug = true

[default.deploy.parameters]
stack_name = "uehara-test-app"
s3_bucket = "cm-da-uehara"
s3_prefix = "sam-deploy"
capabilities = "CAPABILITY_NAMED_IAM"
confirm_changeset = true

1点特筆しておくと、カスタム名を持つIAMリソースを作成する場合は、CAPABILITY_NAMED_IAMの指定が必要になりますので、今回そちらを設定しています。

template.yaml

template.yamlがSAMのテンプレートファイルになります。

※Serverless Frameworkでいうところのserverless.ymlに相当。

記載内容は以下の通りです。

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: "Test SAM Application"

Globals:
  Function:
    Timeout: 180 # 180 seconds
    MemorySize: 128

Resources:
  # Lambda
  MyLambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: "uehara-sam-test-lambda"
      Role: !GetAtt MyLambdaFunctionRole.Arn
      CodeUri: handler/
      Handler: test_func.lambda_handler
      Runtime: python3.9
      Architectures:
        - x86_64
  # Lambda Role
  MyLambdaFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: "uehara-sam-test-lambda-role"
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - lambda.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
  # Glue Job
  MyGlueJob:
    Type: AWS::Glue::Job
    Properties:
      Role: !GetAtt MyGlueJobRole.Arn
      GlueVersion: '3.0'
      Name: "uehara-sam-test-glueJob"
      DefaultArguments:
        "library-set": "analytics"
      Command:
        Name: pythonshell
        ScriptLocation: "s3://cm-da-uehara/glue-scripts/test_glue.py"
        PythonVersion: "3.9"
      ExecutionProperty:
        MaxConcurrentRuns: 3
      MaxCapacity: 0.0625
      MaxRetries: 0
  # Glue Job Role
  MyGlueJobRole:
      Type: AWS::IAM::Role
      Properties:
        RoleName: "uehara-sam-test-glueJob-role"
        AssumeRolePolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Effect: Allow
              Principal:
                Service: glue.amazonaws.com
              Action: "sts:AssumeRole"
        ManagedPolicyArns:
          - arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole
          - arn:aws:iam::aws:policy/AmazonS3FullAccess
  # Step Functions
  MyStepFunctions:
    Type: AWS::Serverless::StateMachine
    Properties:
      Name: "uehara-sam-test-sf"
      Definition:
        Comment: "Test Step Functions"
        StartAt: InvokeLambda
        States:
          InvokeLambda:
            Type: Task
            Resource: !GetAtt MyLambdaFunction.Arn
            Next: InvokeGlueJob
          InvokeGlueJob:
            Type: Task
            Resource: "arn:aws:states:::glue:startJobRun.sync"
            Parameters:
              JobName: !Ref MyGlueJob
            End: true
      Role: !GetAtt MyStepFunctionsRole.Arn
      Events:
        S3Event:
          Type: EventBridgeRule
          Properties:
            RuleName: "uehara-sam-test-sf-event"
            Pattern:
              source:
                - aws.s3
              detail-type:
                - "Object Created"
              detail:
                bucket:
                  name:
                    - "cm-da-uehara"
                object:
                  key:
                    - prefix: "tmp/"
  # Step Functions Role
  MyStepFunctionsRole:
      Type: AWS::IAM::Role
      Properties:
        RoleName: "uehara-sam-test-sf-role"
        AssumeRolePolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - states.ap-northeast-1.amazonaws.com
              Action: sts:AssumeRole
        ManagedPolicyArns:
          - arn:aws:iam::aws:policy/service-role/AWSLambdaRole
          - arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole

Lambdaのメモリサイズやタイムアウト等の設定値については、先にServerless Frameworkでデプロイしたものと同じ値にしています。

Serverless Frameworkと違い、LambdaやStep Functionsについても全てResourcesセクションに記載をします。

Lambdaについて、Serverless Frameworkではserverless.ymlのトップのpackagepatternsで明示的に除外設定を行わなければトップ配下の全てのリソースが各関数にアップロードされる仕組みなのですが、SAMではCodeUriにてLambdaにアップロードするディレクトリを明示的に指定します。

Step Functionsについて、起動のためのEventBridgeのルールはEventsプロパティに記載することができます。

Serverless Frameworkの時と違いEventBridge用のIAMロールを明示的に用意しておりませんが、上記の記載で指定のステートマシンのみを起動することが許可されたポリシーを持つIAMロールが自動で作成されアタッチされます。

また、Step Functionsの定義についてはDefinitionプロパティにてyaml形式で記載することができますが、以下のように別ファイルに外出しして記載することもできます。

MyStepFunctions:
  Type: AWS::Serverless::StateMachine
  Properties:
    Name: "uehara-sam-test-sf"
    DefinitionUri: sfs/xxx.json
    Role: !GetAtt MyStepFunctionsRole.Arn
    ...

デプロイ

以下コマンドでデプロイを行います。

$ sam deploy

デプロイが完了すると、以下のようにStep Functionsが実行できるようになっているかと思います。

明示的に作成していなかったEventBridgeのIAMロールついても念のため確認すると、以下の通り指定のStep Functionsを起動できるのポリシーのみを持つものになっていました。

実行

Serverless Frameworkでのデプロイと同じ内容のものをデプロイしているので、挙動に違いはありません。

Step Functionsが起動されると、Lambda→Glueの順に処理が実施されます。

SAMを利用して気になった点

基本的にはCloudFormationベースでの記載となるため、Serverless FrameworkとSAMで記述方法が似ているところも多いですが、個人的にSAMについて以下の点が気になりました。

yamlファイルが分割できない

Serverless Frameworkではyamlファイルの記述を分割し、ファイルを分けることができます。

例:

serverless.yml

functions:
  # Lambda関数を3つ用意
  - ${file(lambda/lambda_a.yml):functions}
  - ${file(lambda/lambda_b.yml):functions}
  - ${file(config/lambda_c.yml):functions}

これにより、複数人での共同開発においてserverless.ymlの競合をなるべく抑えることができ、またserverless.ymlがダラダラと長くならないため可読性も向上します。

SAMでもファイル分割自体をすることができますが、それは「スタックも別に作成する」ということになり、スタックを別々に作成しネストする他ありません。

SAMで1つのスタックにまとめたいとなるとtemplate.yamlが肥大化することになるので、その点1つのスタックを複数ファイルに分割して記載できるServerless Frameworkは便利だと感じました。

変数が使えない

Serverless Frameworkにはcustomというセクションがあり、自由に変数を定義できます。

SAMでもMappingsセクションにパラメータ値をゴリゴリ書くこともできますが、Serverless Frameworkの変数ほどの汎用性はないためその点についても惜しいポイントだと感じます。

最後に

今回は、Lambda+Glue+Step Functionsの構成をServerless FrameworkとAWS SAMのそれぞれでデプロイしてみました。

参考になりましたら幸いです。

参考文献