Advertising-Flowのインフラ構築について #2

はじめに

SRE (Service Reliability Engineering) 部の村上です!

前回の記事では、Advertising-Flow(以下A-Flowと呼びます)のご紹介とリアーキテクチャまでの流れ、 そして最初に取り組んだことの1つとして「CI/CDの構築」について説明させていただきました。

今回は続きとして、リアーキテクチャにおいてまずやったこと2点のうちのもう片方である「ディレクトリ構成の決定」と、その前提として、Terraformの機能であるModuleについてお話したいと思います。

前提: Moduleの活用について

Terraformには、Moduleと呼ばれる、まとまりを作るための機能があります。

Moduleは、引数 variable を与え、それに従ってresourceを作成することができます。 また、Moduleの出力 output を定義し、Module外で活用することができます。
また、定義したModuleを複数個インスタンス化することもできます。

Moduleの活用は様々あり、例えば、リソース種別(EC2、IAM、S3、etc…)ごとにまとめるといった戦略でModuleを活用しているケースをweb上でよく見かけます。

一方、A-Flowのリアーキテクチャでのインフラ構築においては、Moduleを「機能のまとまり」として活用することにしました。
これは、以下のモチベーションからです:

  • あるスコープで必要なリソースを閉じて実装・管理が可能
  • 命名の半自動化が可能

それぞれについて説明していきます。

メリット: あるスコープで必要なリソースを閉じて実装・管理が可能

まず、1点目が「ある機能を構成するために必要なリソースをModule内に閉じることができる」ということです。

例えば、AWS Lambdaにおいて、関数を1つ作成し動作させるために必要なリソースは何でしょうか?
このとき、Moduleに閉じて実装しておけば、後でそのModuleを見れば必要なリソースが分かります。 実際、見てみると以下であることが分かります:

  • aws_lambda_function
  • aws_cloudwatch_log_group/aws/lambda/{{関数名}}
  • aws_iam_role
  • aws_iam_role_policy_attachment(VPC上に配置したり、AWSの他のリソースへアクセスする場合)

将来、Lambdaについて十分な知見がないジュニアメンバーに引き継がないといけないときでも説明が容易ですね!

では、このLambdaがRDSを参照するようなものである場合は、そのRDSはどこに配置するべきでしょうか?
RDSは必須リソースではないため、他のModuleで定義し、引数でエンドポイント情報等を受取り、環境変数に設定してあげると良さそうです。

メリット: 命名の半自動化が可能

Terraformにおいて、Moduleのディレクトリ名は basename(abspath(path.module)) で取得できます。

Moduleのディレクトリ名をリソースの命名要素とすることで、typoや命名の迷いを減らすことができるというアイディアを以前から持っていました。

更に、Moduleをnestする場合は、呼び出し元が親Moduleから自Moduleまでの名前を適宜渡し、これらを連結することで命名を自動的に行うことができます。

以下に、簡易的なサンプルコードを示します:

variable "name_prefix" {
  type = string
  default = ""
}

locals {
  module_name = basename(abspath(path.module))

  # ディレクトリ名は snake_case リソース名は kebab-case を想定
  name_prefix = replace(
    join(
      "-",
      compact([
        var.name_prefix,
        local.module_name,
      ])
    ), "_", "-"
  )
}

# terraform の resource 名も this とか default とかで簡易に書けるのもメリットかと思います
resource "aws_lambda_function" "this" { 
  function_name = local.name_prefix
    :
}

# 追加で別の文字列をぶら下げたい場合
resource "aws_lambda_function" "some" { 
  function_name = replace(
    join(
      "-",
      [
        local.name_prefix,
        "some",
      ]
    ), "_", "-"
  )
    :
}

デメリットについて

ここまで、Moduleを「機能のまとまり」として活用するケースの長所を並べましたが、短所も勿論あります。
Module間で値を受け渡すためのコーディングを頑張らないといけないことや、切り方によってはModuleのnestが深くなってしまうことがあり見通しが悪くなるという点です。

リアーキテクチャで最初にやったことその2:
ディレクトリ構成の決定

少々長くなってしまいましたが、ここまでModuleの活用について説明させていただきました。
これらの考えを元に、以下のようなディレクトリ構成としました:

terraform/
  roots/
    for_poc/
      main.tf
         :
    for_production/
      main.tf
         :
  modules/
    front/
    app/
      modules/
        services/
        workflows/
           :
    internal/
       :
  shared_modules/
    aws_iam_policy_lambda_execute/
    aws_iam_policy_s3_read/
       :

terraform/roots/* でTerraformを実行し、各Moduleを呼び出していきます。

Module内で作成されるリソース名は、ここまでの説明でご想像がつくかと思います。
例えば、 modules/app/modules/services 配下のリソースであれば app-services app-services-hoge みたいな命名がされていそうですよね。
私は命名について悩みがちなのですが、この仕組みを利用することで「悩むのはディレクトリを切るときだけ」になり、やはり心理的負荷や工数の低減に繋げられたと思います。

また、特定のLambdaの実行権限や、特定のS3 Bucketの読み込み権限のIAM Policyのようにテンプレ化できるリソースについては shared_modules 配下に配置し、引数を与えれば同様のリソースを作成できるようにしました。

最初にこの構成が決まったことで、「このリソースはどこの構成要素であるか」ということを考慮しながら実装することができ、秩序だった実装ができたのではないかと思います。

一方で、先述の通りnestが深くなるリソースもあり、キャッチアップが難しかったという意見がチーム内から出たのも事実で、これらのトレードオフをどうするべきかは今後の課題です。

最後に

というわけで、2回に渡りTerraformについて執筆させていただきました!
本当に有用なツールですので是非皆様に活用していただきたいと思っており、私の記事がその一助となれば幸いです。

一方で、SRE部はインフラの実装だけ、Terraformだけを取り扱うわけではありません。
SLOや監視・通知、トイルの撲滅など、非常にユニークで重要な役割を担っております。

次回はSLOについてお話したいと思っておりますので引き続きよろしくお願いします!

また、本記事のレビュー中に「もっとリアーキテクチャ時の苦労話も書いてほしい」という要望もあったので、続きの話も時間を見つけて書こうと思います!