ENECHANGE Developer Blog

ENECHANGE開発者ブログ

TerraformでVPC周りを作成する

こんにちは、ENECHANGE CTO室のkazです。エネチェンジ、SIMチェンジ、SMAPのインフラを横断的に担当しています。 インフラが担う箇所が動いてる間は誰にも褒められませんが、それが止まると怒られる。そんな陽の光の当たらないところで生きてる私ですが、ENECHANGEテックブログの記念すべき1本目を担当させていただきます!

ENECHANGEが提供するサービスのインフラは一部を除きAWSを使用しており、AWSの設定はオーケストレーションツールのTerraformとプロビジョニングツールのitamaeで設計・構築しています。これらを使い、インフラを全てコード管理することで、非属人化を実現し、客観的なレビューを可能にしています。 また、TerraformはAWS、GCP、Microsoft Azure、OpenStackなどで使用できます。

今回、Terraformの初期設定〜VPCまで足回りをばーーんと一気に構築したいと思います。

要件

1. vpcを作成
2. InternetGatewayを作成
3. 東京リージョンのAZ(a,c)にpublicサブネットとprivateサブネットを作成
4. privateサブネットをDatastoreのサブネットグループとして指定
5. publicサブネットが使用するRoutetableにInternetGatewayを指定
6. publicサブネットにNatGatewayを作成
7. privateサブネットが使用するRoutetableにNatGatewayを指定
  • 図にするとこんな感じ f:id:dev-enechange:20180518105030p:plain

設定

  • まず、OSに適したパッケージを選択し、インストールするディレクトリに解凍します。
  • terraformコマンドを投入すると動作確認できます。
hoge\:~$ terraform
Usage: terraform [--version] [--help] <command> [args]
The available commands for execution are listed below.
The most common, useful commands are shown first, followed by
less common or more advanced commands. If you're just getting
started with Terraform, stick with the common commands. For the
other commands, please read the help and docs before usage.

Common commands:
    apply              Builds or changes infrastructure
    console            Interactive console for Terraform interpolations
    destroy            Destroy Terraform-managed infrastructure
    env                Workspace management
    fmt                Rewrites config files to canonical format
    get                Download and install modules for the configuration
    graph              Create a visual graph of Terraform resources
    import             Import existing infrastructure into Terraform
    init               Initialize a Terraform working directory
    output             Read an output from a state file
    plan               Generate and show an execution plan
    providers          Prints a tree of the providers used in the configuration
    push               Upload this Terraform module to Atlas to run
    refresh            Update local state file against real resources
    show               Inspect Terraform state or plan
    taint              Manually mark a resource for recreation
    untaint            Manually unmark a resource as tainted
    validate           Validates the Terraform files
    version            Prints the Terraform version
    workspace          Workspace management


All other commands:
    debug              Debug output management (experimental)
    force-unlock       Manually unlock the terraform state
    state              Advanced state m
anagement[f:id:dev-enechange:20180518014825p:plain]
  • TerraformがAWSアカウントを使用できるように環境変数AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEYにIAMユーザーのAWS資格情報を設定する必要があります。
provider "aws" {
  access_key = "ACCESS_KEY_HERE"
  secret_key = "SECRET_KEY_HERE"
  region     = "ap-northeast-1"
}
  • 私の環境ではポリシーの異なる複数のIAMユーザを運用しているので、~/. aws/credentialsを参照し、任意のprofileを使用できるようにしています。 ローカルのcredentialsを参照することでmain.tfにハードコードせずに済むので、Git上で管理しやすくなります。

  • TerraformはHashiCorp Configuration Language (HCL) で書いていきます。

    • こんな感じでmain.tfという設定ファイルを作り、そこに使用するプロバイダを記述します。 使用できるプロバイダ一覧はこちら 
provider "aws" {
  region  = "ap-northeast-1"
  profile = "terraform-user001”
}

設定ファイル郡

  • ベストプラクティスではないですが、Resouce毎に分けてます
.
├── db_subnet_group.tf
├── eip.tf
├── internet_gateway.tf
├── main.tf
├── nat_gateway.tf
├── route_table.tf
├── route_table_association.tf
├── subnet.tf
├── terraform_remote_state.tf
├── variables.tf
└── vpc.tf

Configuration

  • main.tf
provider "aws" {
  region  = "ap-northeast-1"
  profile = "terraform-user001"
}

terraform {
  backend "s3" {
    bucket  = "terraform-tfstate"
    key     = "<project-name>/network/terraform.tfstate"
    region  = "ap-northeast-1"
    profile = "terraform-user00-1"
  }
}
  • terraform_remote_state
    • Stateをローカルではなく、Backend(S3)へ置き、複数人で開発できるようにしています。
      • 実は、terraform-tfstateというバケットはENECHANGEが持っています(;・∀・)
        • (バケット名は必ず、Amazon S3 内の既存バケット名の中で一意)
data "terraform_remote_state" "<project-name>_network" {
  backend = "s3"
  config {
    bucket  = "terraform-tfstate"
    key     = "<project-name>/network/terraform.tfstate"
    region  = "ap-northeast-1"
    profile = "terraform-user001"
  }
}
  • variable
    • 算術演算するために必要な数だけ記述しています。
variable "<project-name>-eip" {
  description = "Use EIP"
  default     = {
    "0" = "true",
    "1" = "false"
  }
}

variable "<project-name>-subnet-names" {
  description = "publicsubnet uses global IP"
  type        = "list"
  default     = [
    "<project-name>-public-a",
    "<project-name>-public-c",
    "<project-name>-private-a",
    "<project-name>-private-c"
  ]
}

variable "<project-name>-subnet-cidr_blocks" {
  description = "Create two public-subnet and two private-subnet"
  type        = "list"
  default     = [
    "***.***.***.***/22",
    "***.***.***.***/22",
    "***.***.***.***/22",
    "***.***.***.***/22"
  ]
}

variable "<project-name>-subnet-availability_zones" {
  description = "Use ap-northeast-1a and ap-northeast-1c"
  type        = "list"
  default     = [
    "ap-northeast-1a",
    "ap-northeast-1c"
  ]
}

variable "<project-name>-subnet-map_publicip-on-launchs" {
  description = "instances launched into the subnet should be assigned a public IP address"
  type        = "list"

  default = [
    "true",
    "false"
  ]
}
resource "aws_eip" "prod-natgw" {
  count = "${length(var.<project-name>-eip)}"
  vpc   = "${element(var.<project-name>-eip, count.index / 2)}"

  tags {
    Name        = "<project-name>-natgw"
    Service     = "<project-name>"
    Environment = "<environment>"
    Description = "Managed by Terraform"
  }
}
// <project-name> vpc
resource "aws_vpc" "<project-name>" {
  cidr_block           = "***.***.***.***/16"
  instance_tenancy     = "default"
  enable_dns_support   = "true"
  enable_dns_hostnames = "true"

  tags {
    Name        = "<project-name>"
    Service     = "<project-name>"
    Description = "Managed by Terraform"
  }
}
// internet gateway
resource "aws_internet_gateway" "<project-name>" {
  vpc_id = "${aws_vpc.<project-name>.id}"
  tags {
    Name        = "<project-name>-igw"
    Service     = "<project-name>"
    Description = "Managed by Terraform"
  }
}

*nat_gateway.tf

resource "aws_nat_gateway" "<project-name>-natgw" {
  count         = "${length(var.<project-name>-eip)}"
  allocation_id = "${element(aws_eip.natgw.*.id, count.index)}"
  subnet_id     = "${element(aws_subnet.<project-name>-subnets.*.id, count.index)}"

  tags {
    Name        = "<project-name>-natgw"
    Service     = "<project-name>"
    Description = "Managed by Terraform"
  }
}
resource "aws_subnet" "<project-name>-subnet" {
  vpc_id                  = "${aws_vpc.<project-name>.id}"
  count                   = "${length(var.<project-name>-subnet-names)}"
  availability_zone       = "${element(var.<project-name>-subnet-availability_zones, count.index)}"
  cidr_block              = "${element(var.<project-name>-subnet-cidr_blocks, count.index)}"
  map_public_ip_on_launch = "${lookup(var.<project-name>-subnet-map_publicip-on-launchs, count.index / 2)}"

  tags {
    Name        = "${element(var.<project-name>-subnet-name, count.index)}"
    Service     = "<project-name>"
    Environment = "<environment>"
    Description = "Managed by Terraform"
  }
}
// <project-name>-subnet public-routetable
resource "aws_route_table" "<project-name>-public_route_table" {
  vpc_id = "${aws_vpc.<project-name>.id}"
  count  = 2

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = "${aws_internet_gateway.<project-name>.id}"
  }

  tags {
    Name        = "${element(var.<project-name>-subnet-names, count.index)}"
    Service     = "<project-name>"
    Environment = "<environment>"
    Description = "Managed by Terraform"
  }
}

// <project-name>-subnet private-routetable
resource "aws_route_table" "<project-name>-private_route_tables" {
  vpc_id = "${aws_vpc.<project-name>.id}"
  count  = 2

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = "${element(aws_nat_gateway.<project-name>-natgw.*.id, count.index)}"
  }

  tags {
    Name        = "${element(var.<project-name>-subnet-names, count.index + 2)}"
    Service     = "<project-name>"
    Environment = "<environment>"
    Description = "Managed by Terraform"
  }
}
// <project-name>-public allocate
resource "aws_route_table_association" "<project-name>-public" {
  count          = 2
  subnet_id      = "${element(aws_subnet.<project-name>-subnets.*.id, count.index)}"
  route_table_id = "${element(aws_route_table.<project-name>-public_route_tables.*.id, count.index)}"
}

// <project-name>-private allocate
resource "aws_route_table_association" "<project-name>-private" {
  count          = 2
  subnet_id      = "${element(aws_subnet.<project-name>-subnets.*.id, count.index + 2)}"
  route_table_id = "${element(aws_route_table.<project-name>-private_route_tables.*.id, count.index)}"
}
resource "aws_db_subnet_group" "<project-name>-datastore" {
  name       = "<project-name>-datastore"
  subnet_ids = [
    "${element(aws_subnet.<project-name>-subnets.*.id, 2)}",
    "${element(aws_subnet.<project-name>-subnets.*.id, 3)}"
  ]
  tags {
    Name        = "<project-name>-datastore"
    Service     = "<project-name>"
    Environment = "<environment>"
    Description = "Managed by Terraform"
  }
}

初期化

  • terraform initでTerraform設定ファイルを含む作業ディレクトリを初期化します。
    • ここでsyntaxエラーなどがあればエラーが発生します。
    • TF_LOG=[TRACE,DEBUG,INFO,WARN,ERROR]として環境変数にセットしデバッグログ出力が可能になります。
$ TF_LOG=DEBUG terraform init

2018/05/29 15:20:41 [INFO] Terraform version: 0.11.7
2018/05/29 15:20:41 [INFO] Go runtime version: go1.10.1

(snip)

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.aws: version = "~> 1.20"

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
  • terraform planで実行計画を作成します。
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

(snip)

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.
  • terraform applyで作成した実行計画に基づいた構成を作成します。
    • yesと入力すると処理が走ります
$ terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

(snip)

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:

まとめ

これくらいの構成であれば、この程度の量の設定ファイルで済んでしまいます。 アプリケーション側で設定値が頻繁に変わるようなケースもあるので、terraformで全部管理しようとすると疲弊しちゃいますので、本末転倒。 ステークホルダーと協議しながら管理していきましょう。

これから、運用で蓄積したTipsなど、公開していけたらいいなと思います。