api gateway + lambda(python) + laravel (3) フォームの送信値によって読みこむyamlを変更するとかいう編

この種類のシステムを構築するとき一人でやる場合pythonもphpもやらなくちゃいけないので、混乱気味になりがちだが、対処法は、無い。

lambda(python)側

とりあえず現状

import json

def lambda_handler(event, context):
    # eventの 'body' からデータを取得し、JSONとして解析
    body = json.loads(event.get('body', '{}'))
    
    # keyとvalueのペアを格納する辞書
    output_dict = {}
    
    # body内のすべてのkey-valueペアをイテレーション
    for key, value in body.items():
        output_dict[key] = value  # キーと値をoutput_dictに追加
    
    # レスポンスを生成
    response = {
        'statusCode': 200,
        'body': json.dumps(output_dict)  # output_dictをJSON形式にしてbodyに設定
    }
    
    return response

まず、このコードはほぼ意味を無さないので、もうちょいまじめにやろう。configというディレクトリを作成し、その中にdefault.yamlとdev.yamlという2つのファイルを作成するとする。これはlambda側に用意する。

今は簡単なコードなのでlambdaのオンラインエディタで作成しちゃえばいいと思う

default.yamlの例

settings:
  mode: 'default'
  timeout: 30
  retry: 3
  logging: false

dev.yamlの例

settings:
  mode: 'dev'
  timeout: 60
  retry: 10
  logging: true

まあ、何というかこんなのをでっち上げて読み込むとしましょうか。

オンラインで作っていく

さてさて、環境ができたらflagを受けとろう

laravel編

の前に

api urlをベタ書きするのではなくconfigに追い出してみよう。ここではconfig/services.phpに追記する

config/services.php

     // これをまるっと追加 
    'lambda' => [
        'api_url' => env('LAMBDA_API_URL'),
    ],

この設定ファイルは.envのLAMBDA_API_URLを読みこむので.envにapiのurlを書けばokだ。

        $url = config('services.lambda.api_url');

        // POSTリクエストを送信
        $response = Http::post($url, [
            'flag' => $flag,
        ]);
        // レスポンスを処理(例:JSONとして取得)
        $data = $response->json();
        dd($data);

最終的にこのようなファイルでチェックボックスの有無により返却されるファイル名が変更されていれば成功であーる

lambda側のロギング、テスト

さてさて、まあ今は何のファイルを読んでいるかreturnしているので明確なのだが、これを一応ログに取って見る方法も行ってみよう

import json
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    # eventの 'body' からデータを取得し、JSONとして解析
    body = json.loads(event.get('body', '{}'))
    # キー「flag」を取得、存在しなければ0で埋める
    flag = body.get('flag', 0)
    if flag == 0:
        config_path = 'config/default.yaml'
    else:
        config_path = 'config/dev.yaml'
        
    logging.info(f"Selected config path: {config_path}")
    
    # レスポンスを生成
    response = {
      'statusCode': 200,
      'body': json.dumps({
            'config': config_path
        })  # 読み込んだconfig_dataをJSON形式にしてbodyに設定
    }
    
    return response

これでdeployする。これはimport loggingでロガーをimportし

 logging.info(f"Selected config path: {config_path}")

でログ出力する。fっていうのは {}の中の変数展開するものと覚えといてもまあ問題ない

実行結果は全く変わらないはずだ。なお、最新のログストリームに以下のように主力されていれば成功である。

なお、eventのフルダンプをこのように取得してテストする方法もあるけど、まあ長くなるからええか…

yamlをロードしてみる

yamlのロードの方法はPyYAML を使う方法が一般的であるが、これは標準モジュールではないので取り込んでこないといけない。

モジュールの取り込み方

ここからはちょっとディープであるがライブラリーを取得してそれをzip転送するという方法がある、が、まずそもそもライブラリーを取得せねばなるまい。使っているpythonのバージョンを確認しよう。「関数」のところまで戻ると一覧できるだろう。

今回はこちら

このようにlambdaのpytyon環境はいろいろとランタイムを分ける事ができるので、その環境に応じたライブラリーをひっこぬいてくる事がベストプラクティスである。簡単に行う場合はdockerを利用する事だ(ただしアーキテクチャーの違いは越えられないからね。armで動かしてるならarmのホストで行う事)。

% docker run -it --name temp-container python:3.11 bash
Unable to find image 'python:3.11' locally
3.11: Pulling from library/python
a014e5e7d08c: Already exists
715cea74ecbb: Already exists
003f1109a212: Already exists
a56ae3b61eb9: Already exists
c3668095a3a2: Already exists
7a7e9903522f: Pull complete
e17425f8448e: Pull complete
16fc1f695aa0: Pull complete
Digest: sha256:02808bfd640d6fd360c30abc4261ad91aacacd9494f9ba4e5dcb0b8650661cf5
Status: Downloaded newer image for python:3.11
root@66d820ed532e:/#

こうすると簡単にpythonのそれぞれのバージョンの環境が手に入る

root@66d820ed532e:/# mkdir /packages
root@66d820ed532e:/# pip install pyyaml -t /packages
Collecting pyyaml
  Obtaining dependency information for pyyaml from https://files.pythonhosted.org/packages/5e/94/7d5ee059dfb92ca9e62f4057dcdec9ac08a9e42679644854dc01177f8145/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.metadata
  Downloading PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.metadata (2.1 kB)
Downloading PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (732 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 732.2/732.2 kB 14.8 MB/s eta 0:00:00
Installing collected packages: pyyaml
Successfully installed pyyaml-6.0.1
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
root@66d820ed532e:/# ls -l /packages/
total 12
drwxr-xr-x 2 root root 4096 Sep  5 01:32 PyYAML-6.0.1.dist-info
drwxr-xr-x 3 root root 4096 Sep  5 01:32 _yaml
drwxr-xr-x 3 root root 4096 Sep  5 01:32 yaml

rootでやるなよーと言われるけどまあコンテナなのでいいでしょう

で、コンテナの外に出て

% mkdir tmp
% cd tmp
% docker cp temp-container:/packages .
% ls
packages

などとすればフルコピーできる。コピーしたらコンテナはもう使わないので削除するのがいいだろう

% docker rm -f temp-container
temp-container

さて、ここでこんなディレクトリ構成を作成する

mkdir -p python/lib/python3.11/site-packages/

python3.11は御自身のランタイムのバージョンに合わせる必要があるが、この後

cp -r packages/* python/lib/python3.11/site-packages/

等して、コピーを行った後

zip -r layer.zip python/

でlayer.zipを作成する

なお、もう少し小さくもできる

find ./packages -type d -name '__pycache__' -exec rm -r {} +
find ./packages -name '*.pyc' -exec rm -f {} +

こうしておくとcacheが削除される

いずれにせよlayer.zipを手に入れたならとりあえずアップロードできる環境に転送しよう。aws cliが使えるよーって人はまあここみなくても出来るでしょう。

レイヤーのアップロードの前に失敗コードを書く

import json
import logging
import yaml  # PYAMLをインポート

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    # テスト用のYAMLデータ
    sample_yaml = '''
    name: John
    age: 30
    '''
    
    # YAMLをJSONに変換(PYAMLが正常にインストールされているかのテスト)
    try:
        sample_json = yaml.safe_load(sample_yaml)
        logger.info(f"Converted YAML to JSON: {sample_json}")
    except Exception as e:
        logger.error(f"Could not convert YAML to JSON: {e}")
        
    # eventの 'body' からデータを取得し、JSONとして解析
    body = json.loads(event.get('body', '{}'))
    
    # キー「flag」を取得、存在しなければ0で埋める
    flag = body.get('flag', 0)
    
    if flag == 0:
        config_path = 'config/default.yaml'
    else:
        config_path = 'config/dev.yaml'
        
    logger.info(f"Selected config path: {config_path}")
    
    # レスポンスを生成
    response = {
      'statusCode': 200,
      'body': json.dumps({
            'config': config_path
        })  # 読み込んだconfig_dataをJSON形式にしてbodyに設定
    }
    
    return response

このようにyamlのロードを行うコードを書いておく。import yamlは非標準モジュールなので実行すると失敗するだろう。これはテストで行ってみるのが楽かもしれない。

'lambda_function': No module named 'yaml'になる

レイヤーのアップロード

これは関数ごとに存在しているわけじゃなくて、lambdaのトップから行うんだね

レイヤーの作成から

こんな感じでアップロードしている

そうするとlayerが作成されARNが与えられるのでコピっておく

さて、当該のlambda関数に戻ってlayerを追加する。下の方にあるよ

arnを貼り付けてまあ一応検証するとこんな感じになる。「互換性のあるランタイム」に関してはpython3.10までしか出てこねえんじゃないかなあ?いずれにせよ追加する

でまあ先刻のテストを行うと成功した

ログをよくみりゃテストコードがログに出力されているよね

Converted YAML to JSON: {'name': 'John', 'age': 30}

最後の仕上げとして指定されたconfigを読んで、返す

今はダミーのyaml文字列を読んでるだけだが、configでファイルを指定しているので、これを読んでみよう。

import json
import logging
import yaml  # PYAMLをインポート

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):

    # eventの 'body' からデータを取得し、JSONとして解析
    body = json.loads(event.get('body', '{}'))
    
    # キー「flag」を取得、存在しなければ0で埋める
    flag = body.get('flag', 0)
    
    if flag == 0:
        config_path = 'config/default.yaml'
    else:
        config_path = 'config/dev.yaml'

    # 指定されたyamlファイルのロード
    with open(config_path, 'r') as f:
        config_data = yaml.safe_load(f)
    logger.info(f"Selected config path: {config_path}")
    logger.info(f"Loaded config data: {config_data}")
    
    # レスポンスを生成
    response = {
      'statusCode': 200,
      'body': json.dumps({
            'config_path': config_path,
            'config_data': config_data 
        })  # 読み込んだconfig_dataをJSON形式にしてbodyに設定
    }
    
    return response

phpでの出力は以下のようになるはずである


この記事が気に入ったらサポートをしてみませんか?