ecs_deploy 1.0.7 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +9 -10
- data/.gitignore +2 -0
- data/.rspec +1 -0
- data/CHANGELOG.md +18 -0
- data/README.md +69 -0
- data/ecs_deploy.gemspec +2 -2
- data/lib/ecs_deploy/auto_scaler/null_cluster_resource_manager.rb +13 -0
- data/lib/ecs_deploy/auto_scaler/simple_scaling_config.rb +33 -0
- data/lib/ecs_deploy/auto_scaler.rb +12 -1
- data/lib/ecs_deploy/capistrano.rb +58 -22
- data/lib/ecs_deploy/scheduled_task.rb +17 -29
- data/lib/ecs_deploy/service.rb +183 -108
- data/lib/ecs_deploy/service_deployment.rb +71 -0
- data/lib/ecs_deploy/task_definition.rb +15 -34
- data/lib/ecs_deploy/version.rb +1 -1
- data/lib/ecs_deploy.rb +1 -0
- data/renovate.json +6 -0
- metadata +18 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7ada66f2f084f7ea0e30b0b6808d2635480db1071ae13acfc16f6f7d4cc02fef
|
|
4
|
+
data.tar.gz: 2188aa1ed3727d1c7719bca93006383b831a6f6379ffa28b08aee8a97d5281e5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a3e3f662af6586e71f002853a3599adb107c7db5b8b44c4d7ce3d8d13c6d37ae2210ec9a0a0749813e926f78c4879a3a72d851f6dfd2c466e8ec296a9900f244
|
|
7
|
+
data.tar.gz: f96818f250b0644a3908822fa741b26bba564095feff3d78e9a28acc30baa9da5b435200d3c2bc83edf4a2e3e7b73e48e5702ade9f1f3ac7c614be1a9402a067
|
data/.github/workflows/test.yml
CHANGED
|
@@ -6,18 +6,17 @@ on:
|
|
|
6
6
|
|
|
7
7
|
jobs:
|
|
8
8
|
test:
|
|
9
|
-
|
|
10
9
|
runs-on: ubuntu-latest
|
|
11
10
|
strategy:
|
|
12
11
|
matrix:
|
|
13
|
-
ruby-version: [
|
|
12
|
+
ruby-version: ["3.2", "3.3", "3.4", "4.0"]
|
|
14
13
|
|
|
15
14
|
steps:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
16
|
+
- name: Set up Ruby
|
|
17
|
+
uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
|
|
18
|
+
with:
|
|
19
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
20
|
+
bundler-cache: true
|
|
21
|
+
- name: Run tests
|
|
22
|
+
run: bundle exec rake
|
data/.gitignore
CHANGED
data/.rspec
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--format d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
# v1.1
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### New feature
|
|
6
|
+
|
|
7
|
+
- Add `ecs:describe_deployment`, `ecs:continue_deployment[hook_id]`, `ecs:rollback_deployment[hook_id]`, `ecs:stop_deployment[service_deployment_arn]` capistrano tasks for operating ECS-managed blue/green deployments (lifecycle hook approval / rollback / abort).
|
|
8
|
+
|
|
9
|
+
### Breaking change
|
|
10
|
+
|
|
11
|
+
- `ecs:deploy` capistrano task now passes the entire service hash through to `EcsDeploy::Service.new` instead of forwarding an explicit allow-list of keys. New SDK fields (e.g. `deployment_controller`, `platform_version`, `service_connect_configuration`, `volume_configurations`, `availability_zone_rebalancing`, `load_balancers[].advanced_configuration`) are supported automatically without gem updates. Any custom non-SDK keys previously placed in `set :ecs_services` entries will now reach the SDK and may cause errors — remove or rename them.
|
|
12
|
+
|
|
13
|
+
### Enhancement
|
|
14
|
+
|
|
15
|
+
- Add `wait_strategy` option to `EcsDeploy::Service` for ECS-managed blue/green deployments (`DeploymentController.Type=ECS` with `BLUE_GREEN`/`LINEAR`/`CANARY`). `wait_all_running` now auto-detects ECS-managed deployments and skips polling so Capistrano sessions do not block on multi-day Pause Hooks.
|
|
16
|
+
- `EcsDeploy::Service#update_service` now forwards every field accepted by `Aws::ECS::Client#update_service` (aws-sdk-ecs 1.238+), including fields added after this gem release. Options that ECS cannot apply on an existing service (`launch_type`, `scheduling_strategy`, `role`, `client_token`, `deployment_controller`) are detected and logged as warnings when the user's value differs from the current service. `desired_count: 0` and `propagate_tags: "NONE"` are now honored (previously silently dropped by a truthiness check).
|
|
17
|
+
- Bump minimum `aws-sdk-ecs` to `1.238` for `continue_service_deployment`, `stop_service_deployment`, `LINEAR`/`CANARY` strategy, `lifecycle_hooks`, and `LoadBalancer.advanced_configuration`.
|
|
18
|
+
|
|
1
19
|
# v1.0
|
|
2
20
|
|
|
3
21
|
## Release v1.0.7 - 2024/08/08
|
data/README.md
CHANGED
|
@@ -186,6 +186,75 @@ And rollback
|
|
|
186
186
|
| 6 | myapp:15 | myapp-service | deregister |
|
|
187
187
|
| 7 | myapp:12 | myapp-service | current |
|
|
188
188
|
|
|
189
|
+
## Native Blue/Green Deployment (ECS-managed)
|
|
190
|
+
|
|
191
|
+
`ecs_deploy` supports ECS-managed blue/green deployments where ECS itself drives the rollout (no CodeDeploy required). Set `deployment_controller`, `deployment_configuration`, lifecycle hooks, and `load_balancers[].advanced_configuration` directly on the service entry. The Capistrano `ecs:deploy` task forwards the entire hash through to `EcsDeploy::Service.new` and then to `Aws::ECS::Client#create_service` / `#update_service`, so any new SDK field is supported automatically.
|
|
192
|
+
|
|
193
|
+
When updating an existing service, `EcsDeploy::Service` forwards every field accepted by `Aws::ECS::Client#update_service`, including `deployment_configuration` and `load_balancers[].advanced_configuration`. Options that ECS treats as create-only (`launch_type`, `scheduling_strategy`, `role`, `client_token`, `deployment_controller`) are skipped and, if their value differs from the current service, logged as a warning. Unknown keys are forwarded verbatim and will surface as SDK errors — this is intentional so new SDK fields work without gem updates.
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
set :ecs_services, [
|
|
197
|
+
{
|
|
198
|
+
name: "myapp-#{fetch(:rails_env)}",
|
|
199
|
+
cluster: "myapp-cluster",
|
|
200
|
+
task_definition_name: "myapp-#{fetch(:rails_env)}",
|
|
201
|
+
launch_type: "FARGATE",
|
|
202
|
+
platform_version: "LATEST",
|
|
203
|
+
desired_count: 1,
|
|
204
|
+
network_configuration: { awsvpc_configuration: { subnets: %w[subnet-...], security_groups: %w[sg-...], assign_public_ip: "DISABLED" } },
|
|
205
|
+
deployment_controller: { type: "ECS" },
|
|
206
|
+
deployment_configuration: {
|
|
207
|
+
strategy: "LINEAR",
|
|
208
|
+
linear_configuration: { step_percent: 50.0, step_bake_time_in_minutes: 60 },
|
|
209
|
+
bake_time_in_minutes: 5,
|
|
210
|
+
deployment_circuit_breaker: { enable: true, rollback: true },
|
|
211
|
+
lifecycle_hooks: [
|
|
212
|
+
{
|
|
213
|
+
hook_target_arn: "arn:aws:lambda:ap-northeast-1:<account-id>:function:my-pause-hook",
|
|
214
|
+
role_arn: "arn:aws:iam::<account-id>:role/ecsLifecycleHookRole",
|
|
215
|
+
lifecycle_stages: ["POST_TEST_TRAFFIC_SHIFT"],
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
load_balancers: [{
|
|
220
|
+
target_group_arn: "arn:aws:elasticloadbalancing:...:targetgroup/blue/...",
|
|
221
|
+
container_name: "app",
|
|
222
|
+
container_port: 8080,
|
|
223
|
+
advanced_configuration: {
|
|
224
|
+
alternate_target_group_arn: "arn:aws:elasticloadbalancing:...:targetgroup/green/...",
|
|
225
|
+
production_listener_rule: "arn:aws:elasticloadbalancing:...:listener-rule/...", # for NLB, pass the Listener ARN directly
|
|
226
|
+
test_listener_rule: "arn:aws:elasticloadbalancing:...:listener-rule/...",
|
|
227
|
+
role_arn: "arn:aws:iam::<account-id>:role/ecsInfrastructureRole",
|
|
228
|
+
},
|
|
229
|
+
}],
|
|
230
|
+
health_check_grace_period_seconds: 300,
|
|
231
|
+
|
|
232
|
+
# gem-internal option (not sent to the SDK):
|
|
233
|
+
wait_strategy: :none, # default for ECS-managed deployments is auto-detected as :none
|
|
234
|
+
},
|
|
235
|
+
]
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
| option | values | purpose |
|
|
239
|
+
|--------|--------|---------|
|
|
240
|
+
| `wait_strategy:` | `nil` (auto), `:legacy`, `:none`, `:service_deployment` | `nil` auto-detects ECS-managed deployments and skips waiting (multi-day Pause Hooks make blocking impractical). `:legacy` matches pre-1.1 behavior. `:service_deployment` polls `list_service_deployments` (not recommended for sessions). |
|
|
241
|
+
|
|
242
|
+
### Operational tasks
|
|
243
|
+
|
|
244
|
+
```sh
|
|
245
|
+
bundle exec cap <stage> ecs:describe_deployment # list in-flight deployments with lifecycle hook details
|
|
246
|
+
bundle exec cap <stage> ecs:continue_deployment[hook-id] # approve a paused lifecycle hook
|
|
247
|
+
bundle exec cap <stage> ecs:rollback_deployment[hook-id] # reject a paused lifecycle hook (ECS rolls back)
|
|
248
|
+
bundle exec cap <stage> ecs:stop_deployment[arn] # stop an in-progress deployment; STOP_TYPE=ABORT or ROLLBACK
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Caveats
|
|
252
|
+
|
|
253
|
+
- `deployment_controller` is immutable on an existing service. To switch an existing `CODE_DEPLOY` service to `ECS`, delete and re-create the service.
|
|
254
|
+
- The PAUSE lifecycle hook stages `TEST_TRAFFIC_SHIFT` and `PRODUCTION_TRAFFIC_SHIFT` are rejected by AWS (those stages are also entered during rollback). Use `PRE_*`/`POST_*` variants instead.
|
|
255
|
+
- For NLB, `advanced_configuration.production_listener_rule` should hold the Listener ARN directly (NLBs do not have Listener Rules).
|
|
256
|
+
- Pre-1.1 versions filtered the `set :ecs_services` hash to an allow-list of keys before calling `EcsDeploy::Service.new`. Starting with 1.1, the entire hash is forwarded. Any custom non-SDK keys previously placed there must be removed or renamed; otherwise the SDK will raise.
|
|
257
|
+
|
|
189
258
|
## Autoscaler
|
|
190
259
|
|
|
191
260
|
The autoscaler of `ecs_deploy` supports auto scaling of ECS services and clusters.
|
data/ecs_deploy.gemspec
CHANGED
|
@@ -22,12 +22,12 @@ Gem::Specification.new do |spec|
|
|
|
22
22
|
spec.add_runtime_dependency "aws-sdk-cloudwatch", "~> 1"
|
|
23
23
|
spec.add_runtime_dependency "aws-sdk-cloudwatchevents", "~> 1"
|
|
24
24
|
spec.add_runtime_dependency "aws-sdk-ec2", "~> 1"
|
|
25
|
-
spec.add_runtime_dependency "aws-sdk-ecs", "
|
|
25
|
+
spec.add_runtime_dependency "aws-sdk-ecs", ">= 1.238", "< 2"
|
|
26
26
|
spec.add_runtime_dependency "aws-sdk-sqs", "~> 1"
|
|
27
27
|
spec.add_runtime_dependency "terminal-table"
|
|
28
28
|
spec.add_runtime_dependency "paint"
|
|
29
29
|
|
|
30
|
-
spec.add_development_dependency "bundler", ">= 1.11"
|
|
30
|
+
spec.add_development_dependency "bundler", ">= 1.11"
|
|
31
31
|
spec.add_development_dependency "rake", ">= 10.0"
|
|
32
32
|
spec.add_development_dependency "rspec", "~> 3.0"
|
|
33
33
|
spec.add_development_dependency "rexml" # For aws-sdk-*
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "ecs_deploy/auto_scaler/config_base"
|
|
2
|
+
require "ecs_deploy/auto_scaler/null_cluster_resource_manager"
|
|
3
|
+
|
|
4
|
+
module EcsDeploy
|
|
5
|
+
module AutoScaler
|
|
6
|
+
SimpleScalingConfig = Struct.new(:name, :region, :cluster, :service_configs) do
|
|
7
|
+
include ConfigBase
|
|
8
|
+
|
|
9
|
+
def initialize(attributes = {}, logger)
|
|
10
|
+
attributes = attributes.dup
|
|
11
|
+
services = attributes.delete("services")
|
|
12
|
+
super(attributes, logger)
|
|
13
|
+
self.service_configs = services.map do |s|
|
|
14
|
+
ServiceConfig.new(s.merge("cluster" => cluster, "region" => region), logger)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def update_desired_capacity(required_capacity)
|
|
19
|
+
@logger.debug "#{log_prefix} Skipping infrastructure scaling (managed by capacity provider)"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def cluster_resource_manager
|
|
23
|
+
@cluster_resource_manager ||= NullClusterResourceManager.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def log_prefix
|
|
29
|
+
"[#{self.class.to_s.sub(/\AEcsDeploy::AutoScaler::/, "")} #{name} #{region}]"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -5,6 +5,7 @@ require "yaml"
|
|
|
5
5
|
require "ecs_deploy/auto_scaler/auto_scaling_group_config"
|
|
6
6
|
require "ecs_deploy/auto_scaler/instance_drainer"
|
|
7
7
|
require "ecs_deploy/auto_scaler/service_config"
|
|
8
|
+
require "ecs_deploy/auto_scaler/simple_scaling_config"
|
|
8
9
|
require "ecs_deploy/auto_scaler/spot_fleet_request_config"
|
|
9
10
|
|
|
10
11
|
module EcsDeploy
|
|
@@ -23,7 +24,7 @@ module EcsDeploy
|
|
|
23
24
|
STDERR.sync = true unless error_log_file
|
|
24
25
|
load_config(yaml_path)
|
|
25
26
|
|
|
26
|
-
ths = (auto_scaling_group_configs + spot_fleet_request_configs).map do |cluster_scaling_config|
|
|
27
|
+
ths = (auto_scaling_group_configs + spot_fleet_request_configs + simple_scaling_configs).map do |cluster_scaling_config|
|
|
27
28
|
Thread.new(cluster_scaling_config, &method(:main_loop)).tap { |th| th.abort_on_exception = true }
|
|
28
29
|
end
|
|
29
30
|
|
|
@@ -117,6 +118,16 @@ module EcsDeploy
|
|
|
117
118
|
end.values.flat_map(&:values)
|
|
118
119
|
end
|
|
119
120
|
|
|
121
|
+
def simple_scaling_configs
|
|
122
|
+
@simple_scaling_configs ||= (@config["simple_scaling"] || []).each.with_object({}) do |c, configs|
|
|
123
|
+
configs[c["name"]] ||= {}
|
|
124
|
+
if configs[c["name"]][c["region"]]
|
|
125
|
+
raise "Duplicate entry in simple_scaling (name: #{c["name"]}, region: #{c["region"]})"
|
|
126
|
+
end
|
|
127
|
+
configs[c["name"]][c["region"]] = SimpleScalingConfig.new(c, @logger)
|
|
128
|
+
end.values.flat_map(&:values)
|
|
129
|
+
end
|
|
130
|
+
|
|
120
131
|
private
|
|
121
132
|
|
|
122
133
|
def setup_signal_handlers
|
|
@@ -96,36 +96,72 @@ namespace :ecs do
|
|
|
96
96
|
next unless fetch(:target_task_definition).include?(service[:task_definition_name])
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
-
service_options =
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
load_balancers: service[:load_balancers],
|
|
105
|
-
desired_count: service[:desired_count],
|
|
106
|
-
launch_type: service[:launch_type],
|
|
107
|
-
network_configuration: service[:network_configuration],
|
|
108
|
-
health_check_grace_period_seconds: service[:health_check_grace_period_seconds],
|
|
109
|
-
delete: service[:delete],
|
|
110
|
-
enable_ecs_managed_tags: service[:enable_ecs_managed_tags],
|
|
111
|
-
tags: service[:tags],
|
|
112
|
-
propagate_tags: service[:propagate_tags],
|
|
113
|
-
enable_execute_command: service[:enable_execute_command],
|
|
114
|
-
}
|
|
115
|
-
service_options[:deployment_configuration] = service[:deployment_configuration] if service[:deployment_configuration]
|
|
116
|
-
service_options[:placement_constraints] = service[:placement_constraints] if service[:placement_constraints]
|
|
117
|
-
service_options[:placement_strategy] = service[:placement_strategy] if service[:placement_strategy]
|
|
118
|
-
service_options[:capacity_provider_strategy] = service[:capacity_provider_strategy] if service[:capacity_provider_strategy]
|
|
119
|
-
service_options[:scheduling_strategy] = service[:scheduling_strategy] if service[:scheduling_strategy]
|
|
99
|
+
service_options = service.dup
|
|
100
|
+
service_options[:service_name] = service_options.delete(:name) if service_options.key?(:name)
|
|
101
|
+
service_options[:cluster] ||= fetch(:ecs_default_cluster)
|
|
102
|
+
service_options[:region] = r
|
|
103
|
+
|
|
120
104
|
s = EcsDeploy::Service.new(**service_options)
|
|
121
105
|
s.deploy
|
|
122
106
|
s
|
|
123
|
-
end
|
|
107
|
+
end.compact
|
|
124
108
|
EcsDeploy::Service.wait_all_running(services)
|
|
125
109
|
end
|
|
126
110
|
end
|
|
127
111
|
end
|
|
128
112
|
|
|
113
|
+
desc "Describe in-flight ECS service deployments for services in :ecs_services"
|
|
114
|
+
task describe_deployment: [:configure] do
|
|
115
|
+
if fetch(:ecs_services)
|
|
116
|
+
regions = Array(fetch(:ecs_region))
|
|
117
|
+
regions = [EcsDeploy.config.default_region] if regions.empty?
|
|
118
|
+
EcsDeploy::ServiceDeployment.describe(
|
|
119
|
+
services: fetch(:ecs_services),
|
|
120
|
+
regions: regions,
|
|
121
|
+
default_cluster: fetch(:ecs_default_cluster),
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
desc "Continue an ECS service deployment paused at a lifecycle hook"
|
|
127
|
+
task :continue_deployment, [:hook_id] => [:configure] do |_t, args|
|
|
128
|
+
raise "hook_id is required: cap ... ecs:continue_deployment[hook_id]" unless args[:hook_id]
|
|
129
|
+
regions = Array(fetch(:ecs_region))
|
|
130
|
+
regions = [EcsDeploy.config.default_region] if regions.empty?
|
|
131
|
+
EcsDeploy::ServiceDeployment.invoke_lifecycle_hook(
|
|
132
|
+
hook_id: args[:hook_id],
|
|
133
|
+
action: "CONTINUE",
|
|
134
|
+
services: fetch(:ecs_services),
|
|
135
|
+
regions: regions,
|
|
136
|
+
default_cluster: fetch(:ecs_default_cluster),
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
desc "Roll back an ECS service deployment paused at a lifecycle hook"
|
|
141
|
+
task :rollback_deployment, [:hook_id] => [:configure] do |_t, args|
|
|
142
|
+
raise "hook_id is required: cap ... ecs:rollback_deployment[hook_id]" unless args[:hook_id]
|
|
143
|
+
regions = Array(fetch(:ecs_region))
|
|
144
|
+
regions = [EcsDeploy.config.default_region] if regions.empty?
|
|
145
|
+
EcsDeploy::ServiceDeployment.invoke_lifecycle_hook(
|
|
146
|
+
hook_id: args[:hook_id],
|
|
147
|
+
action: "ROLLBACK",
|
|
148
|
+
services: fetch(:ecs_services),
|
|
149
|
+
regions: regions,
|
|
150
|
+
default_cluster: fetch(:ecs_default_cluster),
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
desc "Stop an in-progress ECS service deployment (STOP_TYPE env: ABORT or ROLLBACK)"
|
|
155
|
+
task :stop_deployment, [:service_deployment_arn] => [:configure] do |_t, args|
|
|
156
|
+
raise "service_deployment_arn is required: cap ... ecs:stop_deployment[arn]" unless args[:service_deployment_arn]
|
|
157
|
+
region = Array(fetch(:ecs_region)).first || EcsDeploy.config.default_region
|
|
158
|
+
EcsDeploy::ServiceDeployment.stop(
|
|
159
|
+
service_deployment_arn: args[:service_deployment_arn],
|
|
160
|
+
region: region,
|
|
161
|
+
stop_type: ENV["STOP_TYPE"],
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
|
|
129
165
|
task rollback: [:configure] do
|
|
130
166
|
if fetch(:ecs_services)
|
|
131
167
|
regions = Array(fetch(:ecs_region))
|
|
@@ -7,28 +7,21 @@ module EcsDeploy
|
|
|
7
7
|
|
|
8
8
|
attr_reader :cluster, :region, :schedule_rule_name
|
|
9
9
|
|
|
10
|
-
def initialize(
|
|
11
|
-
cluster:, rule_name:, schedule_expression:, enabled: true, description: nil, target_id: nil,
|
|
12
|
-
task_definition_name:, revision: nil, task_count: nil, role_arn:, network_configuration: nil, launch_type: nil, platform_version: nil, group: nil,
|
|
13
|
-
region: nil, container_overrides: nil
|
|
14
|
-
)
|
|
10
|
+
def initialize(cluster:, rule_name:, schedule_expression:, task_definition_name:, role_arn:, region: nil, **options)
|
|
15
11
|
@cluster = cluster
|
|
16
12
|
@rule_name = rule_name
|
|
17
13
|
@schedule_expression = schedule_expression
|
|
18
|
-
@enabled = enabled
|
|
19
|
-
@description = description
|
|
20
|
-
@target_id = target_id || task_definition_name
|
|
21
14
|
@task_definition_name = task_definition_name
|
|
22
|
-
@task_count = task_count || 1
|
|
23
|
-
@revision = revision
|
|
24
15
|
@role_arn = role_arn
|
|
25
|
-
|
|
26
|
-
@
|
|
27
|
-
@
|
|
28
|
-
@
|
|
16
|
+
|
|
17
|
+
@options = options.dup
|
|
18
|
+
@options[:enabled] = @options.fetch(:enabled, true)
|
|
19
|
+
@options[:target_id] ||= task_definition_name
|
|
20
|
+
@options[:task_count] ||= 1
|
|
21
|
+
@options[:launch_type] ||= "EC2"
|
|
22
|
+
|
|
29
23
|
region ||= EcsDeploy.config.default_region
|
|
30
24
|
params ||= EcsDeploy.config.ecs_client_params
|
|
31
|
-
@container_overrides = container_overrides
|
|
32
25
|
|
|
33
26
|
@client = region ? Aws::ECS::Client.new(params.merge(region: region)) : Aws::ECS::Client.new(params)
|
|
34
27
|
@region = @client.config.region
|
|
@@ -50,7 +43,7 @@ module EcsDeploy
|
|
|
50
43
|
end
|
|
51
44
|
|
|
52
45
|
def task_definition_arn
|
|
53
|
-
suffix = @revision ? ":#{@revision}" : ""
|
|
46
|
+
suffix = @options[:revision] ? ":#{@options[:revision]}" : ""
|
|
54
47
|
name = "#{@task_definition_name}#{suffix}"
|
|
55
48
|
@client.describe_task_definition(task_definition: name).task_definition.task_definition_arn
|
|
56
49
|
end
|
|
@@ -59,30 +52,25 @@ module EcsDeploy
|
|
|
59
52
|
res = @cloud_watch_events.put_rule(
|
|
60
53
|
name: @rule_name,
|
|
61
54
|
schedule_expression: @schedule_expression,
|
|
62
|
-
state: @enabled ? "ENABLED" : "DISABLED",
|
|
63
|
-
description: @description,
|
|
55
|
+
state: @options[:enabled] ? "ENABLED" : "DISABLED",
|
|
56
|
+
description: @options[:description],
|
|
64
57
|
)
|
|
65
58
|
EcsDeploy.logger.info "created cloudwatch event rule [#{res.rule_arn}] [#{@region}] [#{Paint['OK', :green]}]"
|
|
66
59
|
end
|
|
67
60
|
|
|
68
61
|
def put_targets
|
|
69
62
|
target = {
|
|
70
|
-
id: @target_id,
|
|
63
|
+
id: @options[:target_id],
|
|
71
64
|
arn: cluster_arn,
|
|
72
65
|
role_arn: @role_arn,
|
|
73
|
-
ecs_parameters:
|
|
66
|
+
ecs_parameters: @options.except(:enabled, :description, :target_id, :revision, :container_overrides).merge(
|
|
74
67
|
task_definition_arn: task_definition_arn,
|
|
75
|
-
|
|
76
|
-
network_configuration: @network_configuration,
|
|
77
|
-
launch_type: @launch_type,
|
|
78
|
-
platform_version: @platform_version,
|
|
79
|
-
group: @group,
|
|
80
|
-
},
|
|
68
|
+
),
|
|
81
69
|
}
|
|
82
70
|
target[:ecs_parameters].compact!
|
|
83
71
|
|
|
84
|
-
if @container_overrides
|
|
85
|
-
target.merge!(input: { containerOverrides: @container_overrides }.to_json)
|
|
72
|
+
if @options[:container_overrides]
|
|
73
|
+
target.merge!(input: { containerOverrides: @options[:container_overrides] }.to_json)
|
|
86
74
|
end
|
|
87
75
|
|
|
88
76
|
res = @cloud_watch_events.put_targets(
|
|
@@ -90,7 +78,7 @@ module EcsDeploy
|
|
|
90
78
|
targets: [target]
|
|
91
79
|
)
|
|
92
80
|
if res.failed_entry_count.zero?
|
|
93
|
-
EcsDeploy.logger.info "created cloudwatch event target [#{@target_id}] [#{@region}] [#{Paint['OK', :green]}]"
|
|
81
|
+
EcsDeploy.logger.info "created cloudwatch event target [#{@options[:target_id]}] [#{@region}] [#{Paint['OK', :green]}]"
|
|
94
82
|
else
|
|
95
83
|
res.failed_entries.each do |entry|
|
|
96
84
|
EcsDeploy.logger.error "failed to create cloudwatch event target [#{@region}] target_id=#{entry.target_id} error_code=#{entry.error_code} error_message=#{entry.error_message}"
|
data/lib/ecs_deploy/service.rb
CHANGED
|
@@ -7,44 +7,38 @@ module EcsDeploy
|
|
|
7
7
|
|
|
8
8
|
class TooManyAttemptsError < StandardError; end
|
|
9
9
|
|
|
10
|
-
attr_reader :cluster, :region, :service_name, :delete, :deploy_started_at
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
tags: nil,
|
|
25
|
-
propagate_tags: nil,
|
|
26
|
-
region: nil,
|
|
27
|
-
delete: false,
|
|
28
|
-
enable_execute_command: false
|
|
29
|
-
)
|
|
10
|
+
attr_reader :cluster, :region, :service_name, :delete, :deploy_started_at, :wait_strategy
|
|
11
|
+
|
|
12
|
+
# Options that Aws::ECS::Client#update_service will not honor on an existing
|
|
13
|
+
# service:
|
|
14
|
+
# - launch_type / scheduling_strategy: not in update_service's parameter list
|
|
15
|
+
# - role / client_token: create-only, no update-side equivalent
|
|
16
|
+
# - deployment_controller: accepted by update_service in aws-sdk-ecs 1.238+
|
|
17
|
+
# but AWS rejects any change to the controller type at runtime
|
|
18
|
+
CREATE_ONLY_KEYS = %i[launch_type scheduling_strategy role client_token deployment_controller].freeze
|
|
19
|
+
|
|
20
|
+
VALID_WAIT_STRATEGIES = %i[legacy none service_deployment].freeze
|
|
21
|
+
ECS_NATIVE_BLUE_GREEN_STRATEGIES = %w[BLUE_GREEN LINEAR CANARY].freeze
|
|
22
|
+
|
|
23
|
+
def initialize(cluster:, service_name:, region: nil, **options)
|
|
30
24
|
@cluster = cluster
|
|
31
25
|
@service_name = service_name
|
|
32
|
-
@
|
|
33
|
-
@
|
|
34
|
-
@
|
|
35
|
-
@
|
|
36
|
-
@
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@
|
|
40
|
-
@
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@
|
|
44
|
-
@
|
|
45
|
-
@
|
|
46
|
-
@
|
|
47
|
-
@enable_execute_command
|
|
26
|
+
@options = options.dup
|
|
27
|
+
@task_definition_name = @options.delete(:task_definition_name) || service_name
|
|
28
|
+
@revision = @options.delete(:revision)
|
|
29
|
+
@delete = @options.delete(:delete) || false
|
|
30
|
+
@wait_strategy = @options.delete(:wait_strategy)
|
|
31
|
+
# Snapshot the keys the user actually passed in, so warnings only fire on
|
|
32
|
+
# options the caller explicitly set (not on defaults injected below).
|
|
33
|
+
@user_provided_keys = (options.keys - %i[task_definition_name revision delete wait_strategy]).freeze
|
|
34
|
+
if @wait_strategy && !VALID_WAIT_STRATEGIES.include?(@wait_strategy)
|
|
35
|
+
raise ArgumentError, "Invalid wait_strategy #{@wait_strategy.inspect}, expected nil or one of #{VALID_WAIT_STRATEGIES.inspect}"
|
|
36
|
+
end
|
|
37
|
+
@options[:deployment_configuration] ||= {maximum_percent: 200, minimum_healthy_percent: 100}
|
|
38
|
+
@options[:placement_constraints] ||= []
|
|
39
|
+
@options[:placement_strategy] ||= []
|
|
40
|
+
@options[:scheduling_strategy] ||= 'REPLICA'
|
|
41
|
+
@options[:enable_execute_command] ||= false
|
|
48
42
|
|
|
49
43
|
@response = nil
|
|
50
44
|
|
|
@@ -52,8 +46,6 @@ module EcsDeploy
|
|
|
52
46
|
params ||= EcsDeploy.config.ecs_client_params
|
|
53
47
|
@client = region ? Aws::ECS::Client.new(params.merge(region: region)) : Aws::ECS::Client.new(params)
|
|
54
48
|
@region = @client.config.region
|
|
55
|
-
|
|
56
|
-
@delete = delete
|
|
57
49
|
end
|
|
58
50
|
|
|
59
51
|
def current_task_definition_arn
|
|
@@ -64,75 +56,100 @@ module EcsDeploy
|
|
|
64
56
|
def deploy
|
|
65
57
|
@deploy_started_at = Time.now
|
|
66
58
|
res = @client.describe_services(cluster: @cluster, services: [@service_name])
|
|
67
|
-
|
|
59
|
+
|
|
60
|
+
if res.services.select{ |s| s.status == 'ACTIVE' }.empty?
|
|
61
|
+
return if @delete
|
|
62
|
+
create_service
|
|
63
|
+
else
|
|
64
|
+
return delete_service if @delete
|
|
65
|
+
update_service(res.services[0])
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private def create_service
|
|
70
|
+
service_options = @options.merge(
|
|
68
71
|
cluster: @cluster,
|
|
72
|
+
service_name: @service_name,
|
|
69
73
|
task_definition: task_definition_name_with_revision,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
health_check_grace_period_seconds: @health_check_grace_period_seconds,
|
|
73
|
-
capacity_provider_strategy: @capacity_provider_strategy,
|
|
74
|
-
enable_execute_command: @enable_execute_command,
|
|
75
|
-
enable_ecs_managed_tags: @enable_ecs_managed_tags,
|
|
76
|
-
placement_constraints: @placement_constraints,
|
|
77
|
-
placement_strategy: @placement_strategy,
|
|
78
|
-
}
|
|
74
|
+
)
|
|
75
|
+
service_options[:desired_count] = service_options[:desired_count].to_i
|
|
79
76
|
|
|
80
|
-
if
|
|
81
|
-
service_options.
|
|
82
|
-
role: EcsDeploy.config.ecs_service_role,
|
|
83
|
-
})
|
|
77
|
+
if service_options[:load_balancers] && EcsDeploy.config.ecs_service_role
|
|
78
|
+
service_options[:role] = EcsDeploy.config.ecs_service_role
|
|
84
79
|
end
|
|
85
80
|
|
|
86
|
-
if
|
|
87
|
-
service_options.
|
|
88
|
-
|
|
89
|
-
})
|
|
81
|
+
if service_options[:scheduling_strategy] == 'DAEMON'
|
|
82
|
+
service_options.delete(:desired_count)
|
|
83
|
+
service_options.delete(:placement_strategy)
|
|
90
84
|
end
|
|
91
85
|
|
|
92
|
-
|
|
93
|
-
|
|
86
|
+
@response = @client.create_service(service_options)
|
|
87
|
+
EcsDeploy.logger.info "created service [#{@service_name}] [#{@cluster}] [#{@region}] [#{Paint['OK', :green]}]"
|
|
88
|
+
end
|
|
94
89
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
desired_count: @desired_count.to_i,
|
|
98
|
-
launch_type: @launch_type,
|
|
99
|
-
tags: @tags,
|
|
100
|
-
propagate_tags: @propagate_tags,
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
if @scheduling_strategy == 'DAEMON'
|
|
104
|
-
service_options[:scheduling_strategy] = @scheduling_strategy
|
|
105
|
-
service_options.delete(:desired_count)
|
|
106
|
-
service_options.delete(:placement_strategy)
|
|
107
|
-
end
|
|
108
|
-
@response = @client.create_service(service_options)
|
|
109
|
-
EcsDeploy.logger.info "created service [#{@service_name}] [#{@cluster}] [#{@region}] [#{Paint['OK', :green]}]"
|
|
110
|
-
else
|
|
111
|
-
return delete_service if @delete
|
|
90
|
+
private def update_service(current_service)
|
|
91
|
+
warn_on_ignored_options(current_service)
|
|
112
92
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
93
|
+
service_options = @options.except(*CREATE_ONLY_KEYS, :tags).merge(
|
|
94
|
+
cluster: @cluster,
|
|
95
|
+
service: @service_name,
|
|
96
|
+
task_definition: task_definition_name_with_revision,
|
|
97
|
+
)
|
|
98
|
+
# If the user did not set these explicitly, leave them out so ECS keeps
|
|
99
|
+
# its current values (desired_count is often managed by autoscaling;
|
|
100
|
+
# propagate_tags reflects an existing policy).
|
|
101
|
+
service_options.delete(:desired_count) unless @options.key?(:desired_count)
|
|
102
|
+
service_options.delete(:propagate_tags) unless @options.key?(:propagate_tags)
|
|
103
|
+
service_options[:force_new_deployment] = true if need_force_new_deployment?(current_service)
|
|
104
|
+
service_options.delete(:placement_strategy) if @options[:scheduling_strategy] == 'DAEMON'
|
|
116
105
|
|
|
117
|
-
|
|
118
|
-
service_options.merge!({force_new_deployment: true}) if need_force_new_deployment?(current_service)
|
|
106
|
+
update_tags(@service_name, @options[:tags])
|
|
119
107
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
108
|
+
@response = @client.update_service(service_options)
|
|
109
|
+
EcsDeploy.logger.info "updated service [#{@service_name}] [#{@cluster}] [#{@region}] [#{Paint['OK', :green]}]"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Log a warning for user-supplied options that update_service cannot apply.
|
|
113
|
+
# Silently drop keys whose value matches the current service (harmless
|
|
114
|
+
# re-declaration of the current state); warn only when the user's value
|
|
115
|
+
# would actually change something.
|
|
116
|
+
private def warn_on_ignored_options(current_service)
|
|
117
|
+
CREATE_ONLY_KEYS.each do |key|
|
|
118
|
+
next unless @user_provided_keys.include?(key)
|
|
119
|
+
next if create_only_matches_current?(key, current_service)
|
|
120
|
+
EcsDeploy.logger.warn "[#{@service_name}] option #{key.inspect} cannot be applied by update_service (current: #{create_only_current_display(current_service, key).inspect}, requested: #{@options[key].inspect}), skipping"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private def create_only_matches_current?(key, current_service)
|
|
125
|
+
case key
|
|
126
|
+
when :launch_type
|
|
127
|
+
@options[key].to_s == current_service.launch_type.to_s
|
|
128
|
+
when :scheduling_strategy
|
|
129
|
+
@options[key].to_s == current_service.scheduling_strategy.to_s
|
|
130
|
+
when :deployment_controller
|
|
131
|
+
Hash(@options[key])[:type].to_s == current_service.deployment_controller&.type.to_s
|
|
132
|
+
else
|
|
133
|
+
# role / client_token have no meaningful "current" comparison; always warn.
|
|
134
|
+
false
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private def create_only_current_display(current_service, key)
|
|
139
|
+
case key
|
|
140
|
+
when :launch_type then current_service.launch_type
|
|
141
|
+
when :scheduling_strategy then current_service.scheduling_strategy
|
|
142
|
+
when :deployment_controller then current_service.deployment_controller&.type
|
|
126
143
|
end
|
|
127
144
|
end
|
|
128
145
|
|
|
129
146
|
private def need_force_new_deployment?(service)
|
|
130
|
-
return false unless @capacity_provider_strategy
|
|
147
|
+
return false unless @options[:capacity_provider_strategy]
|
|
131
148
|
return true unless service.capacity_provider_strategy
|
|
132
149
|
|
|
133
|
-
return true if @capacity_provider_strategy.size != service.capacity_provider_strategy.size
|
|
150
|
+
return true if @options[:capacity_provider_strategy].size != service.capacity_provider_strategy.size
|
|
134
151
|
|
|
135
|
-
match_array = @capacity_provider_strategy.all? do |strategy|
|
|
152
|
+
match_array = @options[:capacity_provider_strategy].all? do |strategy|
|
|
136
153
|
service.capacity_provider_strategy.find do |current_strategy|
|
|
137
154
|
strategy[:capacity_provider] == current_strategy.capacity_provider &&
|
|
138
155
|
strategy[:weight] == current_strategy.weight &&
|
|
@@ -144,7 +161,7 @@ module EcsDeploy
|
|
|
144
161
|
end
|
|
145
162
|
|
|
146
163
|
def delete_service
|
|
147
|
-
if @scheduling_strategy != 'DAEMON'
|
|
164
|
+
if @options[:scheduling_strategy] != 'DAEMON'
|
|
148
165
|
@client.update_service(cluster: @cluster, service: @service_name, desired_count: 0)
|
|
149
166
|
sleep 1
|
|
150
167
|
end
|
|
@@ -184,30 +201,88 @@ module EcsDeploy
|
|
|
184
201
|
end
|
|
185
202
|
end
|
|
186
203
|
|
|
204
|
+
def skip_wait?
|
|
205
|
+
case @wait_strategy
|
|
206
|
+
when :none
|
|
207
|
+
true
|
|
208
|
+
when :legacy, :service_deployment
|
|
209
|
+
false
|
|
210
|
+
when nil
|
|
211
|
+
ecs_native_blue_green?
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
private def ecs_native_blue_green?
|
|
216
|
+
svc = @response&.service
|
|
217
|
+
return false unless svc&.deployment_controller&.type == "ECS"
|
|
218
|
+
strategy = svc.deployment_configuration&.strategy.to_s
|
|
219
|
+
ECS_NATIVE_BLUE_GREEN_STRATEGIES.include?(strategy)
|
|
220
|
+
end
|
|
221
|
+
|
|
187
222
|
def self.wait_all_running(services)
|
|
188
|
-
services.group_by { |s| [s.cluster, s.region] }.flat_map do |(cl, region), ss|
|
|
189
|
-
params
|
|
223
|
+
threads = services.group_by { |s| [s.cluster, s.region] }.flat_map do |(cl, region), ss|
|
|
224
|
+
params = EcsDeploy.config.ecs_client_params
|
|
190
225
|
client = Aws::ECS::Client.new(params.merge(region: region))
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
226
|
+
|
|
227
|
+
targets = ss.reject(&:delete).reject do |s|
|
|
228
|
+
if s.skip_wait?
|
|
229
|
+
EcsDeploy.logger.info "skip waiting for service [#{s.service_name}] [#{cl}]: ECS-managed deployment, monitor via ecs:describe_deployment"
|
|
230
|
+
true
|
|
231
|
+
else
|
|
232
|
+
false
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
legacy_targets = targets.reject { |s| s.wait_strategy == :service_deployment }
|
|
237
|
+
sd_targets = targets.select { |s| s.wait_strategy == :service_deployment }
|
|
238
|
+
|
|
239
|
+
legacy_threads(client, cl, ss, legacy_targets) + service_deployment_threads(client, cl, sd_targets)
|
|
240
|
+
end
|
|
241
|
+
threads.each(&:join)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def self.legacy_threads(client, cluster, all_services, targets)
|
|
245
|
+
targets.map(&:service_name).each_slice(MAX_DESCRIBE_SERVICES).map do |chunked_service_names|
|
|
246
|
+
Thread.new do
|
|
247
|
+
EcsDeploy.config.ecs_wait_until_services_stable_max_attempts.times do
|
|
248
|
+
EcsDeploy.logger.info "waiting for services to stabilize [#{chunked_service_names.join(", ")}] [#{cluster}]"
|
|
249
|
+
resp = client.describe_services(cluster: cluster, services: chunked_service_names)
|
|
250
|
+
resp.services.each do |s|
|
|
251
|
+
# cf. https://github.com/aws/aws-sdk-ruby/blob/master/gems/aws-sdk-ecs/lib/aws-sdk-ecs/waiters.rb#L91-L96
|
|
252
|
+
if s.deployments.size == 1 && s.running_count == s.desired_count
|
|
253
|
+
chunked_service_names.delete(s.service_name)
|
|
203
254
|
end
|
|
204
|
-
|
|
205
|
-
|
|
255
|
+
service = all_services.detect { |sc| sc.service_name == s.service_name }
|
|
256
|
+
service&.log_events(s)
|
|
257
|
+
end
|
|
258
|
+
break if chunked_service_names.empty?
|
|
259
|
+
sleep EcsDeploy.config.ecs_wait_until_services_stable_delay
|
|
260
|
+
end
|
|
261
|
+
raise TooManyAttemptsError unless chunked_service_names.empty?
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def self.service_deployment_threads(client, cluster, targets)
|
|
267
|
+
targets.map do |service|
|
|
268
|
+
Thread.new do
|
|
269
|
+
pending = true
|
|
270
|
+
EcsDeploy.config.ecs_wait_until_services_stable_max_attempts.times do
|
|
271
|
+
EcsDeploy.logger.info "waiting for service deployment to settle [#{service.service_name}] [#{cluster}]"
|
|
272
|
+
arns = client.list_service_deployments(
|
|
273
|
+
cluster: cluster,
|
|
274
|
+
service: service.service_name,
|
|
275
|
+
status: %w[IN_PROGRESS PENDING],
|
|
276
|
+
).service_deployments.map(&:service_deployment_arn)
|
|
277
|
+
if arns.empty?
|
|
278
|
+
pending = false
|
|
279
|
+
break
|
|
206
280
|
end
|
|
207
|
-
|
|
281
|
+
sleep EcsDeploy.config.ecs_wait_until_services_stable_delay
|
|
208
282
|
end
|
|
283
|
+
raise TooManyAttemptsError if pending
|
|
209
284
|
end
|
|
210
|
-
end
|
|
285
|
+
end
|
|
211
286
|
end
|
|
212
287
|
|
|
213
288
|
private
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module EcsDeploy
|
|
2
|
+
module ServiceDeployment
|
|
3
|
+
IN_FLIGHT_STATUSES = %w[IN_PROGRESS PENDING].freeze
|
|
4
|
+
DESCRIBE_STATUSES = %w[IN_PROGRESS PENDING STOPPED].freeze
|
|
5
|
+
|
|
6
|
+
class HookNotFoundError < StandardError; end
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def describe(services:, regions:, default_cluster:)
|
|
11
|
+
each_target(services: services, regions: regions, default_cluster: default_cluster) do |client, cluster, svc|
|
|
12
|
+
deployments = list_deployments(client, cluster, svc[:name], statuses: DESCRIBE_STATUSES)
|
|
13
|
+
next if deployments.empty?
|
|
14
|
+
deployments.each { |d| log_deployment(d) }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def invoke_lifecycle_hook(hook_id:, action:, services:, regions:, default_cluster:)
|
|
19
|
+
found = false
|
|
20
|
+
each_target(services: services, regions: regions, default_cluster: default_cluster) do |client, cluster, svc|
|
|
21
|
+
deployments = list_deployments(client, cluster, svc[:name], statuses: IN_FLIGHT_STATUSES)
|
|
22
|
+
deployments.each do |d|
|
|
23
|
+
next unless Array(d.lifecycle_hook_details).any? { |h| h.hook_id == hook_id }
|
|
24
|
+
client.continue_service_deployment(
|
|
25
|
+
service_deployment_arn: d.service_deployment_arn,
|
|
26
|
+
hook_id: hook_id,
|
|
27
|
+
action: action,
|
|
28
|
+
)
|
|
29
|
+
EcsDeploy.logger.info "#{action.downcase}d lifecycle_hook=#{hook_id} service_deployment_arn=#{d.service_deployment_arn}"
|
|
30
|
+
found = true
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
raise HookNotFoundError, "Lifecycle hook #{hook_id} not found in any in-progress service deployment" unless found
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def stop(service_deployment_arn:, region:, stop_type: nil)
|
|
37
|
+
client = Aws::ECS::Client.new(EcsDeploy.config.ecs_client_params.merge(region: region))
|
|
38
|
+
params = { service_deployment_arn: service_deployment_arn }
|
|
39
|
+
params[:stop_type] = stop_type if stop_type
|
|
40
|
+
client.stop_service_deployment(params)
|
|
41
|
+
EcsDeploy.logger.info "stopped service_deployment_arn=#{service_deployment_arn}#{stop_type ? " stop_type=#{stop_type}" : ""}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def each_target(services:, regions:, default_cluster:)
|
|
45
|
+
services.each do |svc|
|
|
46
|
+
cluster = svc[:cluster] || default_cluster
|
|
47
|
+
regions.each do |r|
|
|
48
|
+
client = Aws::ECS::Client.new(EcsDeploy.config.ecs_client_params.merge(region: r))
|
|
49
|
+
yield client, cluster, svc
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def list_deployments(client, cluster, service_name, statuses:)
|
|
55
|
+
arns = client.list_service_deployments(
|
|
56
|
+
cluster: cluster,
|
|
57
|
+
service: service_name,
|
|
58
|
+
status: statuses,
|
|
59
|
+
).service_deployments.map(&:service_deployment_arn)
|
|
60
|
+
return [] if arns.empty?
|
|
61
|
+
client.describe_service_deployments(service_deployment_arns: arns).service_deployments
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def log_deployment(d)
|
|
65
|
+
EcsDeploy.logger.info "service_deployment_arn=#{d.service_deployment_arn} status=#{d.status} lifecycle_stage=#{d.lifecycle_stage}"
|
|
66
|
+
Array(d.lifecycle_hook_details).each do |hook|
|
|
67
|
+
EcsDeploy.logger.info " hook_id=#{hook.hook_id} target=#{hook.target_type} status=#{hook.status} expires_at=#{hook.expires_at} timeout_action=#{hook.timeout_action}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -10,23 +10,19 @@ module EcsDeploy
|
|
|
10
10
|
EcsDeploy.logger.info "deregistered task definition [#{arn}] [#{client.config.region}] [#{Paint['OK', :green]}]"
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
def initialize(
|
|
14
|
-
task_definition_name:, region: nil,
|
|
15
|
-
network_mode: "bridge", volumes: [], container_definitions: [], placement_constraints: [],
|
|
16
|
-
task_role_arn: nil,
|
|
17
|
-
execution_role_arn: nil,
|
|
18
|
-
requires_compatibilities: nil,
|
|
19
|
-
cpu: nil, memory: nil,
|
|
20
|
-
tags: nil,
|
|
21
|
-
runtime_platform: {}
|
|
22
|
-
)
|
|
13
|
+
def initialize(task_definition_name:, region: nil, **options)
|
|
23
14
|
@task_definition_name = task_definition_name
|
|
24
|
-
@task_role_arn = task_role_arn
|
|
25
|
-
@execution_role_arn = execution_role_arn
|
|
26
15
|
region ||= EcsDeploy.config.default_region
|
|
27
16
|
params ||= EcsDeploy.config.ecs_client_params
|
|
28
17
|
|
|
29
|
-
@
|
|
18
|
+
@options = options.dup
|
|
19
|
+
@options[:network_mode] ||= "bridge"
|
|
20
|
+
@options[:volumes] ||= []
|
|
21
|
+
@options[:container_definitions] ||= []
|
|
22
|
+
@options[:placement_constraints] ||= []
|
|
23
|
+
@options[:runtime_platform] ||= {}
|
|
24
|
+
|
|
25
|
+
@options[:container_definitions] = @options[:container_definitions].map do |cd|
|
|
30
26
|
if cd[:docker_labels]
|
|
31
27
|
cd[:docker_labels] = cd[:docker_labels].map { |k, v| [k.to_s, v] }.to_h
|
|
32
28
|
end
|
|
@@ -35,16 +31,11 @@ module EcsDeploy
|
|
|
35
31
|
end
|
|
36
32
|
cd
|
|
37
33
|
end
|
|
38
|
-
@
|
|
39
|
-
@
|
|
40
|
-
|
|
41
|
-
@requires_compatibilities = requires_compatibilities
|
|
42
|
-
@cpu = cpu&.to_s
|
|
43
|
-
@memory = memory&.to_s
|
|
44
|
-
@tags = tags
|
|
34
|
+
@options[:cpu] = @options[:cpu]&.to_s
|
|
35
|
+
@options[:memory] = @options[:memory]&.to_s
|
|
36
|
+
|
|
45
37
|
@client = region ? Aws::ECS::Client.new(params.merge(region: region)) : Aws::ECS::Client.new(params)
|
|
46
38
|
@region = @client.config.region
|
|
47
|
-
@runtime_platform = runtime_platform
|
|
48
39
|
end
|
|
49
40
|
|
|
50
41
|
def recent_task_definition_arns
|
|
@@ -58,19 +49,9 @@ module EcsDeploy
|
|
|
58
49
|
end
|
|
59
50
|
|
|
60
51
|
def register
|
|
61
|
-
res = @client.register_task_definition(
|
|
62
|
-
family: @task_definition_name
|
|
63
|
-
|
|
64
|
-
container_definitions: @container_definitions,
|
|
65
|
-
volumes: @volumes,
|
|
66
|
-
placement_constraints: @placement_constraints,
|
|
67
|
-
task_role_arn: @task_role_arn,
|
|
68
|
-
execution_role_arn: @execution_role_arn,
|
|
69
|
-
requires_compatibilities: @requires_compatibilities,
|
|
70
|
-
cpu: @cpu, memory: @memory,
|
|
71
|
-
tags: @tags,
|
|
72
|
-
runtime_platform: @runtime_platform
|
|
73
|
-
})
|
|
52
|
+
res = @client.register_task_definition(
|
|
53
|
+
@options.merge(family: @task_definition_name)
|
|
54
|
+
)
|
|
74
55
|
EcsDeploy.logger.info "registered task definition [#{@task_definition_name}] [#{@region}] [#{Paint['OK', :green]}]"
|
|
75
56
|
res.task_definition
|
|
76
57
|
end
|
data/lib/ecs_deploy/version.rb
CHANGED
data/lib/ecs_deploy.rb
CHANGED
data/renovate.json
ADDED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ecs_deploy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- joker1007
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: aws-sdk-autoscaling
|
|
@@ -70,16 +69,22 @@ dependencies:
|
|
|
70
69
|
name: aws-sdk-ecs
|
|
71
70
|
requirement: !ruby/object:Gem::Requirement
|
|
72
71
|
requirements:
|
|
73
|
-
- - "
|
|
72
|
+
- - ">="
|
|
74
73
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '1'
|
|
74
|
+
version: '1.238'
|
|
75
|
+
- - "<"
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '2'
|
|
76
78
|
type: :runtime
|
|
77
79
|
prerelease: false
|
|
78
80
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
81
|
requirements:
|
|
80
|
-
- - "
|
|
82
|
+
- - ">="
|
|
81
83
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '1'
|
|
84
|
+
version: '1.238'
|
|
85
|
+
- - "<"
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '2'
|
|
83
88
|
- !ruby/object:Gem::Dependency
|
|
84
89
|
name: aws-sdk-sqs
|
|
85
90
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -129,9 +134,6 @@ dependencies:
|
|
|
129
134
|
- - ">="
|
|
130
135
|
- !ruby/object:Gem::Version
|
|
131
136
|
version: '1.11'
|
|
132
|
-
- - "<"
|
|
133
|
-
- !ruby/object:Gem::Version
|
|
134
|
-
version: '3'
|
|
135
137
|
type: :development
|
|
136
138
|
prerelease: false
|
|
137
139
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -139,9 +141,6 @@ dependencies:
|
|
|
139
141
|
- - ">="
|
|
140
142
|
- !ruby/object:Gem::Version
|
|
141
143
|
version: '1.11'
|
|
142
|
-
- - "<"
|
|
143
|
-
- !ruby/object:Gem::Version
|
|
144
|
-
version: '3'
|
|
145
144
|
- !ruby/object:Gem::Dependency
|
|
146
145
|
name: rake
|
|
147
146
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -194,6 +193,7 @@ extra_rdoc_files: []
|
|
|
194
193
|
files:
|
|
195
194
|
- ".github/workflows/test.yml"
|
|
196
195
|
- ".gitignore"
|
|
196
|
+
- ".rspec"
|
|
197
197
|
- CHANGELOG.md
|
|
198
198
|
- Gemfile
|
|
199
199
|
- README.md
|
|
@@ -208,7 +208,9 @@ files:
|
|
|
208
208
|
- lib/ecs_deploy/auto_scaler/cluster_resource_manager.rb
|
|
209
209
|
- lib/ecs_deploy/auto_scaler/config_base.rb
|
|
210
210
|
- lib/ecs_deploy/auto_scaler/instance_drainer.rb
|
|
211
|
+
- lib/ecs_deploy/auto_scaler/null_cluster_resource_manager.rb
|
|
211
212
|
- lib/ecs_deploy/auto_scaler/service_config.rb
|
|
213
|
+
- lib/ecs_deploy/auto_scaler/simple_scaling_config.rb
|
|
212
214
|
- lib/ecs_deploy/auto_scaler/spot_fleet_request_config.rb
|
|
213
215
|
- lib/ecs_deploy/auto_scaler/trigger_config.rb
|
|
214
216
|
- lib/ecs_deploy/capistrano.rb
|
|
@@ -216,12 +218,13 @@ files:
|
|
|
216
218
|
- lib/ecs_deploy/instance_fluctuation_manager.rb
|
|
217
219
|
- lib/ecs_deploy/scheduled_task.rb
|
|
218
220
|
- lib/ecs_deploy/service.rb
|
|
221
|
+
- lib/ecs_deploy/service_deployment.rb
|
|
219
222
|
- lib/ecs_deploy/task_definition.rb
|
|
220
223
|
- lib/ecs_deploy/version.rb
|
|
224
|
+
- renovate.json
|
|
221
225
|
homepage: https://github.com/reproio/ecs_deploy
|
|
222
226
|
licenses: []
|
|
223
227
|
metadata: {}
|
|
224
|
-
post_install_message:
|
|
225
228
|
rdoc_options: []
|
|
226
229
|
require_paths:
|
|
227
230
|
- lib
|
|
@@ -236,8 +239,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
236
239
|
- !ruby/object:Gem::Version
|
|
237
240
|
version: '0'
|
|
238
241
|
requirements: []
|
|
239
|
-
rubygems_version:
|
|
240
|
-
signing_key:
|
|
242
|
+
rubygems_version: 4.0.6
|
|
241
243
|
specification_version: 4
|
|
242
244
|
summary: AWS ECS deploy helper
|
|
243
245
|
test_files: []
|