@polymorphism-tech/morph-spec 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 +279 -0
- package/bin/morph-spec.js +53 -0
- package/content/.claude/commands/morph-apply.md +66 -0
- package/content/.claude/commands/morph-archive.md +79 -0
- package/content/.claude/commands/morph-costs.md +206 -0
- package/content/.claude/commands/morph-infra.md +209 -0
- package/content/.claude/commands/morph-proposal.md +60 -0
- package/content/.claude/commands/morph-status.md +71 -0
- package/content/.claude/settings.local.json +15 -0
- package/content/.claude/skills/infra/bicep-architect.md +419 -0
- package/content/.claude/skills/infra/container-specialist.md +437 -0
- package/content/.claude/skills/infra/devops-engineer.md +405 -0
- package/content/.claude/skills/integrations/asaas-financial.md +333 -0
- package/content/.claude/skills/integrations/azure-identity.md +309 -0
- package/content/.claude/skills/integrations/clerk-auth.md +290 -0
- package/content/.claude/skills/specialists/azure-architect.md +142 -0
- package/content/.claude/skills/specialists/cost-guardian.md +110 -0
- package/content/.claude/skills/specialists/ef-modeler.md +200 -0
- package/content/.claude/skills/specialists/hangfire-orchestrator.md +245 -0
- package/content/.claude/skills/specialists/ms-agent-expert.md +209 -0
- package/content/.claude/skills/specialists/po-pm-advisor.md +197 -0
- package/content/.claude/skills/specialists/standards-architect.md +78 -0
- package/content/.claude/skills/specialists/ui-ux-designer.md +325 -0
- package/content/.claude/skills/stacks/dotnet-blazor.md +352 -0
- package/content/.claude/skills/stacks/dotnet-nextjs.md +402 -0
- package/content/.claude/skills/stacks/shopify.md +445 -0
- package/content/.morph/archive/.gitkeep +25 -0
- package/content/.morph/config/agents.json +149 -0
- package/content/.morph/config/config.template.json +96 -0
- package/content/.morph/examples/api-nextjs/README.md +241 -0
- package/content/.morph/examples/api-nextjs/contracts.ts +307 -0
- package/content/.morph/examples/api-nextjs/spec.md +399 -0
- package/content/.morph/examples/api-nextjs/tasks.md +168 -0
- package/content/.morph/examples/micro-saas/README.md +125 -0
- package/content/.morph/examples/micro-saas/contracts.cs +358 -0
- package/content/.morph/examples/micro-saas/decisions.md +246 -0
- package/content/.morph/examples/micro-saas/spec.md +236 -0
- package/content/.morph/examples/micro-saas/tasks.md +150 -0
- package/content/.morph/examples/multi-agent/README.md +309 -0
- package/content/.morph/examples/multi-agent/contracts.cs +433 -0
- package/content/.morph/examples/multi-agent/spec.md +479 -0
- package/content/.morph/examples/multi-agent/tasks.md +185 -0
- package/content/.morph/features/.gitkeep +25 -0
- package/content/.morph/project.md +159 -0
- package/content/.morph/specs/.gitkeep +20 -0
- package/content/.morph/standards/architecture.md +190 -0
- package/content/.morph/standards/azure.md +184 -0
- package/content/.morph/standards/coding.md +342 -0
- package/content/.morph/templates/agent.cs +172 -0
- package/content/.morph/templates/component.razor +239 -0
- package/content/.morph/templates/contracts.cs +217 -0
- package/content/.morph/templates/decisions.md +106 -0
- package/content/.morph/templates/infra/app-insights.bicep +63 -0
- package/content/.morph/templates/infra/container-app-env.bicep +49 -0
- package/content/.morph/templates/infra/container-app.bicep +156 -0
- package/content/.morph/templates/infra/key-vault.bicep +91 -0
- package/content/.morph/templates/infra/main.bicep +155 -0
- package/content/.morph/templates/infra/parameters.dev.json +23 -0
- package/content/.morph/templates/infra/parameters.prod.json +23 -0
- package/content/.morph/templates/infra/sql-database.bicep +103 -0
- package/content/.morph/templates/infra/storage.bicep +106 -0
- package/content/.morph/templates/integrations/asaas-client.cs +387 -0
- package/content/.morph/templates/integrations/asaas-webhook.cs +351 -0
- package/content/.morph/templates/integrations/azure-identity-config.cs +288 -0
- package/content/.morph/templates/integrations/clerk-config.cs +258 -0
- package/content/.morph/templates/job.cs +171 -0
- package/content/.morph/templates/migration.cs +83 -0
- package/content/.morph/templates/proposal.md +155 -0
- package/content/.morph/templates/recap.md +105 -0
- package/content/.morph/templates/repository.cs +141 -0
- package/content/.morph/templates/saas/subscription.cs +347 -0
- package/content/.morph/templates/saas/tenant.cs +338 -0
- package/content/.morph/templates/service.cs +139 -0
- package/content/.morph/templates/spec.md +147 -0
- package/content/.morph/templates/tasks.md +235 -0
- package/content/.morph/templates/test.cs +239 -0
- package/content/CLAUDE.md +318 -0
- package/package.json +50 -0
- package/src/commands/doctor.js +132 -0
- package/src/commands/init.js +121 -0
- package/src/commands/update.js +84 -0
- package/src/utils/file-copier.js +50 -0
- package/src/utils/logger.js +32 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// ==============================================================================
|
|
2
|
+
// MORPH-SPEC - Container App
|
|
3
|
+
// Azure Container Apps with ingress and auto-scaling
|
|
4
|
+
// ==============================================================================
|
|
5
|
+
|
|
6
|
+
@description('App name')
|
|
7
|
+
param name string
|
|
8
|
+
|
|
9
|
+
@description('Location')
|
|
10
|
+
param location string
|
|
11
|
+
|
|
12
|
+
@description('Tags')
|
|
13
|
+
param tags object = {}
|
|
14
|
+
|
|
15
|
+
@description('Container App Environment ID')
|
|
16
|
+
param environmentId string
|
|
17
|
+
|
|
18
|
+
@description('Container image')
|
|
19
|
+
param containerImage string
|
|
20
|
+
|
|
21
|
+
@description('App Insights connection string')
|
|
22
|
+
param appInsightsConnectionString string = ''
|
|
23
|
+
|
|
24
|
+
@description('CPU cores (0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0)')
|
|
25
|
+
param cpu string = '0.25'
|
|
26
|
+
|
|
27
|
+
@description('Memory (0.5Gi, 1Gi, 1.5Gi, 2Gi, 3Gi, 4Gi)')
|
|
28
|
+
param memory string = '0.5Gi'
|
|
29
|
+
|
|
30
|
+
@description('Minimum replicas (0 = scale to zero)')
|
|
31
|
+
param minReplicas int = 0
|
|
32
|
+
|
|
33
|
+
@description('Maximum replicas')
|
|
34
|
+
param maxReplicas int = 3
|
|
35
|
+
|
|
36
|
+
@description('Target port')
|
|
37
|
+
param targetPort int = 8080
|
|
38
|
+
|
|
39
|
+
@description('Environment variables')
|
|
40
|
+
param envVars array = []
|
|
41
|
+
|
|
42
|
+
// ==============================================================================
|
|
43
|
+
// CONTAINER APP
|
|
44
|
+
// ==============================================================================
|
|
45
|
+
|
|
46
|
+
var secrets = appInsightsConnectionString != '' ? [
|
|
47
|
+
{
|
|
48
|
+
name: 'appinsights-connection-string'
|
|
49
|
+
value: appInsightsConnectionString
|
|
50
|
+
}
|
|
51
|
+
] : []
|
|
52
|
+
|
|
53
|
+
var defaultEnvVars = appInsightsConnectionString != '' ? [
|
|
54
|
+
{
|
|
55
|
+
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
|
|
56
|
+
secretRef: 'appinsights-connection-string'
|
|
57
|
+
}
|
|
58
|
+
{
|
|
59
|
+
name: 'ASPNETCORE_ENVIRONMENT'
|
|
60
|
+
value: 'Production'
|
|
61
|
+
}
|
|
62
|
+
] : [
|
|
63
|
+
{
|
|
64
|
+
name: 'ASPNETCORE_ENVIRONMENT'
|
|
65
|
+
value: 'Production'
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
|
|
70
|
+
name: name
|
|
71
|
+
location: location
|
|
72
|
+
tags: tags
|
|
73
|
+
properties: {
|
|
74
|
+
managedEnvironmentId: environmentId
|
|
75
|
+
configuration: {
|
|
76
|
+
ingress: {
|
|
77
|
+
external: true
|
|
78
|
+
targetPort: targetPort
|
|
79
|
+
transport: 'http'
|
|
80
|
+
allowInsecure: false
|
|
81
|
+
traffic: [
|
|
82
|
+
{
|
|
83
|
+
latestRevision: true
|
|
84
|
+
weight: 100
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
secrets: secrets
|
|
89
|
+
}
|
|
90
|
+
template: {
|
|
91
|
+
containers: [
|
|
92
|
+
{
|
|
93
|
+
name: name
|
|
94
|
+
image: containerImage
|
|
95
|
+
resources: {
|
|
96
|
+
cpu: json(cpu)
|
|
97
|
+
memory: memory
|
|
98
|
+
}
|
|
99
|
+
env: concat(defaultEnvVars, envVars)
|
|
100
|
+
probes: [
|
|
101
|
+
{
|
|
102
|
+
type: 'Liveness'
|
|
103
|
+
httpGet: {
|
|
104
|
+
path: '/health'
|
|
105
|
+
port: targetPort
|
|
106
|
+
}
|
|
107
|
+
initialDelaySeconds: 10
|
|
108
|
+
periodSeconds: 30
|
|
109
|
+
failureThreshold: 3
|
|
110
|
+
}
|
|
111
|
+
{
|
|
112
|
+
type: 'Readiness'
|
|
113
|
+
httpGet: {
|
|
114
|
+
path: '/health/ready'
|
|
115
|
+
port: targetPort
|
|
116
|
+
}
|
|
117
|
+
initialDelaySeconds: 5
|
|
118
|
+
periodSeconds: 10
|
|
119
|
+
failureThreshold: 3
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
scale: {
|
|
125
|
+
minReplicas: minReplicas
|
|
126
|
+
maxReplicas: maxReplicas
|
|
127
|
+
rules: [
|
|
128
|
+
{
|
|
129
|
+
name: 'http-scale'
|
|
130
|
+
http: {
|
|
131
|
+
metadata: {
|
|
132
|
+
concurrentRequests: '100'
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ==============================================================================
|
|
143
|
+
// OUTPUTS
|
|
144
|
+
// ==============================================================================
|
|
145
|
+
|
|
146
|
+
@description('Container App ID')
|
|
147
|
+
output id string = containerApp.id
|
|
148
|
+
|
|
149
|
+
@description('Container App name')
|
|
150
|
+
output name string = containerApp.name
|
|
151
|
+
|
|
152
|
+
@description('Container App URL')
|
|
153
|
+
output url string = 'https://${containerApp.properties.configuration.ingress.fqdn}'
|
|
154
|
+
|
|
155
|
+
@description('Container App FQDN')
|
|
156
|
+
output fqdn string = containerApp.properties.configuration.ingress.fqdn
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// ==============================================================================
|
|
2
|
+
// MORPH-SPEC - Key Vault
|
|
3
|
+
// Azure Key Vault for secrets management
|
|
4
|
+
// ==============================================================================
|
|
5
|
+
|
|
6
|
+
@description('Key Vault name')
|
|
7
|
+
@minLength(3)
|
|
8
|
+
@maxLength(24)
|
|
9
|
+
param name string
|
|
10
|
+
|
|
11
|
+
@description('Location')
|
|
12
|
+
param location string
|
|
13
|
+
|
|
14
|
+
@description('Tags')
|
|
15
|
+
param tags object = {}
|
|
16
|
+
|
|
17
|
+
@description('Enable soft delete')
|
|
18
|
+
param enableSoftDelete bool = true
|
|
19
|
+
|
|
20
|
+
@description('Soft delete retention days')
|
|
21
|
+
@minValue(7)
|
|
22
|
+
@maxValue(90)
|
|
23
|
+
param softDeleteRetentionDays int = 30
|
|
24
|
+
|
|
25
|
+
@description('Enable purge protection')
|
|
26
|
+
param enablePurgeProtection bool = false
|
|
27
|
+
|
|
28
|
+
@description('Object IDs to grant access (optional)')
|
|
29
|
+
param accessPoliciesObjectIds array = []
|
|
30
|
+
|
|
31
|
+
// ==============================================================================
|
|
32
|
+
// KEY VAULT
|
|
33
|
+
// ==============================================================================
|
|
34
|
+
|
|
35
|
+
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
|
|
36
|
+
name: name
|
|
37
|
+
location: location
|
|
38
|
+
tags: tags
|
|
39
|
+
properties: {
|
|
40
|
+
tenantId: subscription().tenantId
|
|
41
|
+
sku: {
|
|
42
|
+
family: 'A'
|
|
43
|
+
name: 'standard'
|
|
44
|
+
}
|
|
45
|
+
enabledForDeployment: true
|
|
46
|
+
enabledForDiskEncryption: false
|
|
47
|
+
enabledForTemplateDeployment: true
|
|
48
|
+
enableSoftDelete: enableSoftDelete
|
|
49
|
+
softDeleteRetentionInDays: softDeleteRetentionDays
|
|
50
|
+
enablePurgeProtection: enablePurgeProtection ? true : null
|
|
51
|
+
enableRbacAuthorization: true
|
|
52
|
+
publicNetworkAccess: 'Enabled'
|
|
53
|
+
networkAcls: {
|
|
54
|
+
defaultAction: 'Allow'
|
|
55
|
+
bypass: 'AzureServices'
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ==============================================================================
|
|
61
|
+
// ACCESS POLICIES (Optional - if not using RBAC)
|
|
62
|
+
// ==============================================================================
|
|
63
|
+
|
|
64
|
+
resource accessPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2023-07-01' = if (length(accessPoliciesObjectIds) > 0) {
|
|
65
|
+
parent: keyVault
|
|
66
|
+
name: 'add'
|
|
67
|
+
properties: {
|
|
68
|
+
accessPolicies: [for objectId in accessPoliciesObjectIds: {
|
|
69
|
+
tenantId: subscription().tenantId
|
|
70
|
+
objectId: objectId
|
|
71
|
+
permissions: {
|
|
72
|
+
secrets: ['get', 'list', 'set', 'delete']
|
|
73
|
+
keys: ['get', 'list', 'create', 'delete']
|
|
74
|
+
certificates: ['get', 'list', 'create', 'delete']
|
|
75
|
+
}
|
|
76
|
+
}]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ==============================================================================
|
|
81
|
+
// OUTPUTS
|
|
82
|
+
// ==============================================================================
|
|
83
|
+
|
|
84
|
+
@description('Key Vault ID')
|
|
85
|
+
output id string = keyVault.id
|
|
86
|
+
|
|
87
|
+
@description('Key Vault name')
|
|
88
|
+
output name string = keyVault.name
|
|
89
|
+
|
|
90
|
+
@description('Key Vault URI')
|
|
91
|
+
output uri string = keyVault.properties.vaultUri
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// ==============================================================================
|
|
2
|
+
// MORPH-SPEC - Main Bicep Template
|
|
3
|
+
// Entry point para infraestrutura Azure
|
|
4
|
+
// ==============================================================================
|
|
5
|
+
|
|
6
|
+
targetScope = 'resourceGroup'
|
|
7
|
+
|
|
8
|
+
// ==============================================================================
|
|
9
|
+
// PARAMETERS
|
|
10
|
+
// ==============================================================================
|
|
11
|
+
|
|
12
|
+
@description('Environment name (dev, staging, prod)')
|
|
13
|
+
@allowed(['dev', 'staging', 'prod'])
|
|
14
|
+
param environment string = 'dev'
|
|
15
|
+
|
|
16
|
+
@description('Location for all resources')
|
|
17
|
+
param location string = resourceGroup().location
|
|
18
|
+
|
|
19
|
+
@description('Application name (used for naming resources)')
|
|
20
|
+
@minLength(3)
|
|
21
|
+
@maxLength(15)
|
|
22
|
+
param appName string
|
|
23
|
+
|
|
24
|
+
@description('SQL Server administrator password')
|
|
25
|
+
@secure()
|
|
26
|
+
param sqlAdminPassword string
|
|
27
|
+
|
|
28
|
+
@description('Container image to deploy')
|
|
29
|
+
param containerImage string = 'mcr.microsoft.com/hello-world:latest'
|
|
30
|
+
|
|
31
|
+
// ==============================================================================
|
|
32
|
+
// VARIABLES
|
|
33
|
+
// ==============================================================================
|
|
34
|
+
|
|
35
|
+
var resourcePrefix = '${appName}-${environment}'
|
|
36
|
+
var tags = {
|
|
37
|
+
environment: environment
|
|
38
|
+
application: appName
|
|
39
|
+
managedBy: 'bicep'
|
|
40
|
+
framework: 'morph-spec'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ==============================================================================
|
|
44
|
+
// LOG ANALYTICS WORKSPACE
|
|
45
|
+
// Required for Container Apps and Application Insights
|
|
46
|
+
// ==============================================================================
|
|
47
|
+
|
|
48
|
+
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
|
|
49
|
+
name: '${resourcePrefix}-logs'
|
|
50
|
+
location: location
|
|
51
|
+
tags: tags
|
|
52
|
+
properties: {
|
|
53
|
+
sku: {
|
|
54
|
+
name: 'PerGB2018'
|
|
55
|
+
}
|
|
56
|
+
retentionInDays: environment == 'prod' ? 90 : 30
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ==============================================================================
|
|
61
|
+
// MODULES
|
|
62
|
+
// ==============================================================================
|
|
63
|
+
|
|
64
|
+
// Application Insights
|
|
65
|
+
module appInsights 'app-insights.bicep' = {
|
|
66
|
+
name: 'appInsights-${uniqueString(resourceGroup().id)}'
|
|
67
|
+
params: {
|
|
68
|
+
name: '${resourcePrefix}-insights'
|
|
69
|
+
location: location
|
|
70
|
+
tags: tags
|
|
71
|
+
logAnalyticsWorkspaceId: logAnalytics.id
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Key Vault
|
|
76
|
+
module keyVault 'key-vault.bicep' = {
|
|
77
|
+
name: 'keyVault-${uniqueString(resourceGroup().id)}'
|
|
78
|
+
params: {
|
|
79
|
+
name: '${resourcePrefix}-kv'
|
|
80
|
+
location: location
|
|
81
|
+
tags: tags
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Storage Account
|
|
86
|
+
module storage 'storage.bicep' = {
|
|
87
|
+
name: 'storage-${uniqueString(resourceGroup().id)}'
|
|
88
|
+
params: {
|
|
89
|
+
name: replace('${resourcePrefix}st', '-', '')
|
|
90
|
+
location: location
|
|
91
|
+
tags: tags
|
|
92
|
+
sku: environment == 'prod' ? 'Standard_GRS' : 'Standard_LRS'
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// SQL Database
|
|
97
|
+
module sqlDatabase 'sql-database.bicep' = {
|
|
98
|
+
name: 'sqlDatabase-${uniqueString(resourceGroup().id)}'
|
|
99
|
+
params: {
|
|
100
|
+
serverName: '${resourcePrefix}-sql'
|
|
101
|
+
databaseName: appName
|
|
102
|
+
location: location
|
|
103
|
+
tags: tags
|
|
104
|
+
adminPassword: sqlAdminPassword
|
|
105
|
+
useFree: environment == 'dev'
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Container Apps Environment
|
|
110
|
+
module containerAppEnv 'container-app-env.bicep' = {
|
|
111
|
+
name: 'containerAppEnv-${uniqueString(resourceGroup().id)}'
|
|
112
|
+
params: {
|
|
113
|
+
name: '${resourcePrefix}-env'
|
|
114
|
+
location: location
|
|
115
|
+
tags: tags
|
|
116
|
+
logAnalyticsWorkspaceId: logAnalytics.id
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Container App
|
|
121
|
+
module containerApp 'container-app.bicep' = {
|
|
122
|
+
name: 'containerApp-${uniqueString(resourceGroup().id)}'
|
|
123
|
+
params: {
|
|
124
|
+
name: resourcePrefix
|
|
125
|
+
location: location
|
|
126
|
+
tags: tags
|
|
127
|
+
environmentId: containerAppEnv.outputs.id
|
|
128
|
+
containerImage: containerImage
|
|
129
|
+
appInsightsConnectionString: appInsights.outputs.connectionString
|
|
130
|
+
minReplicas: environment == 'prod' ? 1 : 0
|
|
131
|
+
maxReplicas: environment == 'prod' ? 10 : 3
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ==============================================================================
|
|
136
|
+
// OUTPUTS
|
|
137
|
+
// ==============================================================================
|
|
138
|
+
|
|
139
|
+
@description('Container App URL')
|
|
140
|
+
output containerAppUrl string = containerApp.outputs.url
|
|
141
|
+
|
|
142
|
+
@description('SQL Server connection string')
|
|
143
|
+
output sqlConnectionString string = sqlDatabase.outputs.connectionString
|
|
144
|
+
|
|
145
|
+
@description('Key Vault URI')
|
|
146
|
+
output keyVaultUri string = keyVault.outputs.uri
|
|
147
|
+
|
|
148
|
+
@description('Storage Account connection string')
|
|
149
|
+
output storageConnectionString string = storage.outputs.connectionString
|
|
150
|
+
|
|
151
|
+
@description('Application Insights connection string')
|
|
152
|
+
output appInsightsConnectionString string = appInsights.outputs.connectionString
|
|
153
|
+
|
|
154
|
+
@description('Log Analytics Workspace ID')
|
|
155
|
+
output logAnalyticsWorkspaceId string = logAnalytics.id
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
|
|
3
|
+
"contentVersion": "1.0.0.0",
|
|
4
|
+
"parameters": {
|
|
5
|
+
"environment": {
|
|
6
|
+
"value": "dev"
|
|
7
|
+
},
|
|
8
|
+
"appName": {
|
|
9
|
+
"value": "{{APP_NAME}}"
|
|
10
|
+
},
|
|
11
|
+
"sqlAdminPassword": {
|
|
12
|
+
"reference": {
|
|
13
|
+
"keyVault": {
|
|
14
|
+
"id": "/subscriptions/{{SUBSCRIPTION_ID}}/resourceGroups/{{RESOURCE_GROUP}}/providers/Microsoft.KeyVault/vaults/{{KEY_VAULT_NAME}}"
|
|
15
|
+
},
|
|
16
|
+
"secretName": "sql-admin-password"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"containerImage": {
|
|
20
|
+
"value": "{{ACR_NAME}}.azurecr.io/{{APP_NAME}}:latest"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
|
|
3
|
+
"contentVersion": "1.0.0.0",
|
|
4
|
+
"parameters": {
|
|
5
|
+
"environment": {
|
|
6
|
+
"value": "prod"
|
|
7
|
+
},
|
|
8
|
+
"appName": {
|
|
9
|
+
"value": "{{APP_NAME}}"
|
|
10
|
+
},
|
|
11
|
+
"sqlAdminPassword": {
|
|
12
|
+
"reference": {
|
|
13
|
+
"keyVault": {
|
|
14
|
+
"id": "/subscriptions/{{SUBSCRIPTION_ID}}/resourceGroups/{{RESOURCE_GROUP}}/providers/Microsoft.KeyVault/vaults/{{KEY_VAULT_NAME}}"
|
|
15
|
+
},
|
|
16
|
+
"secretName": "sql-admin-password"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"containerImage": {
|
|
20
|
+
"value": "{{ACR_NAME}}.azurecr.io/{{APP_NAME}}:{{VERSION}}"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// ==============================================================================
|
|
2
|
+
// MORPH-SPEC - SQL Database
|
|
3
|
+
// Azure SQL Server with Database (supports Free tier)
|
|
4
|
+
// ==============================================================================
|
|
5
|
+
|
|
6
|
+
@description('SQL Server name')
|
|
7
|
+
param serverName string
|
|
8
|
+
|
|
9
|
+
@description('Database name')
|
|
10
|
+
param databaseName string
|
|
11
|
+
|
|
12
|
+
@description('Location')
|
|
13
|
+
param location string
|
|
14
|
+
|
|
15
|
+
@description('Tags')
|
|
16
|
+
param tags object = {}
|
|
17
|
+
|
|
18
|
+
@description('Admin username')
|
|
19
|
+
param adminUsername string = 'sqladmin'
|
|
20
|
+
|
|
21
|
+
@description('Admin password')
|
|
22
|
+
@secure()
|
|
23
|
+
param adminPassword string
|
|
24
|
+
|
|
25
|
+
@description('Use free tier (32GB, limited DTUs)')
|
|
26
|
+
param useFree bool = true
|
|
27
|
+
|
|
28
|
+
// ==============================================================================
|
|
29
|
+
// SQL SERVER
|
|
30
|
+
// ==============================================================================
|
|
31
|
+
|
|
32
|
+
resource sqlServer 'Microsoft.Sql/servers@2023-05-01-preview' = {
|
|
33
|
+
name: serverName
|
|
34
|
+
location: location
|
|
35
|
+
tags: tags
|
|
36
|
+
properties: {
|
|
37
|
+
administratorLogin: adminUsername
|
|
38
|
+
administratorLoginPassword: adminPassword
|
|
39
|
+
version: '12.0'
|
|
40
|
+
minimalTlsVersion: '1.2'
|
|
41
|
+
publicNetworkAccess: 'Enabled'
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ==============================================================================
|
|
46
|
+
// SQL DATABASE
|
|
47
|
+
// ==============================================================================
|
|
48
|
+
|
|
49
|
+
resource sqlDatabase 'Microsoft.Sql/servers/databases@2023-05-01-preview' = {
|
|
50
|
+
parent: sqlServer
|
|
51
|
+
name: databaseName
|
|
52
|
+
location: location
|
|
53
|
+
tags: tags
|
|
54
|
+
sku: useFree ? {
|
|
55
|
+
name: 'Free'
|
|
56
|
+
tier: 'Free'
|
|
57
|
+
} : {
|
|
58
|
+
name: 'Basic'
|
|
59
|
+
tier: 'Basic'
|
|
60
|
+
capacity: 5
|
|
61
|
+
}
|
|
62
|
+
properties: {
|
|
63
|
+
collation: 'SQL_Latin1_General_CP1_CI_AS'
|
|
64
|
+
maxSizeBytes: useFree ? 32212254720 : 2147483648 // 32GB free, 2GB basic
|
|
65
|
+
catalogCollation: 'SQL_Latin1_General_CP1_CI_AS'
|
|
66
|
+
zoneRedundant: false
|
|
67
|
+
readScale: 'Disabled'
|
|
68
|
+
requestedBackupStorageRedundancy: 'Local'
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ==============================================================================
|
|
73
|
+
// FIREWALL RULES
|
|
74
|
+
// ==============================================================================
|
|
75
|
+
|
|
76
|
+
// Allow Azure services
|
|
77
|
+
resource firewallAzure 'Microsoft.Sql/servers/firewallRules@2023-05-01-preview' = {
|
|
78
|
+
parent: sqlServer
|
|
79
|
+
name: 'AllowAllAzureIps'
|
|
80
|
+
properties: {
|
|
81
|
+
startIpAddress: '0.0.0.0'
|
|
82
|
+
endIpAddress: '0.0.0.0'
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ==============================================================================
|
|
87
|
+
// OUTPUTS
|
|
88
|
+
// ==============================================================================
|
|
89
|
+
|
|
90
|
+
@description('SQL Server ID')
|
|
91
|
+
output serverId string = sqlServer.id
|
|
92
|
+
|
|
93
|
+
@description('SQL Server FQDN')
|
|
94
|
+
output serverFqdn string = sqlServer.properties.fullyQualifiedDomainName
|
|
95
|
+
|
|
96
|
+
@description('Database ID')
|
|
97
|
+
output databaseId string = sqlDatabase.id
|
|
98
|
+
|
|
99
|
+
@description('Connection string (password placeholder)')
|
|
100
|
+
output connectionString string = 'Server=tcp:${sqlServer.properties.fullyQualifiedDomainName},1433;Database=${databaseName};User ID=${adminUsername};Password=${adminPassword};Encrypt=true;TrustServerCertificate=false;Connection Timeout=30;'
|
|
101
|
+
|
|
102
|
+
@description('Connection string template (no password)')
|
|
103
|
+
output connectionStringTemplate string = 'Server=tcp:${sqlServer.properties.fullyQualifiedDomainName},1433;Database=${databaseName};User ID=${adminUsername};Password={your_password};Encrypt=true;TrustServerCertificate=false;Connection Timeout=30;'
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// ==============================================================================
|
|
2
|
+
// MORPH-SPEC - Storage Account
|
|
3
|
+
// Azure Storage Account with Blob containers
|
|
4
|
+
// ==============================================================================
|
|
5
|
+
|
|
6
|
+
@description('Storage account name (lowercase, no hyphens, 3-24 chars)')
|
|
7
|
+
@minLength(3)
|
|
8
|
+
@maxLength(24)
|
|
9
|
+
param name string
|
|
10
|
+
|
|
11
|
+
@description('Location')
|
|
12
|
+
param location string
|
|
13
|
+
|
|
14
|
+
@description('Tags')
|
|
15
|
+
param tags object = {}
|
|
16
|
+
|
|
17
|
+
@description('SKU (Standard_LRS, Standard_GRS, Standard_ZRS)')
|
|
18
|
+
@allowed(['Standard_LRS', 'Standard_GRS', 'Standard_ZRS', 'Premium_LRS'])
|
|
19
|
+
param sku string = 'Standard_LRS'
|
|
20
|
+
|
|
21
|
+
@description('Create default blob containers')
|
|
22
|
+
param createContainers bool = true
|
|
23
|
+
|
|
24
|
+
// ==============================================================================
|
|
25
|
+
// STORAGE ACCOUNT
|
|
26
|
+
// ==============================================================================
|
|
27
|
+
|
|
28
|
+
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
|
|
29
|
+
name: name
|
|
30
|
+
location: location
|
|
31
|
+
tags: tags
|
|
32
|
+
kind: 'StorageV2'
|
|
33
|
+
sku: {
|
|
34
|
+
name: sku
|
|
35
|
+
}
|
|
36
|
+
properties: {
|
|
37
|
+
accessTier: 'Hot'
|
|
38
|
+
allowBlobPublicAccess: false
|
|
39
|
+
allowSharedKeyAccess: true
|
|
40
|
+
minimumTlsVersion: 'TLS1_2'
|
|
41
|
+
supportsHttpsTrafficOnly: true
|
|
42
|
+
networkAcls: {
|
|
43
|
+
defaultAction: 'Allow'
|
|
44
|
+
bypass: 'AzureServices'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ==============================================================================
|
|
50
|
+
// BLOB SERVICE
|
|
51
|
+
// ==============================================================================
|
|
52
|
+
|
|
53
|
+
resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
|
|
54
|
+
parent: storageAccount
|
|
55
|
+
name: 'default'
|
|
56
|
+
properties: {
|
|
57
|
+
deleteRetentionPolicy: {
|
|
58
|
+
enabled: true
|
|
59
|
+
days: 7
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ==============================================================================
|
|
65
|
+
// DEFAULT CONTAINERS
|
|
66
|
+
// ==============================================================================
|
|
67
|
+
|
|
68
|
+
resource uploadsContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = if (createContainers) {
|
|
69
|
+
parent: blobService
|
|
70
|
+
name: 'uploads'
|
|
71
|
+
properties: {
|
|
72
|
+
publicAccess: 'None'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
resource reportsContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = if (createContainers) {
|
|
77
|
+
parent: blobService
|
|
78
|
+
name: 'reports'
|
|
79
|
+
properties: {
|
|
80
|
+
publicAccess: 'None'
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
resource backupsContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = if (createContainers) {
|
|
85
|
+
parent: blobService
|
|
86
|
+
name: 'backups'
|
|
87
|
+
properties: {
|
|
88
|
+
publicAccess: 'None'
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ==============================================================================
|
|
93
|
+
// OUTPUTS
|
|
94
|
+
// ==============================================================================
|
|
95
|
+
|
|
96
|
+
@description('Storage Account ID')
|
|
97
|
+
output id string = storageAccount.id
|
|
98
|
+
|
|
99
|
+
@description('Storage Account name')
|
|
100
|
+
output name string = storageAccount.name
|
|
101
|
+
|
|
102
|
+
@description('Primary blob endpoint')
|
|
103
|
+
output primaryBlobEndpoint string = storageAccount.properties.primaryEndpoints.blob
|
|
104
|
+
|
|
105
|
+
@description('Connection string')
|
|
106
|
+
output connectionString string = 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value};EndpointSuffix=core.windows.net'
|