Cloud9とSAMでStep Functionsを試す(part2_コード編集)

記事タイトルとURLをコピーする

はじめに

SRE1課の石井です。 本記事は同タイトルの「part1_環境準備」の続きとなる記事です。 以下本記事の概要です。

  • 本記事では下記の要素の記法や使い方についてフォーカスしています。

    • Step Functions
    • StateMachine(ASLファイル)
    • Former2
  • 本記事では複雑な条件を持たず、EC2に単純なコマンドを実行するStateMachineを作成を目指します。

対象者

StepFunctionsのASLファイルを初めて編集する人

Step Functions について

まずはStep Functionsについて軽く概要について軽く見てみます。

Step Functions 概要

Step Functionsはワークフローを作成するサービスです。 Step Functionsの特徴としてワークフローの処理内容をステートマシンとして「Amazon States Language」(以下ASL)という独自のDSL言語を用いて定義できます。 StateMachineは実行するとマネージドコンソール上でヴィジュアライズされた形式で確認できます。

StateMachineとは

アプリケーションは、複数の処理が連携して動作します。それぞれの処理を実行するためには処理(State)間のデータの受け渡し、条件分岐、同期・並列実行、リトライや例外処理等の様々な考慮が必要になります。 これらの一連の処理内容を「StateMachine」として定義してStep Functionsに登録します。

AWS Step Functions とは?

Step Functionsの注意点

Step Functions単体ではStateMachineをスケジューリングしたりタスク失敗時に特定のタスクから再実行するといった機能がありません。 また、タスクは全てAWSの各種サービスにAPI通信を行うので、EC2のLinuxサーバへ直接bashコマンドをタスクに記載することもできません。

イメージにすると下記の図になると思います。

Step Functions のメリット

上記の内容をまとめて、Step Functionsというサービスのメリットをまとめると下記となります。

  • StateMachine(ワークフローの処理内容)をコードとして管理できる(YAMLまたはJSON形式)
  • AWSの各種サービスに対する作業(インスタンスの停止や起動、失敗時の作業など)を自動化できる
  • APIの通信となるためエージェントという概念がなく、ジョブ実行するためにマネージャからSSHする必要もないため導入が楽

StateMachine(ASLファイル)

本項目ではsam initでクローンしたサンプルファイルを元にStateMachineを編集します。 シンプルに以下の動作をするStateMachineを作ってみます。

実装するStateMachine:

  1. 実行時に指定されたEC2のインスタンスIDを起動
  2. 対象のEC2対しbashで「uname -a、python -V」を実行
  3. 起動したインスタンスを停止する

ASLファイルを編集

ASLファイルを編集してみます。 サンプルのaslの場所は、プロジェクトディレクトリ直下の「statemachine」ディレクトリ内の「stock_trader.asl.json」となります。

中を開くとマネージドコンソールで見た処理内容がコードで記載されています。 サンプルはjsonで出力されていますが、#でコメントを残して作業していきたいので、ymlに変換して編集します。(変換は各種ツールやVisual Studio Codeの拡張機能などで行えます)

今回実装するStateMachineの内容をASLで表現すると下記となります。 シンプルな内容にしたいため、エラーやリトライに関しては次回の記事で記載します。

sampleのStateMachine

Comment: sample
StartAt: StartInstances
States:
  StartInstances:
    Type: Task
    #実行時のjsonで指定された引数を$.InstanceIdsで取得し、InstanceIds.$に格納される
    Parameters:
      InstanceIds.$: States.Array($.InstanceIds)
    Resource: arn:aws:states:::aws-sdk:ec2:startInstances
    #ステートで出力されたパラメータを$.StartingInstancesで受け取る
    ResultPath: $.StartingInstances
    #次のステートに渡すパラメータを指定。$で全てのパラメータを渡す
    OutputPath: $
    Next: WaitInstanceStart
  WaitInstanceStart:
    Type: Wait
    Seconds: 60
    Next: SendCommand
  SendCommand:
    Type: Task
    Resource: arn:aws:states:::aws-sdk:ssm:sendCommand
    InputPath: $
    Parameters:
      DocumentName: AWS-RunShellScript
      InstanceIds.$: States.Array($.InstanceIds)
      Parameters:
        commands:
          - python -V
          - uname -a
    ResultPath: $.SendCommand
    OutputPath: $
    Next: StopInstances
  StopInstances:
    InputPath: $
    Type: Task
    Parameters:
      InstanceIds.$: States.Array($.InstanceIds)
    OutputPath: $
    Resource: arn:aws:states:::aws-sdk:ec2:stopInstances
    End: true

sampleのStateMachine実行時に指定するパラメータ

{
    "InstanceIds": "i-XXXXXXXXXXX"
}

StateMachine全体に関わる定義

それではStateMachineの内容を細かく確認します。

Comment: sample
StartAt: StartInstances
TimeoutSeconds: 600
States:
  StartInstances:

StateMachine全体に関わる定義です。実際の処理内容(ステート)の前に記載します。

定義内容:

  • Comment (オプション):ステートマシンの人間が読み取れる説明。
  • StartAt (必須):最初に実行するステートの名前を指定します。プログラムのmainの指定に近いイメージ。
  • TimeoutSeconds (オプション) : 秒数を指定する。ステートマシン全体 の実行時間がこの秒数を超過するとタイムアウトエラー になり実行が終了する。
  • States (必須):ステートマシンを構成するStateを定義する。複数含めることが可能。

参考:ステートマシン構造

Stateに関する定義:インスタンスの起動

    Type: Task
    Parameters:
      InstanceIds.$: States.Array($.InstanceIds)
    Resource: arn:aws:states:::aws-sdk:ec2:startInstances
    ResultPath: $.StartingInstances
    OutputPath: $
    Next: WaitInstanceStart

ステート(処理内容)に関する記載です。 ステートは8種類あり、各種ステートは実装したい処理に合わせて別途調べるのが良さそうです。 今回はtaskとwaitを使用しています。

参考:状態

定義内容:

インスタンスを起動する処理を記載しています。他の設定事項は下記となります。

  • Type(必須):ステートの種類を選択します。今回は単一の処理を行うステートのTaskを選択しています。
  • Parameters (オプション):接続されたリソースの API アクションに情報を渡すのに使用します。
    • InstanceIds.$:実行時に指定されたjsonの内容を受け取ります。詳細は後述。
  • Resource (必須):URI (特に、実行する特定のタスクを一意に識別する ARN)。
  • ResultPath (オプション):処理結果となるパラメータをどのようなパラメータ名で受け取るか指定。
  • OutputPath (オプション):次の State に対して引き渡す (出力する) データを指定。$で全ての内容を次の処理に渡す。今回は実行時に指定された「InstanceIds」と「StartingInstances」が格納されます。

またマネージドコンソール上では「ステップ出力」のタブに出力内容が表示されます。

参考:Task

States.Array($.InstanceIds)について

States.Array($.InstanceIds)という表記はstartInstancesというAPIの入力が配列形式となっているため、このような記載を行っています。

StartInstancesというAPIは下記のように、配列形式で入力することが記載されています。

InstanceId.N

The IDs of the instances.

Type: Array of strings

InstanceIdsを下記のような配列で記載したいのですが・・・

"InstanceIds.$": [
    "$.InstanceIds"
  ]

保存しようとすると、構文チェックに引っかかり下記のエラーが発生します。

ステートマシンの定義に Amazon ステート言語エラーがあります。エラーを修正して続行してください。 The value for the field 'InstanceIds.$' must be a STRING that contains a JSONPath but was an ARRAY (at /States/StartInstances/Parameters)

そのためASLファイルでは、パラメータに配列で渡すときはStates.Arrayという組み込み関数を使用します。

参考:組み込み関数

Stateに関する定義:インスタンス起動のためのwait

  WaitInstanceStart:
    Type: Wait
    Seconds: 60
    Next: SendCommand

インスタンスを起動するまで60秒待機する処理です。 本来であれば続く処理にリソースの確認を行い「InProgress状態であれば〜・・・」と言った処理を追加しますが、今回はフローが複雑になるためwait処理で記載しています。

Stateに関する定義:インスタンスに対してコマンド実行

  SendCommand:
    Type: Task
    Resource: arn:aws:states:::aws-sdk:ssm:sendCommand
    InputPath: $
    Parameters:
      DocumentName: AWS-RunShellScript
      InstanceIds.$: States.Array($.InstanceIds)
      Parameters:
        commands:
          - python -V
          - uname -a
    ResultPath: $.SendCommand
    OutputPath: $
    Next: StopInstances

定義内容:

EC2にコマンドを実行する処理です。 実体はSSMでbashを実行しています。(マネージドコンソールからRun Commandを選び「AWS-RunShellScript」ドキュメントを選択して実行するコマンドを指定するイメージです。)

他の設定事項は下記となります。

※他Parameters以外は全てインスタンス起動のステートと同一なので省略

  • InputPath(オプション):渡された入力のうち、何をState内で使用するかを指定。$を指定すれば全てステートに渡ります。

こちらも出力と同じく、マネージドコンソールから入力されるパラメータを確認することができます。

Parameters以下の内容:

Resourceで指定したSendCommandのAPIリファレンスを見ながら記載します。

  • DocumentName(必須):bashを実行したいので、「 AWS-RunShellScript」というドキュメントを選択しています。
  • InstanceIds.$(必須):インスタンスIDですが、前述の通り引数形式なので組み込み変数を使います。
  • Parameters(オプション):SendCommandのParametersオプションです。実行するbashコマンドの内容を指定しています。

参考:SendCommand

Stateに関する定義:インスタンスの停止

  StopInstances:
    InputPath: $
    Type: Task
    Parameters:
      InstanceIds.$: States.Array($.InstanceIds)
    OutputPath: $
    Resource: arn:aws:states:::aws-sdk:ec2:stopInstances
    End: true

インスタンスを停止する処理です。インスタンス起動の記載とほぼ変わりませんが、一連の処理の終わりに必ずEnd: trueを指定する必要があります。

一通りStateMachineの内容を記載できたところで、ファイル名を 「stock_trader.asl.json」から「instance_sample.asl.yml」に変更して保存します。

SAMテンプレート

本項ではSAMテンプレートの修正に関して記載します。 具体的な内容部分についてはPart3で記載します。

SAMテンプレートはCloudFormationの内容を記載するイメージです。 先程編集したStateMachineの定義ファイルの場所や付与するIAMロールの内容を記載しています。 ※今回修正したStateMachineはLambdaを使用していないため、削除しています。Lambdaを使ったStateMachineは次回の投稿で記載します。

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
  sample-sam

  Sample SAM Template for sample-sam

Resources:
  SampleStateMachine:
    Name: sample
    Type: AWS::Serverless::StateMachine # More info about State Machine Resource: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-statemachine.html
    Properties:
      DefinitionUri: statemachine/instance_sample.asl.yml
      Role: !GetAtt StepFunctionsRole.Arn
  StepFunctionsRole:
      Type: "AWS::IAM::Role"
      Properties:
          Path: "/"
          RoleName: "sample-StepFunctionsRole"
          AssumeRolePolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Principal:
                  Service:
                    - states.amazonaws.com
                Action:
                  - sts:AssumeRole
          MaxSessionDuration: 3600
          ManagedPolicyArns: 
            - "arn:aws:iam::aws:policy/AmazonEC2FullAccess"
            - "arn:aws:iam::aws:policy/AmazonSSMFullAccess"
          Description: "Allows Step Functions to access AWS resources on your behalf."

内容としてはCloudFormationの記法で、作成したStateMachineを記載すれば良いのですが 今回SSMとEC2に対して実行可能なRoleをStateMachineに付与しなければなりません。

本来であれば、SAMテンプレートにRoleをCloudFormationで記載しなければならないのですが、面倒です。

そこで私はまずGUIでリソースを作成した後、Former2というサービスを使用してリソースをCloudFormationのコードとして出力させ、それをSAMテンプレートに記載する方式で作業しています。

Former2

Former2はAPI実行可能なIAMユーザーが必要です。 外部サイトにcredentialを残すのに抵抗のある方は、Docker版もあります。 そしてCloud9はDockerがすぐ使えるので、本記事ではDocker版で動かしてみたいと思います。

※fomer2の作業を始める前にIAMユーザーと出力させたいRoleを作成しておきます。 fomer2のユーザーはReadOnlyAccess権限でAPIアクセスを許可設定を行います。 StateMachine用のRoleはAmazonEC2FullAccessとAmazonSSMFullAccessを付与しておきます。

Fomer2起動

#ホームディレクトリに移動
$ cd ~
# pipでdocker-composeをインストール
$ pip install docker-compose
# リポジトリからgit clone
$ git clone https://github.com/iann0036/former2.git
# ディレクトリ移動
$ cd former2/
# docker-compose.ymlのポート番号変更 ※理由は後述
$ sed -i -e 's/80:80/8080:80/' docker-compose.yml
#起動
$ docker-compose up -d
#起動確認
$ docker ps

正常に起動していれば下記のような結果が出力されます。

Fomer2へアクセス

上記手順でEC2でFomer2が作動しているかと思います。 Cloud9ではコンテナで作動しているWEBアプリに接続することができるので、早速アクセスしてみます。 下記画像のように、Cloud9のメニューバーから「Preview」→「Preview Running Application」をクリックします。

正常にアクセスできれば下記のような初期セットアップの画面が表示されます。 後はウィザードに従いReadOnlyAccessで作成したcredentialを入力すれば使用可能です。

セットアップが完了後にダッシュボードの右上の「Scan Account」ボタンをクリックし、アカウント内部のリソースをスキャンします。

後はTOPのダッシュボードからIAMの画面へ遷移し、Roleタブから作成したStateMachine用のRoleにチェックを入れて「Add Selected」をクリックします。

最後に画面上部の「Generate」ボタンをクリックすると下記のようなCloudFormationの生成画面が出力されます。これがSAMテンプレートに貼り付ける素材となります。

デプロイ

SAMテンプレートの編集が終わればデプロイを行います。 下記コマンドを実行して、ビルド、デプロイを行います。

sam build -u
sam deploy --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND CAPABILITY_NAMED_IAM

実行確認

マネージドコンソールからデプロイしたStateMachineへ移動し、起動時のjsonパラメータを入力して起動させてみましょう。

※入力するjsonパラメータ

{
    "InstanceIds": "i-XXXXXXXXXXX"
}

うまくいけば下記のように全てのタスクが通るはずです。

以上でPart2が終了です。Step Functionsの動作とASLファイルに関して動きがつかめればと思います。