AWS Systems Manager Automation Runbook を CDK で簡単に構築する

Systems Manager Automation Runbook を CDKで作ってみます。
2023.05.03

1. はじめに

お疲れさまです。とーちです。
AWS Systems Manager Automation(以下 Automation)をご存知でしょうか?
ざっくりご説明すると運用作業等のタスクを一連のフローとして定義し実行できるサービスなのですが、最近この Automation Runbook(フローが書かれた定義)を CDK で簡単に作成できることを知ったので本ブログではこのやり方をご紹介したいと思います。

2. SSM Document CDK について

Automation Runbook は SSM ドキュメントの一種なのですが、この SSM ドキュメントを CDK の L2 コンストラクトとして実装できるライブラリが公開されています。

SSM ドキュメントは様々な種類のものが存在するのですが、このライブラリでサポートされているのは 2023/5/2 現時点では、以下の2つとなっており、かつ CommandDocument についてはシミュレーション(作成した SSM ドキュメントのローカルでのテスト)が制限されている点にご注意ください。

  • AutomationDocument(Automation Runbook)
    • Systems Manager の Automation,State Manager,Maintenance Windows で使用できるタイプの SSM ドキュメントです
  • CommandDocument
    • Systems Manager の Run Command,State Manager,Maintenance Windows で使用できるタイプの SSM ドキュメントです

他にもこのライブラリには SSM ドキュメント関連の色々な機能があるので、興味のあるかたは上記のリポジトリを確認してみてください。

3. やってみた

今回は実際に上記のライブラリを使って、Systems Manager Automation を使った以下のようなワークフローを作ってみたいと思います。

  • Automation 実行時の入力パラメータとして EC2 インスタンス ID と DryRun フラグを指定して実行
  1. DryRun がTrueならステップ 3 を、Falseならステップ 2 を実行する
  2. 承認者が承認を行う
  3. 指定されたタグの EC2 を(Dryrun が True のときは Dryrun で)停止する

図で書くとこんなかんじです。

3.1. 前準備

まず cdk-ssm-documents ライブラリをインストールします。

> mkdir cdk-ssmAutomation-test/ && cd cdk-ssmAutomation-test
> cdk init app --language=typescript
> npm install @cdklabs/cdk-ssm-documents --save-prod

3.2. CDK コード作成

CDK のコードは以下の通りとなります。

lib/cdk-ssm_automation-test-stack.ts

import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as iam from "aws-cdk-lib/aws-iam";
import * as sns from "aws-cdk-lib/aws-sns";
import * as snssub from "aws-cdk-lib/aws-sns-subscriptions";
import {
AutomationDocument,
AwsApiStep,
BranchStep,
ApproveStep,
Choice,
DataTypeEnum,
DocumentFormat,
Input,
Operation,
StringVariable,
StringListVariable,
HardCodedString,
HardCodedStringList,
AwsService,
isStringList,
} from "@cdklabs/cdk-ssm-documents";
require("dotenv").config();

export class CdkSsmAutomationTestStack extends cdk.Stack {
// シミュレーション(テスト)時に使用するので変数定義しておく
readonly myDoc: AutomationDocument;

    constructor(scope: Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);
        // 環境変数で承認者のIAMロールARNと承認者のメールアドレスを指定
        const approverroleArn: string = process.env.APPROVER_ROLE_ARN ?? "";
        const snsSubEmail: string = process.env.SNS_SUB_EMAIL ?? "";
        // 作成するAutomation Runbookが使用するサービスロール
        const svcrole = new iam.Role(this, "mysvcrole", {
            roleName: "AutomationServiceRole",
            assumedBy: new iam.ServicePrincipal(
                "ssm.amazonaws.com"
            ).withConditions({
                ["StringEquals"]: {
                    "aws:SourceAccount": cdk.Stack.of(this).account,
                },
                ["ArnLike"]: {
                    "aws:SourceArn": `arn:aws:ssm:*:${
                        cdk.Stack.of(this).account
                    }:automation-execution/*`,
                },
            }),
        });
        svcrole.addManagedPolicy(
            iam.ManagedPolicy.fromAwsManagedPolicyName(
                "service-role/AmazonSSMAutomationRole"
            )
        );
        svcrole.addManagedPolicy(
            iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonEC2FullAccess")
        );
        // 承認ステップに入ったときに通知で使用するSNS
        const mysns = new sns.Topic(this, "mysns", {
            // AmazonSSMAutomationRoleポリシーではプレフィックスとして「Automation」
            // がついているSNSトピックにしかpublishできないので注意
            topicName: "AutomationSnsTopic",
        });
        mysns.addSubscription(new snssub.EmailSubscription(snsSubEmail));

        // Systems Manager Automation Runbook
        // Automation Runbook(SSMドキュメント)のインスタンスを作成
        this.myDoc = new AutomationDocument(this, "myAutomationRunbook", {
            documentFormat: DocumentFormat.YAML,
            tags: [{ key: "myTag", value: "myValue" }],
            documentName: "StopEC2",
            description: "Stop the ec2 instance.",
            assumeRole: HardCodedString.of(svcrole.roleArn),
            updateMethod: "NewVersion", //SSMドキュメント更新時の動作
            docInputs: [
                //Automation Runbook実行時にユーザーが入力するパラメータの設定
                Input.ofTypeString("DryRun", {
                    allowedValues: ["True", "False"],
                    description: "(Required) Specify true to dryrun.",
                }),
                Input.ofTypeStringList("TargetInstanceIds", {
                    description: "(Required) Specify instance IDs to Stop.",
                }),
            ],
        });
        // 手動承認ステップを作成
        // https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/automation-action-approve.html
        const approveStep = new ApproveStep(this, "approve", {
            // 承認者のIAM ARNを指定する
            approvers: HardCodedStringList.of([approverroleArn]),
            // 本ステップに入ったときに通知に使用するSNSトピックARNを指定
            notificationArn: HardCodedString.of(mysns.topicArn),
        });

        // AWS APIの呼び出し実行ステップを作成、ほとんどのAPI操作がサポートされているとのこと
        // https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/automation-action-executeAwsApi.html
        const apiStep = new AwsApiStep(this, "api", {
            service: AwsService.EC2,
            pascalCaseApi: "StopInstances", // 実行する API オペレーション名
            apiParams: {
                // 実行するAPIに渡すパラメータ
                InstanceIds: StringListVariable.of("TargetInstanceIds"),
            },
            isEnd: true, //このステップでAutomation実行を終了させるときにtrueにする
            // このステップのoutputとして出力する値を指定、outputは後続のステップでinputとして使用することも可能
            outputs: [
                {
                    outputType: DataTypeEnum.STRING,
                    name: "CurrentState",
                    // APIレスポンスの中のどの値を取得するかをJSONPath 構文で指定する
                    selector: "$.StoppingInstances[0].CurrentState.Name",
                },
            ],
        });
        // フローを条件分岐させるためのステップを作成
        // https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/automation-action-branch.html
        const branchStep = new BranchStep(this, "branch", {
            choices: [
                new Choice({
                    operation: Operation.STRING_EQUALS, // 条件判断に使用する条件式
                    variable: StringVariable.of("DryRun"), // 条件判断に使用するAutomation実行時に指定するパラメータ
                    constant: "True", // 条件判断に使用する定数、これとvariableで指定した値を比較する
                    jumpToStepName: apiStep.name, // 条件に合致した場合の次のステップ
                }),
                new Choice({
                    operation: Operation.STRING_EQUALS,
                    variable: StringVariable.of("DryRun"),
                    constant: "False",
                    jumpToStepName: approveStep.name,
                }),
            ],
        });

        // AutomationDocumentインスタンスにaddStepで処理ステップを追加する。
        // デフォルトでは追加した順番でAutomationの処理が実行される
        this.myDoc.addStep(branchStep);
        this.myDoc.addStep(approveStep);
        this.myDoc.addStep(apiStep);
    }

}

ポイントとなる部分を解説していきます。

Automation 実行時に使用する IAM ロールについて

Automation では上記の通り AWS API を直接実行することが出来ますが、API を実行するために IAM 権限が必要となります。
Automation はデフォルトでは Automation を実行したユーザーの IAM 権限で Automation の各ステップの処理が実行されますが、ベストプラクティスとしては、サービスロールを使用する(Automation Runbook 自体に使用するロールを定義する)ことが推奨されます。今回はそれに従った形で実装しました。73 行目の指定がそれにあたります。
サービスロールに付与する IAM ポリシーは AWS 管理ポリシーの「AmazonSSMAutomationRole」を必ずつけ、その他に必要な IAM 権限を追加するようにしましょう。
参考:Method 2: Use IAM to configure roles for Automation - AWS Systems Manager

Automation の各ステップ等に入力として与えるパラメータについて

Automation 実行時には入力パラメータを指定することができ、Automation の各ステップ等でそのパラメータの値を使用することが出来ます。しかし、例えば承認ステップにおける承認者の IAM ARN 等、入力パラメータではなく固定の値を使用したい場合も多くあるかと思います。CDK 上で固定の値をコーディングしたい際にどのように書けばいいのかで少し悩みましたのでご説明します。

まず SSM ドキュメントの構文として入力パラメータには以下のタイプが用意されています。

  • String、StringList、Integer、Boolean、MapList、StringMap

SSM Document CDK ライブラリでは上記それぞれのタイプ毎に以下の2種類のクラスが用意されています。

  • ~Variable(例:StringVariable、StringListVariable)
  • HardCoded~(例:HardCodedString、HardCodedStringList)

「~Variable」は Automation 実行時の入力されたパラメータの値を使う場合に使用します。
「HardCoded~」ですが、こちらが SSM ドキュメントにハードコードしたい場合に使用するクラスとなっています。
つまり、入力パラメータの値を使う場合は「~Variable」、入力パラメータではなく固定の値を使いたい場合は「HardCoded~」を使う形となります。

また、各ステップの入力だけではなく、Automation Runbook で使用するサービスロールの指定などにも、上記の「~Variable」または「HardCoded~」を使って指定するプロパティがある点にも注意です。

それぞれのクラスの使い方ですが、「~Variable」の場合は以下のような形になります。「~Variable」クラスの of メソッドを使って引数に入力パラメータ名を指定する形です。

        this.myDoc = new AutomationDocument(this, "myAutomationRunbook", {
            documentFormat: DocumentFormat.YAML,
            <中略>
            docInputs: [
                //Automation Runbook実行時にユーザーが入力するパラメータの設定
                Input.ofTypeString("DryRun", {
                    allowedValues: ["True", "False"],
                    description: "(Required) Specify true to dryrun.",
                }),
                Input.ofTypeStringList("TargetInstanceIds", { //入力パラメータ名
                    description: "(Required) Specify instance IDs to Stop.",
                }),
            ],
            <中略>
        const apiStep = new AwsApiStep(this, "api", {
        <中略>
            apiParams: {
                // 実行するAPIに渡すパラメータ
                InstanceIds: StringListVariable.of("TargetInstanceIds"),// 入力パラメータ名を指定
            },

また、「HardCoded~」の場合は以下のような形になります。「HardCoded~」クラスの of メソッドを使って引数にハードコードする値を入れます。ここでは、前段で作成した SNS インスタンスのプロパティ(topicArn)を参照して値を入れています。もちろん直接値を書くことも可能です。

        const approveStep = new ApproveStep(this, "approve", {
            <中略>
            // 本ステップに入ったときに通知に使用するSNSトピックARNを指定
            notificationArn: HardCodedString.of(mysns.topicArn),
        });

3.3. 動かしてみる

作成された Automation Runbook を実際に動かしてみます。あえて EC2 関連の IAM 権限を持っていない IAM ロールを作って動かしてみました。

  1. AWS Systems Manager の画面から「オートメーション」を選択し、「オートメーションの実行」を押します。途中の画面は割愛します。
  2. 「シンプルな実行」を選択し、入力パラメータに起動済の EC2 のインスタンス ID を入れます。DryRun フラグは False で動かします。画面を下にスクロールして「実行」を押せば Automation の実行が始まります。
  3. 承認ステップに到達するとこんな感じのメールが届きます。
  4. Approve の URL をクリックすると承認画面に移ります。このとき承認者として指定した IAM でログインしていないと承認できません。承認を選択して送信すると承認されます。
  5. EC2 画面で対象の EC2 が正常に停止されたことが確認できました。

4. まとめ

AWS Systems Manager Automation Runbook を CDK で作ってみました。
タイトルでは簡単にと書いてしまいましたが、個人的には結構詰まったところが多かったのでなかなか大変でしたが、慣れると簡単に SSM ドキュメントを作れるので大変便利です。
実はシミュレーション(ローカルでのテスト)もやってはみたのですが、自分の理解力が足らずテストを成功ステータスにすることが出来なかったので、この記事には載せませんでした。
シミュレーションも便利な機能だと思うので、うまく検証できたらまた別記事で書きたいと思います。

この記事が誰かのお役に立てばなによりです。
以上、とーちでした。

参考