ecsutil 0.1.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 +7 -0
- data/.gitignore +18 -0
- data/README.md +118 -0
- data/Rakefile +9 -0
- data/bin/ecsutil +9 -0
- data/ecsutil.gemspec +22 -0
- data/lib/ecsutil.rb +6 -0
- data/lib/ecsutil/aws.rb +258 -0
- data/lib/ecsutil/command.rb +24 -0
- data/lib/ecsutil/commands/deploy.rb +70 -0
- data/lib/ecsutil/commands/destroy.rb +19 -0
- data/lib/ecsutil/commands/help.rb +14 -0
- data/lib/ecsutil/commands/init.rb +94 -0
- data/lib/ecsutil/commands/run.rb +47 -0
- data/lib/ecsutil/commands/scale.rb +24 -0
- data/lib/ecsutil/commands/secrets.rb +91 -0
- data/lib/ecsutil/commands/status.rb +41 -0
- data/lib/ecsutil/config.rb +60 -0
- data/lib/ecsutil/helpers.rb +53 -0
- data/lib/ecsutil/runner.rb +119 -0
- data/lib/ecsutil/shared.rb +67 -0
- data/lib/ecsutil/terraform.rb +24 -0
- data/lib/ecsutil/vault.rb +42 -0
- data/lib/ecsutil/version.rb +3 -0
- metadata +123 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
require "ecsutil/helpers"
|
2
|
+
require "ecsutil/aws"
|
3
|
+
require "ecsutil/vault"
|
4
|
+
require "ecsutil/shared"
|
5
|
+
|
6
|
+
module ECSUtil
|
7
|
+
module Commands
|
8
|
+
end
|
9
|
+
|
10
|
+
class Command
|
11
|
+
include ECSUtil::Helpers
|
12
|
+
include ECSUtil::AWS
|
13
|
+
include ECSUtil::Vault
|
14
|
+
include ECSUtil::Shared
|
15
|
+
|
16
|
+
attr_reader :config, :action, :args
|
17
|
+
|
18
|
+
def initialize(config, action = nil, args = [])
|
19
|
+
@config = config
|
20
|
+
@action = action
|
21
|
+
@args = args
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
class ECSUtil::Commands::DeployCommand < ECSUtil::Command
|
2
|
+
def run
|
3
|
+
confirm
|
4
|
+
|
5
|
+
load_active_task_definitions
|
6
|
+
load_secrets
|
7
|
+
load_services
|
8
|
+
|
9
|
+
register_tasks
|
10
|
+
register_scheduled_tasks
|
11
|
+
register_services
|
12
|
+
|
13
|
+
deregister_tasks
|
14
|
+
deregister_scheduled_tasks
|
15
|
+
deregister_services
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def register_tasks
|
21
|
+
config["tasks"].each_pair do |name, task_config|
|
22
|
+
step_info "Registering task definition: #{name}"
|
23
|
+
task_def = generate_task_definition(config, name)
|
24
|
+
result = register_task_definition(task_def)
|
25
|
+
arn = result["taskDefinitionArn"]
|
26
|
+
|
27
|
+
config["tasks"][name]["arn"] = arn
|
28
|
+
step_info "Registered #{name}: #{arn}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def register_scheduled_tasks
|
33
|
+
config["scheduled_tasks"].each_pair do |name, schedule|
|
34
|
+
step_info "Creating event rule for #{name}"
|
35
|
+
|
36
|
+
task = config["tasks"][schedule["task"]]
|
37
|
+
rule_name = sprintf("%s-%s-%s", config["app"], config["env"], name)
|
38
|
+
rule_exp = schedule["expression"]
|
39
|
+
|
40
|
+
rule_data = generate_event_rule(
|
41
|
+
name: rule_name,
|
42
|
+
expression: rule_exp,
|
43
|
+
enabled: schedule.key?("enabled") ? schedule["enabled"] == true : true
|
44
|
+
)
|
45
|
+
|
46
|
+
result = put_rule(rule_data)
|
47
|
+
config["scheduled_tasks"][name]["rule_name"] = rule_name
|
48
|
+
config["scheduled_tasks"][name]["rule_arn"] = result["RuleArn"]
|
49
|
+
|
50
|
+
step_info "Creating event target for #{name}"
|
51
|
+
rule_targets = generate_event_target(config, schedule["task"], name)
|
52
|
+
put_targets(rule_targets)
|
53
|
+
end
|
54
|
+
|
55
|
+
def register_services
|
56
|
+
config["services"].each_pair do |service_name, service|
|
57
|
+
full_name = sprintf("%s-%s-%s", config["app"], config["env"], service_name)
|
58
|
+
service["exists"] = @existing_services.include?(full_name)
|
59
|
+
|
60
|
+
if service["exists"]
|
61
|
+
step_info "Updating service #{service_name}"
|
62
|
+
update_service(config, service_name)
|
63
|
+
else
|
64
|
+
step_info "Creating service #{service_name}"
|
65
|
+
create_service(config, service_name)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class ECSUtil::Commands::DestroyCommand < ECSUtil::Command
|
2
|
+
def run
|
3
|
+
confirm "DANGER: You are about to deprovision all tasks and services! Proceed?"
|
4
|
+
confirm "Are you absolutely sure?"
|
5
|
+
|
6
|
+
config["tasks"] = {}
|
7
|
+
config["scheduled_tasks"] = {}
|
8
|
+
config["services"] = {}
|
9
|
+
|
10
|
+
load_active_task_definitions
|
11
|
+
load_secrets
|
12
|
+
load_services
|
13
|
+
|
14
|
+
deregister_tasks
|
15
|
+
deregister_scheduled_tasks
|
16
|
+
deregister_services
|
17
|
+
deregister_secrets
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class ECSUtil::Commands::HelpCommand < ECSUtil::Command
|
2
|
+
def run
|
3
|
+
puts [
|
4
|
+
"Usage: escutil <stage> <command>",
|
5
|
+
"Available commands:",
|
6
|
+
"* deploy - Perform a deployment",
|
7
|
+
"* run - Run a task",
|
8
|
+
"* scale - Change service quantities",
|
9
|
+
"* status - Show current status",
|
10
|
+
"* secrets - Manage secrets",
|
11
|
+
"* destroy - Delete all cloud resources"
|
12
|
+
].join("\n")
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
|
3
|
+
class ECSUtil::Commands::InitCommand < ECSUtil::Command
|
4
|
+
def run
|
5
|
+
check_dependencies
|
6
|
+
init_password_file
|
7
|
+
check_password_contents
|
8
|
+
check_gitignore
|
9
|
+
init_secrets
|
10
|
+
init_terraform
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def password_path
|
16
|
+
@password_path ||= config.secrets_vaultpass
|
17
|
+
end
|
18
|
+
|
19
|
+
def gitignore_path
|
20
|
+
@gitignore_path ||= File.join(Dir.pwd, ".gitignore")
|
21
|
+
end
|
22
|
+
|
23
|
+
def secrets_path
|
24
|
+
@secrets_path ||= File.join(Dir.pwd, "deploy", config.stage, "secrets")
|
25
|
+
end
|
26
|
+
|
27
|
+
def terraform_path
|
28
|
+
@terraform_path ||= File.join(Dir.pwd, "terraform", config.stage)
|
29
|
+
end
|
30
|
+
|
31
|
+
def check_dependencies
|
32
|
+
step_info "Checking AWS CLI"
|
33
|
+
if `which aws`.strip.empty?
|
34
|
+
terminate("AWS CLI is not installed")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def init_password_file
|
39
|
+
if !password_path || password_path && !File.exists?(password_path)
|
40
|
+
create_password_file
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_password_file
|
45
|
+
step_info "Vault password file not found at #{password_path}, creating..."
|
46
|
+
File.write(password_path, SecureRandom.hex(20))
|
47
|
+
end
|
48
|
+
|
49
|
+
def check_password_contents
|
50
|
+
if File.read(password_path).strip.empty?
|
51
|
+
terminate "Your vault password file is empty!"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def check_gitignore
|
56
|
+
unless File.exists?(gitignore_path)
|
57
|
+
step_info "Creating .gitignore file"
|
58
|
+
FileUtils.touch(gitignore_path)
|
59
|
+
end
|
60
|
+
|
61
|
+
data = File.read(gitignore_path)
|
62
|
+
return if data.include?("vaultpass")
|
63
|
+
|
64
|
+
step_info "Adding vaultpass to .gitignore"
|
65
|
+
data += "\nvaultpass"
|
66
|
+
File.write(gitignore_path, data.strip + "\n")
|
67
|
+
end
|
68
|
+
|
69
|
+
def init_secrets
|
70
|
+
step_info "Setting up secrets file at #{secrets_path}"
|
71
|
+
|
72
|
+
FileUtils.mkdir_p(File.dirname(secrets_path))
|
73
|
+
|
74
|
+
if File.exists?(secrets_path)
|
75
|
+
step_info "Secrets file already exists, skipping..."
|
76
|
+
return
|
77
|
+
end
|
78
|
+
|
79
|
+
vault_write(secrets_path, config.secrets_vaultpass, "# This is your secrets file")
|
80
|
+
end
|
81
|
+
|
82
|
+
def init_terraform
|
83
|
+
step_info "Checking if Terraform is installed"
|
84
|
+
if `which terraform`.strip.empty?
|
85
|
+
step_info "Terraform is not found, skipping..."
|
86
|
+
return
|
87
|
+
end
|
88
|
+
|
89
|
+
unless File.exists?(terraform_path)
|
90
|
+
step_info "Setting up Terraform directory at #{terraform_path}"
|
91
|
+
FileUtils.mkdir_p(terraform_path)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class ECSUtil::Commands::RunCommand < ECSUtil::Command
|
2
|
+
def run
|
3
|
+
task_name = args.shift
|
4
|
+
terminate("Task name is required") unless task_name
|
5
|
+
|
6
|
+
task = config["tasks"][task_name]
|
7
|
+
family = sprintf("%s-%s-%s", config["app"], config["env"], task_name)
|
8
|
+
|
9
|
+
arn = load_active_task_definitions.find do |task_arn|
|
10
|
+
task_arn.split("/", 2).last.start_with?(family)
|
11
|
+
end
|
12
|
+
|
13
|
+
if !arn
|
14
|
+
terminate "Cant find task definition for #{family}"
|
15
|
+
end
|
16
|
+
|
17
|
+
opts = {
|
18
|
+
startedBy: config["user"],
|
19
|
+
cluster: config["cluster"],
|
20
|
+
taskDefinition: arn,
|
21
|
+
launchType: "FARGATE",
|
22
|
+
networkConfiguration: {
|
23
|
+
awsvpcConfiguration: {
|
24
|
+
subnets: [config["subnets"]].flatten,
|
25
|
+
securityGroups: [task["security_groups"]].flatten,
|
26
|
+
"assignPublicIp": "ENABLED"
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
if args && args.any?
|
32
|
+
step_info "Using override command: #{args}"
|
33
|
+
|
34
|
+
opts[:overrides] = {
|
35
|
+
containerOverrides: [
|
36
|
+
{
|
37
|
+
name: task_name,
|
38
|
+
command: args,
|
39
|
+
}
|
40
|
+
]
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
step_info "Running task using #{arn}"
|
45
|
+
aws_call("ecs", "run-task", opts)
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class ECSUtil::Commands::ScaleCommand < ECSUtil::Command
|
2
|
+
def run
|
3
|
+
services = config["services"] || {}
|
4
|
+
terminate("No services found") if services.empty?
|
5
|
+
|
6
|
+
services.each_pair do |service_name, service|
|
7
|
+
info = describe_service(config, service_name)
|
8
|
+
service["exists"] = true
|
9
|
+
config["tasks"][service["task"]]["arn"] = info["taskDefinition"]
|
10
|
+
|
11
|
+
if info["runningCount"] != service["desired_count"]
|
12
|
+
step_info "Scaling #{service_name} from %d to %d",
|
13
|
+
info["runningCount"],
|
14
|
+
service["desired_count"]
|
15
|
+
|
16
|
+
update_service(config, service_name)
|
17
|
+
else
|
18
|
+
step_info "Scaling is skipped on #{service_name}. Requested: %d, actual %d",
|
19
|
+
service["desired_count"],
|
20
|
+
info["runningCount"]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
class ECSUtil::Commands::SecretsCommand < ECSUtil::Command
|
2
|
+
def run
|
3
|
+
case action
|
4
|
+
when nil, "show"
|
5
|
+
show_local_secrets
|
6
|
+
when "edit"
|
7
|
+
edit_secrets
|
8
|
+
when "push"
|
9
|
+
push_secrets
|
10
|
+
when "live"
|
11
|
+
load_secrets
|
12
|
+
show_live_secrets
|
13
|
+
when "delete"
|
14
|
+
confirm
|
15
|
+
load_secrets
|
16
|
+
deregister_secrets
|
17
|
+
else
|
18
|
+
fail "Invalid action: #{action}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def load_local_secrets
|
25
|
+
vault_read(config["secrets_file"], config["secrets_vaultpass"])
|
26
|
+
end
|
27
|
+
|
28
|
+
def show_local_secrets
|
29
|
+
step_info "Loading secrets from %s", config["secrets_file"]
|
30
|
+
puts load_local_secrets
|
31
|
+
end
|
32
|
+
|
33
|
+
def show_live_secrets
|
34
|
+
if config["secrets_data"].empty?
|
35
|
+
puts "No secrets found for prefix #{config["secrets_prefix"]}"
|
36
|
+
return
|
37
|
+
end
|
38
|
+
|
39
|
+
config["secrets_data"].each do |secret|
|
40
|
+
printf("%s=%s\n", secret[:key], secret[:value])
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def edit_secrets
|
45
|
+
step_info "Editing secrets file %s", config["secrets_file"]
|
46
|
+
vault_edit(config["secrets_file"], config["secrets_vaultpass"])
|
47
|
+
end
|
48
|
+
|
49
|
+
def push_secrets
|
50
|
+
confirm("Will push secrets live to #{config["secrets_prefix"]}")
|
51
|
+
load_secrets
|
52
|
+
|
53
|
+
local = parse_env_data(load_local_secrets)
|
54
|
+
live = config["secrets_data"].map { |item| [item[:key], item[:value]] }.to_h
|
55
|
+
|
56
|
+
added_count = 0
|
57
|
+
skipped_count = 0
|
58
|
+
removed_count = 0
|
59
|
+
|
60
|
+
local.each_pair do |key, value|
|
61
|
+
if live[key] == value
|
62
|
+
step_info "Skipping #{key}, already set"
|
63
|
+
skipped_count += 1
|
64
|
+
next
|
65
|
+
end
|
66
|
+
|
67
|
+
step_info "Setting #{key} to #{value}"
|
68
|
+
aws_call("ssm", "put-parameter", {
|
69
|
+
Type: "SecureString",
|
70
|
+
Name: sprintf("%s/%s", config["secrets_prefix"], key),
|
71
|
+
Value: value,
|
72
|
+
KeyId: config["secrets_key"],
|
73
|
+
Overwrite: true
|
74
|
+
})
|
75
|
+
added_count += 1
|
76
|
+
end
|
77
|
+
|
78
|
+
config["secrets_data"].each do |secret|
|
79
|
+
if !local[secret[:key]]
|
80
|
+
step_info "Removing #{secret[:key]}"
|
81
|
+
aws_call("ssm", "delete-parameter", "--name=#{secret[:name]}")
|
82
|
+
removed_count += 1
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
step_info "Skipped: %d, Added: %d, Removed: %d\n",
|
87
|
+
skipped_count,
|
88
|
+
added_count,
|
89
|
+
removed_count
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class ECSUtil::Commands::StatusCommand < ECSUtil::Command
|
2
|
+
def run
|
3
|
+
step_info "Fetching active task definitions..."
|
4
|
+
active_task_definitions.each do |name|
|
5
|
+
puts name
|
6
|
+
end
|
7
|
+
|
8
|
+
step_info "Fetching services..."
|
9
|
+
fetch_active_services.each do |service|
|
10
|
+
deployment = service["deployments"].first || {}
|
11
|
+
|
12
|
+
printf(
|
13
|
+
"%s STATUS=%s DESIRED=%d PENDING=%d RUNNING=%d\n",
|
14
|
+
service["serviceName"],
|
15
|
+
service["status"],
|
16
|
+
deployment["desiredCount"],
|
17
|
+
deployment["pendingCount"],
|
18
|
+
deployment["runningCount"]
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def active_task_definitions
|
26
|
+
list_active_task_definitions.select do |arn|
|
27
|
+
arn.include?(config.namespace)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def active_services
|
32
|
+
list_services(config.cluster).select do |name|
|
33
|
+
name.include?(config.namespace)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def fetch_active_services
|
38
|
+
names = active_services
|
39
|
+
names.any? ? describe_services(config, names) : []
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "yaml"
|
2
|
+
require "hashie"
|
3
|
+
|
4
|
+
module ECSUtil
|
5
|
+
class Config < Hashie::Mash
|
6
|
+
# Do not show any Hashie warnings
|
7
|
+
disable_warnings
|
8
|
+
|
9
|
+
def self.read(path, stage, outputs = {})
|
10
|
+
data = File.read(path).gsub(/(\$tf.([\w]+))+/i) do |m|
|
11
|
+
key = $2
|
12
|
+
fail "Terraform output key #{key} not found!" unless outputs[key]
|
13
|
+
outputs[key]
|
14
|
+
end
|
15
|
+
|
16
|
+
result = YAML.load(data).tap do |config|
|
17
|
+
fail "App name is required" unless config["app"]
|
18
|
+
fail "App env is required" unless config["env"]
|
19
|
+
fail "Cluster is required" unless config["cluster"]
|
20
|
+
fail "Repository is required" unless config["repository"]
|
21
|
+
|
22
|
+
# Check AWS configuration
|
23
|
+
if !config["aws_profile"] && !ENV["AWS_PROFILE"]
|
24
|
+
fail "AWS profile is not set! Set 'aws_profile' var in config or use AWS_PROFILE env var!"
|
25
|
+
end
|
26
|
+
|
27
|
+
# Override environment variable in case if it was set
|
28
|
+
ENV["AWS_PROFILE"] = config["aws_profile"]
|
29
|
+
|
30
|
+
# Set stage and namespace
|
31
|
+
config["user"] ||= `whoami`.strip
|
32
|
+
config["stage"] ||= stage
|
33
|
+
config["namespace"] ||= sprintf("%s-%s", config["app"], config["env"])
|
34
|
+
|
35
|
+
# Set default sections
|
36
|
+
config["tasks"] ||= {}
|
37
|
+
config["scheduled_tasks"] ||= {}
|
38
|
+
config["services"] ||= {}
|
39
|
+
|
40
|
+
# Parent dir
|
41
|
+
parent_dir = File.expand_path(File.join(File.dirname(path), ".."))
|
42
|
+
|
43
|
+
# Set secrets
|
44
|
+
config.merge!(
|
45
|
+
"secrets_vaultpass" => File.join(parent_dir, "vaultpass"),
|
46
|
+
"secrets_file" => File.join(File.dirname(path), "#{stage}/secrets"),
|
47
|
+
"secrets_key" => outputs["kms_key"],
|
48
|
+
"secrets_prefix" => sprintf("/%s", config["namespace"]),
|
49
|
+
"secrets_data" => {}
|
50
|
+
)
|
51
|
+
|
52
|
+
# Set default vars
|
53
|
+
config["git_commit"] ||= `git rev-parse HEAD`.strip
|
54
|
+
config["git_branch"] ||= `git rev-parse --abbrev-ref HEAD`.strip
|
55
|
+
end
|
56
|
+
|
57
|
+
Config.new(result)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|