ecsutil 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|