この記事はクラウドワークス アドベントカレンダー6日目の記事です。 前日の記事は@bugfireのgithub-script は便利でした。GitHub Actionsでのちょっとした作業が捗りますね!
SREチームの@kangaechuです。 気がつくと入社から2年が経ちました。2年前のAdvent Calendarでは ぴよぴよSREという記事を書くくらい何もわかっていませんでしたが、ようやく自分なりに動けるようになってきました。 この記事ではcrowdworks.jpのSREチームで、この2年間でどのようなことをやっていたのかを振り返ります。 SREチームの範囲は幅広く、いろいろなことをやっていました。今回はDocker化とTerraformの2つの取り組みについてご紹介します。
なんで1年じゃなく2年かって?去年はaws-vault についてのあれこれを書いたからだよ。
Docker化
crowdworks.jp では、開発にDockerを2016年頃から使用し始め、2019年に本番環境のDocker化が完了しました。私が入社した2018年からは、crowdworks.jp アプリケーションサーバ、リバースプロキシとfluentdのDocker化を実施しました。
入社時のシステム構成
2020年11月時点のシステム構成
アプリケーションサーバ
変更前の課題
入社当時、クラウドワークスの開発環境や周辺サービスではDocker化が進んでいたものの、crowdworks.jp のアプリケーションサーバはDocker化されていませんでした。RubyのバージョンやAMIの更新などがある場合、インフラデプロイと呼ばれるBlue-Greenデプロイが必要でした。EC2にChefでプロビジョニングを行い、アプリケーションのデプロイをCapistranoで実行するというものです。 CrowdWorksデプロイ物語 に詳しくあります。
ただ、その構成にはいくつかの課題がありました。
RubyやLinuxのパッケージのバージョンを更新する場合、AMIの作成を含む複雑な作業をする必要がありました。また、ちょっとした変更をするだけ、少しスケールアウトしたいだけなのにプロビジョニングに何十分も待たされるのは嬉しくありません。またChefの設定ファイルやデプロイ方法が複雑で、積極的にさわりたくない問題も発生していました。 そうですね。コンテナに載せればみんなハッピーです。
対応
ただ、その道のりは簡単ではありませんでした。コンテナ化するためには状態を持ってはいけません。いつコンテナが死んでも別のコンテナが仕事を引き継ぐ必要があります。Postfixをなくすため、crowdworks.jp自体のコードに手を入れたり、デプロイの方法をCapistranoからCircleCIに移す必要が出てきました。コンテナフレンドリーではなかったRailsアプリケーションをDocker(ECS)に移行するまでの戦いに @minamijoyo が苦闘した記録が残されています。 長い戦いの末、CrowdWorks本体のDocker化が完了しました。やったね!
リバースプロキシ
変更前の課題
crowdworks.jpのリクエストはリバースプロキシと呼ばれるサーバが一度受けとり、ヘッダ情報などをもとにアプリケーションサーバを含む後続のサーバに渡しています。こちらもいくつかの課題がありました。
- 設定に対するテストがないので、心理的安全がない
- プロビジョニング(特にChef)が複雑で理解しづらい
リバースプロキシはnginxを使用していますが、今までは設定に対するテストがありませんでした。そのため、バージョンアップや設定変更の際にはデプロイ後に本番環境で失敗を検知する、いわゆるお祈りデプロイが必要でした。 また、Chefの設定もわかりづらく、デプロイ手順も複雑でした。
対応
Docker化のタイミングに伴い、テストをしっかり書くことにより、安心して設定変更を行えるようになりました。また、デプロイもmasterブランチにマージするだけで、あとは自動でデプロイが走るようにし、デプロイに関する手作業が不要になりました。スケールアウトもし放題です。いいことしかないですね。
テストはE2Eテストとしました。clientコンテナはHTTPのクライアントとして、Golangで記述されたテストコードを実行します。nginxコンテナはテスト対象のnginx.confを設定したnginxを起動し、バックエンドに配置したGolangで作成した複数のモックのHTTPサーバにリクエストを振り分けます。
エラーが返るようなテストの場合は、clientコンテナからのリクエスト時に特別なヘッダをセットし、モックのHTTPサーバがそのヘッダに対して特定のレスポンスをすることで実現しています。 テスト時はこれらのコンテナ群をdocker-composeで起動することで、どの環境でもテストができるようにしています。
fluentd
課題
crowdworks.jpや周辺サーバのログはfluentdに集められ、それをS3に保存していました。これにもいくつかの課題がありました。
- fluentdのバージョンが0.12系で古い
- ログの検索がgrepでつらい
- テストがない
- fluentdがSPOF
- Chefで構築されている
fluentdは長年大事に使われていたため、バージョンが0.12で止まっていました。最新は1.11なので、約10倍のジャンプアップが必要になります。単純にバージョンアップすることすら苦難の道です。 また、ログの検索はfluentdサーバやS3に保存されたログをgrepする必要がありました。対象のログを探索するためにはsed/awk/perlを使ったワンライナーの腕が試される、90年代を彷彿とさせる作りとなっていました。 また、fluentdがSPOFとなっており、fluentdのサーバが止まると各サービスがログを送信できなくなり、サービスが死ぬというドキドキが止まらない作りになっていました。
対応
そのため、こちらもDocker化することにしました。 ログ集約にはFluent Bitを選定しました。構成としてはNLBを前段に置いて、Fargateに配置したFluent Bitのコンテナでログを集め、Firehose経由でログをS3に保存します。ログはAthenaでSQLライクに検索できるようにしたのでログの検索も簡単になりました。リバースプロキシと同様にテストも追加し、デプロイもmasterブランチにマージするだけで自動でデプロイが完了するつくりとなっています。
こちらは今度別記事で詳細を書いてみたいです。
Terraform
CrowdWorksのSREチームは Terraform関連の様々なツールを生み出し続けるTerraform職人の@minamijoyo と、実践Terraform を書いた@tmknomを擁するTerraformつよつよ勢が集まっていることで知られています。
課題
crowdworks.jp は2012年にサービス開始し、その翌年の2013年にAWSへの移行を行いました。Terraformによる構成管理を開始したのは2015年でした。 当初はVPC単位でtfstate(Terraformの状態管理ファイル)を管理し、IAMやRoute53はアカウントごとにtfstateを管理していました。crowdworks.jpのAWSアカウントは歴史ある環境のため、1つのVPCにRailsアプリケーション・リバースプロキシ・fluentdなどの多数のアプリケーションが一緒くたに定義されている様子を想像いただければ、その複雑さがわかるかと思います。その後もさまざまなリソースの追加や、Terraformにインポートする対象を増やしていき、複雑さはどんどん増していきました。それにより以下の課題を解決する必要が出てきました。
- planやapplyに時間がかかる
- 経験の浅いメンバーの理解に時間がかかる
- プルリクエストにterraform planの結果を手動で貼るのつらい
- Terraformやプロバイダーのバージョンアップに追従しづらい
- AWS マネジメントコンソールでの手修正に気付きづらい
対応
tfstateの分割
planやapplyの時間を短縮し、可読性を向上するため、ディレクトリをVPC単位からアプリケーションのリソース単位に分割することにしました。
services ├── crowdworks │ ├── staging │ └── production └── management ├── staging └── production
変更後のディレクトリ構成(アプリケーションのリソース単位)
services ├── crowdworks │ ├── application │ │ └── rails │ │ ├── datastore │ │ │ ├── staging │ │ │ └── production │ │ └── compute │ │ ├── staging │ │ └── production │ ├── base │ │ └── network │ │ ├── production │ │ └── staging (省略)
1つのtfstateで管理する対象を少なくすることにより、管理対象が明確化し、可読性が向上しました。また、planやapplyにかかる時間を短縮することができました。
plan 自動化
plan結果をプルリクエストのコメントに貼り付けるため、tfnotifyを使用しています。
これにより、常にコードと同期した状態でのplan結果を確認することができるようになりました。
最新バージョンへの追従
Terraformやプロバイダのバージョンアップには@minamijoyoのtfupdateを使用しています。
これにより、Terraformやプロバイダのバージョンアップの際にはGitHubのリポジトリにプルリクエストが作成されます。アップデートの内容が問題なく、plan結果に差分がないことを確認してマージするだけでアップデートが完了できます。tfstateの数が多くなり、管理するディレクトリごとにバージョンをpinしているので、アップデートの自動化はかなり便利です。
マネジメントコンソールでの修正に気づく
毎朝全てのtfstateを持つディレクトリにplanを実行し、plan差分がないかを確認しています。 これにより、手での修正に気がついたり、merge後のapplyを忘れた時に気がつくことができるようになりました。 実行しているスクリプトはtfupdateでTerraform本体/プロバイダ/モジュールのバージョンアップを自動化する - 実運用するための工夫 にあります。 applyも自動化したくなります。
tfmigrate
tfstateの分割にはterraform stateコマンドを使用しますが、terraform stateコマンドは変更を管理することができません。しかし、tfstateの変更を伴う構成変更時であっても、設定変更をコミットし、プルリクエストによるレビューをするというプロセスに乗りたいですね。 それを解消するために@minamijoyoが作成したのがtfmigrateになります。
tfmigrateはtfstateの変更を記述したマイグレーションファイルを作成することで、通常のTerraformと同じようにplan、applyが可能となります。
tfmigrateはディレクトリ構成の変更時に非常に役に立ちました。間違ったら最悪リソースが消し飛びかねないtfstateの変更は神経を使う作業です。それをテストやレビューを行いながら、心理的安全を持った状態で作業できました。
また、tfmigrate 0.2.0では履歴管理機能を備えたので、どこまで変更を適用したかを管理することができます。tfmigrateも自動化できそうですね。
まとめ
CrowdWorksのSREチームがこの2年で行っていたことは数多くありますが、今回はDocker化とTerraformの変更に絞って紹介しました。 これらの変更により、今までよりもシンプルで変更しやすい環境・構成を推進できたと考えています。 ただ、まだまだ改善したい点、改善すべき点がたくさんある状態です。また、サービスの成長やAWSの新機能などの外部要因により、修正すべき点が増えていくのは間違いありません。 安定したサービスを持続的に提供しながらも、より開発しやすく効率の良い仕組みを作っていきたいなーと思っています。