AutoScalingで1台ずつ自動的にパッチを当てるSSM Automationやってみた

SSM AutomationでAutoScaling環境のインスタンス1台ずつにパッチ当てを行う方法を紹介します。運用の自動化、がんばりましょう。
2019.02.17

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

こんにちは、臼田です。

皆さん、パッケージアップデートしてますか?(挨拶

AWSではSystems Manager(以下SSM)を利用してEC2に対して様々な操作が可能で、その中でもパッチマネージャ等の機能を利用すればパッケージのアップデート(yum update等)が可能です。

で、パッチを自動的に当てていくにあたって、例えばWebサービスではAutoScaling + ALB構成で複数AZにまたがってEC2が立っていたりするわけですが、せっかく冗長化されているのに全台一度にメンテナンスしたらもったいないですよね。

というわけで、SSMのAutomationを利用して1台ずつメンテナンスしたいわけです。

そんなときはAWS-PatchAsgInstanceを参考にしましょう。

SSMやAutomationについては下記を参考にしてください。

Amazon EC2 Systems Manager の Automation をやってみた #reinvent

AWS-PatchAsgInstanceとは

SSM Automationでは様々なAWSサービスを組み合わせた処理を自動的に行えるサービスですが、その処理を定義するドキュメントが、いくつかAWSによって用意されています。その一つがAWS-PatchAsgInstanceです。

これは名前の通り、AutoScalingのインスタンスに対してパッチを当てる(AWS-RunPatchBaselineを実行する)ドキュメントで、下記のフローになっています。

  1. パッチを当てるインスタンスにタグ「AutoPatchInstanceInASG: InProgress」をつける
  2. インスタンスをASGから切り離す(Standbyに移行)
  3. パッチ当て(AWS-RunPatchBaseline)
  4. 指定した時間までSleep
  5. インスタンスをASGに戻す
  6. インスタンスのタグを「AutoPatchInstanceInASG: Completed」に変更
  7. 指定した時間までSleep

Sleepが2回あるのですが、インストールの完了や次のインスタンスの実行を想定した待ち時間を挟むためのものです。しかし、これはやや使いづらく、メンテナンスウインドウとの相性も悪いので、今回はこれを下記のように削ってみます。

{
  "description": "Systems Manager Automation - Patch instances in an Auto Scaling Group",
  "schemaVersion": "0.3",
  "assumeRole": "{{AutomationAssumeRole}}",
  "parameters": {
    "InstanceId": {
      "type": "String",
      "description": "(Required) ID of the Instance to patch. Only specify when not running from Maintenance Windows."
    },
    "LambdaRoleArn": {
      "default": "",
      "type": "String",
      "description": "(Optional) The ARN of the role that allows Lambda created by Automation to perform the actions on your behalf. If not specified a transient role will be created to execute the Lambda function."
    },
    "AutomationAssumeRole": {
      "type": "String",
      "description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.",
      "default": ""
    }
  },
  "mainSteps": [
    {
      "name": "createPatchGroupTags",
      "action": "aws:createTags",
      "maxAttempts": 1,
      "onFailure": "Continue",
      "inputs": {
        "ResourceType": "EC2",
        "ResourceIds": [
          "{{InstanceId}}"
        ],
        "Tags": [
          {
            "Key": "AutoPatchInstanceInASG",
            "Value": "InProgress"
          }
        ]
      }
    },
    {
      "name": "EnterStandby",
      "action": "aws:executeAutomation",
      "maxAttempts": 1,
      "timeoutSeconds": 300,
      "onFailure": "Abort",
      "inputs": {
        "DocumentName": "AWS-ASGEnterStandby",
        "RuntimeParameters": {
          "InstanceId": [
            "{{InstanceId}}"
          ],
          "LambdaRoleArn": [
            "{{LambdaRoleArn}}"
          ],
          "AutomationAssumeRole": [
            "{{AutomationAssumeRole}}"
          ]
        }
      }
    },
    {
      "name": "installMissingOSUpdates",
      "action": "aws:runCommand",
      "maxAttempts": 1,
      "onFailure": "Continue",
      "inputs": {
        "DocumentName": "AWS-RunPatchBaseline",
        "InstanceIds": [
          "{{InstanceId}}"
        ],
        "Parameters": {
          "Operation": "Install"
        }
      }
    },
    {
      "name": "ExitStandby",
      "action": "aws:executeAutomation",
      "maxAttempts": 1,
      "timeoutSeconds": 300,
      "onFailure": "Abort",
      "inputs": {
        "DocumentName": "AWS-ASGExitStandby",
        "RuntimeParameters": {
          "InstanceId": [
            "{{InstanceId}}"
          ],
          "LambdaRoleArn": [
            "{{LambdaRoleArn}}"
          ],
          "AutomationAssumeRole": [
            "{{AutomationAssumeRole}}"
          ]
        }
      }
    },
    {
      "name": "CompletePatchGroupTags",
      "action": "aws:createTags",
      "maxAttempts": 1,
      "onFailure": "Continue",
      "inputs": {
        "ResourceType": "EC2",
        "ResourceIds": [
          "{{InstanceId}}"
        ],
        "Tags": [
          {
            "Key": "AutoPatchInstanceInASG",
            "Value": "Completed"
          }
        ]
      }
    }
  ]
}

これによりSleepが削られて扱いやすくなりました。それではこのドキュメントを利用して1台ずつ自動的にパッチを当てる仕組みを作っていきます。

やってみた

今回のコンポーネントは下記のとおりです。

  • SSMメンテナンスウインドウ
    • パッチを当てるトリガーを引く
    • パッチ対象のインスタンス群を指定する
  • SSM Automation
    • カスタマイズされたAWS-PatchAsgInstanceを実行
  • SSM Run Command
    • AWS-PatchAsgInstance内でAWS-RunPatchBaselineを実行

SSMは登場人物が多くなりがちで複雑で分かりづらいですが、適宜補足を入れていきます。

実行対象について

下記の構成に対してパッチを当てて行きます。シンプルです。

EC2については以下のようになっています。

  • Amazon Linux 2
  • SSM Agentインストール済み
  • AmazonEC2RoleforSSMポリシーアタッチ済み

SSMが利用できる状態であることは、マネジメントコンソール上でSSMのマネージドインスタンスから確認できます。

コンプライアンスの確認

今回は古いAMIから起動していてパッケージが更新されていません。その状態を確認するためにRun CommandからAWS-RunPatchBaselineScanを実行します。必要に応じてベースラインを変更しておいてください。

Run Command画面から「コマンドの実行」を押します。

ページを送ってAWS-RunPatchBaselineを選択します。

パラメータはScanのままにしておきます。

ターゲットの指定の仕方は自由ですが、対象のインスタンスをすべて選択します。

「実行」を押します。

しばらくするとSSMのコンプライアンスページにて状況を確認できます。対象のインスタンスに対して適切にパッチが当たっていないことが確認できました。

AWS-PatchAsgInstanceのカスタマイズ

それでは、自動実行のためにAWS-PatchAsgInstanceのドキュメントをカスタマイズしていきます。

ドキュメントの確認はSSMのドキュメントページから該当のドキュメントの詳細画面に移動し、

「コンテンツ」から確認できます。必要に応じてコピーしましょう。

ドキュメントの作成は同じくSSMのドキュメントページから「ドキュメントの作成」を押します。

適当なドキュメント名を入れ、オートメーションドキュメントを選択します。コンテンツに上述したjsonを貼り付け、ページ下部の「ドキュメントの作成」を押して作成します。

IAM Roleの作成

SSMのメンテンスウインドウからAutomationを実行する際に利用するIAM Roleを作成します。下記を参考に作成してください。追加で当てているPolicyも必要になります。

SSM Automationを実行するIAM Roleを作成してみた

メンテナンスウインドウの作成

SSMのメンテナンスウインドウは定期的に決められたタスクを実行することができます。今回は週に1回パッチの適用を行うように設定していきます。

まずSSMの「メンテナンスウインドウ」から「メンテナンスウインドウの作成」を押します。

名前と説明を適当に入力して、「未登録ターゲット」は今回は外します。この設定はメンテナンスウインドウで設定しているターゲット以外に対してもタスクの設定でインスタンスを追加することを許可するもので、今回は明示的にターゲットを選択するので必要ないため外しています。誤爆が怖いので、基本外すほうがいいと個人的には思います。

スケジュールを設定します。Cronスケジュールビルダーを利用すると書き方を忘れがちなAWS特有のCronを書かなくて済みます。時間はGMTでもいけますが、後でTimezoneを指定できるので日本時間のつもりで設定するほうがいいでしょう。「期間」はタスクを実行する最長時間です。想定するタスクやターゲットの数に合わせて設定しましょう。「タスクの開始を停止」は、メンテンスウインドウが終了したあとに次のタスクのスケジュールを停止する時間です。実行後に他の処理に影響があることを行う場合には設定してもいいですが、基本0になるでしょう。

タイムゾーンはAsia/Tokyoがいいと思います。「メンテンスウインドウの作成」で作成できます。

ターゲットの登録

続いてメンテンスウインドウにターゲットを登録していきます。作成したメンテンスウインドウを選択します。

「ターゲット」から「ターゲットの登録」を押します。

名前等を適当に入力します。

ターゲットをタグかインスタンスを手動で登録します。AutoScalingのインスタンスが対象であれば、aws:autoscaling:groupNameタグを指定するとやりやすいでしょう。「ターゲットの登録」で完了です。

タスクの登録

タスクを登録していきます。同じくメンテンスウインドウの詳細画面から「タスク」タブで「タスクを登録する」から「オートメーションタスクの登録」を押します。

名前と説明を適当に入れます。

ターゲットとして先程登録したものを選択します。

レート制御では、一度にいくつのターゲットに対してタスクを実行するかという「並行性」と失敗した際にタスクを停止する「誤差閾値」の設定があります。1台ずつ行いたいので並行性は1ターゲット、エラーが起きたらすぐに停止してほしいので誤差閾値を0エラーとします。

IAMサービスロールは先ほど作成したものを選択します。

パラメータとしてInstanceIDを取るため、メンテンスウインドウで選択したターゲットのインスタンスIDを渡すために{{ TARGET_ID }}を入力します。これはメンテンスウインドウで利用できる共通パラメータで詳細はこちらにあります。「オートメーションタスクの登録」を押して完了です。

タスクを実行してみた

実際にタスクが実行されているところを見てみます。メンテンスウインドウの詳細画面「履歴」タブから実行状況を確認できます。実行中に見るとわかりますが、タスクが想定通りインスタンス1台ずつ呼ばれています。実行されていない方は保留中となっています。

途中でインスタンスがAutoScalingグループから切り離されスタンバイになります。Desiredの数が1つ減り、スタンバイになっていることが確認できます。

これは完全に実行内容や環境に依存しますが、私の環境では1台あたり10分程度でパッチ当て含めたタスク全体が完了していました。

おまけ

実行に失敗したら通知を受け取る

もし自動化がうまく行って運用負荷が軽減したら、今度は失敗したときだけ通知を受け取りたくなります。下記を参考にCloudWatch Eventを設定してみてください。

SSM Automationが失敗したら通知するCloudWatch Eventを設定してみた

Automationの内容をアレンジする

今回のAutomationドキュメントはほんの一例です。ご利用のユースケースに合わせてアレンジしてください。他にも参考になりそうな情報を紹介します。

まとめ

SSMのAutomationやメンテンスウインドウを活用してAutoScalingのインスタンスを1台ずつパッチ当てしてみました。

パッチの適用が自動化されれば運用がいろいろ楽になると思います。

もし本番環境でいきなり適用ができないなら、検証環境から使っていきましょう。もしくは、テストケースをキチンと作っておけば済むでしょう。サービスに問題がないか・デグレが無いかなどは監視するパラメータやテストなどで明確に確認できるようにしましょう。

パッチの適用や検証が手動だと運用が辛いですから、なるべく自動化して楽をしましょう。