開発環境のECSリプレイスとterraformでのコード化

こんにちは、Misoca開発チームの黒曜です。

先日シャニマスの1stライブで会場限定CDを入手したので、作業用BGMにヘビロテしています。
甜花ちゃんのアルストロメリアが特にお気に入りです。

真面目な近況としては、Rails Developer Meetup 2019に登壇するので発表準備をしてます。

railsdm.github.io

以前@lulu-ululが記事にしていたビジュアルリグレッションテストについて、構成やその後などを掘り下げてお話する予定です。
もしご興味があればセッションにお越しください。

tech.misoca.jp

🐳 環境のDocker化

Misocaのローカル開発環境はDockerで立ち上げられるように整備されているのですが、デプロイ先の環境はEC2になっていました。

これでも問題なく運用できていたのですが、以下のような課題がありました。

  • 逐次更新を積み重ねており現状の構成がわからず、新しい環境は既存の環境からスナップショットを取って立ち上げる必要がある
  • 上記に関連し、OS・ミドルウェアのバージョンを手作業で揃えており煩雑
  • レビュー環境はECSで立ち上げており、他の環境と構成に差異がある
  • 一部システムをマイクロサービス的に切り出しており、各環境でそのサービスを立ち上げ連携させていく必要がある

これらの課題をどうしていくか検討した結果、デプロイ先の環境もDockerコンテナを利用してECSに載せ替えることにしました。
合わせて、この構成をterraformで記述してコード管理できるようにし、新しい環境も立ち上げやすいようにしまています。

この記事では、ECS化でどういう構成になったか、terraformをどのように書いたかなどをまとめていきたいと思います。

🏗 大まかな構成

早速ですが、ECS移行後はこんな感じの構成になりました。*1

f:id:kokuyouwind:20190315104620p:plain

MisocaがメインとなるWebサービス、Backendが裏側で動くAPIサービスです。

概ね既存のEC2インスタンスに載っていたサービスをコンテナ化しただけですが、一部構成を変えた点があるため以下で説明していきます。

Cognitoでの認証

もともと、開発環境を外部から利用されないようmod_auth_openidcでの認証をApacheレイヤーでかけていました。

ECSへの載せ替え後は、ALB+Cognitoを使って同等の認証をかけるようにしています。 この設定はAmazon CognitoユーザープールLambdaトリガーでALB認証のメールアドレスを制限するを参考にさせていただきました。

NATゲートウェイでの外部リクエストIP固定化

利用しているAPIの中に、IP単位での許可申請を行う必要があるものがありました。

以前はEC2インスタンスごとにElasticIPを振って申請していたのですが、ECS化を機にNAT Gatewayを利用して外から見えるIPが固定されるように変更しました。

もちろんこの構成はEC2インスタンスでも可能ですが、既存のインスタンスのネットワーク構成は気軽に変更できないため、ECS化と合わせて実施することができました。

DynamoDBをマネージドサービスに変更

切り出したBackendサービスでは、データストアにDynamoDBを使用しています。

既存の環境ではリザーブドキャパシティを最低にしても負荷に対してコストが高かったため、DynamoDBローカルを使用していました。
しかし2018年11月に開催されたRe:InventでDynamoDBの従量課金が利用できるようになったため、ECSに置き換えるのと合わせてバックエンドをマネージドサービスに置き換えました。

これによりECSコンテナではデータを一切保持しない構成になったため、サービスの破棄・再生成が容易になり、Fargateに載せることもできるようになりました。*2

デプロイ時のCodePipelineソースをECRに変更

図中には示していないですが、サービスの更新にはCodePipelineを利用しています。

移行前は以下のような流れでサービスの更新を行っていました。

  1. CodePipelineでソースの変更を検知
  2. CodeBuildで静的ファイルをビルド
  3. CodeDeployでEC2上のサービスを更新

移行後もCodeBuildの処理をDockerイメージの構築に変えれば大体同じ流れにできたのですが、これも2018年11月のRe:InventでAmazon ECRをCodePipelineのソースにできるようになったため、以下のような流れにしました。

  1. CodeBuildがソースの変更を検知し、Dockerイメージを構築
  2. ECRへの変更をCodePipelineが検知し、ECSのサービスを更新

これによりソースコードをS3経由で受け渡す必要がなくなり、S3へのファイル転送にかかっていた時間を短縮することができました。

デプロイ周りについてはMulti Stage Buildを使ったビルド高速化など工夫した点が多いので、また別の記事でご紹介できればと思います。

🔧 terraformでのコード化

最初のECS環境はAWS Webコンソールから試行錯誤しつつ作ったのですが、きちんと可視化して同様構成も作りやすくするため、terraformでのコード化を行いました。

この際、ネットワークなど各環境で共有するものと、ECSサービスなど環境ごとに必要なものとがあったため、以下のように「共有リソース」「各環境で必要なリソース」を分け、それぞれを1つのterraform stateで管理するようにしました。

ディレクトリ構成は以下のようになっています。

+ misoca-infrastructure
  + misoca-dev-common
  + misoca-dev-envs
  + misoca-dev-review

環境ごとに対応するリソースの分類を以下の図に示します。 この図を使いながら、各ディレクトリで管理しているリソースを説明していきます。

f:id:kokuyouwind:20190315105147p:plain

共有リソース(misoca-common)

一番上の点線で囲った箇所が共有の構成で、この部分は単純に terraform import を使ってコードに吸い出しています。

なお、ALBは複数使用するとその分料金がかかるため、1つのALBを複数の環境で共有するようにしました。各リスナールールでドメインに基づいて環境を振り分けるようにしています。

常設環境(misoca-dev-envs)

真ん中と下はそれぞれ1つの常設環境に対応しており、真ん中が通常の環境、下は外部と連携するためにGoogle認証を外す必要がある環境です。

これらは、 misoca-environment モジュールに渡す変数でリソース定義を変えるようにしています。

module "misoca-test" {
  source = "../modules/misoca-environment"
  name = "test"
  domain = "test.misoca.jp"
}

module "misoca-test2" {
  source = "../modules/misoca-environment"
  name = "test2"
  domain = "test2.misoca.jp"
  require-google-auth = false
}

このようにreqire-google-authを渡し、モジュール側では countを使って作成するリソースを切り替えます。

variable "require-google-auth" {
  type = "string"
  default = true
  description = "use goole authentication"
}

// with google authentication
resource "aws_lb_listener_rule" "forward-to-ecs-misoca" {
  count = "${var.require-google-auth ? 1 : 0}"
  // ...
}

// without google authentication
resource "aws_lb_listener_rule" "forward-to-ecs-misoca-without-auth" {
  count = "${var.require-google-auth ? 0 : 1}"
  // ...
}

他のリソースについても環境によって異なる場合は変数に基づいて切り替えるようにしています。 これにより、misoca-environmentモジュールを使えば任意の設定で環境を立ち上げられます。

レビュー環境(misoca-review)

以前の記事で書かれたレビュー環境についてもterraformを使って管理していきたいのですが、レビュー環境はいくつ環境が必要になるか不明で、単純にコードを書いていくと管理が面倒になります。

そこで、以下のようにterraform workspaceを使って環境を立ち上げるようにしました。

module "misoca-review" {
  source = "../modules/misoca-environment"
  name = "review-${terraform.workspace}"
  domain = "${terraform.workspace}.review.misoca.jp"
}

こうしておくことで、kokuyouというworkspaceに切り替えてから適用すると kokuyou.review.misoca.jp の環境を立ち上げる事ができます。

🤔 困っていること

上記のような構成で概ねいい感じに動いているのですが、ECSの立ち上げ環境をFargateにした結果、サーバにsshしてのコマンド実行ができなくなってしまいました。

特に困るのはデータメンテナンスや開発用のrakeタスク実行、手動でのバッチリトライができないことです。 ECSタスクをコマンド上書きして立ち上げることで一応は可能ですが、vpcやセキュリティグループの設定を揃えるのが面倒なうえ、ちょっとした作業でもFargate起動の時間を待たないといけないという点が不便です。

ひとまずは以下のようなECSタスクを手軽に立ち上げるスクリプトを書いて凌いでいます。

ただ結局Fargate起動にかかる時間がかかるのと対話的な操作ができないのは不便です。

RunCommandやSessionManagerが使えなくなるのは結構痛いんですが、みんなどうしてるんでしょうかね…?

🌈 今後の展望

開発環境のDocker化を推し進めたのは良いのですが、今度は本番環境と乖離してしまっているという課題があるため、本番環境も同様の構成へ載せ替えていきたいと思います。

本番ではログや監視などもしっかり考えないといけないので、開発環境でそのあたりの設定を吟味した上で移行していく必要がありそうです。

また、レビュー環境についてはGitHubにブランチをpushしたら自動で立ち上がる、という部分まではまだ実現できていません。

こちらはJenkinsで新規ブランチのpushを元にterraformコマンドを実行させればできそうかな、と考えています。

📢 宣伝

MisocaではInfrastructure as a Codeを実践したいエンジニアを募集しています!

www.wantedly.com

*1:routerがないとかNAT Gatewayの位置がおかしいとか、いろいろ突っ込みどころがあると思いますが見逃してください🙏

*2:Fargateでは永続化ボリュームを利用できないため、永続化データは外部に持つ必要があります