三度の飯よりGitHub-Flowが好き
GitHub-flowはシンプルなブランチ戦略で、以下のような手順で本番環境に変更を反映する
- mainブランチからfeatureブランチを作成
- 変更をコミットし、プルリクエストを作成
- レビュワーからの指摘を受けつつ、リモートレポジトリの内容を変更
- プルリクエストがapproveされたら、mainブランチに反映
図解するとこんな感じ:
詳しくは以下参照:
上記図の「リモートにpushしてPull Requestをオープン」の部分で、テスト用環境にデプロイできればさらに便利になりそうだ。ローカル環境でのテスト実行がうまくいっても、クラウド環境上でうまくいくとは限らない。GitHub Actionsを工夫して書けば、PRを提出した段階でテスト用の環境にデプロイする「ブランチデプロイ」が実現でき、開発フローをのシンプルさを維持できる。実際、オーケストレーションツールのdagsterはこのBranch Deploymentをネイティブ機能として組み込んでいたりする。
このブランチデプロイをServerless Frameworkで実行してみたいと思う。
Serverless Frameworkについて
Serverless Frameworkはサーバーレスのアプリケーションのデプロイを簡素化するフレームワークである。詳細は割愛するが、AWS Lambda関数のデプロイが簡単になったりして嬉しい。自分の業務だと、データパイプラインの一部でAWS Lambdaを使っているため、関数の管理を抽象化する用途でServerless Frameworkを利用している。
なお、Serverless FrameworkにもBranch Deploymentsの機能はあるが、思っていたのとは違ったのでGithub Actionsで実装する方針をとっている。
実装
要件定義:どんなふうにするか
- 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