├── .rspec ├── lib ├── ecs_cmd │ ├── version.rb │ ├── utils.rb │ ├── services.rb │ ├── clusters.rb │ ├── exec.rb │ ├── run_task.rb │ ├── task_definition.rb │ └── service.rb └── ecs_cmd.rb ├── Rakefile ├── .travis.yml ├── bin ├── setup ├── console └── ecs-cmd ├── .rubocop.yml ├── .gitignore ├── Gemfile ├── spec ├── spec_helper.rb ├── ecs_cmd │ ├── clusters_spec.rb │ ├── services_spec.rb │ └── service_spec.rb └── ecs_cmd_spec.rb ├── CHANGELOG.md ├── LICENSE.txt ├── ecs_cmd.gemspec ├── Gemfile.lock ├── CODE_OF_CONDUCT.md ├── .rubocop_todo.yml └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/ecs_cmd/version.rb: -------------------------------------------------------------------------------- 1 | module EcsCmd 2 | VERSION = "0.2.0" 3 | end 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | env: 4 | - GLI_DEBUG=true 5 | rvm: 6 | - 2.4.3 7 | - 2.5.1 8 | before_install: gem install bundler -v 2.1.4 9 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | Metrics/LineLength: 4 | Max: 120 5 | Metrics/MethodLength: 6 | Max: 20 7 | Style/FrozenStringLiteralComment: 8 | Enabled: false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | .vscode 10 | 11 | # rspec failure tracking 12 | .rspec_status 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in ecs_cmd.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /lib/ecs_cmd.rb: -------------------------------------------------------------------------------- 1 | require 'ecs_cmd/version' 2 | require 'ecs_cmd/clusters.rb' 3 | require 'ecs_cmd/exec.rb' 4 | require 'ecs_cmd/run_task.rb' 5 | require 'ecs_cmd/services.rb' 6 | require 'ecs_cmd/service.rb' 7 | require 'ecs_cmd/task_definition.rb' 8 | require 'ecs_cmd/utils.rb' 9 | 10 | # module EcsCmd 11 | # # Your code goes here.. 12 | 13 | # end 14 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "ecs_cmd" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "pry" 14 | Pry.start(__FILE__) 15 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require 'simplecov' 3 | SimpleCov.start 4 | require "ecs_cmd" 5 | 6 | RSpec.configure do |config| 7 | # Enable flags like --only-failures and --next-failure 8 | config.example_status_persistence_file_path = ".rspec_status" 9 | 10 | # Disable RSpec exposing methods globally on `Module` and `main` 11 | config.disable_monkey_patching! 12 | 13 | config.expect_with :rspec do |c| 14 | c.syntax = :expect 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /spec/ecs_cmd/clusters_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe EcsCmd::Clusters do 4 | it 'gets clusters' do 5 | expect(EcsCmd::Clusters.new('us-east-1').get_clusters['cluster_arns']).to include('arn:aws:ecs:us-east-1:111111111111:cluster/staging') 6 | end 7 | 8 | # it 'get_container_instance_count' do 9 | # expect(EcsCmd::Clusters.new('us-east-1').get_container_instance_count).to eq 5 10 | # end 11 | 12 | it 'lists clusters' do 13 | expect(EcsCmd::Clusters.new('us-east-1').cluster_names).to include('arn:aws:ecs:us-east-1:111111111111:cluster/staging') 14 | end 15 | end -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Next 2 | 3 | # 0.2.0 4 | 5 | - add --sudo flag to enable running docker commands with sudo, defaults to true 6 | - Combine shell and exec commands. Pass a shell like `sh` or `bash` to interactively login to a container. 7 | 8 | # 0.1.7 9 | 10 | - bugfix: Run task did not work when task definition contained secrets. 11 | - bugfix: Failed to parse image name for images from docker hub (e.g. redis:alpine) 12 | 13 | # 0.1.6 14 | 15 | - bugfix: remove pry require 16 | 17 | # 0.1.4 18 | 19 | - Added user flag to shell command. `ecs-cmd shell -c production -s foo -u nobody` 20 | - Added user flag to exec command. `ecs-cmd exec -c production -s foo -u nobody whoami` -------------------------------------------------------------------------------- /spec/ecs_cmd/services_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe EcsCmd::Services do 4 | 5 | it 'lists services' do 6 | Aws.config[:ecs] = { 7 | stub_responses: { 8 | list_clusters: { 9 | cluster_arns: 10 | ['arn:aws:ecs:us-east-1:111111111111:cluster/staging'] 11 | }, 12 | list_services: { 13 | service_arns: [ 14 | 'arn:aws:ecs:us-east-1:111111111111:service/foo', 15 | 'arn:aws:ecs:us-east-1:111111111111:service/bar', 16 | 'arn:aws:ecs:us-east-1:111111111111:service/staging/baz' 17 | ] 18 | } 19 | } 20 | } 21 | expect(EcsCmd::Services.new('us-east-1', 'staging').get_services).to eq([ 22 | 'arn:aws:ecs:us-east-1:111111111111:service/foo', 'arn:aws:ecs:us-east-1:111111111111:service/bar', 'arn:aws:ecs:us-east-1:111111111111:service/staging/baz' 23 | ]) 24 | end 25 | end -------------------------------------------------------------------------------- /lib/ecs_cmd/utils.rb: -------------------------------------------------------------------------------- 1 | module EcsCmd 2 | module Utils 3 | def self.parse_image_name(image) 4 | regex = /^([a-zA-Z0-9\.\-]+):?([0-9]+)?\/?([a-zA-Z0-9\._\-]+)(\/[\/a-zA-Z0-9\._\-]+)?:?([a-zA-Z0-9\._\-]+)?$/ 5 | raise 'invalid image supplied, please verify correct image format' unless regex.match(image) 6 | if regex.match(image)[5].nil? || regex.match(image)[5] == false 7 | regex.match(image)[1] 8 | elsif regex.match(image)[4].nil? || regex.match(image)[4] == false 9 | regex.match(image)[3] 10 | else 11 | regex.match(image)[4].gsub(/\//, '') 12 | end 13 | end 14 | 15 | def self.parse_image_tag(image); end 16 | end 17 | end 18 | 19 | class String 20 | def tokenize 21 | self. 22 | split(/\s(?=(?:[^'"]|'[^']*'|"[^"]*")*$)/). 23 | select {|s| not s.empty? }. 24 | map {|s| s.gsub(/(^ +)|( +$)|(^["']+)|(["']+$)/,'')} 25 | end 26 | end -------------------------------------------------------------------------------- /lib/ecs_cmd/services.rb: -------------------------------------------------------------------------------- 1 | require 'aws-sdk-ecs' 2 | require 'terminal-table' 3 | 4 | module EcsCmd 5 | class Services 6 | def initialize(region, cluster) 7 | @client = Aws::ECS::Client.new(region: region) 8 | @reg = region 9 | @cluster = cluster 10 | end 11 | 12 | def get_services 13 | @service_arns = [] 14 | @client.list_services(cluster: @cluster).each do |r| 15 | @service_arns << r[0] 16 | @service_arns.flatten! 17 | end 18 | @service_arns 19 | end 20 | 21 | def list_services 22 | @service_list = get_services 23 | rows = [] 24 | @service_list.map do |service| 25 | service_name = service.split(%r{/}).last 26 | serv = EcsCmd::Service.new(@cluster, service_name, @reg) 27 | rows << [service_name, serv.desired_count, serv.running_count, serv.pending_count] 28 | end 29 | table = Terminal::Table.new headings: ['SERVICE NAME', 'DESIRED_COUNT', 30 | 'RUNNING_COUNT', 'PENDING_COUNT'], rows: rows 31 | puts table 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Daniel Schaaff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/ecs_cmd/clusters.rb: -------------------------------------------------------------------------------- 1 | require 'aws-sdk-ecs' 2 | require 'terminal-table' 3 | 4 | module EcsCmd 5 | class Clusters 6 | def initialize(region) 7 | @client = Aws::ECS::Client.new(region: region) 8 | end 9 | 10 | def get_clusters 11 | @cluster_arns = @client.list_clusters 12 | end 13 | 14 | def get_container_instance_count(stats) 15 | stats['registered_container_instances_count'] 16 | end 17 | 18 | def get_service_count(stats) 19 | stats['active_services_count'] 20 | end 21 | 22 | def get_running_task_count(stats) 23 | stats['running_tasks_count'] 24 | end 25 | 26 | def get_pending_task_count(stats) 27 | stats['pending_tasks_count'] 28 | end 29 | 30 | def cluster_names 31 | get_clusters[0] 32 | end 33 | 34 | def list_clusters 35 | rows = [] 36 | cluster_names.map do |c| 37 | cluster_name = c.split('/')[1] 38 | stats = @client.describe_clusters(clusters: [cluster_name])[0][0] 39 | rows << [cluster_name, get_container_instance_count(stats), get_service_count(stats), 40 | get_running_task_count(stats), get_pending_task_count(stats)] 41 | end 42 | table = Terminal::Table.new headings: ['CLUSTER NAME', 'CONTAINER_INSTANCE_COUNT', 43 | 'SERVICES', 'RUNNING_TASKS', 'PENDING_TASKS'], rows: rows 44 | puts table 45 | end 46 | end 47 | end -------------------------------------------------------------------------------- /lib/ecs_cmd/exec.rb: -------------------------------------------------------------------------------- 1 | require 'aws-sdk-ec2' 2 | require 'aws-sdk-ecs' 3 | require 'open3' 4 | require 'shellwords' 5 | 6 | module EcsCmd 7 | module Exec 8 | class << self 9 | # move this to a config 10 | def ssh_cmd(ip) 11 | cmd = 'ssh -tt -o StrictHostKeyChecking=no ' 12 | cmd << ip.to_s 13 | end 14 | 15 | def ssh(ip) 16 | exec(ssh_cmd(ip)) 17 | end 18 | 19 | def execute(task_family, ip, command, user = 'root', sudo = true) 20 | cmd = if sudo == true 21 | "sudo docker exec -i -t -u #{user} `#{docker_ps_task(task_family)}` #{command}" 22 | else 23 | "docker exec -i -t -u #{user} `#{docker_ps_task(task_family)}` #{command}" 24 | end 25 | exec(ssh_cmd(ip) + " '#{cmd}' ") 26 | end 27 | 28 | def logs(task_family, ip, lines, sudo) 29 | cmd = if sudo == true 30 | "sudo docker logs -f --tail=#{lines} `#{docker_ps_task(task_family)}`" 31 | else 32 | "docker logs -f --tail=#{lines} `#{docker_ps_task(task_family)}`" 33 | end 34 | exec(ssh_cmd(ip) + " '#{cmd}' ") 35 | end 36 | 37 | # docker ps command to get container id 38 | def docker_ps_task(task_family, sudo = true) 39 | if sudo == true 40 | "sudo docker ps -n 1 -q --filter name=#{Shellwords.shellescape(task_family)}" 41 | else 42 | "docker ps -n 1 -q --filter name=#{Shellwords.shellescape(task_family)}" 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/ecs_cmd/run_task.rb: -------------------------------------------------------------------------------- 1 | require 'aws-sdk-ecs' 2 | require 'terminal-table' 3 | 4 | module EcsCmd 5 | class RunTask 6 | def initialize(region, cluster, task_def, container_name = nil, command = []) 7 | @client = Aws::ECS::Client.new(region: region) 8 | @cluster = cluster 9 | @container_name = container_name 10 | @task_def = task_def 11 | @command = command 12 | end 13 | 14 | # simply run the task 15 | def run 16 | puts 'running task...' 17 | resp = if @container_name 18 | @client.run_task(cluster: @cluster, task_definition: @task_def, overrides: { 19 | container_overrides: [{ 20 | name: @container_name, command: @command 21 | }] 22 | }) 23 | else 24 | @client.run_task(cluster: @cluster, task_definition: @task_def) 25 | end 26 | task_arn = resp[0][0]['task_arn'] 27 | result = 28 | begin 29 | puts 'waiting for task to complete...' 30 | @client.wait_until(:tasks_stopped, cluster: @cluster, tasks: [task_arn]) 31 | rescue Aws::Waiters::Errors::WaiterFailed => error 32 | puts "failed waiting for task to run: #{error.message}" 33 | end 34 | puts "task ended with exit code #{result[0][0]['containers'][0]['exit_code']}" 35 | # return exit code 36 | raise 'task appears to have failed, check container logs' if result[0][0]['containers'][0]['exit_code'] != 0 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /ecs_cmd.gemspec: -------------------------------------------------------------------------------- 1 | 2 | # frozen_string_literal: true 3 | 4 | lib = File.expand_path('lib', __dir__) 5 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 6 | require 'ecs_cmd/version' 7 | 8 | Gem::Specification.new do |spec| 9 | spec.name = 'ecs_cmd' 10 | spec.version = EcsCmd::VERSION 11 | spec.authors = ['Daniel Schaaff'] 12 | spec.email = ['dbschaaff@gmail.com'] 13 | 14 | spec.summary = 'AWS ECS CLI Utility' 15 | spec.description = 'AWS ECS CLI Utility' 16 | spec.homepage = 'https://danielschaaff.com' 17 | spec.license = 'MIT' 18 | 19 | # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' 20 | # to allow pushing to a single host or delete this section to allow pushing to any host. 21 | # if spec.respond_to?(:metadata) 22 | # spec.metadata['allowed_push_host'] = "rubyge'" 23 | # else 24 | # raise 'RubyGems 2.0 or newer is required to protect against ' \ 25 | # 'public gem pushes.' 26 | # end 27 | 28 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 29 | f.match(%r{^(test|spec|features)/}) 30 | end 31 | spec.bindir = 'bin' 32 | spec.executables << 'ecs-cmd' 33 | spec.require_paths = ['lib'] 34 | spec.add_runtime_dependency 'aws-sdk-ec2' 35 | spec.add_runtime_dependency 'aws-sdk-ecs' 36 | spec.add_runtime_dependency 'gli', '2.18.0' 37 | spec.add_runtime_dependency 'terminal-table' 38 | 39 | spec.add_development_dependency 'bundler', '~> 2.1' 40 | spec.add_development_dependency 'rake', '~> 12.3.3' 41 | spec.add_development_dependency 'solargraph' 42 | spec.add_development_dependency 'rspec', '~> 3.0' 43 | spec.add_development_dependency 'pry' 44 | spec.add_development_dependency 'simplecov' 45 | end 46 | -------------------------------------------------------------------------------- /lib/ecs_cmd/task_definition.rb: -------------------------------------------------------------------------------- 1 | require 'aws-sdk-ecs' 2 | require 'terminal-table' 3 | require 'ecs_cmd/utils' 4 | 5 | module EcsCmd 6 | class TaskDefinition 7 | def initialize(task_definition, region) 8 | @client = Aws::ECS::Client.new(region: region) 9 | @task_def = @client.describe_task_definition(task_definition: task_definition)[0] 10 | end 11 | 12 | 13 | def container_definitions 14 | @task_def['container_definitions'] 15 | end 16 | 17 | def images 18 | self.container_definitions.each do |e| 19 | puts e['image'] 20 | end 21 | end 22 | 23 | def family 24 | @task_def['family'] 25 | end 26 | 27 | def revision 28 | @task_def['revision'] 29 | end 30 | 31 | def volumes 32 | @task_def['volumes'] 33 | end 34 | 35 | def hash 36 | @task_def.to_hash 37 | end 38 | 39 | def json 40 | hash.to_json 41 | end 42 | 43 | def task_role_arn 44 | @task_def['task_role_arn'] 45 | end 46 | 47 | def execution_role_arn 48 | @task_def['execution_role_arn'] || '' 49 | end 50 | 51 | def update_image(image) 52 | @new_task_def = @task_def.to_hash 53 | @new_task_def[:container_definitions].each do |e, i=image| 54 | if Utils.parse_image_name(e[:image]) == Utils.parse_image_name(i) 55 | e[:image] = image 56 | end 57 | end 58 | @new_task_def 59 | resp = register_task_definition(@new_task_def[:family], @new_task_def[:container_definitions], 60 | @new_task_def[:volumes], @new_task_def[:task_role_arn]) 61 | resp.task_definition.task_definition_arn 62 | end 63 | 64 | def register_task_definition(family, container_definitions, volumes, task_role_arn) 65 | @client.register_task_definition(family: family, container_definitions: container_definitions, volumes: volumes, 66 | task_role_arn: task_role_arn, execution_role_arn: execution_role_arn) 67 | end 68 | 69 | def print_json 70 | puts JSON.pretty_generate(JSON[json]) 71 | end 72 | 73 | end 74 | end -------------------------------------------------------------------------------- /spec/ecs_cmd/service_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe EcsCmd::Service do 4 | context 'with non existent service' do 5 | before(:each) do 6 | Aws.config[:ecs] = { 7 | stub_responses: {} 8 | } 9 | end 10 | it 'fails on invalid service' do 11 | expect do 12 | EcsCmd::Service.new('staging', 'empty', 'us-east-1').list_service 13 | end.to raise_error(RuntimeError, 'service does not appear to exist') 14 | end 15 | end 16 | 17 | it 'gets service arn' do 18 | expect(EcsCmd::Service.new('staging', 'foo', 'us-east-1').arn).to eq('arn:aws:ecs:us-east-1:111111111111:service/foo') 19 | end 20 | 21 | it 'gets service status' do 22 | expect(EcsCmd::Service.new('staging', 'foo', 'us-east-1').status).to eq('ACTIVE') 23 | end 24 | 25 | it 'gets deployments' do 26 | expect(EcsCmd::Service.new('staging', 'foo', 'us-east-1').deployments).to_not be nil 27 | end 28 | 29 | it 'gets deployment configuration' do 30 | expect(EcsCmd::Service.new('staging', 'foo', 'us-east-1').deployment_configuration.to_s).to eq('{:maximum_percent=>200, :minimum_healthy_percent=>100}') 31 | end 32 | 33 | it 'gets desired count' do 34 | expect(EcsCmd::Service.new('staging', 'foo', 'us-east-1').desired_count).to eq(2) 35 | end 36 | 37 | it 'gets running count' do 38 | expect(EcsCmd::Service.new('staging', 'foo', 'us-east-1').running_count).to eq(2) 39 | end 40 | 41 | it 'gets pending count' do 42 | expect(EcsCmd::Service.new('staging', 'foo', 'us-east-1').pending_count).to eq(0) 43 | end 44 | 45 | it 'gets task definition' do 46 | expect(EcsCmd::Service.new('staging', 'foo', 'us-east-1').task_definition).to eq('arn:aws:ecs:us-east-1:111111111111:task-definition/foo:1') 47 | end 48 | 49 | it 'gets health check grace period' do 50 | expect(EcsCmd::Service.new('staging', 'foo', 'us-east-1').health_check_grace_period).to eq(0) 51 | end 52 | 53 | it 'gets event stream' do 54 | expect(EcsCmd::Service.new('staging', 'foo', 'us-east-1').events.to_s).to include('(service foo)has reached a steady state') 55 | end 56 | 57 | it 'gets service name' do 58 | expect(EcsCmd::Service.new('staging', 'foo', 'us-east-1').name).to eq('foo') 59 | end 60 | 61 | it 'gets launch type' do 62 | expect(EcsCmd::Service.new('staging', 'foo', 'us-east-1').launch_type).to eq('EC2') 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/ecs_cmd_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe EcsCmd do 2 | Aws.config[:ecs] = { 3 | stub_responses: { 4 | list_clusters: { 5 | cluster_arns: 6 | ['arn:aws:ecs:us-east-1:111111111111:cluster/staging'] 7 | }, 8 | list_services: { 9 | service_arns: [ 10 | 'arn:aws:ecs:us-east-1:111111111111:service/foo', 11 | 'arn:aws:ecs:us-east-1:111111111111:service/bar', 12 | 'arn:aws:ecs:us-east-1:111111111111:service/staging/baz' 13 | ] 14 | }, 15 | describe_clusters: { 16 | clusters: [ 17 | cluster_arn: 'arn:aws:ecs:us-east-1:111111111111:cluster/staging', 18 | cluster_name: 'staging', 19 | status: 'ACTIVE', 20 | registered_container_instances_count: 5, 21 | running_tasks_count: 20, 22 | pending_tasks_count: 1, 23 | active_services_count: 6 24 | ] 25 | }, 26 | describe_services: { 27 | services: [ 28 | { 29 | service_arn: 'arn:aws:ecs:us-east-1:111111111111:service/foo', 30 | service_name: 'foo', 31 | status: 'ACTIVE', 32 | desired_count: 2, 33 | running_count: 2, 34 | pending_count: 0, 35 | health_check_grace_period_seconds: 0, 36 | task_definition: 'arn:aws:ecs:us-east-1:111111111111:task-definition/foo:1', 37 | deployment_configuration: { 38 | maximum_percent: 200, 39 | minimum_healthy_percent: 100 40 | }, 41 | events: [ 42 | { 43 | id: '1234', 44 | created_at: Time.new('2018-06-15 07:50:46 -0700'), 45 | message: '(service foo)has reached a steady state' 46 | } 47 | ], 48 | deployments: [ 49 | { 50 | id: 'ecs-svc/9223370507780529222', 51 | status: 'primary', 52 | task_definition: 'arn:aws:ecs:us-east-1:111111111111:task-definition/foo:1', 53 | desired_count: 2, 54 | pending_count: 0, 55 | running_count: 2, 56 | created_at: Time.new('2018-06-15 07:50:46 -0700'), 57 | updated_at: Time.new('2018-06-15 07:51:46 -0700'), 58 | launch_type: 'EC2' 59 | } 60 | ] 61 | }, 62 | { 63 | service_arn: 'arn:aws:ecs:us-east-1:111111111111:service/empty', 64 | service_name: 'foo' 65 | } 66 | ] 67 | } 68 | } 69 | } 70 | it 'has a version number' do 71 | expect(EcsCmd::VERSION).not_to be nil 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | ecs_cmd (0.2.0) 5 | aws-sdk-ec2 6 | aws-sdk-ecs 7 | gli (= 2.18.0) 8 | terminal-table 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | ast (2.4.0) 14 | aws-eventstream (1.0.3) 15 | aws-partitions (1.286.0) 16 | aws-sdk-core (3.92.0) 17 | aws-eventstream (~> 1.0, >= 1.0.2) 18 | aws-partitions (~> 1, >= 1.239.0) 19 | aws-sigv4 (~> 1.1) 20 | jmespath (~> 1.0) 21 | aws-sdk-ec2 (1.151.0) 22 | aws-sdk-core (~> 3, >= 3.71.0) 23 | aws-sigv4 (~> 1.1) 24 | aws-sdk-ecs (1.59.0) 25 | aws-sdk-core (~> 3, >= 3.71.0) 26 | aws-sigv4 (~> 1.1) 27 | aws-sigv4 (1.1.1) 28 | aws-eventstream (~> 1.0, >= 1.0.2) 29 | backport (0.3.0) 30 | coderay (1.1.2) 31 | diff-lcs (1.3) 32 | docile (1.3.1) 33 | gli (2.18.0) 34 | htmlentities (4.3.4) 35 | jaro_winkler (1.5.4) 36 | jmespath (1.4.0) 37 | json (2.1.0) 38 | kramdown (1.17.0) 39 | method_source (0.9.0) 40 | mini_portile2 (2.4.0) 41 | nokogiri (1.10.9) 42 | mini_portile2 (~> 2.4.0) 43 | parallel (1.19.1) 44 | parser (2.7.0.4) 45 | ast (~> 2.4.0) 46 | pry (0.11.3) 47 | coderay (~> 1.1.0) 48 | method_source (~> 0.9.0) 49 | rainbow (3.0.0) 50 | rake (12.3.3) 51 | reverse_markdown (1.4.0) 52 | nokogiri 53 | rexml (3.2.4) 54 | rspec (3.7.0) 55 | rspec-core (~> 3.7.0) 56 | rspec-expectations (~> 3.7.0) 57 | rspec-mocks (~> 3.7.0) 58 | rspec-core (3.7.1) 59 | rspec-support (~> 3.7.0) 60 | rspec-expectations (3.7.0) 61 | diff-lcs (>= 1.2.0, < 2.0) 62 | rspec-support (~> 3.7.0) 63 | rspec-mocks (3.7.0) 64 | diff-lcs (>= 1.2.0, < 2.0) 65 | rspec-support (~> 3.7.0) 66 | rspec-support (3.7.1) 67 | rubocop (0.80.1) 68 | jaro_winkler (~> 1.5.1) 69 | parallel (~> 1.10) 70 | parser (>= 2.7.0.1) 71 | rainbow (>= 2.2.2, < 4.0) 72 | rexml 73 | ruby-progressbar (~> 1.7) 74 | unicode-display_width (>= 1.4.0, < 1.7) 75 | ruby-progressbar (1.10.1) 76 | simplecov (0.16.1) 77 | docile (~> 1.1) 78 | json (>= 1.8, < 3) 79 | simplecov-html (~> 0.10.0) 80 | simplecov-html (0.10.2) 81 | solargraph (0.31.3) 82 | backport (~> 0.3) 83 | htmlentities (~> 4.3, >= 4.3.4) 84 | jaro_winkler (~> 1.5) 85 | kramdown (~> 1.16) 86 | parser (~> 2.3) 87 | reverse_markdown (~> 1.0, >= 1.0.5) 88 | rubocop (~> 0.52) 89 | thor (~> 0.19, >= 0.19.4) 90 | tilt (~> 2.0) 91 | yard (~> 0.9) 92 | terminal-table (1.8.0) 93 | unicode-display_width (~> 1.1, >= 1.1.1) 94 | thor (0.20.3) 95 | tilt (2.0.10) 96 | unicode-display_width (1.4.1) 97 | yard (0.9.24) 98 | 99 | PLATFORMS 100 | ruby 101 | x86_64-darwin-17 102 | 103 | DEPENDENCIES 104 | bundler (~> 2.1) 105 | ecs_cmd! 106 | pry 107 | rake (~> 12.3.3) 108 | rspec (~> 3.0) 109 | simplecov 110 | solargraph 111 | 112 | BUNDLED WITH 113 | 2.1.4 114 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at daniel.schaaff@verve.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /lib/ecs_cmd/service.rb: -------------------------------------------------------------------------------- 1 | require 'aws-sdk-ecs' 2 | require 'aws-sdk-ec2' 3 | require 'terminal-table' 4 | 5 | module EcsCmd 6 | # rubocop:disable Metrics/ClassLength 7 | class Service 8 | def initialize(cluster, name, region) 9 | @client = Aws::ECS::Client.new(region: region) 10 | @ec2_client = Aws::EC2::Client.new(region: region) 11 | @cluster = cluster 12 | @name = name 13 | @service_stats = @client.describe_services(cluster: cluster, services: [name])[0] 14 | raise 'service does not appear to exist' if @service_stats.empty? 15 | end 16 | 17 | def arn 18 | @service_stats[0]['service_arn'] 19 | end 20 | 21 | def status 22 | @service_stats[0]['status'] 23 | end 24 | 25 | def deployments 26 | t = [] 27 | @service_stats[0]['deployments'].each do |e| 28 | t << ['id', e['id']] 29 | t << ['status', e['status']] 30 | t << ['task definition', e['task_definition']] 31 | t << ['desired count', e['desired_count']] 32 | t << ['pending count', e['pending_count']] 33 | t << ['running count', e['running_count']] 34 | t << ['created at', e['created_at']] 35 | t << ['updated at', e['updated_at']] 36 | t << ["\n"] 37 | end 38 | table = Terminal::Table.new headings: ['DEPLOYMENTS', ''], rows: t 39 | table 40 | end 41 | 42 | def deployment_configuration 43 | @service_stats[0]['deployment_configuration'] 44 | end 45 | 46 | def desired_count 47 | @service_stats[0]['desired_count'] 48 | end 49 | 50 | def running_count 51 | @service_stats[0]['running_count'] 52 | end 53 | 54 | def pending_count 55 | @service_stats[0]['pending_count'] 56 | end 57 | 58 | def task_definition 59 | @service_stats[0]['task_definition'] 60 | end 61 | 62 | def task_family 63 | # known issue, won't work with / in task family names 64 | # TODO: improve this later 65 | @service_stats[0]['task_definition'].split('/')[1].split(':')[0] 66 | end 67 | # list task arns for a service 68 | def tasks 69 | @client.list_tasks(cluster: @cluster, service_name: @name)[0] 70 | end 71 | # list all container instance arns for given service's tasks 72 | def container_instances 73 | instances = [] 74 | @client.describe_tasks(cluster: @cluster, tasks: tasks)[0].each do |e| 75 | instances << e[:container_instance_arn] 76 | end 77 | instances 78 | end 79 | # return container instance arn for given task id 80 | def container_instance(task_arn) 81 | instance = [] 82 | @client.describe_tasks(cluster: @cluster, tasks: [task_arn])[0].each do |e| 83 | instance << e[:container_instance_arn] 84 | end 85 | instance[0] 86 | end 87 | 88 | def container_instance_id(arn) 89 | instance = [arn.to_s] 90 | @client.describe_container_instances(cluster: @cluster, container_instances: instance)[0][0][:ec2_instance_id] 91 | end 92 | 93 | def container_instance_ids 94 | ids = [] 95 | @client.describe_container_instances(cluster: @cluster, container_instances: container_instances)[0].each do |e| 96 | ids << e[:ec2_instance_id] 97 | end 98 | ids.uniq 99 | end 100 | 101 | def container_instance_ip(instance_id) 102 | id = [instance_id] 103 | @ec2_client.describe_instances(instance_ids: id)[:reservations][0][:instances][0][:private_ip_address] 104 | end 105 | 106 | def container_instance_ips 107 | ips = [] 108 | @ec2_client.describe_instances(instance_ids: container_instance_ids)[:reservations][0][:instances].each do |e| 109 | ips << e[:private_ip_address] 110 | end 111 | ips 112 | end 113 | 114 | def health_check_grace_period 115 | @service_stats[0]['health_check_grace_period_seconds'] 116 | end 117 | 118 | def events 119 | @service_stats[0]['events'].each do |e| 120 | puts "#{e['created_at']}: #{e['message']}" 121 | end 122 | end 123 | 124 | def name 125 | @service_stats[0]['service_name'] 126 | end 127 | 128 | def launch_type 129 | @service_stats[0]['deployments'][0]['launch_type'] 130 | end 131 | 132 | def tasks_table 133 | t = [] 134 | if launch_type == 'FARGATE' 135 | tasks.each do |e| 136 | t << [e] 137 | end 138 | table = Terminal::Table.new headings: ['TASK_ID'], rows: t 139 | else 140 | tasks.each do |e| 141 | t << [e, container_instance_id(container_instance(e)), 142 | container_instance_ip(container_instance_id(container_instance(e)))] 143 | end 144 | table = Terminal::Table.new headings: ['TASK_ID', 'INSTANCE_ID', 'IP'], rows: t 145 | end 146 | table 147 | end 148 | 149 | def overview_table 150 | row1 = [] 151 | row1 << [name, status, running_count, desired_count, pending_count, 152 | deployment_configuration['maximum_percent'], deployment_configuration['minimum_healthy_percent']] 153 | table1 = Terminal::Table.new headings: ['NAME', 'STATUS', 'RUNNING COUNT', 154 | 'DESIRED COUNT', 'PENDING COUNT', 155 | 'MAX HEALTHY', 'MIN HEALTHY'], rows: row1 156 | table1 157 | end 158 | 159 | def task_def_table 160 | row2 = [] 161 | row2 << [task_definition] 162 | table2 = Terminal::Table.new headings: ['TASK DEFINITION'], rows: row2 163 | table2 164 | end 165 | 166 | def list_service 167 | puts overview_table 168 | puts task_def_table 169 | puts deployments 170 | puts tasks_table 171 | end 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /bin/ecs-cmd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'gli' 3 | require 'ecs_cmd' 4 | include GLI::App 5 | include EcsCmd 6 | program_desc 'Command utility for interacting with AWS ECS' 7 | 8 | version EcsCmd::VERSION 9 | 10 | subcommand_option_handling :normal 11 | arguments :strict 12 | 13 | desc 'Set the aws region' 14 | default_value ENV['AWS_DEFAULT_REGION'] || 'us-east-1' 15 | arg_name 'region' 16 | flag %i[r region] 17 | 18 | desc 'Get Info on Clusters and Services' 19 | command :get do |c| 20 | c.command :clusters do |clusters| 21 | clusters.desc 'list ecs clusters' 22 | clusters.action do |global_options, _options, _args| 23 | clust = EcsCmd::Clusters.new(global_options[:region]) 24 | clust.list_clusters 25 | end 26 | end 27 | 28 | c.command :services do |services| 29 | services.flag %i[c cluster], desc: 'cluster name', arg_name: 'cluster', required: true 30 | services.desc 'list services in a given ecs cluster' 31 | services.action do |global_options, options, _args| 32 | serv = EcsCmd::Services.new(global_options[:region],options[:cluster]) 33 | serv.list_services 34 | end 35 | end 36 | 37 | c.command :service do |service| 38 | service.flag %i[c cluster], desc: 'cluster name', arg_name: 'cluster', required: true 39 | service.flag %i[s service], desc: 'service name', arg_name: 'service', required: true 40 | service.default_desc 'get info about an ecs service' 41 | service.switch %i[e events], desc: 'get ecs events', default_value: false 42 | service.switch %i[t task_definition], desc: 'get current task definition for service', default_value: false 43 | service.action do |global_options, options| 44 | if options[:events] == true 45 | EcsCmd::Service.new(options[:cluster], options[:service], global_options[:region]).events 46 | elsif options[:task_definition] == true 47 | task_def = EcsCmd::Service.new(options[:cluster], options[:service], global_options[:region]).task_definition 48 | EcsCmd::TaskDefinition.new(task_def, global_options[:region]).print_json 49 | else 50 | EcsCmd::Service.new(options[:cluster], options[:service], global_options[:region]).list_service 51 | end 52 | end 53 | end 54 | end 55 | 56 | desc 'Run a One Off Task On an ECS Cluster' 57 | command :'run-task' do |c| 58 | c.flag %i[c cluster], desc: 'cluster name', arg_name: 'cluster', required: true 59 | c.flag %i[n container-name], desc: 'container name', arg_name: 'container name', required: false 60 | c.flag %i[t task-definition], desc: 'the task definition to use for task', required: true 61 | c.flag %i[i image], desc: 'docker image to use for task', arg_name: 'image', required: false 62 | c.flag %i[d command], desc: 'override task definition command', arg_name: 'command' 63 | c.action do |global_options, options, _args| 64 | command = options[:d].tokenize 65 | if options[:i] 66 | puts "generating new task definition with image #{options[:i]}" 67 | new_task = EcsCmd::TaskDefinition.new(options[:t], global_options[:region]).update_image(options[:i]) 68 | puts "registered task definition #{new_task}" 69 | EcsCmd::RunTask.new(global_options[:region], options[:c], new_task, options[:n], command).run 70 | else 71 | EcsCmd::RunTask.new(global_options[:region], options[:c], options[:t], options[:n], command).run 72 | end 73 | end 74 | end 75 | 76 | desc 'SSH into Host Task is Running On' 77 | command :ssh do |c| 78 | c.flag %i[c cluster], desc: 'cluster name', arg_name: 'cluster', required: true 79 | c.flag %i[s service], desc: 'service name', arg_name: 'service', required: true 80 | c.action do |global_options, options, args| 81 | ip = EcsCmd::Service.new(options[:c], options[:s], global_options[:region]).container_instance_ips[0] 82 | puts "opening ssh connection to #{ip}" 83 | EcsCmd::Exec.ssh(ip) 84 | end 85 | end 86 | 87 | desc "Open a Shell Inside a Service's Container" 88 | arg_name 'command', :required 89 | command 'exec' do |c| 90 | c.flag %i[c cluster], desc: 'cluster name', arg_name: 'cluster', required: true 91 | c.flag %i[s service], desc: 'service name', arg_name: 'service', required: true 92 | c.flag %i[u user], desc: 'user name or uid of container user', 93 | arg_name: 'user', default_value: 'root', required: false 94 | c.switch %i[sudo], desc: 'use sudo for docker commands, necessary if not in docker group', default_value: true 95 | c.action do |global_options, options, args| 96 | service = EcsCmd::Service.new(options[:c], options[:s], global_options[:region]) 97 | ip = service.container_instance_ips[0] 98 | task_family = service.task_family 99 | command = args.join(' ') 100 | EcsCmd::Exec.execute(task_family, ip, command, options[:u], options[:sudo]) 101 | end 102 | end 103 | 104 | desc "Tail Logs From a Service's Container" 105 | command 'logs' do |c| 106 | c.flag %i[c cluster], desc: 'cluster name', arg_name: 'cluster', required: true 107 | c.flag %i[s service], desc: 'service name', arg_name: 'service', required: true 108 | c.flag %i[l lines], desc: 'number of historical lines to tail', arg_name: 'lines', default_value: 30, required: false 109 | c.switch %i[sudo], desc: 'use sudo for docker commands, necessary if not in docker group', default_value: true 110 | c.action do |global_options, options| 111 | service = EcsCmd::Service.new(options[:c], options[:s], global_options[:region]) 112 | ip = service.container_instance_ips[0] 113 | task_family = service.task_family 114 | EcsCmd::Exec.logs(task_family, ip, options[:lines], options[:sudo]) 115 | end 116 | end 117 | # desc 'Describe complete here' 118 | # arg_name 'Describe arguments to complete here' 119 | # command :complete do |c| 120 | # c.action do |global_options,options,args| 121 | # puts "complete command ran" 122 | # end 123 | # end 124 | 125 | pre do |_global, _command, _options, _args| 126 | # Pre logic here 127 | # Return true to proceed; false to abort and not call the 128 | # chosen command 129 | # Use skips_pre before a command to skip this block 130 | # on that command only 131 | true 132 | end 133 | 134 | post do |global, command, options, args| 135 | # Post logic here 136 | # Use skips_post before a command to skip this 137 | # block on that command only 138 | end 139 | 140 | on_error do |_exception| 141 | # Error logic here 142 | # return false to skip default error handling 143 | true 144 | end 145 | 146 | exit run(ARGV) 147 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2020-03-16 16:50:28 -0700 using RuboCop version 0.80.1. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | # Cop supports --auto-correct. 11 | # Configuration parameters: TreatCommentsAsGroupSeparators, Include. 12 | # Include: **/*.gemspec 13 | Gemspec/OrderedDependencies: 14 | Exclude: 15 | - 'ecs_cmd.gemspec' 16 | 17 | # Offense count: 1 18 | # Cop supports --auto-correct. 19 | Layout/CommentIndentation: 20 | Exclude: 21 | - 'lib/ecs_cmd/service.rb' 22 | 23 | # Offense count: 3 24 | # Cop supports --auto-correct. 25 | # Configuration parameters: EnforcedStyle. 26 | # SupportedStyles: leading, trailing 27 | Layout/DotPosition: 28 | Exclude: 29 | - 'lib/ecs_cmd/utils.rb' 30 | 31 | # Offense count: 1 32 | # Cop supports --auto-correct. 33 | Layout/EmptyLineAfterGuardClause: 34 | Exclude: 35 | - 'lib/ecs_cmd/utils.rb' 36 | 37 | # Offense count: 4 38 | # Cop supports --auto-correct. 39 | # Configuration parameters: AllowAdjacentOneLineDefs, NumberOfEmptyLines. 40 | Layout/EmptyLineBetweenDefs: 41 | Exclude: 42 | - 'lib/ecs_cmd/service.rb' 43 | - 'lib/ecs_cmd/task_definition.rb' 44 | 45 | # Offense count: 1 46 | # Cop supports --auto-correct. 47 | Layout/EmptyLines: 48 | Exclude: 49 | - 'lib/ecs_cmd/task_definition.rb' 50 | 51 | # Offense count: 1 52 | # Cop supports --auto-correct. 53 | # Configuration parameters: EnforcedStyle. 54 | # SupportedStyles: empty_lines, no_empty_lines 55 | Layout/EmptyLinesAroundBlockBody: 56 | Exclude: 57 | - 'spec/ecs_cmd/services_spec.rb' 58 | 59 | # Offense count: 1 60 | # Cop supports --auto-correct. 61 | # Configuration parameters: EnforcedStyle. 62 | # SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only 63 | Layout/EmptyLinesAroundClassBody: 64 | Exclude: 65 | - 'lib/ecs_cmd/task_definition.rb' 66 | 67 | # Offense count: 2 68 | # Cop supports --auto-correct. 69 | # Configuration parameters: IndentationWidth. 70 | # SupportedStyles: special_inside_parentheses, consistent, align_brackets 71 | Layout/FirstArrayElementIndentation: 72 | EnforcedStyle: consistent 73 | 74 | # Offense count: 2 75 | # Cop supports --auto-correct. 76 | # Configuration parameters: EnforcedStyle, IndentationWidth. 77 | # SupportedStyles: special_inside_parentheses, consistent, align_braces 78 | Layout/FirstHashElementIndentation: 79 | Exclude: 80 | - 'spec/ecs_cmd/services_spec.rb' 81 | 82 | # Offense count: 2 83 | # Cop supports --auto-correct. 84 | # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. 85 | # SupportedHashRocketStyles: key, separator, table 86 | # SupportedColonStyles: key, separator, table 87 | # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit 88 | Layout/HashAlignment: 89 | Exclude: 90 | - 'spec/ecs_cmd_spec.rb' 91 | 92 | # Offense count: 1 93 | # Cop supports --auto-correct. 94 | # Configuration parameters: EnforcedStyle. 95 | # SupportedStyles: normal, indented_internal_methods 96 | Layout/IndentationConsistency: 97 | Exclude: 98 | - 'spec/ecs_cmd/services_spec.rb' 99 | 100 | # Offense count: 2 101 | # Cop supports --auto-correct. 102 | # Configuration parameters: Width, IgnoredPatterns. 103 | Layout/IndentationWidth: 104 | Exclude: 105 | - 'lib/ecs_cmd/service.rb' 106 | - 'spec/ecs_cmd/services_spec.rb' 107 | 108 | # Offense count: 1 109 | # Cop supports --auto-correct. 110 | Layout/LeadingEmptyLines: 111 | Exclude: 112 | - 'ecs_cmd.gemspec' 113 | 114 | # Offense count: 11 115 | # Cop supports --auto-correct. 116 | # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. 117 | # URISchemes: http, https 118 | Layout/LineLength: 119 | Max: 188 120 | 121 | # Offense count: 2 122 | # Cop supports --auto-correct. 123 | Layout/SpaceAfterComma: 124 | Exclude: 125 | - 'bin/ecs-cmd' 126 | - 'lib/ecs_cmd/utils.rb' 127 | 128 | # Offense count: 1 129 | # Cop supports --auto-correct. 130 | # Configuration parameters: EnforcedStyle. 131 | # SupportedStyles: space, no_space 132 | Layout/SpaceAroundEqualsInParameterDefault: 133 | Exclude: 134 | - 'lib/ecs_cmd/task_definition.rb' 135 | 136 | # Offense count: 4 137 | # Cop supports --auto-correct. 138 | # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. 139 | # SupportedStyles: space, no_space 140 | # SupportedStylesForEmptyBraces: space, no_space 141 | Layout/SpaceInsideBlockBraces: 142 | Exclude: 143 | - 'Gemfile' 144 | - 'lib/ecs_cmd/utils.rb' 145 | 146 | # Offense count: 5 147 | # Cop supports --auto-correct. 148 | # Configuration parameters: EnforcedStyle. 149 | # SupportedStyles: final_newline, final_blank_line 150 | Layout/TrailingEmptyLines: 151 | Exclude: 152 | - 'lib/ecs_cmd/clusters.rb' 153 | - 'lib/ecs_cmd/task_definition.rb' 154 | - 'lib/ecs_cmd/utils.rb' 155 | - 'spec/ecs_cmd/clusters_spec.rb' 156 | - 'spec/ecs_cmd/services_spec.rb' 157 | 158 | # Offense count: 5 159 | # Cop supports --auto-correct. 160 | # Configuration parameters: AllowInHeredoc. 161 | Layout/TrailingWhitespace: 162 | Exclude: 163 | - 'lib/ecs_cmd.rb' 164 | - 'lib/ecs_cmd/task_definition.rb' 165 | - 'spec/spec_helper.rb' 166 | 167 | # Offense count: 1 168 | # Configuration parameters: MaximumRangeSize. 169 | Lint/MissingCopEnableDirective: 170 | Exclude: 171 | - 'lib/ecs_cmd/service.rb' 172 | 173 | # Offense count: 1 174 | # Cop supports --auto-correct. 175 | # Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. 176 | Lint/UnusedBlockArgument: 177 | Exclude: 178 | - 'bin/ecs-cmd' 179 | 180 | # Offense count: 1 181 | # Configuration parameters: CheckForMethodsWithNoSideEffects. 182 | Lint/Void: 183 | Exclude: 184 | - 'lib/ecs_cmd/task_definition.rb' 185 | 186 | # Offense count: 4 187 | Metrics/AbcSize: 188 | Max: 23 189 | 190 | # Offense count: 2 191 | # Configuration parameters: CountComments, ExcludedMethods. 192 | # ExcludedMethods: refine 193 | Metrics/BlockLength: 194 | Max: 71 195 | 196 | # Offense count: 4 197 | # Configuration parameters: CountComments, ExcludedMethods. 198 | Metrics/MethodLength: 199 | Max: 20 200 | 201 | # Offense count: 2 202 | Naming/AccessorMethodName: 203 | Exclude: 204 | - 'lib/ecs_cmd/clusters.rb' 205 | - 'lib/ecs_cmd/services.rb' 206 | 207 | # Offense count: 1 208 | # Cop supports --auto-correct. 209 | # Configuration parameters: PreferredName. 210 | Naming/RescuedExceptionsVariableName: 211 | Exclude: 212 | - 'lib/ecs_cmd/run_task.rb' 213 | 214 | # Offense count: 8 215 | Style/Documentation: 216 | Exclude: 217 | - 'spec/**/*' 218 | - 'test/**/*' 219 | - 'lib/ecs_cmd/clusters.rb' 220 | - 'lib/ecs_cmd/exec.rb' 221 | - 'lib/ecs_cmd/run_task.rb' 222 | - 'lib/ecs_cmd/service.rb' 223 | - 'lib/ecs_cmd/services.rb' 224 | - 'lib/ecs_cmd/task_definition.rb' 225 | - 'lib/ecs_cmd/utils.rb' 226 | 227 | # Offense count: 1 228 | # Cop supports --auto-correct. 229 | # Configuration parameters: EnforcedStyle, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. 230 | # SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys 231 | Style/HashSyntax: 232 | Exclude: 233 | - 'Rakefile' 234 | 235 | # Offense count: 1 236 | # Cop supports --auto-correct. 237 | Style/IfUnlessModifier: 238 | Exclude: 239 | - 'lib/ecs_cmd/task_definition.rb' 240 | 241 | # Offense count: 1 242 | # Cop supports --auto-correct. 243 | # Configuration parameters: InverseMethods, InverseBlocks. 244 | Style/InverseMethods: 245 | Exclude: 246 | - 'lib/ecs_cmd/utils.rb' 247 | 248 | # Offense count: 2 249 | Style/MixinUsage: 250 | Exclude: 251 | - 'bin/ecs-cmd' 252 | 253 | # Offense count: 1 254 | # Cop supports --auto-correct. 255 | # Configuration parameters: EnforcedStyle. 256 | # SupportedStyles: literals, strict 257 | Style/MutableConstant: 258 | Exclude: 259 | - 'lib/ecs_cmd/version.rb' 260 | 261 | # Offense count: 1 262 | # Cop supports --auto-correct. 263 | Style/Not: 264 | Exclude: 265 | - 'lib/ecs_cmd/utils.rb' 266 | 267 | # Offense count: 2 268 | # Cop supports --auto-correct. 269 | Style/RedundantSelf: 270 | Exclude: 271 | - 'lib/ecs_cmd/task_definition.rb' 272 | - 'lib/ecs_cmd/utils.rb' 273 | 274 | # Offense count: 2 275 | # Cop supports --auto-correct. 276 | # Configuration parameters: EnforcedStyle, AllowInnerSlashes. 277 | # SupportedStyles: slashes, percent_r, mixed 278 | Style/RegexpLiteral: 279 | Exclude: 280 | - 'lib/ecs_cmd/utils.rb' 281 | 282 | # Offense count: 10 283 | # Cop supports --auto-correct. 284 | # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. 285 | # SupportedStyles: single_quotes, double_quotes 286 | Style/StringLiterals: 287 | Exclude: 288 | - 'Gemfile' 289 | - 'Rakefile' 290 | - 'bin/console' 291 | - 'lib/ecs_cmd/version.rb' 292 | - 'spec/spec_helper.rb' 293 | 294 | # Offense count: 1 295 | # Cop supports --auto-correct. 296 | # Configuration parameters: MinSize, WordRegex. 297 | # SupportedStyles: percent, brackets 298 | Style/WordArray: 299 | EnforcedStyle: brackets 300 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ecs-Cmd 2 | 3 | [![Build Status](https://travis-ci.org/dschaaff/ecs-cmd.svg?branch=master)](https://travis-ci.org/dschaaff/ecs-cmd) [![Gem Version](https://badge.fury.io/rb/ecs_cmd.svg)](https://badge.fury.io/rb/ecs_cmd) 4 | 5 | This is a command line application for interacting with AWS ECS. The standard AWS cli can be a bit 6 | cumbersome for some tasks. Ecs-cmd aims to simplify those tasks. 7 | 8 | ## Installation 9 | 10 | ```bash 11 | gem install ecs_cmd 12 | ``` 13 | 14 | The gem uses the standard aws crendential chain for authentication actions. See https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html for reference. 15 | 16 | ## Usage 17 | 18 | ```text 19 | $ ecs-cmd 20 | NAME 21 | ecs-cmd - Command utility for interacting with AWS ECS 22 | 23 | SYNOPSIS 24 | ecs-cmd [global options] command [command options] [arguments...] 25 | 26 | VERSION 27 | 0.1.3 28 | 29 | GLOBAL OPTIONS 30 | --help - Show this message 31 | -r, --region=region - Set the aws region (default: us-east-1) 32 | --version - Display the program version 33 | 34 | COMMANDS 35 | exec - Execute a Command Within a Service's Container 36 | get - Get Info on Clusters and Services 37 | help - Shows a list of commands or help for one command 38 | logs - Tail Logs From a Service's Container 39 | run-task - Run a One Off Task On an ECS Cluster 40 | shell - Open a Shell Inside a Service's Container 41 | ssh - SSH into Host Task is Running On 42 | ``` 43 | 44 | ### Get Commands 45 | 46 | These allow you to query information from ECS. 47 | 48 | #### Get Clusters 49 | 50 | ```text 51 | $ ecs-cmd get clusters 52 | +--------------------+--------------------------+----------+---------------+---------------+ 53 | | CLUSTER NAME | CONTAINER_INSTANCE_COUNT | SERVICES | RUNNING_TASKS | PENDING_TASKS | 54 | +--------------------+--------------------------+----------+---------------+---------------+ 55 | | production | 20 | 39 | 82 | 0 | 56 | | development | 1 | 2 | 1 | 0 | 57 | +--------------------+--------------------------+----------+---------------+---------------+ 58 | ``` 59 | 60 | #### Get Services 61 | 62 | Prints an overview of the services in a given cluster. 63 | 64 | ```text 65 | $ ecs-cmd get services --help 66 | NAME 67 | services - 68 | 69 | SYNOPSIS 70 | ecs-cmd [global options] get services [command options] 71 | 72 | COMMAND OPTIONS 73 | -c, --cluster=cluster - cluster name (required, default: none) 74 | ``` 75 | 76 | ```text 77 | $ ecs-cmd get services -c testing 78 | +--------------------+---------------+---------------+---------------+ 79 | | SERVICE NAME | DESIRED_COUNT | RUNNING_COUNT | PENDING_COUNT | 80 | +--------------------+---------------+---------------+---------------+ 81 | | datadog-agent | 1 | 0 | 0 | 82 | | foo-bar | 1 | 1 | 0 | 83 | +--------------------+---------------+---------------+---------------+ 84 | ``` 85 | 86 | #### Get Service 87 | 88 | Get information on a specific service in a cluster. 89 | 90 | ```text 91 | $ ecs-cmd get service --help 92 | NAME 93 | service - 94 | 95 | SYNOPSIS 96 | ecs-cmd [global options] get service [command options] 97 | 98 | COMMAND OPTIONS 99 | -c, --cluster=cluster - cluster name (required, default: none) 100 | -e, --[no-]events - get ecs events 101 | -s, --service=service - service name (required, default: none) 102 | -t, --[no-]task_definition - get current task definition for service 103 | ``` 104 | 105 | ```text 106 | $ ecs-cmd get service -c production -s foo 107 | +------+--------+---------------+---------------+---------------+-------------+-------------+ 108 | | NAME | STATUS | RUNNING COUNT | DESIRED COUNT | PENDING COUNT | MAX HEALTHY | MIN HEALTHY | 109 | +------+--------+---------------+---------------+---------------+-------------+-------------+ 110 | | foo | ACTIVE | 2 | 2 | 0 | 200 | 50 | 111 | +------+--------+---------------+---------------+---------------+-------------+-------------+ 112 | +----------------------------------------------------------------------+ 113 | | TASK DEFINITION | 114 | +----------------------------------------------------------------------+ 115 | | arn:aws:ecs:us-east-1:xxxxxxxxxxxx:task-definition/foo-production:25 | 116 | +----------------------------------------------------------------------+ 117 | +---------------------+--------------+ 118 | | INSTANCE ID | IP | 119 | +---------------------+--------------+ 120 | | i-xxxxxxxxxxxxxxxxx | 10.0.230.1 | 121 | | i-xxxxxxxxxxxxxxxxx | 10.0.220.3 | 122 | +---------------------+--------------+ 123 | +-----------------+----------------------------------------------------------------------+ 124 | | DEPLOYMENTS | | 125 | +-----------------+----------------------------------------------------------------------+ 126 | | id | ecs-svc/xxxxxxxxxxxxxxxxxxx | 127 | | status | PRIMARY | 128 | | task definition | arn:aws:ecs:us-east-1:xxxxxxxxxxxx:task-definition/foo-production:25 | 129 | | desired count | 2 | 130 | | pending count | 0 | 131 | | running count | 2 | 132 | | created at | 2018-12-07 09:44:59 -0800 | 133 | | updated at | 2018-12-07 09:45:58 -0800 | 134 | +-----------------+----------------------------------------------------------------------+ 135 | +------------------------------------------------------------------------------+---------------------+---------------+ 136 | | TASK_ID | INSTANCE_ID | IP | 137 | +------------------------------------------------------------------------------+---------------------+---------------+ 138 | | arn:aws:ecs:us-east-1:xxxxxxxxxxxx:task/50a84065-4bcf-4450-9a3b-3ee411bb3fb0 | i-xxxxxxxxxxxxxxxxx | 10.20.205.150 | 139 | +------------------------------------------------------------------------------+---------------------+---------------+ 140 | ``` 141 | 142 | ### Logs 143 | 144 | **_works for ec2 type services only_** 145 | 146 | **_requires ssh access to instance and sudo or docker group membership_** 147 | 148 | Streams logs from 1 of a services running tasks using the docker logs command 149 | 150 | ```text 151 | $ ecs-cmd logs --help 152 | NAME 153 | logs - Tail Logs From a Service's Container 154 | 155 | SYNOPSIS 156 | ecs-cmd [global options] logs [command options] 157 | 158 | COMMAND OPTIONS 159 | -c, --cluster=cluster - cluster name (required, default: none) 160 | -l, --lines=lines - number of historical lines to tail (default: 30) 161 | -s, --service=service - service name (required, default: none) 162 | --[no-]sudo - use sudo for docker commands, necessary if not in docker group (default: enabled) 163 | ``` 164 | 165 | ### Run Task 166 | 167 | Run a one off task in ECS. This will poll for the task to exit and report its exit code. This is 168 | handy for tasks like rails migrations in ci/cd pipelines. If a docker image is passed it will create a new revision of 169 | the task definition prior to running the task. 170 | 171 | ```text 172 | $ ecs-cmd run-task --help 173 | NAME 174 | run-task - Run a One Off Task On an ECS Cluster 175 | 176 | SYNOPSIS 177 | ecs-cmd [global options] run-task [command options] 178 | 179 | COMMAND OPTIONS 180 | -c, --cluster=cluster - cluster name (required, default: none) 181 | -d, --command=command - override task definition command (default: none) 182 | -i, --image=image - docker image to use for task (default: none) 183 | -n, --container-name=container name - container name (default: none) 184 | -t, --task-definition=arg - the task definition to use for task (required, default: none) 185 | ``` 186 | 187 | ### Exec 188 | 189 | **_works for ec2 type services only and requires sudo or docker group membership_** 190 | 191 | **_requires ssh access to instance_** 192 | 193 | Run a command inside a container for a given service. If a shell is given as a command (e.g. `sh`) 194 | an interactive login terminal will be opened. 195 | 196 | ```text 197 | $ ecs-cmd shell --help 198 | NAME 199 | exec - Open a Shell Inside a Service's Container 200 | 201 | SYNOPSIS 202 | ecs-cmd [global options] exec [command options] command 203 | 204 | COMMAND OPTIONS 205 | -c, --cluster=cluster - cluster name (required, default: none) 206 | -s, --service=service - service name (required, default: none) 207 | --[no-]sudo - use sudo for docker commands, necessary if not in docker group (default: enabled) 208 | -u, --user=user - user name or uid of container user (default: root) 209 | ``` 210 | 211 | ### SSH 212 | 213 | **_works for ec2 type services only_** 214 | 215 | **_requires ssh access to instance_** 216 | 217 | SSH onto a host where a container for a given service is running. 218 | 219 | ```text 220 | $ ecs-cmd ssh --help 221 | NAME 222 | ssh - SSH into Host Task is Running On 223 | 224 | SYNOPSIS 225 | ecs-cmd [global options] ssh [command options] 226 | 227 | COMMAND OPTIONS 228 | -c, --cluster=cluster - cluster name (required, default: none) 229 | -s, --service=service - service name (required, default: none) 230 | ``` 231 | 232 | ## Development 233 | 234 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 235 | 236 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 237 | 238 | ## Contributing 239 | 240 | Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/ecs_cmd. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 241 | 242 | ## License 243 | 244 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 245 | 246 | ## Code of Conduct 247 | 248 | Everyone interacting in the EcsCmd project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/ecs_cmd/blob/master/CODE_OF_CONDUCT.md). 249 | --------------------------------------------------------------------------------