【備忘録】Terraformの仕組みを整理する

#本記事の目的
社内でのクラウド検証に合わせて、Terraformの基礎知識を学び、アウトプットとして備忘録を記す。
本記事ではTerraformの基本操作、基本構文、及び基本的なAWSリソースの作成方法について説明する。

#主要コマンド
####1.terraform init
terraform実行に必要なバイナリファイルを準備する。
このコマンドを最初に実施する事で、他のterraformコマンドが実施できるようになる。

####2.terraform validate
設定ファイルが有効かどうかをチェックする。

####3.terraform plan
設定ファイルに従い、リソースの実行計画を出力する。

####4.terraform apply
設定ファイルに従い、リソースを作成・変更する。

####5.terraform destroy
設定ファイルに従って作成したリソースを削除する。


#設定ファイル
####1.tfファイル
terraformで作成・変更するリソースを定義したファイル。ユーザー自身が作成する。

####2.tfvarsファイル
tfファイルに読み込ませたい変数を定義したファイル。ユーザー自身が作成する。

####3.tfstateファイル
現時点でのterraformで作成・変更されたリソースの状態を記録したファイル。terraform applyコマンドを実行すると、自動的に作成・変更される。

#tfファイルにおける基本ブロック
####1.resource
作成・変更するリソースを定義する。

```resource.tf
resource "aws_instance" "web" {
  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"
}
```

####2.data
データソースを定義し、外部データの参照する。

```data.tf
data "aws_ami" "example" {
  most_recent = true

  owners = ["self"]
  tags = {
    Name   = "app-server"
    Tested = "true"
  }
}
```


####3.provider
リソースを作成するのに必要なクラウドプロバイダー、Saasプロバイダー、その他APIを定義する。

```provider.tf
provider "aws" {
  region = "ap-northeast-1"
}
```


####4.variable
グローバル変数を定義する。

```variable.tf
variable "image_id" {
  type = string
}

variable "availability_zone_names" {
  type    = list(string)
  default = ["ap-northeast-1"]
}

variable "docker_ports" {
  type = list(object({
    internal = number
    external = number
    protocol = string
  }))
  default = [
    {
      internal = 80
      external = 10080
      protocol = "tcp"
    }
  ]
}
```


####5.locals
ローカル変数を定義する。

```locals.tf
locals {
  service_name = "forum"
  owner        = "Community Team"
}
```

####6.output
出力値を定義し、terraform applyコマンド実行時に特定のデータを出力する。

```output.tf
output "instance_ip_addr" {
  value = aws_instance.server.private_ip
}
```



####7.module
モジュールを定義し、特定のディレクトリ配下の設定ファイルを利用する。

```module.tf
module "servers" {
  source = "./app-cluster"

  servers = 5
}
```

#代表的なAWSリソースの作成方法

####1.IAM


| 主要resource名 | 機能 |
| ---- | ---- |
| aws_iam_user | IAMユーザの作成　|
| aws_iam_user_login_profile | ログインプロファイルの作成 |
| aws_iam_access_key | アクセスキーの作成 |
| aws_iam_group | IAMグループの作成 |
| aws_iam_policy | 管理ポリシーの作成 |
| aws_iam_group_policy_attachment | IAMグループに対する管理ポリシーのアタッチ |
| aws_iam_user_group_membership | IAMグループへのIAMユーザの追加 |
| aws_iam_role | IAMロールの作成 |
| aws_iam_role_policy_attachment | IAMロールに対する管理ポリシーのアタッチ |

- 例①：IAMユーザ/ログインプロファイル/アクセスキーの作成(ログインパスワードとシークレットアクセスキーを出力)

```example_iam_user.tf
resource "aws_iam_user" "test-user" {
  name          = "taro"
  path          = "/"
}

resource "aws_iam_access_key" "test-user" {
  user    = aws_iam_user.test-user.name
  pgp_key = "keybase:some_person_that_exists"
}

resource "aws_iam_user_login_profile" "taro" {
  user    = aws_iam_user.test-user.name
  pgp_key = "keybase:some_person_that_exists"
}

output "secret" {
  value       = aws_iam_access_key.test-user.encrypted_secret
  description = "IAMユーザの暗号化されたパスワード"
}

output "password" {
  value       = aws_iam_user_login_profile.test-user.encrypted_password
  description = "IAMユーザの暗号化されたシークレットキー"
}
```

- 例②：IAMグループを作成、IAMユーザを作成しIAMグループに追加、管理ポリシーを作成しIAMグループにアタッチ

```example_iam_group.tf
resource "aws_iam_user_group_membership" "test-membership" {
  user = aws_iam_user.test-user.name

  groups = [
    aws_iam_group.test-group.name,
  ]
}

resource "aws_iam_user" "test-user" {
  name = "jiro"
}

resource "aws_iam_group" "test-group" {
  name = "engineer"
}

resource "aws_iam_policy" "test-policy" {
  name        = "EC2 describe policy"
  description = "A test policy"
  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "ec2:Describe*"
      ],
      "Effect": "Allow",
      "Resource": "*"
    }
  ]
}
EOF
}

resource "aws_iam_group_policy_attachment" "test-attach" {
  group      = aws_iam_group.test-group.name
  policy_arn = aws_iam_policy.test-policy.arn
}
```

- 例③：IAMロールを作成、管理ポリシーを作成しIAMロールにアタッチ

```example_iam_role.tf
resource "aws_iam_role" "test-role" {
  name = "test-role"
}

resource "aws_iam_policy" "test-policy" {
  name        = "EC2 describe policy"
  description = "A test policy"

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "ec2:Describe*"
      ],
      "Effect": "Allow",
      "Resource": "*"
    }
  ]
}
EOF
}

resource "aws_iam_role_policy_attachment" "test-attach" {
  role       = aws_iam_role.test-role.name
  policy_arn = aws_iam_policy.test-policy.arn
}
```


####2.EC2/VPC
| 主要resource名 | 機能 |
| ---- | ---- |
| aws_instance | EC2インスタンスの作成 |
| aws_key_pair | キーペアの作成 |
| aws_vpc | VPCの作成　|
| aws_subnet | サブネットの作成 |
| aws_internet_gateway | インターネットゲートウェイの作成 |
| aws_route_table | ルーティングテーブルの作成 |
| aws_route | ルーティングテーブルに対するルートの追加 |
| aws_route_table_association | ルーティングテーブルとサブネットの関連付け |
| aws_security_group | セキュリティグループの作成 |
| aws_security_group_rule | セキュリティグループに対するルールの追加 |


- 例①：VPC作成、パブリックサブネット作成、EC2インスタンス起動(パブリックIPを出力)

```example_instance.tf
resource "aws_instance" "test-instance" {
  ami                    = "ami-0fccdb46e227b9538" # ap-northeast-1
  instance_type          = "t2.micro"
  subnet_id              = aws_subnet.test-public-subnet.id
  vpc_security_group_ids = [aws_security_group.test-sg.id]
  key_name               = aws_key_pair.test-key.id
}

resource "aws_key_pair" "test-key" {
  key_name   = "test-key"
  public_key = file("./tf-test.pub") # `ssh-keygen`コマンドで作成した公開鍵を指定
}

resource "aws_vpc" "test-vpc" {
  cidr_block = "10.0.0.0/16"
  enable_dns_support   = true　# DNS解決有効化
  enable_dns_hostnames = true　# DNSホスト名有効化

  tags = {
    Name = "my-vpc"
  }
}

resource "aws_subnet" "test-public-subnet" {
  vpc_id            = aws_vpc.test-vpc.id
  cidr_block        = "10.0.10.0/24"
  map_public_ip_on_launch = true　#インスタンス起動時におけるパブリックIPアドレスの自動割り当ての有効化

  tags = {
    Name = "my-public-subnet"
  }
}

resource "aws_internet_gateway" "test-ingw" {
  vpc_id = aws_vpc.test-vpc.id

  tags = {
    Name = "my-ingw"
  }
}

resource "aws_route_table" "test-public-rt" {
  vpc_id = aws_vpc.test-vpc.id
  tags = {
    Name = "my-public-route-table"
  }
}


resource "aws_route" "test-public-route" {
  route_table_id         = aws_route_table.test-public-rt.id
  gateway_id             = aws_internet_gateway.test-ingw.id
  destination_cidr_block = "0.0.0.0/0"
}

resource "aws_route_table_association" "test-public-subrt" {
  subnet_id      = aws_subnet.test-public-subnet.id
  route_table_id = aws_route_table.test-public-route.id
}

resource "aws_security_group" "test-sg" {
  name        = "test-sg"
  vpc_id      = aws_vpc.test-vpc.id

  tags = {
    Name = "test-sg"
  }
}

resource "aws_security_group_rule" "in_ssh" {
  security_group_id = aws_security_group.test-sg.id
  type              = "ingress"
  cidr_blocks       = ["0.0.0.0/0"]
  from_port         = 22
  to_port           = 22
  protocol          = "tcp"
}

resource "aws_security_group_rule" "in_icmp" {
  security_group_id = aws_security_group.test-sg.id
  type              = "ingress" #インバウンドルール
  cidr_blocks       = ["0.0.0.0/0"]
  from_port         = -1
  to_port           = -1
  protocol          = "icmp"
}

resource "aws_security_group_rule" "out_all" {
  security_group_id = aws_security_group.test-sg.id
  type              = "egress" #アウトバウンドルール
  cidr_blocks       = ["0.0.0.0/0"]
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
}

output "public_ip" {
  value       = aws_instance.test-instance.public_ip
  description = "EC2インスタンスのパプリックIP"
}
```

####3.S3
| 主要resource名 | 機能 |
| ---- | ---- |
| aws_s3_bucket | S3バケットの作成　|
| aws_s3_bucket_object | オブジェクトのアップロード |


- 例①：S3バケット作成、バージョニング有効化、暗号化

```example_s3.tf
resource "aws_s3_bucket" "test-bucket" {
  bucket = "s3-test-bucket"
  acl    = "private"

  versioning {
    enabled = true
  } # バージョニング

  server_side_encryption_configuration {
    rule {
      apply_server_side_encryption_by_default {
        sse_algorithm     = "AES256"
      }
    } # 暗号化
  }

  tags = {
    Name        = "My bucket"
  }
}
```

- 例②：S3バケット作成、静的Webサイトホスティング有効化、オブジェクトアップロード

```example_s3_website.tf
resource "aws_s3_bucket" "test-website-bucket" {
  bucket = "s3-website-test.com"
  acl    = "public-read" #静的Webサイトホスティング有効化

  website {
    index_document = "index.html"
    error_document = "error.html"
  }

  tags = {
    Name        = "My website bucket"
  }
}

resource "aws_s3_bucket_object" "test-index" {
  bucket = aws_s3_bucket.test-bucket.id
  key    = "index.html"
  source = "/work/index.html" # 作成した`index.html`を指定
}

resource "aws_s3_bucket_object" "test-error" {
  bucket = aws_s3_bucket.test-bucket.id
  key    = "error.html"
  source = "/work/error.html" # 作成した`error.html`を指定
}

```

#感想
Terraformに限らないが、IaCツールは同じインフラ構成を再現する際には非常に有用であるものの、既存のインフラ構成を変化させる場合に柔軟に対応しにくいという欠点も存在する。

そのためインフラの自動化を追求する場合は、変化が少ないインフラ構成は再現性の高いIaCツールで実現し、変化が多いインフラ構成は柔軟性の高いCLIツールで都度実現するというように、インフラ構成に応じて使い分ける方が重要であると考える。

今後もIaCとCLI両面で、インフラ自動化について探求してみたい。
(ちなみに「代表的なAWSリソースの作成方法」の章については、もう少し別のAWSサービスのtfファイルもまとめてみようか検討中・・・)


#参考文献
- 公式サイト：[Terraform by HashiCorp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs)
- 書籍：[実践Terraform　AWSにおけるシステム設計とベストプラクティス](https://www.amazon.co.jp/dp/B07XT7LJLC)
- Udemy：[米シリコンバレーDevOps監修！超Terraform完全入門(0.14) + AWS DevOps IaCをマスター！](https://www.udemy.com/course/hashicorp-terraform-aws-devops-iac/)