🪴

Ruby環境からRunTaskAPIを使用してECSタスクを起動する

2025/02/23に公開

はじめに

Ruby環境からRunTaskAPIを使用してECSタスクを起動する手順を記載します。

具体的には、RailsアプリケーションのECSタスクから、データの集計やレポート作成など、時間がかかる処理をバックグラウンドで実行するために、RunTaskAPIを利用してバッチ処理専用のECSタスクを単発で起動させました。
また、今回はRunTaskAPIをAWS SDK(gem 'aws-sdk-ecs')にて使用しました。

背景

  • 管理画面からユーザーのアクションによって実行する集計処理があるが、7分ほどかかる
  • ユーザビリティやサーバー負荷を鑑みて非同期で実行をさせたかったが、コスト削減のため常駐のジョブサーバー等を用意したくない
  • そのため、単発でECSタスクを起動できるRunTaskAPIを使用することになった

RunTaskAPIとは

  • ECSクラスター内で、タスクを実行するためのAPI。
  • 実用上はサービスを使ってECSタスクを管理・スケーリングすることが多いが、サービス管理されないタスクをクラスター内に単発で起動することも可能。
  • RunTask を単独で使用する場合は、手動またはAPI経由(AWS CLI や AWS SDK)で実行する。例えば、バッチ処理やワークフローの一部として利用される。

手順

1. RailsアプリケーションにSDK(gem)を導入

Gemfile
gem 'aws-sdk-ecs'
(gem 'aws-sdk-ec2'
bundle install --path vendor/bundle
  • gem 'aws-sdk-ec2'はRunTask自体には不要ですが、RunTaskをする際の設定でサブネット・セキュリティグループを動的に取得するために今回は追加しています。不要であれば追加する必要はありません。

2. RunTask APIを呼び出す実装

RunTaskを行うServiceクラスを作成

まずは、コードを記載します。

require 'aws-sdk-ec2'
require 'aws-sdk-ecs'

class EcsTaskRunner
  def initialize(command, task_cpu = '512', task_memory = '1024', container_memory_reservation = '1024')
    @ecs_client = Aws::ECS::Client.new
    @ec2_client = Aws::EC2::Client.new 
    @command = command
    @task_cpu = task_cpu
    @task_memory = task_memory
    @container_memory_reservation = container_memory_reservation
  end

  def run
    response = @ecs_client.run_task(
      cluster: "#{Rails.env}-cluster", # タスクを立ち上げるクラスター名を指定
      task_definition: "#{Rails.env}-app-nginx", # 使用するタスク定義を指定
      count: 1, # 立ち上げるタスク数
      network_configuration: {
        awsvpc_configuration: {
          subnets: subnet_ids, # サブネットIDの設定
          security_groups: security_group_ids, # セキュリティグループIDの設定
          assign_public_ip: 'ENABLED' # 起動したタスクから外部インターネット通信したい場合必要
        }
      },
      overrides: {
        cpu: @task_cpu, # タスクのCPU
        memory: @task_memory, # タスクのメモリ(ハード)
        container_overrides: [
          {
            name: "app-#{Rails.env}-container", # オーバーライドするコンテナ名を指定
            command: @command, # 実行コマンド
            memory_reservation: @container_memory_reservation # コンテナのメモリ(ソフト)
          }
        ]
      }
    )
    parsed_response = response.to_h

    # 失敗した場合
    raise StandardError, "RunTaskに失敗しました: #{parsed_response['failures']}" if parsed_response['failures'].present?

    # 成功した場合
    success_response = {
      'statusCode' => 200,
      'headers' => { 'Content-Type' => 'application/json' },
      'body' => parsed_response
    }
    Rails.logger.info("[EcsTaskRunner] RunTaskに成功しました: #{success_response}")
    success_response
  rescue StandardError => e
    error_response = {
      'statusCode' => 500,
      'headers' => { 'Content-Type' => 'application/json' },
      'body' => {
        'error' => 'タスクの起動に失敗しました',
        'message' => e.message
      }
    }
    Rails.logger.info("[EcsTaskRunner] 例外が発生しました: #{error_response}")
    error_response
  end

  private

  # サブネットIDをNameというkeyのタグ名を指定して取得
  def subnet_ids
    subnet_tags = @ec2_client.describe_subnets(
      filters: [
        {
          name: 'tag:Name',
          values: ["{Rails.env}-app-publicSubnet*"]
        }
      ]
    ).subnets

    subnet_tags.map(&:subnet_id)
  end

  # セキュリティグループIDをNameというkeyのタグ名を指定して取得
  def security_group_ids
    security_group_tags = @ec2_client.describe_security_groups(
      filters: [
        {
          name: 'tag:Name',
          values: ["#{Rails.env}-app"]
        }
      ]
    ).security_groups

    security_group_tags.map(&:group_id)
  end
end

解説

  • 今回、タスクロールにポリシーを付与する方法で権限を設定するため、アプリケーション内でAWS_ACCESS_KEY_ID や AWS_SECRET_ACCESS_KEYを設定する必要はありません。3. AWS権限の設定で説明)
  • RunTaskするタスクのサブネット・セキュリティグループを動的に(タグ名から)取得できるようにしたため、aws-sdk-ec2も使用しています。
  • タスクのCPU・メモリ、コンテナのメモリ(ソフト制限)・実行コマンドを引数に渡して任意で設定できるようにしています。
  • RunTaskの設定として、クラスター、タスク定義、サブネット、セキュリティグループ、立ち上げるタスク数等を指定しています。他の設定項目はドキュメントを参照してください。
  • Rails.envの部分により、環境ごとにクラスターやタスク定義が指定されています。

Serviceクラスの呼び出し

def execute
 command = ['bundle', 'exec', 'rake', "hoge:aggregate_data"]
 ecs_task_runner = EcsTaskRunner.new(command, 1024, 2048, 2048)
 response = ecs_task_runner.run

 if response['statusCode'] == 200
  redirect_to xxxxxx_path, notice: '再集計を開始しました'
 else
  redirect_to xxxxxx_path, alert: '再集計の開始に失敗しました。'
 end
end
  • 下記の例では、タスクのCPU=1vCPU、memory=2048MiB、コンテナのmemory_reservation=2048MiBで設定しています。実行コマンドは、Rakeタスクを実行しています。

3. AWS権限の設定

RunTaskAPIを呼び出す側のECSに、「ECSタスクを呼び出すAWS権限」を付与する必要があります。

AWSの認証情報の設定方法はいくつかありますが、今回はタスクロールにポリシーを付与する方法を選択しました。
ECSのIAM RoleはCDKで管理しているため、addToPrincipalPolicyでポリシーを追加しました。

下記はCDKのコードです。
のついたポリシーは、実装するタスクの内容によって必要なものになりますので、必要に応じて追加してください。
足りないポリシーがあれば、ログの内容を参照しながら追加していきます

import { Construct } from 'constructs'
import { Role, ServicePrincipal, ManagedPolicy, PolicyStatement } from 'aws-cdk-lib/aws-iam'

export class EcsIam extends Resource {
  public ecsTaskExecutionRole: Role

  constructor() {
    super()
  }

  createResources(scope: Construct) {
    const roleName: string = 'production-ecs-task-execution-role'

    this.ecsTaskExecutionRole = new Role(scope, roleName, {
      roleName: roleName,
      assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'),
      managedPolicies: [
        ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy'),
      ],
    })

    // ***************ここから追加***************
    // ECS RunTaskするためのポリシー
    this.ecsTaskExecutionRole.addToPrincipalPolicy(
      new PolicyStatement({
        actions: [
          'ecs:RunTask',
        ],
        resources: [
          `arn:aws:ecs:ap-northeast-1:xxxxxxxxx:cluster/production-cluster`,
          `arn:aws:ecs:ap-northeast-1:xxxxxxxxx:task-definition/production-app-nginx:*`,
        ]
      })
    );

    // RunTaskされたECSタスクに適切なIAMロールを渡すためのポリシー
    this.ecsTaskExecutionRole.addToPrincipalPolicy(
      new PolicyStatement({
        actions: ['iam:PassRole'],
        resources: [
          `arn:aws:iam::xxxxxxx:role/production-ecs-task-execution-role`
        ], // RunTaskで実行されるタスクに付与する実行ロール
        conditions: {
          StringEquals: {
            'iam:PassedToService': 'ecs-tasks.amazonaws.com' // ECSタスクのみにロールの受け渡しを制限
          }
        }
      })
    );

    // ★サブネット・セキュリティグループを取得するポリシー(RunTaskする際にタグ名で動的に取得するようにしたため)
    this.ecsTaskExecutionRole.addToPrincipalPolicy(
      new PolicyStatement({
        actions: [
          'ec2:DescribeSubnets',
          'ec2:DescribeSecurityGroups',
        ],
        resources: ['*']
      })
    );

    // ★SSM Parameter Storeからシークレットを取得するポリシー(RunTaskされたECSタスクがParameter Storeを参照したいため)
    this.ecsTaskExecutionRole.addToPrincipalPolicy(
      new PolicyStatement({
        actions: ['ssm:GetParameters', 'ssm:GetParameter', 'ssm:DescribeParameters'],
        resources: [`arn:aws:ssm:ap-northeast-1:xxxxxxx:parameter/*`],
      })
    );
  }
}

まとめ

💡 今回は、ECSタスク(Ruby環境)からRunTaskAPI(AWS SDK = gem 'aws-sdk-ecs')を使用して別のECSタスクを起動する方法を記載しました。

💡 RunTaskAPIを呼び出す側のECSに、「ECSタスクを呼び出すAWS権限」を付与する必要があり、タスクロールにポリシーを付与する方法で設定をしました。

参考にさせていただいた記事

株式会社L&E Group

Discussion