Lambda関数をOSXのローカルにてバージョン管理しつつ更新するフローまで組み立てた記録

管理コンソール上で直接作成したLambda関数を、バージョン管理の導入とユニットテスト及びローカルからの更新フローに切り替えたくなった時、大体以下のような課題が出てきます。

  • リポジトリへの登録
  • AWS上への反映
  • テストの実施

構成を出来る限りシンプルにしつつ、手戻り等が発生しないことを心掛けながら、再現性のある手続きを備忘録として残してみました。

環境整理

Python3.6で、以下のライブラリを使います。

ライブラリ名 導入方法
pipenv Homebrew
anyenv Homebrew
direnv Homebrew
tox pypi
lambda-uploader pypi
pytest pypi

ディレクトリ構成

Lambda関数の実装をインターフェイスとモデルで分離する場合は、srcとtests内に配置します。

.
|-- Brewfile
|-- .gitignore
|-- .envrc
|-- Pipfile
|-- Pipfile.lock
|-- README.md
|-- function.py
|-- lambda.json
|-- requirements.txt
|-- reset_lambda_json.py
|-- src
|   `-- lib
|       |-- __init__.py
|       `-- lib.py
|-- tests
|   |-- conftest.py
|   |-- fixtures
|   |   `-- event.json
|   `-- test_lib.py
|-- tox.ini
|-- update_lambda_json.py
`-- upload.sh

gitignore

環境に依存するファイルや一時ディレクトリを記録します。

% vim .gitignore
.venv/
.tox/
.envrc
*.pyc 

ライブラリ導入

pipenvにはdev指定で入れることにより、requirements.txtの更新生成手順を簡潔にします。

% brew install pipenv direnv anyenv
% echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc
% echo 'eval "$(anyenv init -)"' >> ~/.zshrc
% echo 'export PIPENV_VENV_IN_PROJECT=true' >> ~/.zshrc
% echo 'eval "$(pipenv --completion)"' >> ~/.zshrc
% exec $SHELL -l
% pipenv install --python 3.6
% pipenv install --dev python-lambda-local lambda-uploader pytest tox

環境変数設定

個別の環境に依存する変数も想定して、コミットせずにローカル管理します。

3行目に追加しているsourceによって、作業ディレクトリへ移動すると同時に仮想環境を起動しています。

% direnv edit .
export AWS_PROFILE=xxxxx
export XXX=xxx
source .venv/bin/activate

テスト環境設定

direnvで設定した環境変数でテスト時に使いたいものがある場合はpassenvに追加指定します。

% vim tox.ini
[tox]
skipsdist = True
[testenv]
passenv = AWS_PROFILE
deps = pipenv
setenv = PYTHONPATH = {toxinidir}/src
         PIPENV_VERBOSITY = -1
commands = pipenv sync --dev
           pipenv run py.test tests/ --pdb

Lambda更新用手続き

lambda-uploader実行の為の設定を行います。基本的にはlambda.jsonに追記するだけです。

ただし、環境変数に関してはdirenvの内容を反映しつつもコミットを防止するため、更新時するタイミングでlambda.jsonへ反映し、更新が終わったら反映を元に戻します。

function.pyへの移植

管理コンソール上で直接書いていた関数の内容を移植します。

後は必要に応じて実装を分離してlib以下に配置します。

lambda.jsonへの設定

コンソール上に反映したくないファイルをignoreセクションに追加します。多少追加もれがあるかもしれません。

{
    "name": "<function name>",
    "description": "function",
    "region": "ap-northeast-1",
    "runtime": "python3.6",
    "handler": "function.lambda_handler",
    "role": "arn:aws:iam::00000000000:role/lambda_basic_execution",
    "ignore": [
        "lambda_function\.zip$",
        "circle\.yml$",
        "\.git$",
        "\.gitignore$",
        "/.*\.pyc$",
        "Pipfile$",
        "Pipfile\.lock$",
        "tests$",
        "\.envrc$",
        "tox\.ini$",
        "upload_lamdba_json\.py$",
        "reset_lamdba_json\.py$",
        "\.tox$"
        "\.venv$"
    ],
    "timeout": 300,
    "memory": 128,
    "variables": {}
}

アップロード処理

極力環境変数を引数に渡さないで実行可能にします。MFA設定済みのアカウントの場合は毎回認証が発生します。

% vim upload.sh
direnv allow . #本来想定している環境変数で上書きする
rm -rf .tox/ #テスト用の一時ディレクトリが存在する場合、消滅したファイルを参照しにいくケースがあったため
rm -rf .lambda_uploader_temp/ #前回のアップロード用一時ファイルを参照しようとして、エラーになるケースがあったため
pipenv lock -r > requirements.txt
python update_lambda_json.py #lambda.jsonに環境変数を一時的に書き加える
lambda-uploader
python reset_lambda_json.py #lambda.jsonに加えた一時的な更新を元に戻す

MFA設定

AWSアカウントに二段階認証(MFA)を設定済みの場合は、configにmfa_serialを忘れずに設定します。

% vim ~/.aws/config
[profile XXXX]
mfa_serial = arn:aws:iam::000000000000000000:mfa/XXXXXXXXXXXXXXXX
...

upload_lambda_json.py

lamdba.jsonの環境変数指定部分を一時的に書き換えるためのバッチです。

import json
import os

envs = ['XXXX', 'LINE_CHANNEL_ACCESS_TOKEN', 'LINE_CHANNEL_SECRET']
dir_path = os.path.dirname(__file__)
json_data = None
with open(os.path.join(dir_path, 'lambda.json'), 'r') as r:
    json_data = json.load(r)
    variables = dict()
    for env in envs:
        variables.update({env: os.environ[env]})
    json_data['variables']  = variables
with open(os.path.join(dir_path, 'lambda.json'), 'w') as w:
    json.dump(json_data, w, indent=4)

reset_lambda_json.py

lambda.jsonで書き換えた環境変数指定部分をリセットするためのバッチです。

import json
import os

dir_path = os.path.dirname(__file__)
json_data = None
with open(os.path.join(dir_path, 'lambda.json'), 'r') as r:
    json_data = json.load(r)
    json_data['variables']  = dict()
with open(os.path.join(dir_path, 'lambda.json'), 'w') as w:
    json.dump(json_data, w, indent=4)

各フェイスでの操作

環境変数等の指定をdirenvに極力寄せたため、単純にコマンドを実行するだけになります。

テスト

テストで失敗するとデバッガが作動します。

tox

更新

必要に応じてワンタイムトークンを表示するデバイスも準備してください。

sh upload.sh

まとめ

開発とテストが極力繰り返しやすくなるような設計にしてみました。特に環境変数指定で悩まされることがなくなるはずです。

あくまでも一例ですが、Lambda関数等の開発で参考になれば幸いです。