yqを利用してSAMのテンプレートファイルを分割する

2024.04.03

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

今回は、yqを利用してSAMのテンプレートファイルを分割してみたいと思います。

はじめに

皆さんはAWS Serverless Application Model (AWS SAM) をお使いでしょうか。

SAMのテンプレートファイルはCloudFormationベースで記載することができるので、個人的には比較的とっつきやすい印象を持っています。

しかし、SAMはそのテンプレートファイルであるtemplate.yamlを別のyamlファイルに分割したい場合、必然的に別スタックとして作成し、スタックをネストさせる必要があります。

したがって、1つのスタックでデプロイしたい場合はどうしてもtemplate.yamlが長くなりがちです。

長くなる1つのテンプレートファイルには、以下の問題が発生します。

  • 可読性の低下・改修工数の増加
  • 共同開発における競合解消の複雑化

今回はファイルを分割しつつ1つのスタックでデプロイすることを目標に、yqを利用して、何とかtemplate.yamlを分割してみたいと思います。

前提

今回デプロイする構成

今回は以前私が以下のブログで紹介したLambda+Glue+Step Functionsの構成を2つ分デプロイしたいと思います。

yqのインストール

今回は上述の通りyqを利用します。

yqはHomebrewを利用して簡単にインストールすることができますので、インストールがまだの方は以下を実施して下さい。

$ brew install yq

なお、バージョンは4を利用します。

$ brew info yq  
==> yq: stable 4.43.1 (bottled), HEAD

ファイル準備

フォルダ構成

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

.
├── deploy.sh
├── glue_scripts
│   └── test_glue.py
├── handler
│   └── test_func.py
├── resources
│   ├── etl_job_1.yaml
│   ├── etl_job_2.yaml
│   └── iam_role.yaml
├── samconfig.toml
└── template.yaml

上記の内、glue_scripts配下のtest_glue.pyファイルと、handler配下のtest_func.py、およびsamconfig.tomlファイルは先に紹介したこちらのブログと同じものになるので、今回は記載を割愛します。

template.yamlの作成

今回、SAMのテンプレートファイルとなるtemplate.yamlは以下のように記載します。

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:
  - $file: resources/iam_role.yaml
  - $file: resources/etl_job_1.yaml
  - $file: resources/etl_job_2.yaml

外部ファイルを読み込む際のキーは$fileとしました。

上記の通りResourcesセクションの記述を3つのファイルに分割しており、具体的にはIAM Roleと、ETLジョブ(Lambda+Glue+Step Functions)をそれぞれ別のyamlで記載します。

こうすることで、例えばETLジョブを複数人で並行して開発する場合でも、開発者はそれぞれのyamlにリソースを追記し、template.yamlの編集を最小限に抑えることができます。

iam_role.yamlの作成

iam_role.yamlは以下の通りです。Lambda, Glue, Step Functionsにアタッチするロールをそれぞれ記載しています。

iam_role.yaml

# 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 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 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

記載方法は上記の通り、本来Resourcesセクションに記載する内容をそのままyamlファイルに記載しています。

etl_job_1.yamlとetl_job_2.yamlの作成

etl_job_1.yamlは以下の通りです。Lambda, Glue, Step Functions(+それを起動するEventBridge)をそれぞれ記載しています。

etl_job_1.yaml

# Lambda
MyLambdaFunction1:
  Type: AWS::Serverless::Function
  Properties:
    FunctionName: "uehara-sam-test-lambda-1"
    Role: !GetAtt MyLambdaFunctionRole.Arn
    CodeUri: handler/
    Handler: test_func.lambda_handler
    Runtime: python3.9
    Architectures:
      - x86_64
# Glue Job
MyGlueJob1:
  Type: AWS::Glue::Job
  Properties:
    Role: !GetAtt MyGlueJobRole.Arn
    GlueVersion: '3.0'
    Name: "uehara-sam-test-glueJob-1"
    DefaultArguments:
      "library-set": "analytics"
    Command:
      Name: pythonshell
      ScriptLocation: "s3://cm-da-uehara/glue-scripts/test_script.py"
      PythonVersion: "3.9"
    ExecutionProperty:
      MaxConcurrentRuns: 3
    MaxCapacity: 0.0625
    MaxRetries: 0
# Step Functions
MyStepFunctions1:
  Type: AWS::Serverless::StateMachine
  Properties:
    Name: "uehara-sam-test-sf-1"
    Definition:
      Comment: "Test Step Functions"
      StartAt: InvokeLambda
      States:
        InvokeLambda:
          Type: Task
          Resource: !GetAtt MyLambdaFunction1.Arn
          Next: InvokeGlueJob
        InvokeGlueJob:
          Type: Task
          Resource: "arn:aws:states:::glue:startJobRun.sync"
          Parameters:
            JobName: !Ref MyGlueJob1
          End: true
    Role: !GetAtt MyStepFunctionsRole.Arn
    Events:
      S3Event:
        Type: EventBridgeRule
        Properties:
          RuleName: "uehara-sam-test-sf-event-1"
          Pattern:
            source:
              - aws.s3
            detail-type:
              - "Object Created"
            detail:
              bucket:
                name:
                  - "cm-da-uehara"
              object:
                key:
                  - prefix: "tmp/"

次にetl_job_2.yamlです。

あくまで検証用なので、今回はetl_job_1.yamlの名前を変更するのみにします。

etl_job_2.yaml

# Lambda
MyLambdaFunction2:
  Type: AWS::Serverless::Function
  Properties:
    FunctionName: "uehara-sam-test-lambda-2"
    Role: !GetAtt MyLambdaFunctionRole.Arn
    CodeUri: handler/
    Handler: test_func.lambda_handler
    Runtime: python3.9
    Architectures:
      - x86_64
# Glue Job
MyGlueJob2:
  Type: AWS::Glue::Job
  Properties:
    Role: !GetAtt MyGlueJobRole.Arn
    GlueVersion: '3.0'
    Name: "uehara-sam-test-glueJob-2"
    DefaultArguments:
      "library-set": "analytics"
    Command:
      Name: pythonshell
      ScriptLocation: "s3://cm-da-uehara/glue-scripts/test_script.py"
      PythonVersion: "3.9"
    ExecutionProperty:
      MaxConcurrentRuns: 3
    MaxCapacity: 0.0625
    MaxRetries: 0
# Step Functions
MyStepFunctions2:
  Type: AWS::Serverless::StateMachine
  Properties:
    Name: "uehara-sam-test-sf-2"
    Definition:
      Comment: "Test Step Functions"
      StartAt: InvokeLambda
      States:
        InvokeLambda:
          Type: Task
          Resource: !GetAtt MyLambdaFunction2.Arn
          Next: InvokeGlueJob
        InvokeGlueJob:
          Type: Task
          Resource: "arn:aws:states:::glue:startJobRun.sync"
          Parameters:
            JobName: !Ref MyGlueJob2
          End: true
    Role: !GetAtt MyStepFunctionsRole.Arn
    Events:
      S3Event:
        Type: EventBridgeRule
        Properties:
          RuleName: "uehara-sam-test-sf-event-2"
          Pattern:
            source:
              - aws.s3
            detail-type:
              - "Object Created"
            detail:
              bucket:
                name:
                  - "cm-da-uehara"
              object:
                key:
                  - prefix: "tmp/"

deploy.shの作成

今回一番重要なdeploy.shを作成します。記載内容は以下の通りです。

deploy.sh

#!/bin/bash
set -eu

TMP_FILE='temp.yaml'

yq '(.. | select(has("$file"))) |= load(.$file) | .Resources = (.Resources[] as $item ireduce ({}; . * $item))' template.yaml > ${TMP_FILE}

sam deploy --template ${TMP_FILE}

rm ${TMP_FILE}

やっていることとしては、yqのloadを使用して別のyamlファイルからデータを読み込み、ただこの状態ではResourcesが配列になっており本来のtemplate.yamlのフォーマットになってないのでireduceしたテンプレートファイルでデプロイを行うという形になっています。

実行してみる

実行は以下で可能です。

$ bash deploy.sh

変更スタックを確認してみると、別々のyamlファイルに記載したリソースがきちんと読み込まれていることが伺えます。

そのまま進めばAWS環境にリソースがデプロイされます。

最後に

今回は、yqを利用してSAMのテンプレートファイルを分割してみました。

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

参考文献