現実モデリング

データとかエンジニアリングとか健エミュとか

Serverless Frameworkで実現するGitHub-flow

三度の飯よりGitHub-Flowが好き

GitHub-flowはシンプルなブランチ戦略で、以下のような手順で本番環境に変更を反映する

  1. mainブランチからfeatureブランチを作成
  2. 変更をコミットし、プルリクエストを作成
  3. レビュワーからの指摘を受けつつ、リモートレポジトリの内容を変更
  4. プルリクエストがapproveされたら、mainブランチに反映

図解するとこんな感じ:

GitHub-flowについて

詳しくは以下参照:

docs.github.com

上記図の「リモートにpushしてPull Requestをオープン」の部分で、テスト用環境にデプロイできればさらに便利になりそうだ。ローカル環境でのテスト実行がうまくいっても、クラウド環境上でうまくいくとは限らない。GitHub Actionsを工夫して書けば、PRを提出した段階でテスト用の環境にデプロイする「ブランチデプロイ」が実現でき、開発フローをのシンプルさを維持できる。実際、オーケストレーションツールのdagsterはこのBranch Deploymentをネイティブ機能として組み込んでいたりする。

docs.dagster.io

このブランチデプロイをServerless Frameworkで実行してみたいと思う。

Serverless Frameworkについて

Serverless Frameworkはサーバーレスのアプリケーションのデプロイを簡素化するフレームワークである。詳細は割愛するが、AWS Lambda関数のデプロイが簡単になったりして嬉しい。自分の業務だと、データパイプラインの一部でAWS Lambdaを使っているため、関数の管理を抽象化する用途でServerless Frameworkを利用している。

github.com

なお、Serverless FrameworkにもBranch Deploymentsの機能はあるが、思っていたのとは違ったのでGithub Actionsで実装する方針をとっている。

www.serverless.com

実装

要件定義:どんなふうにするか

  • PRを提出したら、PRごとに個別に作成されたステージにデプロイされるようにする
  • cron式は除外して、定期実行されないようにする
  • PRをクローズしたら自動的に関数が削除されるようにする

ディレクトリ構成

.
├── .github
│   └── workflows
│       ├── delete_cron_trigger_from_serverless_yml.py
│       ├── sls_branch_deployment.yml
│       └── sls_main_deploy.yml
├── ServerlessFramework
│   ├── serverless.yml
│   ├── DockerImagePattern
│   │   ├── Dockerfile
│   │   ├── lambda_function.py
│   │   └── requirements.txt
│   └── SampleFunction
│       ├── lambda_function.py
│       └── requirements.txt
...

ファイル

sls_branch_deployment.ymlの中身は以下の通り: ステージ名はブランチ名からとっており、AWS Lambdaの関数の命名規則上使えない特殊文字は全てエスケープしている。

name: Serverless Framework branch deployment

on:
  pull_request:
    types: [opened, synchronize, reopened, closed]
    paths:
      - "ServerlessFramework/**"
  workflow_dispatch:

env:
  SERVERLESS_ACCESS_KEY: ${{ secrets.SERVERLESS_ACCESS_KEY }}

jobs:
  branch_deployment:
    name: Branch Deployment
    runs-on: ubuntu-latest
    if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'reopened'
    steps:
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 16

      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          python-version: 3.9

      - name: Install serverless framework
        run: |
          echo "install serverless framework"
          npm install -g serverless
          npm install serverless-python-requirements

      - name: checkout for deployment
        uses: actions/checkout@v4
        with:
          ref: ${{ github.head_ref }}
          sparse-checkout: |
            ServerlessFramework
            .github
          path: 'project-repo'

      - name: Remove Cron trigger
        run: |
          cd project-repo
          pwd
          pip3 install pyyaml
          python3 .github/workflows/delete_cron_trigger_from_serverless_yml.py ServerlessFramework/serverless.yml
      
      - name: Make dot env file
        run: |
          touch .env
          echo env_val=${{vars.ENV_VAL}} >> .env
          // 必要に応じて追加
          cp .env project-repo/ServerlessFramework/.env

      - name: Deploy Functions
        run: |
          # Deploy to lambda
          echo "deploy to lambda"
          cd project-repo/ServerlessFramework
          branch_name=${{ github.head_ref }}
          escaped_branch_name=${branch_name//[^[:alnum:]]/-}
          short_branch_name=${escaped_branch_name:0:10}
          sls deploy --stage ${short_branch_name} --verbose

  delete_functions:
    name: Delete Function
    runs-on: ubuntu-latest
    if: github.event.action == 'closed'
    steps:
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 16

      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          python-version: 3.9

      - name: Install serverless framework
        run: |
          echo "install serverless framework"
          npm install -g serverless
          npm install serverless-python-requirements

      - name: checkout main branch
        uses: actions/checkout@v4
        with:
          ref: main
          sparse-checkout: |
            ServerlessFramework
          path: 'project-repo'
      
      - name: Make dot env file
        run: |
          touch .env
          echo env_val=${{vars.ENV_VAL}} >> .env
          // 必要に応じて追加
          cp .env project-repo/ServerlessFramework/.env
      - name: Delete Functions
        run: |
          echo "remove from lambda"
          cd project-repo/ServerlessFramework
          branch_name=${{ github.head_ref }}
          escaped_branch_name=${branch_name//[^[:alnum:]]/-}
          short_branch_name=${escaped_branch_name:0:10}
          sls remove --stage ${short_branch_name} --verbose

ブランチデプロイ環境で関数が定期実行されては困るので、serverless.ymlからcron式を除去する必要がある。 この動作を実行するPythonスクリプトdelete_cron_trigger_from_serverless_yml.pyの中身は以下の通り:

import yaml
import sys

serverless_yml_filepath = sys.argv[1]
# YAMLファイルを読み込む
with open(serverless_yml_filepath, "r") as f:
    raw_yaml = yaml.safe_load(f)

for function_name in raw_yaml["functions"].keys():
    function_detail_infomation = raw_yaml["functions"][function_name]
    if ("events" in function_detail_infomation.keys()) and (
        "schedule" in function_detail_infomation["events"][0].keys()
    ):
        del raw_yaml["functions"][function_name]["events"]
    else:
        pass

# 編集後のYAMLをファイルに書き戻す
with open(serverless_yml_filepath, "w") as f:
    yaml.safe_dump(raw_yaml, f)

本番用にデプロイしたい場合は、発動条件を変更し、ブランチデプロイ用のymlからcron式の除外部分を削除し、ステージ名をmainに変えれば良い。 本番用のGithub Actionsのコードsls_main_deployment.ymlは以下の通り:

name:  Serverless Framework prod deployment

on:
  push:
    branches:
      - main
    paths:
      - "ServerlessFramework/**"
  workflow_dispatch:

env:
  SERVERLESS_ACCESS_KEY: ${{ secrets.SERVERLESS_ACCESS_KEY }}

jobs:
  main_deployment:
    name: main_deployment
    runs-on: ubuntu-latest

    steps:
      - name: checkout for deployment
        uses: actions/checkout@v4
        with:
          ref: ${{ github.head_ref }}
          sparse-checkout: |
            ServerlessFramework
            .github
          path: 'project-repo'
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 16

      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          python-version: 3.9

      - name: Install serverless framework
        run: |
          echo "install serverless framework"
          npm install -g serverless
          npm install serverless-python-requirements

      - name: Make dot env file
        run: |
          touch .env
          echo env_val=${{vars.ENV_VAL}} >> .env
          // 必要に応じて追加
          cp .env project-repo/ServerlessFramework/.env
      
      - name: Deploy Functions
        run: |
          echo "deploy to lambda"
          cd project-repo/ServerlessFramework
          sls deploy --stage dev --verbose