Terraformざっくりめも：環境構築、モジュール形式のTerraform定義、既存リソースのImport

業務でTerraformを数ヶ月使用しました。内容を忘れた時に見返す用としてメモします。
業務ではモジュールを使用したフォルダ構成を主に扱ったため、それらを中心に記載しています。
(Workspacesの記載はありません。)

#環境構築
##AWSCLIインストール

以下を参考にAWSCLIをインストールします。

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

##awsプロファイル設定
`~/.aws`フォルダに以下のような`credencal`,`config`ファイルを作成します。

```:credencal
[dev]
aws_access_key_id = アクセスキーを設定
aws_secret_access_key = シークレットアクセスキーを設定
```

通常は以下のように記述します。

```:config　(通常)
[dev]
output = json
region = ap-northeast-1
```

スイッチロールを使用する場合は以下のように記述します。

```:config　(スイッチロールの場合)
[dev]
output = json
region = ap-northeast-1

[profile dev_switchroll]
role_arn = arn:aws:iam::アカウントID:role/スイッチロール先で使用するロール名
source_profile = dev       <- スイッチ前のプロファイルを指定
region = ap-northeast-1
output = json
```

設定したプロファイルは名前を設定しているのでawsコマンドでは`--profile`オプション指定が必要です。

```bash:awsコマンド（--profile指定例）
$ aws sts get-caller-identity --profile dev
$ aws sts get-caller-identity --profile dev_switchroll
```

##terraformインストール
Terraformのインストールは、tfenvを使用するとバージョンを切り替えられるようになるので便利です。

https://github.com/tfutils/tfenv

```bash:tfenvインストール
# git リポジトリをクローンしてパスを通すだけ。
$ git clone https://github.com/tfutils/tfenv.git ~/.tfenv
$ echo 'export PATH="$HOME/.tfenv/bin:$PATH"' >> ~/.bash_profile

# 再ログインまたは.bash_profileを読み込み設定を反映すると完了。
$ source ~/.bash_profile
```

以下を実施してterraformコマンドを使用できるようにする。

```bash:Terraformコマンドインストール
# リモートにあるTerraformのバージョン確認
$ tfenv list-remote

# Terraformのインストール(latestまたはバージョンを指定)
$ tfenv install latest
$ tfenv install バージョン

# インストール済みのTerraformのバージョン確認
$ tfenv list

# Terraformのバージョン切り替え(latestまたはバージョン指定)
$ tfenv use latest
$ tfenv use バージョン

# Terraformのバージョン確認
$ terraform version
```

##stateファイル保存先作成
stateファイルの保存先はS3にするとチームで共有できるのでおすすめです。
後述する`terraform_settings.tf`内で保存先を指定します。指定しない場合、ローカルにstateファイルが作成されます。

以下はCloudFormationの簡単な例です。

```yaml:template.yml（s3バケット作成用CloudFormation）
AWSTemplateFormatVersion: '2010-09-09'
Description: 'create S3 Bucket'

Parameters:
  Env:
    Type: String
    AllowedValues:
      - none
      - dev
      - stg
      - prd
    Default: none
  Projectname:
    Type: String
    Default: 'sample'

Resources:
  StoreSourcesBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Join ['-', [!Ref Projectname, !Ref Env, 'tfstate', !Ref "AWS::AccountId" ]]
      # バージョニングしない場合はEnabledではなくSuspendedを指定する。
      VersioningConfiguration: {"Status": "Enabled"}

```

実行例

```bash:cloudformation実行例
$ ls
template.yml

$ aws cloudformation create-stack \
  --profile dev \
  --stack-name create-s3-bucket-for-test \
  --template-body file://./template.yml \
  --parameters ParameterKey=Env,ParameterValue=dev

```

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html


#Terraformよく使うコマンド
Terraformでよく使うコマンド一覧。

```bash:よく使うコマンド
terraform init            # 初期化
terraform plan            # 計画実行(定義内容の確認)
terraform apply           # 適用
terraform destroy         # リソースの廃棄（削除）
terraform fmt -recursive  # フォーマット
terraform state list      # リソースを一覧表示
terraform state mv        # リソースブロックなどの名前変更
terraform show            # リソース情報表示
terraform import          # 既存リソースからTerraform定義を作成する
```

この中でも`init`,`plan`,`apply`,`destroy`の使用頻度が高いです。

コマンドは上記以外にもあります。
詳細はドキュメントを参照してください。
(ちょっとわかりずらいですが、左メニューから各コマンドの詳細に飛べます。)

https://www.terraform.io/docs/cli/index.html


#作業の流れ
定義作成後のリソース作成は、以下のように初期化、確認、適用、不要になったら削除という流れで作業していました。

```bash:作業の流れ
# 初期化（プロファイルを指定する場合は -backend-config="profile=dev"を付ける）
$ terraform init

# 計画実行(定義内容の確認)
$ terraform plan

# 適用
$ terraform apply

# リソースの廃棄（削除）
$ terraform destroy
```

特定のリソース、モジュール、またはリソースのみ適用したり削除したい場合は、`-target`オプションを使用します。
詳しくは以下を参照してください。

https://learn.hashicorp.com/tutorials/terraform/resource-targeting

```bash:-targetオプションを使用したコマンド例
$ terraform plan -target="module.s3_bucket"
$ terraform plan -target={module.sample.aws_ecs_cluster.ecs_cluster,module.sample.aws_ecs_service.ecs_service}
```

定義作成についてはAWSマネジメントコンソールでリソースを一旦手動で作成し`terraform import`で定義化したり(後述します)、`サービス名 terraform`で検索してリソースのドキュメントを参照しつつ作成したりしていました。
以下のドキュメントの左メニューから対象のサービスを探すのもありです。

https://registry.terraform.io/providers/hashicorp/aws/latest/docs

#フォルダ構成
携わった業務ではモジュール形式でTerraformを使用することが多く、その際のフォルダ構成が以下のような形でした。
各tfファイルについては後述します。

```:フォルダ構成例
terraform-repository　gitリポジトリとかで管理。
　┗ sample        # システムや任意の単位でフォルダを作成(この単位でterraformを実行、stateファイルが作成される)
　　┗ env
　　　┗ dev         # applyの実行場所。環境単位でフォルダを作成。
　　　　┗ files
　　　　　┗ iam_role_policy_for_hoge.json    # IAM用のjsonファイルなど定義で使用するファイルを保存
　　　　┗ terraform_settings.tf              # Terraformのバージョンやプロバイダーのバージョンを指定
　　　　┗ provider.tf        # プロバイダーの情報を設定
　　　　┗ data.tf            # 既存のリソースの情報取得を定義
　　　　┗ variable.tf        # 変数を定義
　　　　┗ main.tf            # モジュール呼び出しを定義
　　　　┗ output.tf          # 作成したリソースをモジュール外などで使用するための出力を定義
　　┗ module
　　　┗ sample1              # モジュール単位でフォルダを作成
　　　　┗ iam.tf             # 各リソース作成用の定義を記入したtfファイルを配置
　　　　┗ ec2.tf
　　　　┗ output.tf
　　　┗ sample2
　　　　┗ sample_sub1        # さらに階層を増やしても良い(モジュール呼び出し時にsourceで指定する)
　　　　　┗ iam.tf
　　　　┗ sample_sub2
```

Terraformではplanやapply実行時に.tfファイルがすべて読み込まれるようです。
ファイル名やフォルダ名は自由に指定できます。(モジュールの.tfファイルはmain.tfで呼び出された時に読み込まれる)

１つのファイルにすべて記入なども可能ですが、わかりやすいように分けています。
同じモジュールをmain.tfから複数回呼ぶことができるため、設定値が違うリソースを複数作成する場合はモジュールを分けていたほうが便利だったりします。

あくまで例なので、terraform_settings.tfとprovider.tfを一緒にしたり、main.tfに変数定義を含めてvariable.tfを作成しないなどもありだと思います。


#env/dev/terraform_settings.tf
https://www.terraform.io/docs/language/settings/index.html

https://www.terraform.io/docs/language/settings/backends/s3.html

Terraformのバージョンやプロバイダーのバージョンを指定しています。
stateファイルの保存先も設定します。

```terraform:例：env/dev/terraform_settings.tf
terraform {
  required_version = "0.14.7"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "3.35.0"
    }
  }

  backend "s3" {
    bucket = "dev-terraform-state-xxxxxxxx"
    key    = "sample/terraform.tfstate"
    region = "ap-northeast-1"
  }
}
```

#env/dev/provider.tf
https://www.terraform.io/docs/language/providers/requirements.html

プロバイダーの情報を設定しています。
aliasを指定することで複数設定でき、リソースを作成するリージョンを変更する際に使用していました。

```terraform:例：env/dev/provider.tf
provider "aws" {
  region = "ap-northeast-1"
  profile = "dev"
}

provider "aws" {
  alias  = "useast1"
  region = "us-east-1"
  profile = "dev"
}
```

プロファイルはモジュール呼び出し時やリソース定義などで指定することができます。

```terraform:モジュール呼び出し時に指定
module "sample" {
  source = "../../module/sample1"

  env = var.env

  providers = {
    aws = aws.useast1
  }
}
```

resourceでも指定できます。
モジュール形式の場合、resourceで指定するとややこしくなるのであまりやらないほうがよさそうです。。

```terraform:リソース定義で指定
resource "aws_kinesis_firehose_delivery_stream" "firehose_sample" {
  provider = aws.useast1

略
```

データソースでも指定できます。

```terraform:データソースでの指定
data "aws_cloudformation_stack" "sample1_stack" {
  name     = "sample1-fucntion-stack"
  provider = aws.useast1
}
```

#env/dev/data.tf
https://www.terraform.io/docs/language/data-sources/index.html

既存のリソースのarnなどをTerraform定義で使用したい場合に定義します。
取得方法については各リソースのData Sourceのドキュメントを参照してください。
いくつか例を記載します。

```terraform:例：env/dev/data.tf
# state情報を取得
data "terraform_remote_state" "sample_state" {
  backend = "s3"

  config = {
    bucket  = "dev-terraform-state-xxxxxxxx"
    key     = "sample_state/terraform.tfstate"
    region  = "ap-northeast-1"
    profile = "dev"
  }
}

# VPCの情報を取得
data "aws_vpc" "main" {
  filter {
    name   = "tag:Name"
    values = ["main_vpc"]
  }
}

# サブネットIDs（fileterでワイルドカード「*」が指定できる）
data "aws_subnet_ids" "subnet_ids" {
  vpc_id = data.aws_vpc.main.id
  filter {
    name   = "tag:Name"
    # アベイラビリティーゾーンの文字識別子を「*」で指定し複数取得している
    values = ["dev-sample-ap-northeast-1*"]
  }
}

# Lambda関数の情報を取得
data "aws_lambda_function" "sample" {
  function_name = "dev-sample-function"
}

# Cloudformationのstackを取得
data "aws_cloudformation_stack" "sample_cloudformation_stack" {
  name = "sample_stack"
}
```

Lambda関数はTerraformで作成するよりもSAM([Serverless Application Model](https://aws.amazon.com/jp/serverless/sam/))で作成したほうが楽だったりするので、
SAMで作成したLambda関数をdataで取り込んで使用するなどしていました。

#env/dev/variable.tf
変数を設定しています。設定方法にはVariablesとLocal Valuesがあります。

##Variables

https://www.terraform.io/docs/language/values/variables.html

以下のように設定します。(ドキュメントから引用)

```terraform:例：env/dev/variable.tf（variable）
variable "image_id" {
  type = string
}

variable "availability_zone_names" {
  type    = list(string)
  default = ["us-west-1a"]
}

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

```terraform:変数の使用例（variable）
resource "aws_instance" "example" {
  instance_type = "t2.micro"
  ami           = var.image_id
}
```

変数値の指定しつつapply実行する場合、以下のようにします。

```bash:変数値の指定しつつapply実行（例）
$ terraform apply -var="image_id=ami-abc123"
```

##Local Values
https://www.terraform.io/docs/language/values/locals.html

以下のように設定します。(ドキュメントから引用)

```terraform:例：env/dev/variable.tf（locals）
locals {
  # Ids for multiple sets of EC2 instances, merged together
  instance_ids = concat(aws_instance.blue.*.id, aws_instance.green.*.id)
}

locals {
  # Common tags to be assigned to all resources
  common_tags = {
    Service = local.service_name
    Owner   = local.owner
  }
}
```

```terraform:変数の使用例（locals）
resource "aws_instance" "example" {
  # ...

  tags = local.common_tags
}
```

##variableとlocalsの使い分け
基本はLocal Valuesを使用して、`terraform apply -var`で値を変えられるようにしておきたい時にVariablesを使用するのが良さそうです。
参考：https://febc-yamamoto.hatenablog.jp/entry/2018/01/30/185416


#env/dev/main.tf
https://www.terraform.io/docs/language/modules/syntax.html

モジュールとmain.tfについてはひとまとめに以下の例で示します。
mian.tfには作成したモジュールを指定し、必要な変数の値を設定します。
以下、S3を作成するモジュールを作成し、main.tf呼び出している例です。
※terraform_settings.tfなどは省略

```:フォルダ構成
sample1
　┗env/dev/main.tf
　┗module/sample_s3/s3.tf
```

```terraform:env/dev/main.tf（S3を作成するモジュールを呼び出している）
module "sample" {
  # sourceにモジュールのパスを指定する。
  source = "../../module/sample_s3"

  # モジュールで設定した変数を設定する。
  bucket_name = "sample-s3-bucket"
  env         = "dev"
}
```

```terraform:module/sample_s3/s3.tf（S3を作成するモジュール）
resource "aws_s3_bucket" "s3_bucket" {
  bucket = var.bucket_name
  acl    = "private"

  tags = {
    env  = var.env
  }
}

variable "bucket_name" {}
variable "env" {}
```


#env/dev/output.tf
https://www.terraform.io/docs/language/values/outputs.html

作成したリソースをモジュール外などで使用したい場合に設定します。
以下のようにリソース種別.リソース名で設定します。
以下はenv/dev/main.tfの例で作成したモジュールをoutputした例です。

```terraform:例：sample1/module/sample_s3/output.tf
output "aws_s3_bucket" {
  value = aws_s3_bucket.s3_bucket
}
```

outputした内容は別のモジュール呼び出しで使用する場合は以下のようにします。

```terraform:sample1/env/dev/main.tf（モジュールでoutputした内容を使用する例）
module "sample" {
  # 略（env/dev/main.tfと同じ）
}

module "sample_use_module_output" {
  source = "../../module/sample"

  # outputした内容を使用する例
  sample_val = module.sample.aws_s3_bucket.arn
}
```

別state(※この言い方でいいのかは謎)で使用したい場合はさらにoutputします。

```terraform:sample1/env/dev/output.tf
output "sample_s3_bucket" {
  value = module.sample.aws_s3_bucket
}
```

別state情報をデータソースで取得して使用します。

```terraform:sample2/env/dev/main.tf
# state情報を取得
data "terraform_remote_state" "sample_state" {
  backend = "s3"

  config = {
    bucket  = "dev-terraform-state-xxxxxxxx"
    key     = "sample_state/terraform.tfstate"
    region  = "ap-northeast-1"
    profile = "dev"
  }
}

# state情報の内容を使用
module "sample" {
  source = "../../module/sample2"

  s3_bucket_name  = data.terraform_remote_state.sample_state.outputs.sample_s3_bucket.arn

　# ～～略～～
}
```

# 関数

Terraformでは便利な関数が用意されており、Terraform定義上で使用できます。
以下のドキュメントから参照してみてください。
(左のメニューのFunctionsから各関数の詳細が見れます)

https://www.terraform.io/docs/language/functions/index.html

いくつかよく使いそうな関数の例を紹介します。

##file
ファイルを読み込みます。
https://www.terraform.io/docs/language/functions/file.html

```terraform:関数使用例：file
file("${path.module}/files/hello.txt")
```

##templatefile
ファイルを読み込みます。さらにファイル内で変数を定義し、読み込む際に代入できます。
https://www.terraform.io/docs/language/functions/templatefile.html

```json:role_policy.json（templatefileから呼び出されるファイル）
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "${policy_principal}"
            },
            "Action": "s3:GetObject",
            "Resource": "${bucket_arn}/*"
        }
    ]
}
```

```terraform:関数使用例：templatefile
templatefile(
  "${path.module}/files/role_policy.json",
  {
    bucket_arn       = local.bucket_arn
    policy_principal = local.policy_principal
  }
)
```

##concat
複数のリストを結合して1つのリストにできます。
https://www.terraform.io/docs/language/functions/concat.html

```terraform:関数使用例：concat
concat(["a", ""], ["b", "c"])
# 結果
[
  "a",
  "",
  "b",
  "c",
]
```

##flatten
複数のネストされたリストを1つのリストにできます。
https://www.terraform.io/docs/language/functions/flatten.html

```terraform:関数使用例：flatten
flatten([["a", "b"], [], ["c"]])
# 結果
["a", "b", "c"]

flatten([[["a", "b"], []], ["c"]])
# 結果
["a", "b", "c"]
```

##path.module
file、templatefileで使用している`path.module`は配置されるモジュールのファイルシステムパスを指します。
https://www.terraform.io/docs/language/expressions/references.html

main.tfで`path.module`を使用している際のファイルの位置関係は以下のようになります。

```
sample
 ┗ env/dev
    ┗ main.tf
    ┗ files/hello.txt
```

output.tfで出力してみると`path.module`には「`.`」が入っていました。(他のpathなども一緒に出力してみた)

```
path_module         = "."
path_root           = "."
path_cwd            = "/home/username/terraform定義を保存しているパス/sample/env/dev"
terraform_workspace = "default"
```

# ダイナミックブロック(Dynamic Blocks)
https://www.terraform.io/docs/language/expressions/dynamic-blocks.html

ダイナミックブロックを使用すると一部リソースタイプなどにある繰り返し指定可能なブロックを、与えられたリストの要素数だけ繰り返し設定できます。
resource, data, provider, provisionerブロックの内部でサポートされています。
(ドキュメントには可読性や保守性が落ちる可能性があると記述されています。なので乱用厳禁)

繰り返し指定可能なブロックは以下のようなブロックです。
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/elastic_beanstalk_environment#example-with-options

```terraform:繰り返し指定可能なブロックの例。ドキュメントにあるsettingは複数指定できる。
resource "aws_elastic_beanstalk_environment" "tfenvtest" {
  name                = "tf-test-name"
  application         = aws_elastic_beanstalk_application.tftest.name
  solution_stack_name = "64bit Amazon Linux 2015.03 v2.0.3 running Go 1.4"

  setting {
    namespace = "aws:ec2:vpc"
    name      = "VPCId"
    value     = "vpc-xxxxxxxx"
  }

  setting {
    namespace = "aws:ec2:vpc"
    name      = "Subnets"
    value     = "subnet-xxxxxxxx"
  }
}
```

以下はCloudFrontでLambda@Edgeを設定している例です。
(ちょっと長いので該当箇所のみ抜粋)

```terraform:例：モジュール定義
resource "aws_cloudfront_distribution" "distribution" {

# ～～略～～

  default_cache_behavior {
  
  # ～～略～～

    # ダイナミックブロックで複数設定
    dynamic "lambda_function_association" {
      for_each = var.edgelambda_list
      content {
        event_type   = lambda_function_association.value.event_type
        lambda_arn   = lambda_function_association.value.lambda_arn
        include_body = false
      }
    }
  }
}

```

```terraform:例：呼び出し
module "cloudfront_sample" {
  source = "../../module/common/sample"

  # ～～略～～

  edgelambda_list = [
    {
      event_type = local.edgelambda_sample1_event_type
      lambda_arn = local.edgelambda_sample1_arn
    },
    {
      event_type = local.edgelambda_sample2_event_type
      lambda_arn = local.edgelambda_sample2_arn
    }
  ]

}
```

Lambda@Edgeを複数設定するのに使用しましたが、ダイナミックブロックを使用しなくてもリストで複数設定できるようです。
上記コードはあくまでもダイナミックブロックの使用例として参考にしてください。

```terraform:下記記事より引用。DynamicBlocksを使用しない場合の設定例(未検証)
lambda_function_associations = [
    {
      event_type = "viewer-request"
      // 注意: lambda_arnはpublishされたものしか使えない
      // またlambda_arnをterraformのinterpolation経由で持ってこようとするとうまくいかないのでベタがきしている
      lambda_arn = "arn:aws:lambda:us-east-1:${var.account_id}:function:cloudfront_s3_basic_auth_dev:2"
    },
  ]
```

https://tech.recruit-mp.co.jp/infrastructure/post-16386/

# 既存リソースのimport
以下のようにimport用のtfファイルを作成して実行します。
ECSクラスターをimportする例です。

```:フォルダ構成例
import_sample
 ┗import.tf    # 名前は適当
```

```terraform:例：import.tf
terraform {
  required_version = "0.13.5"
}

provider "aws" {
  region  = "ap-northeast-1"
  profile = "dev"
}

resource "aws_ecs_cluster" "ecs_cluster_sample" {}

```

```bash:例：terraform_import
# 初期化
$ terraform init -backend-config="profile=dev"

# terraform import リソース種別.名前 import対象(各リソース種別のドキュメントにあるImportを参照)
# ここでは既に作成されているsample_clusterという名前を指定しています。
$ terraform import aws_ecs_cluster.ecs_cluster_sample sample_cluster
```

`terraform import`実行後、`terrafrom show`を実行し内容を取得します。

```bash:例：terraform_show
$ terraform show
# aws_ecs_cluster.ecs_cluster_sample:
resource "aws_ecs_cluster" "ecs_cluster_sample" {
    arn                = "arn:aws:ecs:ap-northeast-1:xxxxxxxxxxxx:cluster/sample_cluster"
    capacity_providers = [
        "FARGATE",
        "FARGATE_SPOT",
    ]
    id                 = "arn:aws:ecs:ap-northeast-1:xxxxxxxxxxxx:cluster/sample_cluster"
    name               = "sample_cluster"
    tags               = {}
    tags_all           = {}

    setting {
        name  = "containerInsights"
        value = "enabled"
    }
}
```

取得した内容をもとに不要な部分(arnやid)などを削除したり、変数を定義します。

```terraform:完成例
resource "aws_ecs_cluster" "ecs_cluster_sample" {
    name               = var.ecs_cluster_sample_name

    setting {
        name  = "containerInsights"
        value = "enabled"
    }

    tags = {
        Name      = var.ecs_cluster_sample_name
        Env       = var.env
        Project   = var.project
        Terraform = "true"
    }
}

variable "ecs_cluster_sample_name" {}
```

上記をTerraform定義を作成したいリソースごとに実施します。

# おわり
TerraformよりもTerraform定義化する対象(AWSなど)の勉強が必要という印象が強いですね。。
今回は以上です。