今回は、前回投稿した「OSパッチ適用を自動化させた後、結果をメールに通知させる方法」をAWS CDKで実装する方法をまとめました。
はじめに
前回の記事ではAWSマネジメントコンソールを使って手動でOSパッチ適用の自動化システムを構築しました。
今回は、同じシステムをAWS CDK(Cloud Development Kit)を使ってコード化し、Infrastructure as Code(IaC)として管理できるようにしていきます。
今回作成するリソース
- SNSトピック: メール通知用
- IAMロール: SSM実行用とLambda実行用
- パッチベースライン: Windows Server 2022用
- メンテナンスウィンドウ: 毎月第1月曜日に実行
- Lambda関数: パッチ結果解析とSNS送信
- CloudWatchロググループ: パッチ実行ログ保存用
- サブスクリプションフィルター: Lambda起動トリガー
AWS CDK ソースコード
lib/patch-automation-stack.ts
を、以下のように実装します。
最初に各コンポーネント毎に詳細を解説し、最後にソースコードをまとめて記載します。
また、CDKでは名前の自動設定やgrantメソッドなど便利な機能がありますが、実際の案件では様々なリソースに名前を設定する必要があったため名前を定義できるように実装をしています。
SNSトピックとメール通知設定
// パッチ通知用SNSトピックの作成 const patchtopic = new sns.Topic(this, 'PatchNotificationTopic', { topicName: 'sns-patch-notification', displayName: 'パッチ適用結果通知' }); // SNSトピックに通知先メールアドレスを設定 patchtopic.addSubscription( new subscriptions.EmailSubscription('your-email@example.com') //実際のメールアドレスへ変更 );
ポイント:
EmailSubscription
で通知先メールアドレスを設定- 実際のメールアドレスに変更する必要があります
IAMロールの設定
// SSM Run Command実行用のIAMロール const ssmRunCommandRole = new iam.Role(this, 'SSMRunCommandRole', { assumedBy: new iam.ServicePrincipal('ssm.amazonaws.com'), roleName: 'SSMRunCommandRole', managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonSSMMaintenanceWindowRole'), iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'), ], description: 'Role for SSM Run Command execution' }); // Lambda関数のIAMロール作成 const lambdaPatchRole = new iam.Role(this, 'LambdaPatchRole', { roleName: 'LambdaPatchRole', assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), description: 'Lambda execution role for patch notification', managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole') ] }); // Lambda関数にSNSの権限を付与 new iam.Policy(this, 'LambdaPatchSnsPolicy', { policyName: 'LambdaPatchSnsPolicy', roles: [lambdaPatchRole], statements: [ new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['sns:Publish'], resources: ['*'] }) ] });
ポイント:
- SSMロール: メンテナンスウィンドウでのパッチ適用実行に必要な権限
AmazonSSMMaintenanceWindowRole
: メンテナンスウィンドウ専用権限AmazonSSMManagedInstanceCore
: SSM基本通信権限
- Lambdaロール: CloudWatch LogsアクセスとSNS発行権限
AWSLambdaBasicExecutionRole
: Lambda基本実行権限- カスタムポリシーでSNS発行権限を追加
パッチベースラインの設定
new ssm.CfnPatchBaseline(this, 'WindowsPatchBaseline', { operatingSystem: 'WINDOWS', name: 'winsv-baseline', approvalRules: { patchRules: [{ approveAfterDays: 0, // リリースから0日後に承認 complianceLevel: 'CRITICAL', // コンプライアンスレベル:重要 enableNonSecurity: false, // セキュリティパッチのみを対象 patchFilterGroup: { patchFilters: [ { key: 'PRODUCT', values: ['WindowsServer2022'] // WindowsSrver2022で設定 }, { key: 'CLASSIFICATION', // Windowsの更新プログラム分類 values: ['CriticalUpdates', 'SecurityUpdates'] // 重要な更新プログラム,セキュリティ更新プログラム }, { key: 'MSRC_SEVERITY', // Microsoftが定める重要度 values: ['Critical','Important'] // 致命的な問題を修正,重要な問題を修正 } ] } }] }, patchGroups: ['winsv-patch-group'] // 対象パッチグループ });
ポイント:
operatingSystem
: 対象OS(WINDOWS、AMAZON_LINUX、UBUNTU等)approveAfterDays: 0
: リリース直後からパッチ適用可能(本番では7-30日を推奨)enableNonSecurity: false
: セキュリティパッチのみに限定- フィルター条件:
PRODUCT
: 対象Windows製品CLASSIFICATION
: パッチの分類MSRC_SEVERITY
: Microsoftが定める重要度
メンテナンスウィンドウの設定
const patchMaintenanceWindow = new ssm.CfnMaintenanceWindow(this, 'PatchMaintenanceWindow', { name: 'monthly-patch-maintenance-window', schedule: 'cron(0 16 ? * MON#1 *)', duration: 2, cutoff: 0, allowUnassociatedTargets: false, scheduleTimezone: 'Asia/Tokyo' });
スケジュール設定は現在以下のように設定していますので、要件に応じて修正してください。
cron(0 16 ? * MON#1 *)
: JST時間で第1月曜日16:00duration: 2
: 2時間のメンテナンス枠scheduleTimezone: 'Asia/Tokyo'
: 日本時間で管理
パッチ適用対象の設定
const patchTarget = new ssm.CfnMaintenanceWindowTarget(this, 'PatchTarget', { windowId: patchMaintenanceWindow.ref, targets: [{ key: 'tag:PatchGroup', values: ['winsv-patch-group'] }], resourceType: 'INSTANCE', ownerInformation: 'Patch管理対象インスタンス' });
ポイント:
targets:
でパッチ適用対象インスタンスを設定- 事前にEC2インスタンスでタグの設定が必要
今回の場合必要なタグ設定 - キー: `PatchGroup` – 値: `winsv-patch-group`
RunCommandタスクの作成
// パッチ適用タスクの作成 const patchExecutionTask = new ssm.CfnMaintenanceWindowTask(this, 'PatchExecutionTask', { // 基本設定 windowId: patchMaintenanceWindow.ref, // メンテナンスウィンドウの参照 taskArn: 'AWS-RunPatchBaseline', // 実行するSSMドキュメント taskType: 'RUN_COMMAND', // タスクタイプ:RunCommand name: 'SecurityPatchInstallation', // タスク名 description: 'セキュリティパッチの自動インストールと再起動', // タスクの説明 priority: 1, // タスクの優先度(1が最高) maxConcurrency: '1', // 同時実行数:1台ずつ順次実行 maxErrors: '0', // エラー許容数:0(1台でもエラーなら停止) serviceRoleArn: ssmRunCommandRole.roleArn, // SSM実行用IAMロール targets: [{ key: 'WindowTargetIds', // ターゲット指定方法 values: [patchTarget.ref] // 事前に定義したターゲットを参照 }], taskInvocationParameters: { maintenanceWindowRunCommandParameters: { documentVersion: '$LATEST', // 最新バージョンのドキュメントを使用 parameters: { Operation: ['Install'], // 操作:パッチのインストール実行 RebootOption: ['RebootIfNeeded'], // 必要に応じて自動再起動 SnapshotId: ['{{WINDOW_EXECUTION_ID}}'] // 実行IDをスナップショットIDとして記録 }, timeoutSeconds: 600, // タイムアウト(必要に応じて修正) cloudWatchOutputConfig: { cloudWatchLogGroupName: 'aws-ssm-patch-execution-logs', // CloudWatch Logsグループ名 cloudWatchOutputEnabled: true // CloudWatch Logs出力を有効化 } } } });
ポイント:
taskArn
: ‘AWS-RunPatchBaseline’でパッチ適用を実行priority
: 1が最高優先度maxConcurrency
: 同時実行数制御(’1’=順次、’50%’=半数同時)maxErrors
: エラー許容数(’0’=1台でもエラーなら停止)- 重要パラメータ:
Operation: ['Install']
: パッチを実際にインストールRebootOption: ['RebootIfNeeded']
: 必要時のみ再起動SnapshotId
: 実行履歴の追跡用
Lambda関数作成
// Lambda関数作成 const patchNotificationLambda = new lambda.Function(this, 'PatchNotificationFunction', { functionName: 'lmd-patch-send-sns', // 関数名 runtime: lambda.Runtime.PYTHON_3_13, // 実行環境:Python 3.13 handler: 'index.lambda_handler', // エントリーポイント code: lambda.Code.fromAsset( // ソースコードの場所 path.join(__dirname, 'lambda') ), role: lambdaPatchRole, // Lambda実行時IAMロール environment: { // 環境変数 SNS_TOPIC_ARN: patchtopic.topicArn, // SNSトピックのARN ALARM_SUBJECT: 'パッチ適用結果通知' // メールの件名 }, timeout: cdk.Duration.seconds(600), // 実行タイムアウト memorySize: 128, // メモリ割り当て(MB) });
ポイント:
runtime
: Python 3.13code
:lib/lambda
ディレクトリからコード読み込みenvironment
: SNSトピックARNとメール件名を環境変数で設定timeout
: 600秒memorySize
: 128MB
関数作成
import logging import json import base64 import gzip import boto3 import os logger = logging.getLogger() logger.setLevel(logging.INFO) def lambda_handler(event, context): # CloudWatchLogsからのデータはbase64エンコードされているのでデコード decoded_data = base64.b64decode(event['awslogs']['data']) # バイナリに圧縮されているため展開 json_data = json.loads(gzip.decompress(decoded_data)) logger.info("EVENT: " + json.dumps(json_data)) # ログデータ取得 log = json_data['logEvents'][0]['message'] instanceid_position = log.find('PatchGroup') instanceidtmp = log[:instanceid_position] instanceid = instanceidtmp.replace('Patch Summary for ','') print(instanceid) result_position = log.find('Results') result = log[result_position:] print(result) patchGroup_start = log.find('PatchGroup') patchGroup_end = log.find('BaselineId') patchGrouptmp = log[patchGroup_start:patchGroup_end] patchGroup = patchGrouptmp.replace('PatchGroup : ','') print(patchGroup) messagetmp = """ パッチ適用結果を通知します。 対象インスタンス・適用結果は下記となります。 〇対象インスタンス{0} 〇パッチグループ {1} 〇適用結果 {2} """ message = messagetmp.format(instanceid,patchGroup,result) print(message) try: sns = boto3.client('sns') #SNS Publish publishResponse = sns.publish( TopicArn = os.environ['SNS_TOPIC_ARN'], Message = message, Subject = os.environ['ALARM_SUBJECT'] ) except Exception as e: print(e)
ポイント:
lib/lambda/index.py
にPythonコードを配置します
ロググループ設定
// パッチログ用のCloudWatchロググループ作成 const patchloggroup = new logs.LogGroup(this, 'PatchLogGroup', { logGroupName: 'aws-ssm-patch-execution-logs', // パッチ適用ロググループ retention: logs.RetentionDays.ONE_YEAR, // 保持期間: 1年間 removalPolicy: cdk.RemovalPolicy.RETAIN // スタック削除時に削除されない }); new logs.SubscriptionFilter(this, 'PatchSummarySubscription', { // サブスクリプションフィルターの設定(Lambda関数の起動トリガー) logGroup: patchloggroup, // 監視対象のロググループ destination: new destinations.LambdaDestination(patchNotificationLambda), // 送信先 filterPattern: logs.FilterPattern.literal('Patch Summary'), // フィルター条件 filterName: 'PatchNotification' // フィルター名 }); }
ポイント:
logGroupName:
はRunCommandタスクの作成で指定したロググループ名と合わせる- 保管期間は要件に合わせて修正する
今回実装したスタックファイルまとめ
import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as ssm from 'aws-cdk-lib/aws-ssm'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as logs from 'aws-cdk-lib/aws-logs'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as sns from 'aws-cdk-lib/aws-sns'; import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions'; import * as path from 'path'; import * as destinations from 'aws-cdk-lib/aws-logs-destinations'; export class PatchAutomationStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // パッチ通知用SNSトピックの作成 const patchtopic = new sns.Topic(this, 'PatchNotificationTopic', { topicName: 'sns-patch-notification', displayName: 'パッチ適用結果通知' }); // SNSトピックに通知先メールアドレスを設定 patchtopic.addSubscription( new subscriptions.EmailSubscription('your-email@example.com') // ここを実際のメールアドレスに変更 ); // SSM Run Command実行用のIAMロール const ssmRunCommandRole = new iam.Role(this, 'SSMRunCommandRole', { roleName: 'SSMRunCommandRole', assumedBy: new iam.ServicePrincipal('ssm.amazonaws.com'), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonSSMMaintenanceWindowRole'), iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'), ], description: 'Role for SSM Run Command execution' }); // Lambda関数のIAMロール作成 const lambdaPatchRole = new iam.Role(this, 'LambdaPatchRole', { roleName: 'LambdaPatchRole', assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), description: 'Lambda execution role for patch notification', managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole') ] }); // Lambda関数にSNSの権限を付与 new iam.Policy(this, 'LambdaPatchSnsPolicy', { policyName: 'LambdaPatchSnsPolicy', roles: [lambdaPatchRole], statements: [ new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['sns:Publish'], resources: ['*'] }) ] }); // パッチベースラインの作成 new ssm.CfnPatchBaseline(this, 'WindowsPatchBaseline', { operatingSystem: 'WINDOWS', name: 'winsv-baseline', approvalRules: { patchRules: [{ approveAfterDays: 0, // リリースから0日後に承認 complianceLevel: 'CRITICAL', // コンプライアンスレベル:重要 enableNonSecurity: false, // セキュリティパッチのみを対象 patchFilterGroup: { patchFilters: [ { key: 'PRODUCT', values: ['WindowsServer2022'] // WindowsServer2022で設定 }, { key: 'CLASSIFICATION', values: ['CriticalUpdates', 'SecurityUpdates'] }, { key: 'MSRC_SEVERITY', values: ['Critical', 'Important'] } ] } }] }, patchGroups: ['winsv-patch-group'] }); // メンテナンスウィンドウの作成 const patchMaintenanceWindow = new ssm.CfnMaintenanceWindow(this, 'PatchMaintenanceWindow', { name: 'monthly-patch-maintenance-window', schedule: 'cron(0 16 ? * MON#1 *)', // 毎月第1月曜日 JST 16:00 duration: 2, // 実行可能時間:2時間 cutoff: 0, // 新規タスク開始の締切時間 allowUnassociatedTargets: false, // 関連付けされたターゲットのみ実行 scheduleTimezone: 'Asia/Tokyo' // スケジュールのタイムゾーン }); // パッチ適用対象の設定 const patchTarget = new ssm.CfnMaintenanceWindowTarget(this, 'PatchTarget', { windowId: patchMaintenanceWindow.ref, // メンテナンスウィンドウの参照 targets: [{ key: 'tag:PatchGroup', // タグによる対象選択 values: ['winsv-patch-group'] // 対象とするタグ値 }], resourceType: 'INSTANCE', // 対象リソース種別:EC2インスタンス ownerInformation: 'Patch管理対象インスタンス' // 管理用メモ }); // パッチ適用タスクの作成 const patchExecutionTask = new ssm.CfnMaintenanceWindowTask(this, 'PatchExecutionTask', { // 基本設定 windowId: patchMaintenanceWindow.ref, // メンテナンスウィンドウの参照 taskArn: 'AWS-RunPatchBaseline', // 実行するSSMドキュメント taskType: 'RUN_COMMAND', // タスクタイプ:RunCommand name: 'SecurityPatchInstallation', // タスク名 description: 'セキュリティパッチの自動インストールと再起動', // タスクの説明 priority: 1, // タスクの優先度(1が最高) maxConcurrency: '1', // 同時実行数:1台ずつ順次実行 maxErrors: '0', // エラー許容数:0(1台でもエラーなら停止) serviceRoleArn: ssmRunCommandRole.roleArn, // SSM実行用IAMロール targets: [{ key: 'WindowTargetIds', // ターゲット指定方法 values: [patchTarget.ref] // 事前に定義したターゲットを参照 }], taskInvocationParameters: { maintenanceWindowRunCommandParameters: { documentVersion: '$LATEST', // 最新バージョンのドキュメントを使用 parameters: { Operation: ['Install'], // 操作:パッチのインストール実行 RebootOption: ['RebootIfNeeded'], // 必要に応じて自動再起動 SnapshotId: ['{{WINDOW_EXECUTION_ID}}'] // 実行IDをスナップショットIDとして記録 }, timeoutSeconds: 600, // タイムアウト(必要に応じて修正) cloudWatchOutputConfig: { cloudWatchLogGroupName: 'aws-ssm-patch-execution-logs', // CloudWatch Logsグループ名 cloudWatchOutputEnabled: true // CloudWatch Logs出力を有効化 } } } }); // Lambda関数作成 const patchNotificationLambda = new lambda.Function(this, 'PatchNotificationFunction', { functionName: 'lmd-patch-send-sns', // 関数名 runtime: lambda.Runtime.PYTHON_3_13, // 実行環境:Python 3.13 handler: 'index.lambda_handler', // エントリーポイント code: lambda.Code.fromAsset( // ソースコードの場所 path.join(__dirname, 'lambda') ), role: lambdaPatchRole, // Lambda実行時IAMロール environment: { // 環境変数 SNS_TOPIC_ARN: patchtopic.topicArn, // SNSトピックのARN ALARM_SUBJECT: 'パッチ適用結果通知' // メールの件名 }, timeout: cdk.Duration.seconds(600), // 実行タイムアウト memorySize: 128, // メモリ割り当て(MB) description: 'パッチ適用結果を解析してSNS通知を送信する関数' // 関数の説明 }); // パッチログ用のCloudWatchロググループ作成 const patchloggroup = new logs.LogGroup(this, 'PatchLogGroup', { logGroupName: 'aws-ssm-patch-execution-logs', // パッチ適用ロググループ retention: logs.RetentionDays.ONE_YEAR, // 保持期間: 1年間 removalPolicy: cdk.RemovalPolicy.RETAIIN // スタック削除時に削除されない }); // サブスクリプションフィルターの設定(Lambda関数の起動トリガー) new logs.SubscriptionFilter(this, 'PatchSummarySubscription', { logGroup: patchloggroup, // 監視対象のロググループ destination: new destinations.LambdaDestination(patchNotificationLambda), // 送信先 filterPattern: logs.FilterPattern.literal('Patch Summary'), // フィルター条件 filterName: 'PatchNotification' // フィルター名 }); } }
まとめ
今回は、パッチ自動適用からメール通知までをAWS CDKでモジュール化してみました。
皆さんのお役に立てば幸いです。