AWS CDKで OS パッチ適用の自動化を実装してみた

今回は、前回投稿した「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:00
  • duration: 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.13
  • codelib/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でモジュール化してみました。
皆さんのお役に立てば幸いです。

著者について

AWSの基盤構築を担当しています。
小物集めにはまっています。

ameiをフォローする

クラウドに強いによるエンジニアブログです。

SCSKクラウドサービス(AWS)は、企業価値の向上につながるAWS 導入を全面支援するオールインワンサービスです。AWS最上位パートナーとして、多種多様な業界のシステム構築実績を持つSCSKが、お客様のDX推進を強力にサポートします。

AWS運用・監視
シェアする
タイトルとURLをコピーしました