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,931 @@
|
|
|
1
|
+
# Skill: Microsoft Azure para Rails
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Configurar, desplegar y gestionar aplicaciones Rails en Microsoft Azure, aprovechando los servicios cloud para escalabilidad, rendimiento y seguridad.
|
|
6
|
+
|
|
7
|
+
## App Service
|
|
8
|
+
|
|
9
|
+
### Configurar Web App para Rails
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Crear grupo de recursos
|
|
13
|
+
az group create --name rails-app-rg --location eastus
|
|
14
|
+
|
|
15
|
+
# Crear App Service Plan
|
|
16
|
+
az appservice plan create \
|
|
17
|
+
--name rails-app-plan \
|
|
18
|
+
--resource-group rails-app-rg \
|
|
19
|
+
--sku B1 \
|
|
20
|
+
--is-linux
|
|
21
|
+
|
|
22
|
+
# Crear Web App con Ruby
|
|
23
|
+
az webapp create \
|
|
24
|
+
--resource-group rails-app-rg \
|
|
25
|
+
--plan rails-app-plan \
|
|
26
|
+
--name mi-rails-app \
|
|
27
|
+
--runtime "RUBY:3.1"
|
|
28
|
+
|
|
29
|
+
# Configurar variables de entorno
|
|
30
|
+
az webapp config appsettings set \
|
|
31
|
+
--resource-group rails-app-rg \
|
|
32
|
+
--name mi-rails-app \
|
|
33
|
+
--settings \
|
|
34
|
+
RAILS_ENV=production \
|
|
35
|
+
RAILS_SERVE_STATIC_FILES=true \
|
|
36
|
+
RAILS_LOG_TO_STDOUT=true
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Deployment desde GitHub
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
# .github/workflows/azure-deploy.yml
|
|
43
|
+
name: Deploy to Azure
|
|
44
|
+
|
|
45
|
+
on:
|
|
46
|
+
push:
|
|
47
|
+
branches: [main]
|
|
48
|
+
|
|
49
|
+
jobs:
|
|
50
|
+
deploy:
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v4
|
|
55
|
+
|
|
56
|
+
- name: Setup Ruby
|
|
57
|
+
uses: ruby/setup-ruby@v1
|
|
58
|
+
with:
|
|
59
|
+
ruby-version: '3.3'
|
|
60
|
+
bundler-cache: true
|
|
61
|
+
|
|
62
|
+
- name: Precompile assets
|
|
63
|
+
run: |
|
|
64
|
+
bundle exec rails assets:precompile
|
|
65
|
+
env:
|
|
66
|
+
RAILS_ENV: production
|
|
67
|
+
SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE }}
|
|
68
|
+
|
|
69
|
+
- name: Deploy to Azure Web App
|
|
70
|
+
uses: azure/webapps-deploy@v2
|
|
71
|
+
with:
|
|
72
|
+
app-name: mi-rails-app
|
|
73
|
+
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Deployment Slots
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Crear slot de staging
|
|
80
|
+
az webapp deployment slot create \
|
|
81
|
+
--name mi-rails-app \
|
|
82
|
+
--resource-group rails-app-rg \
|
|
83
|
+
--slot staging
|
|
84
|
+
|
|
85
|
+
# Configurar settings del slot
|
|
86
|
+
az webapp config appsettings set \
|
|
87
|
+
--resource-group rails-app-rg \
|
|
88
|
+
--name mi-rails-app \
|
|
89
|
+
--slot staging \
|
|
90
|
+
--settings RAILS_ENV=staging
|
|
91
|
+
|
|
92
|
+
# Swap slots (zero-downtime deployment)
|
|
93
|
+
az webapp deployment slot swap \
|
|
94
|
+
--resource-group rails-app-rg \
|
|
95
|
+
--name mi-rails-app \
|
|
96
|
+
--slot staging \
|
|
97
|
+
--target-slot production
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Auto-scaling
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Configurar autoscaling
|
|
104
|
+
az monitor autoscale create \
|
|
105
|
+
--resource-group rails-app-rg \
|
|
106
|
+
--resource mi-rails-app \
|
|
107
|
+
--resource-type Microsoft.Web/serverfarms \
|
|
108
|
+
--name rails-autoscale \
|
|
109
|
+
--min-count 1 \
|
|
110
|
+
--max-count 5 \
|
|
111
|
+
--count 2
|
|
112
|
+
|
|
113
|
+
# Regla: escalar si CPU > 70%
|
|
114
|
+
az monitor autoscale rule create \
|
|
115
|
+
--resource-group rails-app-rg \
|
|
116
|
+
--autoscale-name rails-autoscale \
|
|
117
|
+
--condition "Percentage CPU > 70 avg 5m" \
|
|
118
|
+
--scale out 1
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Blob Storage
|
|
122
|
+
|
|
123
|
+
### Configurar Active Storage
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
# Gemfile
|
|
127
|
+
gem "azure-storage-blob", require: false
|
|
128
|
+
|
|
129
|
+
# config/storage.yml
|
|
130
|
+
azure:
|
|
131
|
+
service: AzureStorage
|
|
132
|
+
storage_account_name: <%= ENV["AZURE_STORAGE_ACCOUNT"] %>
|
|
133
|
+
storage_access_key: <%= ENV["AZURE_STORAGE_ACCESS_KEY"] %>
|
|
134
|
+
container: <%= ENV.fetch("AZURE_STORAGE_CONTAINER") { "uploads-#{Rails.env}" } %>
|
|
135
|
+
|
|
136
|
+
# Para uploads públicos
|
|
137
|
+
azure_public:
|
|
138
|
+
service: AzureStorage
|
|
139
|
+
storage_account_name: <%= ENV["AZURE_STORAGE_ACCOUNT"] %>
|
|
140
|
+
storage_access_key: <%= ENV["AZURE_STORAGE_ACCESS_KEY"] %>
|
|
141
|
+
container: public-uploads
|
|
142
|
+
public: true
|
|
143
|
+
|
|
144
|
+
# config/environments/production.rb
|
|
145
|
+
config.active_storage.service = :azure
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### SAS Tokens (Shared Access Signatures)
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
# app/services/azure_blob_service.rb
|
|
152
|
+
class AzureBlobService
|
|
153
|
+
def initialize
|
|
154
|
+
@account_name = ENV["AZURE_STORAGE_ACCOUNT"]
|
|
155
|
+
@account_key = ENV["AZURE_STORAGE_ACCESS_KEY"]
|
|
156
|
+
@container = ENV["AZURE_STORAGE_CONTAINER"]
|
|
157
|
+
|
|
158
|
+
@client = Azure::Storage::Blob::BlobService.create(
|
|
159
|
+
storage_account_name: @account_name,
|
|
160
|
+
storage_access_key: @account_key
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Generar URL con SAS token para subir
|
|
165
|
+
def generate_upload_url(blob_name, content_type:, expires_in: 15.minutes)
|
|
166
|
+
sas_token = generate_sas_token(
|
|
167
|
+
blob_name,
|
|
168
|
+
permissions: "w",
|
|
169
|
+
expires_in: expires_in
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
"https://#{@account_name}.blob.core.windows.net/#{@container}/#{blob_name}?#{sas_token}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Generar URL con SAS token para descargar
|
|
176
|
+
def generate_download_url(blob_name, expires_in: 1.hour, filename: nil)
|
|
177
|
+
content_disposition = filename ? "attachment; filename=\"#{filename}\"" : nil
|
|
178
|
+
|
|
179
|
+
sas_token = generate_sas_token(
|
|
180
|
+
blob_name,
|
|
181
|
+
permissions: "r",
|
|
182
|
+
expires_in: expires_in,
|
|
183
|
+
content_disposition: content_disposition
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
"https://#{@account_name}.blob.core.windows.net/#{@container}/#{blob_name}?#{sas_token}"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
def generate_sas_token(blob_name, permissions:, expires_in:, content_disposition: nil)
|
|
192
|
+
generator = Azure::Storage::Common::Core::Auth::SharedAccessSignature.new(
|
|
193
|
+
@account_name,
|
|
194
|
+
@account_key
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
options = {
|
|
198
|
+
permissions: permissions,
|
|
199
|
+
start: (Time.current - 5.minutes).utc.iso8601,
|
|
200
|
+
expiry: (Time.current + expires_in).utc.iso8601,
|
|
201
|
+
resource: "b"
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
options[:content_disposition] = content_disposition if content_disposition
|
|
205
|
+
|
|
206
|
+
generator.generate_service_sas_token(
|
|
207
|
+
"#{@container}/#{blob_name}",
|
|
208
|
+
options
|
|
209
|
+
)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### CORS Configuration
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
# Configurar CORS para el storage account
|
|
218
|
+
az storage cors add \
|
|
219
|
+
--account-name mistorageaccount \
|
|
220
|
+
--services b \
|
|
221
|
+
--methods GET PUT POST DELETE \
|
|
222
|
+
--origins "https://miapp.com" "http://localhost:3000" \
|
|
223
|
+
--allowed-headers "*" \
|
|
224
|
+
--exposed-headers "*" \
|
|
225
|
+
--max-age 3600
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Azure SQL / PostgreSQL
|
|
229
|
+
|
|
230
|
+
### Configurar Rails con Azure Database for PostgreSQL
|
|
231
|
+
|
|
232
|
+
```yaml
|
|
233
|
+
# config/database.yml
|
|
234
|
+
production:
|
|
235
|
+
adapter: postgresql
|
|
236
|
+
encoding: unicode
|
|
237
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
|
238
|
+
database: <%= ENV["AZURE_PG_DATABASE"] %>
|
|
239
|
+
username: <%= ENV["AZURE_PG_USERNAME"] %>
|
|
240
|
+
password: <%= ENV["AZURE_PG_PASSWORD"] %>
|
|
241
|
+
host: <%= ENV["AZURE_PG_HOST"] %>
|
|
242
|
+
port: 5432
|
|
243
|
+
sslmode: require
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
# Crear servidor PostgreSQL
|
|
248
|
+
az postgres flexible-server create \
|
|
249
|
+
--resource-group rails-app-rg \
|
|
250
|
+
--name rails-pg-server \
|
|
251
|
+
--location eastus \
|
|
252
|
+
--admin-user railsadmin \
|
|
253
|
+
--admin-password 'SecurePassword123!' \
|
|
254
|
+
--sku-name Standard_B1ms \
|
|
255
|
+
--version 15 \
|
|
256
|
+
--storage-size 32
|
|
257
|
+
|
|
258
|
+
# Crear base de datos
|
|
259
|
+
az postgres flexible-server db create \
|
|
260
|
+
--resource-group rails-app-rg \
|
|
261
|
+
--server-name rails-pg-server \
|
|
262
|
+
--database-name rails_production
|
|
263
|
+
|
|
264
|
+
# Configurar firewall (permitir Azure services)
|
|
265
|
+
az postgres flexible-server firewall-rule create \
|
|
266
|
+
--resource-group rails-app-rg \
|
|
267
|
+
--name rails-pg-server \
|
|
268
|
+
--rule-name AllowAzureServices \
|
|
269
|
+
--start-ip-address 0.0.0.0 \
|
|
270
|
+
--end-ip-address 0.0.0.0
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Backups y Restauración
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
# Los backups son automáticos en Azure PostgreSQL
|
|
277
|
+
# Configurar retención (7-35 días)
|
|
278
|
+
az postgres flexible-server update \
|
|
279
|
+
--resource-group rails-app-rg \
|
|
280
|
+
--name rails-pg-server \
|
|
281
|
+
--backup-retention 14
|
|
282
|
+
|
|
283
|
+
# Restaurar a punto en el tiempo
|
|
284
|
+
az postgres flexible-server restore \
|
|
285
|
+
--resource-group rails-app-rg \
|
|
286
|
+
--name rails-pg-server-restored \
|
|
287
|
+
--source-server rails-pg-server \
|
|
288
|
+
--restore-time "2024-01-15T10:00:00Z"
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Azure Functions
|
|
292
|
+
|
|
293
|
+
### HTTP Trigger para Webhooks
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
# function.json
|
|
297
|
+
{
|
|
298
|
+
"bindings": [
|
|
299
|
+
{
|
|
300
|
+
"authLevel": "function",
|
|
301
|
+
"type": "httpTrigger",
|
|
302
|
+
"direction": "in",
|
|
303
|
+
"name": "req",
|
|
304
|
+
"methods": ["post"]
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
"type": "http",
|
|
308
|
+
"direction": "out",
|
|
309
|
+
"name": "$return"
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
"type": "queue",
|
|
313
|
+
"direction": "out",
|
|
314
|
+
"name": "webhookQueue",
|
|
315
|
+
"queueName": "webhooks",
|
|
316
|
+
"connection": "AzureWebJobsStorage"
|
|
317
|
+
}
|
|
318
|
+
]
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# handler.rb (con Ruby custom handler)
|
|
322
|
+
require "json"
|
|
323
|
+
require "sinatra"
|
|
324
|
+
|
|
325
|
+
post "/api/webhook" do
|
|
326
|
+
content_type :json
|
|
327
|
+
payload = JSON.parse(request.body.read)
|
|
328
|
+
|
|
329
|
+
# Encolar para procesamiento
|
|
330
|
+
{
|
|
331
|
+
statusCode: 200,
|
|
332
|
+
body: { received: true }.to_json,
|
|
333
|
+
webhookQueue: payload.to_json
|
|
334
|
+
}.to_json
|
|
335
|
+
end
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Timer Trigger para Tareas Programadas
|
|
339
|
+
|
|
340
|
+
```json
|
|
341
|
+
{
|
|
342
|
+
"bindings": [
|
|
343
|
+
{
|
|
344
|
+
"name": "timer",
|
|
345
|
+
"type": "timerTrigger",
|
|
346
|
+
"direction": "in",
|
|
347
|
+
"schedule": "0 0 * * * *"
|
|
348
|
+
}
|
|
349
|
+
]
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Container Instances / AKS
|
|
354
|
+
|
|
355
|
+
### Azure Container Instances (Simple)
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
# Crear container instance
|
|
359
|
+
az container create \
|
|
360
|
+
--resource-group rails-app-rg \
|
|
361
|
+
--name rails-app \
|
|
362
|
+
--image miregistry.azurecr.io/rails-app:latest \
|
|
363
|
+
--cpu 1 \
|
|
364
|
+
--memory 1.5 \
|
|
365
|
+
--ports 3000 \
|
|
366
|
+
--environment-variables \
|
|
367
|
+
RAILS_ENV=production \
|
|
368
|
+
RAILS_LOG_TO_STDOUT=true \
|
|
369
|
+
--secure-environment-variables \
|
|
370
|
+
DATABASE_URL='postgresql://...' \
|
|
371
|
+
SECRET_KEY_BASE='...'
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Azure Kubernetes Service (AKS)
|
|
375
|
+
|
|
376
|
+
```yaml
|
|
377
|
+
# kubernetes/deployment.yaml
|
|
378
|
+
apiVersion: apps/v1
|
|
379
|
+
kind: Deployment
|
|
380
|
+
metadata:
|
|
381
|
+
name: rails-app
|
|
382
|
+
spec:
|
|
383
|
+
replicas: 3
|
|
384
|
+
selector:
|
|
385
|
+
matchLabels:
|
|
386
|
+
app: rails-app
|
|
387
|
+
template:
|
|
388
|
+
metadata:
|
|
389
|
+
labels:
|
|
390
|
+
app: rails-app
|
|
391
|
+
spec:
|
|
392
|
+
containers:
|
|
393
|
+
- name: rails-app
|
|
394
|
+
image: miregistry.azurecr.io/rails-app:latest
|
|
395
|
+
ports:
|
|
396
|
+
- containerPort: 3000
|
|
397
|
+
env:
|
|
398
|
+
- name: RAILS_ENV
|
|
399
|
+
value: production
|
|
400
|
+
- name: DATABASE_URL
|
|
401
|
+
valueFrom:
|
|
402
|
+
secretKeyRef:
|
|
403
|
+
name: rails-secrets
|
|
404
|
+
key: database-url
|
|
405
|
+
- name: SECRET_KEY_BASE
|
|
406
|
+
valueFrom:
|
|
407
|
+
secretKeyRef:
|
|
408
|
+
name: rails-secrets
|
|
409
|
+
key: secret-key-base
|
|
410
|
+
resources:
|
|
411
|
+
requests:
|
|
412
|
+
memory: "512Mi"
|
|
413
|
+
cpu: "250m"
|
|
414
|
+
limits:
|
|
415
|
+
memory: "1Gi"
|
|
416
|
+
cpu: "500m"
|
|
417
|
+
livenessProbe:
|
|
418
|
+
httpGet:
|
|
419
|
+
path: /up
|
|
420
|
+
port: 3000
|
|
421
|
+
initialDelaySeconds: 30
|
|
422
|
+
periodSeconds: 10
|
|
423
|
+
readinessProbe:
|
|
424
|
+
httpGet:
|
|
425
|
+
path: /up
|
|
426
|
+
port: 3000
|
|
427
|
+
initialDelaySeconds: 5
|
|
428
|
+
periodSeconds: 5
|
|
429
|
+
---
|
|
430
|
+
apiVersion: v1
|
|
431
|
+
kind: Service
|
|
432
|
+
metadata:
|
|
433
|
+
name: rails-app-service
|
|
434
|
+
spec:
|
|
435
|
+
type: LoadBalancer
|
|
436
|
+
ports:
|
|
437
|
+
- port: 80
|
|
438
|
+
targetPort: 3000
|
|
439
|
+
selector:
|
|
440
|
+
app: rails-app
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
```bash
|
|
444
|
+
# Crear cluster AKS
|
|
445
|
+
az aks create \
|
|
446
|
+
--resource-group rails-app-rg \
|
|
447
|
+
--name rails-aks \
|
|
448
|
+
--node-count 2 \
|
|
449
|
+
--node-vm-size Standard_B2s \
|
|
450
|
+
--generate-ssh-keys
|
|
451
|
+
|
|
452
|
+
# Obtener credenciales
|
|
453
|
+
az aks get-credentials \
|
|
454
|
+
--resource-group rails-app-rg \
|
|
455
|
+
--name rails-aks
|
|
456
|
+
|
|
457
|
+
# Desplegar
|
|
458
|
+
kubectl apply -f kubernetes/
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## Azure CDN
|
|
462
|
+
|
|
463
|
+
### Configurar CDN para Assets
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
# Crear perfil CDN
|
|
467
|
+
az cdn profile create \
|
|
468
|
+
--resource-group rails-app-rg \
|
|
469
|
+
--name rails-cdn-profile \
|
|
470
|
+
--sku Standard_Microsoft
|
|
471
|
+
|
|
472
|
+
# Crear endpoint
|
|
473
|
+
az cdn endpoint create \
|
|
474
|
+
--resource-group rails-app-rg \
|
|
475
|
+
--profile-name rails-cdn-profile \
|
|
476
|
+
--name rails-cdn \
|
|
477
|
+
--origin mi-rails-app.azurewebsites.net \
|
|
478
|
+
--origin-host-header mi-rails-app.azurewebsites.net
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
```ruby
|
|
482
|
+
# config/environments/production.rb
|
|
483
|
+
config.asset_host = ENV["AZURE_CDN_ENDPOINT"]
|
|
484
|
+
# Ejemplo: "https://rails-cdn.azureedge.net"
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### Purge Cache
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
# Purgar todo el contenido
|
|
491
|
+
az cdn endpoint purge \
|
|
492
|
+
--resource-group rails-app-rg \
|
|
493
|
+
--profile-name rails-cdn-profile \
|
|
494
|
+
--name rails-cdn \
|
|
495
|
+
--content-paths "/*"
|
|
496
|
+
|
|
497
|
+
# Purgar assets específicos
|
|
498
|
+
az cdn endpoint purge \
|
|
499
|
+
--resource-group rails-app-rg \
|
|
500
|
+
--profile-name rails-cdn-profile \
|
|
501
|
+
--name rails-cdn \
|
|
502
|
+
--content-paths "/assets/*"
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
## DNS Zones
|
|
506
|
+
|
|
507
|
+
### Configurar DNS
|
|
508
|
+
|
|
509
|
+
```bash
|
|
510
|
+
# Crear zona DNS
|
|
511
|
+
az network dns zone create \
|
|
512
|
+
--resource-group rails-app-rg \
|
|
513
|
+
--name midominio.com
|
|
514
|
+
|
|
515
|
+
# Agregar registro A
|
|
516
|
+
az network dns record-set a add-record \
|
|
517
|
+
--resource-group rails-app-rg \
|
|
518
|
+
--zone-name midominio.com \
|
|
519
|
+
--record-set-name app \
|
|
520
|
+
--ipv4-address 20.xx.xx.xx
|
|
521
|
+
|
|
522
|
+
# Agregar CNAME para CDN
|
|
523
|
+
az network dns record-set cname set-record \
|
|
524
|
+
--resource-group rails-app-rg \
|
|
525
|
+
--zone-name midominio.com \
|
|
526
|
+
--record-set-name cdn \
|
|
527
|
+
--cname rails-cdn.azureedge.net
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### Traffic Manager (Load Balancing Global)
|
|
531
|
+
|
|
532
|
+
```bash
|
|
533
|
+
# Crear perfil Traffic Manager
|
|
534
|
+
az network traffic-manager profile create \
|
|
535
|
+
--resource-group rails-app-rg \
|
|
536
|
+
--name rails-traffic-manager \
|
|
537
|
+
--routing-method Performance \
|
|
538
|
+
--unique-dns-name rails-app-tm
|
|
539
|
+
|
|
540
|
+
# Agregar endpoints
|
|
541
|
+
az network traffic-manager endpoint create \
|
|
542
|
+
--resource-group rails-app-rg \
|
|
543
|
+
--profile-name rails-traffic-manager \
|
|
544
|
+
--name eastus-endpoint \
|
|
545
|
+
--type azureEndpoints \
|
|
546
|
+
--target-resource-id /subscriptions/.../sites/rails-app-eastus \
|
|
547
|
+
--endpoint-status Enabled
|
|
548
|
+
|
|
549
|
+
az network traffic-manager endpoint create \
|
|
550
|
+
--resource-group rails-app-rg \
|
|
551
|
+
--profile-name rails-traffic-manager \
|
|
552
|
+
--name westus-endpoint \
|
|
553
|
+
--type azureEndpoints \
|
|
554
|
+
--target-resource-id /subscriptions/.../sites/rails-app-westus \
|
|
555
|
+
--endpoint-status Enabled
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
## Azure AD
|
|
559
|
+
|
|
560
|
+
### Configurar OAuth con Azure AD
|
|
561
|
+
|
|
562
|
+
```ruby
|
|
563
|
+
# Gemfile
|
|
564
|
+
gem "omniauth-azure-activedirectory-v2"
|
|
565
|
+
|
|
566
|
+
# config/initializers/omniauth.rb
|
|
567
|
+
Rails.application.config.middleware.use OmniAuth::Builder do
|
|
568
|
+
provider :azure_activedirectory_v2,
|
|
569
|
+
client_id: ENV["AZURE_AD_CLIENT_ID"],
|
|
570
|
+
client_secret: ENV["AZURE_AD_CLIENT_SECRET"],
|
|
571
|
+
tenant_id: ENV["AZURE_AD_TENANT_ID"]
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
# app/controllers/sessions_controller.rb
|
|
575
|
+
class SessionsController < ApplicationController
|
|
576
|
+
def azure_callback
|
|
577
|
+
auth = request.env["omniauth.auth"]
|
|
578
|
+
|
|
579
|
+
user = User.find_or_create_by(azure_id: auth.uid) do |u|
|
|
580
|
+
u.email = auth.info.email
|
|
581
|
+
u.name = auth.info.name
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
session[:user_id] = user.id
|
|
585
|
+
redirect_to root_path, notice: "Sesion iniciada"
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def failure
|
|
589
|
+
redirect_to root_path, alert: "Error de autenticacion"
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
# config/routes.rb
|
|
594
|
+
get "/auth/azure_activedirectory_v2/callback", to: "sessions#azure_callback"
|
|
595
|
+
get "/auth/failure", to: "sessions#failure"
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### Managed Identity
|
|
599
|
+
|
|
600
|
+
```ruby
|
|
601
|
+
# Para acceder a recursos Azure sin credenciales hardcodeadas
|
|
602
|
+
# app/services/azure_managed_identity.rb
|
|
603
|
+
class AzureManagedIdentity
|
|
604
|
+
def initialize
|
|
605
|
+
@token_endpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def get_token(resource:)
|
|
609
|
+
response = HTTParty.get(
|
|
610
|
+
@token_endpoint,
|
|
611
|
+
query: {
|
|
612
|
+
"api-version" => "2019-08-01",
|
|
613
|
+
resource: resource
|
|
614
|
+
},
|
|
615
|
+
headers: { "Metadata" => "true" }
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
JSON.parse(response.body)["access_token"]
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def get_secret(vault_name:, secret_name:)
|
|
622
|
+
token = get_token(resource: "https://vault.azure.net")
|
|
623
|
+
|
|
624
|
+
response = HTTParty.get(
|
|
625
|
+
"https://#{vault_name}.vault.azure.net/secrets/#{secret_name}?api-version=7.4",
|
|
626
|
+
headers: { "Authorization" => "Bearer #{token}" }
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
JSON.parse(response.body)["value"]
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
## Redis Cache
|
|
635
|
+
|
|
636
|
+
### Configurar con Rails
|
|
637
|
+
|
|
638
|
+
```ruby
|
|
639
|
+
# Gemfile
|
|
640
|
+
gem "redis"
|
|
641
|
+
gem "hiredis"
|
|
642
|
+
|
|
643
|
+
# config/initializers/redis.rb
|
|
644
|
+
REDIS = Redis.new(
|
|
645
|
+
url: ENV.fetch("AZURE_REDIS_URL") { "redis://localhost:6379/0" },
|
|
646
|
+
ssl: Rails.env.production?,
|
|
647
|
+
ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
# config/environments/production.rb
|
|
651
|
+
config.cache_store = :redis_cache_store, {
|
|
652
|
+
url: ENV["AZURE_REDIS_URL"],
|
|
653
|
+
ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
# Session store
|
|
657
|
+
config.session_store :redis_store,
|
|
658
|
+
servers: [{
|
|
659
|
+
url: ENV["AZURE_REDIS_URL"],
|
|
660
|
+
ssl: true,
|
|
661
|
+
ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }
|
|
662
|
+
}],
|
|
663
|
+
expire_after: 1.day
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
```bash
|
|
667
|
+
# Crear Azure Cache for Redis
|
|
668
|
+
az redis create \
|
|
669
|
+
--resource-group rails-app-rg \
|
|
670
|
+
--name rails-redis-cache \
|
|
671
|
+
--location eastus \
|
|
672
|
+
--sku Basic \
|
|
673
|
+
--vm-size c0
|
|
674
|
+
|
|
675
|
+
# Obtener connection string
|
|
676
|
+
az redis list-keys \
|
|
677
|
+
--resource-group rails-app-rg \
|
|
678
|
+
--name rails-redis-cache
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
## SendGrid / Communication Services
|
|
682
|
+
|
|
683
|
+
### Configurar Email con SendGrid (via Azure)
|
|
684
|
+
|
|
685
|
+
```ruby
|
|
686
|
+
# Gemfile
|
|
687
|
+
gem "sendgrid-ruby"
|
|
688
|
+
|
|
689
|
+
# config/environments/production.rb
|
|
690
|
+
config.action_mailer.delivery_method = :smtp
|
|
691
|
+
config.action_mailer.smtp_settings = {
|
|
692
|
+
address: "smtp.sendgrid.net",
|
|
693
|
+
port: 587,
|
|
694
|
+
domain: "midominio.com",
|
|
695
|
+
user_name: "apikey",
|
|
696
|
+
password: ENV["SENDGRID_API_KEY"],
|
|
697
|
+
authentication: :plain,
|
|
698
|
+
enable_starttls_auto: true
|
|
699
|
+
}
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
### Azure Communication Services (Email)
|
|
703
|
+
|
|
704
|
+
```ruby
|
|
705
|
+
# app/services/azure_email_service.rb
|
|
706
|
+
class AzureEmailService
|
|
707
|
+
def initialize
|
|
708
|
+
@endpoint = ENV["AZURE_COMMUNICATION_ENDPOINT"]
|
|
709
|
+
@access_key = ENV["AZURE_COMMUNICATION_KEY"]
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
def send_email(to:, subject:, body:, from: "noreply@midominio.com")
|
|
713
|
+
uri = URI("#{@endpoint}/emails:send?api-version=2023-03-31")
|
|
714
|
+
|
|
715
|
+
request = Net::HTTP::Post.new(uri)
|
|
716
|
+
request["Content-Type"] = "application/json"
|
|
717
|
+
request["Authorization"] = generate_auth_header("POST", uri.path)
|
|
718
|
+
|
|
719
|
+
request.body = {
|
|
720
|
+
senderAddress: from,
|
|
721
|
+
recipients: {
|
|
722
|
+
to: [{ address: to }]
|
|
723
|
+
},
|
|
724
|
+
content: {
|
|
725
|
+
subject: subject,
|
|
726
|
+
plainText: body
|
|
727
|
+
}
|
|
728
|
+
}.to_json
|
|
729
|
+
|
|
730
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
731
|
+
http.use_ssl = true
|
|
732
|
+
http.request(request)
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
private
|
|
736
|
+
|
|
737
|
+
def generate_auth_header(method, path)
|
|
738
|
+
# Implementar HMAC-SHA256 auth
|
|
739
|
+
# ...
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
## Key Vault
|
|
745
|
+
|
|
746
|
+
### Cargar Secrets al Iniciar
|
|
747
|
+
|
|
748
|
+
```ruby
|
|
749
|
+
# config/initializers/azure_key_vault.rb
|
|
750
|
+
if Rails.env.production?
|
|
751
|
+
require "azure/key_vault"
|
|
752
|
+
|
|
753
|
+
client = Azure::KeyVault::KeyVaultClient.new
|
|
754
|
+
|
|
755
|
+
# Con Managed Identity
|
|
756
|
+
vault_url = "https://rails-app-vault.vault.azure.net"
|
|
757
|
+
|
|
758
|
+
secrets = %w[DATABASE_URL SECRET_KEY_BASE REDIS_URL]
|
|
759
|
+
|
|
760
|
+
secrets.each do |secret_name|
|
|
761
|
+
begin
|
|
762
|
+
secret = client.get_secret(vault_url, secret_name.downcase.tr("_", "-"), "")
|
|
763
|
+
ENV[secret_name] = secret.value unless ENV[secret_name].present?
|
|
764
|
+
rescue StandardError => e
|
|
765
|
+
Rails.logger.warn "Could not load secret #{secret_name}: #{e.message}"
|
|
766
|
+
end
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
```bash
|
|
772
|
+
# Crear Key Vault
|
|
773
|
+
az keyvault create \
|
|
774
|
+
--resource-group rails-app-rg \
|
|
775
|
+
--name rails-app-vault \
|
|
776
|
+
--location eastus
|
|
777
|
+
|
|
778
|
+
# Agregar secreto
|
|
779
|
+
az keyvault secret set \
|
|
780
|
+
--vault-name rails-app-vault \
|
|
781
|
+
--name "database-url" \
|
|
782
|
+
--value "postgresql://..."
|
|
783
|
+
|
|
784
|
+
# Dar acceso a la App Service
|
|
785
|
+
az keyvault set-policy \
|
|
786
|
+
--name rails-app-vault \
|
|
787
|
+
--object-id $(az webapp identity show --name mi-rails-app --resource-group rails-app-rg --query principalId -o tsv) \
|
|
788
|
+
--secret-permissions get list
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
## Application Insights
|
|
792
|
+
|
|
793
|
+
### Configurar Monitoring
|
|
794
|
+
|
|
795
|
+
```ruby
|
|
796
|
+
# Gemfile
|
|
797
|
+
gem "application_insights"
|
|
798
|
+
|
|
799
|
+
# config/initializers/application_insights.rb
|
|
800
|
+
if Rails.env.production?
|
|
801
|
+
require "application_insights"
|
|
802
|
+
|
|
803
|
+
ApplicationInsights::TelemetryClient.new(ENV["AZURE_APP_INSIGHTS_KEY"])
|
|
804
|
+
|
|
805
|
+
# Middleware para tracking automático
|
|
806
|
+
Rails.application.config.middleware.use(
|
|
807
|
+
ApplicationInsights::Rack::TrackRequest,
|
|
808
|
+
ENV["AZURE_APP_INSIGHTS_KEY"]
|
|
809
|
+
)
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
# app/services/azure_insights.rb
|
|
813
|
+
class AzureInsights
|
|
814
|
+
def initialize
|
|
815
|
+
@client = ApplicationInsights::TelemetryClient.new(
|
|
816
|
+
ENV["AZURE_APP_INSIGHTS_KEY"]
|
|
817
|
+
)
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
def track_event(name, properties = {}, measurements = {})
|
|
821
|
+
@client.track_event(name, properties, measurements)
|
|
822
|
+
@client.flush
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
def track_exception(exception, properties = {})
|
|
826
|
+
@client.track_exception(exception, properties)
|
|
827
|
+
@client.flush
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
def track_metric(name, value, properties = {})
|
|
831
|
+
@client.track_metric(name, value, properties: properties)
|
|
832
|
+
@client.flush
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
def track_request(name, url, success, start_time, duration)
|
|
836
|
+
@client.track_request(name, url, start_time, duration, "200", success)
|
|
837
|
+
@client.flush
|
|
838
|
+
end
|
|
839
|
+
end
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
### Custom Logging
|
|
843
|
+
|
|
844
|
+
```ruby
|
|
845
|
+
# app/middleware/azure_logging.rb
|
|
846
|
+
class AzureLogging
|
|
847
|
+
def initialize(app)
|
|
848
|
+
@app = app
|
|
849
|
+
@insights = AzureInsights.new
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
def call(env)
|
|
853
|
+
start_time = Time.current
|
|
854
|
+
|
|
855
|
+
begin
|
|
856
|
+
status, headers, response = @app.call(env)
|
|
857
|
+
|
|
858
|
+
duration = ((Time.current - start_time) * 1000).round
|
|
859
|
+
@insights.track_request(
|
|
860
|
+
env["PATH_INFO"],
|
|
861
|
+
env["REQUEST_URI"] || env["PATH_INFO"],
|
|
862
|
+
status < 400,
|
|
863
|
+
start_time,
|
|
864
|
+
duration
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
[status, headers, response]
|
|
868
|
+
rescue StandardError => e
|
|
869
|
+
@insights.track_exception(e)
|
|
870
|
+
raise
|
|
871
|
+
end
|
|
872
|
+
end
|
|
873
|
+
end
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
## Costos y Optimización
|
|
877
|
+
|
|
878
|
+
### Calculadora de Costos
|
|
879
|
+
|
|
880
|
+
```markdown
|
|
881
|
+
## Estimación mensual para app Rails pequeña-mediana
|
|
882
|
+
|
|
883
|
+
| Servicio | Configuración | Costo/mes |
|
|
884
|
+
|----------|--------------|-----------|
|
|
885
|
+
| App Service | B1 (1 core, 1.75GB) | ~$55 |
|
|
886
|
+
| PostgreSQL | B1ms (1 vCore, 2GB) | ~$25 |
|
|
887
|
+
| Redis Cache | C0 (250MB) | ~$16 |
|
|
888
|
+
| Blob Storage | 50GB + transacciones | ~$5 |
|
|
889
|
+
| CDN | 100GB transferencia | ~$8 |
|
|
890
|
+
| DNS Zone | 1 zona | ~$0.50 |
|
|
891
|
+
| **Total** | | **~$110/mes** |
|
|
892
|
+
|
|
893
|
+
## Optimizaciones:
|
|
894
|
+
- Reserved Instances (1-3 años): hasta 72% ahorro
|
|
895
|
+
- Azure Hybrid Benefit: si tienes licencias Windows
|
|
896
|
+
- Dev/Test pricing: para ambientes no-producción
|
|
897
|
+
- Autoscaling: reducir en horas bajas
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
### Reserved Instances
|
|
901
|
+
|
|
902
|
+
```bash
|
|
903
|
+
# Ver recomendaciones de reservas
|
|
904
|
+
az consumption reservation recommendation list \
|
|
905
|
+
--scope "subscriptions/xxx-xxx-xxx"
|
|
906
|
+
|
|
907
|
+
# Comprar reserva
|
|
908
|
+
az reservations reservation-order purchase \
|
|
909
|
+
--sku Standard_B2s \
|
|
910
|
+
--term P1Y \
|
|
911
|
+
--billing-plan Monthly \
|
|
912
|
+
--quantity 2
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
## Checklist de Deployment
|
|
916
|
+
|
|
917
|
+
- [ ] Resource Group creado
|
|
918
|
+
- [ ] App Service Plan configurado
|
|
919
|
+
- [ ] Web App con Ruby runtime
|
|
920
|
+
- [ ] PostgreSQL/MySQL provisionado
|
|
921
|
+
- [ ] Blob Storage para archivos
|
|
922
|
+
- [ ] Redis Cache para sessions/cache
|
|
923
|
+
- [ ] Key Vault para secrets
|
|
924
|
+
- [ ] Managed Identity habilitado
|
|
925
|
+
- [ ] Application Insights configurado
|
|
926
|
+
- [ ] CDN para assets estáticos
|
|
927
|
+
- [ ] DNS configurado
|
|
928
|
+
- [ ] SSL/TLS con App Service Managed Certificate
|
|
929
|
+
- [ ] Deployment slots para zero-downtime
|
|
930
|
+
- [ ] Backups automáticos verificados
|
|
931
|
+
- [ ] Alertas configuradas
|