putpaws 0.0.9 → 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 +153 -2
- data/lib/Putpawsfile +4 -0
- 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 +15 -2
- 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 +62 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cbb1de2cdc7c119427ca5dfa073f5e8d07f504ff73f93a464f2912a8ef5168ed
|
|
4
|
+
data.tar.gz: 042f6202ae3cb0396b0ab7210b2d299504f7e099e8a0cf43d9c6314f0b6384ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 12b7e736fe78e9a49569627fd296e9f5540cbea849f8b78a7edbb56c8652de73b82d1b2ac92fef279a682f64e4e83e061ba20e2c3afe1b3c99dce7fd35526fe1
|
|
7
|
+
data.tar.gz: efad6347afdc46833694e48070afd0330fe42f8bb0bc16638b6b88a7d5b6d1a091ce50f5ce1ddec4f92cc07e54796f42cc9802c2b5aa07ed4af1d96ba5ebd171
|
data/README.md
CHANGED
|
@@ -1,7 +1,116 @@
|
|
|
1
|
-
# Put your
|
|
1
|
+
# Put your pAWS up!!
|
|
2
|
+
|
|
3
|
+
## Command names
|
|
4
|
+
|
|
5
|
+
The short commands follow a "put your paws ..." image:
|
|
6
|
+
|
|
7
|
+
- `putpaws ready` ... put paws ready: — place your paws and prepare
|
|
8
|
+
- `putpaws steady` ... put paws steady: — hold steady, right before the start
|
|
9
|
+
- `putpaws up` ... put paws up: — everything is ready, raise them up
|
|
10
|
+
- `putpaws ahead` ... put paws ahead: — reach ahead and probe (dry-run)
|
|
11
|
+
- `putpaws away` ... put paws away: — take your paws off and let AI take over
|
|
12
|
+
|
|
13
|
+
## Provisioning
|
|
14
|
+
|
|
15
|
+
Build service infra (security group / ECS cluster / task definitions / service / log groups / CodeBuild) idempotently.
|
|
16
|
+
Deletion is out of scope on purpose: putpaws only creates and updates.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
# Interview basic infra (existing VPC / ECR / SES / S3 / SSM parameter prefix)
|
|
20
|
+
# and generate provision.json + IAM role drafts
|
|
21
|
+
bundle exec putpaws ready
|
|
22
|
+
|
|
23
|
+
# After reviewing/editing the drafts under policies/, create the IAM roles
|
|
24
|
+
# from them as-is and fill their ARNs into provision.json. Idempotent:
|
|
25
|
+
# edit a draft and run again to update the role.
|
|
26
|
+
bundle exec putpaws steady
|
|
27
|
+
|
|
28
|
+
# Dry-run: show what would be created / updated / skipped
|
|
29
|
+
bundle exec putpaws ahead
|
|
30
|
+
|
|
31
|
+
# Build the rest as far as possible. `up` stops with instructions where a manual
|
|
32
|
+
# step is required. Fix things by hand, then just run `up` again. Safe to re-run any time.
|
|
33
|
+
bundle exec putpaws up
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Running `steady` is your explicit confirmation of the drafts: `up` never touches IAM
|
|
37
|
+
and stops until the role ARNs are filled. Filling in an existing role ARN in
|
|
38
|
+
provision.json instead makes `steady` leave that role alone
|
|
39
|
+
(useful when IAM is managed by another team).
|
|
40
|
+
|
|
41
|
+
Files live under `.putpaws/provisioning/`:
|
|
42
|
+
|
|
43
|
+
- `provisioning/{service_name}/provision.json` ... inputs (edit by hand; `steady` fills the role ARNs)
|
|
44
|
+
- `provisioning/{service_name}/state.json` ... created resource info (written by putpaws)
|
|
45
|
+
- `provisioning/{service_name}/policies/` ... IAM role drafts to review, applied as-is by `putpaws steady`
|
|
46
|
+
- `provisioning/presets/{preset}/` ... defaults and task definition templates (copied here on `ready`, edit freely)
|
|
47
|
+
|
|
48
|
+
Presets are searched in this order: project local (`.putpaws/provisioning/presets/`),
|
|
49
|
+
user global (directories in `PUTPAWS_PRESETS_PATH`, then `~/.putpaws/presets/`), and the ones bundled in the gem.
|
|
50
|
+
Whichever you pick on `ready` is copied into the project so the project owns its snapshot.
|
|
51
|
+
|
|
52
|
+
Security groups are either specified (one or more existing ones; putpaws never touches their rules)
|
|
53
|
+
or a new one is created for the service when left empty on `ready`.
|
|
54
|
+
|
|
55
|
+
When the service step completes, the new service is reflected into
|
|
56
|
+
`.putpaws/application.json` and `.putpaws/infra.json` (diff is shown and confirmed),
|
|
57
|
+
so `ecs:attach`, `ecs:shell`, `ecs:run` and `log:*` work immediately.
|
|
58
|
+
Secrets are handled by reference only: task definitions point to existing
|
|
59
|
+
SSM parameters (Ex: `/{service_name}/RAILS_MASTER_KEY`) and putpaws never touches the values.
|
|
60
|
+
|
|
61
|
+
A scheduler role is always created too, and the `target` entry of infra.json carries it,
|
|
62
|
+
so `scheduler:deploy` works any time by just writing `.putpaws/schedule.json`.
|
|
63
|
+
|
|
64
|
+
CodeBuild is created as CI: builds run inside the VPC with the same subnets and
|
|
65
|
+
security group as the service, so `db:migrate` against RDS works from the build.
|
|
66
|
+
Note that the private subnets need a NAT gateway (or VPC endpoints) so that
|
|
67
|
+
builds can reach GitHub / ECR / CloudWatch Logs.
|
|
68
|
+
|
|
69
|
+
## Operator permissions
|
|
70
|
+
|
|
71
|
+
Declare named operator profiles in `.putpaws/operators.json` (generated with examples on first run),
|
|
72
|
+
then resolve them into managed policies per service, idempotently
|
|
73
|
+
(CREATE / UPDATE as a new policy version / SKIP — re-running never piles up policies):
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
bundle exec putpaws awesome-api-staging iam:grant profile=developer
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
{
|
|
81
|
+
"developer": { "groups": ["attach", "shell", "deploy", "logs", "codebuild"] },
|
|
82
|
+
"viewer": { "groups": ["logs"] }
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Groups map to putpaws command namespaces: `attach` (ecs:attach/forward), `shell` (ecs:shell/run —
|
|
87
|
+
note this implies reading all secrets of the service), `deploy` (ecs:deploy), `logs` (log:*),
|
|
88
|
+
`codebuild` (code_build:build), `scheduler` (scheduler:deploy).
|
|
89
|
+
`extra_statements` on a profile appends raw IAM statements.
|
|
90
|
+
The policy is named `{service}-operator-{profile}`. Attaching it to users/groups is left to your admin
|
|
91
|
+
(an attach command example is printed).
|
|
92
|
+
|
|
93
|
+
## AI setup
|
|
94
|
+
|
|
95
|
+
`putpaws away` installs a guide for AI agents (Claude Code etc.) into the project:
|
|
96
|
+
`.claude/skills/putpaws/SKILL.md` (commands, read-only vs mutating classification,
|
|
97
|
+
non-interactive recipes, suggested permission allowlist) and a pointer block in
|
|
98
|
+
`CLAUDE.md` (or `AGENTS.md` when there is no CLAUDE.md). Re-run it after updating putpaws.
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
bundle exec putpaws away
|
|
102
|
+
```
|
|
2
103
|
|
|
3
104
|
## Example
|
|
4
105
|
|
|
106
|
+
### Info
|
|
107
|
+
|
|
108
|
+
Show resolved settings for a service without touching AWS
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
bundle exec putpaws awesome-api-staging info
|
|
112
|
+
```
|
|
113
|
+
|
|
5
114
|
### ECS
|
|
6
115
|
|
|
7
116
|
Attach to staging specific container
|
|
@@ -21,6 +130,46 @@ bundle exec putpaws awesome-api-staging ecs:forward container=app remote=example
|
|
|
21
130
|
mysql -u awesome_user -p --port 1050 -h 127.0.0.1
|
|
22
131
|
```
|
|
23
132
|
|
|
133
|
+
Redeploy the service (update-service with force new deployment).
|
|
134
|
+
With a mutable image tag like `latest`, this rolls out the newly pushed image.
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
bundle exec putpaws awesome-api-staging ecs:deploy
|
|
138
|
+
|
|
139
|
+
# Optionally change desired count / task definition, and wait until stable
|
|
140
|
+
bundle exec putpaws awesome-api-staging ecs:deploy desired=2 taskdef=awesome-api-staging-web wait=true
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Run a command on a temporary task. The task terminates itself when the command finishes.
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
bundle exec putpaws awesome-api-staging ecs:run cmd='bundle exec rake db:migrate'
|
|
147
|
+
|
|
148
|
+
# Pass wait=true to wait until the task stops and check the exit code
|
|
149
|
+
bundle exec putpaws awesome-api-staging ecs:run cmd='bundle exec rake db:migrate' wait=true
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Launch a temporary task for operation and attach to it (like SSH-ing into the environment).
|
|
153
|
+
The task is launched with its command overridden by `sleep <ttl>`, so it always terminates itself when ttl passes even if your shell is gone.
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
# ttl is 30 minutes by default
|
|
157
|
+
bundle exec putpaws awesome-api-staging ecs:shell
|
|
158
|
+
|
|
159
|
+
# Specify ttl: "45" and "45m" mean 45 minutes, "90s" and "2h" also work
|
|
160
|
+
bundle exec putpaws awesome-api-staging ecs:shell ttl=45m
|
|
161
|
+
|
|
162
|
+
# The task is stopped immediately when you exit the shell.
|
|
163
|
+
# Pass keep=true to keep it running until ttl passes.
|
|
164
|
+
bundle exec putpaws awesome-api-staging ecs:shell keep=true
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Temporary tasks are launched with `group` / `started_by` set to `putpaws-run` or `putpaws-shell`,
|
|
168
|
+
so you can identify them on the AWS console and on the task list of `ecs:attach` / `ecs:forward`.
|
|
169
|
+
|
|
170
|
+
`ecs:run` and `ecs:shell` require `network` and `target` in `.putpaws/application.json`,
|
|
171
|
+
which refer to the `network` / `target` sections of `.putpaws/infra.json` (the same ones used by scheduler).
|
|
172
|
+
|
|
24
173
|
### CloudWatch Logs
|
|
25
174
|
|
|
26
175
|
`tail -f`
|
|
@@ -65,7 +214,9 @@ gem 'putpaws'
|
|
|
65
214
|
"log_group_prefix": "/ecs/awesome/awesome-api-staging",
|
|
66
215
|
"log_region": null,
|
|
67
216
|
"build_log_group_prefix": "/aws/codebuild/awesome-api-staging",
|
|
68
|
-
"build_project_name_prefix": "awesome-api-staging"
|
|
217
|
+
"build_project_name_prefix": "awesome-api-staging",
|
|
218
|
+
"network": "awesome-private-staging",
|
|
219
|
+
"target": "awesome-staging"
|
|
69
220
|
},
|
|
70
221
|
"awesome-api-production": {
|
|
71
222
|
"region": "ap-northeast-1",
|
data/lib/Putpawsfile
CHANGED
|
@@ -6,3 +6,7 @@ require "putpaws/ecs/ecs_task"
|
|
|
6
6
|
require "putpaws/cloud_watch/log_task"
|
|
7
7
|
require "putpaws/scheduler/scheduler_task"
|
|
8
8
|
require "putpaws/code_build/build_task"
|
|
9
|
+
require "putpaws/provision/provision_task"
|
|
10
|
+
require "putpaws/iam/iam_task"
|
|
11
|
+
require "putpaws/info"
|
|
12
|
+
require "putpaws/ai/ai_task"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
load File.expand_path("../tasks/ai_task.rake", __FILE__)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'putpaws/version'
|
|
3
|
+
|
|
4
|
+
module Putpaws
|
|
5
|
+
module Ai
|
|
6
|
+
# `putpaws away`: put paws away — take your paws off and let AI take over.
|
|
7
|
+
# Installs a guide for AI agents (Claude Code etc.) into the host project:
|
|
8
|
+
# a skill file plus a pointer block in CLAUDE.md / AGENTS.md.
|
|
9
|
+
# Re-runnable: everything is regenerated in place.
|
|
10
|
+
class Guide
|
|
11
|
+
SKILL_PATH = File.join('.claude', 'skills', 'putpaws', 'SKILL.md')
|
|
12
|
+
BEGIN_MARK = '<!-- putpaws:begin -->'
|
|
13
|
+
END_MARK = '<!-- putpaws:end -->'
|
|
14
|
+
|
|
15
|
+
def self.install!(root: '.', services: nil)
|
|
16
|
+
services ||= Putpaws::ApplicationConfig.all.map(&:name)
|
|
17
|
+
skill_path = File.join(root, SKILL_PATH)
|
|
18
|
+
FileUtils.mkdir_p(File.dirname(skill_path))
|
|
19
|
+
File.write(skill_path, skill_content(services: services))
|
|
20
|
+
pointer_path = pointer_target(root)
|
|
21
|
+
update_pointer!(pointer_path)
|
|
22
|
+
[skill_path, pointer_path]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Prefer an existing CLAUDE.md; otherwise use AGENTS.md (created if absent).
|
|
26
|
+
def self.pointer_target(root)
|
|
27
|
+
claude = File.join(root, 'CLAUDE.md')
|
|
28
|
+
return claude if File.exist?(claude)
|
|
29
|
+
File.join(root, 'AGENTS.md')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.update_pointer!(path)
|
|
33
|
+
block = pointer_block
|
|
34
|
+
content = File.exist?(path) ? File.read(path) : ''
|
|
35
|
+
if content.include?(BEGIN_MARK)
|
|
36
|
+
content = content.sub(/#{Regexp.escape(BEGIN_MARK)}.*#{Regexp.escape(END_MARK)}/m, block.strip)
|
|
37
|
+
else
|
|
38
|
+
content = content.empty? ? block : content.rstrip + "\n\n" + block
|
|
39
|
+
end
|
|
40
|
+
File.write(path, content)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.pointer_block
|
|
44
|
+
<<~MD
|
|
45
|
+
#{BEGIN_MARK}
|
|
46
|
+
## putpaws (AWS operations)
|
|
47
|
+
|
|
48
|
+
This project uses the putpaws gem for AWS/ECS operations (logs, deploy, migrations, one-off tasks).
|
|
49
|
+
Read `.claude/skills/putpaws/SKILL.md` for commands and safety notes before running AWS operations.
|
|
50
|
+
Quick reference: services are the top-level keys of `.putpaws/application.json`;
|
|
51
|
+
`bundle exec putpaws <service> info` shows resolved settings without touching AWS.
|
|
52
|
+
#{END_MARK}
|
|
53
|
+
MD
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.skill_content(services: [])
|
|
57
|
+
services_line = services.empty? ? '(none configured yet — check .putpaws/application.json)' : services.join(', ')
|
|
58
|
+
<<~MD
|
|
59
|
+
---
|
|
60
|
+
name: putpaws
|
|
61
|
+
description: AWS operations for this project (CloudWatch logs, ECS deploy, db:migrate and one-off tasks on Fargate, CodeBuild CI) via the putpaws CLI. Use when asked to investigate logs, deploy, run rake tasks on AWS, or inspect ECS services.
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
# putpaws
|
|
65
|
+
|
|
66
|
+
Capistrano-style AWS operation commands. General form:
|
|
67
|
+
|
|
68
|
+
bundle exec putpaws <service> <command> key=value
|
|
69
|
+
|
|
70
|
+
Services in this project: #{services_line}
|
|
71
|
+
(services are the top-level keys of `.putpaws/application.json`)
|
|
72
|
+
|
|
73
|
+
Discover commands with `bundle exec putpaws -T` (or `-D` for full descriptions).
|
|
74
|
+
|
|
75
|
+
## Read-only commands (safe to run without asking)
|
|
76
|
+
|
|
77
|
+
- `bundle exec putpaws <service> info` — resolved settings, no AWS access
|
|
78
|
+
- `bundle exec putpaws <service> log:tail since=2h` — CloudWatch logs (units: s/m/h/d/w, add `for=1h` to bound the range)
|
|
79
|
+
- `bundle exec putpaws <service> log:tailf` — follow logs (long-running; prefer `log:tail` in automation)
|
|
80
|
+
- `bundle exec putpaws ahead` — dry-run of provisioning (CREATE/UPDATE/SKIP)
|
|
81
|
+
|
|
82
|
+
## Mutating commands (confirm with the user before running)
|
|
83
|
+
|
|
84
|
+
- `bundle exec putpaws <service> ecs:run cmd='bundle exec rake db:migrate' wait=true`
|
|
85
|
+
— runs a one-off command on a temporary Fargate task and exits non-zero on failure.
|
|
86
|
+
The best choice for migrations and rake tasks. Fully non-interactive.
|
|
87
|
+
- `bundle exec putpaws <service> ecs:deploy wait=true` — redeploy the service (force new deployment)
|
|
88
|
+
- `bundle exec putpaws <service> code_build:build branch=main` — start the CI build (build + push + deploy per buildspec)
|
|
89
|
+
- `bundle exec putpaws <service> scheduler:deploy` — deploy schedules from `.putpaws/schedule.json`
|
|
90
|
+
|
|
91
|
+
## Interactive commands (avoid in non-TTY sessions; meant for humans)
|
|
92
|
+
|
|
93
|
+
- `ecs:attach`, `ecs:shell`, `ecs:forward` — open live sessions into containers
|
|
94
|
+
- `ready`, `steady`, `up`, `iam:grant` — provisioning flows with confirmation prompts
|
|
95
|
+
|
|
96
|
+
## Config files (source of truth, safe to read)
|
|
97
|
+
|
|
98
|
+
- `.putpaws/application.json` — services and their cluster/log/build settings
|
|
99
|
+
- `.putpaws/infra.json` — network (subnets/security groups) and target (cluster/taskdef) definitions
|
|
100
|
+
- `.putpaws/schedule.json` — named schedules, `.putpaws/operators.json` — operator permission profiles
|
|
101
|
+
- `.putpaws/provisioning/<service>/` — provisioning inputs (provision.json), state, IAM role drafts
|
|
102
|
+
|
|
103
|
+
## Suggested permission allowlist (.claude/settings.json)
|
|
104
|
+
|
|
105
|
+
"permissions": {
|
|
106
|
+
"allow": [
|
|
107
|
+
"Bash(bundle exec putpaws -T*)",
|
|
108
|
+
"Bash(bundle exec putpaws * info)",
|
|
109
|
+
"Bash(bundle exec putpaws * log:*)"
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
Generated by putpaws #{Putpaws::VERSION}. Refresh with `bundle exec putpaws away` after updating the gem.
|
|
115
|
+
MD
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require "putpaws/ai/guide"
|
|
2
|
+
|
|
3
|
+
namespace :ai do
|
|
4
|
+
desc "Install the AI agent guide (.claude/skills/putpaws/SKILL.md + a pointer in CLAUDE.md/AGENTS.md). Re-runnable. (alias: away)"
|
|
5
|
+
task :install do
|
|
6
|
+
files = Putpaws::Ai::Guide.install!
|
|
7
|
+
puts "Generated:"
|
|
8
|
+
files.each{|f| puts " #{f}"}
|
|
9
|
+
puts "Re-run `bundle exec putpaws away` after updating putpaws to refresh the guide."
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
desc "Alias of ai:install"
|
|
14
|
+
task away: 'ai:install'
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
require 'json'
|
|
2
2
|
require 'pathname'
|
|
3
3
|
require "putpaws/schedule_config"
|
|
4
|
+
require "putpaws/infra_network_config"
|
|
5
|
+
require "putpaws/infra_target_config"
|
|
4
6
|
|
|
5
7
|
class Putpaws::ApplicationConfig < Struct.new(
|
|
6
|
-
:name, :region,
|
|
8
|
+
:name, :region,
|
|
7
9
|
:cluster, :service, :task_name_prefix, :ecs_region,
|
|
8
10
|
:log_group_prefix, :log_region,
|
|
9
11
|
:build_project_name_prefix, :build_log_group_prefix, :build_region,
|
|
10
12
|
:schedules,
|
|
13
|
+
:network, :target,
|
|
11
14
|
keyword_init: true)
|
|
12
15
|
def self.load(path_prefix: '.putpaws')
|
|
13
16
|
@application_data ||= begin
|
|
@@ -37,7 +40,7 @@ class Putpaws::ApplicationConfig < Struct.new(
|
|
|
37
40
|
{
|
|
38
41
|
region: ecs_region || region,
|
|
39
42
|
cluster: cluster,
|
|
40
|
-
|
|
43
|
+
service: service,
|
|
41
44
|
task_name_prefix: task_name_prefix,
|
|
42
45
|
}
|
|
43
46
|
end
|
|
@@ -60,6 +63,16 @@ class Putpaws::ApplicationConfig < Struct.new(
|
|
|
60
63
|
@schedules ||= (self[:schedules] || []).map{|x| Putpaws::ScheduleConfig.find(x)}.compact
|
|
61
64
|
end
|
|
62
65
|
|
|
66
|
+
def network
|
|
67
|
+
return nil unless self[:network]
|
|
68
|
+
@network ||= Putpaws::InfraNetworkConfig.find(self[:network])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def target
|
|
72
|
+
return nil unless self[:target]
|
|
73
|
+
@target ||= Putpaws::InfraTargetConfig.find(self[:target])
|
|
74
|
+
end
|
|
75
|
+
|
|
63
76
|
def codebuild_command_params
|
|
64
77
|
{
|
|
65
78
|
region: build_region || region,
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
require 'aws-sdk-ecs'
|
|
2
|
+
|
|
3
|
+
module Putpaws::Ecs
|
|
4
|
+
class RunCommand
|
|
5
|
+
DEFAULT_TTL = '30m'
|
|
6
|
+
SECONDS = {
|
|
7
|
+
's' => 1,
|
|
8
|
+
'm' => 60,
|
|
9
|
+
'h' => 60 * 60,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
def self.config(config)
|
|
13
|
+
unless config.network && config.target
|
|
14
|
+
raise "Please set `network` and `target` at .putpaws/application.json\n" +
|
|
15
|
+
"They refer to `network` and `target` sections at .putpaws/infra.json\n" +
|
|
16
|
+
"(Ex) \"network\": \"awesome-private-staging\", \"target\": \"awesome-staging\""
|
|
17
|
+
end
|
|
18
|
+
new(
|
|
19
|
+
region: config.ecs_region || config.region,
|
|
20
|
+
network: config.network,
|
|
21
|
+
target: config.target,
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# "45" and "45m" mean 45 minutes. "90s" and "2h" are also acceptable.
|
|
26
|
+
def self.parse_ttl(ut)
|
|
27
|
+
ut = DEFAULT_TTL if ut.nil? || ut.strip.empty?
|
|
28
|
+
matched, number, unit = ut.strip.match(/\A(\d+)([smh])?\z/).to_a
|
|
29
|
+
raise "Invalid ttl: #{ut} (Ex) ttl=30m ttl=2h ttl=90s" unless matched
|
|
30
|
+
number.to_i * SECONDS[unit || 'm']
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
attr_reader :ecs_client
|
|
34
|
+
attr_reader :region, :network, :target
|
|
35
|
+
def initialize(region:, network:, target:)
|
|
36
|
+
@ecs_client = Aws::ECS::Client.new({region: region})
|
|
37
|
+
@region = region
|
|
38
|
+
@network = network
|
|
39
|
+
@target = target
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def cluster_name
|
|
43
|
+
target.cluster.split('/').last
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def container_name
|
|
47
|
+
target.container_name || 'app'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Launch a temporary task whose main process is overridden with the given
|
|
51
|
+
# command, so that the task terminates itself when the command finishes.
|
|
52
|
+
# started_by and group are set so that temporary tasks are identifiable.
|
|
53
|
+
def run_ecs_task(command:, started_by:, group:, container: nil)
|
|
54
|
+
res = ecs_client.run_task({
|
|
55
|
+
cluster: target.cluster,
|
|
56
|
+
task_definition: target.task_definition,
|
|
57
|
+
count: 1,
|
|
58
|
+
launch_type: 'FARGATE',
|
|
59
|
+
enable_execute_command: true,
|
|
60
|
+
started_by: started_by,
|
|
61
|
+
group: group,
|
|
62
|
+
network_configuration: {
|
|
63
|
+
awsvpc_configuration: {
|
|
64
|
+
subnets: network.subnets,
|
|
65
|
+
security_groups: network.security_groups,
|
|
66
|
+
assign_public_ip: network.assign_public_ip,
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
overrides: {
|
|
70
|
+
container_overrides: [
|
|
71
|
+
{
|
|
72
|
+
name: container || container_name,
|
|
73
|
+
command: ['/bin/sh', '-c', command],
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
failure = res.failures.first
|
|
79
|
+
raise "Failed to run task: #{failure.reason} #{failure.detail}" if failure
|
|
80
|
+
res.tasks.first
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def wait_until_attachable(task_arn, timeout: 600, interval: 5)
|
|
84
|
+
deadline = Time.now + timeout
|
|
85
|
+
loop do
|
|
86
|
+
res = ecs_client.describe_tasks(cluster: target.cluster, tasks: [task_arn])
|
|
87
|
+
task = res.tasks.first
|
|
88
|
+
if task.nil? || task.last_status == 'STOPPED'
|
|
89
|
+
raise "Task was stopped: #{task && task.stopped_reason}"
|
|
90
|
+
end
|
|
91
|
+
ctn = task.containers.detect{|c| c.name == container_name}
|
|
92
|
+
agent = ctn && (ctn.managed_agents || []).detect{|a| a.name == 'ExecuteCommandAgent'}
|
|
93
|
+
return task if task.last_status == 'RUNNING' && agent && agent.last_status == 'RUNNING'
|
|
94
|
+
raise "Timed out waiting for task to be attachable: #{task_arn}" if Time.now > deadline
|
|
95
|
+
sleep interval
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def wait_until_stopped(task_arn, timeout: 3600, interval: 10)
|
|
100
|
+
deadline = Time.now + timeout
|
|
101
|
+
loop do
|
|
102
|
+
res = ecs_client.describe_tasks(cluster: target.cluster, tasks: [task_arn])
|
|
103
|
+
task = res.tasks.first
|
|
104
|
+
raise "Task not found: #{task_arn}" unless task
|
|
105
|
+
return task if task.last_status == 'STOPPED'
|
|
106
|
+
raise "Timed out waiting for task to stop: #{task_arn}" if Time.now > deadline
|
|
107
|
+
sleep interval
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def stop_ecs_task(task_arn, reason: 'Stopped by putpaws')
|
|
112
|
+
ecs_client.stop_task(cluster: target.cluster, task: task_arn, reason: reason)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require 'socket'
|
|
1
2
|
require 'aws-sdk-ecs'
|
|
2
3
|
require 'aws-sdk-ssm'
|
|
3
4
|
|
|
@@ -8,18 +9,20 @@ module Putpaws::Ecs
|
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
attr_reader :ecs_client
|
|
11
|
-
attr_reader :region, :cluster, :task_name_prefix
|
|
12
|
+
attr_reader :region, :cluster, :service, :task_name_prefix
|
|
12
13
|
attr_accessor :ecs_task
|
|
13
|
-
def initialize(region:, cluster:, task_name_prefix: nil)
|
|
14
|
+
def initialize(region:, cluster:, service: nil, task_name_prefix: nil)
|
|
14
15
|
@ecs_client = Aws::ECS::Client.new({region: region})
|
|
15
16
|
@region = region
|
|
16
17
|
@cluster = cluster
|
|
18
|
+
@service = service
|
|
17
19
|
@task_name_prefix = task_name_prefix
|
|
18
20
|
@ecs_task = nil
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
def list_ecs_tasks
|
|
22
24
|
res = ecs_client.list_tasks(cluster: cluster)
|
|
25
|
+
return [] if res.task_arns.empty?
|
|
23
26
|
res = ecs_client.describe_tasks(tasks: res.task_arns, cluster: cluster)
|
|
24
27
|
return res.tasks unless task_name_prefix
|
|
25
28
|
res.tasks.select{|t|
|
|
@@ -28,6 +31,36 @@ module Putpaws::Ecs
|
|
|
28
31
|
}
|
|
29
32
|
end
|
|
30
33
|
|
|
34
|
+
def list_ecs_services
|
|
35
|
+
res = ecs_client.list_services(cluster: cluster, max_results: 100)
|
|
36
|
+
return [] if res.service_arns.empty?
|
|
37
|
+
res = ecs_client.describe_services(cluster: cluster, services: res.service_arns)
|
|
38
|
+
services = res.services.select{|s| s.status == 'ACTIVE'}
|
|
39
|
+
return services unless task_name_prefix
|
|
40
|
+
filtered = services.select{|s| s.service_name.start_with?(task_name_prefix)}
|
|
41
|
+
filtered.empty? ? services : filtered
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def update_ecs_service(service:, desired_count: nil, task_definition: nil)
|
|
45
|
+
params = {
|
|
46
|
+
cluster: cluster,
|
|
47
|
+
service: service,
|
|
48
|
+
force_new_deployment: true,
|
|
49
|
+
}
|
|
50
|
+
params[:desired_count] = desired_count.to_i if desired_count
|
|
51
|
+
params[:task_definition] = task_definition if task_definition
|
|
52
|
+
res = ecs_client.update_service(**params)
|
|
53
|
+
res.service
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def wait_ecs_service_stable(service:, timeout: 600)
|
|
57
|
+
ecs_client.wait_until(
|
|
58
|
+
:services_stable,
|
|
59
|
+
{cluster: cluster, services: [service]},
|
|
60
|
+
{delay: 15, max_attempts: (timeout / 15.0).ceil}
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
31
64
|
def get_session_target(container: 'app')
|
|
32
65
|
raise "ECS Task Not Set" unless ecs_task
|
|
33
66
|
ctn = ecs_task.containers.detect{|c| c.name == container}
|
|
@@ -36,11 +69,31 @@ module Putpaws::Ecs
|
|
|
36
69
|
"ecs:#{cluster}_#{task_id}_#{ctn.runtime_id}"
|
|
37
70
|
end
|
|
38
71
|
|
|
72
|
+
def local_port_available?(port)
|
|
73
|
+
TCPServer.new('127.0.0.1', port.to_i).close
|
|
74
|
+
true
|
|
75
|
+
rescue Errno::EADDRINUSE, Errno::EACCES
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def resolve_local_port(local_port)
|
|
80
|
+
if local_port
|
|
81
|
+
unless local_port_available?(local_port)
|
|
82
|
+
raise "Local port #{local_port} is already in use"
|
|
83
|
+
end
|
|
84
|
+
local_port.to_s
|
|
85
|
+
else
|
|
86
|
+
port = (1050..1079).to_a.shuffle.detect{|p| local_port_available?(p)}
|
|
87
|
+
raise "No available local port between 1050 and 1079. Please specify one like: local=:8080" unless port
|
|
88
|
+
port.to_s
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
39
92
|
def get_port_forwarding_command(container: nil, remote_port:, remote_host:, local_port: nil)
|
|
40
93
|
container ||= 'app'
|
|
41
94
|
target = get_session_target(container: container)
|
|
42
95
|
ssm_client = Aws::SSM::Client.new({region: region})
|
|
43
|
-
local_port
|
|
96
|
+
local_port = resolve_local_port(local_port)
|
|
44
97
|
puts "Starting to use local port: #{local_port}"
|
|
45
98
|
res = ssm_client.start_session({
|
|
46
99
|
target: target,
|
|
@@ -67,10 +120,12 @@ module Putpaws::Ecs
|
|
|
67
120
|
build_session_manager_plugin_command(session: res.session, target: target)
|
|
68
121
|
end
|
|
69
122
|
|
|
123
|
+
# Returns an argument array so that callers can spawn session-manager-plugin
|
|
124
|
+
# without a shell like `system(*cmd)`.
|
|
125
|
+
# https://github.com/aws/aws-cli/blob/2a6136010d8656a605d41d1e7b5fdab3c2930cad/awscli/customizations/ecs/executecommand.py#L105
|
|
70
126
|
def build_session_manager_plugin_command(session:, target:)
|
|
71
127
|
ssm_region = ENV['AWS_REGION_SSM'] || @region
|
|
72
128
|
|
|
73
|
-
# https://github.com/aws/aws-cli/blob/2a6136010d8656a605d41d1e7b5fdab3c2930cad/awscli/customizations/ecs/executecommand.py#L105
|
|
74
129
|
session_json = if session.respond_to?(:session_id)
|
|
75
130
|
{
|
|
76
131
|
"SessionId" => session.session_id,
|
|
@@ -85,16 +140,15 @@ module Putpaws::Ecs
|
|
|
85
140
|
target_json = {
|
|
86
141
|
"Target" => target
|
|
87
142
|
}.to_json
|
|
88
|
-
|
|
143
|
+
[
|
|
89
144
|
"session-manager-plugin",
|
|
90
|
-
session_json
|
|
145
|
+
session_json,
|
|
91
146
|
@region,
|
|
92
147
|
"StartSession",
|
|
93
|
-
'
|
|
94
|
-
target_json
|
|
148
|
+
ENV['AWS_PROFILE'].to_s,
|
|
149
|
+
target_json,
|
|
95
150
|
"https://ssm.#{ssm_region}.amazonaws.com"
|
|
96
151
|
]
|
|
97
|
-
cmd.join(' ')
|
|
98
152
|
end
|
|
99
153
|
end
|
|
100
154
|
end
|