ecsutil 0.1.0

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