アスクルのインフラをIaC(Terraform)で構築するために最適なディレクトリ構成を研究する

みやまえゆたか(@yutaka0m)です。

今回は、弊社で取り組んでいるIaC(インフラストラクチャのコード化)についてまとめました。

とりわけディレクトリ構造については、色々とディスカッションしてきたので参考になる部分があるのではないかと思っています。

IaCとはなにか?

当ブログでIaCについて取り上げるのは初めてなので、まずはIaCについて簡単にまとめます。

IaCはInfrastructure as Codeの略で、日本語に訳すと「インフラのコード化」です。

今までのインフラストラクチャ構築の課題は何か?

インフラを構築するには、クラウドプロバイダ(GCP,AWS,Azure等)のコンソールを操作したり、サーバを1台ずつ設定したりすることで実現していました。

しかしそれらは、手作業と、ブラックボックスなノウハウによって支えられていました。そこには、膨大な時間・コスト増・ヒューマンエラーのリスクが伴います。

IaCによる課題解決

それらの課題を解決するのがIaCです。

IaCのツールとして有名なのは、

  • Terraform
  • CloudFormation

等があげられます。

これらのツールを使用することで、インフラの構築をコード化&自動化し、「インフラ構築時間の削減・コスト削減・自動化によりヒューマンエラーの回避」を実現します。

さらに、GitOpsの考え方を取り入れ、インフラコードをGit管理することで「Gitリポジトリさえ確認すれば今のインフラの構造/変更履歴がすべて分かる」という状況にし、インフラのブラックボックス化をなくします。

なぜTerraformを採用したのか?

弊社のとあるプロジェクトでIaCを導入すると決まった後、最初に検討したのは「どのツールにするか?」です。

結果として、Terraformを採用することにしたのですが、理由としては次があげられます。

  • クラウドプロバイダに依存しない(GCP,AWS,Azureどこでも使える)
  • Terraform RegistryというモジュールのOSSコミュニティーがある
  • 情報量が多い
  • 実践Terraformという素敵な入門書があった

terraform-aws-modules/vpc/aws 等のオフィシャルモジュールは、よく作り込まれているので、たいへん重宝しています。

Terraformのディテクトリ構造

次の検討したのが「ディレクトリ構造」です。 次の3つから検討しました。

Workspace

ひとつめは、TerraformのWorkspace機能を使って、同一TerraformリソースでWorkspaceを切り替えることで環境を切り替える方法です。

# 環境ごとの定数を定義する
variable "sg" {
  type = map(object({
    alb_sg = object({
      name                = string
      ingress_cidr_blocks = list(string)
    })
  }))

  default = {
    develop = {
      alb_sg = {
        name                = "alb-sg-develop"
        ingress_cidr_blocks = ["xxx.xxx.xxx.xxx/32"]
      }
    }

    staging = {
      alb_sg = {
        name                = "alb-sg-staging"
        ingress_cidr_blocks = ["yyy.yyy.yyy.yyy/32"]
      }
    }
  }
}

module "alb_sg" {
  source  = "terraform-aws-modules/security-group/aws//modules/http-80"
  version = "~> 3.0"

  name        = var.sg[terraform.workspace].alb_sg.name
  description = "Security group for ALB(HTTP)"
  vpc_id      = aws_vpc.this.id

  ingress_cidr_blocks = var.sg[terraform.workspace].alb_sg.ingress_cidr_blocks
}

メリット

  • DRYになる
  • ファイルの先頭のvariablesですべての環境を書くため、環境の差異がわかりやすい

デメリット

  • 環境ごとに作成するインフラ構造が大きく異なる場合、制御しにくい
  • 環境を切り替えること自体を管理する必要がある

環境ディレクトリ分離

2つめは、環境ごとにディレクトリを分け、個別にTerraformリソースを管理する方法です。

次のようなディレクトリ構造です。

.
├── develop
│   ├── network.tf
│   └── compute.tf
├── staging
│   ├── network.tf
│   └── compute.tf
└── production
    ├── network.tf
    └── compute.tf

メリット

  • 環境差異が大きくても困らない
  • 環境毎の依存がないので、実装の難易度が低い

デメリット

  • DRYにならない

モジュール

3つめは、モジュールによってリソースの作成をテンプレート化して、モジュールを呼びだす際の変数によって環境差異を実現する方法です。

.
├── module
|   ├── compute
|   |   ├── main.tf
|   |   ├── variables.tf
|   |   └── output.tf
|   └── network
|       ├── main.tf
|       ├── variables.tf
|       └── output.tf
├── develop
│   └── main.tf
├── staging
│   └── main.tf
└── production
    └── main.tf

メリット

  • DRYになる

デメリット

  • モジュールの設計は中高度のTerraform理解が必要
  • 汎用性の高いモジュールを書くのはむずかしい

採用したのは、環境ディレクトリ分離

検討した結果「環境ディレクトリ分離」のディレクトリ構造をとることとしました。

DRYにならないのは辛いですが、実装の難易度が低く、環境毎にインフラ構造が異なっていても苦労しないことが採用理由です。

完成したディレクトリ構造

最終的に完成したディレクトリは次のような形となりました。

.
├── module
|   └── moduleXXX
|       ├── main.tf
|       ├── variables.tf
|       └── output.tf
├── a-project
|   ├── develop
|   |   ├── network.tf
|   |   └── compute.tf
|   ├── staging
|   |   ├── network.tf
|   |   └── compute.tf
|   └── production
|       ├── network.tf
|       └── compute.tf
├── b-project
|   ├── develop
|   |   ├── network.tf
|   |   └── container-service.tf
.   .
.   .

プロジェクト / 環境 / Terraformリソースというディレクトリ構成です。

モジュールは、基本的にTerraform Registryから、公式のものを使用するにとどめています。それでも、冗長な記述が多くなってしまった場合のみ、シンプルなローカルモジュールを作成して対応しています。

モジュールで共有の変数を参照する

ディレクトリの構成が決まったあとで、困ったことが発生しました。 それは、「環境やプロジェクトをまたいだ共通の定数をどうやって共通化するか」という点です。 たとえば自社の固定IPアドレスなどを指します。

ネットで検索して上位に出てくる手法は、共通の定数をlocalsで定義し、それぞれのTerraformリソースにシンボリックリンクを配置する方法です。

例 : global-variables.tfが共通の定数。 各環境からシンボリックリンクをはっています。

.
├── global-variables.tf
├── a-project
|   ├── develop
|   |   ├── ../../global-variables.tf
|   |   ├── network.tf
|   |   └── compute.tf
|   ├── staging
|   |   ├── ../../global-variables.tf
|   |   ├── network.tf
|   |   └── compute.tf
.   .
.   .

この手法はシンプルですが、ファイルシステムに依存しているというデメリットがあります。

そこで思いついたのが、モジュールを使う方法です。

ディレクトリ構造を次のようにして、

.
├── modules
│   └── global-variables
│       └── main.tf
└── a-project
    └── dev
        └── main.tf

modules/global-variables/main.tfには次のように記述します。

output "xxx_static_ip_address" {
  description = "xxxの固定IPアドレス"
  value       = "xxx.xxx.xxx.xxx/32"
}

output "yyy_static_ip_address" {
  description = "yyyの固定IPアドレス"
  value       = "yyy.yyy.yyy.yyy/32"
}

このように、outputしかないモジュールを作成し、そこに定数を書きます。

a-project/dev/main.tfから定数を参照する場合は、

module "global-variables" {
  # アカウント間で共通で使用する変数
  source = "../../modules/global-variables"
}

locals {
  # 次のように参照できます
  xxx = module.global-variables.xxx_static_ip_address
  yyy = module.global-variables.yyy_static_ip_address
}

こうすることで、ファイルシステムに依存せず、どこからでも定数を呼びだすことができます。

tfファイルの分割

最後に「1つのtfファイルに何を書くか? / どう分割するか?」という点を整理して、認識合わせをしました。

それが次の表です。

ファイル名 内容 リソースの例(AWSの場合)
load-balancer.tf ロードバランサ aws_lb_listener
dependency.tf 定義済のものをdataで参照する data "aws_iam_policy_document",
data "aws_iam_role"
authority.tf 権限を定義する aws_iam_role_policy_attachment,
aws_iam_role,
firewall.tf ファイアウォール aws_security_group
network.tf ネットワーク aws_vpc,
aws_subnet
configuration.tf Terraformの設定 provider,
terraform

関連するサービスをまとめて、1つのファイルを作成します。

このようにルールをつくることで、新しいファイル名が乱立し可読性が下がることも防止していると思います。

あとは、必要なサービスが増えるごとに命名を考えて、ファイルを追加していきます。

おわりに

アスクルのIaC(Terraform)についてご紹介しました。 新しい学び、発見があればまたブログを書きたいと思います。

それでは〜。

ASKUL Engineering BLOG

2021 © ASKUL Corporation. All rights reserved.