putpaws 0.0.8 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +230 -0
- data/lib/Putpawsfile +6 -1
- data/lib/putpaws/ai/ai_task.rb +1 -0
- data/lib/putpaws/ai/guide.rb +119 -0
- data/lib/putpaws/ai/tasks/ai_task.rake +14 -0
- data/lib/putpaws/application_config.rb +23 -3
- data/lib/putpaws/code_build/build_task.rb +1 -0
- data/lib/putpaws/code_build/project_command.rb +57 -0
- data/lib/putpaws/code_build/tasks/build_task.rake +39 -0
- data/lib/putpaws/ecs/run_command.rb +115 -0
- data/lib/putpaws/ecs/task_command.rb +63 -9
- data/lib/putpaws/ecs/tasks/ecs_task.rake +113 -6
- data/lib/putpaws/iam/grant_command.rb +205 -0
- data/lib/putpaws/iam/iam_task.rb +1 -0
- data/lib/putpaws/iam/operator_config.rb +46 -0
- data/lib/putpaws/iam/tasks/iam_task.rake +65 -0
- data/lib/putpaws/info.rb +1 -0
- data/lib/putpaws/provision/all.rb +29 -0
- data/lib/putpaws/provision/aws_clients.rb +59 -0
- data/lib/putpaws/provision/config_writer.rb +88 -0
- data/lib/putpaws/provision/policy_generator.rb +221 -0
- data/lib/putpaws/provision/preset.rb +91 -0
- data/lib/putpaws/provision/presets/rails-nginx/preset.json +32 -0
- data/lib/putpaws/provision/presets/rails-nginx/templates/taskdef-app.json.erb +29 -0
- data/lib/putpaws/provision/presets/rails-nginx/templates/taskdef-web.json.erb +48 -0
- data/lib/putpaws/provision/provision_config.rb +156 -0
- data/lib/putpaws/provision/provision_task.rb +1 -0
- data/lib/putpaws/provision/resources/base.rb +49 -0
- data/lib/putpaws/provision/resources/cluster.rb +30 -0
- data/lib/putpaws/provision/resources/codebuild_project.rb +85 -0
- data/lib/putpaws/provision/resources/iam_role.rb +89 -0
- data/lib/putpaws/provision/resources/log_group.rb +50 -0
- data/lib/putpaws/provision/resources/security_group.rb +72 -0
- data/lib/putpaws/provision/resources/service.rb +71 -0
- data/lib/putpaws/provision/resources/task_definition.rb +119 -0
- data/lib/putpaws/provision/runner.rb +218 -0
- data/lib/putpaws/provision/state.rb +45 -0
- data/lib/putpaws/provision/tasks/provision.rake +129 -0
- data/lib/putpaws/provision/util.rb +50 -0
- data/lib/putpaws/tasks/info.rake +35 -0
- data/lib/putpaws/version.rb +1 -1
- metadata +80 -2
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require 'putpaws/provision/util'
|
|
5
|
+
|
|
6
|
+
module Putpaws
|
|
7
|
+
module Provision
|
|
8
|
+
# Reflects provisioned resources into .putpaws/application.json and
|
|
9
|
+
# .putpaws/infra.json. Only the keys of the target service are touched.
|
|
10
|
+
# Shows a diff and asks for confirmation before writing.
|
|
11
|
+
class ConfigWriter
|
|
12
|
+
attr_reader :config, :state, :prompt, :io
|
|
13
|
+
def initialize(config:, state:, prompt:, io: $stdout)
|
|
14
|
+
@config = config
|
|
15
|
+
@state = state
|
|
16
|
+
@prompt = prompt
|
|
17
|
+
@io = io
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def application_entry
|
|
21
|
+
{
|
|
22
|
+
region: config.region,
|
|
23
|
+
cluster: config.cluster_name,
|
|
24
|
+
service: config.service_name,
|
|
25
|
+
task_name_prefix: config.service_name,
|
|
26
|
+
log_group_prefix: config.log_group,
|
|
27
|
+
build_log_group_prefix: config.build_log_group,
|
|
28
|
+
build_project_name_prefix: config.build_project_name,
|
|
29
|
+
network: config.service_name,
|
|
30
|
+
target: config.service_name,
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def network_entry
|
|
35
|
+
{
|
|
36
|
+
subnets: config.base[:subnets],
|
|
37
|
+
security_groups: state.resources[:security_group_ids] || [],
|
|
38
|
+
assign_public_ip: config.settings[:assign_public_ip] || 'DISABLED',
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def target_entry
|
|
43
|
+
{
|
|
44
|
+
# Used by EventBridge Scheduler (scheduler:deploy).
|
|
45
|
+
scheduler_role: config.roles[:scheduler_role_arn],
|
|
46
|
+
cluster: state.resources[:cluster_arn],
|
|
47
|
+
# Revision-less on purpose: always run the latest ACTIVE revision.
|
|
48
|
+
task_definition: config.service_task_definition_family,
|
|
49
|
+
container_name: config.service_container_name,
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def apply!
|
|
54
|
+
write_key(path('application.json'), [config.service_name.to_sym], application_entry)
|
|
55
|
+
write_key(path('infra.json'), [:network, config.service_name.to_sym], network_entry)
|
|
56
|
+
write_key(path('infra.json'), [:target, config.service_name.to_sym], target_entry)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def path(basename)
|
|
62
|
+
Pathname.new(config.path_prefix).join(basename)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def write_key(file, keys, entry)
|
|
66
|
+
data = file.exist? ? JSON.parse(File.read(file), symbolize_names: true) : {}
|
|
67
|
+
current = keys.reduce(data){|d, k| d.is_a?(Hash) ? d[k] : nil}
|
|
68
|
+
return if current == entry
|
|
69
|
+
|
|
70
|
+
io.puts "=== #{file} : #{keys.join('.')} ==="
|
|
71
|
+
io.puts "--- before"
|
|
72
|
+
io.puts current ? JSON.pretty_generate(current) : "(none)"
|
|
73
|
+
io.puts "+++ after"
|
|
74
|
+
io.puts JSON.pretty_generate(entry)
|
|
75
|
+
unless prompt.yes?("Update #{file}?")
|
|
76
|
+
io.puts "Skipped updating #{file}"
|
|
77
|
+
return
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
FileUtils.cp(file, "#{file}.bak") if file.exist?
|
|
81
|
+
parent = keys[0..-2].reduce(data){|d, k| d[k] ||= {}}
|
|
82
|
+
parent[keys.last] = entry
|
|
83
|
+
Util.write_json(file, data)
|
|
84
|
+
io.puts "Updated #{file}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'putpaws/provision/util'
|
|
3
|
+
|
|
4
|
+
module Putpaws
|
|
5
|
+
module Provision
|
|
6
|
+
# Generates drafts (たたき台) of IAM policies and roles.
|
|
7
|
+
# These are starting points to review and edit by hand before creating.
|
|
8
|
+
class PolicyGenerator
|
|
9
|
+
ECS_TASKS_TRUST = {
|
|
10
|
+
Version: '2012-10-17',
|
|
11
|
+
Statement: [{Effect: 'Allow', Principal: {Service: 'ecs-tasks.amazonaws.com'}, Action: 'sts:AssumeRole'}]
|
|
12
|
+
}
|
|
13
|
+
CODEBUILD_TRUST = {
|
|
14
|
+
Version: '2012-10-17',
|
|
15
|
+
Statement: [{Effect: 'Allow', Principal: {Service: 'codebuild.amazonaws.com'}, Action: 'sts:AssumeRole'}]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
def scheduler_trust
|
|
19
|
+
{
|
|
20
|
+
Version: '2012-10-17',
|
|
21
|
+
Statement: [{
|
|
22
|
+
Effect: 'Allow',
|
|
23
|
+
Principal: {Service: 'scheduler.amazonaws.com'},
|
|
24
|
+
Action: 'sts:AssumeRole',
|
|
25
|
+
# confused deputy protection: only schedules in this account
|
|
26
|
+
Condition: {StringEquals: {'aws:SourceAccount' => account_id}},
|
|
27
|
+
}]
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
attr_reader :config
|
|
32
|
+
def initialize(config)
|
|
33
|
+
@config = config
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def service_name; config.service_name; end
|
|
37
|
+
def region; config.region; end
|
|
38
|
+
def account_id; config.account_id; end
|
|
39
|
+
|
|
40
|
+
def write_step1_drafts!
|
|
41
|
+
dir = config.policies_dir
|
|
42
|
+
FileUtils.mkdir_p(dir)
|
|
43
|
+
Util.write_json(dir.join('role-task-execution.json'), task_execution_role_draft)
|
|
44
|
+
Util.write_json(dir.join('role-task.json'), task_role_draft)
|
|
45
|
+
Util.write_json(dir.join('role-codebuild.json'), codebuild_role_draft)
|
|
46
|
+
Util.write_json(dir.join('role-scheduler.json'), scheduler_role_draft)
|
|
47
|
+
%w[role-task-execution.json role-task.json role-codebuild.json role-scheduler.json]
|
|
48
|
+
.map{|f| dir.join(f).to_s}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def task_execution_role_draft
|
|
52
|
+
statements = [
|
|
53
|
+
{
|
|
54
|
+
Sid: 'EcrAuth',
|
|
55
|
+
Effect: 'Allow',
|
|
56
|
+
Action: %w[ecr:GetAuthorizationToken],
|
|
57
|
+
Resource: '*',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
Sid: 'EcrPull',
|
|
61
|
+
Effect: 'Allow',
|
|
62
|
+
Action: %w[ecr:BatchCheckLayerAvailability ecr:GetDownloadUrlForLayer ecr:BatchGetImage],
|
|
63
|
+
Resource: config.base[:ecr_repository_arn],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
Sid: 'WriteLogs',
|
|
67
|
+
Effect: 'Allow',
|
|
68
|
+
Action: %w[logs:CreateLogStream logs:PutLogEvents],
|
|
69
|
+
Resource: "arn:aws:logs:#{region}:#{account_id}:log-group:#{config.log_group}:*",
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
unless config.resolved_secrets.empty?
|
|
73
|
+
statements << {
|
|
74
|
+
Sid: 'ReadSecrets',
|
|
75
|
+
Effect: 'Allow',
|
|
76
|
+
Action: %w[ssm:GetParameters],
|
|
77
|
+
Resource: "arn:aws:ssm:#{region}:#{account_id}:parameter#{config.ssm_parameter_prefix}/*",
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
{
|
|
81
|
+
SuggestedRoleName: "#{service_name}-task-execution",
|
|
82
|
+
SuggestedPolicyName: "#{service_name}-task-execution-policy",
|
|
83
|
+
AssumeRolePolicyDocument: ECS_TASKS_TRUST,
|
|
84
|
+
PolicyDocument: {Version: '2012-10-17', Statement: statements},
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def task_role_draft
|
|
89
|
+
statements = [
|
|
90
|
+
{
|
|
91
|
+
Sid: 'EcsExec',
|
|
92
|
+
Effect: 'Allow',
|
|
93
|
+
Action: %w[
|
|
94
|
+
ssmmessages:CreateControlChannel ssmmessages:CreateDataChannel
|
|
95
|
+
ssmmessages:OpenControlChannel ssmmessages:OpenDataChannel
|
|
96
|
+
],
|
|
97
|
+
Resource: '*',
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
unless Util.blank?(config.base[:ses_identity_arn])
|
|
101
|
+
statements << {
|
|
102
|
+
Sid: 'SendMail',
|
|
103
|
+
Effect: 'Allow',
|
|
104
|
+
Action: %w[ses:SendEmail ses:SendRawEmail],
|
|
105
|
+
Resource: config.base[:ses_identity_arn],
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
unless Util.blank?(config.base[:s3_bucket_arn])
|
|
109
|
+
statements << {
|
|
110
|
+
Sid: 'UseBucket',
|
|
111
|
+
Effect: 'Allow',
|
|
112
|
+
Action: %w[s3:GetObject s3:PutObject s3:DeleteObject s3:ListBucket],
|
|
113
|
+
Resource: [config.base[:s3_bucket_arn], "#{config.base[:s3_bucket_arn]}/*"],
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
{
|
|
117
|
+
SuggestedRoleName: "#{service_name}-task",
|
|
118
|
+
SuggestedPolicyName: "#{service_name}-task-policy",
|
|
119
|
+
AssumeRolePolicyDocument: ECS_TASKS_TRUST,
|
|
120
|
+
PolicyDocument: {Version: '2012-10-17', Statement: statements},
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def codebuild_role_draft
|
|
125
|
+
{
|
|
126
|
+
SuggestedRoleName: "#{service_name}-codebuild",
|
|
127
|
+
SuggestedPolicyName: "#{service_name}-codebuild-policy",
|
|
128
|
+
AssumeRolePolicyDocument: CODEBUILD_TRUST,
|
|
129
|
+
PolicyDocument: {
|
|
130
|
+
Version: '2012-10-17',
|
|
131
|
+
Statement: [
|
|
132
|
+
{
|
|
133
|
+
Sid: 'WriteBuildLogs',
|
|
134
|
+
Effect: 'Allow',
|
|
135
|
+
Action: %w[logs:CreateLogStream logs:PutLogEvents],
|
|
136
|
+
Resource: "arn:aws:logs:#{region}:#{account_id}:log-group:#{config.build_log_group}:*",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
Sid: 'EcrAuth',
|
|
140
|
+
Effect: 'Allow',
|
|
141
|
+
Action: %w[ecr:GetAuthorizationToken],
|
|
142
|
+
Resource: '*',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
Sid: 'EcrPushPull',
|
|
146
|
+
Effect: 'Allow',
|
|
147
|
+
Action: %w[
|
|
148
|
+
ecr:BatchCheckLayerAvailability ecr:GetDownloadUrlForLayer ecr:BatchGetImage
|
|
149
|
+
ecr:InitiateLayerUpload ecr:UploadLayerPart ecr:CompleteLayerUpload ecr:PutImage
|
|
150
|
+
],
|
|
151
|
+
Resource: config.base[:ecr_repository_arn],
|
|
152
|
+
},
|
|
153
|
+
# Deploy at the end of the build:
|
|
154
|
+
# aws ecs update-service --force-new-deployment (+ wait services-stable)
|
|
155
|
+
{
|
|
156
|
+
Sid: 'DeployService',
|
|
157
|
+
Effect: 'Allow',
|
|
158
|
+
Action: %w[ecs:UpdateService ecs:DescribeServices],
|
|
159
|
+
Resource: "arn:aws:ecs:#{region}:#{account_id}:service/#{config.cluster_name}/#{service_name}",
|
|
160
|
+
},
|
|
161
|
+
# Required because builds run inside the VPC (vpc_config).
|
|
162
|
+
{
|
|
163
|
+
Sid: 'ManageBuildEni',
|
|
164
|
+
Effect: 'Allow',
|
|
165
|
+
Action: %w[
|
|
166
|
+
ec2:CreateNetworkInterface ec2:DescribeNetworkInterfaces ec2:DeleteNetworkInterface
|
|
167
|
+
ec2:DescribeSubnets ec2:DescribeSecurityGroups ec2:DescribeDhcpOptions ec2:DescribeVpcs
|
|
168
|
+
],
|
|
169
|
+
Resource: '*',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
Sid: 'CreateBuildEniPermission',
|
|
173
|
+
Effect: 'Allow',
|
|
174
|
+
Action: %w[ec2:CreateNetworkInterfacePermission],
|
|
175
|
+
Resource: "arn:aws:ec2:#{region}:#{account_id}:network-interface/*",
|
|
176
|
+
Condition: {
|
|
177
|
+
StringEquals: {
|
|
178
|
+
'ec2:AuthorizedService' => 'codebuild.amazonaws.com',
|
|
179
|
+
'ec2:Subnet' => (config.base[:subnets] || []).map{|s|
|
|
180
|
+
"arn:aws:ec2:#{region}:#{account_id}:subnet/#{s}"
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
]
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Assumed by EventBridge Scheduler to launch tasks (scheduler:deploy).
|
|
191
|
+
# Created for every service so that schedules can be added any time.
|
|
192
|
+
def scheduler_role_draft
|
|
193
|
+
{
|
|
194
|
+
SuggestedRoleName: "#{service_name}-scheduler",
|
|
195
|
+
SuggestedPolicyName: "#{service_name}-scheduler-policy",
|
|
196
|
+
AssumeRolePolicyDocument: scheduler_trust,
|
|
197
|
+
PolicyDocument: {
|
|
198
|
+
Version: '2012-10-17',
|
|
199
|
+
Statement: [
|
|
200
|
+
{
|
|
201
|
+
Sid: 'RunScheduledTask',
|
|
202
|
+
Effect: 'Allow',
|
|
203
|
+
Action: %w[ecs:RunTask],
|
|
204
|
+
Resource: [
|
|
205
|
+
"arn:aws:ecs:#{region}:#{account_id}:task-definition/#{service_name}-*",
|
|
206
|
+
"arn:aws:ecs:#{region}:#{account_id}:task-definition/#{service_name}-*:*",
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
Sid: 'PassRolesToTasks',
|
|
211
|
+
Effect: 'Allow',
|
|
212
|
+
Action: %w[iam:PassRole],
|
|
213
|
+
Resource: "arn:aws:iam::#{account_id}:role/#{service_name}-*",
|
|
214
|
+
},
|
|
215
|
+
]
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'pathname'
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module Putpaws
|
|
6
|
+
module Provision
|
|
7
|
+
class Preset
|
|
8
|
+
BUILTIN_DIR = File.expand_path('presets', __dir__)
|
|
9
|
+
|
|
10
|
+
def self.local_dir(path_prefix: '.putpaws')
|
|
11
|
+
Pathname.new(path_prefix).join('provisioning', 'presets')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Search order: project local -> user global (PUTPAWS_PRESETS_PATH dirs,
|
|
15
|
+
# then ~/.putpaws/presets) -> builtin (bundled in the gem).
|
|
16
|
+
def self.search_paths(path_prefix: '.putpaws')
|
|
17
|
+
paths = [[local_dir(path_prefix: path_prefix).to_s, 'local']]
|
|
18
|
+
ENV['PUTPAWS_PRESETS_PATH'].to_s.split(File::PATH_SEPARATOR).reject(&:empty?).each do |dir|
|
|
19
|
+
paths << [dir, 'global']
|
|
20
|
+
end
|
|
21
|
+
home = ENV['HOME'].to_s
|
|
22
|
+
paths << [File.join(home, '.putpaws', 'presets'), 'global'] unless home.empty?
|
|
23
|
+
paths << [BUILTIN_DIR, 'builtin']
|
|
24
|
+
paths
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# [{name:, dir:, source:}] with name collisions resolved by search order.
|
|
28
|
+
def self.catalog(path_prefix: '.putpaws')
|
|
29
|
+
seen = {}
|
|
30
|
+
search_paths(path_prefix: path_prefix).each do |dir, source|
|
|
31
|
+
Dir.glob(File.join(dir, '*/preset.json')).sort.each do |p|
|
|
32
|
+
name = File.basename(File.dirname(p))
|
|
33
|
+
seen[name] ||= {name: name, dir: File.dirname(p), source: source}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
seen.values.sort_by{|e| e[:name]}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.available(path_prefix: '.putpaws')
|
|
40
|
+
catalog(path_prefix: path_prefix).map{|e| e[:name]}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.find(name, path_prefix: '.putpaws')
|
|
44
|
+
entry = catalog(path_prefix: path_prefix).detect{|e| e[:name] == name}
|
|
45
|
+
entry && new(name: entry[:name], dir: entry[:dir])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Copy a preset into .putpaws so that the project owns its snapshot and
|
|
49
|
+
# users can edit it. Does nothing when the local preset already exists.
|
|
50
|
+
def self.install(name, path_prefix: '.putpaws')
|
|
51
|
+
entry = catalog(path_prefix: path_prefix).detect{|e| e[:name] == name}
|
|
52
|
+
raise "Unknown preset: #{name}" unless entry
|
|
53
|
+
return new(name: entry[:name], dir: entry[:dir]) if entry[:source] == 'local'
|
|
54
|
+
local = local_dir(path_prefix: path_prefix).join(name)
|
|
55
|
+
FileUtils.mkdir_p(local.dirname)
|
|
56
|
+
FileUtils.cp_r(entry[:dir], local)
|
|
57
|
+
find(name, path_prefix: path_prefix)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
attr_reader :name, :dir
|
|
61
|
+
def initialize(name:, dir:)
|
|
62
|
+
@name = name
|
|
63
|
+
@dir = dir
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def data
|
|
67
|
+
@data ||= JSON.parse(File.read(File.join(dir, 'preset.json')), symbolize_names: true)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def defaults
|
|
71
|
+
data[:defaults] || {}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def task_definitions
|
|
75
|
+
data[:task_definitions] || []
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def service_task_definition
|
|
79
|
+
data[:service_task_definition]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def devops_defaults
|
|
83
|
+
data[:devops_defaults] || {}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def template_path(file)
|
|
87
|
+
File.join(dir, 'templates', file)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"defaults": {
|
|
3
|
+
"cpu": "256",
|
|
4
|
+
"memory": "512",
|
|
5
|
+
"desired_count": 1,
|
|
6
|
+
"container_port": 3000,
|
|
7
|
+
"log_retention_days": 30,
|
|
8
|
+
"image_tag": "latest",
|
|
9
|
+
"nginx_image": "public.ecr.aws/nginx/nginx:stable",
|
|
10
|
+
"assign_public_ip": "DISABLED",
|
|
11
|
+
"env": {
|
|
12
|
+
"RAILS_ENV": "production",
|
|
13
|
+
"RACK_ENV": "production",
|
|
14
|
+
"RAILS_LOG_TO_STDOUT": "1"
|
|
15
|
+
},
|
|
16
|
+
"secrets": {
|
|
17
|
+
"RAILS_MASTER_KEY": "{ssm_parameter_prefix}/RAILS_MASTER_KEY"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"task_definitions": [
|
|
21
|
+
{ "suffix": "app", "template": "taskdef-app.json.erb", "container_name": "app" },
|
|
22
|
+
{ "suffix": "web", "template": "taskdef-web.json.erb", "container_name": "app" }
|
|
23
|
+
],
|
|
24
|
+
"service_task_definition": "app",
|
|
25
|
+
"devops_defaults": {
|
|
26
|
+
"source_type": "GITHUB",
|
|
27
|
+
"environment_image": "aws/codebuild/standard:7.0",
|
|
28
|
+
"compute_type": "BUILD_GENERAL1_SMALL",
|
|
29
|
+
"privileged_mode": true,
|
|
30
|
+
"buildspec": "buildspec.yml"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"family": "<%= service_name %>-app",
|
|
3
|
+
"requiresCompatibilities": ["FARGATE"],
|
|
4
|
+
"networkMode": "awsvpc",
|
|
5
|
+
"cpu": "<%= cpu %>",
|
|
6
|
+
"memory": "<%= memory %>",
|
|
7
|
+
"executionRoleArn": "<%= execution_role_arn %>",
|
|
8
|
+
"taskRoleArn": "<%= task_role_arn %>",
|
|
9
|
+
"containerDefinitions": [
|
|
10
|
+
{
|
|
11
|
+
"name": "app",
|
|
12
|
+
"image": "<%= image %>",
|
|
13
|
+
"essential": true,
|
|
14
|
+
"portMappings": [
|
|
15
|
+
{ "containerPort": <%= container_port %>, "protocol": "tcp" }
|
|
16
|
+
],
|
|
17
|
+
"environment": <%= env_json %>,
|
|
18
|
+
"secrets": <%= secrets_json %>,
|
|
19
|
+
"logConfiguration": {
|
|
20
|
+
"logDriver": "awslogs",
|
|
21
|
+
"options": {
|
|
22
|
+
"awslogs-group": "<%= log_group %>",
|
|
23
|
+
"awslogs-region": "<%= region %>",
|
|
24
|
+
"awslogs-stream-prefix": "app"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"family": "<%= service_name %>-web",
|
|
3
|
+
"requiresCompatibilities": ["FARGATE"],
|
|
4
|
+
"networkMode": "awsvpc",
|
|
5
|
+
"cpu": "<%= cpu %>",
|
|
6
|
+
"memory": "<%= memory %>",
|
|
7
|
+
"executionRoleArn": "<%= execution_role_arn %>",
|
|
8
|
+
"taskRoleArn": "<%= task_role_arn %>",
|
|
9
|
+
"containerDefinitions": [
|
|
10
|
+
{
|
|
11
|
+
"name": "app",
|
|
12
|
+
"image": "<%= image %>",
|
|
13
|
+
"essential": true,
|
|
14
|
+
"portMappings": [
|
|
15
|
+
{ "containerPort": <%= container_port %>, "protocol": "tcp" }
|
|
16
|
+
],
|
|
17
|
+
"environment": <%= env_json %>,
|
|
18
|
+
"secrets": <%= secrets_json %>,
|
|
19
|
+
"logConfiguration": {
|
|
20
|
+
"logDriver": "awslogs",
|
|
21
|
+
"options": {
|
|
22
|
+
"awslogs-group": "<%= log_group %>",
|
|
23
|
+
"awslogs-region": "<%= region %>",
|
|
24
|
+
"awslogs-stream-prefix": "app"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "nginx",
|
|
30
|
+
"image": "<%= nginx_image %>",
|
|
31
|
+
"essential": true,
|
|
32
|
+
"portMappings": [
|
|
33
|
+
{ "containerPort": 80, "protocol": "tcp" }
|
|
34
|
+
],
|
|
35
|
+
"dependsOn": [
|
|
36
|
+
{ "containerName": "app", "condition": "START" }
|
|
37
|
+
],
|
|
38
|
+
"logConfiguration": {
|
|
39
|
+
"logDriver": "awslogs",
|
|
40
|
+
"options": {
|
|
41
|
+
"awslogs-group": "<%= log_group %>",
|
|
42
|
+
"awslogs-region": "<%= region %>",
|
|
43
|
+
"awslogs-stream-prefix": "nginx"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'pathname'
|
|
3
|
+
require 'putpaws/provision/util'
|
|
4
|
+
require 'putpaws/provision/preset'
|
|
5
|
+
|
|
6
|
+
module Putpaws
|
|
7
|
+
module Provision
|
|
8
|
+
class ProvisionConfig
|
|
9
|
+
def self.dir_for(service_name, path_prefix: '.putpaws')
|
|
10
|
+
Pathname.new(path_prefix).join('provisioning', service_name)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.services(path_prefix: '.putpaws')
|
|
14
|
+
Dir.glob(Pathname.new(path_prefix).join('provisioning', '*', 'provision.json').to_s)
|
|
15
|
+
.map{|p| File.basename(File.dirname(p))}
|
|
16
|
+
.reject{|name| name == 'presets'}
|
|
17
|
+
.sort
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.load(service_name, path_prefix: '.putpaws')
|
|
21
|
+
path = dir_for(service_name, path_prefix: path_prefix).join('provision.json')
|
|
22
|
+
raise "provision.json not found for #{service_name}. Please run `putpaws ready` first." unless path.exist?
|
|
23
|
+
data = JSON.parse(File.read(path), symbolize_names: true)
|
|
24
|
+
new(data, path_prefix: path_prefix)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attr_reader :data, :path_prefix
|
|
28
|
+
def initialize(data, path_prefix: '.putpaws')
|
|
29
|
+
@data = data
|
|
30
|
+
@path_prefix = path_prefix
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def service_name
|
|
34
|
+
data[:service_name]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def region
|
|
38
|
+
data[:region]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def preset
|
|
42
|
+
@preset ||= begin
|
|
43
|
+
p = Preset.find(data[:preset], path_prefix: path_prefix)
|
|
44
|
+
raise "Preset not found: #{data[:preset]}" unless p
|
|
45
|
+
p
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def base
|
|
50
|
+
data[:base] || {}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def roles
|
|
54
|
+
data[:roles] || {}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def overrides
|
|
58
|
+
data[:overrides] || {}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def devops
|
|
62
|
+
data[:devops] || {}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def dir
|
|
66
|
+
self.class.dir_for(service_name, path_prefix: path_prefix)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def policies_dir
|
|
70
|
+
dir.join('policies')
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# preset defaults <- provision.json overrides (nil values ignored).
|
|
74
|
+
# env/secrets hashes are merged by key.
|
|
75
|
+
def settings
|
|
76
|
+
@settings ||= begin
|
|
77
|
+
merged = preset.defaults.dup
|
|
78
|
+
overrides.each do |k, v|
|
|
79
|
+
next if v.nil?
|
|
80
|
+
next if k == :cluster_name
|
|
81
|
+
if v.is_a?(Hash) && merged[k].is_a?(Hash)
|
|
82
|
+
merged[k] = merged[k].merge(v)
|
|
83
|
+
else
|
|
84
|
+
merged[k] = v
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
merged
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def devops_settings
|
|
92
|
+
preset.devops_defaults.merge(devops.reject{|_, v| v.nil?})
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def account_id
|
|
96
|
+
Util.arn_account_id(base[:ecr_repository_arn])
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def ecr_image_uri(tag: nil)
|
|
100
|
+
"#{Util.ecr_repository_uri(base[:ecr_repository_arn])}:#{tag || settings[:image_tag] || 'latest'}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def ssm_parameter_prefix
|
|
104
|
+
base[:ssm_parameter_prefix]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# {ssm_parameter_prefix} placeholders resolved, values as full parameter paths.
|
|
108
|
+
def resolved_secrets
|
|
109
|
+
(settings[:secrets] || {}).map{|name, path|
|
|
110
|
+
[name.to_s, path.to_s.gsub('{ssm_parameter_prefix}', ssm_parameter_prefix.to_s)]
|
|
111
|
+
}.to_h
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def secret_parameter_arns
|
|
115
|
+
resolved_secrets.values.map{|path|
|
|
116
|
+
"arn:aws:ssm:#{region}:#{account_id}:parameter#{path}"
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# ===== naming conventions (service_name prefix) =====
|
|
121
|
+
|
|
122
|
+
def sg_name
|
|
123
|
+
service_name
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def cluster_name
|
|
127
|
+
Util.blank?(overrides[:cluster_name]) ? service_name : overrides[:cluster_name]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def log_group
|
|
131
|
+
"/ecs/#{service_name}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def build_log_group
|
|
135
|
+
"/aws/codebuild/#{service_name}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def build_project_name
|
|
139
|
+
service_name
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def task_definition_family(suffix)
|
|
143
|
+
"#{service_name}-#{suffix}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def service_task_definition_family
|
|
147
|
+
task_definition_family(preset.service_task_definition)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def service_container_name
|
|
151
|
+
entry = preset.task_definitions.detect{|t| t[:suffix] == preset.service_task_definition}
|
|
152
|
+
(entry && entry[:container_name]) || 'app'
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
load File.expand_path("../tasks/provision.rake", __FILE__)
|