AWSによるパッチ適用の自動化と、ジョブ実行の実装方法を考えてみた

# はじめに

こんにちは、はやぴー（@HayaP）です。

皆さん、クラウド上のサーバー（IaaS）のパッチマネージメントはどのように行っていますか？

今回は、AWS(IaaS)におけるパッチマネージメント戦略について考えていきたいと思います。

## 対象読者
- クラウド(AWS)においての、パッチマネジメント戦略を知りたい
- クラウド(AWS)の、パッチ適用を自動化したい
- クラウド(AWS)で、ジョブ実行を管理したい
# 概要
本記事は、下記構成になっています。

- 【Level.1】単純にパッチ適用したい
- 【Level.2】適用するパッチを変更したい
- 【Level.3】環境(dev/prd)毎に、適用するパッチを分けたい
- 【Level.4】パッチ適用を自動化したい
- 【Level.5】環境(dev/prd)毎に、パッチ適用タイミングを分けたい
- 【Level.6】パッチ適用の前後にジョブを挟みたい

## TL;DR(忙しい人用)
- 【Level.1】単純にパッチ適用したい
    - AWS System Managerの一つである、『Patch Manager』を用いる（[Patch Manager前提条件](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/patch-manager-prerequisites.html)を満たしていること）
    - 『事前定義されたパッチベースライン』を用いることで、Patch Managerサポートの上でパッチ管理が行える
- 【Level.2】適用するパッチを変更したい
    - 『事前定義されたパッチベースライン』を使うだけでなく、『カスタムパッチベースライン』も作成できる
    - 『カスタムパッチベースライン』を用いることで、パッチ適用の基準等を変更する事ができる
- 【Level.3】環境(dev/prd)毎に、適用するパッチを分けたい
    - Patch Managerの『パッチグループ』機能を用いる事で、論理的にパッチ適用基準を分ける事ができる
    - EC2インスタンスタグ(key=PatchGroup value={PatchGroup名})を用いる事で、インスタンス毎にパッチ適用基準を変更できる
- 【Level.4】パッチ適用を自動化したい
    - AWS System Managerの一つである、『Maintenance Windows』を用いる
    - タスクに、RunCommandを設定する。
    - RunCommandで、SSMドキュメント『AWS-RunPatchBaseline』を実行する
- 【Level.5】環境(dev/prd)毎に、パッチ適用タイミングを分けたい
    - 【Level.4】で設定した、『Maintenance Windows』のターゲットに、PatchGroupを設定する
- 【Level.6】パッチ適用の前後にジョブを挟みたい
    - 【Level.4】で設定した、『Maintenance Windows』のタスクに、『StepFunctions』を設定する
    - 上記タスク登録時、入力オプションを下記に設定する事で、ターゲットのインスタンスIDを引数として渡すことができる
        - ``` {"targetInstanceId": "{{RESOURCE_ID}}"} ```


# 詳細
## 前提
### パッチの定義
各OSのセキュリティパッチとします。
### 対象OS
検証済のOSは以下の通りです。

- Amazon Linux2
- Windows2019
- RHEL8
- Ubuntu20.04
### Patch Managerの前提条件
本記事では、AWS Systems Manager(以下、SSM) の一機能である Patch Managerを使用します。前提条件は、[最新のAWS公式ドキュメント](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/patch-manager-prerequisites.html)をご確認下さい。
### ネットワークの前提
各OSのパッチソースリポジトリと疎通できる事が前提です。
もし、サーバーがインターネット疎通できない場合は、下記を参考にしてください。

[[参考]代替パッチソースリポジトリを指定する方法 (Linux)](https://docaws.amazon.com/ja_jp/systems-manager/latest/userguide/patch-manager-how-it-works-alt-source-repository.html)

Windowsの場合はオンプレ環境のWindows Server Update Services (WSUS) サーバの使用を検討してください。

## 【Level.1】単純にパッチ適用したい
SSMの、Patch Managerを利用する事でワンクリックで行えます。
また、AWSがサポートしている「事前定義されたパッチベースライン（※）」を用いることで、簡単にパッチ管理を行う事ができます。
（Patch Managerが、「事前定義されたパッチベースライン」を基にOSに応じたセキュリティパッチを適用します。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/c6163c2e-0adf-e7b3-87f1-42d6a6edf404.png)

パッチ適用に限りませんが、AWSがサポートしている部分は積極的に頼りましょう。
パブリッククラウドを利用する上での大きなメリットになります。

（※パッチベースラインとは？）
OSに対するセキュリティパッチの自動適用を管理するための設定の集合です。パッチベースラインでは、AWSが提供するセキュリティパッチを自動的に適用する基準を定義することができます。例えば、パッチの自動適用時期、適用するパッチの種類やバージョン、除外するパッチなどを指定することができます。

## 【Level.2】適用するパッチを変更したい

独自のセキュリティポリシー等で、「事前定義されたパッチベースライン」
を使えない場合は、パッチベースラインを編集しましょう。

Patch Managerから、パッチベースラインタブを開き「パッチベースラインを作成」を押下します。

オペレーションシステムから、設定したいOSの種類を選択します。

その際、「デフォルトのパッチベースライン」にチェックを入れます。
こうする事で、適用されるパッチの基準を変更する事が出来ます。（OS単位で設定できます）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/40a13ea8-5576-c0d3-1fab-181ae2aa1bdd.png)

## 【Level.3】環境(dev/prd)毎に、適用するパッチを分けたい
パッチグループを使用することで、インスタンスをグループ化し、異なるグループに異なるパッチを適用する事ができます。

### パッチベースラインの作成
まず、パッチベースラインを作成します。
先ほどと同じく、Patch Managerから、パッチベースラインタブを開き「パッチベースラインを作成」を押下します。

今回は、環境(dev/prd)毎なので下記のように作成します。

AML2-dev
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/e8ce0193-15d0-4926-3e8b-2080a3c304f0.png)



AML2-prd
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/f509c58c-7f9d-38dd-35e6-08498ce55a28.png)



### パッチグループの作成＆パッチベースラインの紐付け
次に、パッチグループの作成を行います。
パッチグループは、Patch Managerのパッチグループタブからは作成できないので注意してください。

パッチグループに紐づけたいパッチベースラインを開き、そこからパッチグループの追加(作成)を行います。

先ほどの例を用い、AML2の本番環境用のパッチグループを作成してみます。

**1. 先ほど作成した、パッチベースライン(AML2-prd)を選択します。**
**2. アクションから、パッチグループの変更を押下します。**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/7192b4f6-2ede-b45e-980a-775a34ff2849.png)

**3. 作成したパッチグループ名を入力し、作成を押下します。**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/8953de95-d4ce-4053-7bb7-206c05cafac1.png)

これで、パッチグループの作成と、パッチベースラインの紐付けが完了しました。

[[参考]パッチグループについて](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/sysman-patch-patchgroups.html)

### パッチグループとインスタンス(EC2)の紐付け
各インスタンス(EC2)に、パッチグループを定義するにはタグキー "Patch Group" または "PatchGroup" を使用します。

| Key | Value |
| --- | --- |
| PatchGroup | AML2-prd |
| PatchGroup  |AML2-dev |

Patch Managerは、上記で設定したパッチグループの値に基づいて、使用するパッチベースラインを評価します。
これで、環境毎に適用するパッチを変更する事が出来ました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/1730b0a3-77c5-5f28-873a-ecbf0a24dc07.png)

## 【Level.4】パッチ適用を自動化したい
中規模～大規模になれば、パッチ適用を自動化したい要件も出てくると思います。
方法は様々ありますが、今回はSystem ManagerのMaintenace Windowsを使用した方法を解説します。

Maintenace Windowsでは、
- When(メンテナンスウィンドウ)
- What(タスク)
- Where(ターゲット)

を設定します。
少々、癖のあるUIなので是非記事を見ながら理解してもらえればと思います。

### (When)メンテナンスウィンドウの作成
"create Maintenance Window"を押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/747d9a6f-a577-68a4-731b-ae299096b91e.png)

次に、スケジュールを入力していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/0a0e57d0-825c-1d64-2635-ee75ab9faa6c.png)

上記を例に、【期間】と【タスクの開始を停止する】を解説すると、
メンテナンスウィンドウが午後3時に開始し、【期間】が3時間ある場合、ウィンドウの終了時刻は午後6時になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/1702f9af-30af-968d-0a3c-1856b15a19ea.png)


加えて、【タスクの開始を停止する】値が1時間に設定されている場合、午後5時以降にはメンテナンスウィンドウ内のタスクを開始できません。つまり、午後3時から5時までの2時間の間しか、タスクが実行されません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/c57b1372-ee4f-4d07-7436-ee18b4cfe508.png)

詳細は、[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/sysman-maintenance-create-mw.html)をお読み下さい。

### (Where)ターゲットを作成する
次にターゲットグループを作成します。
先ほど作成したメンテナンスウィンドを選択し、ターゲットの登録を押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/7f02116a-b4b6-724e-93ba-0f14903b3816.png)

今回は、インスタンスを手動で選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/7e7b8fb2-9f68-ebb4-986f-0d3237ce86f0.png)

### (What)タスクを登録する
次に、タスクを登録します。今回は、パッチ適用タスクを登録します。
実施前に、パッチ適用時に使用するIAMロールを作成します。

**(前提)IAMサービスロール作成**
IAMサービスロールは、下記のように作成します。
(今回は、検証目的のため簡易的に作成します。権限等はかなり緩いため、productionで使用しないでください)

IAMロール作成
```bash
aws iam create-role --role-name <ロール名> --assume-role-policy-document file://trust-policy.json
```
<ロール名>: 任意のロール名
trust-policy.json: 以下のJSONを含むファイル

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

```

IAMポリシーの作成:
```bash
aws iam create-policy --policy-name <ポリシー名> --policy-document file://policy.json
```
<ポリシー名>: 任意のポリシー名
policy.json: 以下のJSONを含むファイル

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "ssm:*",
      "Resource": "*"
    }
  ]
}

```

IAMロールと、IAMポリシーのアタッチ:
```bash
aws iam attach-role-policy --role-name <ロール名> --policy-arn <ポリシーARN>
```

以上で、IAMロールの作成は終了です。

**タスクを登録**
まず、タスクの登録から「Run Commandタスクの登録」を押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/4b43aa70-74cf-12bd-b84f-47dcdafee18d.png)

次に、コマンドドキュメントから「AWS-RunPatchBaseline」を選択してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/6a27f7cc-b6dd-2ede-1a5d-72a0fa289490.png)

あとは、パラメーターを入力してきます。
ターゲットは先ほど作成した【ターゲットグループ】を指定してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/c57d4f50-c63a-a0b4-91c4-b07dda4986f9.png)

IAMロールは、先ほど作成したものを選択してください。

Operationは、ScanとInstallから選ぶことができます。
今回は、Installを選択します。他の項目は、適宜選択をしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/9cc60eef-af73-0c60-de5c-129fe9c1835c.png)

詳細は、[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/patch-manager-about-aws-runpatchbaseline.html)をご覧ください。


以上、簡単ですがタスクの登録でした。
これで、選択したインスタンスに自動でパッチが適用されるようになりました！

## 【Level.5】環境(dev/prd)毎に、タイミングを分けたい
Level.3で作成した、メンテナンスウィンドウのターゲットグループを変更する事で実現します。
ターゲットの登録を選択、【インスタンスタグの指定】を押下し、下記のように入力します。
| Key | Value |
| --- | --- |
| PatchGroup  |AML2-dev |


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/89582e85-182e-c428-6257-7ffc328e1548.png)

あとは、パッチグループ毎にメンテナンスウィンドウを作成し、起動時刻を変えるだけです。（Level.3の手順範囲の為、省略）
Level.2で作成したパッチグループを用いる事で、簡単に適用タイミングを分けることができます！

## 【Level.6】パッチ適用の前後にジョブを挟みたい
既存の運用をAWSに持ってくる場合、特にジョブ管理システムを別で持っていた場合は
パッチ適用前後にジョブを実行したい。というケースはかなり多いです。

例えば、
- 事前バックアップ
- 前後にジョブ(Shell)を実行したい
- 監視システムを無効化するAPIを投げたい
- etc...

これを実現する方法も沢山ありますが、今回はAWS Step Functionsを使用したいと思います。

### 全体像
先ほど作成したメンテナンスウィンドウのタスクでAWS Step Functionsを呼び出したいと思います。
呼び出す際は、対象のインスタンスIDを入力値に設定する事とします。
```
targetInstanceId=xxxx
```

### AWS Step Functionsの作成
今回は、下記３つのタスクを作成します。
- 事前ジョブ実行(Shell)
- バックアップ作成(AWS Backup)
- パッチ適用

早速、作成していきましょう。
ステートマシーンの作成を押下し、「ワークフローを視覚的に設計」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/20584522-af78-396f-8aa4-baf1af0305ac.png)

3つのアクションを置きます。識別できるよう、状態名も変えておきます。
- 事前ジョブ実行(Shell)
    - System Manager:SendCommand
- パッチ適用
    - System Manager:SendCommand
- バックアップ作成(AWS Backup)
    - Backup: StartBackupJob
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/bf9f3baf-adb4-b32e-2660-b593cc13c53e.png)

では、それぞれのアクションにパラメーターを設定していきます。

**事前ジョブ実行(Shell)**
APIパラメーターに下記を設定します。

ちなみに、$.targetInstanceIdと設定する事で
StepFunctionsの入力で設定した、targetInstanceIdを参照する事ができます。
```
{
  "DocumentName": "AWS-RunShellScript",
  "InstanceIds.$": "States.Array($.targetInstanceId)",
  "Parameters": {
    "commands": [
      "{起動したい.shのPath}"
    ]
  }
}
```
**パッチ適用**
```
{
  "DocumentName": "AWS-RunPatchBaseline",
  "InstanceIds.$": "States.Array($.targetInstanceId)",
  "Parameters": {
    "Operation": [
      "Install"
    ]
  }
}
```
**バックアップ作成(AWS Backup)**
```
{
  "BackupVaultName": "Default",
  "ResourceArn.$": "States.Format('arn:aws:ec2:ap-northeast-1:{account_id}:instance/{}', $.targetInstanceId)",
  "IamRoleArn": "｛backup IAMロール→設定値は公式docを参照して下さい｝",
  "Lifecycle": {
    "DeleteAfterDays": 180
  }
}
```

上記入力が完了したら、次へを押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/0e74f7f2-98c6-5d29-3079-e608e3fcf67e.png)

ステートマシン名を、入力し
実行ロールは、新しいロールの作成を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/7799e557-1423-aeb3-15dc-5b86e9d5bc44.png)

以上で作成が完了しました！

### メンテナンスウィンドウのタスクに、StepFunctionsを設定
では、先ほど作成したStepFunctionsをタスクに設定します。

タスク登録から、StepFunctionsタスクの登録を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/9257e688-0802-8728-bd68-00968893228e.png)

入力には、下記Jsonを入力してください。
```json
{"targetInstanceId": "{{RESOURCE_ID}}"}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/166c741d-510d-b031-b9d6-8367f5e32b57.png)

RESOURCE_IDに、実行されるターゲット（InstanceID）が入ります。
上記設定を入れる事で、StepFunctionsの入力値にInstanceIDを渡すことができます。

あとは、基本的にLevel.3で設定した値でOKです。
入力を完了します。

以上、StepFunctionsを用いることで
パッチ適用の前後に様々なジョブを挟むことができました！

## さいごに
いかがでしたでしょうか？
AWSの運用サービスを使いこなすだけで、パッチ適用を簡単に設定する事が出来ます。

また、StepFunctionsはエラーハンドリング機能も充実しているので
作りこめば、複雑な条件下でジョブを実行することも可能です。

個人的には、オンプレ時代のジョブ管理機能をそのまま移管するのは
お勧めできません（クラウドにはクラウドの運用方法があると思っています）が、
作りこみ次第で、いくらでもリッチにできるということを知って頂ければと思います。