Terraform ハンズオン資料

部署内ハンズオンで使った資料です。せっかくなので公開してみました。

## Terraform とは

HashiCorp社製の様々なクラウドサービスをコードで管理するためのツールです。 
インフラなどの設定をコード化することで、共同作業や使い回しやバージョン管理などが容易になります。 
dry run やリソースの依存性の可視化など便利な機能も多く、いわゆる Infrastructure as Code を実現するために用いられます。

[Terraform by HashiCorp](https://www.terraform.io/)


## TL;DR

`.tf` ファイルにインフラの設定を記述し、コマンドを実行するとその通りにインフラを構築してくれるツールです。


## 環境構築

### Terraform用のIAMユーザーの作成

Terraform用のIAMユーザーを作成します。以下URLを参考にしながらIAMユーザーを作成します。 
今回はハンズオンなので、Admin権限を持つユーザーを作成します。
SecretAccessKey は再発行されないので、無くしてしまったらアカウントを作り直してください。

[IAM ユーザーの作成 (コンソール)](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_users_create.html)


### AWS CLI のインストール

AWS CLI をインストールし、設定を行います。インストール方法は下記 URL を参考にしてください

[macOS に AWS CLI をインストールする](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/install-macos.html)
[AWS CLI の設定](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-configure.html)

インストールが完了したら AWS CLI の設定を行い、AWS CLI が実行できることを確認します。

```
$ aws --version
$ aws configure
$ aws s3api list-buckets  # S3 のバケット一覧を表示するコマンド
```


### tfenv を介してインストール

Terraform は更新が早く、~~バグもまだまだ多いため~~、気軽にバージョンを変えられるように `tfenv` を使うことを強く勧めます。

```
$ brew install tfenv
$ tfenv -v
$ tfenv list-remote     # インストール可能なバージョンをリモートから取得して列挙します。
$ tfenv install 0.12.10 # 0.12.10 をインストール
$ tfenv use 0.12.10     # 0.12.10　を使用する
$ tfenv list            # インストール済み
$ terraform --version   # バージョン一覧
```


`.terraform-version` ファイルを作ることで、`.ruby-version` のようにバージョン管理なども可能です。

```
$ touch .terraform-version
$ tfenv use 0.12.10       # このディレクトリ以下で 0.12.10　を使用する
```


## HelloWorld

HelloWorld として S3バケットを Terraform で作ります。

```
$ mkdir handson
$ cd handson
$ touch main.tf
```

`main.tf` ファイルを作成し、以下のように記述します。
`tekitouna.domain.nyuryoku` の箇所はS3バケット名になるので、urlに使える文字列 かつ グローバルでユニークな任意の値を入れてください。　


```Terraform:main.tf
# Provider
provider "aws" {
  profile = "default"
  region  = "ap-northeast-1"
}

# Resource
resource "aws_s3_bucket" "tekitouna_domain_nyuryoku" {
 bucket = "tekitouna.domain.nyuryoku"
}
```

`terraform init` コマンドでAWSのプロバイダーをダウンロードし、`terraform plan` コマンドで `main.tf` に新しく作成されるリソースを確認します。


```
$ terraform init
$ terraform plan
```

以下のように新しく作成されるリソースの情報が表示されます。
`tekitouna.domain.nyuryoku` という名前の新しいバケットが作成されることがわかります。

``` 
Terraform will perform the following actions:

  # aws_s3_bucket.tekitouna_domain_nyuryoku will be created
  + resource "aws_s3_bucket" "tekitouna_domain_nyuryoku" {
      + acceleration_status         = (known after apply)
      + acl                         = "private"
      + arn                         = (known after apply)
      + bucket                      = "tekitouna.domain.nyuryoku"
      + bucket_domain_name          = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + versioning {
          + enabled    = (known after apply)
          + mfa_delete = (known after apply)
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.
```

問題なければ `terraform apply`コマンドを実行して、S3バケットを作成します。

```
$ terraform apply
```

新しくS3バケットが作成されました。
以下のコマンドを実行すると新しくバケットが作られていることがわかります。

```
aws s3api list-buckets | grep tekitouna.domain.nyuryoku
```

Applyに成功すると `terraform.tfstate` が生成されます。
`terraform.tfstate` にはTerraformでは管理しているインフラの状態が記述されています。
ハンズオンなので詳しい説明はスキップしますが、複数人での開発では`tfstate` ファイルを S3 などに置くことをお勧めします。

[TerraformでtfstateファイルをS3で管理する](https://qiita.com/tsukakei/items/2751e245e38c814225f1)


```json:terraform.tfstate
{
  "version": 4,
  "terraform_version": "0.12.10",
  "serial": 1,
  "lineage": "6a7bf262-a78d-decf-166f-9f1eac238641",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "aws_s3_bucket",
      "name": "tekitouna_domain_nyuryoku",
      "provider": "provider.aws",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "acceleration_status": "",
            "acl": "private",
            "arn": "arn:aws:s3:::tekitouna.domain.nyuryoku.xxxx",
            "bucket": "tekitouna.domain.nyuryoku.xxxx",
            "bucket_domain_name": "tekitouna.domain.nyuryoku.xxxx.s3.amazonaws.com",
            "bucket_prefix": null,
            "bucket_regional_domain_name": "tekitouna.domain.nyuryoku.xxxx.s3.ap-northeast-1.amazonaws.com",
            "cors_rule": [],
            "force_destroy": false,
            "hosted_zone_id": "Z2M4EHUR26P7ZW",
            "id": "tekitouna.domain.nyuryoku.xxxx",
            "lifecycle_rule": [],
            "logging": [],
            "object_lock_configuration": [],
            "policy": null,
            "region": "ap-northeast-1",
            "replication_configuration": [],
            "request_payer": "BucketOwner",
            "server_side_encryption_configuration": [],
            "tags": null,
            "versioning": [
              {
                "enabled": false,
                "mfa_delete": false
              }
            ],
            "website": [],
            "website_domain": null,
            "website_endpoint": null
          },
          "private": "bnVsbA=="
        }
      ]
    }
  ]
}
```

次に作成したバケットを削除します。

```
$ terraform destroy
```

削除されるリソースの内容が表示されます。

```
Terraform will perform the following actions:

  # aws_s3_bucket.tekitouna_domain_nyuryoku will be destroyed
  - resource "aws_s3_bucket" "tekitouna_domain_nyuryoku" {
      - acl                         = "private" -> null
      - arn                         = "arn:aws:s3:::tekitouna.domain.nyuryoku.xxxx" -> null
      - bucket                      = "tekitouna.domain.nyuryoku.xxxx" -> null
      - bucket_domain_name          = "tekitouna.domain.nyuryoku.xxxx.s3.amazonaws.com" -> null
      - bucket_regional_domain_name = "tekitouna.domain.nyuryoku.xxxx.s3.ap-northeast-1.amazonaws.com" -> null
      - force_destroy               = false -> null
      - hosted_zone_id              = "Z2M4EHUR26P7ZW" -> null
      - id                          = "tekitouna.domain.nyuryoku.xxxx" -> null
      - region                      = "ap-northeast-1" -> null
      - request_payer               = "BucketOwner" -> null
      - tags                        = {} -> null

      - versioning {
          - enabled    = false -> null
          - mfa_delete = false -> null
        }
    }

Plan: 0 to add, 0 to change, 1 to destroy.
```

変更内容に問題がなければ実行します。
これで Terraform による作成と削除はできました。


## コンソールから作ったリソースを Terraform に import してみる

以下の画像の手順を参考に、Webサーバーとして公開されているS3バケットを作成します。

![page1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/e66e7801-559d-dc4a-2930-f2c68c1f7992.png)
![page2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/15fbcd25-08bf-e96c-98be-1f21a2ef9c5b.png)
![page3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/13293c23-ab32-5f61-bbbb-df1cd5c2e15c.png)
![page4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/ffac5428-5ac9-b2bc-d53f-75a550687efc.png)

バケットポリシーを設定します。

![part5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/b23d4a54-2e0f-4ce5-857c-69912443c50e.png)

```
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadForGetBucketObjects",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::tekitouna.domain.nyuryoku/*"
    }
  ]
}
```


ポリシーの設定が完了したら、`index.html`, `error.html` という名前で html ファイルを作成し、バケットにアップデートします。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>タイトル</title>
    </head>
    <body>
        <h1>タイトル</h1>
        <p>Amazon S3を使って静的なウェブページを公開してみた</p>
    </body>
</html>
```

```html:error.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>Error</title>
    </head>
    <body>
        <h1>Error</h1>
        <p>エラーページです</p>
    </body>
</html>
```

しばらく待ってから、S3のファイルのURLにアクセスし、ファイルが表示されることを確認します。

[http://tekitouna.domain.nyuryoku.s3-website-ap-northeast-1.amazonaws.com/index.html](http://tekitouna.domain.nyuryoku.s3-website-ap-northeast-1.amazonaws.com/index.html)
[http://tekitouna.domain.nyuryoku.s3-website-ap-northeast-1.amazonaws.com/error.html](http://tekitouna.domain.nyuryoku.s3-website-ap-northeast-1.amazonaws.com/error.html)

次に作成したS3バケットをTerraform で管理してみようと思います。
以下のコマンドで 作成した S3バケットを Terraform の管理下に取り込みます。

```
$ terraform import aws_s3_bucket.tekitouna_domain_nyuryoku tekitouna.domain.nyuryoku
```

`plan` コマンドで、`main.tf` ファイルと手動で作成したS3バケットとの差分を確認します。

```
$ terraform plan
```

差分を埋める -> `terraform plan` -> 差分を埋める 作業を繰り返します。
以下の差分を消すことができなかったので、無視しました。

```
+ acl                         = "none"
+ force_destroy               = true
```

完成したのがこちらです。
先ほどの手作業をコード化することができました。

```
resource "aws_s3_bucket" "tekitouna_domain_nyuryoku" {
 bucket = "tekitouna.domain.nyuryoku"
 website {
    index_document = "index.html"
  }
}

resource "aws_s3_bucket_policy" "tekitouna_domain_nyuryoku" {
  bucket = aws_s3_bucket.tekitouna_domain_nyuryoku.id
  policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadForGetBucketObjects",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::tekitouna.domain.nyuryoku/*"
    }
  ]
}
POLICY
}
```

`main.tf` と S3バケットとの差分がなくなりました。


## [おまけ]機能を追加してみる

前項までにWebサーバーとして公開されているS3バケットを作成しました。
しかしファイルが存在しないページにアクセスすると `Access Denied`エラーが表示されてしまいます。
ファイルが存在しないページにアクセスしようとしたらエラーページにリダイレクトするように設定します。

リダイレクトの仕組みはS3に用意されています。
以下サイトを参考にコンソールからの設定を行います。

[S3の静的webサイトで404時に別のドメインに飛ばす時](http://farewell-work.hatenablog.com/entry/2017/08/09/202543)

設定後、`terraform plan` を実行し現在の `main.tf` ファイルとS3バケットの差分を確認します。

```
  # aws_s3_bucket.tekitouna_domain_nyuryoku will be updated in-place
  ~ resource "aws_s3_bucket" "tekitouna_domain_nyuryoku" {
        acl                         = "private"
        arn                         = "arn:aws:s3:::tekitouna.domain.nyuryoku"
        bucket                      = "tekitouna.domain.nyuryoku"
        bucket_domain_name          = "tekitouna.domain.nyuryoku.s3.amazonaws.com"
        bucket_regional_domain_name = "tekitouna.domain.nyuryoku.s3.ap-northeast-1.amazonaws.com"
        force_destroy               = false
        hosted_zone_id              = "Z2M4EHUR26P7ZW"
        id                          = "tekitouna.domain.nyuryoku"
        region                      = "ap-northeast-1"
        request_payer               = "BucketOwner"
        tags                        = {}
        website_domain              = "s3-website-ap-northeast-1.amazonaws.com"
        website_endpoint            = "tekitouna.domain.nyuryoku.s3-website-ap-northeast-1.amazonaws.com"

        versioning {
            enabled    = false
            mfa_delete = false
        }

      ~ website {
            error_document = "error.html"
            index_document = "index.html"
          - routing_rules  = jsonencode(
                [
                  - {
                      - Condition = {
                          - HttpErrorCodeReturnedEquals = "404"
                        }
                      - Redirect  = {
                          - ReplaceKeyPrefixWith = "error.html?referer="
                        }
                    },
                ]
            ) -> null
        }
    }

Plan: 0 to add, 1 to change, 0 to destroy.
```

表示された差分を埋めるため、以下のように記述し、`terraform plan` を実行しても差分が出ないことを確認します。


```Terraform:main.tf

resource "aws_s3_bucket" "tekitouna_domain_nyuryoku" {
  bucket = "tekitouna.domain.nyuryoku"
  website {
    error_document = "error.html"
    index_document = "index.html"

    routing_rules = <<EOF
[
  {
    "Condition": {
        "HttpErrorCodeReturnedEquals": "404"
    },
    "Redirect": {
        "ReplaceKeyPrefixWith": "error.html?referer="
    }
  }
]
EOF
  }
}

resource "aws_s3_bucket_policy" "tekitouna_domain_nyuryoku" {
  bucket = aws_s3_bucket.tekitouna_domain_nyuryoku.id
  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadForGetBucketObjects",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::tekitouna.domain.nyuryoku/*"
    }
  ]
}
EOF
}
```


## お掃除

`terraform destroy` コマンドを実行することで、`.tfstate` で管理されているリソースを全て削除することができます。


## 参考

https://www.terraform.io/docs/providers/aws/r/s3_bucket.html