既存のECSタスク定義を参照するStepFunctionsをCDKで作るメモ

# 概要

- ECSタスクを起動するStepFunctionsのステートマシンを作りたい
    - ステートマシンは任意のコマンドをECSタスクに渡せるものとする
- CDKでステートマシンを作成する際に、ECSタスクを同時に作成するか既に作成しているかで実装コードが違う


# 作りたいもの

StepFunctionsのステートマシンからECSタスクを起動したい。
ここではCDKを使って構築する。

https://qiita.com/watta10/items/2504d839773289c1b22e


# ECSタスク定義もCDKで生成した場合

以下コードのECSタスク関連のプロパティは別途CDKで生成したリソースのインスタンスを設定しているとする。

ステートマシンのIAMロールとポリシーは自動で作られるのでコードを書く必要はない。

```ts
// コンテナの実行コマンド上書きに使う、taskDefinitionはリソースをCDKで作成済み
const containerDefinition = taskDefinition.findContainer('hoge')
if (!containerDefinition) {
  throw new Error(`Not Found container hoge`)
}

// ステートマシンのECSタスクを実行するステート
const state = new tasks.EcsRunTask(this, 'hogefuga-runFargate', {
  integrationPattern: sfn.IntegrationPattern.RUN_JOB,
  cluster,  // リソースをCDKで作成済み
  taskDefinition,  // リソースをCDKで作成済み
  securityGroups,  // リソースをCDKで作成済み
  containerOverrides: [  // 入力値によりコンテナの実行コマンドを切り替えている
    {
      containerDefinition,
      command: sfn.JsonPath.listAt('$.command')
    }
  ],
  launchTarget: new tasks.EcsFargateLaunchTarget()
})

const sf = new sfn.StateMachine(this, 'hogefuga-StateMachine', {
  definition: state
})
```

生成したステートマシンのIAMロールには以下のポリシーが付与される。
```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "ecs:RunTask",
            "Resource": "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/hogefuga",
            "Effect": "Allow"
        },
        {
            "Action": [
                "ecs:StopTask",
                "ecs:DescribeTasks"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": "iam:PassRole",
            "Resource": [
                "arn:aws:iam::XXXXXXXXXXXX:role/hogefuga-taskRole",
                "arn:aws:iam::XXXXXXXXXXXX:role/hogefuga-executionRole"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "events:PutTargets",
                "events:PutRule",
                "events:DescribeRule"
            ],
            "Resource": "arn:aws:events:ap-northeast-1:XXXXXXXXXXXX:rule/StepFunctionsGetEventsForECSTaskRule",
            "Effect": "Allow"
        }
    ]
}
```

## 余談

StateMachinePropsの `definition` はaws-cdkの `v2.85.0` からdeprecatedになった。
新しいプロパティ `definitionBody` により文字列とファイルでステートマシンの定義を設定できるようになった。
今までの `State` （上のコードの `new tasks.EcsRunTask()` ）を使う場合は  `definitionBody: sfn.DefinitionBody.fromChainable(state)` とすればそのまま使える。

https://github.com/aws/aws-cdk/releases/tag/v2.85.0

https://github.com/aws/aws-cdk/commit/1cb935172a2a373992167aebf0aaa72f02405d86#diff-abcc3c0806c124dedb27eaa38f38054bc82d896fc3bb2f7ae3fbacc6ae542205R94

また、文字列とファイルによる定義の場合は自動的にIAMロールにポリシーが付与されないらしい。

https://dev.classmethod.jp/articles/aws-step-functions-state-machines-can-now-be-created-from-asl-json-in-aws-cdk-v2850/


# ECSタスク定義はCDKで生成していない場合

以前から運用しているシステムではECSタスク定義をCDK管理していないケースがままある。
こういった場合はクラスターやタスク定義は既存のリソースから読み込む作業が必要になる。

## ITaskDefinitionが使用できない

タスク定義を既存のリソースから読み込むのに使用する `ecs.TaskDefinition.fromTaskDefinitionArn()` などの型は `ITaskDefinition` になる。

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs.TaskDefinition.html#static-fromwbrtaskwbrdefinitionwbrarnscope-id-taskdefinitionarn

今回のコードではこのインターフェースにないプロパティやメソッドを必要とするのでエラーになってしまう。
先ほどのコードのタスク定義のみ `fromTaskDefinitionArn()` を使ったものに変更したコードが以下のもの。

```ts
// taskDefinitionArnからタスク定義を読み込む
const iTaskDefinition = ecs.TaskDefinition.fromTaskDefinitionArn(this, 'hoge-taskDefinition-arn', `${taskDefinition.taskDefinitionArn}`)

// コンテナの実行コマンド上書きに使う
const containerDefinition = iTaskDefinition.findContainer('hoge')
if (!containerDefinition) {
  throw new Error(`Not Found container hoge`)
}

// ステートマシンのECSタスクを実行するステート
const ecsRunTask = new tasks.EcsRunTask(this, 'hogefuga-runFargate', {
  integrationPattern: sfn.IntegrationPattern.RUN_JOB,
  cluster: ecscluster,
  taskDefinition: iTaskDefinition,
  securityGroups,
  containerOverrides: [  // 入力値によりコンテナの実行コマンドを切り替えている
    {
      containerDefinition,
      command: sfn.JsonPath.listAt('$.command')
    }
  ],
  launchTarget: new tasks.EcsFargateLaunchTarget()
})

```

このコードをcdk diffすると、　`findContainer()` で以下のようなエラーが発生する。

```
Type 'ITaskDefinition' is missing the following properties from type 'TaskDefinition': family, containers, volumes, placementConstraints, and 32 more.
```

`findContainer()` をコメントアウトした場合でも、 `new tasks.EcsRunTask()` で同様のエラーになるのだが、こちらのドキュメントには以下のように書かれている。

```
Note: this must be TaskDefinition, and not ITaskDefinition, as it requires properties that are not known for imported task definitions If you want to run a RunTask with an imported task definition, consider using CustomState
```

つまり、既存のECSタスク定義を参照するStepFunctionsのステートマシンではCustomStateを使う必要がある。

大抵のConstruct Propsはインターフェースのプロパティとメソッドを使っているが、これはその例外らしい。

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_stepfunctions_tasks.EcsRunTask.html#taskdefinition


## CustomStateを使う

CustomStateを使うと `Amazon States Language (ASL)` を使った形式でステートマシンを記述できる。
要はAWSコンソールでステートマシンの定義にアクセスしたときに表示される形式で記述する。

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_stepfunctions.CustomState.html

https://docs.aws.amazon.com/step-functions/latest/dg/concepts-amazon-states-language.html

CustomStateを使ったコードは以下の通り。
クラスターでもタスク定義でもARNはインターフェースから取得できる、サブネットやセキュリティグループも同様。
```ts
const stateJson = {
  Type: 'Task',
  Resource: 'arn:aws:states:::ecs:runTask.sync',
  Parameters: {
    Cluster: 'arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:cluster/hogefuga-cluster',
    TaskDefinition: 'arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/hogefuga',
    NetworkConfiguration: {
      AwsvpcConfiguration: {
        Subnets: Subnets.map((r) => r.subnetId),
        SecurityGroups: SecurityGroups.map((r) => r.securityGroupId)
      }
    },
    Overrides: {
      ContainerOverrides: [
        {
          Name: 'hoge',
          'Command.$': '$.command'
        }
      ]
    },
    LaunchType: 'FARGATE'
  }
}

const state = new sfn.CustomState(this, 'hogefuga-runFargate', {
  stateJson,
  // role を設定しないとステートマシンのIAMロールにポリシーが付与されない
})
```

## 付与すべきIAMポリシーについて

CustomStateであっても必要なIAMポリシーはtasks.EcsRunTaskを使った場合のポリシー一覧と同じになる。

今回作成しているStandardのステートマシンではステートが終了するまで処理を待つことができ、そのためにはEventBridgeを使用しているらしい。
そしてその為には以下の部分が必要になる。

```json
{
    "Action": [
        "events:PutTargets",
        "events:PutRule",
        "events:DescribeRule"
    ],
    "Resource": "arn:aws:events:ap-northeast-1:XXXXXXXXXXXX:rule/StepFunctionsGetEventsForECSTaskRule",
    "Effect": "Allow"
}
```

https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/service-integration-iam-templates.html#connect-iam-dynamic-static

`rule/StepFunctionsGetEventsForECSTaskRule` の部分はターゲットのリソースによって異なる。
例えば `AWS Batch` をターゲットにする場合は `rule/StepFunctionsGetEventsForBatchJobsRule` にする必要がある。

どのポリシーを設定すべきかは、上のドキュメントの `IAM ロールの作成に使用されるポリシーテンプレート` の `トピック` のリンク先で確認できる。

ここの設定を間違えると以下のようなエラーが発生することがある。

```
Resource handler returned message: "'arn:aws:iam::XXXXXXXXXX:role/hogefuga-role' is not a
uthorized to create managed-rule.
(Service: AWSStepFunctions; Status Code: 400; Error Code: AccessDeniedException; 
Request ID: XXXXXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX; 
Proxy: null)" 
(RequestToken: XXXXXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX, HandlerErrorCode: AccessDenied)
```

# まとめ

StepFunctionsでECSタスクを呼び出す場合、インターフェースが使えないのでターゲットのECSタスク定義のCDK管理状態によって対応する方法が大きく異なってしまう。