Terraformerを使ってTerraformに既存インフラのリソースをインポートする


インフラエンジニアの寺岡です。
前回Terraformに既存リソースをインポートする方法として
以下の記事でterraform importコマンドをご紹介しました。

Terraformで既存のインフラリソースをインポートする方法

この記事のまとめにも記載していますが
importコマンドで書き換えてくれるのはtfstateのみであり
tfファイルはtfstateとの差分を見ながら自力で書いていく必要があります。
数が多ければ途方もない時間がかかりそうです、これは困った。

そんな皆さんに朗報です。
terraformerというツールがOSSとして公開されています。

https://github.com/GoogleCloudPlatform/terraformer

CLI tool to generate terraform files from existing infrastructure (reverse Terraform). Infrastructure to Code

このように書かれている通り既存のインフラからTerraformのファイルを自動生成するCLIツールのようです。
使い方もGithubに記載されているので実際に使ってみましょう。

インストール

Macの場合はbrewコマンドでインストールできます。

$ brew install terraformer
$ terraformer version
Terraformer v0.8.7

ここまでは簡単ですね。
今回はv0.8.7を使っていきます。

インフラ構成

terraformerでインポートする対象のインフラを事前に作成しておきました。

https://github.com/beyond-teraoka/terraform-aws-multi-environment-sample

構成図

同一のAWSアカウントに3つの環境があります。

  1. develop
  2. production
  3. manage

さらに各環境ごとに以下のリソースがあります。

  1. VPC
  2. Subnet
  3. Route Table
  4. Internet Gateway
  5. NAT Gateway
  6. Security Group
  7. VPC Peering
  8. EIP
  9. EC2
  10. ALB
  11. RDS

図には書いていないですが各環境のリソースにはEnvironmentタグを付与しており
それぞれdev,prod,mngが値として設定されています。

認証情報の用意

AWSの認証情報を用意しておきます。
こちらは皆さんの環境に合わせて用意してください。

$ cat /Users/yuki.teraoka/.aws/credentials

[beyond-poc]
aws_access_key_id = XXXXXXXXXXXXXXXXXXXX
aws_secret_access_key = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

[beyond-poc-admin]
role_arn = arn:aws:iam::XXXXXXXXXXXX:role/XXXXXXXXXXXXXXXXXXXXX
source_profile = beyond-poc

terraformerの実行

まずはGitHubに書いている例の通りに実行してみます。

$ terraformer import aws --resources=alb,ec2_instance,eip,ebs,igw,nat,rds,route_table,sg,subnet,vpc,vpc_peering --regions=ap-northeast-1 --profile=beyond-poc-admin
2020/05/05 18:29:14 aws importing region ap-northeast-1
2020/05/05 18:29:14 aws importing... vpc
2020/05/05 18:29:15 open /Users/yuki.teraoka/.terraform.d/plugins/darwin_amd64: no such file or directory

terraform initした時のプラグインのディレクトリが必要みたいですね。
init.tfを用意してinitします。

$ echo 'provider "aws" {}' > init.tf
$ terraform init

もう一度実行してみます。

$ terraformer import aws --resources=alb,ec2_instance,eip,ebs,igw,nat,rds,route_table,sg,subnet,vpc,vpc_peering --regions=ap-northeast-1 --profile=beyond-poc-admin

インポートできたようです。
先ほどまではなかったgeneratedというディレクトリが出来上がっています。

ディレクトリ構造

$ tree
.
└── aws
    ├── alb
    │   ├── lb.tf
    │   ├── lb_listener.tf
    │   ├── lb_target_group.tf
    │   ├── lb_target_group_attachment.tf
    │   ├── outputs.tf
    │   ├── provider.tf
    │   ├── terraform.tfstate
    │   └── variables.tf
    ├── ebs
    │   ├── ebs_volume.tf
    │   ├── outputs.tf
    │   ├── provider.tf
    │   └── terraform.tfstate
    ├── ec2_instance
    │   ├── instance.tf
    │   ├── outputs.tf
    │   ├── provider.tf
    │   ├── terraform.tfstate
    │   └── variables.tf
    ├── eip
    │   ├── eip.tf
    │   ├── outputs.tf
    │   ├── provider.tf
    │   └── terraform.tfstate
    ├── igw
    │   ├── internet_gateway.tf
    │   ├── outputs.tf
    │   ├── provider.tf
    │   ├── terraform.tfstate
    │   └── variables.tf
    ├── nat
    │   ├── nat_gateway.tf
    │   ├── outputs.tf
    │   ├── provider.tf
    │   └── terraform.tfstate
    ├── rds
    │   ├── db_instance.tf
    │   ├── db_parameter_group.tf
    │   ├── db_subnet_group.tf
    │   ├── outputs.tf
    │   ├── provider.tf
    │   ├── terraform.tfstate
    │   └── variables.tf
    ├── route_table
    │   ├── main_route_table_association.tf
    │   ├── outputs.tf
    │   ├── provider.tf
    │   ├── route_table.tf
    │   ├── route_table_association.tf
    │   ├── terraform.tfstate
    │   └── variables.tf
    ├── sg
    │   ├── outputs.tf
    │   ├── provider.tf
    │   ├── security_group.tf
    │   ├── security_group_rule.tf
    │   ├── terraform.tfstate
    │   └── variables.tf
    ├── subnet
    │   ├── outputs.tf
    │   ├── provider.tf
    │   ├── subnet.tf
    │   ├── terraform.tfstate
    │   └── variables.tf
    ├── vpc
    │   ├── outputs.tf
    │   ├── provider.tf
    │   ├── terraform.tfstate
    │   └── vpc.tf
    └── vpc_peering
        ├── outputs.tf
        ├── provider.tf
        ├── terraform.tfstate
        └── vpc_peering_connection.tf

13 directories, 63 files

ディレクトリの構造に注目してほしいのですが
terraformerはデフォルトで「{output}/{provider}/{service}/{resource}.tf」という構造でインポートするようです。
これはGitHubにも記載されています。

Terraformer by default separates each resource into a file, which is put into a given service directory.

The default path for resource files is {output}/{provider}/{service}/{resource}.tf and can vary for each provider.

この構造では以下の問題が出てきます。

  1. Terraformのリソースごとにtfstateが分割されているため、小規模な変更でも複数回のApplyが必要になる
  2. 全ての環境のリソースが同一のtfstateに記録されているため、1つの環境での変更が全環境に影響を及ぼす可能性がある

できればtfstateの分割基準はdevelop,production,manageなどの環境ごとにして
各環境のリソースは全て同一のtfstateに記録されるようにしたいですね。
これができないか調べてみると以下のことがわかりました。

  1. --path-patternオプションで階層構造を明示的に指定できる
  2. --filterオプションで指定したタグが付与されているリソースのみをインポートできる

この2つを組み合わせると実現できそうです、やってみましょう。

$ terraformer import aws --resources=alb,ec2_instance,eip,ebs,igw,nat,rds,route_table,sg,subnet,vpc,vpc_peering --regions=ap-northeast-1 --profile=beyond-poc-admin --path-pattern {output}/{provider}/develop/ --filter="Name=tags.Environment;Value=dev"
$ terraformer import aws --resources=alb,ec2_instance,eip,ebs,igw,nat,rds,route_table,sg,subnet,vpc,vpc_peering --regions=ap-northeast-1 --profile=beyond-poc-admin --path-pattern {output}/{provider}/production/ --filter="Name=tags.Environment;Value=prod"
$ terraformer import aws --resources=ec2_instance,eip,ebs,igw,route_table,sg,subnet,vpc,vpc_peering --regions=ap-northeast-1 --profile=beyond-poc-admin --path-pattern {output}/{provider}/manage/ --filter="Name=tags.Environment;Value=mng"

インポートが終わった後のディレクトリ構造は以下です。

ディレクトリ構造

$ tree
.
└── aws
    ├── develop
    │   ├── db_instance.tf
    │   ├── db_parameter_group.tf
    │   ├── db_subnet_group.tf
    │   ├── eip.tf
    │   ├── instance.tf
    │   ├── internet_gateway.tf
    │   ├── lb.tf
    │   ├── lb_target_group.tf
    │   ├── nat_gateway.tf
    │   ├── outputs.tf
    │   ├── provider.tf
    │   ├── route_table.tf
    │   ├── security_group.tf
    │   ├── subnet.tf
    │   ├── terraform.tfstate
    │   ├── variables.tf
    │   └── vpc.tf
    ├── manage
    │   ├── instance.tf
    │   ├── internet_gateway.tf
    │   ├── outputs.tf
    │   ├── provider.tf
    │   ├── route_table.tf
    │   ├── security_group.tf
    │   ├── subnet.tf
    │   ├── terraform.tfstate
    │   ├── variables.tf
    │   ├── vpc.tf
    │   └── vpc_peering_connection.tf
    └── production
        ├── db_instance.tf
        ├── db_parameter_group.tf
        ├── db_subnet_group.tf
        ├── eip.tf
        ├── instance.tf
        ├── internet_gateway.tf
        ├── lb.tf
        ├── lb_target_group.tf
        ├── nat_gateway.tf
        ├── outputs.tf
        ├── provider.tf
        ├── route_table.tf
        ├── security_group.tf
        ├── subnet.tf
        ├── terraform.tfstate
        ├── variables.tf
        └── vpc.tf

4 directories, 45 files

リソースが環境ごとに分割されていますね。
試しにdevelop以下のvpc.tfを見てみると対応する環境のVPCのみがインポートされています。

develop/vpc.tf

resource "aws_vpc" "tfer--vpc-002D-0eea2bc99da0550a6" {
  assign_generated_ipv6_cidr_block = "false"
  cidr_block                       = "10.1.0.0/16"
  enable_classiclink               = "false"
  enable_classiclink_dns_support   = "false"
  enable_dns_hostnames             = "true"
  enable_dns_support               = "true"
  instance_tenancy                 = "default"

  tags = {
    Environment = "dev"
    Name        = "vpc-terraformer-dev"
  }
}

tfstateに関しては各環境ごとのリソースが1ファイルに全て記録されているのでこちらも問題なさそうです。
中身は長いので省きます。

懸念点

リソースのID値がハードコーディングされてしまっている箇所がある

以下のaws_security_groupのvpc_idのように
リソースのID値がハードコーディングされてしまっている箇所が複数存在します。

resource "aws_security_group" "tfer--alb-002D-dev-002D-sg_sg-002D-00d3679a2f3309565" {
  description = "for ALB"

  egress {
    cidr_blocks = ["0.0.0.0/0"]
    description = "Outbound ALL"
    from_port   = "0"
    protocol    = "-1"
    self        = "false"
    to_port     = "0"
  }

  ingress {
    cidr_blocks = ["0.0.0.0/0"]
    description = "allow_http_for_alb"
    from_port   = "80"
    protocol    = "tcp"
    self        = "false"
    to_port     = "80"
  }

  name = "alb-dev-sg"

  tags = {
    Environment = "dev"
    Name        = "alb-dev-sg"
  }

  vpc_id = "vpc-0eea2bc99da0550a6"
}

新しくHCLを書く場合は「vpc_id = aws_vpc.vpc.id」のように動的参照するのですが
インポート時にそこまで補完するのはまだまだ難しいようです。
この部分はtfstateには既にVPCのIDが記録されているのでtfファイルをのみを修正すれば良いです。

terraform_remote_stateの記述が0.12に対応していない

以下のaws_subnetのvpc_idのようにHCLの記述がTerraformの0.11系のものになっている箇所があります。

resource "aws_subnet" "tfer--subnet-002D-02f90c599d4c887d3" {
  assign_ipv6_address_on_creation = "false"
  cidr_block                      = "10.1.2.0/24"
  map_public_ip_on_launch         = "true"

  tags = {
    Environment = "dev"
    Name        = "subnet-terraformer-dev-public-1c"
  }

  vpc_id = "${data.terraform_remote_state.local.outputs.aws_vpc_tfer--vpc-002D-0eea2bc99da0550a6_id}"
}

この状態で0.12系でApplyすると実行はできますが警告が発生します。

また、--path-patternオプションで環境ごとにディレクトリを分けるように変更すると
tfstate自体は1ファイルにまとめて出力されますが
tfファイル内でリソースを参照する時は相変わらずterraform_remote_stateで参照されています。
GitHubを見ると以下の記載があるので仕様ですね。

Connect between resources with terraform_remote_state (local and bucket).

上記のvpc_idの場合はaws_vpcとaws_subnetが同じtfstateに記録されているので
単純に「vpc_id = aws_vpc.tfer--vpc-002D-0eea2bc99da0550a6.id」のみで参照可能です。
この部分も自力で修正する必要がありそうです。

まとめ

いかがでしたでしょうか。
terraformerを使えば既存インフラのインポートも捗りそうですね。
いくつか懸念点はありますが、さほど修正は難しくないので許容できる範囲ではないでしょうか。
私も以前からterraform import辛いなと思っていたので
それをいい具合に解決してくれるツールを作成した「Waze SRE」の方々は素直に尊敬します。
皆さんも是非利用してみてください。


この記事をかいた人

About the author

寺岡佑樹

2016年新卒入社、現在5年目。
SREとして、社内の運用業務の仕組みの検討・実装をしつつ
社外で技術的な登壇を行うエバンジェリスト的な側面も持つ。

・GitHub
https://github.com/nezumisannn

・登壇経歴
https://github.com/nezumisannn/my-profile

・発表資料(SpeakerDeck)
https://speakerdeck.com/nezumisannn