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(変更可能)
サーバーレスなら保存したデータの存続時間と読み書きの量なので、別途用意してもコストはあまり変わらなそう。

料金 – Amazon ElastiCache | AWS

サーバーレス、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とかでも良さそう。

料金 – AWS Fargate | AWS

アジアパシフィック (東京)
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等に失敗通知

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です