ECSでマルチステージング環境を実現した設計と実装

こんにちは、普段PR TIMES STORY(以下STORY)の開発リーダーをしている岩下(@SardineTa23)です。

PR TIMES STORYは弊社のMissionである「行動者発の情報が、人の心を揺さぶる時代へ」をそのまま体現したようなプロダクトで、「創業ストーリー」や「開発秘話」などの行動者の熱量をそのまま配信して、企業とメディア、生活者のより良い・多くのリレーションが生まれることを目的としています。

PR TIMES STORYトップページ

本記事では、私も今月から2年目となったので新卒の時よりさらにチームに貢献していきたい!と思っていたところ、STORYの現状の環境では開発速度が出しづらい状況を抱えていたので、そちらを解消するためにチームで行ったことを書いていきます。

目次

現状

STORYは一部PR TIMESとは別リポジトリ、別インフラで独立して管理されており、独立している部分に関してはAWSのECS上にデプロイされています。デプロイフローはAWSのCodeシリーズを使って管理されており、GitHubのdevelopブランチがステージング環境に、masterブランチが本番環境にそれぞれデプロイされています。

PR TIMES STORYのデプロイフロー

発生していた問題は「developブランチに気軽に作業内容がマージができない」というものです。Aさんがdevelopにマージした内容をステージング環境でQAして要修正となると、リリースせずにdevelopブランチ居座り続けることとなります。そうすると別の変更を加えたBさんはdevelopになかなかマージすることができずにオープンなPull Requestが何日も放置される事象がしばしば発生していました。本来であれば作業した内容はすぐにQAして、問題がなければすぐにリリースしたいです。

BさんはAさんの作業途中には、developに簡単にマージできないので動作確認できない。

PR TIMESの開発では、以前から複数のステージング環境を使えるようになっており、STORYでも似たような仕組みが欲しいなと思っていました。

作ってみた

上記の問題を解消するために、エンジニア各々が新たな環境をGitHub Actionsを使ってデプロイできる仕組みを作成しました。新規作成したAWSリソースはterraformで管理されています。新たな環境を含めたステージング環境の構成は以下です。比較のためにこれまでの通常ステージングのリクエストの流れを黒線で、今回作成したリソースの流れをそれ以外の色で表しています。

今回開発したマルチステージングを使うためには、開発者はまず自身の構築したい環境のドメインを通常ステージング環境のサブドメインとして発行します。こちらの流れはterraform + GitHub Actionsで管理されており、上の図の青い線が一連の流れです。

開発者は自身のGitHub IDを以下のようにterraformの連想配列に追加するだけで 、<GitHub ID>.example.com のような形で新環境用のAWSリソースをデプロイできます。ちなみに連想配列のKeyは以下で述べるリスナールールのpriorityを決める際に使用しています。

新環境を作る際のPull Requestの差分

terraform applyで作成されるリソースは以下です。

Terraform will perform the following actions:

  # aws_ecs_service.pr_check["4"] will be created
  + resource "aws_ecs_service" "マルチステージング環境名" {
      + cluster                            = "AWSアカウントID:cluster/マルチステージング環境名"
      + deployment_maximum_percent         = 200
      + deployment_minimum_healthy_percent = 100
      + desired_count                      = 1
      + enable_ecs_managed_tags            = false
      + enable_execute_command             = true
      + iam_role                           = (known after apply)
      + id                                 = (known after apply)
      + launch_type                        = "FARGATE"
      + name                               = "hoge"
      + platform_version                   = (known after apply)
      + scheduling_strategy                = "REPLICA"
      + task_definition                    = "AWSアカウントID:task-definition/Stubコンテナイメージ"
      + wait_for_steady_state              = false

      + load_balancer {
          + container_name   = "nginx"
          + container_port   = 80
          + target_group_arn = (known after apply)
        }

      + network_configuration {
          ~~~省略~~~
        }
    }

  # aws_lb_listener_rule.pr_check["4"] will be created
  + resource "aws_lb_listener_rule" "マルチステージング環境名" {
      + arn          = (known after apply)
      + id           = (known after apply)
      + listener_arn = "AWSアカウントID:listener/app/マルチステージング環境名"
      + priority     = 104 // 連想配列のKey値を使ったpriority

      + action {
          + order            = (known after apply)
          + target_group_arn = (known after apply)
          + type             = "forward"
        }

      + condition {
          + host_header {
              + values = [
                  + "hoge.マルチステージングドメイン",
                ]
            }
        }
    }

  # aws_lb_target_group.pr_check["4"] will be created
  + resource "aws_lb_target_group" "マルチステージング環境名" {
      ~~~省略~~~
    }

  # aws_route53_record.ecs_tasks_pr_check["4"] will be created
  + resource "aws_route53_record" "マルチステージング環境名" {
      ~~~省略~~~
      + name            = "hoge"
      + type            = "A"
      + zone_id         = "~~~省略~~~"

      ~~~省略~~~
    }

Plan: 4 to add, 0 to change, 0 to destroy.
  1. hoge.example.comのようなサブドメインがRoute53のホストゾーンに登録される
  2. ALB関連ではホストベースルーティングの機能を使用して、hoge環境用へのリクエストをさばくためのリスナールールとターゲットグループを作成する
  3. 前もって作成されていたマルチステージング用のECSクラスター内に、2で作成したターゲットグループに所属するECSサービスが作成される

ECSのサービスを作成しておくためにはなんらか実行中となるECSタスクが必要です。そのために今回はNginxのコンテナイメージをECR Publicから取得し、stubタスクと名付けてECSサービス内で実行させています。

この時点で名前解決とリクエストを受け付けるための仕組みが作られており、実際にリクエストを飛ばすとstubタスクのNginx Welcomeページが表示されます。

$ curl https://hoge.通常ステージングドメイン
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

アプリケーションコードをECSにデプロイする

ここまでで、ECSに個人のデプロイ環境を用意するところまでは完了しました。あとは実際に作業したアプリケーションのコードをECSにデプロイすればやりたいことは実現できます。

今回はGitHub Actionsをアプリケーションを管理しているリポジトリに設定して、ecspressoを使ってECSにデプロイする手法を選択しました。

name: Deploy to Amazon ECS

on:
  workflow_dispatch:

env:
  AWS_REGION: ap-northeast-1
  ECR_REPOSITORY: 
  ECS_CLUSTER: 
  ECS_SERVICE: ${{ github.actor }}
  AWS_ROLE_ARN: 

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    environment: production
    # These permissions are needed to interact with GitHub's OIDC Token endpoint.
    permissions:
      id-token: write
      contents: read

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Configure AWS credentials from IAM Role
        uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: ${{ env.AWS_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}

      〜〜〜省略〜〜〜

      - uses: kayac/ecspresso@v1
        with:
          version: v1.7.7

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.actor }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG-nginx -f docker/nginx/Dockerfile .
          // NginxイメージをPush
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG-nginx
          echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG-nginx"
          docker build -t prtimes/story:tmp .
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          // PHPイメージをPush
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
          ecspresso init --region ap-northeast-1 --cluster $ECS_CLUSTER --service $ECS_SERVICE --config config.yaml
          cat .github/workflows/ecs-task-def.json.tpl | sed "s/##IMAGE_TAG##/${IMAGE_TAG}/" | sed "s/##AUTHOR##/${ECS_SERVICE}/" > ecs-task-def.json
          ecspresso deploy --config config.yaml

あとはよしなにespressoのためにECSタスク定義ファイルを書けば自動デプロイが走るようになります。

workflow_dispatchをGitHub Actionsの発火のトリガーにしておけば、毎度毎度mainブランチにマージせずとも手動でworkflowを実行することができて、開発が捗るためおすすめです。

Run workflowを押すことで手動実行できます。

今後やりたいこと

現状の問題点としては、

  1. GitHub IDは一意のため、Aさんが2つの環境を作ることができない
  2. fargate停止の仕組みが確立しておらず、完全に停止するためには前述した連想配列から消したい環境の該当部分を削除してterraform applyする必要がある

などがあります。

1に関しては、サブドメインに自由な値を指定できるようにアプリケーションリポジトリのGitHub Actionsを改修すれば実現可能です。

2に関しては、fargateを停止させるための定時実行Lambdaや停止用のGitHub Actionsを作成すれば可能です。いずれも任意のECSタスクのdesired countを0にするロジックを組めば動くかなと考えています。

これらは今後開発予定ですので進捗がありましたらまたブログの記事で発表します。

最後に

複数のステージング環境は開発・QA両方の観点で必要なものだったので、無事動くものが完成してよかったです。前述したようにまだまだ改善点はありますが、最低限形にはなったかなと思います。これからどんどんこの環境を使って、これまで以上のスピードでPR TIMES STORYをより良いプロダクトにできるように頑張っていきたいです 💪

この記事を書いた人

PR TIMES STORYの開発をしています。

目次
閉じる