巻頭言:Slack botを使ったGoogle Cloudの一時権限付与をOpenAI APIで実現してみた

目次

Slack botを使ったGoogle Cloudの一時権限付与をOpenAI APIで実現してみた

はじめに

夏が来ましたね。しょっさん( @syossan27 )です。

今回は適切な権限管理を行うために、一時権限付与を便利にしようという試みをAIを使ってやってみたという記事になります。

背景と課題

SREにとってサービスをセキュアに保つためにも、権限付与を管理されているところも多いかと思います。
弊チームでは、Slack botとOpenAI APIを組み合わせて、Google Cloudプロジェクトの一時権限を自然言語で付与する機能を開発しました。

今までの権限付与の問題点

今までは一時権限付与はSlackでスラッシュコマンドを利用して、例えば以下のように実行していました。

/fansta grant roles/container.developer hoge@example.com

ただし、一時権限付与のたびにこれを思い出して実行してもらうのは中々開発チームにとって骨ではあります。
また、「やりたいことに対して適切な権限はなにか?」というのも、SREsへの問い合わせを挟むことがあったので、それもまた手間となっておりました。

目指した解決策

これらの課題を解決するために、以下のような要件を設定しました:

  • 自然言語での権限要求: 「Compute Engineの管理権限を〇〇さんに付与して」のような自然な表現で権限を要求
  • 一時的な権限付与: これまでと変わらず、有効期限付きの権限付与(デフォルト7日間)
  • 適切な権限の付与: 自然言語で指定した適切な権限を検索・付与
  • 承認フロー: 権限付与前の確認プロセス

こうすることで、なるたけ開発・SREsのお互いに益のあるような姿を目指しました。

ここで承認フローが突然出てきましたが、これは自然言語でやる以上、付与対象者や付与する権限が違うものを指定されてしまう可能性があります。
そのため、最終的には人間の目で判断してOKなら承認して実行という流れにしたかったわけです。

システム概要

アーキテクチャ

システムは以下のコンポーネントで構成されています:

graph LR
    Slack -- Botへのメンション --> CF1
    CF1 --> PubSub
    PubSub --> CF2
    CF2 --> Responses
    Responses --> IAM

    Slack[Slack]
    CF1[Cloud Functions: Slack bot handler]
    PubSub[Pub/Sub]
    CF2[Cloud Functions: Slack bot backend]
    Responses[Responses API]
    IAM[Google Cloud IAM API]

コンポーネントごとの動きとしてはザッとこのようになります。

  1. Slack bot handler: Slackのメンションイベントを受信し、Pub/Subメッセージを送信。Slack botは3秒以内にレスポンスを返す必要があり、一旦このハンドラから受け付けた旨のレスポンスを返す
  2. Slack bot backend: Pub/Subメッセージを受信し、OpenAI API(Responses API)を呼び出す
  3. OpenAI API(Responses API): 自然言語を読み取り、Function Callingを用いて一時権限付与機能を実行
  4. Google Cloud IAM API: 一時権限付与を実行

それでは、次は実装の詳細を見ていきましょう。

実装詳細

1. Slackイベントの処理

まず、Slackからのメンション(@bot)イベントを受信するCloud Functionsの実装になります。

func handleSlackEvent(w http.ResponseWriter, r *http.Request) {
    // Slackイベントの解析
    var eventCallback SlackEventCallback
    if err := json.NewDecoder(bytes.NewReader(bodyBytes)).Decode(&eventCallback); err != nil {
        log.Printf("Error decoding JSON: %v", err)
        return
    }

    switch eventCallback.Type {
    case "event_callback":
        var appMentionEvent AppMentionEvent
        if err := json.Unmarshal(eventCallback.Event, &appMentionEvent); err == nil {
            if appMentionEvent.Type == "app_mention" {
                // Pub/Subに送信
                publishToPubSub(args, event.Channel, event.User)
            }
        }
    }
}

まずはSlackから飛んできたリクエスト内容を読み取り、そのままSlack bot backendへPub/Subを通して丸投げします。
ここには記載してませんが、投げた後はSlackへ「処理中のため、ちょっと待っててね」というメッセージをメンション付きで投稿しています。
Slack bot handlerの役割はここで終わりで、この先はSlack bot backendの役目になります。

2. OpenAI APIとの連携

Slack bot backendでは、まずはOpenAI APIを使用して自然言語を構造化データに変換します。
Azure OpenAIのResponses APIを活用し、Function Callingの仕組みを利用しています。

func callAzureOpenAI(question string) (*AzureOpenAIResponse, error) {
    client := openai.NewClient(
        azure.WithAPIKey(apiKey),
        option.WithBaseURL(endpoint+"/openai/v1"),
    )

    // Function定義
    tools := []responses.ToolUnionParam{
        {
            OfFunction: &responses.FunctionToolParam{
                Name:        "grantTemporaryPermission",
                Description: "ユーザーに一時的なIAMロールを付与します。",
                Parameters: openai.FunctionParameters{
                    "type": "object",
                    "properties": map[string]interface{}{
                        "role": map[string]interface{}{
                            "type": "string",
                            "description": "付与するGoogle Cloud IAMのロール名",
                        },
                        "members": map[string]interface{}{
                            "type": "array",
                            "description": "権限を付与する対象ユーザー",
                        },
                    },
                },
            },
        },
    }

    resp, err := client.Responses.New(ctx, responses.ResponseNewParams{
        Input: responses.ResponseNewParamsInputUnion{OfString: openai.String(question)},
        Model: openai.ChatModelGPT4o,
        Tools: tools,
    })

    return parseResponse(resp)
}

ここではOpenAI APIでの定義を書いていまして、Function Callingに利用するFunction定義やResponses APIの定義を書いてたりします。
Function Callingについてご存じない方への簡単な説明として「プロンプトの内容から実行が必要とされる外部関数を呼び出してくれる」という機能になります。
今回で言うと、「権限の付与」的な意味合いを持つプロンプトだと grantTemporaryPermission の関数実行が走るという感じですね。

3. 自然言語処理の仕組み

OpenAI APIには、以下の情報を含むFunction定義を提供しています:

■ ロール名の変換

  • 入力: 「Compute Engineの管理権限」
  • 出力: roles/compute.instanceAdmin.v1

この変換のために、Google CloudのIAMロール一覧をVector Storeに格納し、File Search機能を活用しています。
以下はVector Storeに格納するために別途作成したスクリプトになります。

vector_store = client.vector_stores.create(
    name="Financial Statements",
    chunking_strategy={
        "type": "static",
        "static": {
            "max_chunk_size_tokens": 300,
            "chunk_overlap_tokens": 20
        }
    }
)

file_batch = client.vector_stores.file_batches.upload_and_poll(
    vector_store_id=vector_store.id,
    files=[open("roles.txt", "rb")]
)

Vector Storeに格納するファイルは gcloud iam roles list で取得してきたIAMについての説明付きの一覧になります。以下のような感じで出力されているので、こちらの内容をもとに付与したい権限を推察させるわけですね。

description: Grants ability to create Workstation resources.
etag: AA==
name: roles/workstations.workstationCreator
stage: GA
title: Cloud Workstations Creator
---
description: Grants ability to create workstations with exemption from max_usable_workstations
  Limit.
etag: AA==
name: roles/workstations.workstationLimitExemptedCreator
stage: GA
title: Cloud Workstations Limit Exempted Creator

また、descriptionがかなり推察に寄与する要素ですので、実際は以下のように作っております。

"role": map[string]interface{}{
    "type": "string",
    "description": "付与するGoogle Cloud IAMのロール名を指定します。" +
        "例えば「roles/compute.instanceAdmin.v1」や、自然言語で「Compute Engineのインスタンス管理者権限」などが指定されます。" +
        "自然言語で指定された場合はFile Searchを行い、適切なロールに変換してください。" +
        "例として「Compute Engineのインスタンス管理者権限」を指定された場合には「roles/compute.instanceAdmin.v1」といった形式に変換してください。",
},

■ ユーザー名の変換

  • 入力: 「〇〇さん」「しょっさん」
  • 出力: user:hoge@example.com

Function定義内に名前とメールアドレスの対応表を埋め込み、変換を実現しています。
なるたけ、自然言語としての広がりを持たせるためにニックネームが浸透している方はニックネームでも対象とできるようにしてみました。

こちらもdescriptionは以下のように作っております。

"members": map[string]interface{}{
    "type": "array",
    "items": map[string]interface{}{
        "type": "string",
    },
    "description": "権限を付与する対象ユーザーが1人、もしくは複数人が指定されます。" +
        "例えば「〇〇さん」といった名前が指定されます。" +
        "この名前をもとに、紐づいたGoogle Cloud IAMのユーザーアカウントを指定する必要があります。" +
        "例えば「〇〇さん」を指定されたときには「user:hoge@example.com」といった形式に変換してください。" +
        "複数人指定された場合は、変換して配列で指定してください。" +
        "以下にフルネーム(ふりがな, ニックネーム)とユーザーアカウントの対応表を示しますので、変換の参考にしてください。\n\n" +
        "```\n" +
        "山田 太郎(やまだ たろう): user:taro.yamada@example.com\n" +
        "井上 翔太(いのうえ しょうた、しょっさん): user:syossan27@example.com\n" +
        "```\n",
},

「ニックネーム、判別できたりしないかしら?」と、descriptionにニックネームを記載しない状態でも不安定ながら推察してユーザーを選別してくれたので、より確実性を持たせるためにニックネームをdescriptionに明記しておきました。

4. 承認フロー

最後に、権限付与前にSlackのインタラクティブボタンを使用した承認フローの実装です。
人によって確認待ちかどうか?を判定するために状態をCloud Storageに保存して簡易的に判断しています。

func showConfirmationMessage(channelID, userID, functionName, functionArgs string) {
    // 確認IDを生成
    confirmationID := generateConfirmationID()

    // 確認待ち状態をCloud Storageに保存
    saveConfirmationToStorage(confirmationID, channelID, userID, functionName, functionArgs)

    // インタラクティブボタン付きメッセージを送信
    attachment := slack.Attachment{
        CallbackID: confirmationID,
        Title:      "以下の操作を実行しますか?",
        Text:       generateFunctionDescription(functionName, functionArgs),
        Actions: []slack.AttachmentAction{
            {Name: "confirm", Text: "はい", Type: "button"},
            {Name: "cancel", Text: "いいえ", Type: "button"},
        },
    }

    api.PostMessage(channelID, slack.MsgOptionAttachments(attachment))
}

5. 権限付与の実行

確認が完了すると、Google Cloud IAM APIを使用して実際の権限付与を実行します。

func grantTemporaryPermission(channelID, userID string, args string) {
    // 期限付きの条件を設定(7日間)
    expireTime := time.Now().Add(168 * time.Hour).Format(time.RFC3339)
    condition := &cloudresourcemanager.Expr{
        Title:       "Temporary Access",
        Description: "Access expires after 7 days",
        Expression:  fmt.Sprintf("request.time < timestamp('%s')", expireTime),
    }

    // IAMポリシーの更新
    service, _ := cloudresourcemanager.NewService(ctx)
    policy, _ := service.Projects.GetIamPolicy(projectID, request).Do()

    // 条件付きバインディングを追加
    updatedBindings = append(updatedBindings, &cloudresourcemanager.Binding{
        Role:      role,
        Members:   members,
        Condition: condition,
    })

    policy.Bindings = updatedBindings
    service.Projects.SetIamPolicy(projectID, &cloudresourcemanager.SetIamPolicyRequest{
        Policy: policy,
    }).Do()
}

Google CloudのIAMでは 条件付きロール バインディング を利用して一時的な権限を付与することが出来ます。

一時的な権限はチームメンバーで付与できるようにSlack botを介して、恒久的な権限はIaCリポジトリからTerraformを介して付与するように使い分けていたりします。

実際の使用例

使用法

@fansta-bot Compute Engineの管理権限をしょっさんに付与して

この入力に対して、こちらの機能は以下のように動作します:

  1. OpenAI APIが「Compute Engineの管理権限」を roles/compute.instanceAdmin.v1 に変換
  2. 「しょっさん」を user:syossan27@example.com に変換
  3. 承認メッセージをメンション付きで投稿
  4. 承認後、7日間の期限付きで対象ユーザーに権限を付与

また、以下のように複数人への付与も可能です。お手伝い頂いているQAチームへ一時権限を付与することがあるので、チームの複数人に付与できるようにしてあるのです。

@fansta-bot Cloud SQLの管理権限を〇〇さんとしょっさんに付与して

一応、モザイク多めですが実際のスクショもぺたり。

まとめ

というわけで、今回開発したSlack botを使った一時権限付与機能のお話でした。
OpenAI APIの自然言語処理をSlack botとかけ合わせて活用することで、従来の利用に少し難があった機能を大幅に簡素化できました。

特に以下の点が重要な成果です:

  1. 自然言語を利用することでの簡便性: 知識を必要とせず、誰でも直感的に使用可能
  2. 付与対象者のメールアドレス特定や、必要な権限の検索を自動化: 手動でやらなくてはいけないことを可能な限り最小限に抑制

あと、ここまで書いておいて気付いたのですが自分用に権限検索機能も欲しくなってきました。Google Cloudで権限付与する時に、「あれ?どれ付与すれば良いんだっけ?」ってたまーになるので。

今回のように、SREの現場ではこのようにAIを有効活用して、よりDXの改善や自分たちのSRE活動の円滑化がしやすい世の中になりました。今後もAIを活用した運用自動化の取り組みを進めていく予定です!

ということで、開発チームが使いやすいように一時権限付与機能を使いやすくしてみたよという記事でした。この事例が何かの参考になれば幸いです!🙏