TerraformでTROCCOのユーザーをいい感じに管理する

## はじめに
前回は、TerraformでTROCCOのユーザーを作ったり消したりして利用イメージをご紹介しました。

https://qiita.com/khpkymgc/items/ca67c95ac7b70a176b0c

今回は、実際にTerraformを使ってどのようにTROCCOのユーザーを管理していくと良さそうか、というところを考えていきたいと思います。

## やりたいこと
TerraformでTROCCOのユーザーを作成するには下記情報を .tfファイルに書き込む必要があります。

```.tf
resource "trocco_user" "example" {
  email                           = "trocco@example.com"
  password                        = "Jb1p4f1uuC"
  role                            = "member"
  can_use_audit_log               = false
  is_restricted_connection_modify = false
}
```
社内のユーザ管理や弊社Dさんが公開しているサンプルリポジトリを見てどのようにユーザ管理ができれば便利か考えてみました。

https://github.com/SoySoySoyB/terraform-trocco-sample

以下サンプルリポジトリから抜粋

```.tf
admins = [
    "${local.your_email_before_at}+admin1@${local.your_email_domain}",
  ]
  developers = [
    "${local.your_email_before_at}+dev1@${local.your_email_domain}",
    "${local.your_email_before_at}+dev2@${local.your_email_domain}",
  ]
  operators = [
    "${local.your_email_before_at}+user1@${local.your_email_domain}",
    "${local.your_email_before_at}+user2@${local.your_email_domain}",
  ]
  ```

こんな感じでグループごとに所属するメンバー(アドレス)を管理する方法が良さそう。
加えて、もっとこうなったら便利だな～と思ったものは下記です。

- **email**のドメインは最初に定義しておいて毎回書かなくて良い状態にしたい
- 権限関連(role、can_use_audit_log、is_restricted_connection_modify)は所属グループで判別して自動で設定してほしい

これらができるように設定を進めていきます。

:::note warn
以下はすべて .tfファイル内の設定になります。
前回の記事で作成した .tfファイルに追記する、もしくは新しく .tfファイルを作成して同ディレクトリに配置しても問題ありません。
:::

## emailのドメインを定義する
Terraformでは、**variable**を使うことで変数を定義できるようです。

https://envader.plus/article/190

ドメイン(primenumber.co.jp)を下記のように定義しました。
```variable "domain".tf
variable "domain" {
  default = "example.com"
}
```


## ユーザーリストをつくる
今回は例として sales、tech、management の3部署分のユーザーリストを作りました

```variable "departments".tf
variable "departments" {
  type = map(list(string))
  default = {
    "sales" = ["user1", "user2"]
    "tech"   = ["user3", "user4"]
    "management" = ["user5"]
  }
}
```

:::note info
```type = map(list(string))``` を利用すると文字列のリストが作成できます。
https://qiita.com/hikaru_motomiya/items/d773355d3ffa854c7ad3#list
:::

## 権限周りは所属グループに合わせて自動で設定する
続いて、権限を自動で設定できるようにします。
部署ごとに、どのように権限をつけるか指定します。
**sales**チーム、**tech**チームはroleをmember、その他は無効にします。
**management**チームは**roleをadmin、can_use_audit_log**を有効にします。

```variable "role".tf
variable "role" {
  type = map(object({
    role                            = string
    can_use_audit_log               = bool
    is_restricted_connection_modify = bool
  }))
  default = {
    management = {
      role                            = "admin"
      can_use_audit_log               = true
      is_restricted_connection_modify = false
    }
    sales = {
      role                            = "member"
      can_use_audit_log               = false
      is_restricted_connection_modify = false
    }
    tech = {
      role                            = "member"
      can_use_audit_log               = false
      is_restricted_connection_modify = false
    }
  }
}
```

:::note info
```type = map(object ～)``` でリストの要素1つずつに型を指定できます。
:::

## ユーザーを作成してみる

ChatGPTと話し合いをした結果、下記のようになりました。

```.tf
resource "trocco_user" "example" {
  for_each = tomap({
    for user in flatten([
      for department, members in var.departments : [
        for member in members : {
          email                           = "${member}@${var.domain}"
          role                            = var.role[lower(department)].role
          can_use_audit_log               = var.role[lower(department)].can_use_audit_log
          is_restricted_connection_modify = var.role[lower(department)].is_restricted_connection_modify
        }
      ]
    ]) : user.email => user
  })
  
  email                           = each.value.email
  password                        = "Jb1p4f1uuC"
  role                            = each.value.role
  can_use_audit_log               = each.value.can_use_audit_log
  is_restricted_connection_modify = each.value.is_restricted_connection_modify
}
```

処理の内容をご紹介していきます。

::: note alert
当初、インデックスを使ってキーを生成していましたが、flattenとインデックスを併用するとユーザを増やした際にインデックスにずれが発生することがわかりました。
代わりに、キーをメールアドレスにすることで解決しました。
:::

::: note warn
処理の内容についてはChatGPTを質問責めにして返ってきた内容でふわっと理解しています。
:::

variable "departments"（var.departments）内に含まれるdepartment（部署）に対してループ実行します。キー（部署名）とその値（メンバー）取得し、部署ごとのメンバーリストを作ります。
```
for department, members in var.departments : [
```

取得例:
```
department = "sales", members = ["user1", "user2"]
```

部署の各メンバーに対してループ処理し、各メンバー毎のリストを作成します。
```
for member in members : {
```

このとき、下記内容も一緒に参照します。
```
role                            = var.role[lower(department)].role
can_use_audit_log               = var.role[lower(department)].can_use_audit_log
is_restricted_connection_modify = var.role[lower(department)].is_restricted_connection_modify
```

取得例:
```
member = "user1"
email = "user1@example.com"
var.role_settings["sales"].role → "member"
var.role[lower(department)].can_use_audit_log → false
var.role[lower(department)].is_restricted_connection_modify → false
```


取得したリストをフラット化（flatten）します。
また、現時点ではキーが部署名のため一意のキーがありません。フラット化した各要素をループ処理してemailを割り当てます。

```
for user in flatten ～～ : user.email => user
```

:::note info
Terraformのfor_eachに渡す値は、すべてユニークなキー（マップのキーやセットの要素）が必要です。下記のようにリソースの名前に使用されるため重複が許されないためです。

```
Terraform will perform the following actions:

  # trocco_user.example["0"] will be created
  ～～～
  # trocco_user.example["1"] will be created
  ～～～
```
:::

for_each に渡す値を明示的にマップ型に変換します。リストや他のデータ構造をそのままfor_eachにわたすことができないためです。

https://envader.plus/article/215

```
for_each = tomap({
```

今実行しているループで取得した値を各要素に挿入します
```
email                           = each.value.email
password                        = "Jb1p4f1uuC"
role                            = each.value.role
can_use_audit_log               = each.value.can_use_audit_log
is_restricted_connection_modify = each.value.is_restricted_connection_modify
```

## 実行してみる
処理の内容を認識したところで```terraform plan```を実行してどのような結果になるか見てみます。

```
>terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # trocco_user.example["0"] will be created
  + resource "trocco_user" "example" {
      + can_use_audit_log               = true
      + email                           = "user5@example.com"
      + id                              = (known after apply)
      + is_restricted_connection_modify = false
      + password                        = (sensitive value)
      + role                            = "admin"
    }

  # trocco_user.example["1"] will be created
  + resource "trocco_user" "example" {
      + can_use_audit_log               = false
      + email                           = "user1@example.com""
      + id                              = (known after apply)
      + is_restricted_connection_modify = false
      + password                        = (sensitive value)
      + role                            = "member"
    }

  # trocco_user.example["2"] will be created
  + resource "trocco_user" "example" {
      + can_use_audit_log               = false
      + email                           = "user2@example.com""
      + id                              = (known after apply)
      + is_restricted_connection_modify = false
      + password                        = (sensitive value)
      + role                            = "member"
    }

  # trocco_user.example["3"] will be created
  + resource "trocco_user" "example" {
      + can_use_audit_log               = false
      + email                           = "user3@example.com""
      + id                              = (known after apply)
      + is_restricted_connection_modify = false
      + password                        = (sensitive value)
      + role                            = "member"
    }

  # trocco_user.example["4"] will be created
  + resource "trocco_user" "example" {
      + can_use_audit_log               = false
      + email                           = "user4@example.com""
      + id                              = (known after apply)
      + is_restricted_connection_modify = false
      + password                        = (sensitive value)
      + role                            = "member"
    }

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

想定している設定になっていそうです！

## 反映させる
```terraform apply```で反映してみます。（ドメインは環境に合わせて変更しています）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929084/5793a61c-41d8-10a1-1efb-1508e9b2fcda.png)

思った通りに動作して、無事権限の違いがあるアカウントを一括で作ることができました！
メンバーに変更が発生した場合は```variable "departments"```の各部署のメンバーを増減させるだけでよいので管理が単純化できそうですね。


## おわりに
Terraformでは、処理を指示している文の中で下記の```member```のように要素の名前を定義し始めるので **それどこから出てきた？？？** となることが多かったです・・・

```
for department, members in var.departments : [
        for member in members : {
```

また、ループ処理をコードで指定するのは初めてだったのでどのように動作するのかなど全然わからず理解に時間がかかりました。
設定方法を理解するまでは **コード管理むずい！！！GUIで設定で良くないか？？** と思ったりもしました...。

ただ、設定が終わって実行してみると下記のようなメリットがあると実感でき、コード管理したいというご要望が多い理由もわかったような気がしました。

- 登録するユーザーの権限について考慮しなくて良い（部署だけ把握していればOK）
- 一括で複数ユーザーが操作できる（GUIでポチポチするより圧倒的に早い）（手軽さがすごい）


今回、部署毎でユーザ管理したので、TROCCOのチームもTerraformで管理するようにするとより便利になりそうです。
次はチーム管理もTerraformで行ってみたいと思います！