はじめに
前回まではStep Functionsの基礎と事前準備を行いましたが、今回から実際に作成したStep Functionsの説明をしながら進めていこうと思います。
Step Functionsのフロー概要
今回作成したStep Functionsのフロー概要は以下となります。
今回の処理では起動時はRDSを先に起動させてからEC2、ECSを起動するようにしており、停止時はまとめて停止するようにしております。
これは、2層、3層アーキテクチャ構成の場合、先にRDSを起動しておかないとフロント側サービスが起動できないようにしていたりするシステムが多いかと思うのと、RDSの起動に時間がかかることから、先にEC2等のフロントサービスを立ち上げてもサービスを利用できない時間が長くなるため、RDSの起動が完了してからフロント側サービスを起動するようなフローにしました。
停止については特に停止順を気にする必要は無いかと思いますのでまとめて停止するようにしております。
とはいえ、システムにより、起動順、停止順が決められている場合もあるかと思うので、もし本Step Functionsを使用する場合は、システム傾向に合わせて修正して使用してください。
Step Functionsの説明
以下より、実際のフローについてStep Functionsの書式を説明しながら進めていきます。
Step FunctionsのJSONについては次回説明する起動時の処理部分も含めてまとめて以下に記載します。
停止起動Step Functions(展開してください)
{
"Comment": "A description of my state machine",
"StartAt": "カレンダー確認",
"States": {
"カレンダー確認": {
"Type": "Task",
"Arguments": {
"CalendarNames": [
"arn:aws:ssm:ap-northeast-1:123456789012:document/event-calendar"
]
},
"Resource": "arn:aws:states:::aws-sdk:ssm:getCalendarState",
"Next": "カレンダー状態判定"
},
"カレンダー状態判定": {
"Type": "Choice",
"Choices": [
{
"Next": "RDS存在確認(起動)",
"Condition": "{% ($states.input.State) = (\"OPEN\") %}"
},
{
"Next": "並行停止実行",
"Condition": "{% ($states.input.State) = (\"CLOSED\") %}"
}
]
},
"RDS存在確認(起動)": {
"Type": "Task",
"Arguments": {},
"Resource": "arn:aws:states:::aws-sdk:rds:describeDBClusters",
"Output": "{% $states.result.DbClusters[] ? $states.result.DbClusters[].DbClusterArn : null %}",
"Next": "RDS存在判定(起動)"
},
"RDS存在判定(起動)": {
"Type": "Choice",
"Choices": [
{
"Next": "並行起動実行",
"Condition": "{% $type($states.input) = \"null\" %}"
}
],
"Default": "RDS起動処理ループ"
},
"RDS起動処理ループ": {
"Type": "Map",
"ItemProcessor": {
"ProcessorConfig": {
"Mode": "INLINE"
},
"StartAt": "RDS起動対象タグ確認",
"States": {
"RDS起動対象タグ確認": {
"Type": "Task",
"Arguments": {
"ResourceName": "{% $states.input %}"
},
"Resource": "arn:aws:states:::aws-sdk:rds:listTagsForResource",
"Output": {
"StartStopSFNTag": "{% $states.result.(TagList[Key=\"StartStopSFN\"] ? TagList[Key=\"StartStopSFN\"].Value : null) %}"
},
"Next": "RDS起動対象タグ判定",
"Assign": {
"rdsCluster": "{% $states.input %}"
}
},
"RDS起動対象タグ判定": {
"Type": "Choice",
"Choices": [
{
"Next": "RDS起動スキップ",
"Condition": "{% ($states.input.StartStopSFNTag) = (\"disable\") %}"
}
],
"Default": "RDS起動前状態確認"
},
"RDS起動前状態確認": {
"Type": "Task",
"Arguments": {
"DbClusterIdentifier": "{% $rdsCluster %}"
},
"Resource": "arn:aws:states:::aws-sdk:rds:describeDBClusters",
"Next": "RDS起動前判定",
"Assign": {
"rdsStartCheck": "{% $single($states.result.DbClusters[].Status) %}"
}
},
"RDS起動前判定": {
"Type": "Choice",
"Choices": [
{
"Next": "RDS起動スキップ",
"Condition": "{% (($rdsStartCheck) = (\"available\")) %}"
}
],
"Default": "RDS起動"
},
"RDS起動": {
"Type": "Task",
"Arguments": {
"DbClusterIdentifier": "{% $rdsCluster %}"
},
"Resource": "arn:aws:states:::aws-sdk:rds:startDBCluster",
"End": true,
"Output": {
"DbClusterIdentifier": "{% $states.result.DbCluster.DbClusterIdentifier %}",
"Status": "{% $states.result.DbCluster.Status %}"
}
},
"RDS起動スキップ": {
"Type": "Succeed"
}
}
},
"Next": "RDS起動対象一覧確認"
},
"RDS起動対象一覧確認": {
"Type": "Task",
"Arguments": {},
"Resource": "arn:aws:states:::aws-sdk:rds:describeDBClusters",
"Next": "RDS起動対象一覧判定",
"Output": "{% $states.result.DbClusters[(TagList[Key=\"StartStopSFN\" and Value!=\"disable\"]) or ($exists(TagList[Key=\"StartStopSFN\"]) = false)] ? $states.result.DbClusters[(TagList[Key=\"StartStopSFN\" and Value!=\"disable\"]) or ($exists(TagList[Key=\"StartStopSFN\"]) = false)].DbClusterArn[] : null %}"
},
"RDS起動対象一覧判定": {
"Type": "Choice",
"Choices": [
{
"Next": "並行起動実行",
"Condition": "{% $type($states.input) = \"null\" %}"
}
],
"Default": "RDS起動判定処理ループ"
},
"RDS起動判定処理ループ": {
"Type": "Map",
"ItemProcessor": {
"ProcessorConfig": {
"Mode": "INLINE"
},
"StartAt": "RDS変数設定",
"States": {
"RDS変数設定": {
"Type": "Pass",
"Next": "RDS起動状態確認",
"Assign": {
"rdsCluster": "{% $states.input %}"
}
},
"RDS起動状態確認": {
"Type": "Task",
"Arguments": {
"DbClusterIdentifier": "{% $rdsCluster %}"
},
"Resource": "arn:aws:states:::aws-sdk:rds:describeDBClusters",
"Next": "RDS起動判定",
"Assign": {
"rdsStatusCheck": "{% $single($states.result.DbClusters[].Status) %}"
}
},
"RDS起動判定": {
"Type": "Choice",
"Choices": [
{
"Next": "RDS起動待機",
"Condition": "{% $not(($rdsStatusCheck) = (\"available\")) %}"
}
],
"Default": "RDS起動完了"
},
"RDS起動待機": {
"Type": "Wait",
"Seconds": 600,
"Next": "RDS起動状態確認"
},
"RDS起動完了": {
"Type": "Succeed"
}
}
},
"Next": "並行起動実行"
},
"並行起動実行": {
"Type": "Parallel",
"Branches": [
{
"StartAt": "ECS存在確認(起動)",
"States": {
"ECS存在確認(起動)": {
"Type": "Task",
"Arguments": {},
"Resource": "arn:aws:states:::aws-sdk:ecs:listClusters",
"Output": "{% $states.result.ClusterArns ? $states.result.ClusterArns : null %}",
"Next": "ECS存在判定(起動)"
},
"ECS存在判定(起動)": {
"Type": "Choice",
"Choices": [
{
"Next": "ECS起動完了",
"Condition": "{% $type($states.input) = \"null\" %}"
}
],
"Default": "ECSクラスタ起動処理ループ"
},
"ECS起動完了": {
"Type": "Succeed"
},
"ECSクラスタ起動処理ループ": {
"Type": "Map",
"ItemProcessor": {
"ProcessorConfig": {
"Mode": "INLINE"
},
"StartAt": "ECS起動対象タグ確認",
"States": {
"ECS起動対象タグ確認": {
"Type": "Task",
"Arguments": {
"ResourceArn": "{% $states.input %}"
},
"Resource": "arn:aws:states:::aws-sdk:ecs:listTagsForResource",
"Next": "ECS起動対象タグ判定",
"Output": {
"StartStopSFNTag": "{% $states.result.(Tags[Key=\"StartStopSFN\"] ? Tags[Key=\"StartStopSFN\"].Value : null) %}"
},
"Assign": {
"ecsCluster": "{% $states.input %}"
}
},
"ECS起動対象タグ判定": {
"Type": "Choice",
"Choices": [
{
"Condition": "{% ($states.input.StartStopSFNTag) = (\"disable\") %}",
"Next": "ECS起動スキップ"
}
],
"Default": "ECSサービス起動対象一覧確認"
},
"ECS起動スキップ": {
"Type": "Succeed"
},
"ECSサービス起動対象一覧確認": {
"Type": "Task",
"Arguments": {
"Cluster": "{% $ecsCluster %}"
},
"Resource": "arn:aws:states:::aws-sdk:ecs:listServices",
"Next": "ECSサービス起動処理ループ",
"Output": "{% $states.result.ServiceArns %}"
},
"ECSサービス起動処理ループ": {
"Type": "Map",
"ItemProcessor": {
"ProcessorConfig": {
"Mode": "INLINE"
},
"StartAt": "ECS起動",
"States": {
"ECS起動": {
"Type": "Task",
"Arguments": {
"Cluster": "{% $ecsCluster %}",
"Service": "{% $states.input %}",
"DesiredCount": 1
},
"Resource": "arn:aws:states:::aws-sdk:ecs:updateService",
"End": true,
"Output": {
"ClusterArn": "{% $states.result.Service.ClusterArn %}",
"DesiredCount": "{% $states.result.Service.DesiredCount %}",
"ServiceName": "{% $states.result.Service.ServiceName %}",
"Status": "{% $states.result.Service.Status %}"
}
}
}
},
"End": true
}
}
},
"End": true
}
}
},
{
"StartAt": "EC2存在確認(起動)",
"States": {
"EC2存在確認(起動)": {
"Type": "Task",
"Arguments": {},
"Resource": "arn:aws:states:::aws-sdk:ec2:describeInstances",
"Output": "{% $states.result.Reservations[] ? $states.result.Reservations[].Instances[].InstanceId : null %}",
"Next": "EC2存在判定(起動)"
},
"EC2存在判定(起動)": {
"Type": "Choice",
"Choices": [
{
"Next": "EC2起動完了",
"Condition": "{% $type($states.input) = \"null\" %}"
}
],
"Default": "EC2起動処理ループ"
},
"EC2起動完了": {
"Type": "Succeed"
},
"EC2起動処理ループ": {
"Type": "Map",
"ItemProcessor": {
"ProcessorConfig": {
"Mode": "INLINE"
},
"StartAt": "EC2起動対象タグ確認",
"States": {
"EC2起動対象タグ確認": {
"Type": "Task",
"Resource": "arn:aws:states:::aws-sdk:ec2:describeInstances",
"Arguments": {
"InstanceIds": [
"{% $states.input %}"
]
},
"Next": "EC2起動対象タグ判定",
"Output": {
"StartStopSFNTag": "{% $single($states.result.Reservations[].Instances[].(Tags[Key=\"StartStopSFN\"] ? Tags[Key=\"StartStopSFN\"].Value[] : null)) %}"
},
"Assign": {
"ec2Instance": "{% $states.input %}"
}
},
"EC2起動対象タグ判定": {
"Type": "Choice",
"Choices": [
{
"Condition": "{% ($states.input.StartStopSFNTag) = (\"disable\") %}",
"Next": "EC2起動スキップ"
}
],
"Default": "EC2起動"
},
"EC2起動": {
"Type": "Task",
"Arguments": {
"InstanceIds": [
"{% $ec2Instance %}"
]
},
"Resource": "arn:aws:states:::aws-sdk:ec2:startInstances",
"End": true,
"Output": {
"InstanceId": "{% $states.result.StartingInstances.InstanceId %}",
"CurrentState": "{% $states.result.StartingInstances.CurrentState.Name %}",
"PreviousState": "{% $states.result.StartingInstances.PreviousState.Name %}"
}
},
"EC2起動スキップ": {
"Type": "Succeed"
}
}
},
"End": true
}
}
}
],
"End": true
},
"並行停止実行": {
"Type": "Parallel",
"Branches": [
{
"StartAt": "ECS存在確認(停止)",
"States": {
"ECS存在確認(停止)": {
"Type": "Task",
"Arguments": {},
"Resource": "arn:aws:states:::aws-sdk:ecs:listClusters",
"Next": "ECS存在判定(停止)",
"Output": "{% $states.result.ClusterArns ? $states.result.ClusterArns : null %}"
},
"ECS存在判定(停止)": {
"Type": "Choice",
"Choices": [
{
"Next": "ECS停止終了",
"Condition": "{% $type($states.input) = \"null\" %}"
}
],
"Default": "ECSクラスタ停止処理ループ"
},
"ECS停止終了": {
"Type": "Succeed"
},
"ECSクラスタ停止処理ループ": {
"Type": "Map",
"ItemProcessor": {
"ProcessorConfig": {
"Mode": "INLINE"
},
"StartAt": "ECS停止対象タグ確認",
"States": {
"ECS停止対象タグ確認": {
"Type": "Task",
"Arguments": {
"ResourceArn": "{% $states.input %}"
},
"Resource": "arn:aws:states:::aws-sdk:ecs:listTagsForResource",
"Next": "ECS停止対象タグ判定",
"Output": {
"StartStopSFNTag": "{% $states.result.(Tags[Key=\"StartStopSFN\"] ? Tags[Key=\"StartStopSFN\"].Value : null) %}"
},
"Assign": {
"ecsCluster": "{% $states.input %}"
}
},
"ECS停止対象タグ判定": {
"Type": "Choice",
"Choices": [
{
"Condition": "{% ($states.input.StartStopSFNTag) = (\"disable\") %}",
"Next": "ECS停止スキップ"
}
],
"Default": "ECSサービス停止対象一覧確認"
},
"ECS停止スキップ": {
"Type": "Succeed"
},
"ECSサービス停止対象一覧確認": {
"Type": "Task",
"Arguments": {
"Cluster": "{% $ecsCluster %}"
},
"Resource": "arn:aws:states:::aws-sdk:ecs:listServices",
"Next": "ECSサービス停止処理ループ",
"Output": "{% $states.result.ServiceArns %}"
},
"ECSサービス停止処理ループ": {
"Type": "Map",
"ItemProcessor": {
"ProcessorConfig": {
"Mode": "INLINE"
},
"StartAt": "ECSサービス停止",
"States": {
"ECSサービス停止": {
"Type": "Task",
"Arguments": {
"Cluster": "{% $ecsCluster %}",
"Service": "{% $states.input %}",
"DesiredCount": 0
},
"Resource": "arn:aws:states:::aws-sdk:ecs:updateService",
"End": true,
"Output": {
"ClusterArn": "{% $states.result.Service.ClusterArn %}",
"DesiredCount": "{% $states.result.Service.DesiredCount %}",
"ServiceName": "{% $states.result.Service.ServiceName %}",
"Status": "{% $states.result.Service.Status %}"
}
}
}
},
"End": true
}
}
},
"End": true
}
}
},
{
"StartAt": "EC2存在確認(停止)",
"States": {
"EC2存在確認(停止)": {
"Type": "Task",
"Arguments": {},
"Resource": "arn:aws:states:::aws-sdk:ec2:describeInstances",
"Output": "{% $states.result.Reservations[] ? $states.result.Reservations[].Instances[].InstanceId : null %}",
"Next": "EC2存在判定(停止)"
},
"EC2存在判定(停止)": {
"Type": "Choice",
"Choices": [
{
"Next": "EC2停止完了",
"Condition": "{% $type($states.input) = \"null\" %}"
}
],
"Default": "EC2停止処理ループ"
},
"EC2停止完了": {
"Type": "Succeed"
},
"EC2停止処理ループ": {
"Type": "Map",
"ItemProcessor": {
"ProcessorConfig": {
"Mode": "INLINE"
},
"StartAt": "EC2停止対象タグ確認",
"States": {
"EC2停止対象タグ確認": {
"Type": "Task",
"Resource": "arn:aws:states:::aws-sdk:ec2:describeInstances",
"Arguments": {
"InstanceIds": [
"{% $states.input %}"
]
},
"Next": "EC2停止対象タグ判定",
"Output": {
"StartStopSFNTag": "{% $single($states.result.Reservations[].Instances[].(Tags[Key=\"StartStopSFN\"] ? Tags[Key=\"StartStopSFN\"].Value[] : null)) %}"
},
"Assign": {
"ec2Instance": "{% $states.input %}"
}
},
"EC2停止対象タグ判定": {
"Type": "Choice",
"Choices": [
{
"Next": "EC2停止スキップ",
"Condition": "{% ($states.input.StartStopSFNTag) = (\"disable\") %}"
}
],
"Default": "EC2停止"
},
"EC2停止": {
"Type": "Task",
"Arguments": {
"InstanceIds": [
"{% $ec2Instance %}"
]
},
"Resource": "arn:aws:states:::aws-sdk:ec2:stopInstances",
"End": true,
"Output": {
"InstanceId": "{% $states.result.StoppingInstances.InstanceId %}",
"CurrentState": "{% $states.result.StoppingInstances.CurrentState.Name %}",
"PreviousState": "{% $states.result.StoppingInstances.PreviousState.Name %}"
}
},
"EC2停止スキップ": {
"Type": "Succeed"
}
}
},
"End": true
}
}
},
{
"StartAt": "RDS存在確認(停止)",
"States": {
"RDS存在確認(停止)": {
"Type": "Task",
"Arguments": {},
"Resource": "arn:aws:states:::aws-sdk:rds:describeDBClusters",
"Output": "{% $states.result.DbClusters[] ? $states.result.DbClusters[].DbClusterArn : null %}",
"Next": "RDS存在判定(停止)"
},
"RDS存在判定(停止)": {
"Type": "Choice",
"Choices": [
{
"Next": "RDS停止完了",
"Condition": "{% $type($states.input) = \"null\" %}"
}
],
"Default": "RDS停止処理ループ"
},
"RDS停止処理ループ": {
"Type": "Map",
"ItemProcessor": {
"ProcessorConfig": {
"Mode": "INLINE"
},
"StartAt": "RDS停止対象タグ確認",
"States": {
"RDS停止対象タグ確認": {
"Type": "Task",
"Arguments": {
"ResourceName": "{% $states.input %}"
},
"Resource": "arn:aws:states:::aws-sdk:rds:listTagsForResource",
"Output": {
"StartStopSFNTag": "{% $states.result.(TagList[Key=\"StartStopSFN\"] ? TagList[Key=\"StartStopSFN\"].Value : null) %}"
},
"Next": "RDS停止対象タグ判定",
"Assign": {
"rdsCluster": "{% $states.input %}"
}
},
"RDS停止対象タグ判定": {
"Type": "Choice",
"Choices": [
{
"Next": "RDS停止スキップ",
"Condition": "{% ($states.input.StartStopSFNTag) = (\"disable\") %}"
}
],
"Default": "RDS停止前状態確認"
},
"RDS停止前状態確認": {
"Type": "Task",
"Arguments": {
"DbClusterIdentifier": "{% $rdsCluster %}"
},
"Resource": "arn:aws:states:::aws-sdk:rds:describeDBClusters",
"Next": "RDS停止前判定",
"Assign": {
"rdsStopCheck": "{% $single($states.result.DbClusters[].Status) %}"
}
},
"RDS停止前判定": {
"Type": "Choice",
"Choices": [
{
"Next": "RDS停止スキップ",
"Condition": "{% (($rdsStopCheck) = (\"stopped\")) %}"
}
],
"Default": "RDS停止"
},
"RDS停止": {
"Type": "Task",
"Arguments": {
"DbClusterIdentifier": "{% $rdsCluster %}"
},
"Resource": "arn:aws:states:::aws-sdk:rds:stopDBCluster",
"End": true,
"Output": {
"DbClusterIdentifier": "{% $states.result.DbCluster.DbClusterIdentifier %}",
"Status": "{% $states.result.DbCluster.Status %}"
}
},
"RDS停止スキップ": {
"Type": "Succeed"
}
}
},
"End": true
},
"RDS停止完了": {
"Type": "Succeed"
}
}
}
],
"End": true
}
},
"QueryLanguage": "JSONata"
}
カレンダーの状態判定
Change Calendarの状態遷移の通知をEventBridgeで受けて、Step Functionsを実行しますが、今回はChange Calendarの状態遷移の情報をStep Functionsに渡していないため、ここで現在のカレンダーの状態を取得します。
やろうと思えばEventBridgeで受け取ったChange Calendarからの値を引き渡すことも可能ですが、EventBridgeで引き渡した場合、Step Functionsを単体実行するときにChange Calendarの状態の入力が必要となって面倒なので、最初のステップでStep Functions側からカレンダーの状態を確認しに行くようにしています。
カレンダーの状態はOPEN、CLOSEDの2つとなるため、次の「Choice」フローを使用して条件分岐させてそれぞれの処理を実行させます。
条件式は「Rule」の鉛筆アイコンを選択して、「条件を追加」をすると条件式を入力できる画面になるため、「ベーシック」で条件式を選択、入力するか、「アドバンス」で直接JSONataの書式を記載することで作成できます。
以下はベーシックの場合の例。
「Choise」では1つ前の「GetCalendarState」アクションの結果からリソースの起動フロー、リソースの停止フローに分岐させるため、2つのルールを作成します。
なお、「GetCalendarState」を実行すると以下のような結果が出力されるため、この中からStateを条件として条件式を作成します。
{
"AtTime": "2025-08-27T23:22:22Z",
"NextTransitionTime": "2025-08-28T15:00:00Z",
"State": "OPEN"
}
1つ前の結果は$states.inputで取得でき、今回はStateの結果を条件としたいため、以下のようにルールを設定します。
以下は「アドバンスト」で直接設定した例ですが、「ベーシック」で設定する場合は、上図のように設定すれば同じ設定となります。
{% ($states.input.State) = ("OPEN") %}
{% ($states.input.State) = ("CLOSED") %}
停止時の処理
前述の通り、停止時にはEC2、ECS、RDSそれぞれ並行して停止を行います。
並行して実行するため、「フロー」の「Parallel」を選択して、「Parallel」の中にEC2、ECS、RDSの停止処理を記載していきます。
以下より各サービスごとのフローを以下より説明していきます。
EC2の停止処理
EC2を停止する際のフローの内容は以下となります。
EC2の停止をしようにもそもそも対象のアカウントにEC2が存在しない場合、エラーとなってしまうため、最初の「EC2存在確認(停止)」で、「DescribeInstances」を実行し、EC2が存在しているかを確認しています。
次の「EC2存在判定(停止)」で存在有無を判定させるため、「EC2存在確認(停止)」の「出力」設定で以下のような三項演算子のJSONata式を記載し、出力させるようにします。
{% $states.result.Reservations[] ? $states.result.Reservations[].Instances[].InstanceId : null %}
三項演算子とは、他の言語で言うとif...elseと同じように条件判定で使用する演算子となり、JSONataではif...elseの構文が無いため、条件判定を使用する場合には代わりに使用します。
JSONataでは三項演算子を?と:で示すことから、以下のような書式となります。
[条件式] ? [trueの場合の値] : [falseの場合の値]
先程の存在有無の式に当てはめると「$states.result.Reservations[]が存在する場合は$states.result.Reservations[].Instances[].InstanceIdでEC2の一覧の値を応答し、存在しない場合は、nullという値を応答する」条件式となります。
「EC2存在判定(停止)」では$states.inputの型がnullの場合は処理完了、それ以外の場合は停止処理を実行するように判定します。
{% $type($states.input) = "null" %}
停止処理を実行する場合は先程の三項演算子の処理で取得したEC2の一覧を元に、Mapフローによるループ処理が行われます。
Mapフローによるループ処理
他の言語で言うforやwhileといったループ処理を行う場合、Step Functionsでは「Map」フローを使用することでループ処理を行うことができます。
基本的にはリスト型で取得した一覧情報を「Map」フローに渡して、各対象ごとに処理を繰り返し実行する際に使用します。
今回は前のステップで取得したEC2の一覧を「Map」フローに入力して各インスタンスごとに停止処理を行うようにしております。
リスト型で「Map」フローに渡さないと失敗するため、最初のうちは使いこなすのに苦労するかもしれませんが、使えるようになればStep Functionsを作成するうえで強力なツールとなるためしっかり使いこなせるようにしてください。
EC2停止対象の判定
EC2の停止を行う場合、「アクション」から「EC2: StopInstances」アクションを使用して引数に停止する対象となるEC2のインスタンスIDを指定することで停止することができます。
停止対象となるEC2のインスタンスIDはMapでループする際に1つずつ入力されるため、インスタンスIDの指定で、MapでループしているインスタンスIDを指定してStopInstancesを実行することで停止することができます。
しかし、まとめて停止起動処理を行うと言ってもテスト等で常時起動しておきたいリソースも存在するかと思うので、人手間加えて、今回はそれぞれのリソースにStartStopSFN = disableのタグが付与されていた場合は停止起動処理の対象から除外する処理を行っています。
| 対象リソース | タグのキー | タグの値 |
|---|---|---|
| EC2インスタンス | StartStopSFN | disable |
| ECSクラスタ | StartStopSFN | disable |
| RDSクラスタ | StartStopSFN | disable |
EC2に付与されているタグは「EC2: DescribeInstances」アクションの結果に含まれているため、「引数」欄に以下のように記載することで、MapでループしているインスタンスIDを指定して、結果を出力します。
{
"InstanceIds": [
"{% $states.input %}"
]
}
ついでに後続のステップでも、MapでループしているEC2のインスタンスIDを使用したいため、「変数」欄に以下のように記載して変数に格納しておきます。
{
"ec2Instance": "{% $states.input %}"
}
指定タグの有無は「出力」欄に以下のようにJSONataで判定した結果をStartStopSFNTag変数に格納して次のChoiseフローで判定する際に使用します。
{
"StartStopSFNTag": "{% $single($states.result.Reservations[].Instances[].(Tags[Key=\"StartStopSFN\"] ? Tags[Key=\"StartStopSFN\"].Value[] : null)) %}"
}
式は先程より複雑ですが、三項演算子でTags[Key=\"StartStopSFN\"]とすることでStartStopSFNタグの有無を判定し、タグが付与されていればTags[Key=\"StartStopSFN\"].Value[]でタグの値をStartStopSFNTagという名前の変数に格納し、タグが付与されていなかった場合はnullを格納しております。
また、今回の結果はリスト型で出力されてしまいますが、[]のようなリスト型だと次の判定のときに都合が悪いので、$single関数を使用して、リストで出力されないようにしております。
コードだと少し分かりづらいのでフロー図にしたものが以下となります。
上記のタグの有無確認の出力結果を元に、「EC2起動対象タグ判定」(Choiseフロー)で以下のようにStartStopSFN変数の値がdisableの場合は処理をスキップ、それ以外はデフォルトルールで受けてEC2を停止させるステップに移動します。
{% ($states.input.StartStopSFNTag) = ("disable") %}
EC2の停止
停止は前述の通り「EC2: StopInstances」で行うため、「引数」欄に以下のように指定します。
InstanceIdsの指定は、先ほど変数に格納したec2Instance変数を指定するようにします。
{
"InstanceIds": [
"{% $ec2Instance %}"
]
}
また、出力結果は後段で特に使用しないため何もしなくても良いですが、後述の「各アクションの入出力結果の容量制限」に引っかかる場合があるため、Step Functions実行ログに残しておきたい情報だけ「出力」欄に記載しています。
{
"InstanceId": "{% $states.result.StoppingInstances.InstanceId %}",
"CurrentState": "{% $states.result.StoppingInstances.CurrentState.Name %}",
"PreviousState": "{% $states.result.StoppingInstances.PreviousState.Name %}"
}
RDSの停止処理
RDSを停止する際のフローの内容は以下となります。
処理の全体の流れはEC2の場合と同じため、要点だけ説明していきます。
フローの内容としては先程のEC2のフローとほぼ同じですが、RDSでは元々停止しているRDSに対して停止処理を行うと失敗するためループ内で行っている停止処理を実行する前に、状態判定を行っています。
EC2の処理と同じく、最初に停止する対象が存在するかの確認と、対象のリストの取得を以下のような三項演算子で行います。
{% $states.result.DbClusters[] ? $states.result.DbClusters[].DbClusterArn : null %}
ループ内のタグ判定はEC2の場合と同じですが、タグを取得するために実行している「RDS: ListTagsForResource」アクションはリスト型では出力しないため、今回は$single関数を使用しません。
{
"StartStopSFNTag": "{% $states.result.(TagList[Key=\"StartStopSFN\"] ? TagList[Key=\"StartStopSFN\"].Value : null) %}"
}
タグ判定後の停止処理は、前述の通り、RDSの場合は停止しているクラスタに対して停止を行おうとした場合、エラーで止まってしまうため、「RDS: DescribeDBClusters」アクションの「変数」でRDSの現在の状態をrdsStopCheck変数に格納するようにしております。
{
"rdsStopCheck": "{% $single($states.result.DbClusters[].Status) %}"
}
停止後の結果出力はEC2の場合と同じく、以下のように実行ログに記録しておきたい情報のみ取得して格納しております。
{
"DbClusterIdentifier": "{% $states.result.DbCluster.DbClusterIdentifier %}",
"Status": "{% $states.result.DbCluster.Status %}"
}
ECSの停止処理
ECSを停止する際のフローの内容は以下となります。
処理の全体の流れはEC2、RDSの場合と同じため、要点だけ説明していきます。
ECSのフローもEC2、RDSと流れは同じですが、ECSの場合、ECSクラスタ、ECSサービス、ECSタスクとECSを構成する要素が複数あるため、停止時の処理が若干増えています。
EC2、RDSの処理と同じく停止する対象が存在するかを確認するため、以下のような三項演算子で行います。
{% $states.result.ClusterArns ? $states.result.ClusterArns : null %}
ループ内のタグ判定はEC2、RDSと同じですが、ECSはECSクラスタの要素の中にECSサービスも含まれることから、ECSクラスタ内に存在するECSサービス一覧を取得するため、以下のように「ECS: ListServices」で取得した結果を以下のように出力し、ECSサービス停止のループ処理に引き渡しています。
{% $states.result.ServiceArns %}
ECSサービス停止のループ内では、「ECS: UpdateService」を使用して、ECSサービス内で動作しているECSタスク数を0にすることで停止させるようにしています。
{
"Cluster": "{% $ecsCluster %}",
"Service": "{% $states.input %}",
"DesiredCount": 0
}
なお、オートスケール設定を行っているECSサービスの場合、オートスケールの最小値を0にしておかないと停止する際に0へ変更できず失敗するため注意してください。
また、停止後の結果出力は、今回は以下を指定しました。
{
"ClusterArn": "{% $states.result.Service.ClusterArn %}",
"DesiredCount": "{% $states.result.Service.DesiredCount %}",
"ServiceName": "{% $states.result.Service.ServiceName %}",
"Status": "{% $states.result.Service.Status %}"
}
各アクションの入出力結果の容量制限
各アクションを実行すると、何かしらの実行結果が出力されますが、後段のステップで結果を使用しないようであれば「出力」欄に何も記載しなければ、実行結果がそのままログに記録されるだけで次に進みます。
しかし、Step Functionsで扱える1ステップあたりの入力・出力結果には制限があり、256KiBの容量までしか扱うことができず、256KiBを超えるとエラーとなってしまいます。
そのため、例えばインスタンスの設定一覧を取得するアクションを実行した際、少ない台数しか存在しなかった場合は問題がなくても、台数が増えた場合、Step Functionsの実行に失敗してしまうといったことが発生してしまいます。
容量オーバーで失敗して止まってしまうようなことを防ぐため、後段のステップで結果を使用する用途が無い場合でも、{}のように結果自体を出力しない、もしくは後からログを見るときに欲しい情報のみ出力するようにして、極力不要となる情報は削減する癖をつけましょう。
おわりに
今回はStep Functionsによる停止処理側の説明を行いました。
今回の処理としては一気にまとめて停止するようフローを作成していますが、システムによっては停止順がある場合もあるかと思いますので、もし本Step Functionsを使用するのであれば、システムによって調整してもらえればと思います。
次回は起動処理を説明しようと思います。



