はじめに
こんにちは!
セゾンテクノロジー Advent Calendar 2025 シリーズ1の21日目の記事を書かせていただきます
HULFT Squareには、主に3つの開発環境があります。
開発環境の起動停止について、主に手動対応で設定していました。
しかし、手動対応では不便な点が多く、運用課題がありました。
今回は各環境の起動停止自動化対応について、
- 背景
- 自動化の手段・実現に用いたアーキテクチャ等
- 手動化->自動化できてよかったこと
をお伝えできればと思います。
HULFT Squareの開発環境について
HULFT Squareでは、開発環境として
①dev環境
②integration環境
③preprod環境
の3つを利用して開発・検証を進めております。
①dev環境
開発者が自由に構築・運用できる検証・動作確認用環境
↓
②integration環境
dev環境で検証したアプリケーションの追加・更新・修正等を反映
↓
③preprod環境
本番環境とほぼ同じ構成
リリース前のリハーサルや本番準拠の検証等で利用
↓
本番環境
リハーサルを経て追加機能・修正内容がデプロイされる
HULFT Squareでは、
Common (全体のマイクロサービスを管轄)
Customer (ご契約中のお客様用HULFT Square環境)
App Square (App Square用マイクロサービスを管轄)
といった3種類のAWS環境で起動しております。
HULFT Square起動に必要なAWSリソースとして、
- EKS Pod (EKS Fargate)
- EC2サーバー
- RDS
があり、HULFT Squareの起動中は常時立ち上げている必要があります。
手動起動停止の流れ
EKS Pod・EC2・RDSは起動中に常時コストがかかる為、
夜間帯や休日には停止するような仕組みが導入されています。
[EKS Pod]
EC2内に設置したcrontabを用いて起動停止スケジュールを予定時刻に沿ってシェル・Pythonスクリプト等を実行
◾️停止時
シェルを用いてPodを停止して、現在のレプリカ数を記録
↓
◾️起動時
記録されたレプリカ数を元にPodを起動
[EC2・RDS]
対象アカウントのリソースをEventBridgeを用いて
定めたスケジュール通りに起動・停止
といったことを行っています
手動対応の流れとしては、以下の①②③を行います
①利用したい方から起動設定依頼を受ける。起動停止用の予定表で日時を確認する。
②EC2・RDS起動用のEventBridgeを起動時刻に設定
③EC2のcrontabを編集 (Common・Customer・App Square環境分)
この対応の課題として、
- EventBridgeの日時設定追加・修正
- crontabの日時設定追加・修正
を都度コンソール等から行う必要があります。
その結果、以下のデメリットが露呈していました。
- 起動/停止設定に手間がかかる
- 設定ミスをする可能性がある
- 操作手順が複雑で属人化しやすい
これらの課題を解決するために、自動化への挑戦を決意して設計・実装を開始しました
自動化への挑戦 (SlackBot)
自動化の設計をするにあたり、次の要望は叶えたいと考えました
- 誰でも簡単に操作できるようにしたい
→現状AWSの知識がある人しか操作できない
→緊急の休日対応等で開発者が咄嗟に使えないことが起こり得る
- 共通のプラットフォームで管理したい
- 実行履歴が簡単に確認できるようにしたい
ここで思いついたのが、Slackチャンネル内でコマンドから
Slack APIを呼び出してBotのように利用する方法です。
選択メニューを表示して、選んだ環境ごとに
起動・停止できれば課題が解決できるのではないかと考えました。
SlackBotによる自動化のアーキテクチャ
以下の構成で実装しました
[構成図]
[Step Functions Flow]
[使用リソース]
- Slack SDK・Slack API
- API gateway
- Step Functions
- Lambda × 7
<処理の流れ>
①Slack Bot
slackチャンネルからコマンド実行して入力する
→利用者がチャンネル上で専用のコマンド(/hsq)を実行
②API Gateway / Lambda
API Gatewayエンドポイント宛にリクエストが送信される
→Slack Bolt起動用のLambdaが稼働
③Slack Bolt
Lambdaのコード内でSlack Boltを呼び出す
→Slack Boltのモーダルからチャンネル上に入力フォームが表示される
設定したい内容をフォームに入力 (環境名・起動 or 停止)
→パラメータをStep Functionsに渡す
④Step Functions / Lambda
Step Functions起動用のLambdaが稼働する
→Step Functionsから各Lambdaを呼び出し実行する
Podの起動/停止
→SSMコマンドでEC2内のスクリプトを実行
※コマンドの実行結果が出るまでループするLambdaを実行
Podの自動削除 (停止時のみ)
→SSMコマンドでEC2内のスクリプトを実行
※コマンドの実行結果が出るまでループするLambdaを実行
EC2 /RDSの起動停止
※RDSのステータスをチェックして停止が確認できてから次のステップへ進める
⑤Slack Bot
完了通知が実行ユーザー宛に投稿される
※SSMコマンドの実行でエラーがあった場合もSlackチャンネルに通知される
作成したリソース・実装の紹介 (Slack Bot)
[API Gateway]
◾️リソースを作成
・POSTメソッドを作成
・リクエストバリデータを作成
・HTTPリクエストヘッダーにX-Slack-Request-Timestamp, X-Slack-Signatureを設定
◾️ステージを作成
・レート制限を設定
◾️オーソライザー
・呼び出し先のLambdaで認証
[Slack API]
◾️Slash Commands
・起動停止用slackチャンネルで利用するコマンドの登録
・API Gatewayで作成したURLをRequest URLに設定
◾️App-Level Tokens
・トークンを作成してScopeにconnections:writeを設定
[Slack Bolt & Step Functions呼び出し用Lambda]
環境変数として以下を登録
・起動停止を実行するslackチャンネルのID
・Signing Secret
・Step FunctionsのARN
◾️Slackリクエストの署名検証
<役割>
・なりすまし防止
・改ざん検出
・タイムスタンプ検証
def verify_***(event):
"""Verify the signature of a Slack request"""
logger.info(f"Event: {json.dumps(event)}")
headers = event.get('headers', {})
body = event.get('body', '')
logger.info(f"Headers: {json.dumps(headers)}")
logger.info(f"Body: {body}")
logger.info(f"X-Slack-Request-Timestamp: {timestamp}")
logger.info(f"X-Slack-Signature: {slack_signature}")
# リクエスト認証に失敗したらエラー
if not timestamp or not slack_signature:
logger.error("Timestamp or Slack signature is missing.")
return False
# タイムスタンプの検証
current_time = int(time.time())
if abs(current_time - int(timestamp)) > 60 * 5:
logger.error("Request timestamp is too old.")
return False
#署名の比較・検証
basestring = f"v0:{timestamp}:{body}".encode('utf-8')
logger.info(f"Basestring: {basestring}")
signature = 'v0=' + hmac.new(
SIGNING_SECRET.encode('utf-8'),
basestring,
hashlib.sha256
).hexdigest()
logger.info(f"Generated Signature: {signature}")
if hmac.compare_digest(signature, slack_signature):
logger.info("Signatures match.")
return True
else:
logger.error("Signatures do not match.")
return False
※実装の詳細は以下の記事で詳しく解説されてます
https://zenn.dev/t_kakei/articles/f61196a47f9b14
https://zenn.dev/nijigen_plot/articles/create_slack_app
◾️リクエストによる処理の分岐
2種類のリクエストを確認
①Slackコマンドからの初回呼び出し (API Gateway経由)
②Slackのmodalから選択されたリクエスト (環境名・起動or停止)
# リクエストがSlackのモーダルから送られた場合
if body.get('type') == 'view_submission':
logger.info("Processing view submission")
return handle_view_submission(body)
# リクエストがSlack Commandから送られた場合(初回)
elif body.get('command'):
logger.info("Processing slash command")
return handle_slack_command(body)
else:
logger.error(f"Unknown request type: {body}")
return {
'statusCode': 400,
'body': json.dumps("Unknown request type"),
'headers': {'Content-Type': 'application/json'}
}
◾️Slack Modalを呼び出して起動/停止したい環境を指定する
trigger_id = body.get('trigger_id')
channel_id = body.get('channel_id')
# 起動停止用slackチャンネル以外からだとエラー
if channel_id not in ALLOWED_CHANNELS:
logger.error(f"Unauthorized channel access: {channel_id}")
return {
'statusCode': 403,
'body': json.dumps("Unauthorized channel access."),
'headers': {'Content-Type': 'application/json'}
}
try:
# Creating a modal view and adding private_metadata
view = build_modal_view()
view["private_metadata"] = json.dumps({"channel_id": channel_id})
response = client.views_open(
trigger_id=trigger_id,
view=view
)
logger.info(f"Modal response: {response}")
return {
'statusCode': 200,
'body': json.dumps("Request verified and modal opened successfully."),
'headers': {'Content-Type': 'application/json'}
}
except Exception as e:
logger.error(f"Error opening modal: {e}")
return {
'statusCode': 500,
'body': json.dumps(f"Error opening modal: {str(e)}"),
'headers': {'Content-Type': 'application/json'}
}
Slackコマンドによるモーダルの呼び出し
入力フォームから
・起動or停止
・環境
を選択して送信
送信すると内容が通知されて投稿される
◾️モーダル送信処理を受けてStep Functionsを実行
def ***_submission(body):
# ユーザー選択の取得
action = state_values["action_block"]["action_select"]["selected_option"]["value"]
env = state_values["env_block"]["env_select"]["selected_option"]["value"]
# AWS設定の取得
common_id = aws_id[env]['common_id']
appsquare_id = aws_id[env]['appsquare_id']
# Parameter Storeから値取得
common_ec2 = ***_value(f"/{env}/common_ec2")
common_rds = ***_value(f"/{env}/common_rds")
appsquare_ec2 =***_value(f"/{env}/appsquare_ec2")
appsquare_rds = ***_value(f"/{env}/appsquare_rds")
# ペイロード作成
## user_idで実行者のidを渡す
payload = {
'action': action,
'environment': env,
'common_ec2': [common_ec2],
'appsquare_ec2': [appsquare_ec2],
'common_rds': common_rds,
'appsquare_rds': appsquare_rds,
'common_id': common_id,
'appsquare_id': appsquare_id,
'user_id': user_id
}
# Step Functions実行
response = sfn_client.start_execution(
stateMachineArn=***_ARN,
input=json.dumps(payload)
)
◾️パラメータ取得
ユーザーの選択(アクション・環境)を取得
◾️AWS設定取得
Parameter Storeから必要な設定値を取得
◾️Step Functions実行
実際の起動/停止処理を開始
◾️通知送信
Slackチャンネルへの結果通知
リクエストを受けてStep Functionsを実行
以降は、Step FunctionsでこれらのLambdaを順次実行
・Podの起動停止
・EC2・RDSの起動停止 (+ステータスチェック)
・Podの自動削除 (停止時のみ)
・完了通知
全ての動作が終了したら、完了通知が送られる
SlackBotによる自動化の改善効果
自動化による成果です
①起動/停止設定に手間がかかる
→対応速度UP
1回の設定に約9~10分 -> 最短 数10秒〜1,2分程度に短縮
②設定ミスをする可能性がある
→品質向上
極力ミスが起きない仕組み化、設定不備も確認しやすい状態に
③操作手順が複雑で属人化しやすい
→属人化解消
操作方法が簡単でSlack・Outlookから誰でも起動/停止できる仕組みに
既存のPod起動停止用スクリプト等で細かい課題があるものの、
自動化が達成できて作業性が大きく向上しました!
しかし、新たな課題が出てきます..
"スケジュール登録して起動できないのか??"
"利用予定がスケジュール上でみれるといいよね"
"当日に利用予定をお知らせしてくれると助かる"
これらの課題を解決すべく考えた結果、多くの社員が共通で利用してるOutlookスケジュール上の予定を読み取って、起動停止が自動化できればよいのではと考えました。
自動化への挑戦 (Outlookのスケジュール起動・停止)
Outlookを利用するメリットは以下の点があります
- みんなが使えるプラットフォーム
- スケジュール管理ができる
HULFT Squareの起動停止自動化の更なる改善に向けて、Outlookのスケジュールを読み取って作成していた起動停止処理を自動化できないか、試行錯誤が始まりました。
Outlookのスケジュール読み取りには、Microsoft Graph APIのカレンダー読み取り権限が必要なので作成します。
(Microsoft Graph API.Calendars.Read)
https://learn.microsoft.com/ja-jp/graph/api/calendar-get?view=graph-rest-1.0&tabs=http
SlackBotによる自動化のアーキテクチャ
以下の構成で作成しました
[使用リソース]
- Microsoft Graph API
- Lambda × 3
- EventBridge (自動生成)
- Step Functions (SlackBot用を呼び出し)
①Outlook
対象環境の専用メールボックスを出席者登録・日時設定してスケジュール登録
②Lambda (Outlookスケジュール読み込み・登録用)
1時間ごとに起動させて
Outlookの予定を読み取って以下の処理を実行
・予定時刻に起動/停止させる用のEventBridgeを作成
・EventBridgeには起動/停止環境用のパラメータを付与
・Step Function呼び出し用Lambdaの起動用ポリシーを生成
③EventBridge
Step Functions起動用のLambdaを実行
④Lambda (Step Functions呼び出し用)
EventBridgeから呼び出されてStep Functionsを呼び出す
⑤Step Functions
実行に必要なパラメータを元に各起動停止用のLambdaを起動する
作成したリソース・実装の紹介 (Outlook)
[Outlook Microsoft Graph API.Calendars.Read]
Microsoft Entraからアプリの登録画面で"新規登録"から権限を付与するアプリケーションを作成
↓
作成したアプリケーションから、"APIのアクセス許可"を選択して"アクセス許可"の追加を選択
↓
"Microsoft Graph"を追加
↓
"Calendars.Read"と"User.Read" (運用で他者作成の予定編集用)を追加
登録されたスケジュールを取得
[Outlookスケジュール管理用Lambda]
スケジュール登録用のLambdaが実行される
環境変数の設定
①Microsoft Graph API用の環境変数を設定
②AWS・Slackチャンネルの環境変数を設定
③Outlookメールアドレスを設定
④EventBridgeルールの作成数上限値設定
# ①Microsoft Graph API Settings
CLIENT_ID = os.environ['CLIENT_ID']
CLIENT_SECRET = os.environ['CLIENT_SECRET']
TENANT_ID = os.environ['TENANT_ID']
AUTHORITY = f'https://login.microsoftonline.com/{TENANT_ID}'
RESOURCE = 'https://graph.microsoft.com/'
API_VERSION = 'v1.0'
# ②AWS Settings
EXECUTOR_ARN = 'arn:aws:lambda:ap-northeast-1:****:function:****'
SLACK_TOKEN = os.environ['SLACK_TOKEN']
CHANNEL_ID = '****'
# ③Environment configuration
OUTLOOK_EMAILS = {
'preprod': '***preprod.com',
'integration': '***integration.com',
'dev1': '***dev1.com'
}
# ④Rule creation limit
MAX_RULES = 40 # Maximum rules to create per execution
TOTAL_RULES = 270 # Maximum total rules in AWS environment
(Outlookスケジュール管理用のLambdaは1時間ごとに実行されている)
Outlookの予定追加・変更・削除等があると、応じた処理が実行される
追加・変更がない場合はLambdaの動作はスキップされる
<主な処理>
Outlookカレンダーから処理を読み取ってEventBridgeルール作成
Microsoft Graph API認証
def get_access_token():
# OAuth 2.0 Client Credentials Flowでアクセストークンを取得
url = f'{AUTHORITY}/oauth2/v2.0/token'
payload = {
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'grant_type': 'client_credentials',
'scope': 'https://graph.microsoft.com/.default'
}
Outlookカレンダー読み取り
def get_week_events(access_token, env):
# 環境別のOutlookメール取得
outlook_email = OUTLOOK_EMAILS.get(env)
# 7日前から60日先までのイベントを取得
range_start = today_start - datetime.timedelta(days=7)
range_end = today_start + datetime.timedelta(days=60)
# calendarViewで繰り返しイベントも展開して取得
url = f'https://graph.microsoft.com/{API_VERSION}/users/{outlook_email}/calendar/calendarView'
イベントの検証とフィルタリング
def is_cancelled(event):
# キャンセルされたイベントをチェック
subject = event.get('subject', '')
return (event.get('isCancelled', False) or
'キャンセル済み' in subject or
'cancelled' in subject.lower())
def parse_time(event):
# 日本時間(JST)に変換
jst = zoneinfo.ZoneInfo("Asia/Tokyo")
start_dt = datetime.datetime.fromisoformat(start_time.replace('Z', '+00:00'))
return start_dt.astimezone(jst), end_dt.astimezone(jst)
EventBridgeルール名の生成
def create_pattern(env, action, date_jst, time_jst):
date_str = date_jst.strftime('%Y%m%d')
time_str = time_jst.strftime('%H%M')
return f"{env}-{action}-{date_str}-{time_str}"
既存のルールと比較して新規ルールを作成
for env in environments:
# Outlook カレンダーからイベント取得
events = get_week_events(access_token, env)
# 既存ルールとの比較・不整合検出
mismatched_rules = compare_rules(access_token, env, existing_rules)
# 新規EventBridgeルール作成
created_rules = setting_schedule(access_token, env, existing_rules, today_str, today)
Step Functions用ペイロード作成
def send_payload(action, env):
# Parameter Storeから設定値を取得
common_ec2 = ***_value(f"/{env}/common_ec2")
common_rds = ***_value(f"/{env}/common_rds")
payload = {
'action': action,
'environment': env,
'common_ec2': [common_ec2],
'appsquare_ec2': [appsquare_ec2],
'common_rds': common_rds,
'appsquare_rds': appsquare_rds,
'user_id': 'outlook-automation'
}
EventBridgeルールとLambda権限の作成
def eventbridge(rule_name, schedule_expression, input_data, existing_rules):
# EventBridgeルールの作成
events_client.put_rule(
Name=rule_name,
ScheduleExpression=schedule_expression, # cron式
State='ENABLED',
Description=f'Automation schedule for {input_data["action"]} - {input_data["env"]}'
)
# Lambdaターゲットの設定
events_client.put_targets(
Rule=rule_name,
Targets=[{
'Id': '1',
'Arn': ***LAMBDA_ARN,
'Input': json.dumps(input_data)
}]
)
# Lambda実行権限の付与
stmt_id = f'{rule_parts}' # 例: preprodstart202512211000
lambda_client.add_permission(
FunctionName=***LAMBDA_ARN,
StatementId=stmt_id,
Action='lambda:InvokeFunction',
Principal='events.amazonaws.com'
)
Lambda実行後
予定したスケジュールに起動するEventBridgeが作成される
登録された予定がslackで通知される
EventBridge
設定した時刻でスケジュールされるよう自動生成されたEventBridge
Step Fucntionsに引き渡すパラメータ
EventBridgeから呼び出されるStep Functions起動用のLambda
Slack通知用の処理
def post_slack(message, action, env, user_id='outlook-automation'):
"""SLACK_BOT_TOKENによる認証"""
if not SLACK_TOKEN:
logger.info("SLACK_TOKEN not configured, skipping Slack notification")
return
action_text = "Start / 起動" if action == "start" else "Stop / 停止"
environment_text = env
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {SLACK_TOKEN}'
}
#slack通知
payload = {
'channel': SLACK_CHANNEL_ID,
'text': f"Process has been started / 処理が開始されました: Environment / 環境: {environment_text}, Action / アクション: {action_text}",
'blocks': [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":gear: *HULFT Square スケジュール実行開始 / Automated Process Started*"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": f"*Action / アクション:*\n{action_text}"
},
{
"type": "mrkdwn",
"text": f"*Environment / 環境:*\n{environment_text}"
}
]
}
]
}
try:
response = requests.post('https://slack.com/api/chat.postMessage', headers=headers, json=payload)
logger.info(f"Slack post response status: {response.status_code}")
if response.status_code == 200:
response_data = response.json()
if response_data.get('ok'):
logger.info("Slack notification sent successfully")
else:
logger.error(f"Slack API error: {response_data.get('error', 'Unknown error')}")
else:
logger.error(f"Slack HTTP error: {response.status_code}")
except Exception as e:
logger.error(f"Error sending Slack notification: {str(e)}")
Step Functionsの呼び出し処理
def **_step_functions(payload, execution_name):
logger.info(f"Step Functions execution started: {payload.get('action', 'unknown')}")
try:
client = boto3.client('stepfunctions')
# EventBridgeの定数をpayloadとしてStep Functionsに渡す
response = client.start_execution(
stateMachineArn=STEP_FUNCTIONS_ARN,
name=execution_name,
input=json.dumps(payload)
)
logger.info(f"Step Functions execution success: {response['executionArn']}")
return response['executionArn']
except Exception as e:
logger.error(f"Step Functions execution error: {str(e)}")
return None
予定時刻になると、以下のようにslackチャンネルに投稿されて、
Step Fucntionsの処理が開始される
以上がOutlookの予定を読み取った起動停止の自動化処理になります
事前に時間帯を決めて、起動停止設定をすることがとても簡単・手軽になりました
Outlookスケジュール自動化の改善効果
以下の点が改善されました
◾️Outlookの予定から簡単に起動停止予定を登録できるようになった
今まで行っていたEventBridge・Cronの手動スケジュール設定から完全脱却
誰でも簡単に起動停止予定を設定できるようになった
◾️起動停止スケジュールの見える化
カレンダーから起動停止予定を可視化できるようになった
◾️通知機能
slackの自動投稿機能を利用して、当日の利用予定が毎朝確認できる
自動化のススメ
当初思っていたよりも遥かに壮大なAWSリソースの構成になってしまいましたが、
すごく便利になりました。運用面でも時短で対応できてとても助かっています。
しかしながら、運用ルールの整備やPod起動停止処理の改善など、まだまだ課題が残っています。引き続き改善を進めていこうと思います。
皆様も是非自動化を推進して、注力したい業務に集中できるようにしていきましょう!
















