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チャンネルに追加します。
申請者がワークフローをトリガーし、フォームから情報を入力するとGCP Owner宛にダイレクトメッセージが送られます。
ここで「承認する」のようなボタンを用意していくことで、制限なく権限管理フローが進むことを回避しています。
承認が通ると最後にSlack App連携用チャンネルにメッセージが投稿されます。
下記のようにメッセージ内容にフォームの入力情報を変数として挿入しておくことで、Cloud Functionsに伝達することが可能になります。
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を選択します。
Enable EventsをOnにするとRequest URLが表示されるため、Cloud FunctionsのトリガーURLを入力します。
認証が通ると「Verified ✓ 」のメッセージが表示されます。
そのまま下にスクロールし、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とサーバーレスコンピューティングにやらせましょう