【AWS Systems Manager】パッチマネージャー実行時の関連リソースを、絵で見て(完全に)理解する。

パッチマネージャー について、 メンテナンスウインドウやRun Command、ドキュメントなど、他のSystems Managerサービスとの関連を絵で描いて理解しました。

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

コンバンハ、千葉(幸)です。

みなさん、パッチマネージャー理解してますか?

私は、一足先に完全に理解することに成功しました。

パッチマネージャーをマネジメントコンソールからポチポチ操作した時に、裏側ではいろんな Systems Manager と連携しています。当初はその繋がりがわかりませんでした。

そうなるともう絵を描いて理解するしかないので、絵を描きました。ぜひそのアウトプットを見て、皆さんにも完全に理解していただければと思います。

結論

以下のような絵を描きました。

補足

パッチマネージャーによる定期的なスキャンの実行には以下のAWS Systems Managerのサービスを利用する。

  • パッチマネージャー
  • メンテナンスウインドウ
  • ドキュメント
  • Run Command

それぞれ以下を意識する。

  • パッチマネージャー
    • パッチベースライン
    • パッチグループ
  • メンテナンスウインドウ
    • ウインドウ
    • タスク
    • ターゲット
  • ドキュメント
    • AWS-RunPatchBaseline

本稿について

マネジメントコンソールからメンテナンスウインドウを指定したパッチ適用を設定し、裏側で作成されたリソースを確認しながら理解を深めていきます。

なお、パッチ適用を行う対象はAmazon Linux2を前提とします。どの OS でも大まかには共通していますが、一部差異もあるため、適宜読み替えていただくようお願いします。

パッチマネージャーを実行する上での前提事項

ものすごく簡単に言うと、以下の通りです。

  • Systems Managerの前提条件を満たしていること
    • エージェントが導入されていること
    • Systems Managerエンドポイントと疎通可能な状態であること
    • IAMロールがセットアップされていること
  • Systems Managerエージェントを導入したインスタンスがパッチリポジトリと疎通可能であること

インスタンスが直接リポジトリに疎通が取れなくてもいい感じにパッチを取得してくれるのかなと考えていましたが、そういう仕組みではありませんでした。気をつけましょう。

詳細は以下等を確認してください。

1. マネジメントコンソールからパッチ適用を設定する

実際に設定してみて、どのようなリソースが作成されるかを確認していきます。

1.では、マネジメントコンソールで「Systems Manager」の画面から「パッチマネージャー」を選択し、「パッチ適用の設定」を選択した際の画面を順に見ていきます。

なお、画面内で出てくる「パッチベースライン」や「パッチグループ」の詳細については以下の記事で取り上げています。前提知識として抑えておくとより理解しやすいかと思います。

パッチを適用するインスタンス

今回は作成済みのパッチグループを選択します。

パッチグループは「Test」としてあります。

パッチ適用スケジュール

「新しいメンテナンスウィンドウでスケジュールを作成する」を選択します。パラメータとして以下を指定して作成します。

パラメータ
メンテナンスウインドウスケジュールの指定方法 CRON スケジュールビルダーを使用する
メンテナンスウインドウの実行頻度 30 分毎
メンテナンスウインドウの期間 1 時間
メンテナンスウインドウ名 Test-maitenance-window

パッチ適用オペレーション・追加設定

パッチ適用オペレーションは以下から選択可能です。

  • スキャンとインストール
  • スキャンのみ

今回は「スキャンとインストール」を選択します。

追加設定ではパッチグループの登録先のパッチベースラインを変更可能ですが、特に変更は加えず「パッチ適用の設定」を完成させます。

パッチ適用設定が完了すると作成されたメンテナンスウインドウへのリンクが表示されます。

2. メンテナンスウインドウ

前述の画面から遷移すると、メンテナンスウインドウの詳細画面が表示されます。先ほどの画面で指定したパラメータが適用されていることが分かります。

2.1. タスク

「タスク」タブに切り替えてみると、メンテナンスウインドウに紐づくタスク「PatchingTask」が作成されていることが分かります。タスクARNはAWS-RunPatchBaselineで、タイプはRUN_COMMANDです。

メンテナンスウインドウにタスクを割り当てる際のパラメータについては以下ドキュメントに記載があります。

「編集」を押して、タスクがどのような内訳になっているかを確認してみます。以下は画像を分割していますが、縦に長い画面が続くと思ってください。

メンテナンスウインドウのタスクの詳細

タスクの名称、説明が設定されています。Created via Patch Manager Configure Patching Wizardという説明の通り、パッチマネージャーのウィザードから作成されたことが表されています。

今回は「パッチマネージャー」側での操作でこのメンテナンスウインドウが作成されていますが、「メンテナンスウインドウ」のウィザードから同様のものを作成することも可能です。

コマンドのドキュメント

AWS-RunPatchBaselineというドキュメントが実行対象として指定されていることが分かります。このドキュメントは、Run Commandによって実行されます。

両方とも改めて後続で確認します。

ターゲット・レート制御

ターゲットには「PatchingTarget」が指定されていますが、ここでは詳細は分かりません。これも後続で確認します。レート制御にはデフォルトと思われる設定が入っています。

IAMサービスロール・出力オプション・SNS 通知

メンテナンスウインドウによるタスク実行に必要となるため、サービスリンクドロールであるAWSServiceRoleForAmazonSSMが指定されています。

実行結果をS3バケットに出力したり、SNS通知を実施することも可能です。

パラメーター

ドキュメントAWS-RunPatchBaselineに引き渡すパラメーターです。詳細は以下をご確認ください。

SSM ドキュメント AWS-RunPatchBaseline について

リブートオプションについてはデフォルト(指定なし)では「RebootIfNeeded」が選択されるため、再起動させたくない場合には明示的に変更が必要です。

タスクの詳細としてはここまで見てきたもので終わりです。

2.2. ターゲット

メンテナンスウインドウの「ターゲット」タブに切り替えてみると、先ほどタスクで指定されていたターゲットを確認することができます。

こちらも「編集」を押下して内訳を確認してみます。

メンテナンスウィンドウのターゲットの詳細

ターゲットの名前と、説明が設定されています。

ターゲット

メンテナンスウインドウにおけるターゲットの指定の仕方は以下3種があります。

  • インスタンスタグを指定する
  • インスタンスを手動で選択する
  • リソースグループを選択する

今回はパッチマネージャー側でパッチグループ「Test」を指定しましたが、メンテナンスウインドウにおいては「キーがPatchGroupで値がTest」のインスタンスタグを指定したと見なされています。

2.3. 履歴

メンテナンスウインドウによるタスクの実行履歴は「履歴」タブを選択することで確認できます。

履歴の詳細を確認すると、以下のような表示になります。「タスク呼び出し」の詳細を確認すると、Run Commandの実行履歴に遷移します。

長かったメンテナンスウインドウの確認はここまでです。

3. Run Command

前述の画面から、Run Commandの詳細画面に遷移しました。今回はパッチグループ「Test」に所属しているインスタンスが1台だけのため、ターゲットには1台だけ記載があります。ここから個別の実行結果に遷移できます。

遷移すると以下のような画面に。ステップが2つ分かれており、それぞれの出力(コンソール上では2500字まで)が折り畳まれている部分で確認できるようになっています。

ステップ1の出力

今回のケースでは、以下のような出力になっていました。Linux系のOSにおけるパッチ適用の処理が行われていることが分かります。

/usr/bin/python2.7

/usr/bin/python2

/usr/bin/python

/usr/bin/yum

Using Yum version: 3.4.3

Using python binary: 'python2.7'

Using Python Version: Python 2.7.16

03/30/2020 13:00:20 root [INFO]: Downloading payload from https://s3.dualstack.ap-northeast-1.amazonaws.com/aws-ssm-ap-northeast-1/patchbaselineoperations/linux/payloads/patch-baseline-operations-1.35.tar.gz

03/30/2020 13:00:21 root [INFO]: Running with snapshot id = 3f4222b4-0137-43d3-9931-98dc032c4016 and operation = Install

03/30/2020 13:00:22 root [INFO]: Instance Id: i-0338e41615c450ff8

03/30/2020 13:00:22 root [INFO]: Region: ap-northeast-1

03/30/2020 13:00:22 root [INFO]: Product: AmazonLinux2

03/30/2020 13:00:22 root [INFO]: Patch Group: Test

03/30/2020 13:00:22 root [INFO]: Operation type: Install

03/30/2020 13:00:22 root [INFO]: Snapshot Id: 3f4222b4-0137-43d3-9931-98dc032c4016

03/30/2020 13:00:22 root [INFO]: Patch Baseline: {u'approvedPatchesEnableNonSecurity': False, u'baselineId': u'pb-00fda5699d1ae3942', u'name': u'AWS-AmazonLinux2DefaultPatchBaseline', u'modifiedTime': 1529573088.054, u'description': u'Default Patch Baseline for Amazon Linux 2 Provided by AWS.', u'rejectedPatches': [], u'globalFilters': {u'filters': [{u'values': [u'*'], u'key': u'PRODUCT'}]}, u'sources': [], u'approvalRules': {u'rules': [{u'enableNonSecurity': False, u'filterGroup': {u'filters': [{u'values': [u'Security'], u'key': u'CLASSIFICATION'}, {u'values': [u'Critical', u'Important'], u'key': u'SEVERITY'}]}, u'approveAfterDays': 7, u'complianceLevel': u'UNSPECIFIED', u'approveUntilDate': None}, {u'enableNonSecurity': False, u'filterGroup': {u'filters': [{u'values': [u'Bugfix'], u'key': u'CLASSIFICATION'}]}, u'approveAfterDays': 7, u'complianceLevel': u'UNSPECIFIED', u'approveUntilDate': None}]}, u'createdTime': 1529573088.054, u'rejectedPatchesAction': u'ALLOW_AS_DEPENDENCY', u'approvedPatchesComplianceLevel': u'UNSPECIFIED', u'operatingSystem': u'AMAZON_LINUX_2', u'approvedPatches': [], u'accountId': u'xxxxxxxxxxxx'}

03/30/2020 13:00:22 root [INFO]: Reboot Option: RebootIfNeeded

03/30/2020 13:00:22 root [WARNING]: Unable to gain necessary access for possible kernel updates, code: 1.

03/30/2020 13:00:22 root [INFO]: Loading patch snapshot from snapshot.json

03/30/2020 13:00:22 root [INFO]: Reading Local Configuration file

03/30/2020 13:00:22 root [INFO]: {u'pam.x86_64:0:1.1.8-22.amzn2': {u'state': u'InstalledOther', u'installedTime': 1581480446, u'id': u'pam.x86_64'}, u'kpatch-runtime.noar

---Output truncated---

ステップ2の出力

ステップ2の出力は以下です。ターゲットのインスタンスのOSがWindowsであった場合に実行される処理ですが、該当しないためスキップされています。

Step execution skipped due to incompatible platform. Step name: PatchWindows

どのようにステップが分かれているのか?という詳細は、SSM ドキュメントを参照することで確認可能です。

4. SSM ドキュメント

前述のRun Commandでどういった内容が実行されているのかを確認するために、Systems Managerの「ドキュメント」から確認します。

左ペイン「ドキュメント」より、今回タスクで指定されているAWS-RunPatchBaselineを検索し、詳細画面に遷移します。

「コンテンツ」タブを確認すると、Run Commandによってインスタンスに実行される内容が確認できます。

2020年3月時点(バージョン1)では、以下のようになっていました。興味がある方は読み解いてみてはいかがでしょうか。

折り畳み
{
  "schemaVersion": "2.2",
  "description": "Scans for or installs patches from a patch baseline to a Linux or Windows operating system.",
  "parameters": {
    "Operation": {
      "type": "String",
      "description": "(Required) The update or configuration to perform on the instance. The system checks if patches specified in the patch baseline are installed on the instance. The install operation installs patches missing from the baseline.",
      "allowedValues": [
        "Scan",
        "Install"
      ]
    },
    "SnapshotId": {
      "type": "String",
      "description": "(Optional) The snapshot ID to use to retrieve a patch baseline snapshot.",
      "allowedPattern": "(^$)|^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
      "default": ""
    },
    "InstallOverrideList": {
      "type": "String",
      "description": "(Optional) An https URL or an Amazon S3 path-style URL to the list of patches to be installed. This patch installation list overrides the patches specified by the default patch baseline.",
      "allowedPattern": "(^$)|^https://.+$|^s3://([^/]+)/(.*?([^/]+))$",
      "default": ""
    },
    "RebootOption": {
      "type": "String",
      "description": "(Optional) Reboot behavior after a patch Install operation. If you choose NoReboot and patches are installed, the instance is marked as non-compliant until a subsequent reboot and scan.",
      "allowedValues": [
        "RebootIfNeeded",
        "NoReboot"
      ],
      "default": "RebootIfNeeded"
    }
  },
  "mainSteps": [
    {
      "precondition": {
        "StringEquals": [
          "platformType",
          "Windows"
        ]
      },
      "action": "aws:runPowerShellScript",
      "name": "PatchWindows",
      "inputs": {
        "timeoutSeconds": 7200,
        "runCommand": [
          "# Check the OS version",
          "if ([Environment]::OSVersion.Version.Major -le 5) {",
          "    Write-Error 'This command is not supported on Windows 2003 or lower.'",
          "    exit -1",
          "} elseif ([Environment]::OSVersion.Version.Major -ge 10) {",
          "    $sku = (Get-CimInstance -ClassName Win32_OperatingSystem).OperatingSystemSKU",
          "    if ($sku -eq 143 -or $sku -eq 144) {",
          "        Write-Host 'This command is not supported on Windows 2016 Nano Server.'",
          "        exit -1",
          "    }",
          "}",
          "# Check the SSM agent version",
          "$ssmAgentService = Get-ItemProperty 'HKLM:SYSTEM\\CurrentControlSet\\Services\\AmazonSSMAgent\\'",
          "if (-not $ssmAgentService -or $ssmAgentService.Version -lt '2.0.533.0') {",
          "    Write-Host 'This command is not supported with SSM Agent version less than 2.0.533.0.'",
          "    exit -1",
          "}",
          "",
          "# Application specific constants",
          "$appName = 'PatchBaselineOperations'",
          "$psModuleFileName = 'Amazon.PatchBaselineOperations.dll'",
          "$s3FileName = 'Amazon.PatchBaselineOperations-1.24.zip'",
          "$s3LocationUsEast = 'https://s3.amazonaws.com/aws-ssm-{0}/' + $appName.ToLower() + '/' + $s3FileName",
          "$s3LocationRegular = 'https://s3-{0}.amazonaws.com/aws-ssm-{0}/' + $appName.ToLower() + '/' + $s3FileName",
          "$s3LocationCn = 'https://s3.{0}.amazonaws.com.cn/aws-ssm-{0}/' + $appName.ToLower() + '/' + $s3FileName",
          "$s3LocationBah = 'https://s3-{0}.amazonaws.com/aws-patch-manager-{0}-a53fc9dce/' + $appName.ToLower() + '/' + $s3FileName",
          "$s3FileHash = '610B673F7A7624F4A048439ABE62564FB6133FBCC816EB58BAF4078CC69B03CB'",
          "$psModuleHashes = @{",
          "    'Amazon.PatchBaselineOperations.dll' = 'EE67408D7E5C2E3961516D0C99878AF12FAD7F277E60CA2FE050E69B02F58B71';",
          "    'AWSSDK.Core.dll' = 'F024905CDFE4DE022DCD4D9BCF8EE9F237EE27D73DCC32CCB3A69DFC088EB2B0';",
          "    'AWSSDK.S3.dll' = 'B24C349EC0ADEB8462B6B24A27D65650873830416FFF8E816852966C21E87267';",
          "    'AWSSDK.SimpleSystemsManagement.dll' = '627CA33D6B2463C453EEC5F4767C5907D34EB6C1D49D106E5DC9413FADC9DAD9';",
          "    'Newtonsoft.Json.dll' = '0516D4109263C126C779E4E8F5879349663FA0A5B23D6D44167403E14066E6F9';",
          "    'THIRD_PARTY_LICENSES.txt' = '6468E28E2C9EDAF28E98B025EE95C936ED7493AEB19207C36525A5ED5AD4AA56';",
          "    'YamlDotNet.dll' = 'D59E777A42A965327FCC18FC0AB7FA6729C0BCF845D239AC2811BD78F73A7F70'",
          "}",
          "",
          "# Folders and Logging",
          "$tempDirectory = $env:TEMP",
          "$downloadPath = [IO.Path]::Combine($tempDirectory, $s3FileName)",
          "$psModuleInstallLocation = [IO.Path]::Combine([Environment]::GetEnvironmentVariable([Environment+SpecialFolder]::ProgramFiles), 'Amazon', $appName)",
          "$psModuleInstallFile = [IO.Path]::Combine($psModuleInstallLocation, $psModuleFileName)",
          "",
          "function CheckFileHash ($filePath, $fileHash) {",
          "    if (Test-Path($filePath)) {",
          "        $fileStream = New-Object System.IO.FileStream($filePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read)",
          "        $sha256 = [System.Security.Cryptography.HashAlgorithm]::Create('System.Security.Cryptography.SHA256CryptoServiceProvider')",
          "        $sourceHash = [System.BitConverter]::ToString($sha256.ComputeHash($fileStream), 0).Replace('-', '').ToLowerInvariant()",
          "        $sha256.Dispose()",
          "        $fileStream.Dispose()",
          "",
          "        if ($sourceHash -ne $fileHash) {",
          "            return $false",
          "        }",
          "        else {",
          "            return $true",
          "        }",
          "    }",
          "    else {",
          "        return $false",
          "    }",
          "}",
          "",
          "function CheckPowerShellModuleInstallation ([bool]$suppressError) {",
          "    $isInstalled = $false",
          "    # Path does not exist meaning it has never been downloaded.",
          "    if (Test-Path($psModuleInstallLocation)) {",
          "        # Check if the expected number of files and directories are in the folder",
          "        if (((Get-ChildItem $psModuleInstallLocation -Directory | Measure-Object | %{$_.Count}) -eq 0) -and",
          "            ((Get-ChildItem $psModuleInstallLocation -File | Measure-Object | %{$_.Count}) -eq $psModuleHashes.Count)) {",
          "            $validFileHashes = $true",
          "",
          "            # Check each file for their expected file hash.",
          "            Get-ChildItem $psModuleInstallLocation -File | ForEach-Object {",
          "                if ($psModuleHashes.ContainsKey($_.Name)) {",
          "                    $installFile = [IO.Path]::Combine($psModuleInstallLocation, $_.Name)",
          "                    if (-Not (CheckFileHash $installFile $psModuleHashes[$_.Name])) {",
          "                        if (-Not $suppressError) {",
          "                            Write-Error ('The SHA hash of the {0} file does not match the expected value.' -f $_.Name)",
          "                        }",
          "                        $validFileHashes = $false",
          "                    }",
          "                } else {",
          "                    if (-Not $suppressError) {",
          "                        Write-Error ('The PowerShellModule installation folder contains an unexpected file with name {0}.' -f $_.Name)",
          "                    }",
          "                    $validFileHashes = $false",
          "                }",
          "            }",
          "            $isInstalled = $validFileHashes",
          "        } else {",
          "            if (-Not $suppressError) {",
          "                Write-Error ('An incorrect number of files were present in the PowerShellModule installation folder. The contents will be deleted.')",
          "            }",
          "        }",
          "        if (-Not $isInstalled) {",
          "            # Remove all files and folders as the folder contains potentially malicious software.",
          "            Remove-Item $psModuleInstallLocation -Recurse",
          "        }",
          "    }",
          "",
          "    return $isInstalled",
          "}",
          "",
          "function ExtractZipCoreOs ([string]$zipFilePath, [string]$destPath) {",
          "    try {",
          "        [System.Reflection.Assembly]::LoadWithPartialName('System.IO.Compression.FileSystem') | Out-Null",
          "        $zip = [System.IO.Compression.ZipFile]::OpenRead($zipFilePath)",
          "        foreach ($item in $zip.Entries) {",
          "            $extractedPath = Join-Path $destPath $item.FullName",
          "",
          "            if ($item.Length -eq 0) {",
          "                if ((Test-Path $extractedPath) -eq 0) {",
          "                    mkdir $extractedPath | Out-Null",
          "                }",
          "            } else {",
          "                $fileParent = Split-Path $extractedPath",
          "",
          "                if ((Test-Path $fileParent) -eq 0) {",
          "                    mkdir $fileParent | Out-Null",
          "                }",
          "",
          "                [System.IO.Compression.ZipFileExtensions]::ExtractToFile($item, $extractedPath, $true)",
          "            }",
          "        }",
          "    } catch {",
          "        throw 'Error encountered when extracting patch management zip file.`n$($_.Exception.Message)'",
          "    } finally {",
          "        $zip.Dispose()",
          "    }",
          "}",
          "",
          "function InstallPowerShellModule {",
          "    if (-Not (CheckPowerShellModuleInstallation $true)) {",
          "        Write-Output (\"Preparing to download {0} PowerShell module from S3.`r`n\" -f $appName)",
          "",
          "        #Setup the directories if they do not exist.",
          "        if (-Not (Test-Path($psModuleInstallLocation))) {",
          "            $noOp = New-Item $psModuleInstallLocation -ItemType Directory",
          "        }",
          "",
          "        if (-Not (Test-Path($tempDirectory))) {",
          "            $noOp = New-Item $tempDirectory -ItemType Directory",
          "        }",
          "        $region = $env:AWS_SSM_REGION_NAME",
          "        if ($region -eq 'us-east-1') {",
          "            $s3Location = $s3LocationUsEast -f $region",
          "        } elseif (($region -eq 'cn-north-1') -or ($region -eq 'cn-northwest-1')) {",
          "            $s3Location = $s3LocationCn -f $region",
          "        } elseif ($region -eq 'me-south-1') {",
          "            $s3Location = $s3LocationBah -f $region",
          "        } else {",
          "            $s3Location = $s3LocationRegular -f $region",
          "        }",
          "",
          "        Write-Output (\"Downloading {0} PowerShell module from {1} to {2}.`r`n\" -f $appName, $s3Location, $downloadPath)",
          "",
          "        # Add Tls 1.2 support.",
          "        [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bOr [Net.SecurityProtocolType]::Tls12",
          "",
          "        (New-Object Net.WebClient).DownloadFile($s3Location, $downloadPath)",
          "",
          "        if (CheckFileHash $downloadPath $s3FileHash ) {",
          "            Write-Output (\"Extracting {0} zip file contents to temporary folder.`r`n\" -f $appName)",
          "            try {",
          "               (New-Object -Com Shell.Application).namespace($psModuleInstallLocation).CopyHere((New-Object -Com Shell.Application).namespace($downloadPath).Items(), 16)",
          "            } catch [Exception] {",
          "                ExtractZipCoreOs $downloadPath $psModuleInstallLocation",
          "            }",
          "        }",
          "        else {",
          "            throw ('The SHA hash of the {0} S3 source file does not match the expected value.' -f $appName)",
          "        }",
          "",
          "        Write-Output (\"Verifying SHA 256 of the {0} PowerShell module files.`r`n\" -f $appName)",
          "        if (-Not (CheckPowerShellModuleInstallation $false)) {",
          "            throw ('The verification of the {0} PowerShell module did not pass.' -f $appName)",
          "        }",
          "        Write-Output (\"Successfully downloaded and installed the {0} PowerShell module.`r`n\" -f $appName)",
          "    }",
          "}",
          "",
          "try {",
          "    InstallPowerShellModule",
          "} catch [Exception] {",
          "    $msg = \"An error occurred when executing {0}: {1}`r`n\" -f $appName, $_.Exception.Message",
          "    Write-Error $msg",
          "    exit -1",
          "}",
          "finally {",
          "    if (Test-Path $downloadPath) {",
          "        rm $downloadPath",
          "    }",
          "}",
          "",
          "# Setup the command",
          "Import-Module $psModuleInstallFile",
          "$response = Invoke-PatchBaselineOperation -Operation {{Operation}} -SnapshotId '{{SnapshotId}}' -InstallOverrideList '{{InstallOverrideList}}' -RebootOption '{{RebootOption}}' -InstanceId $env:AWS_SSM_INSTANCE_ID -Region $env:AWS_SSM_REGION_NAME",
          "",
          "if ($response.ExitCode -ne 3010)",
          "{",
          "    $response.ToString()",
          "}",
          "",
          "exit $response.ExitCode"
        ]
      }
    },
    {
      "precondition": {
        "StringEquals": [
          "platformType",
          "Linux"
        ]
      },
      "action": "aws:runShellScript",
      "name": "PatchLinux",
      "inputs": {
        "timeoutSeconds": 7200,
        "runCommand": [
          "#!/bin/bash",
          "PYTHON_CMD=''",
          "",
          "check_binary() {",
          "    HAS_VAR_NAME=HAS_$2",
          "    CMD_VAR_NAME=$2_CMD",
          "    if [ \"$(eval echo \\${${HAS_VAR_NAME}})\" = \"0\" ]; then return; fi",
          "    which $1 2>/dev/null",
          "    RET_CODE=$?",
          "    eval \"${HAS_VAR_NAME}=${RET_CODE}\"",
          "    if [ ${RET_CODE} -eq 0 ]; then eval \"${CMD_VAR_NAME}=$1\"; fi",
          "}",
          "",
          "check_binary python3 PYTHON3",
          "check_binary python2.6 PYTHON2_6",
          "check_binary python26 PYTHON26",
          "check_binary python2.7 PYTHON2_7",
          "check_binary python27 PYTHON27",
          "check_binary python2 PYTHON2",
          "",
          "which python 2>/dev/null",
          "if [ $? -eq 0 ]; then",
          "  PYTHON_VERSION=$(python --version 2>&1 | grep -Po '(?<=Python )[\\d]')",
          "  eval \"HAS_PYTHON${PYTHON_VERSION}=0\"",
          "  eval \"PYTHON${PYTHON_VERSION}_CMD='python'\"",
          "fi",
          "",
          "check_binary apt-get APT",
          "check_binary yum YUM",
          "check_binary zypper ZYPP",
          "",
          "check_install_code() {",
          "    if [ $1 -ne 0 ]",
          "    then",
          "        echo \"WARNING: Could not install the $2, this may cause the patching operation to fail.\" >&2",
          "    fi",
          "}",
          "",
          "CANDIDATES=( $HAS_PYTHON2_6 $HAS_PYTHON26 $HAS_PYTHON2_7 $HAS_PYTHON27 $HAS_PYTHON2 )",
          "for CANDIDATE in \"${CANDIDATES[@]}\"",
          "do",
          "    if [ $CANDIDATE -eq 0 ]",
          "    then",
          "        HAS_ANY_PYTHON2=0",
          "    fi",
          "done",
          "",
          "if [ $HAS_APT -eq 0 -a $HAS_PYTHON3 -eq 0 ]",
          "then",
          "    PYTHON_CMD=${PYTHON3_CMD}",
          "    apt-get install python3-apt -y",
          "    check_install_code $? \"python3-apt\"",
          "",
          "elif [ $HAS_YUM -eq 0 -a $HAS_ANY_PYTHON2 -eq 0 ]",
          "then",
          "",
          "    HAS_COMPATIBLE_YUM=false",
          "",
          "    INSTALLED_PYTHON=( $PYTHON2_7_CMD $PYTHON27_CMD $PYTHON2_CMD $PYTHON2_6_CMD $PYTHON26_CMD  )",
          "    for TEST_PYTHON_CMD in \"${INSTALLED_PYTHON[@]}\"",
          "    do",
          "        ${TEST_PYTHON_CMD} -c \"import yum\" 2>/dev/null",
          "        if [ $? -ne 0 ]; then",
          "            echo \"Unable to import yum module on $TEST_PYTHON_CMD\"",
          "        else",
          "            PYTHON_CMD=${TEST_PYTHON_CMD}",
          "            HAS_COMPATIBLE_YUM=true",
          "            break",
          "        fi",
          "    done",
          "    if ! $HAS_COMPATIBLE_YUM; then",
          "        echo \"Unable to import yum module, please check version compatibility between Yum and Python\"",
          "        exit 1",
          "    else",
          "        YUM_VERSION=$(yum --version 2>/dev/null | sed -n 1p)",
          "        echo \"Using Yum version: $YUM_VERSION\"",
          "    fi",
          "",
          "elif [ $HAS_ZYPP -eq 0 -a $HAS_PYTHON2 -eq 0 ]",
          "then",
          "    PYTHON_CMD=${PYTHON2_CMD}",
          "elif [ $HAS_ZYPP -eq 0 -a $HAS_PYTHON3 -eq 0 ]",
          "then",
          "    PYTHON_CMD=${PYTHON3_CMD}",
          "else",
          "    echo \"An unsupported package manager and python version combination was found. Yum requires Python2 and Apt requires Python 3.\"",
          "    echo \"Python3=$HAS_PYTHON3, Python2=$HAS_ANY_PYTHON2, Yum=$HAS_YUM, Apt=$HAS_APT, Zypper=$HAS_ZYPP\"",
          "    echo \"Exiting...\"",
          "    exit 1",
          "fi",
          "",
          "echo \"Using python binary: '${PYTHON_CMD}'\"",
          "PYTHON_VERSION=$(${PYTHON_CMD} --version  2>&1)",
          "echo \"Using Python Version: $PYTHON_VERSION\"",
          "",
          "if [ -z \"$AWS_SSM_REGION_NAME\" ]",
          "then",
          "    echo \"AWS_SSM_REGION_NAME environment variable is empty. Getting region from imdv2.\"",
          "    TOKEN=`curl -X PUT \"http://169.254.169.254/latest/api/token\" -H \"X-aws-ec2-metadata-token-ttl-seconds: 21600\"` && curl -H \"X-aws-ec2-metadata-token: $TOKEN\" -v http://169.254.169.254/latest/meta-data/",
          "    AWS_SSM_DYNAMIC_INSTANCE_IDENTITY_DOCUMENT=`curl -H \"X-aws-ec2-metadata-token: $TOKEN\" -v http://169.254.169.254/latest/dynamic/instance-identity/document`",
          "    echo \"Found dynamic instance identity document using imdv2.\"",
          "    echo $AWS_SSM_DYNAMIC_INSTANCE_IDENTITY_DOCUMENT",
          "    export AWS_SSM_DYNAMIC_INSTANCE_IDENTITY_DOCUMENT=$AWS_SSM_DYNAMIC_INSTANCE_IDENTITY_DOCUMENT",
          "",
          "    AWS_SSM_REGION_NAME=$(echo -e \"import json\\nimport os\\nprint(json.loads(os.environ[\\\"AWS_SSM_DYNAMIC_INSTANCE_IDENTITY_DOCUMENT\\\"])[\\\"region\\\"])\" | python)",
          "    export AWS_SSM_REGION_NAME=$AWS_SSM_REGION_NAME",
          "    echo \"Found the region from the instance identity document.\"",
          "    echo $AWS_SSM_REGION_NAME",
          "fi",
          "",
          "if [ -z \"$AWS_SSM_INSTANCE_ID\" ]",
          "then",
          "    echo \"AWS_SSM_INSTANCE_ID environment variable is empty. Getting instance id from imdv2.\"",
          "    TOKEN=`curl -X PUT \"http://169.254.169.254/latest/api/token\" -H \"X-aws-ec2-metadata-token-ttl-seconds: 21600\"` && curl -H \"X-aws-ec2-metadata-token: $TOKEN\" -v http://169.254.169.254/latest/meta-data/",
          "    AWS_SSM_INSTANCE_ID=`curl -H \"X-aws-ec2-metadata-token: $TOKEN\" -v http://169.254.169.254/latest/meta-data/instance-id`",
          "    export AWS_SSM_INSTANCE_ID=$AWS_SSM_INSTANCE_ID",
          "    echo \"Found the instanceID using imdv2.\"",
          "    echo $AWS_SSM_INSTANCE_ID",
          "fi",
          "",
          "echo '",
          "import errno",
          "import hashlib",
          "import json",
          "import logging",
          "import os",
          "import shutil",
          "import subprocess",
          "import tarfile",
          "import sys",
          "",
          "tmp_dir = os.path.abspath(\"/var/log/amazon/ssm/patch-baseline-operations/\")",
          "reboot_dir = os.path.abspath(\"/var/log/amazon/ssm/patch-baseline-operations-194/\")",
          "reboot_with_failure_dir = os.path.abspath(\"/var/log/amazon/ssm/patch-baseline-operations-195/\")",
          "reboot_with_dependency_failure_dir = os.path.abspath(\"/var/log/amazon/ssm/patch-baseline-operations-196/\")",
          "",
          "ERROR_CODE_MAP = {",
          "    151: \"%s sha256 check failed, should be %s, but is %s\",",
          "    152: \"Unable to load and extract the content of payload, abort.\",",
          "    154: \"Unable to create dir: %s\",",
          "    155: \"Unable to extract tar file: %s.\",",
          "    156: \"Unable to download payload: %s.\"",
          "}",
          "",
          "# When an install occurs and the instance needs a reboot, the agent restarts our plugin.",
          "# Check if these folders exist to know how to succeed or fail a command after a reboot.",
          "# DO NOT remove these files here. They are cleaned in the common startup.",
          "if os.path.exists(reboot_dir) or os.path.exists(reboot_with_failure_dir) or os.path.exists(reboot_with_dependency_failure_dir):",
          "    sys.exit(0)",
          "",
          "",
          "def create_dir(dirpath):",
          "    dirpath = os.path.abspath(dirpath)",
          "    if not os.path.exists(dirpath):",
          "        try:",
          "            os.makedirs(dirpath)",
          "        except OSError as e:  # Guard against race condition",
          "            if e.errno != errno.EEXIST:",
          "                raise e",
          "        except Exception as e:",
          "            logger.error(\"Unable to create dir: %s\", dirpath)",
          "            logger.exception(e)",
          "            abort(154, (dirpath))",
          "",
          "",
          "def use_curl():",
          "    output, has_curl = shell_command([\"which\", \"curl\"])",
          "    if has_curl == 0:",
          "        return True",
          "    else:",
          "        return False",
          "",
          "",
          "def download_to(url, file_path):",
          "    curl_present = use_curl()",
          "    logger.info(\"Downloading payload from %s\", url)",
          "    if curl_present:",
          "        output, curl_return = shell_command([\"curl\", \"-f\", \"-o\", file_path, url])",
          "    else:",
          "        output, curl_return = shell_command([\"wget\", \"-O\", file_path, url])",
          "",
          "    if curl_return != 0:",
          "        download_agent = \"curl\" if curl_present else \"wget\"",
          "        logger.error(\"Error code returned from %s is %d\", download_agent, curl_return)",
          "        abort(156, (url))",
          "",
          "",
          "def download(url):",
          "    if use_curl():",
          "        url_contents, curl_return = shell_command([\"curl\", url])",
          "    else:",
          "        url_contents, curl_return = shell_command([\"wget\", \"-O-\", url])",
          "    if curl_return == 0:",
          "        return url_contents",
          "    else:",
          "        raise Exception(\"Could not curl %s\" % url)",
          "",
          "",
          "def extract_tar(path):",
          "    path = os.path.abspath(path)",
          "    try:",
          "        f = tarfile.open(path, \"r|gz\")",
          "        f.extractall()",
          "    except Exception as e:",
          "        logger.error(\"Unable to extract tar file: %s.\", path)",
          "        logger.exception(e)",
          "        abort(155, (path))",
          "    finally:",
          "        f.close()",
          "",
          "",
          "def shell_command(cmd_list):",
          "    with open(os.devnull, \"w\") as devnull:",
          "        p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=devnull)",
          "        (std_out, _) = p.communicate()",
          "        if not type(std_out) == str:",
          "            std_out = std_out.decode(\"utf-8\")",
          "        return (std_out, p.returncode)",
          "",
          "",
          "def abort(error_code, params = ()):",
          "    if os.path.exists(tmp_dir):",
          "        shutil.rmtree(tmp_dir)",
          "    sys.stderr.write(ERROR_CODE_MAP.get(error_code) % params)",
          "    sys.exit(error_code)",
          "",
          "def sha256_checksum(filename):",
          "    sha256_hash = hashlib.sha256()",
          "    with open(filename,\"rb\") as f:",
          "        # Read and update hash string value in blocks of 4K",
          "        for byte_block in iter(lambda: f.read(4096),b\"\"):",
          "            sha256_hash.update(byte_block)",
          "        return sha256_hash.hexdigest().upper()",
          "",
          "",
          "# cd into the temp directory",
          "create_dir(tmp_dir)",
          "os.chdir(tmp_dir)",
          "",
          "# initialize logging",
          "LOGGER_FORMAT = \"%(asctime)s %(name)s [%(levelname)s]: %(message)s\"",
          "LOGGER_DATEFORMAT = \"%m/%d/%Y %X\"",
          "LOGGER_LEVEL = logging.INFO",
          "LOGGER_STREAM = sys.stdout",
          "",
          "logging.basicConfig(format=LOGGER_FORMAT, datefmt=LOGGER_DATEFORMAT, level=LOGGER_LEVEL, stream=LOGGER_STREAM)",
          "logger = logging.getLogger()",
          "",
          "region = os.environ[\"AWS_SSM_REGION_NAME\"]",
          "",
          "# main logic",
          "# Old bucket location",
          "s3_bucket = \"aws-ssm-%s\" % (region)",
          "s3_prefix = \"patchbaselineoperations/linux/payloads\"",
          "payload_name = \"patch-baseline-operations-1.35.tar.gz\"",
          "payload_sha256 = \"2F9670B98A78F4DBD672EABA8DC0FD38579FF06F8F68424CCC6AB153F4A3D3BB\"",
          "",
          "# New bucket location",
          "if region == \"me-south-1\":",
          "    s3_bucket = \"aws-patch-manager-me-south-1-a53fc9dce\"",
          "",
          "# download payload file and do signature verification",
          "if region.startswith(\"cn-\"):",
          "    url_template = \"https://s3.%s.amazonaws.com.cn/%s/%s\"",
          "elif region.startswith(\"us-gov-\"):",
          "    url_template = \"https://s3-fips-%s.amazonaws.com/%s/%s\"",
          "else:",
          "    url_template = \"https://s3.dualstack.%s.amazonaws.com/%s/%s\"",
          "",
          "download_to(url_template % (region, s3_bucket, os.path.join(s3_prefix, payload_name)), payload_name)",
          "",
          "# payloads are the actual files to be used for linux patching",
          "payloads = []",
          "try:",
          "    sha256_code = sha256_checksum(payload_name)",
          "    if not sha256_code == payload_sha256:",
          "        error_msg = \"%s sha256 check failed, should be %s, but is %s\" % (payload_name, payload_sha256, sha256_code)",
          "        logger.error(error_msg)",
          "        abort(151, (payload_name, payload_sha256, sha256_code))",
          "    extract_tar(payload_name)",
          "    # Change owner & group to be root user for the payload.",
          "    shell_command([\"chown\", \"-R\", \"0:0\", tmp_dir])",
          "except Exception as e:",
          "    error_msg = \"Unable to load and extract the content of payload, abort.\"",
          "    logger.error(error_msg)",
          "    logger.exception(e)",
          "    abort(152)",
          "' | $PYTHON_CMD && \\",
          "echo '",
          "import os",
          "import shutil",
          "import sys",
          "",
          "tmp_dir = os.path.abspath(\"/var/log/amazon/ssm/patch-baseline-operations/\")",
          "reboot_dir = os.path.abspath(\"/var/log/amazon/ssm/patch-baseline-operations-194/\")",
          "reboot_with_failure_dir = os.path.abspath(\"/var/log/amazon/ssm/patch-baseline-operations-195/\")",
          "reboot_with_dependency_failure_dir = os.path.abspath(\"/var/log/amazon/ssm/patch-baseline-operations-196/\")",
          "",
          "# When an install occurs and the instance needs a reboot, the agent restarts our plugin.",
          "# Check if these folders exist to know how to succeed or fail a command after a reboot.",
          "",
          "def check_dir_and_exit(dir, exit_code):",
          "    if os.path.exists(dir):",
          "        shutil.rmtree(dir)",
          "        sys.exit(exit_code)",
          "",
          "check_dir_and_exit(reboot_dir, 0)",
          "check_dir_and_exit(reboot_with_failure_dir, 1)",
          "check_dir_and_exit(reboot_with_dependency_failure_dir, 2)",
          "",
          "os.chdir(tmp_dir)",
          "sys.path.insert(0, tmp_dir)",
          "",
          "import errno",
          "import logging",
          "import stat",
          "import subprocess",
          "import uuid",
          "",
          "",
          "ERROR_CODE_MAP = {",
          "    154: \"Unable to create dir: %s\",",
          "    156: \"Error loading patching payload\"",
          "}",
          "REBOOT_CODE_MAP = {",
          "    194: reboot_dir,",
          "    195: reboot_with_failure_dir,",
          "    196: reboot_with_dependency_failure_dir",
          "}",
          "",
          "def create_dir(dir_path):",
          "    dirpath = os.path.abspath(dir_path)",
          "    # the dir should NOT exists, but do the check anyway",
          "    if not os.path.exists(dirpath):",
          "        try:",
          "            os.makedirs(dirpath)",
          "        except OSError as e:  # Guard against race condition",
          "            if e.errno != errno.EEXIST:",
          "                raise e",
          "        except Exception as e:",
          "            logger.error(\"Unable to create dir: %s\", dirpath)",
          "            logger.exception(e)",
          "            abort(154, (dirpath))",
          "",
          "def remove_dir(dir_path):",
          "    if os.path.exists(dir_path):",
          "        shutil.rmtree(dir_path)",
          "",
          "def exit(code):",
          "    if code in REBOOT_CODE_MAP:",
          "        create_dir(REBOOT_CODE_MAP.get(code))",
          "        # Change code to the reboot code to signal the agent to reboot.",
          "        code = 194",
          "    else:",
          "        # No reboot behavior, remove any possible existing reboot directory",
          "        for dir in REBOOT_CODE_MAP.values():",
          "            remove_dir(dir)",
          "    remove_dir(tmp_dir)",
          "    sys.exit(code)",
          "",
          "",
          "def shell_command(cmd_list):",
          "    with open(os.devnull, \"w\") as devnull:",
          "        p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=devnull)",
          "        (std_out, _) = p.communicate()",
          "        if not type(std_out) == str:",
          "            std_out = std_out.decode(\"utf-8\")",
          "        return std_out, p.returncode",
          "",
          "def abort(error_code, params = ()):",
          "    if os.path.exists(tmp_dir):",
          "        shutil.rmtree(tmp_dir)",
          "    sys.stderr.write(ERROR_CODE_MAP.get(error_code) % params)",
          "    sys.exit(error_code)",
          "",
          "",
          "# initialize logging",
          "LOGGER_FORMAT = \"%(asctime)s %(name)s [%(levelname)s]: %(message)s\"",
          "LOGGER_DATEFORMAT = \"%m/%d/%Y %X\"",
          "LOGGER_LEVEL = logging.INFO",
          "LOGGER_STREAM = sys.stdout",
          "",
          "logging.basicConfig(format=LOGGER_FORMAT, datefmt=LOGGER_DATEFORMAT, level=LOGGER_LEVEL, stream=LOGGER_STREAM)",
          "logger = logging.getLogger()",
          "",
          "# Document parameters.",
          "operation_type = \"{{Operation}}\"",
          "snapshot_id = \"{{SnapshotId}}\"",
          "install_override_list = \"{{InstallOverrideList}}\"",
          "reboot_option = \"{{RebootOption}}\"",
          "",
          "try:",
          "    import os_selector",
          "    exit(os_selector.execute(snapshot_id, operation_type,install_override_list, reboot_option))",
          "except Exception as e:",
          "    error_code = 156",
          "    if hasattr(e, \"error_code\") and type(e.error_code) == int:",
          "        error_code = e.error_code;",
          "    logger.exception(\"Error loading patching payload\")",
          "    abort(error_code)",
          "' | $PYTHON_CMD"
        ]
      }
    }
  ]
}

5. マネージドインスタンス

Systems Managerのマネージドインスタンスからインスタンスを確認できる状態であれば、パッチのステータスを確認することができます。

インスタンスの詳細画面から「パッチ」タブを選択すれば、インストール済みのパッチやインストール待ちのパッチを確認することができます。

パッチステータスについては以下を参照してください。

また、「設定コンプライアンス」タブを選択することでレポートを確認することができます。

設定コンプライアンスについては以下を参照してください。

まとめ

冒頭の絵をもう一度持ってきましたが、理解は進んだでしょうか。

  • パッチマネージャーによるパッチ適用はSSMドキュメントAWS-RunPatchBaselineをRun Commandによって実行することによって行われる
  • 定期的にパッチ適用を行う場合はメンテナンスウインドウによって行われる
  • マネージドインスタンスからパッチの結果を確認することができる

終わりに

ということで、パッチマネージャーを完全に理解することができました。

  • 1.パッチマネージャー 分からない
  • 2.パッチマネージャー 完全に理解した ★いまここ
  • 3.パッチマネージャー やっぱり分からない
  • 4.パッチマネージャー チョットワカル

ステップの踏み具合としては、道半ばです。引き続き頑張りましょう。