目次
概要
メンテナンスモードとは?
メンテナンスモードは、システムのメンテナンス中に一般ユーザーのアクセスを制限し、特定のIPアドレスからのアクセスのみを許可する機能です。この機能により、以下のことが実現できます:
- 🔒 一般ユーザーへのメンテナンス画面表示
- ✅ 管理者の作業用IPアドレスからの通常アクセス維持
- ⏰ 設定された期間のみの制御
- 🔄 複数のアプリケーション(CS/CL/Tool)への個別適用
Cloudformation を利用すればIaCでコード管理もできます。
なぜ必要か?
- システムの安全な更新作業の実施
- ユーザーへの明確な情報提供
- メンテナンス中の意図しないアクセスの防止
- 作業者の通常アクセス確保
対象アプリケーションの説明
メンテナンスモードは以下の3つのタイプのアプリケーションそれぞれに対して個別に制御可能です:
-
CS (Customer Service) - エンドユーザー向けサイト
- 一般ユーザー向けのメインサイト
- サービスの主要機能を提供する公開Webサイト
- 最も多くのユーザーが利用するため、メンテナンス時の影響が大きい
- 24時間365日のサービス提供が期待される
-
CL (Client) - ビジネスユーザー向けサイト
- 取引先・パートナー企業向けの専用サイト
- 情報登録・管理などのビジネス機能を提供
-
Tool - 管理者向けサイト
- システム運用管理者向けの内部ツール
- システム設定や各種管理機能を提供
これらのサイトは異なるユーザー層を持つため、メンテナンス時の対応も個別に検討する必要があります。例えば:
- CSは一般ユーザーへの影響を最小限にするため、深夜帯でのメンテナンスを推奨
- CLは利用企業の業務時間外での実施を考慮
- Toolは運用管理業務への影響を見ながら柔軟に対応
この構成は様々な業種・業態のWebサービスに適用可能です:
- ECサイトと出店者向け管理画面
- 教育サービスと講師用管理システム
- 予約サービスと加盟店舗向けシステム
など
基本的な仕組みの説明
全体アーキテクチャ
主要な制御ポイント
-
パラメータ管理(AWS Systems Manager)
- メンテナンスモードの有効/無効
- 許可IPアドレスリスト
- メンテナンス期間
-
アクセス制御(ALB)
- リスナールールによる振り分け
- IPベースのルーティング
- アプリケーション(CS/CL/Tool)ごとの個別制御
-
メンテナンス画面(Lambda)
- 動的なHTML生成
- メンテナンス情報の表示
主要コンポーネント詳細
1. SSMパラメータストア
各種設定値を一元管理するためのパラメータとその設定方法:
# メンテナンス状態 (String)
Name: /${StackName}/ssm/MaintenanceStatus
Value: "enabled" or "disabled"
# 許可IPアドレス (StringList)
Name: /${StackName}/ssm/MaintenanceAllowedIpRanges
Value: "192.0.2.1/32,192.0.2.2/32" # カンマ区切りで複数指定
# 対象ドメイン (StringList)
Name: /${StackName}/ssm/MaintenanceTargetDomains
Value: "example.com,service.example.com" # カンマ区切りで複数指定
# メンテナンススケジュール (String - JSON形式)
Name: /${StackName}/ssm/MaintenanceSchedule
Value: |
{
"startTime": "2024年1月1日 00:00",
"endTime": "2024年1月1日 06:00"
}
2. EventBridgeとの連携
メンテナンスモードの自動制御にEventBridgeを使用します:
- パラメータ変更の監視
# EventBridgeルール
eventRuleMaintenanceStatusChange:
Type: AWS::Events::Rule
Properties:
Description: Monitor maintenance status changes
EventPattern: !Sub |
{
"source": ["aws.ssm"],
"detail-type": ["Parameter Store Change"],
"detail": {
"name": ["/${AWS::StackName}/ssm/MaintenanceStatus"],
"operation": ["Update"],
"type": ["String"]
}
}
State: ENABLED
Targets:
- Arn: !GetAtt lambdaMaintenanceControl.Arn
Id: UpdateMaintenanceMode
- Arn: !Ref snsTopicMaintenanceAlert
Id: NotifyMaintenanceModeChange
lambdaPermissionForEventBridge:
Type: AWS::Lambda::Permission
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !Ref lambdaMaintenanceControl
Principal: "events.amazonaws.com"
SourceArn: !GetAtt eventRuleMaintenanceStatusChange.Arn
主な自動化シナリオ:
-
手動トリガー
- SSMパラメータの
MaintenanceStatus
を変更 - EventBridgeがLambdaを呼び出し
- メンテナンスモードの切り替えを実行
- SSMパラメータの
-
スケジュールトリガー
- EventBridgeが定期的にスケジュールを確認
- 開始/終了時刻に応じて自動的に状態を変更
-
エラー通知
- メンテナンスモードの切り替え失敗時
- SNSトピック経由で管理者に通知
2. Lambda関数
メンテナンス制御Lambda(lambdaMaintenanceControl)
- CS, CL, Tool のドメインの判別、設定は適宜変更してください
import boto3
import json
import os
def format_ip_for_condition(ip, is_source_ip=False):
"""
条件タイプに応じてIPアドレスを適切な形式に変換
source-ip の場合は CIDR形式に、そうでない場合はプレーンなIP形式に
"""
if '/' in ip: # CIDR形式の場合
return ip if is_source_ip else ip.split('/')[0]
else: # プレーンなIP形式の場合
return f"{ip}/32" if is_source_ip else ip
def lambda_handler(event, context):
ssm = boto3.client('ssm')
elbv2 = boto3.client('elbv2')
stack_name = os.environ['STACK_NAME']
try:
# Get environment type from stack name
environment = stack_name.split('-')[1] # Assuming format: "estate-{env}"
is_production = environment == 'prd'
# Get maintenance status from SSM Parameter Store
status_param = ssm.get_parameter(Name=f"/{stack_name}/ssm/MaintenanceStatus")
status = status_param['Parameter']['Value']
# Get allowed IP ranges
ip_ranges_param = ssm.get_parameter(Name=f"/{stack_name}/ssm/MaintenanceAllowedIpRanges")
allowed_ips = ip_ranges_param['Parameter']['Value'].split(',')
# Get target domains
domains_param = ssm.get_parameter(Name=f"/{stack_name}/ssm/MaintenanceTargetDomains")
target_domains = domains_param['Parameter']['Value'].split(',')
if len(allowed_ips) > 5:
print(f"Warning: Only first 5 IP ranges will be used due to ALB listener rule limitations. Provided: {len(allowed_ips)}")
allowed_ips = allowed_ips[:5]
apps = [
{
'name': 'cs',
'target_group_arn': os.environ['TG_APP_CS_ARN'],
'domain_pattern': lambda d: not (d.startswith('cl-') or d.startswith('tool-') or
d.startswith('cl.') or d.startswith('tool.')),
'admin_priority': 15,
'maintenance_priority': 25
},
{
'name': 'cl',
'target_group_arn': os.environ['TG_APP_CL_ARN'],
'domain_pattern': lambda d: d.startswith('cl-') or d.startswith('cl.'),
'admin_priority': 16,
'maintenance_priority': 26
},
{
'name': 'tool',
'target_group_arn': os.environ['TG_APP_TOOL_ARN'],
'domain_pattern': lambda d: d.startswith('tool-') or d.startswith('tool.'),
'admin_priority': 17,
'maintenance_priority': 27
}
]
# まず既存のメンテナンス関連ルールを削除
try:
rules = elbv2.describe_rules(ListenerArn=os.environ['LISTENER_ARN'])['Rules']
for app in apps:
for priority in [app['admin_priority'], app['maintenance_priority']]:
existing_rule = next((r for r in rules if r['Priority'] == str(priority)), None)
if existing_rule:
elbv2.delete_rule(RuleArn=existing_rule['RuleArn'])
except Exception as e:
print(f"Warning: Error cleaning up maintenance rules: {str(e)}")
# メンテナンスモードが有効な場合のみ、新しいルールを作成
if status == 'enabled':
for app in apps:
# そのアプリケーションに関連するドメインをフィルタリング
app_domains = [d for d in target_domains if app['domain_pattern'](d)]
if not app_domains:
continue
print(f"Creating rules for {app['name']} with domains: {app_domains}")
# 環境に応じたIP制限の条件を設定
if is_production:
# Production環境: True-Client-IPヘッダーを使用(プレーンなIP形式)
formatted_ips = [format_ip_for_condition(ip, False) for ip in allowed_ips]
ip_condition = {
'Field': 'http-header',
'HttpHeaderConfig': {
'HttpHeaderName': 'True-Client-IP',
'Values': formatted_ips
}
}
else:
# 非Production環境: source-ipを使用(CIDR形式)
formatted_ips = [format_ip_for_condition(ip, True) for ip in allowed_ips]
ip_condition = {
'Field': 'source-ip',
'SourceIpConfig': {
'Values': formatted_ips
}
}
# 1. 管理者IP向けのルール(通常アプリケーションにアクセス可能)
admin_rule = {
'ListenerArn': os.environ['LISTENER_ARN'],
'Priority': app['admin_priority'],
'Conditions': [
{
'Field': 'host-header',
'Values': app_domains
},
ip_condition
],
'Actions': [{
'Type': 'forward',
'TargetGroupArn': app['target_group_arn']
}]
}
# 2. その他のアクセス(メンテナンス画面に転送)
maintenance_rule = {
'ListenerArn': os.environ['LISTENER_ARN'],
'Priority': app['maintenance_priority'],
'Conditions': [
{
'Field': 'host-header',
'Values': app_domains
}
],
'Actions': [{
'Type': 'forward',
'TargetGroupArn': os.environ['MAINTENANCE_TARGET_GROUP_ARN']
}]
}
# メンテナンスルールを作成
elbv2.create_rule(**admin_rule)
elbv2.create_rule(**maintenance_rule)
return {
'statusCode': 200,
'body': json.dumps({
'message': f'Successfully updated maintenance mode to {status}',
'domains': target_domains
})
}
except Exception as e:
print(f"Error: {str(e)}")
raise e
メンテナンス画面Lambda(lambdaMaintenancePage)
import json
import boto3
import logging
from datetime import datetime
from typing import Dict, Optional
logger = logging.getLogger()
logger.setLevel(logging.INFO)
class MaintenancePageGenerator:
def __init__(self, stack_name: str):
self.stack_name = stack_name
self.ssm = boto3.client('ssm')
def get_maintenance_schedule(self) -> Dict[str, str]:
"""メンテナンススケジュールの取得"""
try:
param = self.ssm.get_parameter(
Name=f'/{self.stack_name}/ssm/MaintenanceSchedule'
)
return json.loads(param['Parameter']['Value'])
except Exception as e:
logger.error(f"Failed to get maintenance schedule: {str(e)}")
return {
'startTime': 'Not specified',
'endTime': 'Not specified'
}
def generate_html(self, schedule: Dict[str, str]) -> str:
"""メンテナンス画面HTMLの生成"""
current_year = datetime.now().year
return f"""
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>システムメンテナンス中</title>
<style>
body {{
font-family: -apple-system, BlinkMacSystemFont,
"Segoe UI", Roboto, "Helvetica Neue",
Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
background: #f5f5f5;
}}
.maintenance-container {{
max-width: 600px;
margin: 40px auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}}
.maintenance-title {{
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
color: #333;
}}
.maintenance-period {{
background: #f8f8f8;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
}}
.footer {{
margin-top: 30px;
text-align: center;
font-size: 14px;
color: #666;
}}
</style>
</head>
<body>
<div class="maintenance-container">
<h1 class="maintenance-title">
システムメンテナンス中
</h1>
<p>
ご利用のシステムは現在メンテナンス中です。<br>
ご不便をおかけし申し訳ございません。
</p>
<div class="maintenance-period">
<p><strong>メンテナンス期間</strong></p>
<p>開始: {schedule['startTime']}<br>
終了予定: {schedule['endTime']}</p>
</div>
<p>
メンテナンス終了まで今しばらくお待ちください。
</p>
<div class="footer">
© {current_year} System Administrator
</div>
</div>
</body>
</html>
"""
def lambda_handler(event: Dict, context) -> Dict:
"""Lambda handler"""
try:
logger.info(f"Received event: {json.dumps(event)}")
# スタック名の取得
stack_name = event.get('stack_name', 'default-stack')
# メンテナンスページジェネレーターの初期化
generator = MaintenancePageGenerator(stack_name)
# スケジュールの取得
schedule = generator.get_maintenance_schedule()
# HTMLの生成
html = generator.generate_html(schedule)
# レスポンスの返却
return {
'statusCode': 503,
'headers': {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-cache'
},
'body': html
}
except Exception as e:
logger.error(f"Error in lambda_handler: {str(e)}")
return {
'statusCode': 500,
'headers': {
'Content-Type': 'text/html; charset=utf-8'
},
'body': '<h1>System Error</h1><p>メンテナンス画面の表示に失敗しました。</p>'
}
3. ALBリスナールール
本番環境のみ Akamai 利用想定です。
環境ごとの設定の違い:
環境 | IP制御方式 | 形式 |
---|---|---|
本番 | True-Client-IPヘッダー | プレーンIP |
開発 | source-ip | CIDR形式 |
動作フロー
メンテナンスモード有効化時
アクセス時の判定フロー
実装のポイント
1. 優先順位の設計
ALBリスナールールの優先順位が重要です:
優先度の階層:
15-17: 管理者アクセス用ルール(IP制限あり)
25-27: 一般ユーザー用ルール(メンテナンス画面)
CS: 15/25
CL: 16/26
Tool: 17/27
2. IP制限の実装
環境による違いを考慮した実装:
# 本番環境の場合
if is_production:
ip_condition = {
'Field': 'http-header',
'HttpHeaderConfig': {
'HttpHeaderName': 'True-Client-IP',
'Values': formatted_ips
}
}
else:
# 開発環境の場合
ip_condition = {
'Field': 'source-ip',
'SourceIpConfig': {
'Values': formatted_ips
}
}
3. エラーハンドリング
主要なエラーケースと対応:
-
SSMパラメータ取得エラー
try: params = ssm.get_parameters(...) except ClientError as e: logger.error(f"SSMパラメータ取得エラー: {e}") # エラー時のフォールバック処理
-
ALBルール作成エラー
try: elbv2.create_rule(...) except Exception as e: logger.error(f"ルール作成エラー: {e}") # クリーンアップ処理
トラブルシューティング
よくある問題と解決方法
-
メンテナンスモードが有効にならない
- SSMパラメータの値を確認
- Lambda関数のログを確認
- EventBridgeルールの状態確認
-
特定のIPからアクセスできない
- IP形式が環境に合っているか確認
- リスナールールの優先順位確認
- IPアドレスリストの形式確認
-
メンテナンス画面が表示されない
- Lambda関数のログ確認
- ALBターゲットグループの健全性確認
- HTMLレスポンスの状態確認
デバッグ方法
-
CloudWatch Logsの確認
# 必要なロググループ /aws/lambda/lambdaMaintenanceControl /aws/lambda/lambdaMaintenancePage
-
ALBルールの確認
# AWS CLIでのルール一覧取得 aws elbv2 describe-rules --listener-arn [LISTENER-ARN]
-
SSMパラメータの確認
# パラメータ値の確認 aws ssm get-parameter --name "/[StackName]/ssm/MaintenanceStatus"
ベストプラクティス
-
メンテナンス実施前の確認事項
- 許可IPアドレスの事前確認
- メンテナンス期間の適切な設定
- 影響範囲の確認
-
実装時の注意点
- 環境別の適切な設定
- エラーハンドリングの実装
- ログ出力の充実
-
運用上の推奨事項
- 定期的な動作確認
- 設定値の定期レビュー
- 手順書の整備と更新
以上、お疲れ様でした!