Terraformを入門したお話

## はじめに

本記事は、Infrastructure as Code（IaC）ツールである Terraform の基本構造を学び、Docker コンテナから AWS EC2 インスタンスの作成までを行います。また state管理や tfファイル管理なども踏まえながら実務に近い形を学んでいきます。ぜひ〜！

## Terraform とは？

HashiCorp 社が提供する Infrastructure as Code（IaC）を実現するツールです。インフラの構成をコードで宣言的に記述することで、インフラ構成を自動的かつ一貫性を持って管理できます。

また Terraform の大きな特徴は、AWS、Microsoft Azure、Google Cloud Platform など、複数のクラウドプロバイダーやサービスに対応したマルチプラットフォーム対応である点です。マルチクラウド環境においても統一して作成することが可能です。

## 作業環境

今回 2 つの環境を terraform で構築していきます。それぞれで事前に必要な環境をご確認ください。

[Docker を用いた NGINX サーバー構築]

- Rancher Desktop を使用した Docker CLI 環境
- 事前にコンテナが起動済み（`docker ps`コマンドで確認可能）

[AWS EC2 インスタンス構築]

- AWS アカウントおよび AWS CLI
- 作成する AWS リソースへのアクセス権限を持つ IAM ユーザーと適切な認証情報

## 参考資料

本記事のハンズオンは以下チュートリアルに沿って進めていきます。

https://developer.hashicorp.com/terraform/tutorials/aws-get-started

## インストール

初めに Terraform をインストールします。今回は`brew`を用いたインストール方法をご紹介します。

```bash
# Homebrewに新しいレポジトリを作成
brew tap hashicorp/tap

# インストール
brew install hashicorp/tap/terraform

# 動作確認
terraform -help
```

https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli

## Terraform 基本構文

ブロックごとに構成を説明していきましょう。

### Terraform ブロック

```terraform
terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0.1" # dockerのバージョン
    }
  }
  required_version = ">= 1.2.0" # terraformのバージョン
}
```

ここでは terraform の全体に関わる設定を記述します。
具体的には terraform のバージョンや利用するプロバイダー（モジュール化しているサービス・機能）のバージョンなどを記述することができます。

https://developer.hashicorp.com/terraform/language/terraform#terraform-required_version

### Provider ブロック

```terraform
# docker利用
provider "docker" {}

# AWS利用
provider "aws" {
  region  = "us-west-2"
}
```

ここではプロバイダーに関する設定を記述します。terraform ブロックの`required_providers`に含まれているプロバイダーを記述します。
利用するプロバイダーごとに必要な設定内容は異なりますが、リージョンやエンドポイントなど様々です。

https://developer.hashicorp.com/terraform/language/providers/configuration

### Resource ブロック

```terraform
# Dockerイメージを作成する
resource "docker_image" "nginx" {
  name         = "nginx"  # Dockerイメージ名
  keep_locally = false # destroy時Dockerイメージも削除する
}

# EC2インスタンスを作成する
resource "aws_instance" "app_server" {
  ami           = "ami-0a463f27534bdf246" #AmazonマシンイメージのID
  instance_type = "t2.micro" # インスタンスタイプ
}
```

ここでは具体的に構成するインフラ内容を記述します。
プロバイダーに紐づく Resource を記載します。2 つ目の変数（例の場合、`nginx`や`app_server`）は任意の名称です。

https://developer.hashicorp.com/terraform/language/resources/syntax

### Variables ブロック

```terraform:variables.tf
variable "aws_region" { # 変数名
  description = "AWS region" # 変数の概要
  type        = string # 型
  default     = "us-west-2" # デフォルト値
}
```

ここでは変数値を記述します。具体的な変数の値の受け渡しはいくつか方法があります。

①実行時に宣言
コマンド実行時、コンソール上にて対話形式で変数を設定できます。
```bash
$ terraform plan
var.aws_region
  Enter a value: us-west-2  # 手動で入力
```

②コマンドライン引数
コマンド実行時、`-var`引数にて変数を設定できます。
```bash
terraform plan -var 'aws_region=us-west-2'
```

③環境変数
`TF_VAR_`に続く変数を定義することで、変数を設定できます。
```bash
$ export TF_VAR_aws_region=us-west-2
$ terraform plan
```

④tfvarsファイル
`terraform.tfvars`ファイル内に記述し、コマンドライン引数で記述したファイルを宣言することで変数を設定できます。

```terraform:terraform.tfvars
aws_region=us-west-2
```
```bash
terraform plan -var-file=terraform.tfvars
```


利用方法は、宣言されたモジュール内で`var`より取得できます。

```terraform:main.tf
 provider "aws" {
  # region  = "us-west-2" # BEFORE
  region  = var.aws_region # AFTER
 }
```

https://developer.hashicorp.com/terraform/language/values/variables

### Output ブロック

```terraform:output.tf
output "instance_id" { # 変数名
  description = "ID of the EC2 instance" # 変数の概要
  value       = aws_instance.app_server.id # 
}
```

ここでは作成したリソース情報において出力させる情報を記述します。
すでに作成されている環境に対し情報を取得できます。（作成環境がないと出力できない）

```bash
# アウトプットコマンド
$ terraform output
instance_id = "i-0241e32ad00df9b28"
```

https://developer.hashicorp.com/terraform/language/values/outputs


## 実装例 ① Docker+NGINX サーバー構築

Docker を用いた NGINX サーバーを Terraform で作成してみます。
terraform のコードは main.tf に記載していきます。

```terraform:main.tf
terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0.1"
    }
  }
}

provider "docker" {
  host = "unix:///Users/hoge/.rd/docker.sock" # ユーザー環境に準ずる
}

resource "docker_image" "nginx" {
  name         = "nginx"
  keep_locally = false
}

resource "docker_container" "nginx" {
  image = docker_image.nginx.image_id
  name  = "tutorial"

  ports {
    internal = 80
    external = 8000
  }
}
```

:::note warn

以下のようなエラーが発生することがあります

```bash
Error: Error pinging Docker server: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
│
│   with provider["registry.terraform.io/kreuzwerker/docker"],
│   on main.tf line 21, in provider "docker":
│   21: provider "docker" {}
```

Docker daemon というのはコンテナの起動管理をするサーバーを指し、そちらに接続できないよ、というエラーです。
今回 Docker 環境を RancherDesktop にて作成しているためデフォルト値では実行できず、明示的にエンドポイントを指定します。

```bash
$ docker context ls
NAME                DESCRIPTION                               DOCKER ENDPOINT                              ERROR
default             Current DOCKER_HOST based configuration   unix:///var/run/docker.sock
rancher-desktop *   Rancher Desktop moby context              unix:///Users/hoge/.rd/docker.sock
(base)
```

参照：[Terraform の Docker Provider サンプルを M1 Mac の Rancher Desktop で動かす](https://okojomemorandum.com/blog/learn-terraform-docker-container-with-rancher/)

:::

### terraform コマンド

```bash
# 初期化コマンド
terraform init

# 実行コマンド
terraform apply
```

以下のように作成内容が表示されます。問題なければ`yes`を入力します。

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/89e614c8-9dff-4e63-90da-6cff63481024.png">

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/c2951d06-fb74-4d0d-a06a-5b0c352355e3.png">

https://localhost:8000 へアクセスしサーバーが動いていることを確認できれば OK!

<img width="80%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/ef8d686f-9320-40f6-9bc6-163e127c23dd.png">

指定した名前のコンテナが作成できていることも確認できます。

<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/7531ae96-c0ac-4d3f-b0e4-1501ca01332d.png">

最後は作成した環境を削除まで行います！

```bash
# 削除内容確認コマンド
terraform plan -destroy
```
<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/25db03a8-2644-4a6d-8713-648dfda6f058.png">

```bash
# 削除実施コマンド
terraform destroy
```
<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/4c1044ad-2106-4d8f-a764-e506828b7d74.png">

### その他便利コマンド
起動以外に便利！なコマンドをご紹介。

```bash
# 現在の状態と設定ファイルの差分チェック
terraform plan

# フォーマッター
terraform fmt

# 構文チェック
terraform validate
```

## 実装例 ② AWSEC2 インスタンス構築

続いて、AWS の EC2 インスタンス起動を terraform で作成してみます。

:::note warn
複数のユーザー情報を保存しており、AWS 認証情報をプロファイルに保存している場合、Terraform 実行時に指定することが可能です。指定がない場合は`default`が利用されます。

```bash
export AWS_PROFILE={ 呼び出したいprofile-name }
```

AWS 公式：[名前を指定されたプロファイルを使用する](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-files.html#cli-configure-files-using-profiles)

MFA 認証のあるユーザーの場合は一時セッション情報を利用しアクセス可能です。
設定方法についてこちら →[MFA 認証ユーザーでの AWSCLI アクセス設定](https://qiita.com/inacyc_k/items/88dca8b56ba1a42f0090)

他にも AWS 認証情報を Terraform 記述ファイルへ直に記載したり、宣言方法は様々です。
⚠️ どの方法でも決して情報漏洩にならないように！お気をつけください！！

参考：[Terraform でクレデンシャルを読み込む方法あれこれ](https://qiita.com/Hikosaburou/items/1d3765d85d5398e3763f)

:::

### terraform ファイル

```terraform:main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.16"
    }
  }

  required_version = ">= 1.2.0"
}

provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_instance" "app_server" {
  ami           = "ami-0a463f27534bdf246" #AmazonマシンイメージのID（ここではAmazon Linux）
  instance_type = "t2.micro"
}

```

### terraform コマンド

```bash
# 初期化コマンド
terraform init

# 実行コマンド
terraform apply
```

以下のように作成内容が表示されます。問題なければ`yes`を入力します。
<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/9e4b53a6-3e64-4fbd-9701-e86321a0ff93.png">

<img width="80%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/8813ffac-47d2-4509-980e-89e93b260e97.png">

AWS ダッシュボードより中身を確認してみると、、？

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/dd841386-226d-4ef9-95e8-b78abc6e6bc3.png">

見事に EC2 インスタンスが起動できていることが確認できました！

## state 管理

terraform 実行すると自動的にローカル上に`terraform.tfstate`が作成されます。これは「今のリソース」の情報を Terraform 側で管理するファイルとなります。

しかしこの`terraform.tfstate`は Git 管理推奨ではないため、管理する方法があります

今回は２種類の保存方法を行います。

### HCP Terraformで管理
HCP Terraform(旧 Terraform Cloud)を用いてtfstateを管理することができます。このサービスでは他にも実行履歴や環境変数なども同時に管理することが可能です！

まずは無料アカウント登録から！

https://app.terraform.io/signup/account

ログイン完了したら、`Organization`を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/67238996-5c1d-4c69-868f-f6834c946fb6.png)

terraform ファイルへも修正します。`terraform` ブロックの中に `cloud` ブロックを追加します。
（⚠️ Not `backend`ブロック!）
```terraform:main.tf
terraform {
  cloud {
    organization = "organization-name" # 先ほど作成したOrganization名
    workspaces {
      name = "learn-terraform-aws"
    }
  }
  # ~以下略~
}
```
CLIより操作できるようHCP Terraformにログインします。

```bash
# HCP Terraformログインコマンド
terraform login
```
 `yes`を入力すると、自動的にToken発行するサイトへ遷移します。トークン発行し、ターミナルへ入力します。
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/a9510ab5-965f-4118-9d1b-d71413170ffa.png)

無事にログインできました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/b6b1ad2e-bbde-4c21-8e95-b4824c4cfa42.png)

```bash
# (再)初期化コマンド
terraform init
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/2563915d-6ff7-45c8-ad24-16918e610b2c.png)

`yes`で現在作成されているtfstateファイルをアップロード、`no`で新規tfstateファイル作成しアップロードします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/6ac592a7-1815-4df2-8b6a-6d9e39abcbfe.png)

これでtfstateファイルをHCP Terraformにて管理することができました！！
最後に2重管理になるので、ローカルファイルは削除します。

```bash
# ローカルファイルは削除
rm terraform.tfstate
```

### S3バケットで管理

任意のS3バケットを作成し、その中に管理します。必要に応じ同時編集ロック制御のためのDynamoDBを追加してください。

はじめにS3バケットを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/4972779f-f2b6-44a5-90df-c71acaaff562.png)


terraform ファイルへも修正します。`terraform` ブロックの中に `backend` ブロックを追加します。
（⚠️ Not `cloud`ブロック!）
```terraform:backend.tf
terraform {
  backend "s3" {
    bucket = "mybucket-terraform-state-tutorial" # バケット名
    key    = "terraform.tfstate" # ファイル名
    region = "ap-northeast-2" # リージョン
  }
}
```
再度初期化、実行してみると、、？

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/b61cab92-4802-4bd7-88c6-accda566e7dc.png)

無事にS3バケットにアップロードされていました！

これでtfstateファイルをS3バケットにて管理することができました！！
最後に2重管理になるので、ローカルファイルは削除します。

```bash
# ローカルファイルは削除
rm terraform.tfstate
```

参考：

https://zenn.dev/spacemarket/articles/b7bd1422b896ca#terraform-state%E3%81%A8%E3%81%AF%EF%BC%9F

## tf ファイル管理

現在のファイル構成はこのようになっています。
- **main.tf** - Terraformの主要設定ファイル
- **variables.tf** - 変数定義ファイル
- **output.tf** - 実行後に出力する値を定義ファイル
- **backend.tf** - tfstate管理先の定義ファイル
- **terraform.tfstate** - Terraformが管理する現在のインフラ状態を保存するファイル
- **terraform.tfstate.backup** - 前回の状態のバックアップファイル

現状の場合、`main.tf`に全てのリソース記述を記載しています。より複雑で様々なリソースでの構築を行う場合、そして開発/本番環境など複数環境に対し構築を行う場合、どのようなファイル構成がよいか、考えてみました。

### 最終的なファイル構成
```
IaC/
├── env/                      # 環境別の設定
│   ├── dev/                  # 開発環境の設定
│   │   ├── main.tf           # メインのTerraform設定
│   │   ├── variables.tf      # 変数定義
│   │   ├── vars.tfvars       # 変数の値
│   │   ├── backend.tf        # バックエンド設定
│   │   └── .terraform/       # Terraformの状態管理
│   └── prd/                  # 本番環境の設定
│       ├── main.tf           # メインのTerraform設定
│       ├── variables.tf      # 変数定義
│       ├── vars.tfvars       # 変数の値
│       ├── backend.tf        # バックエンド設定
│       └── .terraform/       # Terraformの状態管理
├── shared/                   # 共有リソース
│   └── provider.tf           # プロバイダー設定（AWS等）
└── modules/                  # 再利用可能なモジュール
   ├── lambda/                # Lambda用のモジュール
   │   ├── main.tf            # Lambda関数のメイン設定
   │   └── variables.tf       # モジュールの変数定義
   └── fargate/               # AWS Fargate用のモジュール
       └── main.tf            # Fargateのメイン設定
```

参考：

https://zenn.dev/himekoh/articles/202208191916

各ファイルの構成について紹介します。

### envファイル
作成したい環境ごとにフォルダを分け、この`main.tf`を基準に他ファイルを呼び出していきます。

```terraform:main.tf
# Terraform の設定
module "shared_provider" {
  source = "../../shared" # 
}

# Lambda 関数の作成
module "module_lambda" {
  source = "../../modules/lambda"
  env    = var.env
  name   = module.hoge.name
}
```
リソース作成に必要な情報を`var`変数で受け渡したり、他リソースのOutput値を受け渡したりすることができます。

### sharedファイル
環境ごとに差分が出ない、Terraform ブロックや、Provider ブロックなどを記述します。
Symbolic Linkを行うことで、ファイルをコピペすることなく、一元管理も可能です。
```bash
$ cd IaC/env/dev 
$ ln -s ../../shared/provider.tf
```

https://qiita.com/kakkie/items/9f8f0f550e80bb79355c

### modulesファイル
リソースごとモジュール化し、envファイルの`main.tf`より呼び出されます。
もし変数を利用する場合は`modules`配下でも`variables.tf`定義が必要になります。

:::note info
DRY原則について

Terraform の管理方法を調べていると、「DRY（Don't Repeat Yourself）」という言葉がよく出てきます。
これは、ソフトウェア開発原則の1つで
> すべての知識はシステム内において、単一、かつ明確な、そして信頼できる表現になっていなければならない。

ということ。ただ単にコードの重複をしない、というよりも再利用可能な処理を抽象化し、情報を共通化することが重要（らしい）

もっと詳しく知りたい方はこちら⇩
[DRY原則の適用範囲について](https://zenn.dev/chillnn_tech/articles/super-dry-code)

:::


### おまけ
環境ごとのterraformファイル管理方法として、terraform workspaceで行う方法もありました。
今回においては環境切り替えを自身でやる必要があったのと、環境差分が大きいと不向きであることから採用しませんでした。

実施環境にもよると思いますのでこちらも参考ください。

https://envader.plus/article/221

## まとめ
今回は初めてterraformでのIaCを体験し、state管理やファイル管理まで考察することができました。今回は単純なインフラ構築のため少ない記述のみですが、今後より業務環境に沿ったインフラ構築へ挑むにあたり引き続き学んでいきたいと思います！