claude-agent-framework 1.0.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.
- package/README.md +128 -0
- package/bin/claude-framework +3 -0
- package/framework/agents/design-lead.md +240 -0
- package/framework/agents/product-owner.md +179 -0
- package/framework/agents/tech-lead.md +226 -0
- package/framework/commands/ayuda.md +127 -0
- package/framework/commands/a/303/261adir.md +98 -0
- package/framework/commands/backup.md +397 -0
- package/framework/commands/cambiar.md +110 -0
- package/framework/commands/cloud.md +457 -0
- package/framework/commands/code.md +142 -0
- package/framework/commands/debug.md +334 -0
- package/framework/commands/deploy.md +383 -0
- package/framework/commands/deshacer.md +120 -0
- package/framework/commands/estado.md +218 -0
- package/framework/commands/explica.md +227 -0
- package/framework/commands/feature.md +120 -0
- package/framework/commands/git.md +427 -0
- package/framework/commands/historial.md +202 -0
- package/framework/commands/learn.md +408 -0
- package/framework/commands/movil.md +245 -0
- package/framework/commands/nuevo.md +118 -0
- package/framework/commands/plan.md +134 -0
- package/framework/commands/prd.md +113 -0
- package/framework/commands/probar.md +148 -0
- package/framework/commands/revisar.md +208 -0
- package/framework/commands/seeds.md +230 -0
- package/framework/commands/seguridad.md +226 -0
- package/framework/commands/tasks.md +157 -0
- package/framework/skills/architecture/algorithms.md +970 -0
- package/framework/skills/architecture/clean-code.md +1080 -0
- package/framework/skills/architecture/design-patterns.md +1984 -0
- package/framework/skills/architecture/functional-programming.md +972 -0
- package/framework/skills/architecture/solid.md +991 -0
- package/framework/skills/cloud/cloud-aws.md +848 -0
- package/framework/skills/cloud/cloud-azure.md +931 -0
- package/framework/skills/cloud/cloud-gcp.md +848 -0
- package/framework/skills/cloud/message-queues.md +1229 -0
- package/framework/skills/core/accessibility.md +401 -0
- package/framework/skills/core/api.md +474 -0
- package/framework/skills/core/authentication.md +306 -0
- package/framework/skills/core/authorization.md +388 -0
- package/framework/skills/core/background-jobs.md +341 -0
- package/framework/skills/core/caching.md +473 -0
- package/framework/skills/core/code-review.md +341 -0
- package/framework/skills/core/controllers.md +290 -0
- package/framework/skills/core/cua.md +285 -0
- package/framework/skills/core/documentation.md +472 -0
- package/framework/skills/core/file-uploads.md +351 -0
- package/framework/skills/core/hotwire-native.md +296 -0
- package/framework/skills/core/hotwire.md +278 -0
- package/framework/skills/core/i18n.md +334 -0
- package/framework/skills/core/imports-exports.md +750 -0
- package/framework/skills/core/infrastructure.md +337 -0
- package/framework/skills/core/models.md +228 -0
- package/framework/skills/core/notifications.md +672 -0
- package/framework/skills/core/payments.md +581 -0
- package/framework/skills/core/performance.md +361 -0
- package/framework/skills/core/rails-scaffold.md +131 -0
- package/framework/skills/core/search.md +518 -0
- package/framework/skills/core/security.md +565 -0
- package/framework/skills/core/seeds.md +307 -0
- package/framework/skills/core/seo.md +542 -0
- package/framework/skills/core/testing.md +393 -0
- package/framework/skills/core/views.md +260 -0
- package/framework/skills/core/websockets.md +564 -0
- package/framework/skills/data/advanced-sql.md +1204 -0
- package/framework/skills/data/nosql.md +1141 -0
- package/framework/skills/devops/containers-advanced.md +1237 -0
- package/framework/skills/devops/debugging.md +834 -0
- package/framework/skills/devops/git-workflow.md +752 -0
- package/framework/skills/devops/networking.md +932 -0
- package/framework/skills/devops/shell-scripting.md +1132 -0
- package/framework/sub-agents/architecture-patterns-agent.md +1450 -0
- package/framework/sub-agents/cloud-agent.md +677 -0
- package/framework/sub-agents/data.md +504 -0
- package/framework/sub-agents/debugging-agent.md +554 -0
- package/framework/sub-agents/devops.md +483 -0
- package/framework/sub-agents/docs.md +176 -0
- package/framework/sub-agents/frontend-dev.md +349 -0
- package/framework/sub-agents/git-workflow-agent.md +697 -0
- package/framework/sub-agents/integrations.md +630 -0
- package/framework/sub-agents/native-dev.md +434 -0
- package/framework/sub-agents/qa.md +138 -0
- package/framework/sub-agents/rails-dev.md +375 -0
- package/framework/sub-agents/security.md +526 -0
- package/framework/sub-agents/ui.md +437 -0
- package/framework/sub-agents/ux.md +284 -0
- package/framework/templates/api-spec.md +500 -0
- package/framework/templates/component-spec.md +248 -0
- package/framework/templates/feature.json +13 -0
- package/framework/templates/model-spec.md +318 -0
- package/framework/templates/prd-template.md +80 -0
- package/framework/templates/task-plan.md +122 -0
- package/framework/templates/task-user-story.md +52 -0
- package/framework/templates/technical-spec.md +260 -0
- package/framework/templates/user-story.md +95 -0
- package/package.json +42 -0
- package/project-templates/CLAUDE.md +42 -0
- package/project-templates/contexts/architecture.md +25 -0
- package/project-templates/contexts/conventions.md +46 -0
- package/project-templates/contexts/design-system.md +47 -0
- package/project-templates/contexts/requirements.md +38 -0
- package/project-templates/contexts/stack.md +30 -0
- package/project-templates/history/active/models.md +11 -0
- package/project-templates/history/changelog.md +15 -0
- package/project-templates/workspace/.gitkeep +0 -0
- package/src/cli.js +52 -0
- package/src/init.js +104 -0
- package/src/status.js +75 -0
- package/src/update.js +88 -0
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
# Skill: Amazon Web Services (AWS) para Rails
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Configurar, desplegar y gestionar aplicaciones Rails en Amazon Web Services, aprovechando los servicios cloud para escalabilidad, rendimiento y seguridad.
|
|
6
|
+
|
|
7
|
+
## EC2 (Elastic Compute Cloud)
|
|
8
|
+
|
|
9
|
+
### Configurar instancia para Rails
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Conectar a la instancia
|
|
13
|
+
ssh -i "mi-key.pem" ec2-user@ec2-xx-xx-xx-xx.compute-1.amazonaws.com
|
|
14
|
+
|
|
15
|
+
# Instalar dependencias (Amazon Linux 2023)
|
|
16
|
+
sudo dnf update -y
|
|
17
|
+
sudo dnf install -y git gcc make openssl-devel readline-devel zlib-devel
|
|
18
|
+
|
|
19
|
+
# Instalar rbenv y Ruby
|
|
20
|
+
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
|
|
21
|
+
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
|
|
22
|
+
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
|
|
23
|
+
source ~/.bashrc
|
|
24
|
+
|
|
25
|
+
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
|
|
26
|
+
rbenv install 3.3.0
|
|
27
|
+
rbenv global 3.3.0
|
|
28
|
+
|
|
29
|
+
# Instalar Rails
|
|
30
|
+
gem install bundler rails
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Security Groups
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
# Terraform o CloudFormation para Security Groups
|
|
37
|
+
# security_group.tf
|
|
38
|
+
|
|
39
|
+
resource "aws_security_group" "rails_app" {
|
|
40
|
+
name = "rails-app-sg"
|
|
41
|
+
description = "Security group for Rails application"
|
|
42
|
+
vpc_id = aws_vpc.main.id
|
|
43
|
+
|
|
44
|
+
# SSH
|
|
45
|
+
ingress {
|
|
46
|
+
from_port = 22
|
|
47
|
+
to_port = 22
|
|
48
|
+
protocol = "tcp"
|
|
49
|
+
cidr_blocks = ["TU_IP/32"] # Solo tu IP
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# HTTP
|
|
53
|
+
ingress {
|
|
54
|
+
from_port = 80
|
|
55
|
+
to_port = 80
|
|
56
|
+
protocol = "tcp"
|
|
57
|
+
cidr_blocks = ["0.0.0.0/0"]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# HTTPS
|
|
61
|
+
ingress {
|
|
62
|
+
from_port = 443
|
|
63
|
+
to_port = 443
|
|
64
|
+
protocol = "tcp"
|
|
65
|
+
cidr_blocks = ["0.0.0.0/0"]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Rails en desarrollo
|
|
69
|
+
ingress {
|
|
70
|
+
from_port = 3000
|
|
71
|
+
to_port = 3000
|
|
72
|
+
protocol = "tcp"
|
|
73
|
+
cidr_blocks = ["0.0.0.0/0"]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
egress {
|
|
77
|
+
from_port = 0
|
|
78
|
+
to_port = 0
|
|
79
|
+
protocol = "-1"
|
|
80
|
+
cidr_blocks = ["0.0.0.0/0"]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### AMI personalizada para Rails
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Después de configurar la instancia, crear AMI
|
|
89
|
+
aws ec2 create-image \
|
|
90
|
+
--instance-id i-1234567890abcdef0 \
|
|
91
|
+
--name "Rails-App-AMI-$(date +%Y%m%d)" \
|
|
92
|
+
--description "Rails app with Ruby 3.3, dependencies"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## S3 (Simple Storage Service)
|
|
96
|
+
|
|
97
|
+
### Configurar Active Storage con S3
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
# Gemfile
|
|
101
|
+
gem "aws-sdk-s3", require: false
|
|
102
|
+
|
|
103
|
+
# config/storage.yml
|
|
104
|
+
amazon:
|
|
105
|
+
service: S3
|
|
106
|
+
access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
|
|
107
|
+
secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
|
|
108
|
+
region: us-east-1
|
|
109
|
+
bucket: <%= ENV.fetch("S3_BUCKET") { "mi-app-#{Rails.env}" } %>
|
|
110
|
+
|
|
111
|
+
# Para uploads públicos
|
|
112
|
+
amazon_public:
|
|
113
|
+
service: S3
|
|
114
|
+
access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
|
|
115
|
+
secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
|
|
116
|
+
region: us-east-1
|
|
117
|
+
bucket: mi-app-public
|
|
118
|
+
public: true
|
|
119
|
+
|
|
120
|
+
# config/environments/production.rb
|
|
121
|
+
config.active_storage.service = :amazon
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Bucket Policy para Active Storage
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"Version": "2012-10-17",
|
|
129
|
+
"Statement": [
|
|
130
|
+
{
|
|
131
|
+
"Sid": "AllowRailsApp",
|
|
132
|
+
"Effect": "Allow",
|
|
133
|
+
"Principal": {
|
|
134
|
+
"AWS": "arn:aws:iam::123456789012:user/rails-app-user"
|
|
135
|
+
},
|
|
136
|
+
"Action": [
|
|
137
|
+
"s3:PutObject",
|
|
138
|
+
"s3:GetObject",
|
|
139
|
+
"s3:DeleteObject",
|
|
140
|
+
"s3:ListBucket"
|
|
141
|
+
],
|
|
142
|
+
"Resource": [
|
|
143
|
+
"arn:aws:s3:::mi-app-production",
|
|
144
|
+
"arn:aws:s3:::mi-app-production/*"
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Presigned URLs
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
# app/services/s3_presigner.rb
|
|
155
|
+
class S3Presigner
|
|
156
|
+
def initialize
|
|
157
|
+
@client = Aws::S3::Client.new(
|
|
158
|
+
region: ENV["AWS_REGION"],
|
|
159
|
+
credentials: Aws::Credentials.new(
|
|
160
|
+
Rails.application.credentials.dig(:aws, :access_key_id),
|
|
161
|
+
Rails.application.credentials.dig(:aws, :secret_access_key)
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
@signer = Aws::S3::Presigner.new(client: @client)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# URL para subir archivo
|
|
168
|
+
def presigned_put(key, content_type:, expires_in: 900)
|
|
169
|
+
@signer.presigned_url(:put_object,
|
|
170
|
+
bucket: ENV["S3_BUCKET"],
|
|
171
|
+
key: key,
|
|
172
|
+
content_type: content_type,
|
|
173
|
+
expires_in: expires_in
|
|
174
|
+
)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# URL para descargar archivo (privado)
|
|
178
|
+
def presigned_get(key, expires_in: 3600, filename: nil)
|
|
179
|
+
options = {
|
|
180
|
+
bucket: ENV["S3_BUCKET"],
|
|
181
|
+
key: key,
|
|
182
|
+
expires_in: expires_in
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if filename
|
|
186
|
+
options[:response_content_disposition] = "attachment; filename=\"#{filename}\""
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
@signer.presigned_url(:get_object, options)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Uso en controller
|
|
194
|
+
class UploadsController < ApplicationController
|
|
195
|
+
def presigned_url
|
|
196
|
+
presigner = S3Presigner.new
|
|
197
|
+
key = "uploads/#{SecureRandom.uuid}/#{params[:filename]}"
|
|
198
|
+
|
|
199
|
+
render json: {
|
|
200
|
+
url: presigner.presigned_put(key, content_type: params[:content_type]),
|
|
201
|
+
key: key
|
|
202
|
+
}
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Configurar CORS en S3
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
[
|
|
211
|
+
{
|
|
212
|
+
"AllowedHeaders": ["*"],
|
|
213
|
+
"AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
|
|
214
|
+
"AllowedOrigins": ["https://miapp.com", "http://localhost:3000"],
|
|
215
|
+
"ExposeHeaders": ["ETag"],
|
|
216
|
+
"MaxAgeSeconds": 3000
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## RDS (Relational Database Service)
|
|
222
|
+
|
|
223
|
+
### Configurar Rails con RDS PostgreSQL
|
|
224
|
+
|
|
225
|
+
```yaml
|
|
226
|
+
# config/database.yml
|
|
227
|
+
production:
|
|
228
|
+
adapter: postgresql
|
|
229
|
+
encoding: unicode
|
|
230
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
|
231
|
+
database: <%= ENV["RDS_DB_NAME"] %>
|
|
232
|
+
username: <%= ENV["RDS_USERNAME"] %>
|
|
233
|
+
password: <%= ENV["RDS_PASSWORD"] %>
|
|
234
|
+
host: <%= ENV["RDS_HOSTNAME"] %>
|
|
235
|
+
port: <%= ENV["RDS_PORT"] || 5432 %>
|
|
236
|
+
|
|
237
|
+
# Configuraciones de conexión
|
|
238
|
+
connect_timeout: 5
|
|
239
|
+
checkout_timeout: 5
|
|
240
|
+
reaping_frequency: 10
|
|
241
|
+
dead_connection_timeout: 5
|
|
242
|
+
|
|
243
|
+
# SSL en producción
|
|
244
|
+
sslmode: require
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Read Replicas
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
# config/database.yml con read replica
|
|
251
|
+
production:
|
|
252
|
+
primary:
|
|
253
|
+
adapter: postgresql
|
|
254
|
+
database: <%= ENV["RDS_DB_NAME"] %>
|
|
255
|
+
username: <%= ENV["RDS_USERNAME"] %>
|
|
256
|
+
password: <%= ENV["RDS_PASSWORD"] %>
|
|
257
|
+
host: <%= ENV["RDS_PRIMARY_HOST"] %>
|
|
258
|
+
|
|
259
|
+
primary_replica:
|
|
260
|
+
adapter: postgresql
|
|
261
|
+
database: <%= ENV["RDS_DB_NAME"] %>
|
|
262
|
+
username: <%= ENV["RDS_USERNAME"] %>
|
|
263
|
+
password: <%= ENV["RDS_PASSWORD"] %>
|
|
264
|
+
host: <%= ENV["RDS_REPLICA_HOST"] %>
|
|
265
|
+
replica: true
|
|
266
|
+
|
|
267
|
+
# app/models/application_record.rb
|
|
268
|
+
class ApplicationRecord < ActiveRecord::Base
|
|
269
|
+
primary_abstract_class
|
|
270
|
+
|
|
271
|
+
# Usar replica para lecturas pesadas
|
|
272
|
+
connects_to database: { writing: :primary, reading: :primary_replica }
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Uso en controladores
|
|
276
|
+
class ReportsController < ApplicationController
|
|
277
|
+
def index
|
|
278
|
+
ActiveRecord::Base.connected_to(role: :reading) do
|
|
279
|
+
@reports = Report.complex_query.to_a
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Parameter Groups
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
# Configuración recomendada para Rails (via AWS CLI o Console)
|
|
289
|
+
# Crear parameter group personalizado
|
|
290
|
+
|
|
291
|
+
# Parámetros importantes:
|
|
292
|
+
# - shared_buffers: 25% de la memoria
|
|
293
|
+
# - effective_cache_size: 75% de la memoria
|
|
294
|
+
# - work_mem: 64MB
|
|
295
|
+
# - maintenance_work_mem: 512MB
|
|
296
|
+
# - checkpoint_completion_target: 0.9
|
|
297
|
+
# - wal_buffers: 16MB
|
|
298
|
+
# - random_page_cost: 1.1 (para SSD)
|
|
299
|
+
# - log_statement: 'all' (desarrollo)
|
|
300
|
+
# - log_min_duration_statement: 1000 (queries > 1s)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Lambda
|
|
304
|
+
|
|
305
|
+
### Rails con Lambda via Jets o Lamby
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
# Gemfile
|
|
309
|
+
gem "lamby"
|
|
310
|
+
|
|
311
|
+
# config/lamby.rb
|
|
312
|
+
Lamby.config do |config|
|
|
313
|
+
config.rack_app = Rails.application
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# handler.rb
|
|
317
|
+
require_relative "config/boot"
|
|
318
|
+
require "lamby"
|
|
319
|
+
require_relative "config/application"
|
|
320
|
+
require_relative "config/environment"
|
|
321
|
+
|
|
322
|
+
$app = Rack::Builder.new { run Rails.application }.to_app
|
|
323
|
+
|
|
324
|
+
def handler(event:, context:)
|
|
325
|
+
Lamby.handler($app, event, context)
|
|
326
|
+
end
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### API Gateway + Lambda para Webhooks
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
# Función Lambda para procesar webhooks
|
|
333
|
+
# handler.rb
|
|
334
|
+
require "json"
|
|
335
|
+
require "aws-sdk-sqs"
|
|
336
|
+
|
|
337
|
+
def handler(event:, context:)
|
|
338
|
+
body = JSON.parse(event["body"])
|
|
339
|
+
|
|
340
|
+
# Enviar a SQS para procesamiento async
|
|
341
|
+
sqs = Aws::SQS::Client.new
|
|
342
|
+
sqs.send_message(
|
|
343
|
+
queue_url: ENV["WEBHOOK_QUEUE_URL"],
|
|
344
|
+
message_body: body.to_json
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
{
|
|
348
|
+
statusCode: 200,
|
|
349
|
+
body: JSON.generate({ received: true })
|
|
350
|
+
}
|
|
351
|
+
rescue JSON::ParserError
|
|
352
|
+
{
|
|
353
|
+
statusCode: 400,
|
|
354
|
+
body: JSON.generate({ error: "Invalid JSON" })
|
|
355
|
+
}
|
|
356
|
+
end
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## ECS/Fargate
|
|
360
|
+
|
|
361
|
+
### Dockerfile para Rails
|
|
362
|
+
|
|
363
|
+
```dockerfile
|
|
364
|
+
# Dockerfile
|
|
365
|
+
FROM ruby:3.3.0-slim
|
|
366
|
+
|
|
367
|
+
# Instalar dependencias
|
|
368
|
+
RUN apt-get update -qq && \
|
|
369
|
+
apt-get install -y build-essential libpq-dev nodejs npm && \
|
|
370
|
+
npm install -g yarn && \
|
|
371
|
+
rm -rf /var/lib/apt/lists/*
|
|
372
|
+
|
|
373
|
+
WORKDIR /app
|
|
374
|
+
|
|
375
|
+
# Instalar gems
|
|
376
|
+
COPY Gemfile Gemfile.lock ./
|
|
377
|
+
RUN bundle config set --local deployment 'true' && \
|
|
378
|
+
bundle config set --local without 'development test' && \
|
|
379
|
+
bundle install
|
|
380
|
+
|
|
381
|
+
# Copiar aplicación
|
|
382
|
+
COPY . .
|
|
383
|
+
|
|
384
|
+
# Precompilar assets
|
|
385
|
+
RUN SECRET_KEY_BASE=dummy bundle exec rails assets:precompile
|
|
386
|
+
|
|
387
|
+
# Puerto
|
|
388
|
+
EXPOSE 3000
|
|
389
|
+
|
|
390
|
+
# Comando
|
|
391
|
+
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Task Definition
|
|
395
|
+
|
|
396
|
+
```json
|
|
397
|
+
{
|
|
398
|
+
"family": "rails-app",
|
|
399
|
+
"networkMode": "awsvpc",
|
|
400
|
+
"requiresCompatibilities": ["FARGATE"],
|
|
401
|
+
"cpu": "512",
|
|
402
|
+
"memory": "1024",
|
|
403
|
+
"executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
|
|
404
|
+
"taskRoleArn": "arn:aws:iam::123456789012:role/ecsTaskRole",
|
|
405
|
+
"containerDefinitions": [
|
|
406
|
+
{
|
|
407
|
+
"name": "rails-app",
|
|
408
|
+
"image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/rails-app:latest",
|
|
409
|
+
"portMappings": [
|
|
410
|
+
{
|
|
411
|
+
"containerPort": 3000,
|
|
412
|
+
"protocol": "tcp"
|
|
413
|
+
}
|
|
414
|
+
],
|
|
415
|
+
"environment": [
|
|
416
|
+
{ "name": "RAILS_ENV", "value": "production" },
|
|
417
|
+
{ "name": "RAILS_LOG_TO_STDOUT", "value": "true" }
|
|
418
|
+
],
|
|
419
|
+
"secrets": [
|
|
420
|
+
{
|
|
421
|
+
"name": "DATABASE_URL",
|
|
422
|
+
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:rails-app/database-url"
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
"name": "SECRET_KEY_BASE",
|
|
426
|
+
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:rails-app/secret-key-base"
|
|
427
|
+
}
|
|
428
|
+
],
|
|
429
|
+
"logConfiguration": {
|
|
430
|
+
"logDriver": "awslogs",
|
|
431
|
+
"options": {
|
|
432
|
+
"awslogs-group": "/ecs/rails-app",
|
|
433
|
+
"awslogs-region": "us-east-1",
|
|
434
|
+
"awslogs-stream-prefix": "ecs"
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
]
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
## CloudFront (CDN)
|
|
443
|
+
|
|
444
|
+
### Configurar con Rails Assets
|
|
445
|
+
|
|
446
|
+
```ruby
|
|
447
|
+
# config/environments/production.rb
|
|
448
|
+
config.asset_host = ENV["CLOUDFRONT_DOMAIN"]
|
|
449
|
+
# Ejemplo: "https://d1234567890.cloudfront.net"
|
|
450
|
+
|
|
451
|
+
# Para Active Storage con CloudFront
|
|
452
|
+
config.active_storage.resolve_model_to_route = :rails_storage_proxy
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Invalidar cache después de deploy
|
|
456
|
+
|
|
457
|
+
```ruby
|
|
458
|
+
# lib/tasks/cloudfront.rake
|
|
459
|
+
namespace :cloudfront do
|
|
460
|
+
desc "Invalidar cache de CloudFront"
|
|
461
|
+
task invalidate: :environment do
|
|
462
|
+
require "aws-sdk-cloudfront"
|
|
463
|
+
|
|
464
|
+
client = Aws::CloudFront::Client.new(region: "us-east-1")
|
|
465
|
+
|
|
466
|
+
client.create_invalidation(
|
|
467
|
+
distribution_id: ENV["CLOUDFRONT_DISTRIBUTION_ID"],
|
|
468
|
+
invalidation_batch: {
|
|
469
|
+
paths: {
|
|
470
|
+
quantity: 1,
|
|
471
|
+
items: ["/assets/*"]
|
|
472
|
+
},
|
|
473
|
+
caller_reference: "deploy-#{Time.current.to_i}"
|
|
474
|
+
}
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
puts "Invalidation created"
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
## Route53
|
|
483
|
+
|
|
484
|
+
### Configurar DNS para la app
|
|
485
|
+
|
|
486
|
+
```ruby
|
|
487
|
+
# Ejemplo con Terraform
|
|
488
|
+
resource "aws_route53_record" "app" {
|
|
489
|
+
zone_id = aws_route53_zone.main.zone_id
|
|
490
|
+
name = "app.midominio.com"
|
|
491
|
+
type = "A"
|
|
492
|
+
|
|
493
|
+
alias {
|
|
494
|
+
name = aws_lb.main.dns_name
|
|
495
|
+
zone_id = aws_lb.main.zone_id
|
|
496
|
+
evaluate_target_health = true
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
# Health check
|
|
501
|
+
resource "aws_route53_health_check" "app" {
|
|
502
|
+
fqdn = "app.midominio.com"
|
|
503
|
+
port = 443
|
|
504
|
+
type = "HTTPS"
|
|
505
|
+
resource_path = "/up"
|
|
506
|
+
failure_threshold = "3"
|
|
507
|
+
request_interval = "30"
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
## IAM (Identity and Access Management)
|
|
512
|
+
|
|
513
|
+
### Política mínima para Rails App
|
|
514
|
+
|
|
515
|
+
```json
|
|
516
|
+
{
|
|
517
|
+
"Version": "2012-10-17",
|
|
518
|
+
"Statement": [
|
|
519
|
+
{
|
|
520
|
+
"Sid": "S3Access",
|
|
521
|
+
"Effect": "Allow",
|
|
522
|
+
"Action": [
|
|
523
|
+
"s3:PutObject",
|
|
524
|
+
"s3:GetObject",
|
|
525
|
+
"s3:DeleteObject"
|
|
526
|
+
],
|
|
527
|
+
"Resource": "arn:aws:s3:::mi-app-production/*"
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
"Sid": "S3ListBucket",
|
|
531
|
+
"Effect": "Allow",
|
|
532
|
+
"Action": "s3:ListBucket",
|
|
533
|
+
"Resource": "arn:aws:s3:::mi-app-production"
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
"Sid": "SESAccess",
|
|
537
|
+
"Effect": "Allow",
|
|
538
|
+
"Action": [
|
|
539
|
+
"ses:SendEmail",
|
|
540
|
+
"ses:SendRawEmail"
|
|
541
|
+
],
|
|
542
|
+
"Resource": "*",
|
|
543
|
+
"Condition": {
|
|
544
|
+
"StringEquals": {
|
|
545
|
+
"ses:FromAddress": "noreply@midominio.com"
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
"Sid": "SecretsManagerAccess",
|
|
551
|
+
"Effect": "Allow",
|
|
552
|
+
"Action": [
|
|
553
|
+
"secretsmanager:GetSecretValue"
|
|
554
|
+
],
|
|
555
|
+
"Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:rails-app/*"
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
"Sid": "SQSAccess",
|
|
559
|
+
"Effect": "Allow",
|
|
560
|
+
"Action": [
|
|
561
|
+
"sqs:SendMessage",
|
|
562
|
+
"sqs:ReceiveMessage",
|
|
563
|
+
"sqs:DeleteMessage",
|
|
564
|
+
"sqs:GetQueueAttributes"
|
|
565
|
+
],
|
|
566
|
+
"Resource": "arn:aws:sqs:us-east-1:123456789012:rails-app-*"
|
|
567
|
+
}
|
|
568
|
+
]
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
## ElastiCache (Redis)
|
|
573
|
+
|
|
574
|
+
### Configurar con Rails
|
|
575
|
+
|
|
576
|
+
```ruby
|
|
577
|
+
# Gemfile
|
|
578
|
+
gem "redis"
|
|
579
|
+
gem "hiredis"
|
|
580
|
+
|
|
581
|
+
# config/initializers/redis.rb
|
|
582
|
+
REDIS = Redis.new(
|
|
583
|
+
url: ENV.fetch("ELASTICACHE_URL") { "redis://localhost:6379/0" },
|
|
584
|
+
driver: :hiredis
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
# config/environments/production.rb
|
|
588
|
+
# Cache store
|
|
589
|
+
config.cache_store = :redis_cache_store, {
|
|
590
|
+
url: ENV["ELASTICACHE_URL"],
|
|
591
|
+
pool_size: ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i,
|
|
592
|
+
pool_timeout: 5
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
# Session store
|
|
596
|
+
config.session_store :redis_store,
|
|
597
|
+
servers: [ENV["ELASTICACHE_URL"]],
|
|
598
|
+
expire_after: 1.day,
|
|
599
|
+
key: "_myapp_session"
|
|
600
|
+
|
|
601
|
+
# Action Cable
|
|
602
|
+
config.action_cable.adapter = :redis
|
|
603
|
+
config.action_cable.url = ENV["ELASTICACHE_URL"]
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
## SES (Simple Email Service)
|
|
607
|
+
|
|
608
|
+
### Configurar Action Mailer
|
|
609
|
+
|
|
610
|
+
```ruby
|
|
611
|
+
# config/environments/production.rb
|
|
612
|
+
config.action_mailer.delivery_method = :ses
|
|
613
|
+
|
|
614
|
+
# Opción 1: Con gem aws-sdk-ses
|
|
615
|
+
config.action_mailer.ses_settings = {
|
|
616
|
+
region: "us-east-1",
|
|
617
|
+
credentials: Aws::Credentials.new(
|
|
618
|
+
Rails.application.credentials.dig(:aws, :access_key_id),
|
|
619
|
+
Rails.application.credentials.dig(:aws, :secret_access_key)
|
|
620
|
+
)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
# Opción 2: Con SMTP
|
|
624
|
+
config.action_mailer.smtp_settings = {
|
|
625
|
+
address: "email-smtp.us-east-1.amazonaws.com",
|
|
626
|
+
port: 587,
|
|
627
|
+
user_name: ENV["SES_SMTP_USERNAME"],
|
|
628
|
+
password: ENV["SES_SMTP_PASSWORD"],
|
|
629
|
+
authentication: :login,
|
|
630
|
+
enable_starttls_auto: true
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
## Secrets Manager / Parameter Store
|
|
635
|
+
|
|
636
|
+
### Cargar secrets al iniciar Rails
|
|
637
|
+
|
|
638
|
+
```ruby
|
|
639
|
+
# config/initializers/aws_secrets.rb
|
|
640
|
+
if Rails.env.production?
|
|
641
|
+
require "aws-sdk-secretsmanager"
|
|
642
|
+
|
|
643
|
+
client = Aws::SecretsManager::Client.new(region: "us-east-1")
|
|
644
|
+
|
|
645
|
+
begin
|
|
646
|
+
secret = client.get_secret_value(secret_id: "rails-app/#{Rails.env}")
|
|
647
|
+
secrets = JSON.parse(secret.secret_string)
|
|
648
|
+
|
|
649
|
+
# Setear variables de entorno
|
|
650
|
+
secrets.each do |key, value|
|
|
651
|
+
ENV[key] = value unless ENV[key].present?
|
|
652
|
+
end
|
|
653
|
+
rescue Aws::SecretsManager::Errors::ServiceError => e
|
|
654
|
+
Rails.logger.error "Error loading secrets: #{e.message}"
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### Rotar secrets automáticamente
|
|
660
|
+
|
|
661
|
+
```ruby
|
|
662
|
+
# Lambda function para rotación
|
|
663
|
+
def handler(event:, context:)
|
|
664
|
+
require "aws-sdk-secretsmanager"
|
|
665
|
+
require "securerandom"
|
|
666
|
+
|
|
667
|
+
client = Aws::SecretsManager::Client.new
|
|
668
|
+
secret_id = event["SecretId"]
|
|
669
|
+
step = event["Step"]
|
|
670
|
+
|
|
671
|
+
case step
|
|
672
|
+
when "createSecret"
|
|
673
|
+
# Generar nuevo secret
|
|
674
|
+
new_secret = SecureRandom.hex(64)
|
|
675
|
+
client.put_secret_value(
|
|
676
|
+
secret_id: secret_id,
|
|
677
|
+
client_request_token: event["ClientRequestToken"],
|
|
678
|
+
secret_string: new_secret,
|
|
679
|
+
version_stages: ["AWSPENDING"]
|
|
680
|
+
)
|
|
681
|
+
when "setSecret"
|
|
682
|
+
# Aplicar nuevo secret (actualizar en la app)
|
|
683
|
+
when "testSecret"
|
|
684
|
+
# Verificar que funciona
|
|
685
|
+
when "finishSecret"
|
|
686
|
+
# Marcar como current
|
|
687
|
+
client.update_secret_version_stage(
|
|
688
|
+
secret_id: secret_id,
|
|
689
|
+
version_stage: "AWSCURRENT",
|
|
690
|
+
move_to_version_id: event["ClientRequestToken"],
|
|
691
|
+
remove_from_version_id: get_current_version(client, secret_id)
|
|
692
|
+
)
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
## CloudWatch
|
|
698
|
+
|
|
699
|
+
### Configurar logging
|
|
700
|
+
|
|
701
|
+
```ruby
|
|
702
|
+
# config/environments/production.rb
|
|
703
|
+
if ENV["AWS_EXECUTION_ENV"].present? || ENV["ECS_CONTAINER_METADATA_URI"].present?
|
|
704
|
+
config.logger = ActiveSupport::Logger.new(STDOUT)
|
|
705
|
+
config.logger.formatter = proc do |severity, datetime, progname, msg|
|
|
706
|
+
{
|
|
707
|
+
timestamp: datetime.iso8601,
|
|
708
|
+
level: severity,
|
|
709
|
+
message: msg,
|
|
710
|
+
progname: progname
|
|
711
|
+
}.to_json + "\n"
|
|
712
|
+
end
|
|
713
|
+
end
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
### Custom Metrics
|
|
717
|
+
|
|
718
|
+
```ruby
|
|
719
|
+
# app/services/cloudwatch_metrics.rb
|
|
720
|
+
class CloudwatchMetrics
|
|
721
|
+
def initialize
|
|
722
|
+
@client = Aws::CloudWatch::Client.new(region: "us-east-1")
|
|
723
|
+
@namespace = "RailsApp/#{Rails.env}"
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
def record(metric_name, value, unit: "Count", dimensions: {})
|
|
727
|
+
@client.put_metric_data(
|
|
728
|
+
namespace: @namespace,
|
|
729
|
+
metric_data: [
|
|
730
|
+
{
|
|
731
|
+
metric_name: metric_name,
|
|
732
|
+
value: value,
|
|
733
|
+
unit: unit,
|
|
734
|
+
timestamp: Time.current,
|
|
735
|
+
dimensions: dimensions.map { |k, v| { name: k.to_s, value: v.to_s } }
|
|
736
|
+
}
|
|
737
|
+
]
|
|
738
|
+
)
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
def record_request_time(duration_ms, controller:, action:)
|
|
742
|
+
record(
|
|
743
|
+
"RequestDuration",
|
|
744
|
+
duration_ms,
|
|
745
|
+
unit: "Milliseconds",
|
|
746
|
+
dimensions: { Controller: controller, Action: action }
|
|
747
|
+
)
|
|
748
|
+
end
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
# Uso en ApplicationController
|
|
752
|
+
class ApplicationController < ActionController::Base
|
|
753
|
+
around_action :record_metrics
|
|
754
|
+
|
|
755
|
+
private
|
|
756
|
+
|
|
757
|
+
def record_metrics
|
|
758
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
759
|
+
yield
|
|
760
|
+
ensure
|
|
761
|
+
duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
|
|
762
|
+
CloudwatchMetrics.new.record_request_time(
|
|
763
|
+
duration,
|
|
764
|
+
controller: controller_name,
|
|
765
|
+
action: action_name
|
|
766
|
+
)
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
### Alarms
|
|
772
|
+
|
|
773
|
+
```ruby
|
|
774
|
+
# Crear alarma para errores 5xx
|
|
775
|
+
resource "aws_cloudwatch_metric_alarm" "high_5xx_errors" {
|
|
776
|
+
alarm_name = "rails-app-high-5xx-errors"
|
|
777
|
+
comparison_operator = "GreaterThanThreshold"
|
|
778
|
+
evaluation_periods = "2"
|
|
779
|
+
metric_name = "HTTPCode_Target_5XX_Count"
|
|
780
|
+
namespace = "AWS/ApplicationELB"
|
|
781
|
+
period = "300"
|
|
782
|
+
statistic = "Sum"
|
|
783
|
+
threshold = "10"
|
|
784
|
+
alarm_description = "Alerta cuando hay mas de 10 errores 5xx en 5 minutos"
|
|
785
|
+
|
|
786
|
+
dimensions = {
|
|
787
|
+
LoadBalancer = aws_lb.main.arn_suffix
|
|
788
|
+
TargetGroup = aws_lb_target_group.rails.arn_suffix
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
alarm_actions = [aws_sns_topic.alerts.arn]
|
|
792
|
+
ok_actions = [aws_sns_topic.alerts.arn]
|
|
793
|
+
}
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
## Costos y Optimización
|
|
797
|
+
|
|
798
|
+
### Instancias Reservadas vs Spot
|
|
799
|
+
|
|
800
|
+
```ruby
|
|
801
|
+
# Para workloads predecibles: Reserved Instances
|
|
802
|
+
# Ahorro: 30-60% vs On-Demand
|
|
803
|
+
|
|
804
|
+
# Para workers/jobs: Spot Instances
|
|
805
|
+
# Ahorro: hasta 90%
|
|
806
|
+
|
|
807
|
+
# Estrategia mixta para ECS:
|
|
808
|
+
# - Web servers: On-Demand o Reserved
|
|
809
|
+
# - Background workers: Spot con fallback a On-Demand
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
### Calculadora de costos típicos
|
|
813
|
+
|
|
814
|
+
```markdown
|
|
815
|
+
## Estimación mensual para app Rails pequeña-mediana
|
|
816
|
+
|
|
817
|
+
| Servicio | Configuración | Costo/mes |
|
|
818
|
+
|----------|--------------|-----------|
|
|
819
|
+
| EC2/ECS | t3.medium (2 instancias) | ~$60 |
|
|
820
|
+
| RDS | db.t3.small PostgreSQL | ~$25 |
|
|
821
|
+
| ElastiCache | cache.t3.micro | ~$12 |
|
|
822
|
+
| S3 | 50GB + transferencia | ~$5 |
|
|
823
|
+
| CloudFront | 100GB transferencia | ~$10 |
|
|
824
|
+
| Route53 | 1 zona + queries | ~$1 |
|
|
825
|
+
| SES | 10,000 emails | ~$1 |
|
|
826
|
+
| **Total** | | **~$114/mes** |
|
|
827
|
+
|
|
828
|
+
## Optimizaciones:
|
|
829
|
+
- Usar Savings Plans (hasta 72% ahorro)
|
|
830
|
+
- Auto Scaling para reducir en horas bajas
|
|
831
|
+
- S3 Intelligent-Tiering para archivos
|
|
832
|
+
- Reserved Capacity para RDS
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
## Checklist de Deployment
|
|
836
|
+
|
|
837
|
+
- [ ] VPC con subnets públicas y privadas
|
|
838
|
+
- [ ] Security Groups configurados (mínimo privilegio)
|
|
839
|
+
- [ ] RDS en subnet privada con backups automáticos
|
|
840
|
+
- [ ] S3 bucket con versionado y lifecycle policies
|
|
841
|
+
- [ ] IAM roles con políticas mínimas
|
|
842
|
+
- [ ] Secrets en Secrets Manager o Parameter Store
|
|
843
|
+
- [ ] CloudWatch logs y métricas configuradas
|
|
844
|
+
- [ ] Alarmas para métricas críticas
|
|
845
|
+
- [ ] CloudFront para assets estáticos
|
|
846
|
+
- [ ] Route53 con health checks
|
|
847
|
+
- [ ] SSL/TLS con ACM (Certificate Manager)
|
|
848
|
+
- [ ] Backups automáticos probados
|