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 all 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
32 changes: 32 additions & 0 deletions .github/workflows/rspec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Ruby

on:
push:
branches: [main]
pull_request:

jobs:
rspec:
runs-on: ubuntu-latest
env:
BUNDLE_JOBS: 4
BUNDLE_RETRY: 3
strategy:
fail-fast: false
matrix:
ruby: ["3.0", "3.1", "3.2"]
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v3
with:
path: /home/runner/bundle
key: bundle-${{ matrix.ruby }}-${{ hashFiles('**/*.gemspec') }}
restore-keys: |
bundle-${{ matrix.ruby }}-
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Run rspec
run: bundle exec rspec
1 change: 1 addition & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--require spec_helper
35 changes: 34 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
PATH
remote: .
specs:
sg_fargate_rails (0.1.5)
sg_fargate_rails (0.1.8)
aws-sdk-ec2 (~> 1.413)
aws-sdk-scheduler (~> 1.10)
lograge (~> 0.12)
puma
rack-attack (~> 6.6)
Expand All @@ -27,12 +29,29 @@ 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)
diff-lcs (1.5.0)
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 @@ -71,6 +90,19 @@ GEM
rake (13.0.6)
request_store (1.5.1)
rack (>= 1.4)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
rspec-mocks (~> 3.12.0)
rspec-core (3.12.2)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-mocks (3.12.6)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-support (3.12.1)
thor (1.2.1)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
Expand All @@ -82,6 +114,7 @@ PLATFORMS

DEPENDENCIES
rake (~> 13.0)
rspec
sg_fargate_rails!

BUNDLED WITH
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
125 changes: 125 additions & 0 deletions lib/sg_fargate_rails/event_bridge_schedule.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
require "aws-sdk-scheduler"

module SgFargateRails
class EventBridgeSchedule
CONTAINER_TYPES = {
'small' => { cpu: '512', memory: '1024', },
'medium' => { cpu: '1024', memory: '2048', },
'large' => { cpu: '2048', memory: '4096', },
}.freeze

attr_reader :name

def initialize(name, cron, command, container_type)
@name = name
@cron = cron
@command = command
@container_type = container_type
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: input_overrides_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 input_overrides_json
type = convert_container_type
if type
{
**type,
"containerOverrides": [
{
"name": "rails",
**type,
"command": container_command,
}
]
}.to_json
else
{
"containerOverrides": [
{
"name": "rails",
"command": container_command,
}
]
}.to_json
end
end

def convert_container_type
CONTAINER_TYPES[@container_type]
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.unsafe_load(File.open(filename))[environment]
schedules.map { |name, info| EventBridgeSchedule.new(name, info['cron'], info['command'], info['container_type']) if name != '<<' }
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 environment
ENV['RAILS_ENV']
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 @@ -6,6 +6,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
2 changes: 1 addition & 1 deletion lib/sg_fargate_rails/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module SgFargateRails
VERSION = "0.1.8"
VERSION = "0.1.9"
end
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
5 changes: 4 additions & 1 deletion sg_fargate_rails.gemspec
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# frozen_string_literal: true

require_relative "lib/sg_fargate_rails/version"

Gem::Specification.new do |spec|
Expand Down Expand Up @@ -40,4 +39,8 @@ 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'

spec.add_development_dependency 'rspec'
end
7 changes: 7 additions & 0 deletions spec/fixtures/event_bridge_schedule/blank_schedule.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
common: &common

staging:
<<: *common

production:
<<: *common
Loading
Loading