闇の深い AMI を Packer で Infra as Code する

## 闇が深いとは

このくらいを指すものとします。

- AMIの出所がわからない
    - EC2インスタンスのAMI IDが、自前AMIになってる
    - Linuxディストリはわかるが、起点にしたバージョンはわからない
- AMIがどうやってできたのかわからない
    - sudo vi /etc/hoge.conf してるっぽい
    - sudo yum install -y hoge してるっぽい
    - wget https://~~~~/hoge.tar.gz から make install してるっぽい
- AMI更新手順がわからない
    - 変更したい人々と、変更できる人々が違っている
    - 実施できるのは、本番環境にSSHできる極少人数
    - 簡単なはずのconfの変更も尻込みして進まない
- Dockerize & k8s移行とか盛り上がるけど、遠すぎて動きが鈍い

AWS使っててもこれよりヤバい現場や、オンプレでさらにヤバい現場はいくらでもあるでしょうけど、一旦このくらいで。

## 闇に光を当てる情報収集

現状の変更の作業手順を各方面に聞き出したり、諸々の参照権限をもらったりして、文書化します。「してくださいお願いしますね」ではなく、私が文書化します。「してください」で文書化されるなら、もうされてます。みんな忙しい。

### AWS 利用状況、インフラ構成調査

AWS以外にもSaaSは使ってそうでしたが、聞いた範囲ではAWSにほぼ収まっているので、AWSに絞って調査します。AWSで使っているものは、AWSの月次の請求書を見ればわかります。AWSの請求書はたいへん親切で、

- 何を、何ドル、どのリージョンで、どれだけ使ったか

をひとつの文書にまとめてくれています。しかも、無料枠で収まっていても、利用料金ゼロドルで、無料枠の範囲でこれだけ利用しましたを記載してくれるのが大変良い。

これをもとに、推定と聞き込みとを織り交ぜ、実際の姿を明らかにしていきます。

### AMI 内部構成調査

ここまで明らかになると、どのEC2 AMIをInfra as Codeでカバーしたら効果高いかわかります（わかるよな？）。

アプリケーションコードのデプロイが頻繁で、バグや障害などの問題が、ユーザーから見たダウンタイムに結びつきやすいところからです。つまりはウェブサーバーですね。優先順位を決めて、EC2の内部構成調査のためSSHしてガサガサします。手法は以下のような。

- [インフラエンジニアと謎のサーバ - Qiita](https://qiita.com/kanga/items/979b0bdc4653e025483a)
- [Redhat系Linuxで、OSパッケージを戻す方法 - Qiita](https://qiita.com/sasasin/items/f865848239e7b0a38e06)
- [ざっくりこんなふうに漁る · GitHub](https://gist.github.com/sasasin/db2e2c375b9b160130aedbedf96c5941)

### AMI 更新手順

EC2に、SSHしてオラオラして、AMIを作成して、AutoScaling Groupに入れていました。

<img width="713" alt="スクリーンショット 2019-05-26 15.20.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22947/85483c24-42cf-e760-7859-2490255cd7e7.png">

## 方向性を考える

ここからDocker & k8sに行くには、ジャンプが大きすぎるなあという所感でした。簡単なはずのconfの設置とかも、モジモジして入れられてない。とはいえ、いまさら滅茶苦茶に工数ぶちこんで解決を目指すのももったいない。遠からず捨てる前提で、重厚長大にせず、安全にAMIを変更できることに専念したワークフローを構築したい。

そこで、現状に接ぎ木して、乗っ取って、乗り換えられるような、AMIビルドのパイプラインを考えてみることにしました。

## AMIビルドのパイプラインを妄想する

ちょっと前にCodeBuildの使い方を調べてたので、CodeBuildを使って、仕事の流れを組み立てます。CircleCIもありましたが、AMIにSSHさせるのに難儀しそうだなという直感があり、VPCで動かせるCodeBuildを選びました。

- [いますぐ使うCodeBuild - Qiita](https://qiita.com/sasasin/items/6e5f7001cba350851d49)

CodeBuild + Packer + Ansible を組み入れることで、このような仕事の流れにできそうだよねというのを、まずは妄想します。Ansibleでカバーの追いついてないconfの類は、SSHしてオラつけるよう仕事の流れにも遊びを残しておきます。

<img width="741" alt="スクリーンショット 2019-05-26 15.20.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22947/7c7295d9-88ee-7b05-c061-0ba0451638b7.png">

## AMIビルドのパイプラインを作り始める

ようやく作り始めます。CodeBuildでジョブを組むのは慣れたものです。Docker Hubを漁ると [hashicorp/packer](https://hub.docker.com/r/hashicorp/packer) があるのでこれを使います。Ansibleもチョットデキルので心配してない。

ぶっちゃけると Packer イジるの初めてなので、まず目標地点を

> 闇のAMIを入力に、Packerを通して、EC2内に `sudo echo hello-packer >> /tmp/hogehoge.txt ` したAMIを作る

くらいにしました。しかしここから3営業日くらい、PackerからEC2にSSHできないという症状を解決できず、憂鬱な日々でした。

その過程で、闇のAMIを Packer で Infra as Code する掟を見いだせたので、以下列挙します。

### 掟1. デフォルト VPC は使わない

本件に着手する少し前に [AWS環境のセキュリティ監査サービス insightwatch（インサイトウォッチ）by クラスメソッド](https://insightwatch.io/) の指摘を受けてデフォルトVPCを削除しまくっていました。しかもEC2-ClassicなAWSアカウントなので、サポート問い合わせしないとデフォルトVPCを作れない。めんどくせええ。。。

マニュアルを読むと、嬉しいことに、VPC IDなどを明示して動かすことができるとわかるので、 packer.json に盛り込んでいきます。

- [Amazon EBS - Builders - Packer by HashiCorp](https://www.packer.io/docs/builders/amazon-ebs.html)

振り返ると、デフォルトVPCで動かしてたらより一層進まなくなってたので、まあ、結果オーライでした。

### 掟2. CodeBuild は、本番環境SSH踏台サーバーと同一のサブネット、セキュリティグループで起動させる

今回どうにかしたいAMIは、SSH踏み台サーバーのセキュリティグループがついたところからのみ、SSHを受付けるよう設定されていました。

スパゲッティなセキュリティグループを無理にほぐそうとすると、何が起きるか見極めきれていなかったので、表題のように起動するよう、CodeBuildジョブを設定しました。

### 掟3. AMIは、本番環境のVPC,サブネット,などなど利用時と同一の諸々で起動させる

Packerは、元のAMIから、作業用EC2インスタンスを起動し、AMIを作成します。この作業用EC2インスタンスを、どのような設定で起動させるか。チュートリアルではPackerが、デフォルトVPCで何もかも良しなに仕上げてAMIを作ってくれます。

しかし私がどうにかしたいのは、デフォルトとはおよそ程遠い構成となっているAMIです。

Pakcerから出来上がったAMIを起動させるときのEC2インスタンスと、同一になるように、Packerに起動させます。具体的には、同一のIAMロール、VPC、サブネット、セキュリティグループ、同一のグレードのEC2、SSHキーペア、などとなるよう、packer.json に事細かに明記していきます。

今回は、チュートリアルの packer.json をベースに、つながらないな、これか？これか？これも必要か？などと試していきましたが、闇のAMIをどうにかするというシナリオでは、

- [Amazon EBS - Builders - Packer by HashiCorp](https://www.packer.io/docs/builders/amazon-ebs.html)

ここで列挙されているオプションをまずは全て明記して、これ削っても動くかな？大丈夫かな？ってやったほうが、早く進んだのではないかと思います。

## Ansibleで何かイジる

PackerからSSHさえできれば、ひたすら楽です。

ふとCodeBuildを眺めると、hashicorp/packer を使い続けるのか問題がありました。

- [hashicorp/packer](https://hub.docker.com/r/hashicorp/packer) はalpineベースのコンテナイメージなので、pythonにpipに依存ライブラリに、、、ansibleをインストールするのは割と難儀しそうです。
- また、闇のAMIからEC2インスタンスを作って試してみたところ、 [Ansible Local - Provisioners](https://www.packer.io/docs/provisioners/ansible-local.html) でansibleをインストールして使うのも、pythonを始めとした依存関係の解決にかなり難儀しましたので、これも避けたい。
- [ansible/ansible-runner](https://hub.docker.com/r/ansible/ansible-runner) に [Install Packer](https://www.packer.io/intro/getting-started/install.html) し、 [Ansible - Provisioners ](https://www.packer.io/docs/provisioners/ansible.html) するのが、最も簡単に走れると見極め、実際に最も簡単でした。

Ansibleのロール、プレイブックなどは、 packer.json を置いたのと同一ディレクトリに、 [Best Practices — Ansible Documentation](http://docs.ansible.com/ansible/playbooks_best_practices.html) の体系で置きます。

クレデンシャルは、SSMパラメータストアにKMS暗号化保管したものを、CodeBuildの環境変数に持ってきて、Ansibleではlookupで引き込みます。

- このように。
    - [Ansible メモ - シェル環境変数の参照 - weblog of key_amb](https://keyamb.hatenablog.com/entry/2014/06/14/185913)
- 手法はいくつかありそうでしたが、この記事で理解を整理してlookupだなと。
    - [[Ansible] 環境変数を取得する ansible_env.hoge と lookup("env", "hoge") の違い - てくなべ (tekunabe)](https://tekunabe.hatenablog.jp/entry/2019/03/09/ansible_env)
- SSMパラメータストアのクレデンシャルを、CodeBuildまで持ってくるには
    - [いますぐ使うCodeBuild - Qiita](https://qiita.com/sasasin/items/6e5f7001cba350851d49)

## 闇の深い AMI に光を当てる packer.json

そんなこんなで以下できあがりの packer.json です。variablesのenvは、CodeBuildジョブの環境変数として定義してあります。今回はこの程度で済みましたが、闇の深さによっては、他にも記載すべき項目が増えるのではないかと思います。

- SECURITY_GROUP_ID_BASE には、出来上がりのEC2インスタンスに付けるべきセキュリティグループIDを指定します。今回はひとつだけでしたが、もし複数指定が必要なら、よしなに書き換えてください。
- SECURITY_GROUP_ID_PACKER には、Packer用にSSHポートを開放したセキュリティグループを作成しておき、そのセキュリティグループIDを指定します。
- SSH_XXXX には、ec2-userで22から変更していれば、そのように指定してください。
- Ansibleのsftp_commandは、Amazon Linux前提なのでこう書いてますが、sftp-serverコマンドのパスを指定してください。
- Ansibleを使うことにしてますが、私が超慣れてるからなだけで、ItamaeでもChefでも好きにしてください。

```json
{
    "variables" : {
        "instance_type": "{{env `INSTANCE_TYPE`}}",
        "source_ami_name": "{{env `SOURCE_AMI_NAME`}}",
        "source_ami_owner": "{{env `SOURCE_AMI_OWNER`}}",
        "dest_ami_name": "{{env `DEST_AMI_NAME`}}",
        "region": "{{env `REGION`}}",
        "vpc_id": "{{env `VPC_ID`}}",
        "subnet_id": "{{env `SUBNET_ID`}}",
        "ssh_interface": "{{env `SSH_INTERFACE`}}",
        "security_group_id_base": "{{env `SECURITY_GROUP_ID_BASE`}}",
        "security_group_id_packer": "{{env `SECURITY_GROUP_ID_PACKER`}}",
        "ssh_username": "{{env `SSH_USERNAME`}}",
        "ssh_port": "{{env `SSH_PORT`}}",
        "ssh_timeout": "{{env `SSH_TIMEOUT`}}"
    },
    "builders" : [
        {
            "type": "amazon-ebs",
            "region": "{{user `region`}}",
            "source_ami_filter": {
                "filters": {
                    "name": "{{user `source_ami_name`}}"
                },
                "owners": ["{{user `source_ami_owner`}}"],
                "most_recent": true
            },
            "instance_type": "{{user `instance_type`}}",
            "ssh_username": "{{user `ssh_username`}}",
            "ssh_port": "{{user `ssh_port`}}",
            "ssh_pty": true,
            "ssh_timeout": "{{user `ssh_timeout`}}",
            "vpc_id": "{{user `vpc_id`}}",
            "subnet_id": "{{user `subnet_id`}}",
            "ssh_interface": "{{user `ssh_interface`}}",
            "user_data_file": "./ec2-user-data",
            "security_group_ids" : [
                "{{user `security_group_id_base`}}",
                "{{user `security_group_id_packer`}}"
            ],
            "ami_name": "{{user `dest_ami_name`}}-{{isotime \"20060102-150405\"}}",
            "tags": {
                "Name": "{{user `dest_ami_name`}}-{{isotime \"20060102-150405\"}}",
                "Base_AMI_ID": "{{ .SourceAMI }}",
                "Base_AMI_name": "{{ .SourceAMIName }}"
            }
        }
    ],
    "provisioners": [{
        "type": "shell",
        "inline": [
            "sudo echo hello world > /tmp/helloworld.txt",
            "ls -alF /tmp/helloworld.txt",
            "cat /tmp/helloworld.txt"
        ]
      },
      {
        "type" : "ansible",
        "user" : "ec2-user",
        "sftp_command" : "/usr/libexec/openssh/sftp-server -e",
        "playbook_file" : "site.yml"
      },
      {
        "type": "shell",
        "inline": [
            "sudo echo hello world"
        ]
      }
    ]
}

```

user dataも入れて起動できるので、こんな感じに使ってます。

```bash
#!/bin/bash
# ここで入れたEC2ユーザーデータは、packerビルド時のみ投入される。
# AutoScaling起動設定のもので上書きされる。

# SourceAMIの起動時にねじ込んで実行させるシェルスクリプト
# このファイルが存在してるとPacker生成のSSHキーの設置に失敗する
rm -f /home/ec2-user/.ssh/authorized_keys

# packerしてる最中に動かれると、果てしなく面倒なものは止めておく
chkconfig app-server off || true
```

CodeBuildの buildspec.yml はこのくらいで。DockerイメージはAnsible全部入りの ansible/ansible-runner です。

```yaml
version: 0.2

phases:
  install:
    commands:
      - yum install -y -q wget which unzip
      - (cd /usr/local/bin && wget -q https://releases.hashicorp.com/packer/${PACKER_VERSION}/packer_${PACKER_VERSION}_linux_amd64.zip && unzip ./packer_${PACKER_VERSION}_linux_amd64.zip)
      - pip install --upgrade pip
      - pip install awscli
      - ansible --version
      - packer version
  pre_build:
    commands:
      - ansible-galaxy install -p ./roles -r ./requirements.yml
      - packer validate packer.json
  build:
    commands:
      - packer build -machine-readable packer.json
```

## 未来展望

CodeBuild + Packer + Ansibleで、誰でも安全に変更を反映できる環境を整備できました。この先は、

- 変更したそうな /etc/hoge.conf をAnsible管理下に置くようにする
- 実際に変更していく
- サーバーロール間の結合を疎にするような変更を随時入れていく
- サーバーロール単位でのDockerizeと足回りのk8s移行

みたいな流れを組んでいきたいなあと妄想しています。