既存AWSインフラをterraformで管理する

# TL; DR
- 既存awsインフラを手動でterraformに起こすのは大変面倒くさい．
- dtan4氏によるterraformingはimport作業をある程度自動化するが，問題もある．
- terraformingを改造/手でファイルを書く を併用してimport作業を進めると良いと思う．

_____
terraformは，tfファイルにインフラの設定を記述し，tfstateにインフラの状態を保持することによってコードによる管理を実現しています．terraform管理下にない既存インフラをterraformで管理するためには，

- tfファイルに既存インフラの設定を記述する
- tfstateに既存インフラの状態を追加する

ことが必要になります．

aws上の既存インフラをterraform管理下に入れるにはいくつかの方法があります．ここではそれを整理します．

# 前提
- Terraform v0.11.11+ provider.aws v2.0.0
- AWS上には，terraform管理下に置くべきインフラとそうでないインフラが混在している．

# ゴール
AWS上に構築されている本番環境インフラをコード化し，

1. メンテナンスを容易にする．

1. 本番環境と同一構成の環境（staging環境）をすばやく構築できるようにする．

# 手動でやる方法
例として，vpcの設定をterraformに載せることを考えます．aws_vpcの設定の詳細は公式ドキュメントを参照してください．

#### tfファイルに雛形を用意する．
適当なtfファイルを作成（ここではvpc.tfとした）し，まずは設定のガワだけを記述します． 

```
# resource "awsサービス名" "名前（aws上の名前と一致している必要はない）"
resource "aws_vpc" "first_vpc" {
 
}
```
対象が複数ある場合はその数だけガワを用意してください．

#### tfstateに状態をimportする．
ここではterraformで提供されているterraform importを使います．
`terraform import <サービス名>.<名前> <aws上のID>`
が基本的な書式です．各サービスに対するimportコマンドの詳細も公式ドキュメントに載ってるので参照してください．例えば，先程のvpcの場合は

```text
terraform import aws_vpc.first_vpc vpc-xxxxxxxxx
```
となります．これでfirst_vpcがtfstateに取り込まれました．

#### diffを見ながらtfファイルを書く
terraform importでtfstateは自動生成されましたが，tfファイルは手動で書かないといけません．公式ドキュメントとawsコンソールをにらめっこしながらvpc.tfを埋めていきます．まずはExample Usageを適当にコピペしながら作っていくとよいです．
ある程度書き進めたら```terraform plan```を叩きましょう．すると，下のように現在のtfファイルとaws上の設定との差分が表示されます．この差分をゼロにするようにtfファイルに項目を足していけばいいわけです．

```text:terraform_planの出力

------------------------------------------------------------------------
 
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  ~ update in-place
 
Terraform will perform the following actions:
 
  ~ aws_vpc.first_vpc
      tags.%:      "2" => "0"
      tags.Name:   "first-vpc" => ""
      tags.STATUS: "IN USE" => ""
 
 
Plan: 0 to add, 1 to change, 0 to destroy.
 
------------------------------------------------------------------------
```

tfファイルにすべての項目を正しく記述し，aws上の設定と差異がなくなった状態ででterraform planを叩くと

```
------------------------------------------------------------------------
No changes. Infrastructure is up-to-date.
 
This means that Terraform did not detect any differences between your
configuration and real physical resources that exist. As a result, no
actions need to be performed.
```
と表示されます．これで，awsインフラがterraform管理下に入りました．

# terraformingを使う方法
先ほどの方法は，インフラ一つずつに対して全手動でtfファイルを書き，`terraform import`を叩いてtfstateを作るというものでした．対象のインフラが10個やそこらであればこの方法で気合で乗り切ることができますが，数が多くなると全手動でtfファイルを書くのは辛すぎます．

そこで，[dtan4氏が公開しているterraforming](https://github.com/dtan4/terraforming)を利用します．terraformingはtfファイルの作成を自動化し，tfstateの作成もサービスごとに一括で行うすばらしいツールです．

例えばaws_vpcのtfファイル作成は次のコマンドで完了します．terraformingの出力は標準出力に行われるので，ファイルに出力するときはリダイレクトします．

```
terraforming vpc > vpc.tf
```
tfstateの作成は次のコマンドで行います．

```
# ---------ディレクトリ内にterraform.tfstateがない場合-----------
terraforming vpc --tfstate > terraform.tfstate
terraform refresh
 
# ---------ディレクトリ内に既にterraform.tfstateが存在する場合-----------
terraforming vpc --tfstate --merge=terraform.tfstate > tmp.tfstate
cp tmp.tfstate terraform.tfstate
rm tmp.tfstate
terraform refresh
```

たったこれだけで全vpcの取り込みが終了します．tfファイルを全手動で書き，一つ一つ`terraform import`を叩くより恐ろしくラクなことがわかると思います．

# terraformingを改造する
terraformingを使えばtfファイルもtfstateも一瞬で作れることがわかりました．コレで手軽に既存インフラをAWSに起こせる・・・とは残念ながらなりません．

terraformingにはいくつか問題があります．

- `terraform plan`すると一部のサービスでtfstateとの差分が残る．dtan4/terraformingの最終コミットが2018年9月であり，一部のサービスについてterraform本家のバージョンアップについていけていない．  
例）nat_gatewayおよびelastic_ipでtfファイルにtagsが出力されない．
対象アカウント・対象サービスのすべての設定を取り込むため，terraform管理下におく設定項目を自分で指定することができない．
- referenceがID直書きされているため，出力されたtfファイルを使って新しい環境にインフラを立てることはできない．  
例）internet_gatewayはvpcを参照するが，terraformingで出力されるtfファイルにはvpc_idが直接数字で書き出される．新しい環境にvpcを立てるときは当然vpc_idが異なるので，internet_gatewayからvpcを参照することができない．

そこで，これらの課題に対応できるようにterraformingを改造します．[dtan4/terraformingのforkリポジトリ](https://github.com/dtan4/terraforming/network/members)を見ると，自分で改造して使ってる人はそれなりにいるようです．



手を加えるにあたってまず，terraformingが内部でナニをやっているのか見ることにしましょう．

[terraforming/lib/terraforming/resource/](https://github.com/dtan4/terraforming/tree/master/lib/terraforming/resource)に各サービスごとのrbファイルが，[terraforming/lib/terraforming/template/tf/](https://github.com/dtan4/terraforming/tree/master/lib/terraforming/template/tf)に各サービスごとのerbファイルが配置されています．まずはerbファイルを見ることにしましょう．

例えば[vpc.erb](https://github.com/dtan4/terraforming/blob/master/lib/terraforming/template/tf/vpc.erb)の中身は次のとおりです．

```erb:lib/terraforming/template/tf/vpc.erb
<% vpcs.each do |vpc| -%>
resource "aws_vpc" "<%= module_name_of(vpc) %>" {
    cidr_block           = "<%= vpc.cidr_block %>"
    enable_dns_hostnames = <%= enable_dns_hostnames?(vpc) %>
    enable_dns_support   = <%= enable_dns_support?(vpc) %>
    instance_tenancy     = "<%= vpc.instance_tenancy %>"
 
    tags {
<% vpc.tags.each do |tag| -%>
        "<%= tag.key %>" = "<%= tag.value %>"
<% end -%>
    }
}
 
<% end -%>
```
このカタチから，erbファイルには各サービスごとのtfファイル出力が書かれていることがわかります．tfファイルの出力に問題があるサービスについては，erbファイルを編集すればよさそうです．



つづいて，[terraforming/lib/terraforming/resource/](https://github.com/dtan4/terraforming/tree/master/lib/terraforming/resource)にあるrbファイルを見ることにしましょう．例として，[vpc.rb](https://github.com/dtan4/terraforming/tree/master/lib/terraforming/resource/vpc.rb)を見てみます．すると，その一部にtfstateについての記述があります．

```ruby:lib/terraforming/resource/vpc.rb
def tfstate
  vpcs.inject({}) do |resources, vpc|
    attributes = {
      "cidr_block" => vpc.cidr_block,
      "enable_dns_hostnames" => enable_dns_hostnames?(vpc).to_s,
      "enable_dns_support" => enable_dns_support?(vpc).to_s,
      "id" => vpc.vpc_id,
      "instance_tenancy" => vpc.instance_tenancy,
      "tags.#" => vpc.tags.length.to_s,
    }
    resources["aws_vpc.#{module_name_of(vpc)}"] = {
      "type" => "aws_vpc",
      "primary" => {
        "id" => vpc.vpc_id,
        "attributes" => attributes
      }
    }
 
    resources
  end
end
```

terraformingではtfstateを作るにあたって，terraform importを叩くのではなくJSONを直接書き出しているようです．

実のところ，tfstateのJSONをterraformingでマジメに作り込む必要はありません．サービスタイプやidといった必要最低限の情報さえあれば，`terraform refresh`でterraform.tfstateを自動修復することが可能だからです．


### オレオレterraformingを使うには
rbenvでrubyを別バージョンに切り替えて，自家製terraformingを`gem install`します．terraformingリポジトリの中で以下のコマンドを叩きます．

```terminal
rbenv local <適当なバージョン>
gem uninstall terraforming
gem build terraforming.gemspec
gem install terraforming-0.16.0.gem
```

### tfファイルの欠落項目を補う
terraformingの内部を簡単に見たところで，terraformingをマトモに機能させるために必要な改修に取り掛かります．

まずは，tfファイルの書き出しで足りない項目を追加します．例として，nat_gatewayを改良します．本家terraformingのnat_gatewayではtagsがtfファイルに出力されず，`terraform plan`を叩くと差分があると怒られていました．そこで，[nat_gateway.erb](https://github.com/dtan4/terraforming/blob/master/lib/terraforming/template/tf/nat_gateway.erb)に以下のようにtagsを追記します．

```erb:lib/terraforming/template/tf/nat_gateway.erb
<% nat_gateways.each do |nat_gateway| -%>
<% unless nat_gateway.nat_gateway_addresses.empty? -%>
resource "aws_nat_gateway" "<%= module_name_of(nat_gateway) %>" {
    allocation_id = "<%= nat_gateway.nat_gateway_addresses[0].allocation_id %>"
    subnet_id = "<%= nat_gateway.subnet_id %>"
#----追記ここから----
    tags {
<% nat_gateway.tags.each do |tag| -%>
        "<%= tag.key %>" = "<%= tag.value %>"
<% end -%>
    }
#----追記ここまで----
}
 
<% end -%>
<% end -%>
```

インスタンスについての情報はAWS SDKから取ってきているようです．AWS SDKのドキュメントを眺めながら書くことになります．

注）`<% ~~~ -%>`はカッコで囲われたrubyコードを実行する構文です．結果をテキストとして出力するときは`<%= ~~~ %>`を用います．



### tfファイル中のidを参照形に直す
既存インフラをただコードに起こして管理するだけなら他項目への参照をid直書きで書いても特に問題ありません．しかし，先に述べたとおり同じ構成を別の環境に建てたいのであればtfファイル内の参照を参照形に直す必要があります．参照形とは次のような形式です．（参考：https://learn.hashicorp.com/terraform/getting-started/dependencies）

```text:参照形
vpc_id = "vpc-xxxxxxxx" #id直書きされた形
vpc_id = "${aws_vpc.example-vpc.id}" #参照形
```

vpcを例に説明します．vpcのid-参照形解決を行うために，[Terraforming::Resource::VPC](https://github.com/dtan4/terraforming/tree/master/lib/terraforming/resource/vpc.rb)にname関数を用意しました．

```ruby:lib/terraforming/resource/vpc.rb

module Terraforming
  module Resource
    class VPC
      include Terraforming::Util
 
      def self.name(id, client: Aws::EC2::Client.new)
        self.new(client, []).name(id)
      end
 
      def name(id)
        v = vpcs.select { |e| e.vpc_id==id }
        if v.length > 0
          "${aws_vpc.#{module_name_of(v[0])}.id}"
        else
          id
        end
      end
 
    end
  end
end
```

erbファイル内でvpc_idを出力するときは次のようにします．（下の例はsubnetのvpc_idを参照形に変換している．）

```erb

vpc_id = "<%= Terraforming::Resource::VPC.name(subnet.vpc_id) %>"
```

他のサービスについても同じやり方でidを参照形に直すことができます．

# terraforming改造のサンプル
dtan4/terraformingに対して

- tfファイル出力項目の修正
- id直書きを参照形にする

を施した[改造terraformingリポジトリ](https://github.com/teamhimeh/terraforming)をgithubに公開しています．手を加えたAWSサービスは以下のとおりです．

- application load balancer
- auto scaling group
- auto scaling schedule（新設）
- ec2 instance
- elastic ip
- internet gateway
- launch configuration
- nat gateway
- route table
- security group
- subnet
- vpc

私がterraformingを使用した範囲でしか手を加えてないので，上に挙げたサービスでもdiffが残ることがあります．

# まとめ
既存インフラをterraform管理下に置くにはtf・tfstateファイルを作り込む必要があり，手動でやるととても大変であることを紹介しました．terraformingを使えばいくぶんかラクになりますが，それでもいくらか問題があるためterraformingを改造する手法を紹介しました．

AWSの既存インフラをサクッとterraform管理下に置く方法は，この記事を執筆している現在ではおそらく存在しません．対象のインフラ規模が十分小さければ手動で，大きければterraformingを改造してtfファイルとtfstateファイルを作り込んでいくことになるでしょう．

terraformによる既存インフラ取り込みは各個人単位ではそう何回も行う作業ではないため，キチンと自動化するモチベーションが薄くなかなか万能な自動化ツールが登場しないと考えています．hashicorpから純正ツールが登場する日は来るのでしょうか・・・

tfとtfstateを一通り作った後のはなしは別の記事にすることにします．