Skip to content

Commit

Permalink
Merge pull request #14 from SonicGarden/kzkn-20231013-eventbridge-sch…
Browse files Browse the repository at this point in the history
…eduler

[review] EventBridge Scheduler のスケジュール登録
  • Loading branch information
interu authored Nov 1, 2023
2 parents 9ed506f + bdff0f1 commit d0e2229
Show file tree
Hide file tree
Showing 14 changed files with 416 additions and 3 deletions.
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:)
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

0 comments on commit d0e2229

Please sign in to comment.