Infrastructure as CodeのTips【Terraform/CDK/Cloudformation】

# TL; DR
普段色々なツールを使ってインフラコード化を細々としています。
自分の中で少し溜まってきたので整理と備忘録も兼ねて書いていきます。

# Terraform
みんな大好きTerraformです。筆者も大好きです。
なんとなくで書けちゃうのがいいんですよね。とりあえず`plan`投げてエラー見て直してみたいな。
Terraformのおかげでエラー出ても焦らなくなりました。(`init`で出るとちょっと自分にムッとはしますがw)
多機能な分、追えていなかったり避けている機能がちょこちょこあったので使ってみるか〜くらいでやったのですがよかったものをピックアップしてます。

### for_each
名前から繰り返しの処理をするやつなんだろうな、と思いつつ`count`で避けてきた`for_each`くん。
慣れてしまえば多用しそうになるやつです。
`aws_subnet`とかで使ってます。便利です。
`resource`ブロックでしか利用できないので注意です。

```golang:count&nbsp;style
resource "aws_subnet" "public_subnet" {
  count                   = length(split(",",lookup(var.availability_zones,default)))  // 2とかでもOK
  vpc_id                  = aws_vpc.service_vpc.id
  cidr_block              = cidrsubnet(aws_vpc.service_vpc.cidr_block, 4, count.index)
  availability_zone       = element(split(",", lookup(var.availability_zones, default)), count.index)
  map_public_ip_on_launch = true
}

variable "availability_zones" {
  default = "ap-northeast-1a,ap-northeast-1c"
}
```

```go:for_each&nbsp;style
resource "aws_subnet" "public_subnet" {
  for_each                = var.availability_zones
  vpc_id                  = aws_vpc.service_vpc.id
  cidr_block              = cidrsubnet(aws_vpc.service_vpc.cidr_block, 4, each.value)
  availability_zone       = each.key
  map_public_ip_on_launch = true
}

variable "availability_zones" {
  default = {
    "ap-northeast-1a" = 0
    "ap-northeast-1c" = 1
  }
}
```
こんな感じで違った書き方ができます。
なんとなくTerraformの`Functions`が苦手な筆者はこっちのがシンプルやんけ！と書けたときに小躍りしました。
リソースのインデックスもわかりやすいのでプラン結果やデプロイ中も見守りやすいのも理由です。

```go:plan&nbsp;result
  # module.service_vpc.aws_subnet.public_subnet["ap-northeast-1a"] will be created
  + resource "aws_subnet" "public_subnet" {
      + arn                             = (known after apply)
      + assign_ipv6_address_on_creation = false
      + availability_zone               = "ap-northeast-1a"
      + availability_zone_id            = (known after apply)
      + cidr_block                      = "192.168.0.0/20"
      + id                              = (known after apply)
      + ipv6_cidr_block                 = (known after apply)
      + ipv6_cidr_block_association_id  = (known after apply)
      + map_public_ip_on_launch         = true
      + owner_id                        = (known after apply)
      + tags                            = {
          + "ENV"     = "sandbox"
          + "Name"    = "terraform-sand-public-ap-northeast-1a"
          + "Service" = "terraform"
        }
      + vpc_id                          = (known after apply)
    }

  # module.service_vpc.aws_subnet.public_subnet["ap-northeast-1c"] will be created
  + resource "aws_subnet" "public_subnet" {
      + arn                             = (known after apply)
      + assign_ipv6_address_on_creation = false
      + availability_zone               = "ap-northeast-1c"
      + availability_zone_id            = (known after apply)
      + cidr_block                      = "192.168.16.0/20"
      + id                              = (known after apply)
      + ipv6_cidr_block                 = (known after apply)
      + ipv6_cidr_block_association_id  = (known after apply)
      + map_public_ip_on_launch         = true
      + owner_id                        = (known after apply)
      + tags                            = {
          + "ENV"     = "sandbox"
          + "Name"    = "terraform-sand-public-ap-northeast-1c"
          + "Service" = "terraform"
        }
      + vpc_id                          = (known after apply)
    }
```
これらのリソースをアウトプットするときは少し工夫が必要で`for`文で回してあげる必要があります。

```go:output.tf
output "public_subnets" {
  value       = [for subnet in aws_subnet.public_subnet : subnet.id]  // 欲しいattributeは変数から取る
}

// NG例
output "public_subnets" {
  value       = aws_subnet.public_subnet.*.id
}

output "public_subnets" {
  value       = aws_subnet.public_subnet[*].id
}
```

### dynamic block
複数設定可能な項目などは繰り返し書くのが面倒だったり、環境によって必要・不要が分かれていたりすることもあると思います。
そう言った部分を良きに計らってくれる優れものです。個人的には使いすぎはよくないとは思うので`aws_db_parameter_group`なんかはちょうどいいのかな？と思います。

```go:multiple&nbsp;written
resource "aws_db_parameter_group" "mysql_56" {
  name   = "${var.service_name}-${var.env}-${var.engine}-56"
  family = "mysql5.6"

  parameter {
    name         = "character_set_server"
    value        = "utf8"
    apply_method = "pending-reboot"
  }

  parameter {
    name         = "character_set_client"
    value        = "utf8"
    apply_method = "immediate"
  }

  parameter {
    name         = "performance_schema"
    value        = "1"
    apply_method = "pending-reboot"
  }
}
```

```go:dynamic&nbsp;written
resource "aws_db_parameter_group" "mysql_56" {
  name   = "${var.service_name}-${var.env}-${var.engine}-56"
  family = "mysql5.6"

  dynamic "parameter" {
    for_each = var.db_params

    content {
      name         = each.value.name
      value        = each.value.value
      apply_method = each.value.apply_method
    }
}

variable db_params {
  default = {
    "server_character" = {
        name         = "character_set_server"
        value        = "utf8"
        apply_method = "pending-reboot"
    },
    "client_character" = {
        name         = "character_set_client"
        value        = "utf8"
        apply_method = "immediate"
    },
    "insight" = {
        name         = "performance_schema"
        value        = "1"
        apply_method = "pending-reboot"
    }
  }
}
```
いや、変数冗長じゃね？と思うかと思いますが、`module`などを利用して環境ごとに出し分けているとこういう`count`とかが使えないものが出てきてつらみに陥ることがあるんです…。それが`resource`同じで`module`側だけ書き換えるみたいな運用ならまあ許容できるかなと思ってます。(そもそもそんな環境構成にするなよ、みたいなのはまた別のお話しです。)

# AWS CDK
最近は細々としたリソースをつくるときに利用したりしています。
ある程度まとまったリソース単位でStackとして利用できるので中々好きです！AWS公式ですし、Typescriptの型の恩恵受けまくりで書きやすいしエラーの発見も簡単です。
新しいリソースへの対応も早いので、ローンチされたばかりのサービスでもガンガン使えちゃいます！(AWS-SDK-JSとかからインポートして使う、が大体ですが。)

### cdk.json
`cdk init`すると自動で作成されるものですが、うまく使うと便利です！
以前[IAMを管理している記事](https://qiita.com/KsntsTt/items/9930a6c032ce4afa0985)を書いたものをちょこちょことアップグレードしていて、今は`cdk.json`を使ってユーザーやロールの出し分けをしています。

旧バージョンのユーザー作成の部分だけ抜き出しています。
ユーザーはベタがきでコードにそのまま書いています。

```typescript:old&nbsp;style
import { Construct, Stack, StackProps }  from '@aws-cdk/core';
import { Group, Policy, PolicyStatement, ManagedPolicy, User } from '@aws-cdk/aws-iam';
import AWS = require('aws-sdk');

const admins = 'testAdminGroup';
const adminUsers = [
  'demo01',
];

const developers = 'testDevGroup';
const devUsers = [
  'demo02',
  'demo03'
];

export class IamUserStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // Admin group
    const adminGroup = new Group(this, admins, { groupName: admins });
    adminGroup.addManagedPolicy(adminPolicy);
    adminGroup.attachInlinePolicy(commonPolicy);

    // Developer group
    const devGroup = new Group(this, developers, { groupName: developers });
    devGroup.addManagedPolicy(powerUserPolicy);
    devGroup.attachInlinePolicy(devPolicy);
    devGroup.attachInlinePolicy(commonPolicy);

    // Create users
    adminUsers.forEach(adminUser => {
      new User(this, adminUser, {
        userName: adminUser,
        groups: [adminGroup],
      });
    });

    devUsers.forEach(devUser => {
      new User(this, devUser, {
        userName: devUser,
        groups: [devGroup]
      });
    });
  }
}
```
`cdk.json`を利用しているパターンはこちら。
5-14行目で`cdk.json`をcontextとして利用できるように呼び出しています。

```typescript:using&nbsp;cdk.json
import { Aws, Construct, Stack, StackProps, App, CfnOutput, Fn, Arn }  from '@aws-cdk/core';
import { Group, Policy, PolicyStatement, ManagedPolicy, User } from '@aws-cdk/aws-iam';
import AWS = require('aws-sdk');

const app = new App();
const stage: any = app.node.tryGetContext("stage")

// Admin Users Configure
const admins: string = 'adminGroup';
const adminUsers = app.node.tryGetContext(stage).adminUsers  // contextとしてユーザーをcdk.jsonから呼び出している。

// Develop Users Configure
const developers: string = 'devGroup';
const devUsers = app.node.tryGetContext(stage).devUsers

export class IamUserStack extends Stack {
  /** @returns the ARN of the IAM User */
  public readonly userArn: string;

  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // Admin group
    const adminGroup = new Group(this, admins, { groupName: admins });
    adminGroup.addManagedPolicy(adminPolicy);
    adminGroup.attachInlinePolicy(commonPolicy);

    // Developer group
    const devGroup = new Group(this, developers, { groupName: developers });
    devGroup.addManagedPolicy(powerUserPolicy);
    devGroup.attachInlinePolicy(devPolicy);
    devGroup.attachInlinePolicy(commonPolicy);

    // Create Admin users    
    adminUsers.forEach((adminUser: string) => {
      const adminUserName = new User(this, adminUser, { userName: adminUser, groups: [adminGroup] });
      new CfnOutput(this, `${adminUserName}`, {
        value: adminUserName.userArn,
        description: `${stage} environment admin user arn`,
        exportName: `${stage}AdminUsersArn`
      });
    })

    // Create Develop users
    devUsers.forEach((devUser: string) => {
      const devUserName = new User(this, devUser, { userName: devUser, groups: [devGroup] });
      new CfnOutput(this, `${devUserName}`, {
        value: devUserName.userArn,
        description: `${stage} environment develop user arn`,
        exportName: `${stage}DevUsersArn`
      });
    })
  }
}
```

```json:cdk.json
{
  "context": {
    "division": "hogehoge",
    "prj": "fugafuga",
    "dev": {
      "adminUsers": [
        "demo01"
      ],
      "devUsers": [
        "demo02",
        "demo03"
      ]
    },
    "stg": {
      "adminUsers": [
        "circleci",
        "github"
      ],
      "devUsers": [
        "jenkins"
      ]
    },
    "prd": {
      "adminUsers": [
        "circleci",
        "github"
      ],
      "devUsers": [
        "jenkins"
      ]
    }
  },
  "app": "npx ts-node bin/iam_user.ts"
}
```
こんな感じのファイル構成をしています。
`division`や`prj`はタグ付けなどに利用していますので、特に環境ごとの配下にしていません。
これらをデプロイするときは`cdk deploy -c stage=dev`と言った感じで出し分けています。
本当は環境ではなくアカウントナンバーにしてstackの中でアカウントナンバーを取得してからcontextを注入したかったのですが、その前に`cdk.json`が読み込まれてしまうため実現できませんでした。
今は一旦これでいいかな、という感じで落ち着いています。

# Cloudformation
AWSの初期からある(たしか…)インフラコード化の走りみたいな存在ですね。
正直あんまり使ってこなかったのですが、`AWS CDK`の利用に伴い色々と理解が必要だったので使っています。
`yaml`形式自体なので可読性は高いと思うのですが、リソース名や独自の関数など覚えることいっぱいです。
まあ時間をみつけてゆるゆるとやっている感じです。

### ユーザーデーターが使える
完全にシェルこねこねと思っていたのですが、Cloudformationに普通に書けました。

```yaml:userdata&nbsp;template
Resources:
  BastionLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: bastion-launch-template
      LaunchTemplateData:
        UserData:
          Fn::Base64: !Sub
            - |
              #!/bin/bash

              # timezone
              timedatectl set-timezone "Asia/Tokyo"
     
              # time sync
              yum -y remove ntp
              yum -y install chrony
              echo '#Add TimeSync' >> /etc/chrony.conf
              echo 'server 169.254.169.123 prefer iburst' >> /etc/chrony.conf
              systemctl start chrony
              systemctl enable chrony
```

# 共通
リソース名の命名規則は最初から揃えて、変数なり`Parameter`なりで使いまわせるようにしましょう。
ベタがき、不揃いは後々つらくなってきます…。

```go:terraform

variable "Name" { default = "hoge" }  
variable "Service" { default = "fuga" }  
variable "Env" { default = "hogefuga" }  

resource "hogehoge" "fugafuga" {

/////////////省略/////////////

  tags = {
    Name     = "${var.service_name}-${var.env}-fugahoge"
    Service  = var.service_name
    Env      = var.env
  }
}
```

```json:cdk.json
{
  "context": {
    "Name": "hoge",
    "Service": "fuga",
    "Env": "hogefuga"
  }
}
```

```yaml:Cloudformation
Parameters:
  Name:
    Description: resource name.
    Type: String
    Default: hoge
  Service:
    Description: service name.
    Type: String
    Default: fuga
  Env:
    Description: environment name.
    Type: String
    Default: hogefuga

##############################
Tags: 
  - 
    Key: "Name"
    Value: !Ref Name
  - 
    Key: "Service"
    Value: !Ref Service
  - 
    Key: "Env"
    Value: !Ref Env
```

# おまけ
どうでもいいけどなんでQiitaのコードブロックから`HCL`を消してしまったのでしょう…。
Terraformのコードか見づらいじゃないの…布教できない…。