Okta Access Requestsの承認依頼時にミラーボールを光らせる。
はじめに
皆様こんにちは、あかいけです。
突然ですが、みなさんはミラーボールを持っていますか?私は持っています。
以前、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をすでに利用されている方を対象としています。
具体的には以下の記事のような内容を理解されている方、または実際に運用されている方です。
また、SwitchBotのミラーボールとSwitchBot APIの設定については過去のミラーボールシリーズ記事をご参照ください。
デプロイする
AWS Lambda
まずはSwitchBot APIを呼び出すAWS Lambdaを作成します。
いつもの通り、Terraformで作成します。
Terraform
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
}
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
}
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
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:ListFunctionsとlambda: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つのフローを作成します。
まずは新しいフローを作成し、トリガーを選択します。
今回はOkta Access Requestsから呼び出すため、Built-in triggersの中から「Delegated Flow」を選択します。

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

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

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

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

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

最終的に以下の2つのDelegated Flowができあがります。
invoke-lambda-mirror-ball-on:ミラーボールをONにするフローinvoke-lambda-mirror-ball-off:ミラーボールをOFFにするフロー
どちらのフローもON(有効)にしておきます。

Okta Access Requests
続いてOkta Access Requestsの設定をします。
まず、作成したOkta WorkflowsフローをAccess Requestsから呼び出せるように連携する必要がありますが、本ブログでは割愛するので、気になる方は以下ブログをご参照ください。
Access RequestsのSettings > Workflowsタブを開くと、Okta Workflowsで作成したDelegated Flowが一覧表示されます。
invoke-lambda-mirror-ball-onとinvoke-lambda-mirror-ball-offの両方が表示されていればOKです。

次に、作成済みのRequest TypeのTasks & Actionsにミラーボールのアクションを追加します。
今回は以下ブログで作成したフローを流用しています。
アクション追加時に「Run a workflow」を選択します。
「Assign individual app to user」や「Add user to a group」などと並んで「Run a workflow」が選択できます。

承認時にミラーボールを回すアクションを設定します。
タスク名を「ミラーボール、回ります。」とし、TypeをOkta「Run a workflow」、ワークフローにはinvoke-lambda-mirror-ball-onを選択します。
また承認タスク「承認者は申請内容を確認して…」の前にドラッグ&ドロップで移動しておきます。
これにより「承認タスクの前 = 承認依頼時」にミラーボールが回り始めます。

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

「ミラーボール、止まります。」アクションのLogicタブで、表示条件を設定します。
「Only show this task if」として承認タスク「承認者は申請内容を確認して…」が「is completed」の場合のみ表示されるよう設定します。
これにより承認が完了したあとにのみミラーボールOFFのアクションが実行されます。
承認もされていないのにミラーボールが止まってしまったら困りますからね。

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

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

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

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

止まりました。承認完了です。
これでミラーボールも役目を終えます。お疲れ様でした。
さいごに
以上、Okta Access RequestsとOkta Workflowsで承認依頼時にミラーボールを光らせてみました。
CloudWatch Alarm、Claude Code Hooksと続いてきたミラーボールシリーズも今回で3作目になりました。ミラーボールを軸に技術を繋いでいく、そういうスタイルで回し続けていきます。
本ブログで実装したものは、承認依頼と連動してミラーボールが回ったり止まったりする、ただそれだけのことです。
しかし日常の業務の中にこういった遊び心を入れると、なかなか楽しくなります。
権限管理という地味なオペレーションにミラーボールを持ち込む、それが今の私のパッションです。
ぜひ皆様もお手元のOkta Access Requestsに組み込んで遊んでみてください。
この記事が誰かのお役に立てば幸いです。









