ECSD 0.9.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c069cbb5aaee9472442db19e06ac7985d348888fe4792e64f507b1e99d385505
4
+ data.tar.gz: 20ee8759eb119d57eda9f092b601218b0e4653bc5baf38db83fd4fd97f65926a
5
+ SHA512:
6
+ metadata.gz: c6983cfa3baaeb1218206de55ee79aa347ffb8066ad93c2c39e7ae343183413780fcef89c4e394eff1d2a20af940a896988965081cae4628293defe818c30791
7
+ data.tar.gz: c4a1d62a25eb836623cd27c824b5f17e094e90dcc1c4b279e55a7b35a8292242d1f417ca9d40785f43ef719b1269b1306a160c44c4d6b89d6fae40458a073ecd
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ # 0.9.0
2
+
3
+ See commit log.
data/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # ECSD
2
+
3
+ Simple service for generate ec2_sd config files (one file per cluster)
4
+ to allow ECS cluster to be discovered by prometheus monitoring service.
5
+
6
+ Warning! Tested only with EC2 cluster model.
7
+ Working with fargate cluster model is not guaranteed
8
+
9
+ ## Getting started
10
+
11
+ First create AWS IAM user with roles for describe and list.
12
+
13
+ Then add gem to your service:
14
+ ```ruby
15
+ gem 'ecsd'
16
+ ```
17
+ Or install directly:
18
+ ```
19
+ $ gem install ecsd
20
+ ```
21
+
22
+ To start discover ECS cluster define config:
23
+ ```ruby
24
+ require 'ecsd'
25
+
26
+ ECSD.config do |c|
27
+ c.clusters = %w[cluster_name]
28
+ c.region = 'region'
29
+ c.credentials = { AWS_ACCESS_KEY_ID: 'ID', AWS_SECRET_ACCESS_KEY: 'SECRET' }
30
+ c.options = {
31
+ export_path: '/dir/path/to/export/folder',
32
+ timeout: 101
33
+ }
34
+ c.logger = Logger.new($stdout)
35
+ end
36
+ ```
37
+
38
+ - `clusters` - array of cluster names to discover. Same name as ECS cluster
39
+ - `region` - AWS region name where ECS cluster deployed
40
+ - `credentials` - `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` from your IAM user
41
+ - `options`
42
+ - `export_path` - path to export config folder. Default is `#{service root}/export`
43
+ - `timeout` - time in seconds between cluster discovery cycles.
44
+ - `logger` - define your logger or default logger will be set
45
+
46
+ Then start discover:
47
+ ```ruby
48
+ ECSD.start
49
+ ```
50
+ By default ECSD continuously discovering defined clusters and save templates into
51
+ corresponded files.
52
+
53
+ Also it's possible to start only ony cycle discover:
54
+ ```ruby
55
+ ECSD.start(cycle: false)
56
+ ```
57
+
58
+ ## Contributing
59
+
60
+ [Fork the project](https://github.com/Ad1n/ECSD) and send pull
61
+ requests.
@@ -0,0 +1,59 @@
1
+ # @example
2
+ # ECSD.config do |c|
3
+ # c.clusters = %w[cluster0 cluster1 cluster2]
4
+ # c.region = "aws_region_name"
5
+ # c.credentials = { AWS_ACCESS_KEY_ID: "ID", AWS_SECRET_ACCESS_KEY: "SECRET" }
6
+ # c.options = {
7
+ # export_path: 'dir/path/to/export_config/folder',
8
+ # timeout: 101
9
+ # }
10
+ # c.logger = Logger.new($stdout)
11
+ # end
12
+ module ECSD
13
+ unless defined?(CoreConfig)
14
+ class CoreConfig < Struct.new(:credentials, :region, :clusters, :options, :logger)
15
+ # @param payload [Hash] { AWS_ACCESS_KEY_ID: 'XXX', AWS_SECRET_ACCESS_KEY: 'XXX' }
16
+ def credentials=(payload)
17
+ validate_credentials!(payload)
18
+ super(::Aws::Credentials.new(
19
+ payload[:AWS_ACCESS_KEY_ID],
20
+ payload[:AWS_SECRET_ACCESS_KEY]
21
+ ))
22
+ end
23
+
24
+ # @param payload [Hash] { export_path: 'path/to/folder', timeout: 0 }
25
+ def options=(payload)
26
+ payload[:export_path] ||= ECSD::DEFAULT_EXPORT_PATH
27
+ payload[:timeout] ||= 0
28
+ super(payload)
29
+ end
30
+
31
+ private
32
+
33
+ # Removing the secret credentials from the default inspect string.
34
+ # @api private
35
+ def inspect
36
+ "#<#{self.class.name} clusters=#{clusters.inspect} region=#{region.inspect} options=#{options.inspect} logger=#{logger.inspect}>"
37
+ end
38
+
39
+ def validate_credentials!(p)
40
+ raise Config::ValidationError, 'Please, provide AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY' if p.values.size != 2
41
+ raise Config::ValidationError, 'Invalid AWS credentials' unless p.values.all? String
42
+ end
43
+ end
44
+ end
45
+
46
+ def config
47
+ yield @config if block_given?
48
+
49
+ @config
50
+ end
51
+ module_function :config
52
+
53
+ def logger
54
+ @config.logger || Logger.new($stdout)
55
+ end
56
+ module_function :logger
57
+
58
+ @config ||= CoreConfig.new
59
+ end
@@ -0,0 +1,10 @@
1
+ module ECSD
2
+ module Constants
3
+ DEFAULT_METRICS_PATH = '/metrics'.freeze
4
+ DEFAULT_EXPORT_PATH = (File.expand_path('../..', __dir__) << '/export').freeze
5
+ LAUNCH_TYPE = 'EC2'.freeze
6
+ CONNECTIVITY = {
7
+ connected: 'CONNECTED'
8
+ }.freeze
9
+ end
10
+ end
data/lib/ecsd/core.rb ADDED
@@ -0,0 +1,60 @@
1
+ module ECSD
2
+ class Core
3
+ include Helpers::EC2
4
+ include Helpers::ECS
5
+ include Helpers::Common
6
+
7
+ attr_reader :ecs, :ec2, :cluster
8
+
9
+ def initialize(cluster_name)
10
+ @ecs = ::Aws::ECS::Client.new(
11
+ credentials: ECSD.config.credentials,
12
+ region: ECSD.config.region
13
+ )
14
+ @ec2 = ::Aws::EC2::Client.new(
15
+ credentials: ECSD.config.credentials,
16
+ region: ECSD.config.region
17
+ )
18
+ @cluster = cluster_name
19
+ end
20
+
21
+ def handle!
22
+ task_arns = task_arns(cluster)
23
+ tasks = connected_tasks(cluster, task_arns)
24
+ handle_tasks(tasks)
25
+ end
26
+
27
+ def handle_tasks(tasks)
28
+ ECSD.logger.info <<~TEXT
29
+
30
+ Handle cluster: [#{cluster}]
31
+ Started at: #{Time.now}
32
+ TEXT
33
+ tasks.map do |t|
34
+ instance_id = instance_id(cluster, t)
35
+ task_definition_name, task_revision = task_definition_data(t)
36
+ task_container = task_container(t)
37
+
38
+ TEMPLATE.call(
39
+ ip_addr: instance_ip_addr(instance_id),
40
+ dyn_port: dyn_port(task_container),
41
+ task_id: task_id(t),
42
+ task_definition_name: task_definition_name,
43
+ task_revision: task_revision,
44
+ cluster_name: cluster_name(t),
45
+ container_name: task_container.name,
46
+ instance_id: instance_id
47
+ )
48
+ end.then do |r|
49
+ write_to_file(cluster, r)
50
+
51
+ ECSD.logger.info <<~TEXT
52
+
53
+ Handled cluster: [#{cluster}]
54
+ Tasks in cluster: #{r.count}
55
+ Finished at: #{Time.now}
56
+ TEXT
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,7 @@
1
+ module ECSD
2
+ class BaseError < RuntimeError; end
3
+
4
+ class Config
5
+ class ValidationError < BaseError; end
6
+ end
7
+ end
@@ -0,0 +1,40 @@
1
+ module ECSD
2
+ module Helpers
3
+ module Common
4
+ def write_to_file(cluster, data, path = ECSD.config.options[:export_path] || DEFAULT_EXPORT_PATH)
5
+ File.open("#{path}/#{cluster.downcase}.yml", 'w') do |f|
6
+ f.write(data.join)
7
+ end
8
+ end
9
+
10
+ def task_id(task)
11
+ task.task_arn
12
+ .split('/')
13
+ .last
14
+ end
15
+
16
+ def task_definition_data(task)
17
+ task.task_definition_arn
18
+ .split('/')
19
+ .last
20
+ .split(':')
21
+ end
22
+
23
+ def cluster_name(task)
24
+ task.cluster_arn
25
+ .split('/')
26
+ .last
27
+ end
28
+
29
+ def task_container(task)
30
+ task.containers.find { |c| c.task_arn == task.task_arn }
31
+ end
32
+
33
+ def dyn_port(task_container)
34
+ task_container.network_bindings
35
+ .first
36
+ .host_port
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,17 @@
1
+ module ECSD
2
+ module Helpers
3
+ module EC2
4
+ # @param instance_id [String]
5
+ # @return [String] private ip
6
+ def instance_ip_addr(instance_id)
7
+ ec2.describe_instances(instance_ids: Array(instance_id))
8
+ .data
9
+ .reservations
10
+ .first
11
+ .instances
12
+ .first
13
+ .private_ip_address
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,38 @@
1
+ module ECSD
2
+ module Helpers
3
+ module ECS
4
+ # @param cluster [String] cluster name
5
+ # @return [Array<Array<String>>] batched task_arns by 100
6
+ def task_arns(cluster)
7
+ ecs.list_tasks(cluster: cluster).each.with_object([]) do |response, task_arns|
8
+ task_arns << response[:task_arns]
9
+ end
10
+ end
11
+
12
+ # @param cluster [String] cluster name
13
+ # @param task_arns [Array<Array<String>>] batched task_arns by 100
14
+ # @return [Array<Array<Aws::ECS::Types:Task>>] connected tasks
15
+ def connected_tasks(cluster, task_arns)
16
+ task_arns.each.with_object([]) do |pt_batch, connected_tasks|
17
+ connected_tasks << ecs.describe_tasks(cluster: cluster, tasks: pt_batch)
18
+ .data
19
+ .tasks
20
+ .select { |t| t.connectivity == ECSD::Constants::CONNECTIVITY[:connected] }
21
+ end.flatten
22
+ end
23
+
24
+ # @param cluster [String] cluster name
25
+ # @param task [Aws::ECS::Types::Task]
26
+ # @return [String]
27
+ def instance_id(cluster, task)
28
+ ecs.describe_container_instances(
29
+ cluster: cluster,
30
+ container_instances: Array(task.container_instance_arn)
31
+ ).data
32
+ .container_instances
33
+ .first
34
+ .ec2_instance_id
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,35 @@
1
+ module ECSD
2
+ # @info Start ECS clusters discovery
3
+ def start(cycle: :infinite)
4
+ case cycle
5
+ when :infinite
6
+ loop { invoke!; wait }
7
+ else
8
+ invoke!
9
+ end
10
+ end
11
+ module_function :start
12
+
13
+ private
14
+
15
+ # @info one discovery cycle invocation
16
+ def invoke!
17
+ config.clusters.each do |cluster|
18
+ core(cluster).handle!
19
+ end
20
+ end
21
+ module_function :invoke!
22
+
23
+ # @info Initialize core component
24
+ # @param cluster [String] cluster name
25
+ def core(cluster)
26
+ ECSD::Core.new(cluster)
27
+ end
28
+ module_function :core
29
+
30
+ # @info Time in seconds between discovery iterations
31
+ def wait
32
+ sleep ECSD.config.options[:timeout]
33
+ end
34
+ module_function :wait
35
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ECSD
4
+ TEMPLATE = lambda do |ip_addr:,
5
+ dyn_port:,
6
+ task_id:,
7
+ task_definition_name:,
8
+ task_revision:,
9
+ cluster_name:,
10
+ container_name:,
11
+ instance_id:,
12
+ metrics_path: DEFAULT_METRICS_PATH|
13
+ <<~TEXT
14
+ - targets:
15
+ - #{ip_addr}:#{dyn_port}
16
+ labels:
17
+ task_id: #{task_id}
18
+ task_definition_name: #{task_definition_name}
19
+ task_revision: #{task_revision}
20
+ cluster_name: #{cluster_name}
21
+ container_name: #{container_name}
22
+ instance_id: #{instance_id}
23
+ __metrics_path__: #{metrics_path}
24
+ TEXT
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ module ECSD
2
+ VERSION = '0.9.0'.freeze
3
+ end
data/lib/ecsd.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-ecs'
4
+ require 'aws-sdk-ec2'
5
+
6
+ require_relative 'ecsd/version'
7
+ require_relative 'ecsd/errors'
8
+ require_relative 'ecsd/constants'
9
+ require_relative 'ecsd/template'
10
+ require_relative 'ecsd/config'
11
+ require_relative 'ecsd/helpers/ec2'
12
+ require_relative 'ecsd/helpers/ecs'
13
+ require_relative 'ecsd/helpers/common'
14
+ require_relative 'ecsd/runner'
15
+ require_relative 'ecsd/core'
16
+
17
+ #:nodoc
18
+ module ECSD
19
+ include Constants
20
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ECSD
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - Anton Shevtsov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-02-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-ec2
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.298'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.298'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk-ecs
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.95'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.95'
41
+ description: Generate ec2 yaml config files to allow ECS cluster to be discovered
42
+ by prometheus monitoring service
43
+ email: shevtsovav@bk.ru
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - CHANGELOG.md
49
+ - README.md
50
+ - lib/ecsd.rb
51
+ - lib/ecsd/config.rb
52
+ - lib/ecsd/constants.rb
53
+ - lib/ecsd/core.rb
54
+ - lib/ecsd/errors.rb
55
+ - lib/ecsd/helpers/common.rb
56
+ - lib/ecsd/helpers/ec2.rb
57
+ - lib/ecsd/helpers/ecs.rb
58
+ - lib/ecsd/runner.rb
59
+ - lib/ecsd/template.rb
60
+ - lib/ecsd/version.rb
61
+ homepage:
62
+ licenses:
63
+ - MIT
64
+ metadata: {}
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 2.4.0
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubygems_version: 3.1.4
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: ECS(EC2 model) discovery
84
+ test_files: []