Docker × AWS × terraform  = Infrastructure as Code

Docker上にterraformの環境を構築し、クラウドサービス上(AWS)に自動でインフラを構築する。
Docker上にterraform環境を構築したのは、ホスト環境をキレイなままに保っておきたかったからです。この記事では、開発環境と基礎知識をまとめます。

開発環境は、下記で実施しました。

##### 開発環境
- Edition: Windows 11 Home, Version: 22H2, OSビルド: 22621.1702
- WSL2
- Docker Desktop for Windows: 4.20.1 (110738)
- Docker Engine: 24.0.2
- Docker Compose: v2.18.1
- terraform 1.4.6
- aws(provider) 5.4.0

次に、ディレクトリは下記の構成にしました。

##### ディレクトリ構成
Docker/home/user
└terraform

まずは、必要なソフトをインストールしていきます。

##### AWS CLIのインストール
下記の公式サイトにアクセス

https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/getting-started-install.html

```docker:console
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
apt install unzip #Linux環境にインストールされていない場合に実行
unzip awscliv2.zip
sudo ./aws/install
```
インストールが完了されているか、AWSのVersionを確認する。
```docker:console
aws --version
# aws-cli/2.12.1 Python/3.11.3 Linux/5.10.16.3-microsoft-standard-WSL2 exe/x86_64.ubuntu.20 prompt/off
```

##### IAMユーザーの作成
AWSマネジメントコンソールから、AWS CLI管理用とTerraform用にIAMユーザーを作成していきます。下記のFullAccess権限を持ったユーザーを作成します。

- AWS CLI管理用：すべてのリソースに対するFullAccess権限
- Terraform用：すべてのリソースに対するFullAccess権限

作成したユーザーをAWS CLIのプロファイルに登録していきます。
AWS CLIを使ってAWSを操作する為に必要な認証情報になります。
```docker:profile
#aws configure [--profile] user_name

aws configure #defualt
AWS Access Key ID [None]: *******
AWS Secret Access Key [None]: *******
Default region name [None]: ap-northeast-1
Default output format [None]: json

aws configure  --profile terraform
AWS Access Key ID [None]: *******
AWS Secret Access Key [None]: *******
Default region name [None]: ap-northeast-1
Default output format [None]: json
```

##### Terraformのインストール
次にTerraformをインストールしていきます。
1. tfenvインストール

    - GitHubリポジトリクローン
    ```docker:console
    cd ~
    git clone --depth=1 https://github.com/tfutils/tfenv.git ~/.tfenv
    ```
    - パスを通す
    ```docker:console
    echo 'export PATH="$HOME/.tfenv/bin:$PATH"' >> ~/.bash_profile &&
    e ~/.bash_profile> source ~/.bash_profile
    ```
2. terraformインストール
    ```docker:console
    tfenv list-remote
    tfenv install <VERSION>
    tfenv use <VERSION>
    ```
3. 動作確認
    ```text:
    terraform version
    ```

##### git-secrets インストール
git-secretsとは、アクセスキーやシークレットキー等credentials情報をGithubへアップロードすることを防いでくれるツール。

- git-secretsインストール
```docker:console
#リポジトリをクローン
git clone https://github.com/awslabs/git-secrets
#インストール
cd git-secrets
make install

#動作確認
git secrets

######
usage: git secrets --scan [-r|--recursive] [--cached] [--no-index] [--untracked] [<files>...]
   or: git secrets --scan-history
   or: git secrets --install [-f|--force] [<target-directory>]
   or: git secrets --list [--global]
   or: git secrets --add [-a|--allowed] [-l|--literal] [--global] <pattern>
   or: git secrets --add-provider [--global] <command> [arguments...]
   or: git secrets --register-aws [--global]
   or: git secrets --aws-provider [<credentials-file>]
######
``` 
- git-secrets初期化
```docker:console
git secrets --register-aws --global
git secrets --install ~/.git-templates/git-secrets -f
git config --global init.templatedir ~/.git-templates/git-secrets
```
- 動作確認
```docker:console
git init
git add .
git commit -m "test"
```

##### EC2起動
ここまでの設定が出来たら、テストとして、EC2を起動してみます。
terrafomディレクトリ直下にmain.tfを作成します。
```docker:console
touch main.tf
```
```terraform:main.tf
provider "aws" {
    profile = "terraform"
    region = "ap-northeast-1"
}

resource "aws_instance" "hello-world" {
    ami = "ami-0f9816f78187c68fb" #EC2のamiを確認して下さい。
    instance_type = "t2.micro"
}

```
terraformディレクトリ直下へ移動してから、下記のコマンドを実行します。
```docker:./trraform/
terraform init
terraform plan
terraform apply
#############
Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes #yesを入力
#############
```

マネジメントコンソールからEC2を検索し、インスタンスでEC2が起動されていることを確認して下さい。

##### .tfstateファイル
インスタンスを起動させるとterraformディレクトリに.tfstateファイルが作成される。.tfstateファイルには、terraformを使って構築したクラウドの状態が記載されている。

##### EC2削除
今回はテストとして、EC2を起動したので、確認出来たら削除しておきます。
下記コマンドを実行し、削除しておく。
```docker:./trraform/
terraform destroy
```

-----------------------------------------------------------------------


#### Terraformの基本コマンド

##### terraform plan
現状のソースコードを元に実行計画を確認
```terraform:terraform plan
terraform plan [-var <KEY>=<VALUE>] [-var-faile <VAR_FILE>]
# -var <KEY>=<VALUE> 変数の指定
# -var-faile <VAR_FILE>　変数ファイルの指定
```

##### terraform apply
ソースコードに従って変更を適用する
```terraform:terraform apply
terraform apply [-auto-approve][-var <KEY>=<VALUE>] [-var-faile <VAR_FILE>]
# -auto-approve 実行計画の確認なしで適用(yes省略)
# -var <KEY>=<VALUE> 変数の指定
# -var-faile <VAR_FILE>　変数ファイルの指定
```

##### terraform destroy
Terraform管理のインフラ環境を削除する
```terraform:terraform destroy
terraform destroy [-auto-approve]
# -auto-approve 実行計画の確認なしで適用(yes省略)
```
##### terraform fmt
コードのインデント修正コマンド
```terraform:terraform fmt
terraform fmt
```

##### HCL2について
HashiCorpLanguage2:HCL2とは、ハシコープ社の独自言語。
- jsonによく似ているが少し違う。
- 簡単なプログラムが組める。
- #でコメントが記載出来る
- KEY・VALUEを"."ではなく"="で指定
- ヒアドキュメントが利用可能
- ブロックごとに記述する
- ブロックには、接頭辞のブロックタイプ、次に続くラベルがある。

##### ブロックタイプ
```terraform:block_type
#ブロックタイプの種類：説明
locals #外部から変更できないローカル変数
variable #外部から変更可能な変数
terraform #Terraformの設定
provider #プロバイダ
data #Terraform管理していないリソースの取り込み
resource #Terraform管理対象となるリソース
output #外部から参照できるようにする値
```
##### locals(変数)ブロックの設定
- HCL2で利用可能な変数は下記の2種類
```terraform:locals.tf
locals   #ローカル変数。
         #プライベートな変数で外部外部から変更できない。
variable #外部から変更可能な変数。
         #コマンドライン実行時にオプションやファイル指定で上書きできる
```
```terraform:locals.tf
#localsブロックで定義して${local.変数名}で参照
locals {
    project = "tastylog"
    env = "dev"
}

resource <RESORCE_TYPE> <RESORCE_NAME> {
    tags = {
        Name = "${local.project}-${local.env}-vpc"
    }
}

output <OUTPUT_NAME>{
    ####
}
```
##### variableブロックの設定
```terraform:variable.tf
#variableブロックで定義して${var.変数名}で参照
variable "project" { #変数名
    type = string #変数の型
    default = "tastylog" #変数のデフォルト値
}

resource <RESORCE_TYPE> <RESORCE_NAME> {
    tags = {
        Name = "${var.project}-dev-vpc"
    }
}

output <OUTPUT_NAME>{
    ####
}
```

##### データ型
```terraform:data_type.tf
#データの型名：種類・説明
string #プリミティブ：Unicode文字列
number #プリミティブ：数値。整数と少数の両方を表現
bool #プリミティブ：true/falseの2値。
object({<NAME>=<TYPE>, ・・・}) #構造体：キーバリュー型データ
tuple([<TYPE>, ・・・]) #構造体：各列の型が決まっている配列。
list(<TYPE>) #コレクション：特定の型で構成される配列。
map(<TYPE>) #コレクション：キーが文字列の配列
set(<TYPE>) #コレクション：値の重複がない配列
```

- string
```terraform:string_type.tf
variable "message" {
    type = string 
    default = "Hello World"
}

variable "max_count" {
    type = number
    default = 10
}

variable "is_enable" {
    type = bool
    default = true
}
```
- object
キーバリュー形式で定義されるデータ型
```terraform:object_type.tf
variable "obj_sample" {
    type = object({
        name = string
        age = number
    })
    default = {
        name = "tanaka"
        age = 28
    }
}
```
- tuple
配列のN番目にどういった型を使うかが決められたデータ型
```terraform:tuple_type.tf
variable "tuple_sample" {
    type = tuple([
        string, number #基本は配列でN番目の型が固定される
    ])
    default = ["tanaka", 28]
}
```
- list
全て同じ型で指定される配列
```terraform:list.tf
variable "list_sample" {
    type = list(string)
    default = ["tanaka", "sato"]
}

username = var.list_sample[0]
```
- map
キーが文字列、バリューが指定された型となる配列
```terraform:map.tf
variable "map_sample" {
    type = map(string)
    default = {
        "High" = "m5.2xlarge"
        "Mid" = "m5.2large"
        "Low" = "t2.micro"
    }
}

instance = var.map_sample.High
```
- set
バリューの重複が排除される配列
```terraform:set.tf
variable "set_sample" {
    type = set(string)
    default = [
        "tanaka",
        "sato",
        "tanaka",
        "sato"
    ]
}

[for itm in var.set_sample : itm]
```

##### terraformブロックの設定
terraformとプロバイダー(AWS,GCP,Azure)は別物。
それぞれバージョンが存在し、日々変化していく。

- terradformのバージョン固定
```terraform:terraform_version_fix
required_version #Terraformのバージョン指定。main.tfに記載
```

- プロバイダーのバージョン固定
```terraform:provider_version_fix
required_providers #providerのバージョン指定。main.tfに記載
```

##### providerブロックの設定
```terraform:provider_block
profile #AWSへアクセスする為のプロファイル(クレデンシャル)
region #デフォルトリージョン
```

##### dataブロックの設定
dataブロックでは、管理対象外のリソース(「AWSが作成したリソース」「既存システムのリソース」「外だしの設定」)を参照させる。
取り込みたいリソースによって記述方法が異なる。

##### outputブロックの設定
作成済みのリソースを外部参照できるようにするブロック。

```terraform:output.tf
resource "aws_instance" "hello-world" {
    ami = "ami-0ce107...."
    instance_type = "t2.micro"
}

output "ec2_instance_id" { #参照時に指定する名前
    value = aws_instance.hello-world.id #出力する値
}
```

##### リソース参照
HCL2でリソース参照する記述
"."(ドット)で連結して参照
```terraform:resource.tf
<BLOCK_TYPE>.<LABEL_1>.<LABEL_2>
```
"."(ドット)連結で表現されるリソースパスを"アドレス"と呼ぶ。
resourceブロックの場合、省略可能。
```terraform:resource.tf
<LABEL_1>.<LABEL_2> #LABELから始まる
```

##### 組み込み関数

Hashicorp社HP → "Docs" → "Terraform Language" → "Functions"以下にリファレンスがある。

https://developer.hashicorp.com/terraform/language/functions

```terraform:function.tf
#種類：説明
numeric #数値演算、絶対値、切り上げ、切り捨て、最大、最小等。
string #文字列操作、フォーマット、結合、抜き出し等。
collection #配列操作、結合、分割、ソート等。
encoding #エンコード、Base64、CSV、JSON、YAMLなどの変換。
filesystem #ファイル操作。ディレクトリ名取得、ファイル読み取り等。
date & time #日付操作、現在時刻取得、データのフォーマット指定など。
Hash & Crypt #ハッシュおよび暗号化、ホスト名取得、サブネット名取得など。
IP Network #CIDR表記の演算、ホスト名取得、サブネット名取得など。
Type Conversion #型変換、bool、string、numberなどへの変換。
```
##### 組み込み関数の試し方
指定された設定で関数を実行します。

```terraform:funtion.tf
terraform console
[-var <KEY>=<VALUE>] #変数の指定
[-var-file <VAR_FILE>] #変数ファイルの指定
```
-----------------------------------------------------------------------
##### 複数リソースの作成
##### count
複数リソースを生成する方法として、count,for_eachの2つがある。countとfor_eachは同時に作成する事はできない。指定された数分のリソースを生成する。

```terraform:count.tf
resource <RESORCE_TYPE> <RESORCE_NAME> {
    count = 4
    
    ami = ""
    instance_type = ""    
    
    tags = {
        Name = "${count.index}"
    }
}
```

##### for_each
指定されたmapまたはsetを展開しながら複数リソース生成する

```terraform:for_each.tf
# mapの場合
resource "aws_vpc" "vpc" {
    cidr_block = "192.168.0.0/20"
}

resource "aws_subnet" "subnet" {
    for_each = { #キーバリュー形式で指定
        "192.168.1.0/24" = "ap-northeast-1a"
        "192.168.2.0/24" = "ap-northeast-1c"
        "192.168.3.0/24" = "ap-northeast-1d"
    }
    
    vpc_id = aws_vpc.vpc.id
    #each.keyまたｈeach.valueで値を利用
    cidr_block = each.key
    availability_zone = each.value
}
```
配列を指定したい場合、toset関数を利用してsetへ変換する
```terraform:toset.tf
# for_eachの場合
resource "aws_iam_user" "user" {
    for_each = toset([ #toset()でsetへ変換。重複削除
        "tanaka",
        "sato"
    ])
    
    name = each.key #利用可能なのはeach.keyのみ
}

```
##### for文
リスト型(list, map, set)を異なるリスト型(list, map)へ変換する構文
```terraform:for.tf
#list,set => list
[for s in var.list : upper(s)]

#list,set => map
{for s in var.list :s => upper(s)}

#map => list
[for k, v in var.map : upper(v)]

#map => map
{for k, v in var.list :k => upper(v)}
```

##### フィルター
```terraform:filter.tf
[for s in var.list upper(s) if s != ""]
```

##### 3項演算子
terraformにif文がないので、if文の代わりに使用する
```terraform:operator.tf
[var.a != "" ? var.a : "default_value"]
```

##### モジュール

モジュールとは、リソース生成処理を1つの塊にして呼び出せるようにしたもの(プログラミングにおける関数のようなもの)。関数というくらいなので、「呼び出し方法」と「定義方法」の2つがある。

- 定義方法
"modules"などの下に別フォルダ作成して定義する
ROOT
├ modules
├    └ MODULE NAME
└ main.tf

モジュールのディレクトリ内にファイルを作成していく。
MODULE NAME
├ variables.tf #引数
├ main.tf #処理
├ output.tf #戻り値
└ README.md #説明

- 呼び出し方法
"modules"ブロックを利用して呼び出す
ROOT
├ modules
├    └ nginx_server
└ main.tf

```terraform:module.tf
module "webserver" {
  source = "./modules/nginx_server"

  #モジュールが必要とする引数を列挙する
  instance_type = "t2.micro"
```

モジュールの出力(output)は"."(ドット)表現(アドレス表現)で参照
ROOT
├ modules
├    └ nginx_server
└ main.tf

```terraform:module.tf
module "webserver" {
  source = "./modules/nginx_server"

  instance_type = "t2.micro"
}
output "instance_id" {
  value = module.webserver.id
}
```