Okta Access Requestsの承認依頼時にミラーボールを光らせる。

Okta Access Requestsの承認依頼時にミラーボールを光らせる。

2026.05.05

はじめに

皆様こんにちは、あかいけです。
突然ですが、みなさんはミラーボールを持っていますか?私は持っています。

以前、CloudWatch Alarmのアラートをトリガーにミラーボールを光らせる記事を書きました。
その後、Claude Code Hooksをトリガーにミラーボールを光らせる記事も書きました。
ミラーボールシリーズ、着々と進んでいます。

そして今回はついに「Okta Access RequestsとOkta Workflowsで承認依頼時にミラーボールを光らせる」という、ミラーボールシリーズ第3弾をお届けします。

なぜ承認時にミラーボールを回す必要があるのか

しかし何故、Okta Access Requestsの承認時に自宅のミラーボールを光らせる必要があるのでしょうか。全然意味がわかりません。

Okta Access Requestsは、権限申請のワークフローを管理するサービスです。
誰かが「この権限をください」と申請し、承認者がそれを承認する、そのフローを自動化できます。

承認する側の立場に立ってみましょう。
大量の承認依頼が飛んでくる中、黙々とApproveボタンを押し続ける日々。しかしメール通知では気づかず、承認が積み上がっていくことも…。

もし申請が届いた瞬間に部屋のミラーボールが光り出し、Approveして止まるとしたら…。

少なくとも承認漏れはなくなりませんか? 承認作業も少しだけ楽しくなりませんか?

というわけで今回は承認者側の立場になって、申請が届いたらミラーボールをON、承認完了でOFFにする仕組みをOkta Access Requests + Okta Workflows + AWS Lambda + SwitchBot APIで実装してみました。

前提条件

本記事はOkta Access Requestsをすでに利用されている方を対象としています。
具体的には以下の記事のような内容を理解されている方、または実際に運用されている方です。

https://dev.classmethod.jp/articles/okta_identity_center_sso/
https://dev.classmethod.jp/articles/okta-access-requests/

また、SwitchBotのミラーボールとSwitchBot APIの設定については過去のミラーボールシリーズ記事をご参照ください。

https://dev.classmethod.jp/articles/cloud-watch-alarm-mirror-ball-tips/
https://dev.classmethod.jp/articles/claude-code-hooks-mirror-ball-tips/

デプロイする

AWS Lambda

まずはSwitchBot APIを呼び出すAWS Lambdaを作成します。
いつもの通り、Terraformで作成します。

Terraform
main.tf
terraform {
  required_version = ">= 1.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 6.0"
    }
    archive = {
      source  = "hashicorp/archive"
      version = ">= 2.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

data "archive_file" "lambda_zip" {
  type        = "zip"
  output_path = "${path.module}/lambda_function.zip"
  source {
    content  = file("${path.module}/lambda_function.py")
    filename = "lambda_function.py"
  }
}

resource "aws_iam_role" "lambda_execution_role" {
  name = "mirror-ball-lambda-execution-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
  role       = aws_iam_role.lambda_execution_role.name
}

resource "aws_lambda_function" "mirror_ball_controller" {
  filename         = data.archive_file.lambda_zip.output_path
  function_name    = "mirror-ball-controller"
  role             = aws_iam_role.lambda_execution_role.arn
  handler          = "lambda_function.lambda_handler"
  runtime          = "python3.13"
  timeout          = 30
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256

  environment {
    variables = {
      SWITCHBOT_TOKEN  = var.switchbot_token
      SWITCHBOT_SECRET = var.switchbot_secret
      DEVICE_ID        = var.device_id
    }
  }

  depends_on = [
    aws_iam_role_policy_attachment.lambda_basic_execution,
  ]
}

resource "aws_iam_user" "okta_workflow" {
  name = "okta-workflow-user"
}

resource "aws_iam_policy" "okta_workflow_lambda" {
  name        = "okta-workflow-lambda-policy"
  description = "Allows Okta Workflows Lambda connector to list and invoke the mirror ball controller"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid      = "ListLambdaFunctions"
        Effect   = "Allow"
        Action   = "lambda:ListFunctions"
        Resource = "*"
      },
      {
        Sid      = "InvokeMirrorBallController"
        Effect   = "Allow"
        Action   = "lambda:InvokeFunction"
        Resource = aws_lambda_function.mirror_ball_controller.arn
      }
    ]
  })
}

resource "aws_iam_user_policy_attachment" "okta_workflow_lambda" {
  user       = aws_iam_user.okta_workflow.name
  policy_arn = aws_iam_policy.okta_workflow_lambda.arn
}

resource "aws_iam_access_key" "okta_workflow" {
  user = aws_iam_user.okta_workflow.name
}
variables.tf
variable "aws_region" {
  description = "AWS region"
  type        = string
  default     = "ap-northeast-1"
}

variable "switchbot_token" {
  description = "SwitchBot API token"
  type        = string
  sensitive   = true
}

variable "switchbot_secret" {
  description = "SwitchBot API secret"
  type        = string
  sensitive   = true
}

variable "device_id" {
  description = "SwitchBot device ID for the mirror ball"
  type        = string
}
outputs.tf
output "lambda_function_name" {
  description = "Name of the mirror ball controller Lambda function"
  value       = aws_lambda_function.mirror_ball_controller.function_name
}

output "lambda_function_arn" {
  description = "ARN of the mirror ball controller Lambda function"
  value       = aws_lambda_function.mirror_ball_controller.arn
}

output "okta_workflow_access_key_id" {
  description = "Access key ID for the Okta Workflows IAM user"
  value       = aws_iam_access_key.okta_workflow.id
}

output "okta_workflow_secret_access_key" {
  description = "Secret access key for the Okta Workflows IAM user (sensitive)"
  value       = aws_iam_access_key.okta_workflow.secret
  sensitive   = true
}

Lambda関数本体はPythonで実装します。

Python
lambda_function.py
import time
import hashlib
import hmac
import base64
import json
import os
import urllib.request
import urllib.error

def lambda_handler(event, context):
    token = os.environ.get("SWITCHBOT_TOKEN")
    secret = os.environ.get("SWITCHBOT_SECRET")
    device_id = os.environ.get("DEVICE_ID")

    # Support both direct invocation and API Gateway proxy integration
    if isinstance(event.get("body"), str):
        body = json.loads(event["body"])
    elif isinstance(event, dict):
        body = event
    else:
        body = {}

    action = body.get("action", "").lower()

    if action == "on":
        command = "turnOn"
    elif action == "off":
        command = "turnOff"
    else:
        return {
            "statusCode": 400,
            "body": json.dumps(
                {
                    "message": f'Invalid action: "{action}". Expected "on" or "off".',
                    "success": False,
                },
                ensure_ascii=False,
            ),
        }

    try:
        t = int(round(time.time() * 1000))
        nonce = ""
        string_to_sign = bytes(f"{token}{t}{nonce}", "utf-8")
        secret_bytes = bytes(secret, "utf-8")
        sign = base64.b64encode(
            hmac.new(secret_bytes, msg=string_to_sign, digestmod=hashlib.sha256).digest()
        ).decode("utf-8")

        headers = {
            "Authorization": token,
            "t": str(t),
            "sign": sign,
            "nonce": nonce,
            "Content-Type": "application/json",
        }

        payload = json.dumps({"command": command, "commandType": "command"}).encode("utf-8")
        url = f"https://api.switch-bot.com/v1.1/devices/{device_id}/commands"

        req = urllib.request.Request(url, data=payload, headers=headers, method="POST")
        with urllib.request.urlopen(req, timeout=10) as response:
            status_code = response.getcode()
            response_body = json.loads(response.read().decode("utf-8"))

        return {
            "statusCode": status_code,
            "body": json.dumps(
                {
                    "message": f'Command "{command}" sent successfully.',
                    "device_id": device_id,
                    "switchbot_response": response_body,
                    "success": True,
                },
                ensure_ascii=False,
            ),
        }

    except urllib.error.HTTPError as e:
        error_body = e.read().decode("utf-8")
        return {
            "statusCode": e.code,
            "body": json.dumps(
                {
                    "message": f"HTTP error occurred: {e.code}",
                    "error": error_body,
                    "success": False,
                },
                ensure_ascii=False,
            ),
        }

    except Exception as e:
        return {
            "statusCode": 500,
            "body": json.dumps(
                {
                    "message": "Unexpected error occurred.",
                    "error": str(e),
                    "success": False,
                },
                ensure_ascii=False,
            ),
        }

actionパラメータに"on"または"off"を渡すことで、ミラーボールのON/OFFを制御します。またSwitchBot APIはHMAC-SHA256による署名認証が必要なため、Lambda内でタイムスタンプと署名を生成しています。

まとめるとTerraformで作成するリソースは以下の通りです。

  • aws_lambda_function:ミラーボールを制御するLambda関数
  • aws_iam_role / aws_iam_role_policy_attachment:Lambda実行ロール
  • aws_iam_user + aws_iam_policy + aws_iam_user_policy_attachment:Okta Workflowsが使用するIAMユーザーとポリシー(lambda:ListFunctionslambda:InvokeFunctionのみに絞って許可)
  • aws_iam_access_key:Okta WorkflowsのLambdaコネクタ設定に使用するアクセスキー

なお今回はお遊びのためIAMユーザーのアクセスキーも含めてTerraformリソースで作成していますが、tfstateにアクセスキーが平文で記載されてしまうため、本番運用では絶対にやらないでください。

terraform applyしたら以下コマンドでアクセスキーを取得できます。
後ほど使うのでメモしておきます。

# アクセスキー ID
terraform output okta_workflow_access_key_id

# シークレットアクセスキー
terraform output -raw okta_workflow_secret_access_key

Okta Workflows

次に、Okta WorkflowsでAWS Lambdaを呼び出すフローを作成します。
今回はミラーボールのON/OFFそれぞれ1つずつ、合計2つのフローを作成します。

https://dev.classmethod.jp/articles/okta-workflows-lambda-connector/

まずは新しいフローを作成し、トリガーを選択します。
今回はOkta Access Requestsから呼び出すため、Built-in triggersの中から「Delegated Flow」を選択します。

スクリーンショット 2026-05-05 4.03.56

「Okta - Delegated Flow」をトリガーに選択したら、「Then do this」のアクションとしてAWS Lambdaコネクタを追加します。
右側にAWS Lambdaの「Invoke」(Lambda関数を呼び出す)と「List Functions」が表示されるので、「Invoke」を選択します。

スクリーンショット 2026-05-05 4.04.09

AWS Lambdaコネクタを初めて使う場合は接続情報を設定します。
TerraformのOutputsに出力されたIAMユーザーのアクセスキーIDとシークレットアクセスキー、およびリージョンを入力して接続を作成します。

スクリーンショット 2026-05-05 4.04.29

接続設定が完了したら「Invoke」アクションの設定に入ります。
「Use Function Name?」をNoに設定すると、接続したAWSアカウントに存在するLambda関数の一覧が「Your Functions」ドロップダウンに表示されます。
先ほどデプロイしたmirror-ball-controllerを選択します。

スクリーンショット 2026-05-05 4.11.40

続いてパラメータを設定します。payload{ "action": "on" }を入力します。
これがLambda関数に渡されるJSONで、ミラーボールをONにする指示となります。

スクリーンショット 2026-05-05 4.12.02

フローを保存した後に、フローのメニューから「Duplicate」をクリックしてコピーを作成します。
作成したコピーはpayload{ "action": "off" }にしておきます。

スクリーンショット 2026-05-05 4.12.52

最終的に以下の2つのDelegated Flowができあがります。

  • invoke-lambda-mirror-ball-on:ミラーボールをONにするフロー
  • invoke-lambda-mirror-ball-off:ミラーボールをOFFにするフロー

どちらのフローもON(有効)にしておきます。

スクリーンショット 2026-05-05 4.13.46

Okta Access Requests

続いてOkta Access Requestsの設定をします。
まず、作成したOkta WorkflowsフローをAccess Requestsから呼び出せるように連携する必要がありますが、本ブログでは割愛するので、気になる方は以下ブログをご参照ください。

https://dev.classmethod.jp/articles/okta-access-requests-request-type-okta-workflow/

Access RequestsのSettings > Workflowsタブを開くと、Okta Workflowsで作成したDelegated Flowが一覧表示されます。
invoke-lambda-mirror-ball-oninvoke-lambda-mirror-ball-offの両方が表示されていればOKです。

スクリーンショット 2026-05-05 4.14.54

次に、作成済みのRequest TypeのTasks & Actionsにミラーボールのアクションを追加します。
今回は以下ブログで作成したフローを流用しています。

https://dev.classmethod.jp/articles/okta-access-requests-aws-jit-multi-day-workflow/

アクション追加時に「Run a workflow」を選択します。
「Assign individual app to user」や「Add user to a group」などと並んで「Run a workflow」が選択できます。

スクリーンショット 2026-05-05 4.16.29

承認時にミラーボールを回すアクションを設定します。
タスク名を「ミラーボール、回ります。」とし、TypeをOkta「Run a workflow」、ワークフローにはinvoke-lambda-mirror-ball-onを選択します。

また承認タスク「承認者は申請内容を確認して…」の前にドラッグ&ドロップで移動しておきます。
これにより「承認タスクの前 = 承認依頼時」にミラーボールが回り始めます。

スクリーンショット 2026-05-05 4.16.53

次に承認完了後にミラーボールを止めるアクションも追加します。
タスク名を「ミラーボール、止まります。」とし、ワークフローはinvoke-lambda-mirror-ball-offを選択します。

スクリーンショット 2026-05-05 4.17.05

「ミラーボール、止まります。」アクションのLogicタブで、表示条件を設定します。
「Only show this task if」として承認タスク「承認者は申請内容を確認して…」が「is completed」の場合のみ表示されるよう設定します。

これにより承認が完了したあとにのみミラーボールOFFのアクションが実行されます。
承認もされていないのにミラーボールが止まってしまったら困りますからね。

スクリーンショット 2026-05-05 4.17.14

申請してみる

以上で設定が完了しました。
では実際に申請を出してみましょう。

申請フォームに申請理由を書き添えて「Submit new request」をクリックします。
今回は「ミラーボールを回したいです。回させてください。お願いします。お願いします。お願いします。」と入力しました。気持ちが溢れています。

スクリーンショット 2026-05-05 4.21.23

申請が送信されると、「ミラーボール、回ります。」アクションが実行されます。
承認者が判断を下すより先に、ミラーボールが光り出します。

before-approval

回っています。キレイだね…。

ミラーボールが光り続ける中、承認者側の画面では承認タスクが「IN PROGRESS」で表示されています。
Approveするか、Denyするか、今それが問われています…。

スクリーンショット 2026-05-05 4.22.31

承認が完了すると「ミラーボール、止まります。」アクションが実行されます。

after-approval

止まりました。承認完了です。
これでミラーボールも役目を終えます。お疲れ様でした。

さいごに

以上、Okta Access RequestsとOkta Workflowsで承認依頼時にミラーボールを光らせてみました。
CloudWatch Alarm、Claude Code Hooksと続いてきたミラーボールシリーズも今回で3作目になりました。ミラーボールを軸に技術を繋いでいく、そういうスタイルで回し続けていきます。

本ブログで実装したものは、承認依頼と連動してミラーボールが回ったり止まったりする、ただそれだけのことです。
しかし日常の業務の中にこういった遊び心を入れると、なかなか楽しくなります。
権限管理という地味なオペレーションにミラーボールを持ち込む、それが今の私のパッションです。

ぜひ皆様もお手元のOkta Access Requestsに組み込んで遊んでみてください。
この記事が誰かのお役に立てば幸いです。


製造業のクラウド活用とデジタル化を支援します

クラスメソッドの専門家による包括的なクラウド導入とデジタル化支援で、製造業の業務効率を最大化しましょう。AWSの導入から運用、最適化まで、最新技術と豊富な知見であらゆる課題に対応します。生産ラインのデジタル化やデータ活用、IoTの導入事例もございます。ぜひ、弊社の実績をご覧ください。

製造業界での支援内容を見る

この記事をシェアする

関連記事