僕たちとECSとデプロイとその改善

みなさんこんにちは。Backlog課のGitチームに所属するテリーです。今回は僕たちが日々運用するECS Fargate上のアプリケーションのデプロイ方法とそれをどう改善してきたかについてまとめました。デプロイについて迷っている方や日々の業務の改善が好きな人に読んでもらえたらと思います。

Git機能におけるデプロイ

ECS上で動くアプリケーション

BacklogのGitはEC2上で動くアプリケーションとECS Fargate上で動くアプリケーションで構成されています。

アーキテクチャの外観は上記のようなになっています。Gitのアーキテクチャについて過去に詳しく説明したブログを発表していますので詳しくはこちらを参照ください。

僕たちは上記のアーキテクチャ中の4つのアプリケーションをECS Fargate上で運用しています。また、内部的な話になりますがBacklogには内部構造として複数の本番環境があり4 * 環境分の個数のアプリケーションを本番環境にデプロイしていく必要があります。

 

ECSのデプロイ方法

Fargate上で動くアプリケーションのいくつかはAWS CoeDeploy (以下、CodeDeploy)を使用してBlue/Greenデプロイをしています。アプリケーションによってBlue/Greenデプロイとローリングデプロイを使い分けています。

デプロイの定義

デプロイの定義も整理します。この記事のAmazon ECS(以下、ECS)へのデプロイは新たにタスクを立ち上げること、サービスの更新、タスク定義の追加、CodeDeployの設定の更新、CodeDeployのデプロイメントの作成を行うことを示しています。コンテナイメージの作成やECRへの登録は含めません。

 

さてここまでが前置きです。これからは今までの僕たちのデプロイ方法の変遷をもとに、何を考えて改善を行ったのかを振り返ります。

デプロイ方法の変遷

1.Terraformによる手動デプロイ

環境の構築

ECSとCodedeployはTerraformによって構築されており、当初はアプリケーションのデプロイに関するタスク定義などもTerraformを利用してデプロイしていました。

BacklogはAWSのインフラを構築するのにTerraformを使っており、既存の環境との整合性を保つためにはTerraformを使用するのが最適でした。またTerraformのworkspacesを使用してネットワークの構成や複数の異なる環境へ同じようなECSクラスタ/サービスを一気にどかっと立ち上げることができました。

継続的にデプロイをするために

環境の構築が落ち着きアプリケーションの修正を継続的にデプロイしたくなってきました。がしかしアプリケーションの継続的にデプロイをTerraformに任せ続けることは考えませんでした。アプリケーションのデプロイのたびにTerraform を適用をするのは少し足回りが重く感じられたからです。Terraformに関するリポジトリはBacklogチーム内で集約されており、ECSの構築以外にもさまざまなインフラ基盤がIaCとして管理されています。アプリケーションに関する変更とECSの基盤に対する変更がバッティングする可能性などを考えると、アプリケーションのデプロイ固有の設定の変更はTerraformに任せるのではなく専用のデプロイツールがある方が良いように思われました。

デプロイ設定をアプリケーションと同じリポジトリに

アプリケーションのソースコードと同じリポジトリにデプロイの設定も集約できたら開発者の認知的負荷も下がり良いのではないかという議論にもなり、新たなデプロイ方法を考える必要がありました。

改善できたこと

  • 環境構築とデプロイを実現できた

残った課題

  • tfファイルを管理する共通リポジトリだとチーム外とも共有されるため継続的デプロイを実現するには足回りが重かった
  • デプロイ設定をアプリケーションと同じリポジトリにしたかった
  • Jenkinsジョブの整備、継続的にデプロイできる状況を作る必要性があった

2.Jenkinsによるデプロイ

新たにデプロイの方法を考える必要があり、継続的デプロイを実現するために以下のことをやりました。

  • デプロイ設定をアプリケーションと同じリポジトリに
  • デプロイ用のスクリプトの開発
  • デプロイ用のJenkinsジョブの作成

デプロイ設定をアプリケーションと同じリポジトリに

アプリケーションのデプロイには以下のような情報の管理が必要です。

  • AWS region名
  • Backlogの本番環境名
  • 起動タスク数
  • アプリケーション固有の設定
  • コンテナイメージのバージョン
  • cpu/memoryの値
  • CodeDeployの設定

そこで上記のような情報をアプリケーションと同じリポジトリに保持して、コンテナイメージのバージョンはJenkinsジョブから渡されるパラメータによって設定しました。

ディレクトリ構成

上記で何度か言及している通りBacklogにはn個の環境があり各環境ごとにデプロイをさせる必要があります。共通で設定する項目も多いのですがシンプルにするため設定の共通化などはせずに、複数のディレクトリを作成し対象のディレクトリに移動し後述のecs-toolを実行しデプロイすることにしました。

以下のようなイメージです。

.
├── env-1
│   ├── appspec.yaml
│   └── task_def.json
├── env-2
│   ├── appspec.yaml
│   └── task_def.json
├── env-3
│   ├── appspec.yaml
│   └── task_def.json

デプロイ用のスクリプトの開発 

すでにECSのデプロイツールは世の中に多くあり、それらを使うことを考えましたが少し調べたところ自分達の取り巻く状況をうまく満たしてくれるツールを探し切ることができませんでした(CodeDeployとの連携など)。またAWS CLIで比較的簡単にデプロイを行うことができたので、AWS CLIを組み合わせてシンプルなデプロイ用のスクリプトを作成することにしました。

そこでできたのがecs-toolと呼ばれるシェルスクリプトです。高度なことは何もやってくれませんが、AWS CLIを組み合わせてCodeDeployを用いたデプロイとローリングデプロイをしてくれます。

前述の通りJenkinsジョブ上からこれを実行させています。ecs-toolに関してはOSSにしたいと考えていますが、個人的にまだリファクタリングをしたかったり、ドキュメントを作成できておらず公開できる状態にはありません。

改善できたこと

  • 開発者のローカルマシンでなくJenkins上でのデプロイを実現できた
  • デプロイ設定をチームだけが触るリポジトリに移動でき変更をしやすくなった

残された課題

  • アプリケーションが4つあるため全てをデプロイするには4回Jenkinsの画面にアクセスする必要があり面倒だった
  • アプリケーションのリポジトリは4つに分かれており各リポジトリにスクリプトを用意しているため変更する際に効率が悪かった
  • 良さそうに思えたアプリケーションのソースコードとデプロイ設定を同じリポジトリで管理する方法だが、アプリケーションの停止などの操作のたびにコミットが必要となりソースコードの修正とは関係のないコミットが開発者としてはノイズとなった

3.ChatOps(チャットからJenkinsジョブを実行)

毎回Jenkinsの画面に遷移するわずわらしさ

実際に運用していると、デプロイのたびにJenkinsの画面にアクセスするのが思った以上に面倒でした。チーム内でも、いつも使ってるTypetalkからJenkinsジョブを実行できたら楽なのではないかといった意見が出たのですぐにChatBotを作成して対象のJenkinsジョブを実行できるようにしました。またデプロイの進捗もTypetalk上で確認できたら楽だったので通知もTypetalkに集約させました。

デプロイ&通知Botの作成

Typetalkでは簡単にBotの作成と投稿ができます。また他のチームがすでに使っているBotに相乗りして利用したので各種Botの作成には時間はかかりませんでした。

改善できたこと

  • チャットからデプロイができるようになったことで、Jenkinsの特定のページに遷移する必要がなくなり心理的に楽になった
  • 同時にデプロイ後の確認用のスモークテストの実行を自動でできるように整備したため一つ手数を減らせた

残された課題

  • Jenkins画面に移動するのがチャットのメンションに変わっただけで本質的なめんどくささは変わらなかった
  • 設定の変更はGitの情報の方も変えなければならず毎度Git push -> レビュー -> マージ -> チャット上でコマンド実行と手数が多かった
  • 複数のアプリケーションを同時にリリースするには複数リポジトリでファイルの変更をしてgit pushして回る必要があり依然難があった

4.GitOps(GitのWebhookからJenkinsジョブを実行)

デプロイ設定を一つのリポジトリに

ChatOpsでの非効率な部分も踏まえ、Gitの操作だけでなんとかデプロイを完結できないかとチームで考え始めました。チーム内でアプリケーションの情報を一つのデプロイを担当するリポジトリに集約してGitのmainブランチへの変更をそのまま適用すれば楽なのではという議論になり、4つのリポジトリにあるデプロイ設定を一つの新たなリポジトリで管理するようにしました。

メリットとして

  • 複数のアプリケーションを容易に同時にデプロイできることにつながる
  • アプリケーションのコミットログを汚さないで済む。

などが考えられました

リポジトリのディレクトリ構成の変更

リポジトリのディレクトリ構成は以下のようにしました。deploymentsの下に各アプリケーションのディレクトリを用意し、その下に各環境ごとの設定を配置しています。

.
├── deployments
│   ├── git-http
│   │   ├── env-1
│   │   ├── env-2
│   ├── git-proxy
│   ├── git-replication-worker
│   └── git-ssh

Jenkinsファイルの書き方

例えばgit-proxyに変更を加えたのにgit-httpも再デプロイとなると、ユーザー影響がないとはいえ効率が悪いです。git-proxyのenv-1に変更があったらgit-proxyのenv-1にだけデプロイしたいところです。

あまりJenkinsファイルに詳しくなかったのですが調べたところwhenを使って設定すれば変更分を検知して最適な関数を実行できることがわかりました。並列実行もできますが、デプロイスクリプトの処理一つ一つはすぐ終わるため直列実行にしました。

catchError は失敗した後も後続のStageを実行するために使用しています。stateResultFAILUREとすることでStageの失敗には後々Jenkinsの画面上で気づけるように設定しています。

pipeline {
    agent {
        label 'any'
    }
    stages {
        stage('git-http deploy') {
            when {
                changeset 'deployments/git-http/**/*'
            steps {
                catchError(stageResult:'FAILURE') {
                    deploy("git-http")
                }
            }
        }
        stage("git-ssh deploy") {
            when {
                changeset 'deployments/git-ssh/**/*'
            }
            steps {
                catchError(stageResult:'FAILURE') {
                    deploy("git-ssh")
                }
            }
        }
    }
}

さらにJenkinsのグローバル変数であるcurrentbuild.changeSetsの中身を見れば、Backlogの環境毎のディレクトリに変更を加えたのかを検知できるため、以下のような関数を用意して変更のあった環境だけを抜き出すことにしました。ディレクトリの階層が固定されているためロジックは単純です。

def collectEnvNames(appName, changeLogSets) {
   def envNames = []
    for (int i = 0; i < changeLogSets.size(); i++) {
        def entries = changeLogSets[i].items
        for (int j = 0; j < entries.length; j++) {
            def entry = entries[j]
            def files = new ArrayList(entry.affectedFiles)
            for (int k = 0; k < files.size(); k++) {
                def file = files[k]
                String[] paths = file.path.split("/") 
                if(paths.length > 3) {
                    if(appName == paths[1]) {
                        envNames.push(paths[2])
                    }
                }
            }
    return envNames.unique()
}

これで対象の環境、アプリケーションのみをデプロイする準備が整いました。あとは抜き出した情報を元に対象のディレクトリに移動してデプロイスクリプト(ecs-tool)を実行するだけです。

改善できたこと

  • アプリケーションの設定のプルリクエストの作成、マージ、デプロイまでがスムーズに進むようになったのでデプロイの手数は大幅に減少
  • 複数アプリケーションを簡単に同時にリリースすることができた

残された課題

  • JenkinsとBacklogは別システムなのでたまにGitのpushの通知に失敗する。その際は手動でJenkinsのjobを実行できるようになっているが不便な面も
  • 設定に変更のないただの再起動もJenkinsジョブの手動実行をしなければならない

その他の改善

スモークテストの自動実行

デプロイした後の最低限の動作確認用にスモークテストを用意しています。Gitチームが管理するアプリケーションに対して複数のスモークテストを実行させています。今まではデプロイした後に一つ一つ対応するスモークテストを手動で実施していました。これはデプロイの安心度を高めてくれていました。ただスモークテストはJenkinsジョブから実行されるようになっていて、毎回手動で実行するのがめんどくさいと感じていました。

今回のデプロイ周りの改善に合わせてアプリケーションのデプロイ直後に自動でスモークテストを実行させるようしました。

おかげでスモークテストの実行し忘れを防止できデプロイが完了されるまでJenkinsジョブの実行画面で待つというような無駄なリードタイムも削減することができました。

ChatBotへのデプロイ以外の機能追加

ecs-toolの本質はBacklogの各本番環境に向けてAWS CLIで簡単に操作できることだったのでこの仕組みを応用して実行中の特定の環境のコンテナのタスク定義をChatBotを利用して取得できるようにしました。

この機能のおかげで、サポートチームが開発チームに問い合わせなくてもチャットを使うだけでアプリケーションの状態を取得することができるようになりました。

ChatBot用のトピックの作成

CodeDeployの通知、Jenkinsジョブからの通知、人間のコマンド入力とその応答をチャットに集約した結果チームのコミュニケーションに必要でない情報が溢れるようになってきました。そのため自分たちのチームのコミュニケーション用のトピック(Slackで言うチャンネル)とChatBot用のトピックを分けて運用するようにしました。

まとめ

以上が今回の話になります。インフラから運用まで全部に責任をもつGitチームでは運用の改善はかなり大事な要素の一つです。デプロイや運用の改善はまだまだ道半ばです。今後もデプロイのスムーズ化、運用の自動化を通じてユーザーの皆様により本質的な機能をお届けできるようチーム一丸となって頑張っていきます。

より良いチームワークを生み出す

チームの創造力を高めるコラボレーションツール

製品をみる