Terraformを実運用したら, 色々ハマった話

## はじめに

CYBIRDエンジニア [Advent Calendar 2017](https://qiita.com/advent-calendar/2017/cybird)1日目担当の[@ntrv](https://qiita.com/ntrv)です。

CYBIRDでは女性向け恋愛ゲームのサーバ運用・インフラ改善を担当しております。
新卒2年目のGolangでうまいことやりたいと思っているエンジニアです。
最近AWSのRe:inventを連続徹夜で見ていてものすごく眠いです...
ということで, 今年もアドベントカレンダーを開催いたします！

## Terraformとは?
* Terraformとはインフラ構築をコード化することのできるツールです。
* CYBIRDではAWSを使用した, 割と最近のタイトルで使用しております。

## 運用して困ったケース
* ここでは実際に運用しているタイトルでTerraformを使用したタイトルで実際に困った事例を紹介していきたいと思います。

1. 手動で追加したセキュリティグループをTerraformに後から追加するとき
1. ELB配下のEC2インスタンス(サービス中)のインスタンスタイプを変更したいとき

## 運用上困ったケース その1
### やりたいこと
* 取り急ぎセキュリティグループを手動で追加し, ALBにattachした。
* 手動で追加したセキュリティグループを後からTerraform管理下におきたい。

### 実施したこと
* `terraform import`を行い, 既存のリソースを`.tfstate`に取り込むことを考えた。
  * `terraform import module.fuga.aws_security_group.https_hoge sg-1234abcd`
* その後, `terraform plan`で差分がなくなるまで`*.tf`に書いていく。

### 困ったこと

#### 理想
* 以下のように`*.tf`に記述できることを望んでいた。 
  * 既存のコードと書きっぷりが変わらないようにしたかった。
* `terraform import`を行うことで`aws_security_group.https_hoge`だけが`.tfstate`に追加されるものだと考えていた。

```nginx
resource "aws_security_group" "https_hoge" {
  name        = "${var.env}-${var.app_name}-HTTPSFromHoge"
  description = "Allow HTTPS From Hoge"
  vpc_id      = "${var.vpc_id}"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"

    cidr_blocks = [
      "8.8.8.8/32",
      "8.8.8.4/32",
    ]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = -1
    cidr_blocks = ["0.0.0.0/0"]
  }
}
```
#### 現実
* 実際には以下のように記述する必要がありました。
    * セキュリティグループを記述する方法として二通り存在するため。
        * `aws_security_group` + `aws_security_group_rule`を使用する方法 <- こちらに合わせて書く必要があった。
        * `aws_security_group`にインラインで記述する方法
* `aws_security_group_rule`に関しては自由な名前を付けることが出来なかった。
    * 今回は`aws_security_group.https_hoge`の"https_hoge"から連想されてか"`aws_security_group_rule.https_hoge-1`と名付けられている。

```nginx
## 現実
resource "aws_security_group" "https_hoge" {
  name        = "${var.env}-${var.app_name}-HTTPSFromHoge"
  description = "Allow HTTPS From HOGE"
  vpc_id      = "${var.vpc_id}"
}

resource "aws_security_group_rule" "https_hoge" {
  type      = "ingress"
  from_port = 443
  to_port   = 443
  protocol  = "tcp"

  security_group_id = "${aws_security_group.hoge.id}"

  cidr_blocks = [
    "8.8.8.8/32",
    "8.8.8.4/32",
  ]
}

resource "aws_security_group_rule" "https_hoge-1" {
  type      = "egress"
  from_port = 0
  to_port   = 0
  protocol  = "-1"

  security_group_id = "${aws_security_group.hoge.id}"

  cidr_blocks = [
    "0.0.0.0/0",
  ]
}
```

### どのようにすればよいか
* 急ぎの場合以外はTerraform管理下のリソースに直接変更を加えない。
    * `terraform import`を極力行わないようにする。


## 運用上困ったケース その2
### やりたいこと

ELB配下のEC2インスタンスのインスタンスタイプを"安全に"変更したい場合, 手動によるオペレーションでは以下のように必要があります。

1. ALBからdetachする
1. アクセスログを確認し, 完全に切り離されたことを確認し電源を落とす
1. インスタンスタイプを変更し電源をあげる
1. ALBにattachする

この手順をTerraform管理下で実施する場合, 以下のような方法が考えられます。

1. 手動で上記手順を実施し, 後からTerraformを修正する...①
1. ELBへのattachmentはTerraform管理下に置かないようにし, 他はTerraformにまかせる...②

### 困ったこと
* 今まで(Terraform v0.8.5)は①の方法で実施しておりました。
    * しかしTerraform v0.10.4で同じ方法を実施したところ, リソース再作成のplanが表示されました。。
        * その後v0.10.7で検証しましたが, 再現しなかった... 今後詳しく調査する予定です。

### Terraform管理下で実施する方法
* そもそもTerraformで管理しているリソースに手を加えること自体危険であったので, ②の方針にしようかと考えております。
    * Terraform v0.8.8からはインスタンスタイプ変更時にリソース再作成ではなく, 再起動してくれるようになりました。
        * `provider/aws: Allow aws_instances to be resized rather than forcing a new instance` ([#11998](https://github.com/hashicorp/terraform/pull/11998))

②の方法を採用すると実際の手順は以下のようになります。

1. ALBからdetachする
1. アクセスログを確認し, 完全に切り離されたことを確認する
1. あらかじめ変更しておいた`*.tf`を使用し`terraform apply`を実行する
1. インスタンスタイプが変更され再起動されているので,  ALBに手動でattachする

## 得られた教訓
* `terraform import`を多用すると, コードがカオスなことになってくる。
    * 緊急時以外はterraform管理下のリソースに手動で変更しない。
    * `terraform import`で間違えたときに切り戻しできるように, backendのversioningの機能はONにしておく。
* 運用することを考えて, Terraformを使用する
    * 何でもTerraformに落とし込んでしまうと, 運用時に困るものこともある。
    * `ignore_changes`を有効利用する。
* Terraformで管理しているリソースに手動で変更を行うことは基本的にしない。
    * Terraformで保持している状態と実際の状態が乖離していく。
    * 後からTerraform側の修正で対応すると大変なことになる。

## その他主張したいこと
* とにかく`terraform plan`は絶対必須!!
    * TerraformがどのようにAPIを叩くのかを見ることができるため。
    * あくまで`terraform`の立てた実行計画なので, `apply`時に失敗することはありますが..
* 安全にTerraformを実行できる環境づくりは必要!!
  * RDS等消えると困るリソースは `lifecycle.prevent_destroy`でリソース再作成時にエラーとなるようにする。
  * State Lockingで`terraform apply`を同時実行されることを防止する。
  * `terraform plan`時に生成される`.tfplan`を`terraform apply`に渡す。
  * `terraform apply`はCircle CIやJenkins上から実行する。
  * tfenvを使用し, Terraformのバージョンをリポジトリごとに固定する。

## 感想
* Terraformは難しい...(おざなりな感想)

## 最後に
CYBIRDエンジニア [Advent Calendar 2017](https://qiita.com/advent-calendar/2017/cybird) 明日は、 [@masatoshiitoh](https://qiita.com/masatoshiitoh) の「TCPサーバーを書いてみよう」です！
私のデスクの隣にいる方で, 社内一の超テッキーではないかと噂になっている方です。楽しみですね！