見出し画像

SlackとCloud Functionsによる業務効率化

電通デジタルで機械学習エンジニアをしている今井です。
本記事では、SlackとCloud FunctionsによるGCPアクセス権限の管理効率化について紹介します。

どうしてアクセス権限管理を効率化?

本記事での「GCPアクセス権限の管理」とは新卒/中途/異動などでGCP利用者に入れ替えが発生したときに各ユーザーに対するIAM権限の付与・削除を行うことを指しています。
普段はGCP Console上で作業することが多いですが、まとめて処理したいときなどはgcloudコマンドやクライアントライブラリを使用しています。

ではなぜアクセス権限管理を効率化したいと思ったのか。
電通デジタルでは
- クライアント様向けにGCP環境を構築して共同運用する
- 他電通グループ会社と共通のGCPプロジェクトで開発・分析作業する
といった使われ方が多いです。

そのため、GCP Ownerにはクライアント担当やプロジェクトマネージャー、GCP Maintainerには開発責任者という組織構成になりやすく、例えば
1. 他電通グループ会社メンバーより権限申請の連絡
2. プロジェクトマネージャーが承認
3. 開発責任者にて権限付与
といったフローで権限管理が進みます。

そこで、1→2→3をSlackワークフロービルダーで定型化し、2→3をSlack AppとCloud Functionsで自動化することで権限管理の効率化を行いました。

本記事ではこれらの開発手順について紹介します。

Slackワークフロービルダーの設定

はじめにSlackワークフロービルダーを設定します。
ワークフロービルダーについてはSlack公式ガイドを参照してください。

今回必要な情報は
- メールアドレス
- GCPプロジェクト名
- 申請する役割
になるため、以下のようなワークフローを作成し、申請用Slackチャンネルに追加します。

図1

申請者がワークフローをトリガーし、フォームから情報を入力するとGCP Owner宛にダイレクトメッセージが送られます。
ここで「承認する」のようなボタンを用意していくことで、制限なく権限管理フローが進むことを回避しています。

承認が通ると最後にSlack App連携用チャンネルにメッセージが投稿されます。
下記のようにメッセージ内容にフォームの入力情報を変数として挿入しておくことで、Cloud Functionsに伝達することが可能になります。

図2

Slack AppとCloud Functionsの設定

まずCloud Functionsの設定をします。
トリガーのタイプをHTTP、ランタイムをPythonにして関数を作成し、main.pyに下記のコードを挿入してデプロイします。

import json
if request.get_json().get('type') == 'url_verification':
   body = json.dumps({'challenge': request.get_json()['challenge']})
   headers = {'Content-Type': 'application/json'}
   return (body, 200, headers)

これはSlack Appに対するレスポンス認証のコードになります。

デプロイ完了後、作成した関数を選択し、トリガーURLをコピーします。
ここでCloud Functionsの設定を一時中断し、Slack Appの設定に移ります。

リンクからSlack Appを作成し、下記の画面に遷移したら左タブのEvent Subscriptionsを選択します。

スクリーンショット 2021-09-01 17.14.32

Enable EventsをOnにするとRequest URLが表示されるため、Cloud FunctionsのトリガーURLを入力します。
認証が通ると「Verified ✓ 」のメッセージが表示されます。

スクリーンショット 2021-09-02 11.25.06

そのまま下にスクロールし、Subscribe to bot eventsのAdd Bot User Eventをクリックし、Slack App連携用チャンネルがPrivateチャンネルの場合はmessage.groupsを、Publicチャンネルの場合はmessage.channelsを選択します。

左タブのBasic Informationに遷移し、Install to WorkspaceでSlack Appをワークスペースと連携します。
Slack App連携用チャンネルからアプリを追加後、チャンネルにメッセージが投稿されるとCloud Functionsにメタ情報とともにメッセージ内容がPOSTされるようになります。
* 過剰なPOSTを避けるために、Slack App連携用チャンネルはPrivateチャンネルに設定し、最低限のメンバーのみで使用することをお勧めします。

最後にBasic Informationをさらに下にスクロールし、Verification Tokenをコピーします。
これはCloud FunctionsでSlack AppからのPOSTを判別するのに使用します。

再度GCPでの作業に戻ります。
Cloud Functionsを編集する前に以下の作業を行います。
- Secret ManagerからVerification Tokenを登録する
- IAMと管理からCloud Functionsのサービスアカウントに「Project IAM 管理者」と「Secret Manager のシークレット アクセサー」を追加する

完了後、作成した関数の編集を選択し、以下のように設定します。

1. セキュリティの設定からVerification TokenをSLACK_TOKENの名称で「環境変数として公開」にする

2. requirements.txtに「oauth2client」と「google-api-python-client」を追記する

3. main.pyを下記のコードに置換する
* コードの注釈
- ROLESのkeyはSlackワークフローの申請フォームで入力/選択される名称
- IAMポリシーのメンバータイプは接頭辞で識別されます(詳しくはGCP公式ドキュメントを参照)
- 権限削除はappendしてる箇所をremoveに変更するなどで対応可能(詳しくはクライアントライブラリを参照)

import os
import json
from oauth2client.client import GoogleCredentials
from googleapiclient.discovery import build

# 適宜修正する
ROLES = {
   'BigQueryユーザー': ['roles/bigquery.dataEditor', 'roles/bigquery.jobUser']
}

HEADERS = {'Content-Type': 'application/json'}

def gcp_resource_management_using_slack(request):
   request_json = request.get_json()

   # Verifies ownership of an Events API Request URL
   if request_json.get('type') == 'url_verification':
       body = json.dumps({'challenge': request_json['challenge']})
       return (body, 200, HEADERS)

   if request_json.get('token') != os.environ['SLACK_TOKEN']:
       body = json.dumps({'message': 'Unauthorized token'})
       return (body, 401, HEADERS)

   # Update IAM policy triggered by slack app
   elif request_json.get('event', {}).get('username') == 'slack-workflow-name':
       elements = request_json['event']['blocks'][0]['elements'][0]['elements']
       member = 'user:{}'.format(elements[1]['text'])
       role = elements[3]['text']
       project_id = elements[5]['text']

       credentials = GoogleCredentials.get_application_default()
       service = build('cloudresourcemanager', 'v1', credentials=credentials)

       # Gets IAM policy
       policy = service.projects().getIamPolicy(resource=project_id).execute()

       # Append existing role binding
       for i, binding in enumerate(policy['bindings']):
           if binding['role'] not in ROLES[role]: continue
           if member not in policy['bindings'][i]['members']:
               policy['bindings'][i]['members'].append(member)

       # Add new role binding
       for new_role in set(ROLES[role]) - set([b['role'] for b in policy['bindings']]):
           policy['bindings'].append({'role': new_role, 'members': [member]})

       # Sets IAM policy
       service.projects().setIamPolicy(
           resource=project_id,
           body={'policy': policy}).execute()

       body = json.dumps({'message': 'OK'})
       return (body, 200, HEADERS)

   else:
       body = json.dumps({'message': 'Bad Request'})
       return (body, 400, HEADERS)

4. エントリポイントをgcp_resource_management_using_slackに変更する

デプロイすると、Slackワークフロー → Slack App → Cloud Functionsの流れで権限管理が自動化されます。
もちろんCloud Functionsの実装を変えれば他のGCPリソースなどと連携することも可能ですし、Lambdaに変更すればAWSリソースとの連携も容易に実現できます。

退屈なことはSlackとサーバーレスコンピューティングにやらせましょう