AWS Copilot JobsでECSタスクの定期実行を行う

7 min read読了の目安(約7000字

この記事でやること

先月、AWS CopilotがGAになりましたね。
このAWS Copilotですが、GAになる一ヶ月ほど前(v0.5)に、ECSタスクの定期実行を行う「Jobs」を構築できるようになりました。
以前、CopilotでAPIサーバーを作成したのですが、定期実行するバッチ処理が欲しくなったためCopilot Jobsを利用して処理を追加していきたいと思います。

AWS Copilotとは

AWSでコンテナ化されたアプリケーションの開発、リリースを容易に行うためのコマンドラインツールです。
コマンドを叩くとCloudFormationが動き、必要なリソースの作成やデプロイを行うことができます。
CI/CDパイプラインもコマンド一つで作成できます。

Copilotを支える概念

Service

ECS上で動くコンテナアプリケーションのことです。
主にECSのサービスやタスク定義と関連があります。

Environment

本番環境やステージング環境といったステージのことです。
Environmentが異なるとVPCレベルで異なります。
主に以下のリソースと関連があります。

  • VPC, Subnet
  • ECS Cluster
  • ALB, Security Group, Internet Gateway
  • Route53

Application

ServiceとEnvironmentを束ねたひとまとまりのことです。

全体像

Service、Environment、Applicationをまとめた全体像は以下のようになります。

https://aws.github.io/copilot-cli/docs/concepts/environments/

Copilot Jobsとは

ECSタスクを定期実行するアーキテクチャです。
上記で説明した概念の中だとServiceと同じレイヤーの概念で、JobsのAWSリソースはEnvironmentに内包されます。
2種類の定期実行方法があります。

  • Rate: 固定レート。「3時間毎」のように固定の時間間隔で定期実行できます。
  • Cron: クーロン。「月初の12時30分」のように柔軟に定期実行できます。

実体はECSクラスタのScheduled Tasksかと思いきや、Step functionsのステートマシンです。
理由は、Step functionsの方がリトライやタイムアウト等の定期実行処理に必要な機能を持っているからとのことです。(David Killmonさんありがとうございます)

サンプル

すでにこちらの手順でCopilotを利用してAPIサーバーを作成済みなので、フォルダ構成は以下のような状態から始めます。Railsを使っていますが、もちろん何の言語でも大丈夫です。

├── config
├── copilot
│  ├── api
│  │    └── manifest.yml
│  └── .workspace
├── db
...

作成する定期実行処理の中身には触れずに、早速Jobを作成していきましょう。
まず、ECSタスク用のDockerfileを作成します。ほぼAPIサーバー用のDockerfileのコピーなので、不要な部分が混じっていますがご容赦ください。

task.Dockerfile
FROM ruby:2.7.1-alpine3.12

ENV ROOT="/marketing" \
    LANG=C.UTF-8 \
    TZ=Asia/Tokyo \
    EDITOR=vim

WORKDIR ${ROOT}

RUN apk update && \
    apk upgrade && \
    apk add --no-cache \
        tzdata \
        nodejs \
        mysql-dev \
        mysql-client \
        vim && \
    apk add --virtual build-packs --no-cache \
        build-base \
        curl-dev \
        gcc \
        g++ \
        libc-dev \
        libxml2-dev \
        linux-headers \
        make

COPY Gemfile ${ROOT}
COPY Gemfile.lock ${ROOT}

RUN bundle install
RUN apk del build-packs

COPY . ${ROOT}

CMD ["bundle", "exec", "rake", "test_task"]

Jobの定義

Dockerfileと定期実行したい処理(test_task)を作成したらJobを定義していきます。
このように対話型のコマンドでJobを定義できます。

$ copilot job init

  # Jobの名前
  What do you want to name this Scheduled Job? [? for help] 
  > task
  
  # ECSタスク用のDockerfileの場所
  Which Dockerfile would you like to use for test?  [Use arrows to move, type to filter, ? for more help]
    ./Dockerfile
  > Enter custom path for your Dockerfile
    Use an existing image instead
  
  What is the path to the Dockerfile for test? [? for help] 
  > ./task.Dockerfile
  
  # 定期実行方法(RateかCronか)
  How would you like to schedule this job?  [Use arrows to move, type to filter, ? for more help]
    Rate
  > Fixed Schedule
  
  # 具体的なスケジュール
  What schedule would you like to use?  [Use arrows to move, type to filter, ? for more help]
  > Custom
    Hourly
    Daily
    Weekly
    Monthly
    Yearly
  
  What custom cron schedule would you like to use? [? for help] (0 * * * *) 
  > 0 */3 * * *
  
  # スケジュールの確認
  Your job will run at the following times: At 0 minutes past the hour, every 3 hours
  Would you like to use this schedule? [? for help] (y/N) 
  > y

上記が完了すると、以下のようにJobが定義された旨のメッセージが表示されると共に、Dockerイメージ格納用のECRリポジトリが1つ作成されます。

  ✔ Wrote the manifest for job task at copilot/task/manifest.yml
  Your manifest contains configurations like your container size and job schedule (0 */3 * * *).

  ✔ Created ECR repositories for job task.

  Recommended follow-up actions:
  - Update your manifest copilot/task/manifest.yml to change the defaults.
  - Run `copilot job deploy --name task --env test` to deploy your job to a test environment.

manifest.ymlの変更

この時点でフォルダ構成は以下のようになっているはずです。

├── config
├── copilot
│  ├── api
│  │    └── manifest.yml
│  ├── task
│  │    └── manifest.yml
│  └── .workspace
├── db
...

manifest.ymlはECSのタスク定義のいくつかのパラメータの変更、環境に応じた実行スケジュールの変更等ができます。
今回は、リトライ回数とタイムアウト時間、Environmentがproductionの場合にはRAILS_ENVを変更するような設定をしました。

copilot/task/manifest.yml
name: task
type: Scheduled Job

image:
  build: ./task.Dockerfile

cpu: 256
memory: 512
retries: 3
timeout: 10m

on:
  schedule: "0 */3 * * *"
  
variables:
  RAILS_ENV: development

environments:
  production:
    variables:
      RAILS_ENV: production
    on:
      schedule: "0 */3 * * *"

デプロイ

デプロイしていきます。
Service(APIサーバー)ではなく、今回定義したJobをデプロイするよう選択します。

$ copilot deploy

  Select a service or job in your workspace  [Use arrows to move, type to filter]
    api
  > task

以下のように、イメージビルド -> タグ付け -> ECRリポジトリログイン -> イメージのプッシュ -> デプロイという流れでデプロイが完了しました。

  Name:  task
  Only found one environment, defaulting to: production
  Sending build context to Docker daemon  186.4kB
  Step 1/10 : FROM ruby:2.7.1-alpine3.12
   ---> b46ea0bc5984
  ...
  Successfully built f6b288315c52
  Successfully tagged ************.dkr.ecr.ap-northeast-1.amazonaws.com/marketing/task:2da91fe
  Login Succeeded
  The push refers to repository [************.dkr.ecr.ap-northeast-1.amazonaws.com/marketing/task]
  7cf9918bf5c1: Pushed 
  ...
  2da91fe: digest: sha256:48bdc970f9c3a09c6cbe5ada90a4b5d287e5c57c9bd6098b5685d830f299f8d8 size: 2831

  ✔ Deployed task.

動作確認

CloudFormationを確認すると、以下のようなリソースが作成されます。
ServiceではECSサービスが作成されていたのに対して、JobではEventBridgeとStep functionsのリソースが作成されています。

CloudFormation

しばらく時間を置いてStep functionsのコンソールを見ると、ステートマシンが3時間置きに実行されていることがわかりました。(確認のため初回だけ手動実行してしまいました)

Step funcions

ハマりどころ

manifest.ymlでは、Enviromentによって環境変数や実行スケジュールを変更できます。
Environmentによって同一の実行スケジュールを使う場合には、environmentsに実行スケジュールを記述する必要はなさそうですが、記述しないとデプロイ時にエラーが発生します。(多分バグだと思います)

copilot/task/manifest.yml
on:
  schedule: "0 */3 * * *"

variables:
  RAILS_ENV: development

environments:
  production:
    variables:
      RAILS_ENV: production
    # 以下の記述がないとエラー
    on:
      schedule: "0 */2 * * *"

エラーログ。scheduleを定義しろと怒られています。

  $ copilot deploy

  ...
  ...
  ✘ Failed to deploy job.

  ✘ execute job deploy: deploy job: convert schedule for job task: missing required field "schedule" in manifest for job task

こちらのissueでも触れられていますが、2020/12/16時点で治りそうな気配はありませんでした。

It looks like schedule is required when we have environments on the manifest.

We have to add
on:
schedule
on the environments too.
example:
environments:
test:
on:
schedule: "expersion"

if not added, it will generate an error schedule required.

https://github.com/aws/copilot-cli/issues/1736

おわりに

Copilot Jobsを利用して定期実行するECSタスクを作成することができました。
Dockerfileだけ作成すればアーキテクチャ構築からデプロイまでやってくれるので、単発処理をサクッと作りたいような時にはLambdaやECSのスケジューリングタスクよりも簡単にできるのではないでしょうか。