AWS
Terraform
Hashicorp
俺でもわかるシリーズ

俺が結論づけたterraformベスト・プラクティスとworkspaceとmodule考え方(AWS Provider編)

俺です。
最強のテラフォーマーズなみなさんこんばんは。
久しぶりにterraformを弄っていて、かっこよくworkspaceとmodule使ってapplyキメたくなったので、どうつかったもんか、纏めたものです。

対応バージョン

  • 本体: terraform v0.11.1
  • provider: aws v1.5.0

terraform運用俺なりの結論

素terraformが一番ラク

それでも俺はWorkspaceやModuleを駆使するterraform新人類であり続けるぜ。むしろ既に触りすぎてて離れられない体だわはっはっはという俺な方は考え方読み飛ばしてどうぞ

考え方

  • 全部寺したいかたは寺どうぞ。
  • terraformは文学, CloudFormationは芸術, プルリクは人生, コードレビューは友情です。

環境構築

  • 設計思想やModule仕様をまとめたDocumentを常々更新するパワーがない
  • チームメンバーに伝達して一定品質保てるようなパワーがなければ素のterraformが楽です

運用

  • 全部寺でもいいけどLaunch/create/attach/destroyする、に留めるのがよいです
  • ルールの更新については俺達のCodenize.toolsで補えるものはこっちに寄せる方がRuby DSLによる表現力の高さや強力なDry-Runの恩恵を受けることができます。最高。
  • stateを持つコンポーネントであるRDSやElastiCache周りの構成変更はDestory判定から逃れられないものが多いのでCLIが安牌です

ex)ElastiCache Redisのreplication group memberの修正(Single-AZ->Multi-AZ化)

  • 1nodeのreplication group作成
  • number of nodeを1->2にするとDestroy -> Create判定になる

結局俺が選んだ最強のterraform設計is...?

  • Workspaceと異なり視覚効果に優れた方針を最強Opsの@shiruさんが公開してくれている。いきなりWorkspace/Moduleへ手を出す前に俺はコッチのほうをオススメしたい

それでもWorkspaceとModuleを使うんや!という方は次いってみましょー。

Workspaceについて

仮想的なディレクトリです。
terraform_workspace.png

resource内に ${terraform.env} を記述することでplan/apply時にcurrent workspace名が読み込まれ、冗長なコードを削減することができます。
上記図の例では、構築/運用対象の環境をworkspaceに収めることで、plan/apply実行時の構成変更対象を環境単位に実行することができます。

詳しくはssh絶対殺すマン@shogomuranushiさんのTerraform Best Practices in 2017を読むと良いです。

Workspaceの実装方法

ぼくはEC2原人なのでVPCを中心に実装方法を考えます。
今回の内容はregionについてはふれません。
マルチリージョンで構成される場合は他のWorkspaceの考え方がでてくるとも思います。

難易度 俺俺名称 VPCの状態 環境分離の方法
専用テナント型 環境ごとにVPCが分離されている場合 develop/stage/productionといった名称のVPCで環境が分離されている
共有テナント型 環境がVPCで分離されていない 1VPCとしてセキュリティグループレベルで環境が分離されている

専用テナント型

dedicated_workspace.png

全環境内に作るリソースを均一にできる場合はコレが最も楽です。
workspaceを切り替えてplan->applyでOKです。

terraform workspace new <環境名>
terraform workspace select <環境名>
module "ad" {
    source = "../../modules/provider/aws/ec2"
    ec2 = "${var.ad}"
    iam_instance_profile = "${data.terraform_remote_state.iam.activedirectory["name"]}"
    subnet_id = "${data.terraform_remote_state.network.vpc.${terraform.env}.protected-route-nat-a}"
    vpc_security_group_ids = ["${data.terraform_remote_state.network.common["id"]}",
                              "${data.terraform_remote_state.network.ad["id"]}"]
}

共有テナント型

shared_workspace.png

1VPCに複数の環境を作り、SGで分離する場合
これもworkspaceを切り替えてplan->applyでOKですが共有テナントを呼び出す時は${terraform.env}を書かないようにします。

terraform workspace new <環境名>
terraform workspace select <環境名>

★の箇所で${terraform.env}を指定せず、共有テナントが存在するworkspace名を直接指定する。

module "ad" {
    source = "../../modules/provider/aws/ec2"
    ec2 = "${var.ad}"
    iam_instance_profile = "${data.terraform_remote_state.iam.activedirectory["name"]}"
★    subnet_id = "${data.terraform_remote_state.network.vpc.prod.protected-route-nat-a}"
    vpc_security_group_ids = ["${data.terraform_remote_state.network.common["id"]}",
                              "${data.terraform_remote_state.network.ad["id"]}"]
}

Moduleについて

どこまでモジュール化するか。ちょっと悩みます。
なんでもモジュール化は良くないです。
モジュール化のレベルをキメておくのが良いです。
俺は2レベルでのモジュール化を推したいと思います。

public module

実体。俗に言うガワの定義
第三者に公開してもよい定義を表す。github.com などで公開してもよい。
リソース例:aws_vpc,aws_subnet,aws_instance,aws_eip等

private module

環境固有の情報が含れる属性を表す。
github.comのプライベートリポジトリやgithub enterprise上で管理する。
terraformにおいてガワと属性を分離して定義できないものもprivate moduleとして扱うのがよい。

リソース例:aws_security_group_rule, aws_elb等。ガワに対して中身をどうするかの定義。
SGの定義(企業のIPが入ってるもの)はPrivate moduleとして管理して使いまわせるとメンテが楽。
※aws_elbはELBとlistenerを分離して作成できないためprivate module化がよい。

やるな module

そもそもモジュール化してはならないモノ
実態と属性の関連付けを行うリソースはモジュールしないことが吉
リソース例: aws_elb_attachment,aws_eip_association等

Moduleの呼び出し例

  • 実体のvariable
variable "ad" {
    type = "map"
    default = {
        prod.ami = "ami-ec279c8a"  # Windows 2016
        prod.key_name = "XXXXXXXx"
        prod.public_key = "**********"
        prod.instance_type = "t2.medium"
        prod.iam_instance_profile = "ad"
        prod.source_dest_check = true
        prod.ebs_optimized = false
        prod.root_block_device = "gp2"
        prod.root_block_device_size = 64
        prod.ec2_count = 1
        prod.eip_count = 0
        prod.tag_name = "ad"
        prod.tag_role = "ad"
        prod.tag_amirotate = ""
    }
}
variable "ad_elb" {
    type = "map"
    default = {
        prod.name = "ad-elb"
        prod.instance_port = "3389"
        prod.availability_zones = ["ap-northeast-1a","ap-northeast-1c"]
        prod.access_logs_bucket = "XXXXXXXXXXX"
        prod.access_logs_bucket_prefix = ""
        prod.instance_protocol = "tcp"
        prod.lb_port = 3389
        prod.lb_protocol = "tcp"
        prod.lb_healthcheck_interval = 30
        prod.healthy_threshold = 2
        prod.unhealthy_threshold = 3
        prod.lb_target_timeout = 15
        prod.lb_healthcheck_interval = 5
        prod.cross_zone_load_balancing = true
        prod.idle_timeout = 60
        prod.connection_draining = 10
        prod.connection_draining_timeout = 15
    }
}
  • 実体の作成と関連付け
module "ad" {
    source = "../../modules/provider/aws/ec2"
    ec2 = "${var.ad}"
    iam_instance_profile = "${data.terraform_remote_state.iam.activedirectory["name"]}"
    subnet_id = "${data.terraform_remote_state.network.vpc.prod.protected-route-nat-a}"
    vpc_security_group_ids = ["${data.terraform_remote_state.network.common["id"]}",
                              "${data.terraform_remote_state.network.ad["id"]}"]
}

module "ad_elb" {
    source = "../../modules/provider/aws/loadbalancer/elb"
    elb = "${var.ad_elb}"
}

resource "aws_elb_attachment" "ad" {
  elb      = "${module.ad_elb.id}"
  instance = "${module.adelb.id}"
}

これで作り込みすぎないモジュール人生が送れると思います。

workspaceとmoduleの図解

2つを組み合わせるとこーんなかんじになります。
これの学習コストは高いか、低いかというと実装していて高いなって感じました。
workspace_and_module.png

terraformをこれから始めよう、とりあえず素terraformから卒業しようという方は、視認性の高いshiru式terraform設計で慣れてからworkspaceやmoduleへの取り組むステップを踏むと、workspaceとmoduleがよくわからないけどいいらしい。
から事故リスクを踏まえていい感じのterraform設計へつなげることができると思います。

それでは素晴らしいterraform生活を!