Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[review] EventBridge Scheduler のスケジュール登録 #14

Merged
merged 17 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
PATH
remote: .
specs:
sg_fargate_rails (0.1.5)
sg_fargate_rails (0.1.6)
aws-sdk-ec2 (~> 1.413)
lograge (~> 0.12)
puma
rack-attack (~> 6.6)
Expand All @@ -27,12 +28,28 @@ GEM
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
aws-eventstream (1.2.0)
aws-partitions (1.835.0)
aws-sdk-core (3.185.1)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1)
aws-sdk-ec2 (1.413.0)
aws-sdk-core (~> 3, >= 3.184.0)
aws-sigv4 (~> 1.1)
aws-sdk-scheduler (1.10.0)
aws-sdk-core (~> 3, >= 3.184.0)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.6.0)
aws-eventstream (~> 1, >= 1.0.2)
builder (3.2.4)
concurrent-ruby (1.2.0)
crass (1.0.6)
erubi (1.12.0)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
jmespath (1.6.2)
lograge (0.12.0)
actionpack (>= 4)
activesupport (>= 4)
Expand Down Expand Up @@ -81,6 +98,7 @@ PLATFORMS
x86_64-linux

DEPENDENCIES
aws-sdk-scheduler (~> 1.10)
rake (~> 13.0)
sg_fargate_rails!

Expand Down
2 changes: 2 additions & 0 deletions lib/sg_fargate_rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

require_relative "sg_fargate_rails/version"
require_relative "sg_fargate_rails/config"
require_relative "sg_fargate_rails/current_ecs_task"
require_relative "sg_fargate_rails/event_bridge_schedule"
require 'lograge'

if defined?(::Rails::Railtie)
Expand Down
82 changes: 82 additions & 0 deletions lib/sg_fargate_rails/current_ecs_task.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
require 'net/http'
require 'json'
require 'aws-sdk-ec2'

module SgFargateRails
class CurrentEcsTask
def cluster_arn
metadata[:Cluster]
end

def task_definition_arn
"#{cluster_arn.split(":cluster/")[0]}:task-definition/#{metadata[:Family]}:#{metadata[:Revision]}"
end

def cfn_stack_name
"#{ENV['COPILOT_APPLICATION_NAME']}-#{ENV['COPILOT_ENVIRONMENT_NAME']}"
end

def security_group_ids
@security_group_ids ||= fetch_security_group_ids
end

def public_subnet_ids
@public_subnet_ids ||= fetch_public_subnet_ids
end

private

def metadata
@metadata ||= begin
response = Net::HTTP.get(URI.parse("#{ENV['ECS_CONTAINER_METADATA_URI']}/task"))
JSON.parse(response, symbolize_names: true)
end
end

def region
ENV['AWS_REGION'] || 'ap-northeast-1'
end

def ec2_client
@ec2_client ||= Aws::EC2::Client.new(region: region, credentials: credentials)
end

def credentials
@credentials ||= Aws::ECSCredentials.new(retries: 3)
end

def fetch_security_group_ids
security_group_params = {
filters: [
{
name: 'tag:aws:cloudformation:logical-id',
values: ['EnvironmentSecurityGroup'],
},
{
name: 'tag:aws:cloudformation:stack-name',
values: [cfn_stack_name],
}
],
}
resp = ec2_client.describe_security_groups(security_group_params)
resp.to_h[:security_groups].map { |group| group[:group_id] }
end

def fetch_public_subnet_ids
subnet_params = {
filters: [
{
name: 'tag:aws:cloudformation:logical-id',
values: %w[PublicSubnet1 PublicSubnet2],
},
{
name: 'tag:aws:cloudformation:stack-name',
values: [cfn_stack_name],
},
],
}
resp = ec2_client.describe_subnets(subnet_params)
resp.to_h[:subnets].map { |subnet| subnet[:subnet_id] }
end
end
end
92 changes: 92 additions & 0 deletions lib/sg_fargate_rails/event_bridge_schedule.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
require "aws-sdk-scheduler"

module SgFargateRails
class EventBridgeSchedule
attr_reader :name

def initialize(name, cron, command)
@name = name
@cron = cron
@command = command
end

def create_run_task(group_name:, cluster_arn:, task_definition_arn:, network_configuration:)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ruby 3.1の記法じゃなかったっけ?3.0とかでもOKだったっけ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここはメソッドのキーワード引数(必須)なので、前から書けるはず。
3.1 からのやつはハッシュリテラルの省略記法ですね。

a = 1
{ a: } # => { a: 1 }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

なるほど!

params = {
name: @name,
state: 'ENABLED',
flexible_time_window: { mode: 'OFF' },
group_name: group_name,
schedule_expression: @cron,
schedule_expression_timezone: timezone,
target: {
arn: cluster_arn,
ecs_parameters: {
task_count: 1,
task_definition_arn: task_definition_arn,
launch_type: 'FARGATE',
network_configuration: network_configuration
},
input: {
"containerOverrides": [
{
"name": "rails",
"command": container_command,
}
]
}.to_json,
retry_policy: {
maximum_event_age_in_seconds: 120,
maximum_retry_attempts: 2,
},
role_arn: role_arn_for(group_name, cluster_arn),
},
}
client.create_schedule(params)
end

def container_command
%w[bundle exec] + @command.split(' ')
end

private

def timezone
ENV['TZ'] || 'Asia/Tokyo'
end

def role_arn_for(group_name, cluster_arn)
account_id = cluster_arn.split(':')[4]
"arn:aws:iam::#{account_id}:role/#{group_name}-eventbridge-scheduler-role"
end

def client
self.class.client
end

class << self
def parse(filename)
schedules = YAML.load(File.open(filename))
schedules.map { |name, info| EventBridgeSchedule.new(name, info['cron'], info['command']) }
end

def delete_all!(group_name)
client.list_schedules(group_name: group_name, max_results: 100).schedules.each do |schedule|
client.delete_schedule(name: schedule.name, group_name: group_name)
Rails.logger.info "[EventBridgeSchedule] Deleted #{group_name}/#{schedule.name}"
end
end

def client
@client ||= Aws::Scheduler::Client.new(region: region, credentials: credentials)
end

def region
ENV['AWS_REGION'] || 'ap-northeast-1'
end

def credentials
Aws::ECSCredentials.new(retries: 3)
end
end
end
end
4 changes: 4 additions & 0 deletions lib/sg_fargate_rails/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

module SgFargateRails
class Railtie < ::Rails::Railtie
rake_tasks do
load File.expand_path('../tasks/sg_fargate_rails.rake', __dir__)
end

initializer :initialize_sg_fargate_rails do |app|
unless ::Rails.env.in?(%w[development test])
SgFargateRails::RackAttack.setup
Expand Down
33 changes: 33 additions & 0 deletions lib/tasks/sg_fargate_rails.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace :sg_fargate_rails do
require 'sg_fargate_rails'

desc 'EventBridge Schedules'
task recreate_schedules: :environment do
ecs_task = SgFargateRails::CurrentEcsTask.new
Rails.logger.info "[INFO] security_group_ids: #{ecs_task.security_group_ids}"
Rails.logger.info "[INFO] subnet_ids: #{ecs_task.public_subnet_ids}"

group_name = ecs_task.cfn_stack_name
Rails.logger.info "[EventBridgeSchedule] Clear all schedules in #{group_name}"
SgFargateRails::EventBridgeSchedule.delete_all!(group_name)

Rails.logger.info "[EventBridgeSchedule] Register schedules in #{group_name}"
config_file = Rails.root.join('config/eventbridge_schedules.yml')
SgFargateRails::EventBridgeSchedule.parse(config_file).each do |schedule|
Rails.logger.info "[EventBridgeSchedule] Register schedule #{schedule.name} in #{group_name}"
# TODO: この辺で AWS の API Limit などのエラーが発生するとスケジュールが消えたままとなるので、エラーの内容に応じてリトライなどのエラー処理が必要
schedule.create_run_task(
group_name: group_name,
cluster_arn: ecs_task.cluster_arn,
task_definition_arn: ecs_task.task_definition_arn,
network_configuration: {
awsvpc_configuration: {
assign_public_ip: 'ENABLED',
security_groups: ecs_task.security_group_ids,
subnets: ecs_task.public_subnet_ids,
},
}
)
end
end
end
2 changes: 2 additions & 0 deletions sg_fargate_rails.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ Gem::Specification.new do |spec|
spec.add_dependency 'puma'
spec.add_dependency 'lograge', '~> 0.12'
spec.add_dependency 'rack-attack', '~> 6.6'
spec.add_dependency 'aws-sdk-ec2', '~> 1.413'
spec.add_dependency 'aws-sdk-scheduler', '~> 1.10'
end