GitHub Flowで開発者の人数が増えてくると、QA環境の順番待ちが発生するようになります。
待ちが発生すると作業効率が悪くなるので、専用で使える環境が欲しくなります。
プレビューデプロイ環境(以降、preview環境)は、QA環境と同じく、masterマージ前に本番に近い環境でテストを行う為の環境です。
ローカル(開発)環境だと、push漏れや環境差異で、正しくテストできない場合があるので避けたい。ステージング(STG)環境だと、masterマージ後にデプロイするので、リリースブロックが発生します。外部連携の問題で、QA環境やSTG環境でしかテストできない場合もありますが、代替手段を用意して回避すると良さそう。
GitFlow+定期リリースだと通常はSTG環境で足りますが、バージョンアップ等、developマージ前にテストしたい時にも役立ちます。
→ GitHub Flowは本当に良いのか?
トリガーを何にするか?
テストが必ず必要になるとは限らない。単位はブランチかPR(Pull Request)か。
ブランチの場合、ブランチ名が決めたルール(preview/xxx等)でpushされたら環境構築。
ブランチ削除したら環境削除。
環境停止のトリガーがないのと、使おうと思った時にブランチ名を変える必要があるのが難点。
PRの場合、特定のラベル(create-preview等)を付けたら環境構築。
ラベルを外したら環境停止。
PRをClose(Merge含む)か、特定のラベル(destroy-preview等)を付けたら環境削除。
ラベルの方が自由度が高いので、良さそう。
DBを分けるか?
preview環境では、STG環境のDBを使う事が多そうだが、最終的にmigrateしない事になった場合に戻す手間が発生する。バグったデータが入った場合も同様。
なので、STG環境のDBをコピーするか、新たに作るのが良さそう。
コストを下げるには?
ALBとRDSのコストが意外と掛かる。削除するまでコストが発生する。
QA環境やSTG環境のと共存するのが良さそう。
ALBは使わずにグローバルIPでアクセス方法もあるけど、
HTTPSでのアクセスや冗長化のテストができなくなる。
マルチドメインかrelative_url_rootでルールを作って、ECS向けのターゲットグループへ転送するのが良さそう。
RDSはconfig/database.ymlのdatabaseをENVで変更すれば良さそう。
DBユーザー(username)がdb:createできる権限があると楽そう。
assets置き場
本番ではないのでDocker内でも良いけど、コンパイル済みのassetsがある方が高速です。
S3やEFSで共存しても、どこかの段階でクリーニングしないと増え続けるのは変わらない。
ファイル数減らせるので、少しだけどコスト的にも有利。
Sidekiqで使うRedis
パラメータの/0を/1とかに変えればデータベースを変えられるけど、デフォルト16(変更可能)
サーバーレスなら保存したデータの存続時間と読み書きの量なので、別途用意してもコストはあまり変わらなそう。
サーバーレス、Valkey、アジアパシフィック (東京) 保存されたデータ USD 0.101 / GB-時 ElastiCache Processing Units (ECPU) USD 0.0027 / 100 万 ECPU
ECS Fargate
メモリよりもvCPUのコストインパクトが大きい。
preview環境ならあまりアクセスないので、0.5で良さそうだけど、
taskは、通常シングルプロセスなので、減らしても待ち時間増えるだけなので、1で、
assets:precompileは、マルチプロセスで動くので、4とかでも良さそう。
メモリは、コストインパクト少ないので、足りなくならないように、
2GB、taskは4GB、assets:precompileは8GBとかでも良さそう。
アジアパシフィック (東京) 1 時間あたりの vCPU 単位 USD 0.05056 1 時間あたりの GB 単位 USD 0.00553
Github Actionsの雛形を作る
.github/workflows/create-preview.yml
name: Create preview
on:
pull_request:
types: [labeled]
PRにラベルと付けたら実行。指定したラベルが付いた時だけ実行はできなそう。
→ chatさんにlabels教えてもらいましたが動かなかった。
concurrency:
group: preview-${{ github.ref }}
cancel-in-progress: true
同じgroupのが動いていたら停止させる。今回はstop-preview.ymlと揃える。
env:
CREATE_LABEL: 'create-preview'
DESTROY_LABEL: 'destroy-preview'
jobs:
check-target:
name: Check target
runs-on: ubuntu-latest
outputs:
TARGET_ACTION: ${{ steps.check.outputs.TARGET_ACTION }}
steps:
- name: Check if PR is target label was added
id: check
if: ${{ github.event.label.name == env.CREATE_LABEL && github.event.pull_request.state != 'closed' && !contains(toJson(github.event.pull_request.labels.*.name), env.DESTROY_LABEL) }}
run: echo "TARGET_ACTION=true" | tee -a $GITHUB_OUTPUT
以降のjobをスキップする為に、判定結果を残しておく。
→ それぞれのstepやjobでifで判定するしかなさそう。
build-and-push:
name: Build and Push
runs-on: ubuntu-latest
needs: check-target
if: needs.check-target.outputs.TARGET_ACTION == 'true'
steps:
# TODO: buildしてECRにpush
register-task-definition:
name: Register task definition
runs-on: ubuntu-latest
needs: [build-and-push, check-target]
if: ${{ success() && needs.check-target.outputs.TARGET_ACTION == 'true' }}
steps:
# TODO: ECSのタスク定義を作成(app, task-normal, task-high)
run-initialize-task:
name: Run initialize task
runs-on: ubuntu-latest
needs: [register-task-definition, check-target]
if: ${{ success() && needs.check-target.outputs.TARGET_ACTION == 'true' }}
steps:
# TODO: db:create db:migrate assets:precompileを実行
create-or-update-service:
name: Create or Update service
runs-on: ubuntu-latest
# needs: [run-initialize-task, register-task-definition, check-target] # NOTE: previewのみ高速化の為、並列実行
needs: [register-task-definition, check-target]
if: ${{ success() && needs.check-target.outputs.TARGET_ACTION == 'true' }}
steps:
# TODO: ALBのターゲットグループとルールを作成、ECSのサービスを作成 or 更新(停止でdesired-countを0にしたのを戻す)
notify-success:
name: Notify success
runs-on: ubuntu-latest
# needs: [create-or-update-service, check-target] # NOTE: previewのみ高速化の為、並列実行に変更
needs: [create-or-update-service, run-initialize-task, check-target]
if: ${{ success() && needs.check-target.outputs.TARGET_ACTION == 'true' }}
steps:
# TODO: PRにコメントやSlack等に成功通知
notify-failed:
name: Notify failure
runs-on: ubuntu-latest
# needs: [create-or-update-service, check-target]
needs: [create-or-update-service, run-initialize-task, check-target] # NOTE: previewのみ高速化の為、並列実行に変更
if: ${{ failure() && needs.check-target.outputs.TARGET_ACTION == 'true' }}
steps:
# TODO: PRにコメントやSlack等に失敗通知
.github/workflows/stop-preview.yml
name: Stop preview
on:
pull_request:
types: [unlabeled]
concurrency:
group: preview-${{ github.ref }}
cancel-in-progress: true
env:
CREATE_LABEL: 'create-preview'
DESTROY_LABEL: 'destroy-preview'
jobs:
check-target:
runs-on: ubuntu-latest
outputs:
TARGET_ACTION: ${{ steps.check.outputs.TARGET_ACTION }}
steps:
- name: Check if target label was removed
id: check
if: ${{ github.event.label.name == env.CREATE_LABEL && github.event.pull_request.state != 'closed' && !contains(toJson(github.event.pull_request.labels.*.name), env.DESTROY_LABEL) }}
run: echo "TARGET_ACTION=true" | tee -a $GITHUB_OUTPUT
update-service:
name: Update service
runs-on: ubuntu-latest
needs: check-target
if: needs.check-target.outputs.TARGET_ACTION == 'true'
steps:
# TODO: ECSのサービスを更新(desired-countを0に変更)
notify-success:
name: Notify success
runs-on: ubuntu-latest
needs: [update-service, check-target]
if: ${{ success() && needs.check-target.outputs.TARGET_ACTION == 'true' }}
steps:
# TODO: PRにコメントやSlack等に成功通知
notify-failed:
name: Notify failure
runs-on: ubuntu-latest
needs: [update-service, check-target]
if: ${{ failure() && needs.check-target.outputs.TARGET_ACTION == 'true'}}
steps:
# TODO: PRにコメントやSlack等に失敗通知
.github/workflows/destroy-preview.yml
name: Destroy preview
on:
pull_request:
types: [closed, labeled]
concurrency:
group: destroy-preview-${{ github.ref }} # NOTE: create-previewが後から動くと止まってしまう為、groupを変える
cancel-in-progress: true
env:
CREATE_LABEL: 'create-preview'
DESTROY_LABEL: 'destroy-preview'
jobs:
check-target:
name: Check target
runs-on: ubuntu-latest
outputs:
TARGET_ACTION: ${{ steps.check.outputs.TARGET_ACTION }}
steps:
- name: Check if PR is closed or target label was added
id: check
if: ${{ github.event.pull_request.state == 'closed' || github.event.label.name == env.DESTROY_LABEL }}
run: echo "TARGET_ACTION=true" | tee -a $GITHUB_OUTPUT
delete-service:
name: Delete service
runs-on: ubuntu-latest
needs: check-target
if: needs.check-target.outputs.TARGET_ACTION == 'true'
steps:
# TODO: ECSのサービスを削除、ALBのルールとターゲットグループを削除
rub-db-drop:
name: Run db drop
runs-on: ubuntu-latest
needs: check-target
if: needs.check-target.outputs.TARGET_ACTION == 'true'
steps:
# TODO: db:dropを実行
delete-task-definitions:
name: Deregister task definitions
runs-on: ubuntu-latest
needs: [delete-service, rub-db-drop, check-target]
if: ${{ success() && needs.check-target.outputs.TARGET_ACTION == 'true' }}
steps:
# TODO: 対象のECSのタスク定義を削除
delete_ecr_images:
name: Delete ecr images
runs-on: ubuntu-latest
needs: [delete-service, rub-db-drop, check-target]
if: ${{ success() && needs.check-target.outputs.TARGET_ACTION == 'true' }}
steps:
# TODO: 対象のECRのイメージを削除
remove-labels:
name: Remove labels
runs-on: ubuntu-latest
needs: [delete-task-definitions, delete_ecr_images, check-target]
if: ${{ success() && needs.check-target.outputs.TARGET_ACTION == 'true' }}
steps:
# TODO: CREATE_LABELとDESTROY_LABELを外す
notify-success:
name: Notify success
runs-on: ubuntu-latest
needs: [delete-task-definitions, delete_ecr_images, check-target]
if: ${{ success() && needs.check-target.outputs.TARGET_ACTION == 'true' }}
steps:
# TODO: PRにコメントやSlack等に成功通知
notify-failed:
name: Notify failure
runs-on: ubuntu-latest
needs: [delete-task-definitions, delete_ecr_images, check-target]
if: ${{ failure() && needs.check-target.outputs.TARGET_ACTION == 'true'}}
steps:
# TODO: PRにコメントやSlack等に失敗通知