AWS Copilot CLIのScheduled JobとSpring Shellを組み合わせて一回限りのタスクを実行する

はじめに

エキサイト株式会社 バックエンドエンジニアの山縣(@zsp2088dev)です。 エキサイトホールディングス - Qiita Advent Calendar 2024 - Qiita の18日目を担当します。

エキサイトブログの開発では、新機能の開発や既存機能の改修時に、データベースに新たなテーブルを作成し既存のデータを移行することがあります。 データ移行を何度か経験していく中で、より快適なデータ移行環境を目指し、AWS Copilot CLIのScheduled JobとSpring Shellを組み合わせたデータ移行環境を構築しました。 本記事では、このような環境を用意した背景や、実際に取り組んでいることについて紹介します。

背景

一般に、あるテーブルから別のテーブルへデータを移行する場合、複雑な加工処理をしないのであればINSERT... SELECTで済むことが多いです。 一方で、スキーマ間のデータ移行や複雑な加工処理を必要とするデータ移行では、アプリケーション内でデータの加工をしたほうが柔軟性が高くなります。 例えば、データの正規化や結合、不要なデータのフィルタリングなどを必要とする場合、SQLだけでは記述が複雑になりがちです。 また、アプリケーションコードを書くことで、テストコードを書くことができたり、ログ出力ができたりなどの利点があります。

エキサイトブログの開発でも、昨年のブログテーマ機能の改修のときに以下のような取り組みをしていました。

tech.excite.co.jp

当時は、EC2上でgit cloneしてアプリケーションコードを手動実行していました。 データ移行は繰り返し行うものではなく一回限りの実行であるため、煩わしさを感じつつも「頻度が高くないからこのままでいいな」と考えていました。 しかし、今後も同様の操作をすることを考えたときに、より快適なデータ移行環境を目指したほうがよいと考え、新たなデータ移行環境を検討するようになりました。

環境

本記事で扱う環境は以下の通りです。 記事内では実際にデータ移行しているところまでは触れず、「データ移行を実行する環境」について触れていきます。

Spring Shellの設定

Spring Shell 3系では@Commandアノテーションを使用してコマンド登録ができます。 下記のコードに対して、./gradlew bootRun --args="hello"と実行したときに、test1と出力されることが確認できます。 同様に、./gradlew bootRun --args="world"と実行したときに、test2と出力されます。 このように、引数に応じて実行したいメソッドを切り替えることができます。

package com.example.command;

import lombok.RequiredArgsConstructor;
import org.springframework.shell.command.annotation.Command;

@Command
@RequiredArgsConstructor
public class SampleCommand {
    @Command(command = "hello")
    public String hello() {
        return "test1"
    }

    @Command(command = "world")
    public String hello() {
        return "test2"
    }
}

データ移行時は、本メソッド内にデータの読み込み、書き込み、加工処理を書いていました。

Spring Bootをコンテナ化する

jibを使用して、Spring Bootで作成したアプリケーションをコンテナ化しています。 argsプロパティに対して、@Commandアノテーションに定義したcommandの値を設定することで、コンテナ実行時に任意のメソッドを実行できるようになります。 これを利用して、./gradlew jibDockerBuild -Pargs=helloとコンテナ化することで、コンテナ実行時にtest1と出力するコンテナイメージを作成できます。

jib {
    container {
        if (project.hasProperty("args")) {
            args = [
                    project.property('args')
            ]
        }
    }
}

jibの詳細やその他オプションについては下記のドキュメントをご参照ください。

github.com

AWS Copilot CLIの設定

Amazon ECS内でコンテナ化されたアプリケーションを実行するために、AWS Copilot CLIScheduled Jobを使用しています。 Scheduled Jobマニフェストscheduledフィールドにnoneを設定することで、タスクが定期実行されないように設定できます。

on:
  schedule: "none"

aws.github.io

上記の設定により、タスクは定期実行されません。 Scheduled Jobのドキュメント内には、noneを設定するのは一時的無効化したい場合と記載されているため、本来の用途とは違った使い方をしています。 copilot job runコマンドを実行すると、任意のタイミングでタスクを実行することができます。 これにより、タスクを実行したいときだけcopilot job runコマンドを実行すればよいです。

aws.github.io

GitHub Actionsから実行する

上記の内容までを実装することで、ローカル環境上でcopilot job runと実行するとタスクを実行できます。 これを、ローカル環境上にcopilotコマンドが無くても実行できるように、GitHub Actions上で実行できるようにしていきます。

ここでは、ワークフローを実行するのにGitHub Actionsのworkflow_dispatchイベントと、workflow_dispatch.inputsを使用します。 以下のようにworkflow_dispatch.inputsを定義すると、ワークフロー内で ${{ inputs.ARGS }}として扱えます。

on:
  workflow_dispatch:
    inputs:
      ARGS:
        description: ジョブ名を入力してください
        required: true
        type: string
      ENV:
        description: デプロイ先の環境を選択してください
        default: test
        required: true
        type: choice
        options:
          - test
          - prod

これにより、下記のようにhelloと入力し、ワークフロー内でコンテナをビルド、コンテナレジストリにプッシュ、copilot job run と実行することで、ECS上でtest1と出力できるようになります。

おわりに

本記事では、AWS Copilot CLIのScheduled JobとSpring Shellを組み合わせた一回限りのタスクを実行する方法について紹介しました。 サンプルでは標準出力をしただけですが、我々の開発ではデータベースからデータを読み取り、アプリケーション内で加工、データを書き込みといった操作をしています。 一度環境を整備してしまえば、いつでもすぐに利用できるのが非常に嬉しいです。

一方で、本記事では事業部内で知見のあるAWS Copilot CLIを使用していますが、以下のissueの通り、開発体制が不安定な状況にあるようです。 もし本記事と同様のことを実現したい場合は、以下の内容についてご留意いただけますと幸いです。

github.com

採用アナウンス

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しています。 また、長期インターンも歓迎しています。

カジュアル面談からもOKです。少しでもご興味がございましたら、お気軽にご連絡頂ければ幸いです。

▼ 募集職種一覧 ▼ recruit.jobcan.jp