ハウテレビジョン開発者ブログ

『外資就活ドットコム』を日夜開発している技術陣がプログラミングネタ・業務改善ネタ・よしなしごとについて記していきます。

運用基盤を刷新しました-Terraform本格導入編

こんにちは、SREチームの小川です。
最近やっと「2001年宇宙の旅」を見ました。のっけから真っ暗な画面が流れ、なんのこっちゃ?という感じでしたが、あれは宇宙の始まりを表していたのでしょうか。そして人類が生まれ歴史が作られる壮大な物語が始まったのでしょうか。
あ、外資就活でもTerraformを利用してインフラを改めて作り出したので、今回の記事ではその軌跡をご紹介します!

はじめに

外資就活運用基盤刷新記事の第2弾です。
この記事では、インフラ面で多くの課題を抱えていた外資就活が、Terraformの導入により飛躍的に改善した軌跡を紹介します。
自身のインフラに課題を感じている方々にとって、私たちの経験が一助になればと考えています!

Terraformとは

Introduction to Terraform
「infrastructure as code(IaC)」の1つです。
オンデマンドに柔軟にインフラリソースを作成することができる点から、オンプレミスからクラウドへの移行がトレンドなっています。
こうした柔軟性はクラウドの利点である反面、その手軽さからインフラリソースを無計画に作成することで管理が不行き届きになってしまうことがままあります。
しかしIaCを導入することで、インフラリソースを宣言的に管理することが可能になるため、クラウドの柔軟性を享受しつつインフラリソースをエンジニアが管理することが可能になります。
そしてご多分に漏れず、AWSを利用している外資就活には管理されていないEC2などが山積していたため、Terraformを本格導入することでエンジニアがインフラリソースを掌握できる環境を整えました。

外資就活のTerraform

まず、Terraformを外資就活に導入するに際して意識したことや解決した課題がありますが、御託を並べる前にTerraformがどのように運用されているか紹介します。

導入・運用に当たって意識したこと

Terraformを導入する1番の目的は、効率的にインフラリソースを管理することです。
効率的な管理とは、属人性が排除されていることや、冪等性が担保されていることや、環境差分が不必要に生じていないことを意味します。これらを実現するために以下を意識していました。

  • 変更に強く保守性の高いディレクトリレイアウトを採用する
  • モジュール機能を積極的に利用してTerraformをDRYに保つ
  • 環境ごとの差異は変数として切り出すことで変数以外の差が環境間で起きないようにする
  • workspaceを利用して環境の複製を容易に行えるようにする
ディレクトリレイアウト

Terraformの利用にあたっては、変更に強いIaCを実現するためにディレクトリレイアウトの設計が重要になりますが、外資就活では下記のレイアウトを採用しています。ありきたりではありますが、AWSのサービス単位でモジュールを作成し、envディレクトリ以下でパラメータを注入してモジュールを呼び出しています。

├── _modules
│   ├── alb
│   ├── domain
│   ├── ec2
│   ├── eks
│   ├── memcached
│   ├── mysql
│   ├── redis
│   └── s3
└── env
    ├── dev
    │   ├── locals.tf
    │   ├── main.tf
    │   ├── output.tf
    │   ├── dev01.terraform.tfvars
    │   ├── provider.tf
    │   └── variables.tf
    ├── prd
    │   ├── locals.tf
    │   ├── main.tf
    │   ├── provider.tf
    │   └── variables.tf
    └── stg
        ├── locals.tf
        ├── main.tf
        ├── output.tf
        ├── provider.tf
        └── variables.tf
モジュール

EC2やRDSなどAWSサービスのテンプレートを定義できる機能です。マシンスペックなどはprd,stg,devで異なることが一般的ですが、マシンスペックごとにリソースの定義をしていては効率的にIaCを行えているとは言えません。
モジュールを利用することで、EC2やRDSのあるべき姿をテンプレート化し、そのモジュールを呼び出す際にパラメータとしてマシンスペックを注入することで、少ないコードで効率的にインフラリソースを管理することができます。

たとえば、EKSクラスタの作成に関しては_modules/eks/eks.tfで下記のようにリソースを定義してテンプレート化します。

resource "aws_eks_cluster" "cluster" {
  name     = format("%s-%s-cluster", var.env, var.project)
  role_arn = aws_iam_role.eks_cluster.arn
  version  = var.eks_version

  vpc_config {
    subnet_ids = var.subnet_ids
    security_group_ids = [
      aws_security_group.cluster_sg.id
    ]
    endpoint_private_access = true
    endpoint_public_access  = false
  }

  enabled_cluster_log_types = var.eks_log_enabled ? var.eks_log_types : null

  tags = merge(
    var.tags,
    tomap({ Name = format("%s-%s-cluster", var.env, var.project) })
  )

  depends_on = [
    aws_iam_role_policy_attachment.eks_cluster_policy,
    aws_iam_role_policy_attachment.eks_service_policy,
    aws_cloudwatch_log_group.eks
  ]
}

このようにすることで、productionはEKSのバージョンが1.19で動作しているが、developで1.20の動作確認がしたい場合に、developでモジュールを呼び出す際にvar.eks_versionの値を1.20にすることで、productionに影響を与えることなく、developで新しいバージョンのEKSの動作確認を行うことが可能になります。

環境差分

各モジュールでマシンスペックなど環境ごとに差分が生じるパラメータを変数で定義しています。そしてenvディレクトリ以下のmain.tfでモジュールを呼び出す際に、変数に値を指定することで環境差分を管理可能にして柔軟な対応が可能となるようにしています。

さきほどのEKSモジュールを利用してEKSクラスタを作成する場合には各envディレクトリ内のmain.tfを下記のようにします。

module "eks" {
  source          = "../../_modules/eks"
  env             = terraform.workspace
  project         = local.project
  eks_version     = var.eks_version
  eks_log_enabled = var.eks_log_enabled
  eks_log_types   = var.eks_log_types
  subnet_ids      = module.network.private_subnet_ids["eks"]
  tags = {
    Project = local.project
    Env     = terraform.workspace
  }
}

そしてdevelopではEKSバージョンを1.20で、ログ取り込みによるAWSコストの上昇を抑制するためログを収集しないようにし、productionではEKSバージョンが1.19で、ログはしっかり収集したい場合には、変数の定義を下記のようにします。

※ tfvarsとlocals.tfとファイルの種類が異なるのは、一部workspaceを利用しているためです。

## env/dev/dev01.terraform.tfvars
eks_version     = "1.20"
eks_log_enabled = false
eks_log_types   = []

## env/dev/variables.tf
variable "eks_version" {}
variable "eks_log_enabled" {}
variable "eks_log_types" {}
## env/prd/locals.tf
locals {
  eks_version     = "1.19"
  eks_log_enabled = true
  eks_log_types   = ["api", "audit", "authenticator", "controllerManager", "scheduler"]
}

## env/prd/variables.tf
variable "eks_version" {}
variable "eks_log_enabled" {}
variable "eks_log_types" {}
workspace

外資就活のTerraformでは、prd,stg,devといった環境の単位をenv以下にディレクトリで表現しています。
Terraformにはこうした環境の単位をworkspaceという機能で区別することも可能です。これを利用することでディレクトリを増やすことなく環境を追加することが可能となります。そして外資就活ではdevでのみworkspaceを利用しています。

workspaceはenv/dev/ディレクトリで、例えばdev02を作成したい場合に下記コマンドを実行します。

terraform workspace new dev02

あとはenv/dev/main.tfに渡す変数の値をdev02.terraform.tfvarsに定義し、適用時には-var-file dev02.terraform.tfvarsオプションを指定して実行します。このようにすることで、main.tfで指定したモジュールで定義したリソースを新たに作成することが可能です。

## env/dev/dev02.terraform.tfvars
eks_version     = "1.19"
eks_log_enabled = true
eks_log_types   = ["scheduler"]

workspaceを利用しない場合には、env/dev/のようなディレクトリが環境の数だけ増加してしまいますが、workspaceを利用することで簡単に管理を行うことができます。そしてこの特徴は環境を増やす要望が出やすいdevelopと親和性が高いと判断したので導入しています。

一方でprd,stgについては、workspaceでの管理には特性上向いていないと判断し、ディレクトリによる管理を行うことにしています。
外資就活のTerraformでは、main.tfでのみ呼び出すモジュールを指定することが可能となっています。そしてprd,stgについては全てのモジュールを呼び出す必要があります。
workspaceではmain.tfを各環境で共有しつつ、tfvarsで環境差分を表現します。これではdevelopで、特定のモジュールを構築しない選択肢が無くなってしまいます。

module "domain" {
  source = "../../_modules/domain"
}

module "eks" {    ## <=== このモジュールだけ利用しないという柔軟な選択ができない!
  source = "../../_modules/eks"
}

module "alb" {
  source = "../../_modules/alb"
}

またworkspaceはterraform workspace select xxxで選択しますが、単純にこのコマンドを忘れたために切り替え漏れによるオペレーションミスを未然に防ぐ目的もあります。productionへ誤った内容の適用は笑えませんので。

課題

では、実際Terraform導入以前、もとい運用基盤刷新以前の外資就活のインフラリソースに、どのような課題があったのか紹介したいと思います。

冒頭で説明した通り、クラウドサービスは便利な反面、管理が不充分になってしまう落とし穴もあり、クラウド上にある外資就活のインフラリソースには以下の課題がありました。

  • IaCが中途半端で完全に宣言的にリソースを定義できていない
  • リソース命名規則が固定されておらず混乱を招く
  • リソース構成が管理されておらず不明瞭
  • 環境間で見過ごせない差分が存在する

IaCが中途半端

厳密にはTerraformは利用されていました。しかし利用は徹底されておらず、新規に作成したリソースのみTerraform管理とされていたり、途中からTerraformの利用を止めてマネジメントコンソールで修正を加えたりされていました。
これではかえって、IaCと手動でのリソース管理となってしまうことで、Terraformのコードを読み解いても現実と乖離していて時間を無駄にしてしまう、といった弊害が生じてしまいます。

リソース命名規則にムラがある

AWSが発行するランダムでユニークな文字列で構成されたリソース名と、エンジニアが恣意的に命名したリソース名が混在していました。
また恣意的に命名されたものに関しても、特定の機能を提供するAWSサービス群、つまりバックエンドのAPIを提供するALB・EC2・Elasticacheで、backendの機能だと表す名称に揺れがあるなどしました。
さらに、prd,stg,devでAWSアカウントを個別に設けていますが、アカウントごとに同一の機能を利用しているにもかかわらず、命名規則が異なるものもありました。
このような状態はエンジニアに混乱をもたらすうえ、AWSリソースの管理ツール(RDSの自動起動・停止)をLambdaなどを利用して作成する際に、環境ごとに変に条件分岐を設ける必要が出るなど負担となってしまいます。

リソースの構成が不明瞭

IaCで管理されておらず、どのリソースにどのような振る舞いをして欲しいか不明確な状態でした。
これにより、インフラに変更を加えたい場合でも、その影響範囲を測ることができず心理的負担をエンジニアに強いる状態でした。
当然、障害が生じた際にもボトルネックの特定が遅れてしまう原因になりますし、本来必要のないリソースが稼働することによるAWSコストの上昇も避けることができません。

prd,stg,devでの乖離が大きい

リソース命名でも触れましたが、環境ごとの差分が生じていました。
たとえば、ALBのルーティングのルールが異なり、環境ごとにトラフィックの通り道に違いがありました。
これでは変更を加える際に、devで検証して問題なくても、その変更が本番では意図した通りに動作しないといった不都合が生じてしまいます。

Terraformを導入してみて

それではTerraformの本格導入によって、外資就活のインフラリソース管理がどのようになったのか、課題は解消されたのか紹介したいと思います。

IaC文化の定着・AWSリソースの把握

AWSリソースの変更を原則Terraformからしか行なっていないため、Terraformのコードを読むことでAWSリソースを把握することが可能となりました。
これまではインフラ面の機能追加や改修が必要な場合に、変更による影響範囲を考慮するために多くの時間を費やしましたが、IaC化され管理された状態となることで費やす時間を大幅に削減できました。

またTerraformの利用を原則とすることで、インフラリソースの定義をGithub上でレビューする習慣がSREチーム内で絶対となりました。
第3者によるレビューによりミスを未然に防げることはもちろん、SREチーム内でインフラリソース知識の平準化をもたらすこともできます。これは「あの分野のことは〇〇に聞いて」や「xxのことは知ってるけどyyのことは良くわからないんだよね、ごめん」といった、コミュニケーションコストの削減と業務の効率化に繋がりました。

そしてこのようにインフラリソースの把握と管理が行えたことで、不具合が生じた場合などに手探りで原因を特定するのでなく、あたりをつけて原因の特定と対応をスムーズに行える心理的に負担の少ない土壌を醸成することができました。

リソース命名規則をフィックス

name = format("%s-%s-cluster", var.env, var.project)
tags = merge(
  var.tags,
  tomap({ Name = format("%s-%s-cluster", var.env, var.project) })
)

Terraformのフォーマット関数を利用してリソース名に統一をもたらすことができました。非常に瑣末なことではありますが、「名は体を表す」というように、AWSリソースの役割とリソース名をルール化することは大事です。
アプリケーション開発でも変数名には気を使うと思います。それは後からコードを読む人に誤解を与えないためであったりします。インフラリソースにおいても名称をルール化することで、誤解を招くことがないように気を配る空気をIaCを介して作ることができました。

また命名に関連して、IPアドレスについても整理整頓しました。
VPCを/16として、EC2やRDS、EKSといったAWSサービスごとに/24のサブネットを作成しています。さらにEC2は第3オクテットが10~19、RDSは20~29という具合に法則を設けています。/24であればネットワーク部が255個と、よほど大規模なサービスでなければ持て余すほどあるため、このように大胆にサブネットを区切り、よりエンジニアにとって直感的なネットワーク構成としました。
これも瑣末なことですが、よりインフラリソースへ愛着を持って高いモチベーションを持って開発するために必要な取り組みであったと考えています。
TerraformにはcidrhostcidrsubnetといったIPを効率的に管理する関数が用意されており、利用しない手もないので。


このようにTerraformを本格導入することで、数々の課題を解消することができました。
そして、なんといっても、Terraform化に際して既存のAWSリソースをほとんど1から置き換えました。これは非常に骨が折れる作業でしたが、カオス化したインフラリソースを管理する上では必要な作業でしたし、得られた恩恵は数知れないものとなっています。

今回の運用基盤刷新を機に、IaCを導入し、インフラリソースをレビューする実績を残すことができました。今後も外資就活ではインフラリソースをエンジニアが過不足なく引き継ぎながら、機能追加や改修を行なっていくことができるようになったと確信しています。

さいごに

Terraformを本格導入し、かつAWSリソースを1から構築しなおしたことで、完全なインフラの管理を実現することができました。
しかし難しい作業でもあったため、「既存のリソースをterraform importして妥協できるところはしようかなぁ」とか「段階的にリソースを置き換えていけば楽なのでは?」といった迷いも当然生じました。インフラがある程度大きくなってしまってから、Terraformを導入するなど整備する場合には、このような移行プランもしっかりと議論する必要があると感じます。
ただ今回の運用基盤の刷新においては、段階的な移行を行なっていては完遂することは難しかったはずで、「よいしょ!」で一斉にインフラを置き換える選択肢に間違いはなかったように思います。インフラはアプリケーションと比較して変更が難しく、変更の際のストレス・負担が大きいです。そのストレスを断続的に甘受するよりは、思い切ってまとめて置き換える選択肢もアリだと思います。

この記事ではテクニカルな紹介が少なくなってしまいました...
しかし、EKSをTerraformでどのように管理しているのかであったり、KubernetesプロバイダーやHelmプロバイダーを検証した時の話など、運用基盤刷新で培ったテクニカルな部分も今後アウトプットしていきます!

また、まだまだ「これを使ってもっと良くしたい」とか「あそこはこうしなきゃ...」など改善していきたいと考えています。一緒に取り組んでくれるエンジニアの方、募集しております!

https://herp.careers/v1/howtv