🤖

Systems manager ランブックの変数について

に公開

概要

業務でAWS Systems Managerの変更管理ツール機能の1つである、ドキュメントのオートメーションランブックをコードベースで作る際に変数の定義の仕方でわかりにくいと感じた個所があったので備忘録として残しておく。

まとめのなかで使用しているyamlコードはGeminiに書いてもらったものを適宜修正した内容になっている。
内容自体が重要にはなっていないので細かいミスがあってもそこはスルーで。(一応AWS上で動く状態ではある)

ランブック(yaml)の基本的な構造

後続のメモでわかりやすくまとめるために「EC2インスタンス停止用ランブック」をyamlで定義しておく。
ランブックをyamlで定義するときの基本的な要素はだいたい使っていると思う。

簡単にどういうステップにしているかというと、

  1. 停止用EC2インスタンスID確認
  2. 承認者の確認
  3. 停止

の流れになっている。

特に意味はないけど、業務で取り組んだ内容と似た構成にはしてみた。
(対象サービスとか正規表現でのチェック内容とかはテキトーにしてる)

EC2インスタンス停止用ランブック
description: 'EC2インスタンスを承認後に停止するランブック'
schemaVersion: '0.3'
assumeRole: '{{ AutomationAssumeRole }}' # このランブックを実行するIAMロールのARN

parameters:
  InstanceId:
    type: 'String'
    description: '(必須) 停止するEC2インスタンスのID'
  Approvers:
    type: 'StringList'
    description: '(必須) 承認者のIAMユーザーまたはロールのARNリスト'
  AutomationAssumeRole:
    type: 'String'
    description: '(必須) Automationが引き受けるIAMロールのARN'
    allowedPattern: '^arn:aws:iam::\d{12}:role/[\w+=,.@-]+$'
  Message:
    type: 'String'
    description: '(オプション) 承認リクエスト時に表示するメッセージ'
    default: '停止するEC2インスタンスは想定しているものですか?'

mainSteps:
  - name: validateInstanceIdFormat
    action: 'aws:executeScript'
    description: '入力されたInstanceIdの形式が正しいか正規表現でチェック'
    timeoutSeconds: 30
    isCritical: true       # このステップの失敗はAutomation全体の失敗とみなす
    onFailure: 'Abort'     # 失敗した場合、Automationを中止する
    nextStep: waitForApproval
    inputs:
      Runtime: 'python3.11' # 使用するPythonランタイムを指定 (環境に合わせて変更可能)
      Handler: 'validate_format' # 実行する関数名
      InputPayload:
        # スクリプト内の 'event' 辞書に渡すデータ
        InstanceId: '{{ InstanceId }}' # parametersからInstanceIdの値を取得して渡す
      Script: |-
        import re

        def validate_format(event, context):
            """
            渡されたInstance IDが指定された正規表現パターンに一致するか検証する。
            一致しない場合は例外を発生させてステップを失敗させる。
            """
            instance_id = event['InstanceId']
            # 検証する正規表現パターン
            pattern = r'^i-[0-9a-fA-F]{8,}$|^i-[0-9a-fA-F]{17,}$'

            print(f"Validating InstanceId: {instance_id}")

            if not re.match(pattern, instance_id):
                error_message = f"Invalid InstanceId format: '{instance_id}'. It does not match the required pattern."
                print(error_message)
                # 例外を発生させてステップを失敗させる
                raise ValueError(error_message)
            else:
                success_message = f"InstanceId format for '{instance_id}' is valid."
                print(success_message)
                # 成功を示すメッセージなどを返す(オプション)
                return {"status": "Success", "message": success_message}
    outputs:
      - Name: ValidationResultPayload
        Selector: '$.Payload' # スクリプトの戻り値全体
        Type: 'StringMap'
      - Name: ValidationStatus
        Selector: '$.Payload.status' # スクリプトの戻り値から status を抽出
        Type: 'String'

  - name: waitForApproval # 元の承認ステップ
    action: 'aws:approve'
    timeoutSeconds: 3600 # 承認待ちのタイムアウト時間 (秒)
    nextStep: stopInstance
    inputs:
      Message: '{{ Message }}'
      Approvers: '{{ Approvers }}'
      MinRequiredApprovals: 1 # 承認に必要な最低人数

  - name: stopInstance
    action: 'aws:changeInstanceState'
    maxAttempts: 1
    timeoutSeconds: 600
    onFailure: 'Abort'
    isEnd: true
    inputs:
      InstanceIds:
        - '{{ InstanceId }}'
      DesiredState: 'stopped'

yamlでランブックを書くときは、

  • schemaVersionとdescriptionはコードで新規作成すると記載されていて、descriptionの内容を書き換える感じ
  • ランブック実行時に入力したいパラメータを定義、メインステップの中で処理を定義

ってのが大まかなやることかな。

パラメータの使い方やステップの出力、InputPayloadの変数値の扱い方

上記の「EC2インスタンス停止用ランブック」を踏まえて、特に自分がわかりにくいと感じたのが、

  1. パラメータセクションで定義した変数の使い方
  2. ステップで出力した変数を次のステップでどう使うのか
  3. (aws:executeScriptアクションにおいて)InputPayloadでの変数の定義の仕方とハンドラー内での使い方

このあたりの内容。

ちなみにランブックで扱う変数は一応型が存在していて、基本的にはStringとして扱われている
そのことが書かれていたのが以下のページ
アクション出力の入力としての使用

ランブックで扱う変数についてポイントになると思ったのが、

  • ランブック内のパラメータのデータ型は静的
    つまり、parametersセクションで定義した変数の型は、後続のmainSteps内の処理で変更することはできない

  • ステップの出力を定義した際の挙動
    mainSteps内の処理でoutputsを定義するとき以下のような内容になり、

具体例(「aws:executeAwsApi」を使った場合)
mainsteps
	- name: stopEC2Instance
	  action: aws:executeAwsApi
		inputs:
		  Service: ec2
			Api: StopInstances
	  outputs:
		  - Selector: $.StoppingInstances[0].InstanceId
			  Name: StoppingInstance
			  Type: String

「Typeで指定したデータ型」と「処理によって実際に出力された値のデータ型」が一致しているかどうかが重要になる。
一致していない場合は「Typeで指定したデータ型」に変換される。

ただし、変換できる関係性としては以下の規則がある

String 値は StringList、Integer、および Boolean に変換できます。
Integer 値は String および StringList に変換できます。
Boolean 値は String および StringList に変換できます。
1 つの要素を含む StringList、IntegerList、または BooleanList 値は、String、Integer または Boolean に変換できます。

1. パラメータセクションで定義した変数の使い方

簡単にランブックで扱う変数について触れたところで、わかりにくかった内容を見ていく
「パラメータセクションで定義した変数」というのは以下の箇所で定義した変数(仮にパラメータ変数とする)で、これを後続の処理の中でどう使うか、という話

(略)
parameters:
  InstanceId:
    type: 'String'
    description: '(必須) 停止するEC2インスタンスのID'
  Approvers:
    type: 'StringList'
    description: '(必須) 承認者のIAMユーザーまたはロールのARNリスト'
(略)

さっきも言ったように、ここで宣言した変数の型は後続の処理で変更はできない

そして、このパラメータ変数は以下のようにステップの中で使用できる

mainsteps:
(略)
	- name: stopInstance
	action: 'aws:changeInstanceState'
	maxAttempts: 1
	timeoutSeconds: 600
	onFailure: 'Abort'
	isEnd: true
	inputs:
		InstanceIds:
			- '{{ InstanceId }}'
		DesiredState: 'stopped'

2. ステップで出力した変数を次のステップでどう使うのか

まず、ステップでの出力の定義としては、大きく2パターンがあると思っていて、

  • AWS APIアクションを使っている場合のOutputs
  • スクリプト実行ステップの場合のOutputs

このそれぞれで若干参照値の形式が違う

AWS APIアクションを使っている場合のOutputs
これは先ほどの具体例で示した通り

具体例(「aws:executeAwsApi」を使った場合)
mainsteps
	- name: stopEC2Instance
	  action: aws:executeAwsApi
		inputs:
		  Service: ec2
			Api: StopInstances
	  outputs:
		  - Selector: $.StoppingInstances[0].InstanceId
			  Name: StoppingInstance
			  Type: String

このSelectorの箇所をどう書けばいいかが最初わからなかった。

Selectorの指定の仕方について書かれているページを見つけるのに時間がかかってしまったのでここにメモしておく
ランブックでの JSONPath の使用

基本的には上記の内容に沿って、Boto3の各APIアクションページの「Response Syntax」をみて書いていくイメージ

スクリプト実行ステップの場合のOutputs
これはステップのactionが「aws:executeScript」の時の話で
公式ページの例にも書いてあるように、

action: "aws:executeScript"
inputs: 
 Runtime: runtime
 Handler: "functionName"
 InputPayload: 
  scriptInput: '{{parameterValue}}'
 Script: |-
   def functionName(events, context):
   ...
 Attachment: "scriptAttachment.zip"

このScriptで定義している関数の返り値をステップの出力として定義するには、

outputs:
- Type: String
  Name: TagKey
  Selector: $.Payload.Key

このように定義する。
このときのSelectorの最初の指定が「Payload」になることがポイント。

公式の該当箇所にも記載があるけど、

Payload
関数によって返されるオブジェクトの JSON 形式です。最大 100KB が返されます。リストを出力する場合、最大 100 個の項目が返されます。

3. InputPayloadでの変数の定義の仕方とハンドラー内での使い方

これもステップのactionが「aws:executeScript」の時の話になるけど、

InputPayload:
  instanceId: '{{InstanceId}}'
Script: |-
	def tagInstance(events,context):
		import boto3

		#Initialize client
		ec2 = boto3.client('ec2')
		instanceId = events['instanceId']
		tag = {
				"Key": "Env",
				"Value": "ExamplePython"
		}
		print(f"Adding tag {tag} to instance id {instanceId}")
		ec2.create_tags(
				Resources=[instanceId],
				Tags=[tag]
		)
		return tag

このように、InputPayloadで関数に渡すための値を定義し、
それを関数側でeventsとして受け取って、events['instanceId'] のように使うことができる

全体的なまとめ

自分がわかりにくいと感じた個所について整理してみた。
特にランブックで扱う変数周りに関して「定義の仕方」と「使い方」をまとめた。

Discussion