AWS
Terraform

Terraformの構成 ver.2017冬

https://qiita.com/uraura/items/f20b21baf7f28101a9d9 の続編です.

おおざっぱな説明だけで具体的な構成の話がなくてよくわからん :thinking: って感じなので,続きを書きます.

ディレクトリ構成

.
├── 10_network
│   ├── main.tf
│   ├── output.tf
│   └── variable.tf
├── 10_network.tf
├── 20_backend
│   ├── main.tf
│   ├── output.tf
│   └── variable.tf
├── 20_backend.tf
├── 25_app_backend
│   ├── main.tf
│   ├── output.tf
│   └── variable.tf
├── 25_app_backend.tf
├── 25_app_datastore
│   ├── main.tf
│   ├── output.tf
│   └── variable.tf
├── 25_app_datastore.tf
├── README.md
├── terraform.tf
└── variables.tf

ディレクトリ名のプレフィックスの数値自体に意味はありません.ディレクトリと同じ名前のファイルがあり,それにmoduleが定義されています.
数値の大小で依存関係を表していて,数値の大きいmoduleは数値の小さいmoduleに依存しています.言いかえると,数値の小さいmoduleのoutputを数値の大きいmoduleは利用できる,ということです.

各moduleにどういうリソースが入ってるかは下図の通りです.
image.png

各層に何を入れるか,は厳密にわけるのは難しいのでフィーリングな部分もあります👻が,大きな方針は

  • アプリケーションによらない,AWSでシステム運用するのに必要なリソースをnetwork層に
  • アプリケーションで共通で使用するようなリソースをbackend層に
  • アプリケーション固有のリソースをapp_backend層に
  • アプリケーションがデータストアを必要とするのであればapp_datastore層に

です.backendとかapp_xxxとか名称は試行錯誤してたらこんな感じになってたので深い意味はあまりない...😔
app_backendapp_datastoreが分かれているのは,特にマイクロサービス的な構成をとると,データストアを必要としないサーバーもあったりするためです.

module

20_backendを見てみます.

20_backend.tf

20_backend.tf
module "backend" {
  source = "./20_backend"
}

output "backend_aws_iam_role_lambda" {
  value = "${module.backend.aws_iam_role_lambda}"
}

output "backend_aws_key_pair_ec2_key" {
  value = "${module.backend.aws_key_pair_ec2_key}"
}

output "backend_aws_s3_bucket_config" {
  value = "${module.backend.aws_s3_bucket_config}"
}

トップレベルにあるtfファイルには,moduleの定義と,そのmoduleからのoutputを定義します.
ポイントは

  • このmoduleは外部にどういう値をエクスポートしているのかが明確になる
    • terraform outputで見れるので,実際はわざわざソースを見る必要はない
  • outputする必要ないリソースのことは完全に無視できる(当該moduleをいじるときだけ気にすればいい)

20_backend/variable.tf

20_backend/variable.tf
data "terraform_remote_state" "aws_main" {
  backend     = "s3"
  environment = "${terraform.workspace}"

  config {
    bucket = "tfstate"
    key    = "aws/main/terraform.tfstate"
    region = "ap-northeast-1"
  }
}

Terraform Providerを利用するための設定です.
Terraformのworkspaceを利用して,環境(dev/stg/prd)を表現しています.environmentに渡すことにより,1つのtfstateで環境を考慮していい感じにやってくれます.

20_backend/main.tf

20_backend/main.tf
locals {
  network_aws_subnet_jump                 = "${data.terraform_remote_state.aws_main.network_aws_subnet_jump}"
  network_aws_route53_zone_main           = "${data.terraform_remote_state.aws_main.network_aws_route53_zone_main}"
  network_aws_security_group_localnet     = "${data.terraform_remote_state.aws_main.network_aws_security_group_localnet}"
}

data "aws_route53_zone" "main" {
  zone_id = "${local.network_aws_route53_zone_main}"
}

data "aws_security_group" "localnet" {
  id = "${local.network_aws_security_group_localnet}"
}

ここは実際のリソースが定義されています.
locals内にある,${data.terraform_remote_state.aws_main.network_aws_route53_zone_main}が,Terraform Providerを利用して値を取ってきてるところです.
ここは20_backendなので,それより下位の10_networkoutputだけを利用しています.

とりあえずファイルは1つにして全部おしこんでいますが,リソース数によっては

  • ファイルを分ける
  • サブモジュールとする
  • そもそも階層を増やす

などを考える必要がありそうです.
ファイルを分けるとリソースをどのファイルへ書く?問題が起きますし,サブモジュールにすると見通しが...?階層増えるとそれはそれでメンドいか...?など,まだ自分の中でもベストアンサーはありません🙇

local

余談ですが,localは便利なので積極的に使うと良いと思います.

internal_flagがtrueの場合に何かリソースを生成する,みたいな定義がある場合に,わざわざフラグで判定しなくてもよくなります.

module "foo" {
  internal_flag = true
  # internal_flagがONの場合に作られるリソースの設定値
  config = {
    name = "xxx"
  }
}

# internal_flagによってリソースを作ったり作らなかったりする
resource "aws_lb" "bar" {
  count = "${internal_flag ? 1 : 0}"
  :
}

↓↓↓

sample.tf
module "foo" {
  config = {
    name = "xxx"
  }
}

# moduleにconfigがあるかどうかでフラグを計算
# デフォルト値は{}にしておく必要がある
locals {
  internal_flag = "${length(keys(var.config)) != 0}"
}

# configがあるかによってリソースを作ったり作らなかったりする
resource "aws_lb" "bar" {
  count = "${internal_flag ? 1 : 0}"
  :
}

のようになり,moduleの外で設定しなければならない値が減って良い感じになります.

20_backend/output.tf

output "aws_s3_bucket_config" {
  value = "${aws_s3_bucket.config.id}"
}

外部で使用したいリソースのID等をひたすらoutputするだけです.
20_backend.tfにも書いてるじゃないか!と思うかもしれませんが,Terraform Providerを使うにはrootレベルでoutputされている必要があるため,moduleのoutputをそのままrootでoutputする,という感じになってしまっています.ちょっと面倒ですがまぁ仕方ないかな...と.

outputする値は,dataが使える場合は識別子(ARNやresource id等)のみ,使えない場合は↓な感じでmapにしています.

output "aws_iam_access_key_ci" {
  value = {
    id = "${aws_iam_access_key.ci.id}"
  }
}

他のmodule

25_app_backend25_app_datastoreも,上記と同様の方針でファイルを構成しています.
違うのは,25層は下位の10,20層のどちらのoutputも使用できる点ぐらい.

plan/applyする

現時点での構成では,moduleわけはしたもののリポジトリ単位でわかれているわけではないので,そのままplanなどとすると全モジュール読みこまれてしまい時間がかかる問題は解決されません.

ですが,module分割を行ったことにより,module単位で実行することは可能になります.

terraform plan -target=module.backend

などと引数を付けて実行することで,指定したモジュールだけ(今回の構成でいけば指定した層だけ)を対象にして実行できるので,下位層に変更がないことが分かってる場合は大幅に実行速度が上がります.

注意しなければならない点

  • 下位層のoutput追加と,上位層での利用を同時に追加したら死ぬ
    • 依存関係があるので,先に下位層のoutput追加をapplyまでしてからでないと上位層でいきなり参照してはいけません
  • output追加だけだとplanしてもno changesになってしまうのでapplyを忘れがちになってしまう

今までハマったのはこれぐらい....?もっとあったかも...

あとは,今は単一リポジトリでやっていますが,複数のリポジトリに完全に分離してしまった場合,下位層の変更を上位層はどうやって検知すればいいのか?という問題があるような気がしています.

そのへんは試行錯誤中なのでまたそのうち😶