Edited at

DRY なインフラ定義を手に入れるための4つのステップ


Overview


  • この記事では重複の少ない、DRYなインフラを手に入れるための過程を4つのステップで示します

  • 最終的に Terraform Module Registryterragrunt を使ったリソース構成の例について紹介します

  • これらのリファレンス実装を作って GitHub 上で公開しているので、実際自分の手を動かしながら学習することができます

  • クラウドプラットフォームは AWS を想定しています


対象とする読者


  • インフラ定義にプログラミングのプラクティスを持ち込みたい人

  • terraform module のベストプラクティスがわからなくて悩んでいる人

  • IaC (Infrastructure as Code) の運用を少しでも楽にしたいと考えている人


用語



  • DRY


    • Do Not Repeat Yourself; (DRY 原則とも); 日本語に訳すと「重複回避」; プログラミングで良いとされているプラクティスの一つ。ひとことでいうと、処理の共通化によって管理するリソースを最小にし、管理コストを減らそう、ということ




  • Terraform Module Registory


    • みんなで使い回すことを目的として作られた、再利用性の高い terraform module を公開している場所(Docker Registry のようなもの)




  • terragrunt


    • gruntworks 社が開発している terraform の Wrapper. Terraform にない機能を補完する目的で作られた




背景となる課題


  • terraform の資産が増えてくると、似たようなリソースをたくさん増えてくる

  • DRY 原則からすると、これは良くない


    • 「ちょっとだけ違うけど、ほかはほとんど同じ」リソースをたくさん管理しないといけない


      • 管理コストが増える



    • プログラマによっては「コピペ」という非生産的な作業をしないといけない



  • => 使い回せる資材を1つ作っておいて、新しくリソースを作る場合は、差分の部分だけリソースを作るようにしたら良いのでは?
    これが DRY の考え方です


構築例: Triple-AZ な Public Subnet をもつネットワークリソース

ここから4つのステップでインフラをDRYにしていくわけですが、比較のために具体例があるとわかりやすいと思います。ここでは Multi-AZ に Public なサブネットを 3つ もつ以下のようなネットワークを作るとします。


インフラ構成図

terraform-best-practice-infra.png

作成するリソースは以下です:


  • VPC

  • Subnet

  • Route table & Route Definition

  • Internet Gateway

また、Staging と Production のそれぞれに環境を作るとします。

オーソドックスに作ると、フォルダ構成は以下のようになります。


オーソドックスなリソース構成

.

├── prod
│   ├── main.tf
│   ├── terraform.tfvars
│   └── variables.tf
└── stage
├── main.tf
├── terraform.tfvars
└── variables.tf

main.tf には上で上げたようなリソースの定義が記述されます。ここではコード例は上げませんが、 100-200行程度のコードになると思われます。

さて、ここで問題となるのが、リソースの重複です。環境ごとの違いを variables.tf と terraform.tfvars に切り出したとすると、main.tf は prod と stage で同一のファイルになります。

これはDRYの考え方からすると宜しくない。


ステップ1: シンボリックリンクを使う

すこし強引だが簡単にできる解決方法として、シンボリックリンクがあげられます。

prod/main.tf を stage/main.tf のシンボリックリンクにするのです。


共通部分をシンボリックリンクにする

.

├── prod
│   ├── main.tf -> stage/main.tf
│   ├── terraform.tfvars
│   └── variables.tf
└── stage
├── main.tf
├── terraform.tfvars
└── variables.tf

リソースが少ない場合はここの方法でもある程度回ると思います。

しかし、リソースが増えてくるとどうでしょうか? このVPCにALBを足して次のようにします。


リソースの管理場所が2つに増えた

.

├── prod
│   ├── alb
│   │   ├── main.tf -> stage/alb/main.tf
│   │   ├── terraform.tfvars
│   │   └── variables.tf
│   └── vpc
│   ├── main.tf -> stage/vpc/main.tf
│   ├── terraform.tfvars
│   └── variables.tf
└── stage
├── alb
│   ├── main.tf
│   ├── terraform.tfvars
│   └── variables.tf
└── vpc
├── main.tf
├── terraform.tfvars
└── variables.tf

リソースが2-3個の場合はこれも良さそうですが、それ以上になるとファイルをコピーしたりシンボリックリンクを作る作業が煩雑に思えてきます。

また、そもそも 似たようなネットワーク構成をもう一つ作りたいとすると、リソースの数も倍になります。なにより main.tf がまた重複してしまいます。(再びシンボリックリンクを作る、という方法も考えられますが、シンボリックリンクがどのリソースを参照しているのか?を管理するのもまた煩雑な作業です。)


ステップ2: terraform module を使う

似たようなリソースを重複なく管理する方法として、terraform には標準で module というしくみが用意されています。

詳しい module の定義の方法はここでは述べませんが、リソース構成は以下のようになるでしょう。


moduleを使って重複部分を共通化

.

├── modules
│   ├── alb
│   │   ├── main.tf
│   │   ├── output.tf
│   │   └── variables.tf
│   └── vpc
│   ├── main.tf
│   ├── output.tf
│   └── variables.tf
├── prod
│   ├── alb
│   │   ├── main.tf
│   │   ├── terraform.tfvars
│   │   └── variables.tf
│   └── vpc
│   ├── main.tf
│   ├── terraform.tfvars
│   └── variables.tf
└── stage
├── alb
│   ├── main.tf
│   ├── terraform.tfvars
│   └── variables.tf
└── vpc
├── main.tf
├── terraform.tfvars
└── variables.tf

リソース構成だけからはわかりませんが、 main.tf にべた書きしていた VPC や サブネットなどのリソース定義が、 modules 配下に共通化されています。

prod/vpc/main.tf には、modules/vpc 配下のリソースの参照先のパスと、変数の注入の部分だけが記述されています。


prod/vpc/main.tf

module vpc {

source = "../../modules/vpc"
...
}

modules を作る手間をかける必要はありますが、これでシンボリックリンクという強引な方法をつかわずに、terraform で標準的に提供された方法でリソースを共通化することができました。


ローカルに作成した terraform module の課題

小規模〜中規模までのリソース構成ではこれでもうまく回ると思います。しかし、どんな場合でも使い回しのきく柔軟性をもった module を設計するのはそれなりの技術力と設計コストを要します。

そういった事情から、似たようなサービスをもう一つ増やしたいというときは、既存のサービスと新規サービスで module をそれぞれ独立させる運用のことが多いのではないでしょうか。

つまりmodule を毎回作ることになり、生産性という観点ではさほど改善しないかもしれません(コピペがなくならない。。)。


ステップ3: Terraform Module Registry を使う

2017年9月 terraform を開発している HashiCorp 社が Module Registry という、

ほかの人が作った汎用性の高い module を公開することができるしくみをリリースしました。

これによって moduleをイチから作らなくても、ベストプラクティスにしたがって作られた柔軟性の高い module を誰もが利用できるようになりました。

これを使うことによって、リソースの定義は以下のように書くことができるようになりました。


prod/vpc/main.tf

module "vpc" {

source = "terraform-aws-modules/vpc/aws"
version = "v1.44.0"

name = "${var.name}"
cidr = "${var.cidr}"
azs = "${var.azs}"

public_subnets = "${var.public_subnets}"

tags = {
Terraform = "true"
Environment = "${var.env}"
}
}


ここでポイントは source の指定をフォルダの相対パスで指定するのでなく、Terraform Registry 上のパス terraform-aws-modules/vpc/aws を指定することです。

これによって、Module Registry 上にあるリソース定義を terraform init 時にローカルにダウンロードしてくることができます(ソースコードは GitHub に公開されています)。

我々がすることは、もはやリソースを定義することじゃなくて、あらかじめ定義されたリソースに環境ごとの設定値を注入するだけでよくなりました。


Terraform Module Registry の課題

ただ、一方で main.tf にある module をインポートする設定は stage と prod それぞれに記述する、という重複はまだ残っています。

また Terraform Registry を利用するのは誰でもできますが、社内でのみ保持したいプライベートな Terraform Registry を作ろうとすると、有料のビジネスプランを契約する必要があります。

GitHub をリポジトリのソースにすることもできますが、これには落とし穴があって、リポジトリのバージョン指定ができません。共通 module をいろいろなサービスで利用している場合は、共通モジュールに後方互換性のない変更を加えたときに、すべてのサービスに修正を加える必要があります。小規模ならともかく、中〜大規模なインフラを管理する場合には困ったことになります。

有料プランで利用できる Private な module registry ではバージョニングが利用できることから、もしかしたらビジネス戦略的な意図であえてそうしているのかもしれません。


ステップ4: Terragrunt を使ってもっと DRY に

時系列は前後しますが、2016年1月に terragruntという terraform の wrapper ツールがリリースされました。

このプロジェクトは HashiCorp とは独立して開発されました。こちらのブログで述べられているように、開発された目的は大ききく以下の2つです:


  1. 複数人が同時に apply したときの競合を避けるために、 state ファイルの rock 機能を使いたい 1

  2. GitHub を上で公開した自作の module を再利用できるようにする


terragrunt を使ったリソース構成例

Terragrunt を使ってさきほどのリソースを定義すると以下のような構成になります。


module-repository

.

└── modules
├── alb
│   ├── main.tf
│   ├── output.tf
│   └── variables.tf
└── vpc
├── main.tf
├── output.tf
└── variables.tf


live-resource-repository

.

├── prod
│   ├── account.tfvars
│   ├── alb
│   │   └── terraform.tfvars
│   ├── terraform.tfvars
│   └── vpc
│   └── terraform.tfvars
└── stage
├── account.tfvars
├── alb
│   └── terraform.tfvars
├── terraform.tfvars
└── vpc
└── terraform.tfvars

特徴的なのは、共通 module を定義したリポジトリと実際に plan や apply を実行するリポジトリが別々になることです。

このようなことをする必要があるのは、共通リソースのバージョンをコントロールするためです。


terragrunt は共通モジュールのバージョンを管理できる

以下はインポートされた vpc のモジュールを管理する terragrunt のリソースの定義です:


prod/vpc/terraform.tfvars

terragrunt = {

terraform {
source = "git::git@github.com:billthelizard/terragrunt-module-example.git//ap-northeast-1/network?ref=v1.0.0"
}
..
}

source にある URI を見るとわかると思いますが、git の tag を指定してリソースを引っ張ってくることができます。

これによってサービスAでは v1.0.0 に固定しながら、サービスBでは v.1.2.0 を使う、といったことができ、前述したような後方互換性の問題を心配しなくて良くなります。


モジュールを利用する側のリソース定義が最小限ですむ

もう一つの特徴として、モジュールを利用する側のリソース定義から *.tf ファイルが消えていることが上げられます。


prod/vpc/terraform.tfvars

terragrunt = {

...
}

name = "example"
env = "prod"
cidr = "10.1.0.0/16"
public_subnets = ["10.1.101.0/24", "10.1.102.0/24", "10.1.103.0/24"]
azs = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]


モジュールを利用する側が定義するのは以下の2つだけです。


  1. terragrunt の構成管理用の設定

  2. 環境ごとに異なる変数の値

こうして、ほぼ理想的な DRY なインフラ構成を実現できます。

ちなみに、 terragrunt は terraform の wrapper なので、terraform でできることは基本的にできます。もちろん、 module の定義の中で Terraform Module Registry を source に指定することもできます。


terragrunt の課題

一方で terragrunt にも cons はあります。


コンシューマ側のディレクトリ構成を module の構成に合わせる必要がある

terragrunt は plan や apply を実行すると、内部的に以下のような振る舞いをします


  1. リモートにある GitHub リポジトリの内容をローカルの .terragrunt-cache フォルダ配下にダウンロードしてくる


  2. .terragrunt-cache 配下にダウンロードしてきた *.tf ファイルに環境設定を注入して plan / apply を実行する

つまり、必ず module 単位で plan や apply をする必要があります。

したがって module を使う側のフォルダ構成も module の単位でフォルダを切る必要があります。


module を作ることを前提としている

上述のような振る舞いをするので、外部に module のリポジトリがあることが前提になります。共通化したくない場合であっても module を作らなければならないので、その部分のコスト(module 化にともなう設計/実装の工数)については考慮する必要があります。


リポジトリを分けたりバージョン管理することが煩雑に感じるかも

リポジトリを分けたりバージョン管理したり、といった terragrunt のルールにしたがったリソース管理をするためのコストが生じます。

もちろんバージョン管理をしなくても利用できますが、そうした場合は前述した後方互換性の問題と向き合わなければなりません。


まとめ

長くなってしまいましたが、terraform をベースとしたツールを使ってどのようにに DRY 原則を適用していくかをみてきました。

個人的な所感としては、サービス数が2〜10個以内の中規模のインフラであればステップ3までやっておくと、長期的なインフラ運用がかなり運用が楽になると思います。アリモノはできるだけ使いましょう。

ステップ4については cons の部分を考慮してもメリットがあるかどうかを考えて導入を検討しましょう。

チームのメンバー数とモチベーションとの相談になると思います。

感覚的にはサービス数が 10 個を超えるのであれば ステップ4を検討したらいいと思います。

結局の所、どんな場合でも当てはまる解決方法などなく、やりたいこととのトレードオフになるでしょう

「IaC という考え方を実際の運用に適用してみると結構辛い」という声も聞きますが、ここで紹介した方法によって IaC の運用を楽にするためのヒントになれば幸いです。


おまけ: サンプルリソースについて

今回の記事執筆にあたって、Terraform Registry と terragrunt を使ったリソース構築例のリファレンス実装を作ってみましたので、自分で手を動かしながら学習に使ってください。 :tada:


参考資料



  • Terraform Best Practices


    • リソース構成のベストプラクティスはこのサイトにある example を参考にさせてもらいました。コンテンツは未完成の部分もありますが、terraform のコーディング規約的な記載もあり、役に立つことが多いと思います







  1. 現在は terraform の標準機能として提供されている