見出し画像

GitHub Actions → Step Functions → ECS Run Task するのにハマったポイント

はじめに

マイグレーションのようなワンショットのタスクを実行するのに、Step Functions は非常に便利なのですが、GitHub Actions から実行するのに若干ハマりポイントがあったので記事にしてみます。

Step Functions 使う必要あります?

というツッコミが当然あると思いますが、直接、ECS Run Task しようとすると、サブネットIDやセキュリティグループIDなど複数のパラメータが必要となります。
GitHub Actions のシークレットにそれらを登録したくないので、いい方法はないかと試行錯誤していました。
ECSサービスを設定して、タスク数 1 で起動して、ECS Exec してたこともあるのですが、色々面倒なので、最近では、Step Functions を使うようにしています。
尚、下記のブログにも下記の記述があり、共感しました。

ただし、RunTask 呼び出しの際に指定すべきパラメーターが数多くあるため、そこにもヒューマンエラーの余地が多くあります。 そこで、ECS の RunTask の呼び出しを Step Functions を介して行うようにします。

Step Functions を使って、ECS のワンショットタスクを実行する - Classi開発者ブログ

Step Functions から ECS Run Task を実行するのにハマったポイント

下記は、CloudFormation テンプレートの抜粋なのですが、ポイントが複数ありますので1つずつ解説していきます。

AWSTemplateFormatVersion: 2010-09-09

Resources:
  StateMachineMigrate:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      StateMachineName: ${STATE_MACHINE_NAME}
      RoleArn: ${STATE_MACHINE_ROLE_ARN}
      Definition:
        StartAt: Migrate
        States:
          Migrate:
            Type: Task
            Resource: arn:aws:states:::ecs:runTask.sync
            Parameters:
              Cluster: ${CLUSTER_ARN}
              TaskDefinition: ${TASKDEF_ARN}
              Overrides:
                ContainerOverrides:
                  - Name: "migrate"
                    "Command.$": "States.Array('ls','-la')"
              LaunchType: FARGATE
              NetworkConfiguration:
                AwsvpcConfiguration:
                  Subnets:
                    - ${SUBNETS}
                  SecurityGroups:
                    - ${SECURITY_GROUP}
            Next: PrintLogs
          PrintLogs:
            Type: Task
            Resource: arn:aws:states:::aws-sdk:cloudwatchlogs:filterLogEvents
            Parameters:
              LogGroupName: ${LOG_GROUP_NAME}
              LogStreamNamePrefix: ${LOG_GROUP_NAME_PREFIX}
              "StartTime.$": "$.StartedAt"
            End: true

Step Functions から arn:aws:states:::ecs:runTask.sync するには、StepFunctionsGetEventsForECSTaskRule が必要

下記の公式ドキュメントを参考に権限を追加します。

- Effect: Allow
  Action:
    - events:PutTargets
    - events:PutRule
    - events:DescribeRule
  Resource:
    - !Sub "arn:aws:events:${AWS::Region}:${AWS::AccountId}:rule/StepFunctionsGetEventsForECSTaskRule"

タスク定義にはリビジョンが必要

タスク定義を更新するたびにリビジョンは上がっていくわけですが、それを呼び出す Step Functions も更新が必要になります。
Parameters にて外部からリビジョンを更新するようにするなど工夫が必要です。

AWS CLI であれば、以下のようにして最新のリビジョンを取得できます。

aws ecs describe-task-definition \
	--task-definition $(taskdef) \
	--query 'taskDefinition.revision' \
	--no-cli-pager

これを利用して、MakefileStep Functions を更新できるようにしておきます。
--parameter-overrides TaskdefRevision="$(revision)" がポイントですね。

# Makefile

.PHONY: deploy-step-functions
deploy-step-functions: cache-taskdef-revision
	$(eval revision := $(shell cat $(cache_dir)/taskdef-revision.txt))
	aws --profile "$(profile)" cloudformation deploy \
		--template ./templates/step-functions.yml \
		--stack-name $(stack_name) \
		--parameter-overrides TaskdefRevision="$(revision)" \
		--capabilities CAPABILITY_NAMED_IAM \
		--no-fail-on-empty-changeset

cache-taskdef-revision:
	mkdir -p $(cache_dir)
	aws --profile "$(profile)" ecs describe-task-definition \
		--task-definition $(taskdef) \
		--query 'taskDefinition.revision' \
		--no-cli-pager | tr -d '"' > $(cache_dir)/taskdef-revision.txt

タスクの実行結果は CloudWatch Logs を参照するしかない

色々試したのですが、arn:aws:states:::ecs:runTask.sync する限りは、実行結果は、ログを確認する以外になさそうです。
なので、前述のとおり、arn:aws:states:::aws-sdk:cloudwatchlogs:filterLogEvents でタスク定義で指定しているロググループを検索・出力します。
その際、"StartTime.$": "$.StartedAt" と開始時刻を指定しておくと、直前の Step Functions が開始して以降のログが取得できます。

PrintLogs:
  Type: Task
  Resource: arn:aws:states:::aws-sdk:cloudwatchlogs:filterLogEvents
  Parameters:
    LogGroupName: ${LOG_GROUP_NAME}
    LogStreamNamePrefix: ${LOG_GROUP_NAME_PREFIX}
    "StartTime.$": "$.StartedAt"
  End: true

出力が256Kを超えると aws-sdk:cloudwatchlogs' returned a result with a size exceeding the maximum number of bytes service limit になる

これは、Step Functions の Payload サイズにデフォルトで 256K の上限が設けられているのが原因っぽいです。

FilterPattern を設定することで、ログのサイズを絞ると良いかと思います。

GitHub Actions から Step Functions を実行するのにハマったポイント

Step Functions 自体をデプロイしないといけない

アプリのデプロイのタイミングで、タスク定義が更新されるとすると、それを利用する Step Functions も更新しないといけません。
デプロイ自体は前述の make コマンドを使用して、以下のようにすると良いです。

- name: Deploy Step Functions
  shell: bash
  run: |
    make deploy-step-functions

aws stepfunctions start-execution は非同期なので、完了を待たないといけない / aws stepfunctions start-sync-execution は Express ワークフローのみ 

Step Functions を実行しようと、AWS CLI を使って、aws stepfunctions start-execution とやるわけですが、非同期なので実行結果を待たずに正常終了してしまいます。
start-execution が非同期だからといって、start-sync-execution を選択しようとしますが、標準ワークフローでは使えない、つまり、Express ワークフローのみ使用可能ということに気付きます。
標準ワークフロー と Express ワークフロー の違いについては、下記のリンクを参照ください。

Express ワークフロー は 5分以内という制約がありますが、これで十分な場合は、Express ワークフロー に変更してしまうのもいいかもしれません。
5分以上かかる可能性がある場合は、以下のように完了を待つようにすると良いです。

// .github/workflows/deploy.yml

- name: Execute Step Functions
  shell: bash
  run: |
    EXECUTION_ARN=$(aws stepfunctions start-execution \
      --state-machine-arn ${STATE_MACHINE} | jq '.executionArn
    echo $EXECUTION_ARN
    STATUS=RUNNING
    OUTPUT=""
    while [ "${STATUS}" == "RUNNING" ]
    do
      sleep 5
      OUTPUT=$(aws stepfunctions describe-execution \
        --execution-arn ${EXECUTION_ARN} \
        --no-cli-pager)
      STATUS=$(echo $OUTPUT | jq '.status' | tr -d '"')
    done
    echo $OUTPUT | jq --raw-output '.output' | jq '.Events[]

おわりに

ECS Run Task して結果を確認したいだけなのに、こんなにハマることあるの、という感じだったので、備忘録的に記事にしてみました。
参考になれば幸いです。
ではでは。