Terraform によるマイクロサービス環境の構築

この記事は #Rettyマイクロサービス強化月間 の第5弾の記事です。

engineer.retty.me

前回は鈴木さんの「予約 API のマイクロサービス化と gRPC ゲートウェイの置き方」でした。

インフラチームの幸田です。
自分の番はまだまだ先だと思っていたら、もう月末で焦って記事を執筆しています。

今回の記事ではマイクロサービス基盤を新しく Terraform で構築し直した話をしようと思います。

マイクロサービス化のはじまり

細かいタイムラインは下記記事に書かれていますが、Retty では2018年頃にマイクロサービス化のプロジェクトが立ち上がりました。

engineer.retty.me

プロジェクト発足時、既存のものとは別で新規 VPC が作成され、その上に ECS クラスタなどが様々なリソースが作成されました。そしてこの段階では全てのリソースが Terraform で作成、管理されていました

システム構成

Retty のマイクロサービスは下記のようなシステム構成になっています。(CDN や WAF などは省略)
簡略化するために図では6個のマイクロサービスのみを記載していますが、実際には20個近いサービスが存在しています。

マイクロサービスのシステム構成

ECS on EC2 だった時代もありましたが、現在は全て Fargate 上で動作しています。一部のマイクロサービスでは Lambda を利用していますが、大半のサービスが Fargate で完結しています。

上図の Microservice Cluster と書かれているものが、各種マイクロサービスが動作する ECS クラスタで、新規マイクロサービスが追加される際には、この Microservice Cluster に対して新しい ECS サービスが足されるようなイメージです。

抱えていた問題

プロジェクト発足当初に作られた Terraform リポジトリは次第にメンテナンスされなくなり、気づけば様々なリソースが手動で足され、実質的にコード管理されていない状態となりました。

理由はいくつか考えられますが、

  • 前任のメンバーからうまく引き継げなかった
  • 急ぎの依頼が多く、手動対応に押されてしまった
  • バージョンアップ作業など、継続的にメンテナンスする体制が整っていなかった
    • Terraform バージョンは v0.11 で止まっていました

などが主な理由ではないかと推測しています。

初期はマイクロサービスの数も少なく、新しいサービスが増える頻度も遅かったため、手動管理でもなんとかなっていましたが、マイクロサービス化が進むにつれ、いくつかの問題が出てきました。

マイクロサービス間での設定差異や変更漏れの発生

そのままですが、サービスが増えるにつれてマイクロサービス間の設定がちぐはぐになってきました。具体的には下記のようなものです。

  • オートスケールの設定の有無
  • モニタリングの過不足
  • 各種リソースの命名のばらつき

命名がバラつくのも良くないですが、スケーリングの設定やモニタリングの過不足に関してはサービスの信頼性に関わってくる部分なので、特に問題視していました。

新規サービスの追加に時間がかかる

前述の通り既存の Terraform コードはメンテナンスされていないため、すべて手動でリソースを作成していました。必要なものはサービスによって様々ですが、新規にマイクロサービスを追加するとなれば、下記のようなリソースを作成する必要があります。

  • ECS 関連のリソース
    • ECS Service
    • Security Group
    • ECR Repository
  • (使用するサービスの場合) ALB
    • Target Group
    • Security Group
  • CloudWatch Alarm
  • デプロイ用 IAM Role

これらを開発、ステージング、本番などそれぞれの環境に作成する必要があるため微妙に設定が異なったり、命名が異なるなどの設定差異が発生する原因にもなっていました。

作業に対するレビューができない

これはコード化されていないことによって発生していた課題ですが、手動でリソースを作成、変更するため作業に対するレビュー体制が整っていませんでした。

Linter などによる機械的なレビューはもちろん、レビュアーによる人的な確認もないため1つ目に挙げた「マイクロサービス間での設定差異や変更漏れの発生」に繋がっていました。

課題解決に向けて

上記のような課題は設定をコード化することによって、ある程度解消する見込みがあったため Terraform を利用して、コード化することに決めました。

IaC ツールの選定

コード化するための手段はいくつかありますが、下記のような点から Terraform を採用しました。

  • 社内の他プロジェクトでの導入実績があり知見が溜まっていた
  • エコシステムが充実している

特に2つ目については、Terraform Provider の仕組みによって AWS のみならず、各種 SaaS など数多くのサービスに対応している点や、awesome-terraform に書かれているような様々なツールが利用できる点が個人的には他の IaC ツールに比べて優れている点だと思っています。

Terraform 化の方法について

Terraform では terraform import コマンドを利用することで、既存のリソースを Terraform に取り込むことができますが、今回は既存のコードを一切使用せず、新しいコードで VPC から作り直すことにしました。

理由としては、現行の VPC の構成があまり望ましい形ではなかったこと、既に作成されたリソースの命名規則がバラバラで共通化が難しくなりそうなどが主なポイントです。
特に今回は module を利用して共通箇所をまとめたいと考えていたので、全てのリソースを新規作成し、徐々に新しい環境に移すことを選択しました。

工夫したポイント

Terraform で新しくマイクロサービス基盤を作るにあたって工夫したポイントを紹介します。

Terraform の module の構成について

最初に書いたように、マイクロサービス間の様々な設定や命名がちぐはぐになっているという課題がありました。これを解決するために、マイクロサービス追加にあたって必要なコンポーネント単位で Terraform module を作成することにしました。

作成した module は下記の通りです。

ECS Service module

このモジュールは ECS Service とそれに関連するリソースを作成するためのものです。マイクロサービスが追加される際には基本的にこのモジュールを利用します。

作成される主なリソースは下記の通りです。

  • ECS Service
  • ECS Task Definition
  • ECR Repository
  • CloudWatch Alarm
  • IAM
    • ECS Task Role
    • ECS Task Execution Role
    • CircleCI デプロイ用 IAM Role
  • AWS Cloud Map Service

実際のコードは下記のようになります。必要なパラメータを埋めることで ECS Service を追加することができます。

module "example_service" {
  source = "../../modules/ecs-service/"

  env             = var.env
  aws_account_id  = local.aws_account_id
  circleci_org_id = var.circleci_org_id
  vpc_id          = module.dev.vpc_id
  vpc_subnet_ids  = module.dev.vpc_subnet_ids
  vpc_cidr_blocks = var.vpc_cidr_blocks
  s3_bucket_names = module.dev.s3_bucket_names

  cloudmap_namespace_id = module.dev.cloudmap_namespace_id

  ecs_cluster_name = module.dev.ecs_cluster_names["microservices"]
  service_name = {
    full = "example-service"
  }
  desired_count = 1

  circleci_project_id = "example-b123-45ef-abd6-d789d12d3456"
}

ALB module

このモジュールは ALB とそれに関連するモニタリングのリソースを作成するためのものです。
作成される主なリソースは下記の通りです。基本的には ALB だけでシンプルです。

  • ALB
    • Target Group
    • Listener Rule
  • CloudWatch Alarm

大半のマイクロサービスは AWS Cloud Map を利用したサービスディスカバリによって他のサービスから参照されますが、一部 ALB を使用するものが存在します。
具体的には構成図に書かれているように、ユーザアイコンを管理するための user-icon-service や、OGP 画像を管理する open-graph-image-service などです。これらは直接インターネットからアクセスされるタイプのマイクロサービスであるため前段に ALB が設けられています。

このモジュールで作成されるリソースが少ないため、ECS Service module に組み込むことも考えましたが、分岐やパラメータが多くなることから独立させました。

Lambda module

このモジュールは Lambda 関連のリソースを作成するためのものです。現段階では open-graph-image-service のレンダリング処理を Lambda で行っているため、そこで利用されています。

  • Lambda
  • ECR Repository
  • IAM
    • Lambda 実行 Role
    • CircleCI デプロイ用 IAM Role

CI module

このモジュールは各マイクロサービスのリポジトリに紐づく CircleCI のプロジェクトに対して、サービスデプロイ用の IAM Role の ARN など必要な環境変数を設定するためのものです。設定する数が多いのと、マイクロサービス間でなるべく同じような CI の設定ファイルが利用できるように、環境変数についても Terraform で管理することにしました。

具体的には下記のような環境変数を Terraform から設定しています。

Project Settings > Environment Vairbales

Terraform Provider には mrolla/terraform-provider-circleci を利用しています。

CI/CD について

移行作業や新規リソースの追加作業はチームで行い、今後もなるべく属人化せずに変更を行えることを意識して Terraform の CI/CD をセットアップしました。CI には GitHub Actions を使用しています。

tflint によるルールの強制

利用している方も多いと思いますが、tflint は Terraform コードのための Linter で、予め設定したルールを強制することができます。

github.com

Terraform 本体に対するルールや、AWS など各種 Provider 向けのルールが用意されており、これらを利用することができます。

特に terraform_naming_convention正規表現を利用してリソース名の命名規則を強制できるため、複数人での開発にはオススメです。

terraform-docs によるドキュメント自動生成

terraform-docs は module のドキュメントを自動生成してくれるツールです。公式のページでも利用されています。

github.com

下記のように modules/ の各ディレクトリの中に README.md を配置し、Terraform のコードを修正すると、README が自動的に更新されるようにしています。

├── modules
│   ├── alb
│   │   ├── README.md
│   │   ├── examples
│   │   │   └── main.tf
│   │   ├── ...
│   ├── ci
│   │   ├── README.md
│   │   ├── examples
│   │   │   └── main.tf
│   │   ├── circleci.tf
│   │   ├── ...
│   ├── ecs-service
│   │   ├── README.md
│   │   ├── examples
│   │   │   └── main.tf
│   │   ├── ...
│   └── lambda
│       ├── README.md
│       ├── examples
│       │   └── main.tf
│       ├── ...

更新の自動化は用意されている Action を利用すると簡単に実現できます。

github.com

tfcmt による Pull Request レビュー

tfcmt は terraform planterraform apply の結果を Pull Request にコメントしてくれる便利ツールです。

github.com

Retty では Terraform のブランチ戦略に GitHub Flow を採用しており、デフォルトブランチから新規ブランチを作成し、Pull Request を作成すると terraform plan が実行され、マージすると terraform apply が実行されるようにしています。

レビューをしてもらう上で terraform plan の結果は大事な情報ですが、このツールを利用することでイイ感じにパースされ、RP で見やすくコメントしてくれます。

set-terraform-matrix を利用した Terraform の実行

set-terraform-matrix は Pull Request に付与したラベルに応じて、動的に GitHub Actions の matrix job を組み立てるための Action です。ありそうで無かったので作ってみました。

github.com

Terraform のステートファイルは、開発、ステージング、本番で分割しており、それぞれのディレクトリで terraform planterraform apply を行います。
通常 CI から実行する際には、全てのディレクトリに対して同じ処理が行われますが、場合によっては特定のディレクトリでのみ terraform planterraform apply を実行したいケースがあるかと思います。

set-terraform-matrix では、各環境 (develop/staging/production) と実行対象のディレクトリを下記のような設定ファイルとして定義しておくことで、Pull Request に付与されたラベルに応じて動的に terraform コマンドを実行するディレクトリを選択することができます。

{
    "develop": [
        "envs/dev"
    ],
    "staging": [
        "envs/stg"
    ],
    "production": [
        "envs/prod"
    ]
}

例えば target:develop ラベルを Pull Request に付与すると、/envs/dev ディレクトリに対してのみ terraform plan & terraform apply が実行されるといった感じです。

利用方法やその他の情報は README.md に記載しています。

tfmigrate を利用したステート操作

tfmigrate は terraform state mvterraform import などのいわゆるステートファイルの操作を CI から行えるようにするためのものです。

github.com

Terraform v1.1.0 から moved block が利用可能になりましたが、tfmigrate では importstate rm にも対応しており、複数人で Terraform リポジトリを触る場合にステート操作についても Pull Request ベースでレビューすることができ大変便利です。

また tfmigrate には tfmigrate plan というコマンドが用意されているため、これを利用することで通常の terraform plan と同じ感覚で Pull Request ベースのレビューを行うことができます。
tfmigrate 用のワークフローも設定しており、特定のラベルを付与すると tfmigrate plan で plan 結果が Pull Request にコメントされ、マージ後に tfmigrate apply を実行するようにしています。

Renovate による継続的なアップデート

Terraform 本体や Provider に加えて、上記の tfcmt や tfmigrate などの CI ツールのバージョンアップ作業は Renovate によって自動化しています。

以前はバージョンアップ作業が行われず、気づけば手が入れられない程に新しいバージョンに取り残されることがありましたが、それらの反省を踏まえて今では常に新しいバージョンを利用するようにしています。*1

一部構成が変わっていますが、以前に Renovate で Terraform Provider のバージョンアップを自動化する記事を書いたのでこちらも併せてご覧ください。

engineer.retty.me

OIDC を利用した認証

GitHub Actions では2021年10月27日に OpenID Connect を利用した認証が公式にサポートされました。

github.blog

AWS でいうところの IAM User の AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY といった認証情報を渡さずとも、CI から各クラウドプロバイダへの認証が行えるものです。
Terraform の CI には比較的強めの権限を与える必要があるため、この仕組みはすぐに導入しました。

AWS が出している aws-actions/configure-aws-credentials を使用すると簡単に利用することができます。

またアプリケーション側の CI の話になりますが、最近 CircleCI でも同様のものがサポートされ、同じように IAM User の認証情報なしで AWS の操作が可能になりました。

circleci.com

新しいマイクロサービス基盤の移行するタイミングで、アプリケーション側の CI も見直し、デプロイ用に作成してた IAM User を順次削除し、OIDC 認証に置き換えています。

CircleCI も Orb である circleci/aws-cli によってこの仕組みを簡単に利用することができます。

おわりに

全く整備されていなかった Terraform のコードベースですが、現在では特定メンバーだけでなく、複数人のメンバーによって新しいコードや修正がコミットされています。

また今までになかった成果として、インフラチームだけでなく、開発のメンバーが Pull Request を作成し、サービスを追加するといった動きも見られました。今まではインフラ部分の開発、修正依頼はインフラメンバーのみが担当していたので大きな成果だと感じています。

まだまだやりたいことはたくさんあるので、興味を持った方はぜひ一緒にやりましょう!

hrmos.co

*1:メジャーバージョンアップなど大きなアップデートは少し様子を見てから導入しています