LoginSignup
1
0

AWS SAM で関数とレイヤーを別々に管理(Python)

Posted at

概要

現在 AWS Lambda で複数の関数を運用しているが、共通するライブラリやコードが存在しているため、Lambda レイヤーで共通部分をくくり出したい
Lambda 関数については AWS SAM で実装しているため、レイヤーも同様に実装・管理を行いたい
ただしレイヤーはどの関数も参照するため、どこかの関数と一緒にするよりも独立させ、個別に管理やデプロイを行いたい

このような要望があり、色々調べたのだが、まとまった方法が見つからなかったため、ここにまとめる

環境

  • Python 3.11.1
  • aws-cli/2.13.12
  • SAM CLI, version 1.95.0

結論

いきなり結論から書くが、以下のような形で実装した

ファイル構成(レイヤー)

.
├─ python
|  ├─ __init__.py
|  ├─ utils_layer.py
│  └─ requirements.txt
├─ tests
|  ├─ unit
|  |  ├─ __init__.py
|  |  └─ test_utils_layer.py
|  ├─ __init__.py
│  └─ requirements.txt
├─ samconfig.toml
└─ template.yaml

README などは割愛

テンプレート(レイヤー)

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  Utils Layer

Resources:
  UtilsLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      Description: Utils Layer
      LayerName: utils_layer
      ContentUri: 'python/'
      RetentionPolicy: Retain
      CompatibleRuntimes:
        - python3.11
    Metadata:
      BuildMethod: python3.11
  SSMLatestArn:
    Type: AWS::SSM::Parameter
    Properties:
      Name: !Sub /lambda-layer/latest/${AWS::StackName}
      Type: String
      Value: !Ref UtilsLayer

Outputs:
  Name:
    Description: Stack Name.
    Value: !Ref AWS::StackName
    Export:
      Name: !Ref AWS::StackName

  UtilsLayerArn:
    Description: Published UtilsLayer ARN.
    Value: !Ref UtilsLayer
    Export:
      Name: !Sub ${AWS::StackName}-arn

テンプレート(関数)

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  Get Function

Parameters:
  LayerArn:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /lambda-layer/latest/utils-layer

Resources:
  GetFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: get_function/
      Handler: app.lambda_handler
      Runtime: python3.11
      Layers:
        - !Ref LayerArn
      Architectures:
        - x86_64

必要な部分だけ抜粋

コード(関数)

import utils_layer as utils

def lambda_handler(event, context):
    utils.test()

    return {
        "statusCode": 200,
        "body": "OK"
    }

解説

基本方針として、sam init で作成したテンプレートからなるべく少ない変更で実装を行う

まずレイヤーのファイル構成だが、共有コードや共有ライブラリを登録した requirements.txt を置くフォルダ名は python 固定
ここを別の名前にしてしまうと import しても読み込んでくれず、訳の分からない No module エラーに悩まされることになる
そして共有関数を記述するファイル名は init で作成された app.py のままでも問題ないのだが、import 時にファイル名を指定することになるため、わかりやすい名前に変更しておくほうが良い

続いてテンプレート
基本的には上記をベースに必要な要素を追加してやれば問題なく動くはず
解説が必要な部分としてはレイヤーの読み込み部分だろう
レイヤーの読み込みは関数側のテンプレートにレイヤーの ARN を指定することになるのだが、この ARN はバージョンを含める必要がある
そのため、今回はレイヤーのデプロイ時に最新バージョンを含めた ARN をパラメータストアに保存し、関数側でその値を参照する形を取っている
クロススタックの参照を使っても問題ない
その場合は以下のようになる

      Layers:
        - !ImportValue utils_layer_arn

ただし、上記のように直接指定する場合は1つ注意が必要
ImportValue にしろ、パラメータストアにしろ、デプロイは問題ないのだが、ローカル環境での実行の場合は値を取ってきてくれないのだ
なので、読み込んだ値は一旦パラメーターにデフォルトとして指定しておき、ローカル実行時にはパラメーターをオーバーライドできるようにしておくと使い勝手が良い
以下のようなコマンドで実行できる

sam local invoke --parameter-overrides LayerArn=arn:aws:lambda:region:XXXXXX:layer:utils_layer:XX

ここまでやってレイヤーをデプロイすると、関数から何食わぬ顔で import できるようになる
ただしここでも1つ注意が必要で、ライブラリはともかく、作成した関数の場合、ユニットテストなどを行ったときに No module エラーを食らう場合がある
なのでそういった場合は import をレイヤーを使う関数内で行うようにすると良い

def lambda_handler(event, context):
    import utils_layer as utils

    utils.test()

    return {
        "statusCode": 200,
        "body": "OK"
    }

見栄えが悪い?そうね……

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0