3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

続・Terraform + TROCCO入門 その1: tfmigrateによる既存取込も含めたユーザー/チームの堅牢な管理

Posted at

はじめに

最近開発が進んできているTROCCO®のTerraform対応について、年末のアドベントカレンダーで記事を2本執筆しました。

当時は対応リソースがまだ限定されていましたが、そこから1月に入ってチーム、転送設定、ワークフローと徐々に拡充されています。そこで今回は、Terraform活用の重要性が高いと思われる、ユーザー/チームの管理について取り上げることにします。

こんな方におすすめ

  • Terraformを使ってTROCCO®のユーザー/チームを堅牢に管理したい
  • 利用者の追加や削除、部署変更があったときにシンプルに対応できるようになりたい
  • 既に登録済のリソースについて、Terraform管理下に移行したい(今回はユーザーを例に)

Terraformの基礎やCI/CDについては取り上げないので、基礎から抑えたい方は過去記事をご覧ください!

概要の説明

具体のコードは例によってサンプルリポジトリで公開しているので、まずは要点を掴んでおきましょう。

前提

以下のような組織を想定しています。

  • ユーザーは部門、役職(=マネージャー/メンバー)、個々人という階層で管理する
  • 部門のなかにデータマネジメント部門がある
  • 部門と別で技術管理者がいる

そして、このようなことをやりたいとします。

  • 各部門のマネージャーおよび技術管理者がアカウントの管理者権限を持つ
  • 上記に加えてデータマネジメント部門のみが接続情報の編集権限を持つ
  • データマネジメント部門のマネージャーのみが監査ログの取得権限を持つ(よく考えたらこの前提おかしくないかと思ったのですが、まあ設定ということで苦笑
  • 部門ごとにチームを作って権限管理を行いたい

実現方法

以下のような形でユーザー管理を実現しています。

  • localsブロックでuserの階層構造を記載する
  • 所属部門/役職に応じて権限やチームの所属を指定する
  • ユーザーの新規追加時には、パスワードを発行する必要があるのでユーザーごとにTerraformで生成する
    • 初期パスワードはStateに残ってしまうので、変更依頼をすること
  • 新規追加時には、一度個別に生成したあとに、部門に書き加える形とする
    • これは部門に対応させてチームに追加しようとしたときに、メールアドレスが未承認だとエラーになるため
    • 部門に追加されたタイミングで個別に発行したパスワードのリソースはdestroyされる
      • 管理するリソースが増えすぎてしまうので、都度削除する/なおユーザー側には残る
  • 既存の取り込みの際には、tfmigrateかimportブロックを活用する
    • こちらの詳細は後述します

詳細説明

それでは、具体の説明に入っていきます。

新規ユーザーを作成する

まずは新規ユーザーの作成です。./sample3_advanced/trocco/user_team_management/_resources.tfnewly_created_usersの部分に発行するユーザーを記載します。

サンプルでは以下のようになっています。

./sample3_advanced/trocco/user_team_management/_resources.tf
  newly_created_users = [
    "${local.your_email_before_at}+${local.sample_label}_marketing__manager@${local.your_email_domain}",
    "${local.your_email_before_at}+${local.sample_label}_marketing__member@${local.your_email_domain}",
    "${local.your_email_before_at}+${local.sample_label}_product__manager@${local.your_email_domain}",
    "${local.your_email_before_at}+${local.sample_label}_product__member@${local.your_email_domain}",
    "${local.your_email_before_at}+${local.sample_label}_data_management__manager@${local.your_email_domain}",
    "${local.your_email_before_at}+${local.sample_label}_data_management__member@${local.your_email_domain}",
  ]

applyを実行すると、個々のユーザーに対して別々のパスワードが発行され、招待メールが送られます。サンプルの通りにやろうとすると、自分のメールアドレスのエイリアスになっているので、自分にメールがまとめてきます。

コードは割愛しますがユーザー部分の設定に基づいて、初期発行時には、

  • 権限はメンバー
  • 接続情報の編集権限はなし
  • 監査ログは利用不可

の状態になっています。

続いて、メールに記載されているURLをクリックしてメールアドレスの認証をしましょう。完了したら、newly_created_usersの部分をコメントアウトして、逆に部門ごとのコメントアウトされている部分を変更します。

./sample3_advanced/trocco/user_team_management/_resources.tf
  departments = {
    marketing = {
      department_name_ja = "マーケティング本部"
      managers = [
        "${local.your_email_before_at}+${local.sample_label}_marketing__manager@${local.your_email_domain}",
      ],
      members = [
        "${local.your_email_before_at}+${local.sample_label}_marketing__member@${local.your_email_domain}",
      ],
    }
    product = {
      department_name_ja = "プロダクト本部"
      managers = [
        "${local.your_email_before_at}+${local.sample_label}_product__manager@${local.your_email_domain}",
      ],
      members = [
        "${local.your_email_before_at}+${local.sample_label}_product__member@${local.your_email_domain}",
      ],
    }
    data_management = {
      department_name_ja = "データマネジメント本部"
      managers = [
        "${local.your_email_before_at}+${local.sample_label}_data_management__manager@${local.your_email_domain}",
      ],
      members = [
        "${local.your_email_before_at}+${local.sample_label}_data_management__member@${local.your_email_domain}",
      ],
    }
  }
  # 部門とは別に技術担当者が存在している想定
  technical_admins = [
    "${local.your_email_before_at}+${local.sample_label}_product__member@${local.your_email_domain}",
  ]

合わせて、下部のチームの部分のコメントアウトも外してください。この状態でapplyをすると、ユーザーの権限がそれぞれに応じたものに変更されるとともに、部門ごとの管理者のチームとメンバーのチームが生成されます。

チームの定義は以下のようになっています。

./sample3_advanced/trocco/user_team_management/_resources.tf
# 部門マネージャー+技術管理者による部門ごとの管理者チームを生成
# メールアドレスの認証前にはチームを作成できないので注意
resource "trocco_team" "department_managers" {
  for_each = local.departments
  name     = "${local.departments[each.key].department_name_ja} 管理者"
  members = concat(
    [
      for email in each.value["managers"] : {
        user_id = local.user_email_ids[email],
        role    = "team_admin"
      }
      ], [
      for email in local.technical_admins : {
        user_id = local.user_email_ids[email],
        role    = "team_admin"
      }
    ]
  )
  depends_on = [
    trocco_user.users
  ]
}

# 部門マネージャー+技術管理者をチーム管理者/部門メンバーをチームメンバーとした部門ごとのメンバーチームを生成
resource "trocco_team" "department_members" {
  for_each = local.departments
  name     = "${local.departments[each.key].department_name_ja} メンバー"
  members = concat(
    [
      for email in each.value["managers"] : {
        user_id = local.user_email_ids[email],
        role    = "team_admin"
      }
    ],
    [
      for email in local.technical_admins : {
        user_id = local.user_email_ids[email],
        role    = "team_admin"
      }
    ],
    [
      for email in each.value["members"] : {
        user_id = local.user_email_ids[email],
        role    = "team_member"
      } if !contains(local.technical_admins, email)
    ]
  )
  depends_on = [
    trocco_user.users
  ]
}

なお、リソースグループは後日開発予定なので、もう少々お待ちください!

ユーザーの部署異動に対応する

ユーザーの部署異動に応じて権限変更を行いたい場合は、departmentsのなかで対象のユーザーを変更したい部門/役職に移動します。既にメールアドレス認証は済んでいるので、applyをすれば部署/役職に応じた権限に変更されるとともに、所属するチームが変更されます。

既存ユーザーを取り込む

取り込みの運用を考えるにあたって、Terraformの基本的な挙動をおさらいしておきましょう。Terraformで既存リソースを取り込むには、

  • importコマンド
  • importブロック

がネイティブの機能としてあります。そして、これをより実運用にあたり便利にしたものとして、tfmigrateというツールがあります。

importコマンドでは事前検証なしにリソースを取り込んでしまうので運用上は望ましくなく、そのためimportブロックやtfmigrateを活用します。

もう少し詳しく理解したい人は、以下の記事をご覧ください。

tfmigrateの手順

ツールとしては、リソース定義を記載した上で移行処理の内容をまとめて記載することで、プルリクエストでの実行時に差分がないことを確認し、差分がなければmainブランチにマージすることで取り込みがされるようになっています。

対象ディレクトリは異なりますが、以下のようなイメージです。

image.png

具体の実装としては、下記のファイルで移行処理がどこで、処理結果をどこに保存するかという指定ができ、

./sample3_advanced/trocco/user_team_management/.tfmigrate.hcl
tfmigrate {
  migration_dir = "./state_operation/tfmigrate"
  history {
    storage "gcs" {
      bucket = "{YOUR_BUCKET_NAME_BACKEND}"
      name   = "sample3_advanced/trocco/user_team_management/tfmigrate_history.json"
    }
  }
}

migration_dirに.hclの処理ファイルを格納することで、これが処理対象になります。

記述例
migration "state" "import_users" {
  actions = [
    "import trocco_user.users[\\\"your.mail+dummy1@example.com\\\"] 1",
    "import trocco_user.users[\\\"your.mail+dummy2@example.com\\\"] 2",
    "import trocco_user.users[\\\"your.mail+dummy3@example.com\\\"] 3",
    "import trocco_user.users[\\\"your.mail+dummy4@example.com\\\"] 4",
  ]
}

tfactionはtfmigrateに対応しており、プルリクエストのラベルにtfmigrate:<target>の記載をするだけで、通常のterraform plan / applyではなくtfmigrate plan / applyの実行に処理を変更することができます。簡単すぎて最高です。

リソース定義やhclファイルを書くのは大変なのでは、という点についてはimportブロックとまとめて後述します。

importブロックの手順

tfmigrateではstateの処理でdiffが出ないことを強制させる形になりますが、importブロックではdiffを許容できます。こちらもリソース定義を記載した上で、importブロックを記載するとcreateではなくimportの挙動になり、通常のterraform planで確認/terraform applyで取り込みができます。

記述例
import {
  to = trocco_user.users["your.mail+dummy1@example.com"]
  id = 1
}

import {
  to = trocco_user.users["your.mail+dummy2@example.com"]
  id = 2
}

import {
  to = trocco_user.users["your.mail+dummy3@example.com"]
  id = 3
}

import {
  to = trocco_user.users["your.mail+dummy4@example.com"]
  id = 4
}

リソース定義やtfmigrateのhclファイル/importブロックを生成する

やり方はわかった、でも実際やろうとするとめんどくさいよね、とみなさんも思いますよね。よくわかります。

ということで、リソース定義を生成するためのlocal部分の記載や、tfmigrateのhclファイル、importブロックの記載内容を、ユーザー一覧のAPIで取得したデータから生成するためのPythonスクリプトを作成しました。

./sample3_advanced/trocco/user_team_management/state_operation/script/state_operation.ipynbのスクリプトを実行すると、./sample3_advanced/trocco/user_team_management/state_operation/generated_file/のディレクトリにそれぞれの素材が自動生成されます。

./sample3_advanced/trocco/user_team_management/state_operation/script/state_operation.ipynb
from dotenv import load_dotenv
import urllib, os, json, pandas as pd, pytz, datetime


def fetch_users():
    endpoint = 'users'
    limit = 200  # max 200
    next_cursor = ''
    df_users = pd.DataFrame()
    while True:
        trocco_api = f'https://trocco.io/api/{endpoint}?limit={limit}&cursor={next_cursor}'
        req = urllib.request.Request(trocco_api)
        req.add_header('Authorization', f'Token {os.getenv("trocco_api_key")}')
        res = json.load(urllib.request.urlopen(req))
        df_users = pd.concat([df_users, pd.DataFrame(res['items'])])
        next_cursor = res.get('next_cursor', '')
        if not next_cursor:
            break
    return df_users

def base_file_name(action):
    jst = pytz.timezone('Asia/Tokyo')
    return f'{datetime.datetime.now(jst).strftime("%Y%m%d")}__{action}_users'

def generate_tfmigrate_hcl_file(action, df_users):
    migration_hcl = f'# tfmigrateでユーザーをまとめて{action}するための定義;ファイル名末尾の.tmpを削除してtfmigrateディレクトリに配置することで処理可能になる\nmigration "state" "{action}_users"' + ' {\n  actions = [\n'
    migration_hcl += '\n'.join([f'    "{action if action != "remove" else "rm"} trocco_user.users[\\\\\\"{email}\\\\\\"]{" " + str(id) if action == "import" else ""}",' for email, id in zip(df_users.email, df_users.id)])
    migration_hcl += '\n  ]\n}\n'
    with open(f'../generated_file/{base_file_name(action)}.hcl.tmp', 'w', encoding='utf-8') as f:
        f.write(migration_hcl)

def generate_users_list(action, df_users):
    if action ==  'import':
        users_list = generate_user_list('imported_users', df_users)
        users_list = users_list + '\n' + generate_user_list('imported_super_admin_user', df_users.query('role == "super_admin"'))
        users_list = users_list + '\n' + generate_user_list('imported_admin_users', df_users.query('role == "admin"'))
        users_list = users_list + '\n' + generate_user_list('imported_connection_modify_not_restricted_users', df_users.query('is_restricted_connection_modify == False'))
        if 'can_use_audit_log' in list(df_users.keys()):
            users_list = users_list + '\n' + generate_user_list('can_use_audit_log_users', df_users.query('can_use_audit_log == True'))
        with open(f'../generated_file/{base_file_name(action)}_list_for_tf.txt', 'w', encoding='utf-8') as f:
            f.write('  # ユーザー取込時に定義に差分が出ないようにするため、権限ごとのユーザー一覧を生成;locals部分に記載する\n' + users_list + '\n')

def generate_user_list(list_name, df_users):
    user_list = f'  {list_name} = [\n'
    user_list += '\n'.join([f'    "{email}",  # {id}' for email, id in zip(df_users.email, df_users.id)])
    user_list += '\n  ]'
    return user_list

def generate_blocks(action, df_users):
    blocks = f'# {action if action != "remove" else "removed"}ブロックでユーザーをまとめて{action}するための定義;terraformのルートディレクトリに配置し、処理後に別フォルダに移管する\n'
    if action == 'import':
        blocks += '\n'.join([f'import {{\n  to = trocco_user.users[\"{email}\"]\n  id = {id}\n}}\n' for email, id in zip(df_users.email, df_users.id)])
    if action == 'remove':
        blocks += '\n'.join([f'removed {{\n  from = trocco_user.users[\"{email}\"]\n  lifecycle {{\n    destroy = false\n  }}\n}}\n' for email, id in zip(df_users.email, df_users.id)])
    with open(f'../generated_file/{base_file_name(action)}.tf.tmp', 'w', encoding='utf-8') as f:
        f.write(blocks)

def main():
    # ルートディレクトリに.envファイルを配置しておく
    load_dotenv()
    df_users = fetch_users()

    # 個別処理を入れる場合の例
    # df_users = df_users.query('email.str.find("sample2") > 0')

    if not os.path.isdir('../generated_file'):
        os.makedirs('../generated_file')

    for action in ['import', 'remove']:
        generate_tfmigrate_hcl_file(action, df_users)
        generate_users_list(action, df_users)
        generate_blocks(action, df_users)

if __name__ == '__main__':
    main()

ちなみにこういった処理はremoveに対しても活用できるので、どうせ使うだろうということでremoveに使える素材も生成しています。ということでまとめてimportして、まとめてremoveするのも試せます。

解説は以上になります。基本的にただ取込/除外をしているだけでは差分が出ないはずですが、既存リソースを取り扱う以上、不用意な設定変更が発生しないようお気をつけください。

特に、特権管理者のAPI KEYを利用するとユーザーを削除できてしまうので、既存ユーザーの取り込みを試すなら管理者権限からにしておきましょう。

おわりに

一通り試してみて個人的にもこれは超絶便利では・・・!と思ったものです。これからさらに対応リソースは拡充されていきますし、私も活用方法を模索していくので、引き続きお楽しみに!

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?