Terraformをローカル環境でのインストールをせずにDockerの公式イメージから利用する

(2022/5/17) `Makefile`に記述している`docker`コマンドの環境変数他加筆。

# はじめに

老体に鞭打って`aws`に取り組んでいる昨今だが、どうも`GUI`というかウェブ画面での操作に慣れない。
慣れないというよりやり方を記録しておくのにわざわざスクリーンショットを取るのが煩わしいのだ。

「そんなあなたに`CloudFormation`」というのが`aws`的な流儀なんだろうから一応トライしてみているが、全作業がローカルのターミナルから完結するわけではないのと設定ファイルが難解なのでもう少しなんとかならないかと考えていた。
そこで対抗場として挙げられるのが`Terraform`なのでそのうちやろうと思いつつようやく重い腰を上げてやってみた。

ちなみに以下のリンクのように`aws-cli`のみでごりごりやるのはそれはそれですごいけど若干ついて行けない。

[AWS Fargateを使って、マイクロサービス環境を実現する \- 革命のブログ](https://blog.frevo-works.co.jp/entry/2019/03/04/133336)

また以下の例は`Docker`イメージを使っているもののコンテナ内のシェルにログインして利用しているのでちょっと自分のやりたい方法とは違う。

[DockerでTerraformを使う](https://zenn.dev/nagi125/articles/8d1488ecfc2f5717756b)

# 環境

* `os`: Linux Mint 19.1 Tessa
* `docker`: 20.10.14
* `make`: 4.1(無くてもよいし使わなくてもよい)
* `aws-cli`: 2.4.24

`make`は無くても構わないけどコマンドラインが恐ろしく長くなるし標準的な`Linux`だとデフォルトで入ってるだろうから使ったほうがよい。
後、`aws-cli`は`credentials`情報が作成済みなら今回の作業自体では要らないけどまあ`aws`さわるなら必須だと思う。

# 実作業

以下のリンク(以下、元ネタリンク)のサンプルを元に`ec2`インスタンスを作成してみる。

[Terraform入門 AWSにEC2を作成してみよう \- たけログ](https://takelg.com/aws_create_ec2_terraform/)

## 最新版公式イメージのバージョン確認

とりあえず`Terraform`のバージョンだけ確認

```shell
docker run -it hashicorp/terraform:latest version
```

結果

```shell
Terraform v1.1.9
on linux_amd64
```

## 事前準備

実際に`aws`を利用するためには`credentials`情報が必要となる。以下のサイトを参考に`IAM` ユーザーの作成や`aws cli`のセットアップをしておく。

[10分で理解するTerraform \- Qiita](https://qiita.com/Chanmoro/items/55bf0da3aaf37dc26f73)

そして元ネタリンクから`main.tf`をコピーして実行する。

で

```shell
docker run hashicorp/terraform:latest init
```

としたいのだが、設定ファイル(`*.tf`)が無いと怒られる。

```
Terraform initialized in an empty directory!

The directory has no Terraform configuration files. You may begin working
with Terraform immediately by creating Terraform configuration files.
```

## 最初のはまりポイント

ローカル環境に`main.tf`を準備してもコンテナのイメージ内からは見れない(存在しない)。
なので`volume`マウント(`-v`オプション)し、かつ作業ディレクトリ(`-w`オプション)にも設定する。
マウント先は`root`権限でしか書き込みできないディレクトリだと後々不便なので`/opt`とか`/var/tmp`とかにしてみる。

> 公式イメージが`alpine`のようなのでこのあたりのディレクトリは非`root`でも書き込めると判断

なので訂正版

```shell
docker run -v $(pwd):/opt -w /opt hashicorp/terraform:latest init
```

結果

```shell
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 3.0"...
- Installing hashicorp/aws v3.75.1...
- Installed hashicorp/aws v3.75.1 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!
```

うまくいった。よし。

## 次のはまりポイント

では`plan`実行。標準入力が求められるので`-it`オプションを追加する。

```shell
docker run -it -v $(pwd):/opt -w /opt hashicorp/terraform:latest plan
```

`profile`の入力画面

```
var.aws_profile
  Enter a value: 
```

ここで作成済みの`profile`を入力するも。。。

```shell
│ Error: error configuring Terraform AWS Provider: no valid credential sources for Terraform AWS Provider found.
```

これは`profile`が見つからないとういエラーで先程の例と同じくローカルの`~/.aws/credentilas`がコンテナ内からはアクセスできないからだ。
なのでマウントしてもよいが、ここは以下のように`main.tf`を`profile`を使わないように修正し(`variable "aws_profile" {}`をコメントアウト)、

```diff
--- main.tf.orig	2022-05-02 13:09:15.275770436 +0900
+++ main.tf	2022-05-02 13:09:20.695725517 +0900
@@ -5,8 +5,8 @@
   type    = string
   default = "ap-northeast-1"
 }
-variable "aws_profile" {
-}
+#variable "aws_profile" {
+#}
 
 variable "amis" {
   type = map(string)
```

そして`AWS_ACCESS_KEY_ID`と`AWS_SECRET_ACCESS_KEY`という環境変数を設定して実行する。

```shell
docker run -it -e AWS_ACCESS_KEY_ID=my-key -e AWS_SECRET_ACCESS_KEY=my-secret -v $(pwd):/opt -w /opt hashicorp/terraform:latest plan
```

よし成功。

```
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_instance.terraform-example will be created
  ...
```

## ここで真打ち登場

ここまで来るとコマンドが恐ろしく長いことに気づくはず。
そこで真打ち`GNU Make`の登場となる。

例えば上記コマンドは以下のようにすっきりする。

```shell
make init
make plan
```

### 実装

具体的には以下のようなファイル構成にする。

```
├── .env
├── Makefile
└── main.tf
```

`.env`(各変数は自分のユーザ情報を記述)

```
AWS_ACCESS_KEY_ID=<YOUR ACCESS KEY>
AWS_SECRET_ACCESS_KEY=<YOUR SECRET ACCESS KEY>
```

これまで紹介したコマンドラインのオプションに加え、実行ユーザやログ出力のオプションを加えた`Makefile`の完成形がこちら。

```Makefile
MAKEFLAGS += --warn-undefined-variables
SHELL := /bin/bash
.SHELLFLAGS := -eu -o pipefail -c
.DEFAULT_GOAL := help

# all targets are phony
.PHONY: $(shell egrep -o ^[a-zA-Z_-]+: $(MAKEFILE_LIST) | sed 's/://')

DOCKER_IMAGE=hashicorp/terraform:latest
WORK_DIR=/opt
U_ID=1000
G_ID=1000

AWS_ACCESS_KEY_ID=my-key
AWS_SECRET_ACCESS_KEY=my-secret

TF_LOG=DEBUG
TF_LOG_PATH=debug.log

# .env
ifneq ("$(wildcard ./.env)","")
  include ./.env
endif

export CMD
override define CMD
/usr/bin/docker run -it -u ${U_ID}:${G_ID} \
  -e TZ=Asia/Tokyo \
  -e AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \
  -e AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \
  -e HOME=${WORK_DIR} \
  -e TF_LOG=${TF_LOG} \
  -e TF_LOG_PATH=${TF_LOG_PATH} \
  -v ${PWD}:${WORK_DIR} \
  -w ${WORK_DIR} \
${DOCKER_IMAGE}
endef

version: ## Show terraform version
	@${CMD} -v

terraform-help: ## Show terraform help
	@${CMD} -help

init: ## Prepare working directory
	@${CMD} init

validate: ## Validation configuration file (*.tf)
	@${CMD} validate

fmt: ## Format configuration file (*.tf)
	@${CMD} fmt

plan: ## Show plan
	@${CMD} plan

apply: ## Create or update infrastructure
	@${CMD} apply

apply-auto-approve : ## Create or update infrastructure with -auto-approve
	@${CMD} apply -auto-approve

show: ## Show the plan
	@${CMD} show

output: ## Output resource
	@${CMD} output

destroy: ## Create or update infrastructure
	@${CMD} destroy

destroy-auto-approve: ## Create or update infrastructure -auto-approve
	@${CMD} destroy -auto-approve

help: ## Print this help
	@echo 'Usage: make [target]'
	@echo ''
	@echo 'Targets:'
	@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z0-9_-]+:.*?## / {printf "\033[33m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
```

### Makefileのポイント解説

環境変数を利用するため、`.env`を用いる。
同一階層の`.env`を読み込むための設定が以下。

```Makefile
# .env
ifneq ("$(wildcard ./.env)","")
  include ./.env
endif
```

ただし`.env`存在しない、または`.env`内や環境変数が存在しない場合に備えて事前にデフォルト値を設定しておくほうがよい。

> 実行ユーザの`UID`(変数名は`U_ID`)と`GID`(変数名は`G_ID`)をローカル実行環境と合わせておくと出力ファイル(`terraform.tfstate`等)のパーミッションが統一される。
> というか設定しないと`root`のパーミッションとなる。

(2022/5/17)追記

> `Terraform`は実行時に`~/.terraform.d`というディレクトリを(無ければ)作成するので`HOME`も設定するようにした。

```Makefile
DOCKER_IMAGE=hashicorp/terraform:latest
WORK_DIR=/opt
U_ID=1000
G_ID=1000

AWS_ACCESS_KEY_ID=my-key
AWS_SECRET_ACCESS_KEY=my-secret

TF_LOG=DEBUG
TF_LOG_PATH=debug.log
```

そうすると`.env`に書き込まれた変数だけがオーバライトされる。

> `.env`でなく`tfvars`運用でも構わない。
> いずれにしても`Makefile`からは外出しして`Git`の管理対象外(`.gitignore`)にしておくことが望ましい。

`Docker`のコマンドが何度も出てくるので`CMD`という変数に置き換えるが、ワンライナーで書くと長ったらしいので以下のようにする。

```Makefile
export CMD
override define CMD
/usr/bin/docker run -it -u ${U_ID}:${G_ID} \
  -e TZ=Asia/Tokyo \
  -e AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \
  -e AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \
  -e HOME=${WORK_DIR} \
  -e TF_LOG=${TF_LOG} \
  -e TF_LOG_PATH=${TF_LOG_PATH} \
  -v ${PWD}:${WORK_DIR} \
  -w ${WORK_DIR} \
${DOCKER_IMAGE}
endef
```

後はこんな感じでタスク設定ができる。

```Makefile
init: ## Prepare working directory
	@${CMD} init
```

## 準備ができたら

既に説明した

```shell
make init
make plan
```

までできたら実際に`aws`への適用(デプロイ)は

```shell
make apply
```

成果物の削除は

```shell
make destroy
```

となる。
困ったときは以下でオプションを調べて`Makefile`のタスクに登録すればよい。

```shell
make terraform-help
```

# まとめ

`Terraform`利用歴2日目のため、理解不足の面があるかもしれないことは予めお断りしておく。
しかし初期化、作成、デプロイ、削除と一通りのコマンドは実行できることを確認した。

`GNU Make`を使うといちいちコマンドを覚えなくて良いのと(設定によるが)入力補完が効くので大変便利である。
`Windows`環境でも`GNU Make`のバイナリは提供されていると後、`Docker`さえインストールされていれば`Terraform`はインストールしなくてよいのでとっつきやすいのではないだろうか。

> とはいえ`Windows`環境で実行するには`bash`や`awk`も必要だしそもそも`pwd`とかの設定があるので`Git Bash`や`WSL`とかでないときちんと動かないと思う。

`CloudFormation`より設定ファイルが読みやすいとはいいきれないかもしれないが、今の所`Terraform`のほうがしっくりくる。
ただ両者いずれにしてもウェブのコンソールから実際操作してみないと何をやっているのかどこの箇所の入力をしているのかわからないというのが難点ではある。