🪴
Ruby環境からRunTaskAPIを使用してECSタスクを起動する
はじめに
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権限」を付与する必要があり、タスクロールにポリシーを付与する方法で設定をしました。
Discussion