feat: comprehensive project structure improvements and Cloud for Sovereignty landing zone

- Add Cloud for Sovereignty landing zone architecture and deployment
- Implement complete legal document management system
- Reorganize documentation with improved navigation
- Add infrastructure improvements (Dockerfiles, K8s, monitoring)
- Add operational improvements (graceful shutdown, rate limiting, caching)
- Create comprehensive project structure documentation
- Add Azure deployment automation scripts
- Improve repository navigation and organization
This commit is contained in:
defiQUG
2025-11-13 09:32:55 -08:00
parent 92cc41d26d
commit 6a8582e54d
202 changed files with 22699 additions and 981 deletions

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: azure-config
namespace: the-order
data:
# Azure Configuration (from .env file)
AZURE_REGION: "westeurope" # Default, override via External Secrets
AZURE_SUBSCRIPTION_ID: "" # Set via External Secrets Operator from Key Vault
AZURE_TENANT_ID: "" # Set via External Secrets Operator from Key Vault
# Storage Configuration
AZURE_STORAGE_ACCOUNT: "" # Set via External Secrets Operator
AZURE_STORAGE_CONTAINER: "images"
AZURE_STORAGE_KEY: "" # Set via External Secrets Operator
# Key Vault Configuration
AZURE_KEY_VAULT_NAME: "" # Set via External Secrets Operator
AZURE_KEY_VAULT_URI: "" # Set via External Secrets Operator
# CDN Configuration
AZURE_CDN_PROFILE: "theorder-cdn"
AZURE_CDN_ENDPOINT: "theorder-cdn-endpoint"
CDN_BASE_URL: "" # Set via External Secrets Operator
# AKS Configuration
AKS_CLUSTER_NAME: "the-order-aks"
AKS_RESOURCE_GROUP: "the-order-rg"
# Database Configuration
DATABASE_HOST: "" # Set via External Secrets Operator
DATABASE_NAME: "theorder"
DATABASE_USER: "theorder_admin"
# DATABASE_PASSWORD set via External Secrets Operator

View File

@@ -0,0 +1,119 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: dataroom-service
namespace: the-order
labels:
app: dataroom-service
version: v1
spec:
replicas: 2
selector:
matchLabels:
app: dataroom-service
template:
metadata:
labels:
app: dataroom-service
version: v1
spec:
containers:
- name: dataroom
image: theorder/dataroom-service:latest
ports:
- containerPort: 4004
name: http
env:
- name: PORT
value: "4004"
- name: NODE_ENV
valueFrom:
configMapKeyRef:
name: the-order-config
key: ENVIRONMENT
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: the-order-secrets
key: database-url
- name: STORAGE_BUCKET
valueFrom:
secretKeyRef:
name: the-order-secrets
key: storage-bucket
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 4004
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 4004
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /health
port: 4004
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 30
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
---
apiVersion: v1
kind: Service
metadata:
name: dataroom-service
namespace: the-order
spec:
selector:
app: dataroom-service
ports:
- port: 80
targetPort: 4004
protocol: TCP
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: dataroom-service-hpa
namespace: the-order
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: dataroom-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80

View File

@@ -0,0 +1,58 @@
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: azure-keyvault
namespace: the-order
spec:
provider:
azurekv:
tenantId: "${AZURE_TENANT_ID}" # Set via environment variable
vaultUrl: "${AZURE_KEY_VAULT_URI}" # Set via environment variable
authType: WorkloadIdentity
serviceAccountRef:
name: external-secrets-sa
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: azure-secrets
namespace: the-order
spec:
refreshInterval: 1h
secretStoreRef:
name: azure-keyvault
kind: SecretStore
target:
name: the-order-secrets
creationPolicy: Owner
data:
# Database
- secretKey: database-url
remoteRef:
key: database-url
# Azure Storage
- secretKey: storage-account
remoteRef:
key: storage-account
- secretKey: storage-key
remoteRef:
key: storage-key
# Entra VerifiedID
- secretKey: entra-tenant-id
remoteRef:
key: entra-tenant-id
- secretKey: entra-client-id
remoteRef:
key: entra-client-id
- secretKey: entra-client-secret
remoteRef:
key: entra-client-secret
# Payment Gateway
- secretKey: payment-gateway-api-key
remoteRef:
key: payment-gateway-api-key
# Grafana
- secretKey: grafana-admin-password
remoteRef:
key: grafana-admin-password

View File

@@ -0,0 +1,119 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: finance-service
namespace: the-order
labels:
app: finance-service
version: v1
spec:
replicas: 2
selector:
matchLabels:
app: finance-service
template:
metadata:
labels:
app: finance-service
version: v1
spec:
containers:
- name: finance
image: theorder/finance-service:latest
ports:
- containerPort: 4003
name: http
env:
- name: PORT
value: "4003"
- name: NODE_ENV
valueFrom:
configMapKeyRef:
name: the-order-config
key: ENVIRONMENT
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: the-order-secrets
key: database-url
- name: PAYMENT_GATEWAY_API_KEY
valueFrom:
secretKeyRef:
name: the-order-secrets
key: payment-gateway-api-key
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 4003
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 4003
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /health
port: 4003
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 30
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
---
apiVersion: v1
kind: Service
metadata:
name: finance-service
namespace: the-order
spec:
selector:
app: finance-service
ports:
- port: 80
targetPort: 4003
protocol: TCP
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: finance-service-hpa
namespace: the-order
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: finance-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80

View File

@@ -0,0 +1,129 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: identity-service
namespace: the-order
labels:
app: identity-service
version: v1
spec:
replicas: 2
selector:
matchLabels:
app: identity-service
template:
metadata:
labels:
app: identity-service
version: v1
spec:
containers:
- name: identity
image: theorder/identity-service:latest
ports:
- containerPort: 4002
name: http
env:
- name: PORT
value: "4002"
- name: NODE_ENV
valueFrom:
configMapKeyRef:
name: the-order-config
key: ENVIRONMENT
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: the-order-secrets
key: database-url
- name: ENTRA_TENANT_ID
valueFrom:
secretKeyRef:
name: the-order-secrets
key: entra-tenant-id
- name: ENTRA_CLIENT_ID
valueFrom:
secretKeyRef:
name: the-order-secrets
key: entra-client-id
- name: ENTRA_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: the-order-secrets
key: entra-client-secret
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 4002
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 4002
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /health
port: 4002
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 30
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
---
apiVersion: v1
kind: Service
metadata:
name: identity-service
namespace: the-order
spec:
selector:
app: identity-service
ports:
- port: 80
targetPort: 4002
protocol: TCP
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: identity-service-hpa
namespace: the-order
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: identity-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80

View File

@@ -0,0 +1,119 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: intake-service
namespace: the-order
labels:
app: intake-service
version: v1
spec:
replicas: 2
selector:
matchLabels:
app: intake-service
template:
metadata:
labels:
app: intake-service
version: v1
spec:
containers:
- name: intake
image: theorder/intake-service:latest
ports:
- containerPort: 4001
name: http
env:
- name: PORT
value: "4001"
- name: NODE_ENV
valueFrom:
configMapKeyRef:
name: the-order-config
key: ENVIRONMENT
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: the-order-secrets
key: database-url
- name: STORAGE_BUCKET
valueFrom:
secretKeyRef:
name: the-order-secrets
key: storage-bucket
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 4001
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 4001
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /health
port: 4001
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 30
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
---
apiVersion: v1
kind: Service
metadata:
name: intake-service
namespace: the-order
spec:
selector:
app: intake-service
ports:
- port: 80
targetPort: 4001
protocol: TCP
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: intake-service-hpa
namespace: the-order
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: intake-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80

View File

@@ -7,10 +7,9 @@ resources:
- namespace.yaml
- configmap.yaml
- secrets.yaml
# Add service-specific resources
# - intake/
# - identity/
# - finance/
# - dataroom/
- intake/deployment.yaml
- identity/deployment.yaml
- finance/deployment.yaml
- dataroom/deployment.yaml
- legal-documents/deployment.yaml

View File

@@ -0,0 +1,114 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: legal-documents-service
namespace: the-order
labels:
app: legal-documents-service
version: v1
spec:
replicas: 2
selector:
matchLabels:
app: legal-documents-service
template:
metadata:
labels:
app: legal-documents-service
version: v1
spec:
containers:
- name: legal-documents
image: theorder/legal-documents-service:latest
ports:
- containerPort: 4005
name: http
env:
- name: PORT
value: "4005"
- name: NODE_ENV
valueFrom:
configMapKeyRef:
name: the-order-config
key: ENVIRONMENT
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: the-order-secrets
key: database-url
resources:
requests:
memory: "512Mi"
cpu: "200m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /health
port: 4005
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 4005
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /health
port: 4005
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 30
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
---
apiVersion: v1
kind: Service
metadata:
name: legal-documents-service
namespace: the-order
spec:
selector:
app: legal-documents-service
ports:
- port: 80
targetPort: 4005
protocol: TCP
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: legal-documents-service-hpa
namespace: the-order
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: legal-documents-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80

View File

@@ -0,0 +1,71 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: grafana
namespace: the-order
spec:
replicas: 1
selector:
matchLabels:
app: grafana
template:
metadata:
labels:
app: grafana
spec:
containers:
- name: grafana
image: grafana/grafana:latest
ports:
- containerPort: 3000
env:
- name: GF_SECURITY_ADMIN_USER
value: admin
- name: GF_SECURITY_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: the-order-secrets
key: grafana-admin-password
- name: GF_SERVER_ROOT_URL
value: "%(protocol)s://%(domain)s:%(http_port)s/grafana/"
volumeMounts:
- name: grafana-storage
mountPath: /var/lib/grafana
- name: grafana-dashboards
mountPath: /etc/grafana/provisioning/dashboards
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: grafana-storage
emptyDir: {}
- name: grafana-dashboards
configMap:
name: grafana-dashboards
---
apiVersion: v1
kind: Service
metadata:
name: grafana
namespace: the-order
spec:
selector:
app: grafana
ports:
- port: 3000
targetPort: 3000
type: ClusterIP
---
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-dashboards
namespace: the-order
data:
services-overview.json: |
# Dashboard JSON will be mounted from infra/monitoring/grafana-dashboards/

View File

@@ -0,0 +1,61 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: prometheus
namespace: the-order
spec:
replicas: 1
selector:
matchLabels:
app: prometheus
template:
metadata:
labels:
app: prometheus
spec:
containers:
- name: prometheus
image: prom/prometheus:latest
ports:
- containerPort: 9090
args:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
- '--storage.tsdb.retention.time=30d'
volumeMounts:
- name: prometheus-config
mountPath: /etc/prometheus
- name: prometheus-storage
mountPath: /prometheus
resources:
requests:
memory: "512Mi"
cpu: "200m"
limits:
memory: "2Gi"
cpu: "1000m"
---
apiVersion: v1
kind: Service
metadata:
name: prometheus
namespace: the-order
spec:
selector:
app: prometheus
ports:
- port: 9090
targetPort: 9090
type: ClusterIP
---
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-config
namespace: the-order
data:
prometheus.yml: |
# Prometheus configuration will be mounted from infra/monitoring/prometheus-config.yml

View File

@@ -0,0 +1,103 @@
groups:
- name: service_health
interval: 30s
rules:
- alert: ServiceDown
expr: up{job=~".*-service"} == 0
for: 5m
labels:
severity: critical
annotations:
summary: "Service {{ $labels.job }} is down"
description: "Service {{ $labels.job }} has been down for more than 5 minutes"
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "High error rate in {{ $labels.job }}"
description: "Error rate is {{ $value }} errors per second"
- alert: HighResponseTime
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 2
for: 10m
labels:
severity: warning
annotations:
summary: "High response time in {{ $labels.job }}"
description: "95th percentile response time is {{ $value }} seconds"
- name: resource_usage
interval: 30s
rules:
- alert: HighCPUUsage
expr: rate(container_cpu_usage_seconds_total[5m]) > 0.8
for: 10m
labels:
severity: warning
annotations:
summary: "High CPU usage in {{ $labels.pod }}"
description: "CPU usage is {{ $value }}"
- alert: HighMemoryUsage
expr: container_memory_usage_bytes / container_spec_memory_limit_bytes > 0.9
for: 10m
labels:
severity: warning
annotations:
summary: "High memory usage in {{ $labels.pod }}"
description: "Memory usage is {{ $value }}%"
- alert: PodCrashLooping
expr: rate(kube_pod_container_status_restarts_total[15m]) > 0
for: 5m
labels:
severity: critical
annotations:
summary: "Pod {{ $labels.pod }} is crash looping"
description: "Pod has restarted {{ $value }} times in the last 15 minutes"
- name: database
interval: 30s
rules:
- alert: DatabaseConnectionHigh
expr: pg_stat_database_numbackends / pg_stat_database_max_connections > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "High database connection usage"
description: "{{ $value }}% of max connections in use"
- alert: DatabaseSlowQueries
expr: rate(pg_stat_statements_mean_exec_time[5m]) > 1
for: 10m
labels:
severity: warning
annotations:
summary: "Slow database queries detected"
description: "Average query time is {{ $value }} seconds"
- name: entra_verifiedid
interval: 30s
rules:
- alert: EntraAPIFailure
expr: rate(entra_api_errors_total[5m]) > 0.1
for: 5m
labels:
severity: critical
annotations:
summary: "High Entra VerifiedID API error rate"
description: "Error rate is {{ $value }} errors per second"
- alert: EntraRateLimitApproaching
expr: entra_rate_limit_remaining / entra_rate_limit_total < 0.1
for: 5m
labels:
severity: warning
annotations:
summary: "Entra VerifiedID rate limit approaching"
description: "Only {{ $value }}% of rate limit remaining"

View File

@@ -0,0 +1,85 @@
{
"dashboard": {
"title": "The Order Services Overview",
"tags": ["the-order", "services", "overview"],
"timezone": "browser",
"schemaVersion": 27,
"version": 1,
"refresh": "30s",
"panels": [
{
"id": 1,
"title": "Request Rate",
"type": "graph",
"targets": [
{
"expr": "sum(rate(http_requests_total[5m])) by (job)",
"legendFormat": "{{job}}"
}
],
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0}
},
{
"id": 2,
"title": "Error Rate",
"type": "graph",
"targets": [
{
"expr": "sum(rate(http_requests_total{status=~\"5..\"}[5m])) by (job)",
"legendFormat": "{{job}}"
}
],
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0}
},
{
"id": 3,
"title": "Response Time (95th percentile)",
"type": "graph",
"targets": [
{
"expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, job))",
"legendFormat": "{{job}}"
}
],
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 8}
},
{
"id": 4,
"title": "CPU Usage",
"type": "graph",
"targets": [
{
"expr": "sum(rate(container_cpu_usage_seconds_total[5m])) by (pod)",
"legendFormat": "{{pod}}"
}
],
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 8}
},
{
"id": 5,
"title": "Memory Usage",
"type": "graph",
"targets": [
{
"expr": "sum(container_memory_usage_bytes) by (pod) / sum(container_spec_memory_limit_bytes) by (pod) * 100",
"legendFormat": "{{pod}}"
}
],
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 16}
},
{
"id": 6,
"title": "Active Connections",
"type": "graph",
"targets": [
{
"expr": "sum(pg_stat_database_numbackends) by (datname)",
"legendFormat": "{{datname}}"
}
],
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 16}
}
]
}
}

View File

@@ -0,0 +1,60 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: fluentd-config
namespace: the-order
data:
fluent.conf: |
<source>
@type tail
path /var/log/containers/*.log
pos_file /var/log/fluentd-containers.log.pos
tag kubernetes.*
read_from_head true
<parse>
@type json
time_key time
time_format %Y-%m-%dT%H:%M:%S.%NZ
keep_time_key true
</parse>
</source>
<filter kubernetes.**>
@type kubernetes_metadata
</filter>
<filter kubernetes.**>
@type record_transformer
<record>
cluster_name the-order
environment ${ENVIRONMENT:-production}
</record>
</filter>
<match kubernetes.**>
@type opensearch
host opensearch.logging.svc.cluster.local
port 9200
index_name the-order-logs
type_name _doc
logstash_format true
logstash_prefix the-order
logstash_dateformat %Y.%m.%d
include_tag_key true
tag_key @log_name
flush_interval 10s
<buffer>
@type file
path /var/log/fluentd-buffers/kubernetes.system.buffer
flush_mode interval
retry_type exponential_backoff
flush_thread_count 2
flush_interval 5s
retry_max_interval 30
retry_timeout 60m
chunk_limit_size 2M
queue_limit_length 8
overflow_action block
</buffer>
</match>

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: opensearch-config
namespace: the-order
data:
opensearch.yml: |
cluster.name: the-order-logs
node.name: opensearch-0
network.host: 0.0.0.0
discovery.type: single-node
path.data: /usr/share/opensearch/data
path.logs: /usr/share/opensearch/logs
plugins.security.disabled: true

View File

@@ -0,0 +1,142 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
cluster: 'the-order'
environment: 'production'
scrape_configs:
# Prometheus itself
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# Intake Service
- job_name: 'intake-service'
kubernetes_sd_configs:
- role: pod
namespaces:
names:
- the-order
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
action: keep
regex: intake-service
- source_labels: [__meta_kubernetes_pod_ip]
action: replace
target_label: __address__
replacement: $1:4001
- action: labelmap
regex: __meta_kubernetes_pod_label_(.+)
metrics_path: '/metrics'
# Identity Service
- job_name: 'identity-service'
kubernetes_sd_configs:
- role: pod
namespaces:
names:
- the-order
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
action: keep
regex: identity-service
- source_labels: [__meta_kubernetes_pod_ip]
action: replace
target_label: __address__
replacement: $1:4002
- action: labelmap
regex: __meta_kubernetes_pod_label_(.+)
metrics_path: '/metrics'
# Finance Service
- job_name: 'finance-service'
kubernetes_sd_configs:
- role: pod
namespaces:
names:
- the-order
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
action: keep
regex: finance-service
- source_labels: [__meta_kubernetes_pod_ip]
action: replace
target_label: __address__
replacement: $1:4003
- action: labelmap
regex: __meta_kubernetes_pod_label_(.+)
metrics_path: '/metrics'
# Dataroom Service
- job_name: 'dataroom-service'
kubernetes_sd_configs:
- role: pod
namespaces:
names:
- the-order
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
action: keep
regex: dataroom-service
- source_labels: [__meta_kubernetes_pod_ip]
action: replace
target_label: __address__
replacement: $1:4004
- action: labelmap
regex: __meta_kubernetes_pod_label_(.+)
metrics_path: '/metrics'
# Legal Documents Service
- job_name: 'legal-documents-service'
kubernetes_sd_configs:
- role: pod
namespaces:
names:
- the-order
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
action: keep
regex: legal-documents-service
- source_labels: [__meta_kubernetes_pod_ip]
action: replace
target_label: __address__
replacement: $1:4005
- action: labelmap
regex: __meta_kubernetes_pod_label_(.+)
metrics_path: '/metrics'
# Kubernetes API
- job_name: 'kubernetes-apiservers'
kubernetes_sd_configs:
- role: endpoints
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
relabel_configs:
- source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
action: keep
regex: default;kubernetes;https
# Kubernetes nodes
- job_name: 'kubernetes-nodes'
kubernetes_sd_configs:
- role: node
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
relabel_configs:
- action: labelmap
regex: __meta_kubernetes_node_label_(.+)
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
rule_files:
- '/etc/prometheus/alerts/*.yml'

View File

@@ -0,0 +1,53 @@
#!/bin/bash
# Complete Azure setup using .env file
# This script orchestrates the entire Azure deployment setup process
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ COMPLETE AZURE SETUP FROM .ENV FILE ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
# Step 1: Integrate existing CDN config if available
echo "Step 1: Integrating existing CDN configuration..."
"$SCRIPT_DIR/azure-integrate-cdn-env.sh"
# Step 2: Validate environment
echo ""
echo "Step 2: Validating environment variables..."
source "$SCRIPT_DIR/azure-validate-env.sh"
# Step 3: Sync to Terraform
echo ""
echo "Step 3: Syncing environment to Terraform..."
"$SCRIPT_DIR/azure-sync-env-to-terraform.sh"
# Step 4: Update Kubernetes configs
echo ""
echo "Step 4: Updating Kubernetes configurations..."
"$SCRIPT_DIR/azure-update-k8s-secrets.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ SETUP COMPLETE - READY FOR DEPLOYMENT ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
echo "✅ All configurations synced from .env file"
echo ""
echo "Next steps:"
echo " 1. Review Terraform plan:"
echo " cd infra/terraform && terraform plan"
echo ""
echo " 2. Deploy infrastructure:"
echo " ./infra/scripts/azure-deploy.sh"
echo ""
echo " 3. After deployment, update Kubernetes secrets:"
echo " ./infra/scripts/azure-update-k8s-secrets.sh"
echo ""
echo " 4. Deploy services to Kubernetes:"
echo " kubectl apply -k infra/k8s/overlays/dev"

59
infra/scripts/azure-deploy.sh Executable file
View File

@@ -0,0 +1,59 @@
#!/bin/bash
# Complete Azure deployment script
# Uses environment variables from .env file
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
TERRAFORM_DIR="$PROJECT_ROOT/infra/terraform"
echo "🚀 Starting Azure deployment..."
# Load environment variables
source "$SCRIPT_DIR/azure-load-env.sh"
# Change to Terraform directory
cd "$TERRAFORM_DIR"
# Initialize Terraform
echo "📦 Initializing Terraform..."
terraform init
# Validate configuration
echo "✅ Validating Terraform configuration..."
terraform validate
# Plan deployment
echo "📋 Planning deployment..."
terraform plan -out=tfplan
# Ask for confirmation
read -p "Do you want to apply these changes? (yes/no): " -r
if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then
echo "Deployment cancelled."
exit 0
fi
# Apply changes
echo "🔨 Applying Terraform configuration..."
terraform apply tfplan
# Get outputs
echo ""
echo "📊 Deployment outputs:"
terraform output
# Save kubeconfig if AKS was created
if terraform output -raw aks_kube_config > /dev/null 2>&1; then
KUBECONFIG_FILE="$PROJECT_ROOT/.kube/config"
mkdir -p "$(dirname "$KUBECONFIG_FILE")"
terraform output -raw aks_kube_config > "$KUBECONFIG_FILE"
echo ""
echo "✅ Kubernetes config saved to: $KUBECONFIG_FILE"
echo " You can now use: kubectl --kubeconfig=$KUBECONFIG_FILE get nodes"
fi
echo ""
echo "✅ Azure deployment complete!"

View File

@@ -0,0 +1,77 @@
#!/bin/bash
# Fix .env file to ensure proper ARM_* variable mapping for Terraform
# Adds ARM_* aliases for AZURE_* variables if they don't exist
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
ENV_FILE="$PROJECT_ROOT/.env"
BACKUP_FILE="${ENV_FILE}.backup.$(date +%Y%m%d_%H%M%S)"
if [ ! -f "$ENV_FILE" ]; then
echo "❌ .env file not found at: $ENV_FILE"
exit 1
fi
echo "🔧 Fixing .env file variable mapping..."
echo ""
# Backup original
cp "$ENV_FILE" "$BACKUP_FILE"
echo "✓ Backup created: $BACKUP_FILE"
echo ""
# Load current values
set -a
source "$ENV_FILE"
set +a
# Check what needs to be added
ADDITIONS=()
if [ -n "$AZURE_SUBSCRIPTION_ID" ] && [ -z "$ARM_SUBSCRIPTION_ID" ]; then
ADDITIONS+=("ARM_SUBSCRIPTION_ID=\"$AZURE_SUBSCRIPTION_ID\"")
fi
if [ -n "$AZURE_TENANT_ID" ] && [ -z "$ARM_TENANT_ID" ]; then
ADDITIONS+=("ARM_TENANT_ID=\"$AZURE_TENANT_ID\"")
fi
if [ -n "$AZURE_LOCATION" ] && [ -z "$ARM_LOCATION" ]; then
ADDITIONS+=("ARM_LOCATION=\"$AZURE_LOCATION\"")
fi
# Add TF_VAR_environment if not set
if [ -z "$TF_VAR_environment" ]; then
ADDITIONS+=("TF_VAR_environment=\"dev\"")
fi
if [ ${#ADDITIONS[@]} -eq 0 ]; then
echo "✅ No fixes needed - all variables are properly mapped"
rm -f "$BACKUP_FILE"
exit 0
fi
echo "Adding the following variables:"
for var in "${ADDITIONS[@]}"; do
echo " + $var"
done
echo ""
# Append to .env file
echo "" >> "$ENV_FILE"
echo "# Terraform ARM variables (auto-added by azure-fix-env-mapping.sh)" >> "$ENV_FILE"
for var in "${ADDITIONS[@]}"; do
echo "$var" >> "$ENV_FILE"
done
echo "✅ .env file updated!"
echo ""
echo "Changes:"
echo " • Added ${#ADDITIONS[@]} variable(s)"
echo " • Backup saved to: $BACKUP_FILE"
echo ""
echo "To verify:"
echo " ./infra/scripts/azure-validate-current-env.sh"

View File

@@ -0,0 +1,68 @@
#!/bin/bash
# Integrate existing Azure CDN configuration from azure-cdn-config.env
# Updates .env file with CDN values if they exist
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
CDN_CONFIG="$PROJECT_ROOT/azure-cdn-config.env"
ENV_FILE="$PROJECT_ROOT/.env"
echo "🔄 Integrating Azure CDN configuration..."
if [ -f "$CDN_CONFIG" ]; then
echo "Found existing CDN configuration: $CDN_CONFIG"
# Load CDN config
set -a
source "$CDN_CONFIG"
set +a
# Update .env file with CDN values if not already set
if [ -f "$ENV_FILE" ]; then
# Check if CDN values are already in .env
if ! grep -q "AZURE_STORAGE_ACCOUNT=" "$ENV_FILE" 2>/dev/null; then
echo "Adding CDN configuration to .env file..."
cat >> "$ENV_FILE" << EOF
# Azure CDN Configuration (from azure-cdn-config.env)
AZURE_STORAGE_ACCOUNT=${AZURE_STORAGE_ACCOUNT:-}
AZURE_STORAGE_KEY=${AZURE_STORAGE_KEY:-}
AZURE_STORAGE_CONTAINER=${AZURE_STORAGE_CONTAINER:-images}
AZURE_RESOURCE_GROUP=${AZURE_RESOURCE_GROUP:-}
AZURE_CDN_PROFILE=${AZURE_CDN_PROFILE:-}
AZURE_CDN_ENDPOINT=${AZURE_CDN_ENDPOINT:-}
CDN_BASE_URL=${CDN_BASE_URL:-}
CDN_BASE_URL_BLOB=${CDN_BASE_URL_BLOB:-}
CDN_BASE_URL_CDN=${CDN_BASE_URL_CDN:-}
EOF
echo "✅ CDN configuration added to .env"
else
echo " CDN configuration already exists in .env"
fi
else
echo "⚠️ .env file not found. Creating from CDN config..."
cp "$CDN_CONFIG" "$ENV_FILE"
echo "✅ Created .env from CDN config"
fi
# Export for Terraform
export TF_VAR_storage_account_name="${AZURE_STORAGE_ACCOUNT}"
export TF_VAR_cdn_profile_name="${AZURE_CDN_PROFILE}"
export TF_VAR_cdn_endpoint_name="${AZURE_CDN_ENDPOINT}"
echo ""
echo "CDN Configuration:"
echo " Storage Account: ${AZURE_STORAGE_ACCOUNT}"
echo " CDN Profile: ${AZURE_CDN_PROFILE}"
echo " CDN Endpoint: ${AZURE_CDN_ENDPOINT}"
echo " Base URL: ${CDN_BASE_URL}"
else
echo " No existing CDN configuration found at: $CDN_CONFIG"
echo " CDN will be created by Terraform if needed"
fi
echo ""
echo "✅ CDN integration complete!"

98
infra/scripts/azure-load-env.sh Executable file
View File

@@ -0,0 +1,98 @@
#!/bin/bash
# Load Azure environment variables from .env file
# Usage: source infra/scripts/azure-load-env.sh
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
ENV_FILE="$PROJECT_ROOT/.env"
TERRAFORM_ENV_FILE="$PROJECT_ROOT/infra/terraform/.env"
echo "🔧 Loading Azure environment variables..."
# Check for .env file in project root
if [ -f "$ENV_FILE" ]; then
echo "Loading from: $ENV_FILE"
set -a
source "$ENV_FILE"
set +a
elif [ -f "$TERRAFORM_ENV_FILE" ]; then
echo "Loading from: $TERRAFORM_ENV_FILE"
set -a
source "$TERRAFORM_ENV_FILE"
set +a
else
echo "⚠️ No .env file found. Looking for:"
echo " - $ENV_FILE"
echo " - $TERRAFORM_ENV_FILE"
echo ""
echo "Creating example file..."
cp "$PROJECT_ROOT/infra/terraform/.env.example" "$TERRAFORM_ENV_FILE"
echo "✅ Created $TERRAFORM_ENV_FILE"
echo "Please edit it with your Azure credentials and run this script again."
return 1
fi
# Map AZURE_* to ARM_* if needed (for Terraform compatibility)
if [ -n "$AZURE_SUBSCRIPTION_ID" ] && [ -z "$ARM_SUBSCRIPTION_ID" ]; then
export ARM_SUBSCRIPTION_ID="$AZURE_SUBSCRIPTION_ID"
fi
if [ -n "$AZURE_TENANT_ID" ] && [ -z "$ARM_TENANT_ID" ]; then
export ARM_TENANT_ID="$AZURE_TENANT_ID"
fi
if [ -n "$AZURE_LOCATION" ] && [ -z "$ARM_LOCATION" ]; then
export ARM_LOCATION="$AZURE_LOCATION"
fi
# Validate required variables (check both ARM_* and AZURE_*)
SUBSCRIPTION_ID="${ARM_SUBSCRIPTION_ID:-$AZURE_SUBSCRIPTION_ID}"
TENANT_ID="${ARM_TENANT_ID:-$AZURE_TENANT_ID}"
MISSING_VARS=()
if [ -z "$SUBSCRIPTION_ID" ]; then
MISSING_VARS+=("ARM_SUBSCRIPTION_ID or AZURE_SUBSCRIPTION_ID")
fi
if [ -z "$TENANT_ID" ]; then
MISSING_VARS+=("ARM_TENANT_ID or AZURE_TENANT_ID")
fi
if [ ${#MISSING_VARS[@]} -gt 0 ]; then
echo "❌ Missing required environment variables:"
for var in "${MISSING_VARS[@]}"; do
echo " - $var"
done
echo ""
echo "Please set these in your .env file."
return 1
fi
# Set Terraform variables from environment (use mapped values)
export TF_VAR_subscription_id="${SUBSCRIPTION_ID}"
export TF_VAR_tenant_id="${TENANT_ID}"
export TF_VAR_client_id="${ARM_CLIENT_ID:-$AZURE_CLIENT_ID:-}"
export TF_VAR_client_secret="${ARM_CLIENT_SECRET:-$AZURE_CLIENT_SECRET:-}"
# Set Azure CLI defaults if using CLI auth
if [ -z "$ARM_CLIENT_ID" ] && [ -z "$AZURE_CLIENT_ID" ]; then
echo " Using Azure CLI authentication (no service principal set)"
az account set --subscription "$SUBSCRIPTION_ID" 2>/dev/null || true
fi
echo "✅ Environment variables loaded"
echo ""
echo "Azure Configuration:"
echo " Subscription ID: ${SUBSCRIPTION_ID:0:8}...${SUBSCRIPTION_ID: -4}"
echo " Tenant ID: ${TENANT_ID:0:8}...${TENANT_ID: -4}"
echo " Location: ${ARM_LOCATION:-${AZURE_LOCATION:-westeurope}}"
echo " Environment: ${TF_VAR_environment:-dev}"
if [ -n "$AZURE_MANAGEMENT_GROUP_ID" ]; then
echo " Management Group: $AZURE_MANAGEMENT_GROUP_ID"
fi
echo ""
echo "You can now run Terraform commands."

View File

@@ -0,0 +1,66 @@
#!/bin/bash
# Sync environment variables from .env to Terraform variables
# Ensures Terraform uses values from .env file
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
ENV_FILE="$PROJECT_ROOT/.env"
TERRAFORM_DIR="$PROJECT_ROOT/infra/terraform"
echo "🔄 Syncing environment variables to Terraform..."
# Load and validate environment
source "$SCRIPT_DIR/azure-validate-env.sh"
# Create terraform.tfvars from environment variables
TFVARS_FILE="$TERRAFORM_DIR/terraform.tfvars"
cat > "$TFVARS_FILE" << EOF
# Terraform variables generated from .env file
# DO NOT EDIT MANUALLY - regenerated by azure-sync-env-to-terraform.sh
# Last updated: $(date -u +"%Y-%m-%d %H:%M:%S UTC")
# Azure Configuration
azure_region = "${ARM_LOCATION:-westeurope}"
environment = "${TF_VAR_environment:-dev}"
project_name = "the-order"
# Azure Authentication (sensitive - use environment variables)
# subscription_id = "${ARM_SUBSCRIPTION_ID}"
# tenant_id = "${ARM_TENANT_ID}"
# client_id = "${ARM_CLIENT_ID:-}"
# client_secret = "${ARM_CLIENT_SECRET:-}"
# Resource Naming
resource_group_name = "${TF_VAR_resource_group_name}"
storage_account_name = "${TF_VAR_storage_account_name}"
key_vault_name = "${TF_VAR_key_vault_name}"
# AKS Configuration
aks_cluster_name = "${TF_VAR_aks_cluster_name:-the-order-aks-${TF_VAR_environment:-dev}}"
aks_node_count = ${TF_VAR_aks_node_count:-2}
aks_vm_size = "${TF_VAR_aks_vm_size:-Standard_B2s}"
# Database Configuration
database_name = "${TF_VAR_database_name:-the-order-db-${TF_VAR_environment:-dev}}"
database_admin_user = "${TF_VAR_database_admin_user:-theorder_admin}"
# Tags
tags = {
Environment = "${TF_VAR_environment:-dev}"
Project = "the-order"
ManagedBy = "terraform"
CreatedBy = "azure-sync-env-to-terraform.sh"
}
EOF
echo "✅ Terraform variables synced to: $TFVARS_FILE"
echo ""
echo "You can now run Terraform commands:"
echo " cd $TERRAFORM_DIR"
echo " terraform init"
echo " terraform plan"
echo " terraform apply"

View File

@@ -0,0 +1,64 @@
#!/bin/bash
# Update Kubernetes secrets from Azure Key Vault
# Uses values from .env file to configure External Secrets
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
echo "🔄 Updating Kubernetes secrets configuration from .env..."
# Load environment
source "$SCRIPT_DIR/azure-validate-env.sh"
# Get Key Vault URI from Terraform output if available
cd "$PROJECT_ROOT/infra/terraform"
if terraform output -json key_vault_uri &> /dev/null; then
KEY_VAULT_URI=$(terraform output -raw key_vault_uri)
echo "Found Key Vault URI from Terraform: $KEY_VAULT_URI"
else
# Construct from known values
KEY_VAULT_NAME="${TF_VAR_key_vault_name:-the-order-kv-${TF_VAR_environment:-dev}}"
KEY_VAULT_URI="https://${KEY_VAULT_NAME}.vault.azure.net/"
echo "Using constructed Key Vault URI: $KEY_VAULT_URI"
fi
# Update External Secrets configuration
EXTERNAL_SECRETS_FILE="$PROJECT_ROOT/infra/k8s/base/external-secrets.yaml"
# Use sed or create a template update
if [ -f "$EXTERNAL_SECRETS_FILE" ]; then
# Create updated version
sed -i.bak "s|tenantId: \"\"|tenantId: \"${ARM_TENANT_ID}\"|g" "$EXTERNAL_SECRETS_FILE"
sed -i.bak "s|vaultUrl: \"\"|vaultUrl: \"${KEY_VAULT_URI}\"|g" "$EXTERNAL_SECRETS_FILE"
rm -f "${EXTERNAL_SECRETS_FILE}.bak"
echo "✅ Updated External Secrets configuration"
else
echo "⚠️ External Secrets file not found: $EXTERNAL_SECRETS_FILE"
fi
# Update Azure ConfigMap
CONFIGMAP_FILE="$PROJECT_ROOT/infra/k8s/base/configmap-azure.yaml"
if [ -f "$CONFIGMAP_FILE" ]; then
# Update with actual values (non-sensitive)
sed -i.bak "s|AZURE_REGION: \".*\"|AZURE_REGION: \"${ARM_LOCATION:-westeurope}\"|g" "$CONFIGMAP_FILE"
sed -i.bak "s|AKS_RESOURCE_GROUP: \".*\"|AKS_RESOURCE_GROUP: \"${TF_VAR_resource_group_name}\"|g" "$CONFIGMAP_FILE"
rm -f "${CONFIGMAP_FILE}.bak"
echo "✅ Updated Azure ConfigMap"
else
echo "⚠️ ConfigMap file not found: $CONFIGMAP_FILE"
fi
echo ""
echo "✅ Kubernetes secrets configuration updated!"
echo ""
echo "Next steps:"
echo " 1. Review updated files:"
echo " - $EXTERNAL_SECRETS_FILE"
echo " - $CONFIGMAP_FILE"
echo " 2. Apply to Kubernetes:"
echo " kubectl apply -f $EXTERNAL_SECRETS_FILE"
echo " kubectl apply -f $CONFIGMAP_FILE"

View File

@@ -0,0 +1,188 @@
#!/bin/bash
# Validate current .env file against Azure deployment requirements
# Provides detailed analysis and recommendations
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
ENV_FILE="$PROJECT_ROOT/.env"
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ .ENV FILE ANALYSIS FOR AZURE DEPLOYMENTS ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
if [ ! -f "$ENV_FILE" ]; then
echo "❌ .env file not found at: $ENV_FILE"
exit 1
fi
echo "📄 Analyzing: $ENV_FILE"
echo ""
# Load environment
set -a
source "$ENV_FILE"
set +a
# Check required variables
echo "✅ REQUIRED VARIABLES:"
echo ""
# Subscription ID
if [ -n "$AZURE_SUBSCRIPTION_ID" ] || [ -n "$ARM_SUBSCRIPTION_ID" ]; then
SUB_ID="${AZURE_SUBSCRIPTION_ID:-$ARM_SUBSCRIPTION_ID}"
echo " ✓ Subscription ID: ${SUB_ID:0:8}...${SUB_ID: -4}"
if [[ ! "$SUB_ID" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
echo " ⚠️ Warning: Format may be invalid (should be UUID)"
fi
else
echo " ❌ Subscription ID: MISSING"
fi
# Tenant ID
if [ -n "$AZURE_TENANT_ID" ] || [ -n "$ARM_TENANT_ID" ]; then
TENANT_ID="${AZURE_TENANT_ID:-$ARM_TENANT_ID}"
echo " ✓ Tenant ID: ${TENANT_ID:0:8}...${TENANT_ID: -4}"
if [[ ! "$TENANT_ID" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
echo " ⚠️ Warning: Format may be invalid (should be UUID)"
fi
else
echo " ❌ Tenant ID: MISSING"
fi
# Location
if [ -n "$AZURE_LOCATION" ] || [ -n "$ARM_LOCATION" ]; then
LOCATION="${AZURE_LOCATION:-$ARM_LOCATION}"
echo " ✓ Location: $LOCATION"
if [[ "$LOCATION" =~ ^us ]]; then
echo " ❌ ERROR: US regions are not allowed!"
fi
else
echo " ⚠️ Location: NOT SET (will default to westeurope)"
fi
echo ""
echo "📋 OPTIONAL BUT RECOMMENDED:"
echo ""
# Management Group
if [ -n "$AZURE_MANAGEMENT_GROUP_ID" ]; then
echo " ✓ Management Group: $AZURE_MANAGEMENT_GROUP_ID"
else
echo " ○ Management Group: Not set"
fi
# Resource Group
if [ -n "$AZURE_RESOURCE_GROUP" ] || [ -n "$TF_VAR_resource_group_name" ]; then
RG="${AZURE_RESOURCE_GROUP:-$TF_VAR_resource_group_name}"
echo " ✓ Resource Group: $RG"
else
echo " ⚠️ Resource Group: Not set (will use default naming convention)"
fi
# Environment
if [ -n "$TF_VAR_environment" ]; then
echo " ✓ Environment: $TF_VAR_environment"
else
echo " ⚠️ Environment: Not set (will default to 'dev')"
fi
# Storage Account
if [ -n "$TF_VAR_storage_account_name" ] || [ -n "$AZURE_STORAGE_ACCOUNT" ]; then
SA="${TF_VAR_storage_account_name:-$AZURE_STORAGE_ACCOUNT}"
echo " ✓ Storage Account: $SA"
else
echo " ⚠️ Storage Account: Not set (will use default naming)"
fi
# Key Vault
if [ -n "$TF_VAR_key_vault_name" ] || [ -n "$AZURE_KEY_VAULT_NAME" ]; then
KV="${TF_VAR_key_vault_name:-$AZURE_KEY_VAULT_NAME}"
echo " ✓ Key Vault: $KV"
else
echo " ⚠️ Key Vault: Not set (will use default naming)"
fi
echo ""
echo "🔧 TERRAFORM VARIABLE MAPPING:"
echo ""
# Check if variables need to be mapped
NEEDS_MAPPING=false
if [ -n "$AZURE_SUBSCRIPTION_ID" ] && [ -z "$ARM_SUBSCRIPTION_ID" ]; then
echo " ⚠️ AZURE_SUBSCRIPTION_ID found, but Terraform expects ARM_SUBSCRIPTION_ID"
echo " Recommendation: Add ARM_SUBSCRIPTION_ID=\"$AZURE_SUBSCRIPTION_ID\""
NEEDS_MAPPING=true
fi
if [ -n "$AZURE_TENANT_ID" ] && [ -z "$ARM_TENANT_ID" ]; then
echo " ⚠️ AZURE_TENANT_ID found, but Terraform expects ARM_TENANT_ID"
echo " Recommendation: Add ARM_TENANT_ID=\"$AZURE_TENANT_ID\""
NEEDS_MAPPING=true
fi
if [ -n "$AZURE_LOCATION" ] && [ -z "$ARM_LOCATION" ]; then
echo " ⚠️ AZURE_LOCATION found, but Terraform expects ARM_LOCATION"
echo " Recommendation: Add ARM_LOCATION=\"$AZURE_LOCATION\""
NEEDS_MAPPING=true
fi
if [ "$NEEDS_MAPPING" = false ]; then
echo " ✓ All variables properly mapped for Terraform"
fi
echo ""
echo "📊 SUMMARY:"
echo ""
# Count issues
ISSUES=0
WARNINGS=0
if [ -z "$AZURE_SUBSCRIPTION_ID" ] && [ -z "$ARM_SUBSCRIPTION_ID" ]; then
ISSUES=$((ISSUES + 1))
fi
if [ -z "$AZURE_TENANT_ID" ] && [ -z "$ARM_TENANT_ID" ]; then
ISSUES=$((ISSUES + 1))
fi
if [ -z "$AZURE_LOCATION" ] && [ -z "$ARM_LOCATION" ]; then
WARNINGS=$((WARNINGS + 1))
fi
if [ "$ISSUES" -eq 0 ] && [ "$WARNINGS" -eq 0 ]; then
echo " ✅ .env file is properly configured for Azure deployments"
elif [ "$ISSUES" -eq 0 ]; then
echo " ⚠️ .env file is mostly configured ($WARNINGS warning(s))"
else
echo " ❌ .env file has $ISSUES critical issue(s) and $WARNINGS warning(s)"
fi
echo ""
echo "💡 RECOMMENDATIONS:"
echo ""
if [ "$NEEDS_MAPPING" = true ]; then
echo " 1. Add ARM_* variables for Terraform compatibility"
echo " (Our scripts will auto-map, but explicit is better)"
fi
if [ -z "$TF_VAR_environment" ]; then
echo " 2. Add TF_VAR_environment=\"dev\" (or stage/prod)"
fi
if [ -z "$TF_VAR_resource_group_name" ] && [ -z "$AZURE_RESOURCE_GROUP" ]; then
echo " 3. Consider setting TF_VAR_resource_group_name for custom naming"
fi
echo ""
echo "✅ Analysis complete!"
echo ""
echo "To use with Azure deployments:"
echo " source infra/scripts/azure-validate-env.sh"
echo " ./infra/scripts/azure-complete-setup.sh"

View File

@@ -0,0 +1,133 @@
#!/bin/bash
# Validate Azure environment variables from .env file
# Ensures all required variables are set for deployments
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
ENV_FILE="$PROJECT_ROOT/.env"
echo "🔍 Validating Azure environment configuration..."
# Load environment file if it exists
if [ -f "$ENV_FILE" ]; then
echo "Loading environment from: $ENV_FILE"
set -a
source "$ENV_FILE"
set +a
else
echo "⚠️ No .env file found at: $ENV_FILE"
echo "Creating from example..."
if [ -f "$PROJECT_ROOT/infra/terraform/.env.example" ]; then
cp "$PROJECT_ROOT/infra/terraform/.env.example" "$ENV_FILE"
echo "✅ Created $ENV_FILE - please fill in your values"
fi
exit 1
fi
# Required Azure variables
REQUIRED_VARS=(
"ARM_SUBSCRIPTION_ID"
"ARM_TENANT_ID"
)
# Optional but recommended
RECOMMENDED_VARS=(
"ARM_LOCATION"
"TF_VAR_environment"
"TF_VAR_resource_group_name"
"TF_VAR_storage_account_name"
"TF_VAR_key_vault_name"
)
# Check required variables
MISSING_REQUIRED=()
for var in "${REQUIRED_VARS[@]}"; do
if [ -z "${!var}" ]; then
MISSING_REQUIRED+=("$var")
fi
done
# Check recommended variables
MISSING_RECOMMENDED=()
for var in "${RECOMMENDED_VARS[@]}"; do
if [ -z "${!var}" ]; then
MISSING_RECOMMENDED+=("$var")
fi
done
# Report results
if [ ${#MISSING_REQUIRED[@]} -gt 0 ]; then
echo "❌ Missing required variables:"
for var in "${MISSING_REQUIRED[@]}"; do
echo " - $var"
done
echo ""
echo "Please set these in your .env file."
exit 1
fi
if [ ${#MISSING_RECOMMENDED[@]} -gt 0 ]; then
echo "⚠️ Missing recommended variables (will use defaults):"
for var in "${MISSING_RECOMMENDED[@]}"; do
echo " - $var"
done
echo ""
fi
# Validate Azure CLI authentication
if command -v az &> /dev/null; then
if az account show &> /dev/null; then
CURRENT_SUB=$(az account show --query id -o tsv)
if [ "$CURRENT_SUB" != "$ARM_SUBSCRIPTION_ID" ]; then
echo "⚠️ Azure CLI subscription ($CURRENT_SUB) differs from ARM_SUBSCRIPTION_ID"
echo " Setting Azure CLI to use: $ARM_SUBSCRIPTION_ID"
az account set --subscription "$ARM_SUBSCRIPTION_ID" || true
fi
else
echo "⚠️ Not logged in to Azure CLI. Run: az login"
fi
else
echo "⚠️ Azure CLI not installed. Install from: https://aka.ms/InstallAzureCLIDeb"
fi
# Set defaults for missing recommended vars
export ARM_LOCATION="${ARM_LOCATION:-westeurope}"
export TF_VAR_environment="${TF_VAR_environment:-dev}"
export TF_VAR_azure_region="${ARM_LOCATION}"
# Export Terraform variables
export TF_VAR_subscription_id="${ARM_SUBSCRIPTION_ID}"
export TF_VAR_tenant_id="${ARM_TENANT_ID}"
export TF_VAR_client_id="${ARM_CLIENT_ID:-}"
export TF_VAR_client_secret="${ARM_CLIENT_SECRET:-}"
# Generate resource names if not set
if [ -z "$TF_VAR_resource_group_name" ]; then
export TF_VAR_resource_group_name="the-order-rg-${TF_VAR_environment}"
fi
if [ -z "$TF_VAR_storage_account_name" ]; then
# Generate unique storage account name
TIMESTAMP=$(date +%s | tail -c 5)
export TF_VAR_storage_account_name="theorder${TF_VAR_environment}${TIMESTAMP}"
fi
if [ -z "$TF_VAR_key_vault_name" ]; then
export TF_VAR_key_vault_name="the-order-kv-${TF_VAR_environment}"
fi
echo "✅ Environment validation complete!"
echo ""
echo "Azure Configuration:"
echo " Subscription ID: ${ARM_SUBSCRIPTION_ID:0:8}..."
echo " Tenant ID: ${ARM_TENANT_ID:0:8}..."
echo " Location: ${ARM_LOCATION}"
echo " Environment: ${TF_VAR_environment}"
echo " Resource Group: ${TF_VAR_resource_group_name}"
echo " Storage Account: ${TF_VAR_storage_account_name}"
echo " Key Vault: ${TF_VAR_key_vault_name}"
echo ""
echo "All Terraform variables are set and ready for deployment."

View File

@@ -0,0 +1,109 @@
#!/bin/bash
# Deploy Cloud for Sovereignty Landing Zone
# Uses Well-Architected Framework principles
# Deploys across all non-US commercial Azure regions
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
TERRAFORM_DIR="$PROJECT_ROOT/infra/terraform"
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ CLOUD FOR SOVEREIGNTY LANDING ZONE DEPLOYMENT ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
# Load environment
source "$SCRIPT_DIR/azure-load-env.sh"
# Get management group ID from .env or use default
MANAGEMENT_GROUP_ID="${AZURE_MANAGEMENT_GROUP_ID:-SOVEREIGN-ORDER-OF-HOSPITALLERS}"
ENVIRONMENT="${TF_VAR_environment:-dev}"
echo "Configuration:"
echo " Management Group: $MANAGEMENT_GROUP_ID"
echo " Environment: $ENVIRONMENT"
echo " Subscription: ${ARM_SUBSCRIPTION_ID:0:8}..."
echo ""
# Confirm deployment
read -p "Deploy landing zone to all non-US commercial regions? (yes/no): " -r
if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then
echo "Deployment cancelled."
exit 0
fi
cd "$TERRAFORM_DIR"
# Step 1: Deploy Management Group Hierarchy
echo ""
echo "Step 1: Deploying Management Group Hierarchy..."
cd management-groups
terraform init
terraform plan -var="management_group_id=$MANAGEMENT_GROUP_ID" -out=tfplan
read -p "Apply management group changes? (yes/no): " -r
if [[ $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then
terraform apply tfplan
fi
cd ..
# Step 2: Deploy Policies
echo ""
echo "Step 2: Deploying Sovereignty Policies..."
cd policies
terraform init
terraform plan -var="management_group_id=$MANAGEMENT_GROUP_ID" -out=tfplan
read -p "Apply policy changes? (yes/no): " -r
if [[ $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then
terraform apply tfplan
fi
cd ..
# Step 3: Deploy Multi-Region Landing Zones
echo ""
echo "Step 3: Deploying Multi-Region Landing Zones..."
cd multi-region
terraform init
terraform plan \
-var="environment=$ENVIRONMENT" \
-var="management_group_id=$MANAGEMENT_GROUP_ID" \
-var="deploy_all_regions=true" \
-out=tfplan
echo ""
echo "This will deploy landing zones to:"
echo " • West Europe (Netherlands) - Primary"
echo " • North Europe (Ireland) - Secondary"
echo " • UK South (London)"
echo " • Switzerland North (Zurich)"
echo " • Norway East (Oslo)"
echo " • France Central (Paris)"
echo " • Germany West Central (Frankfurt)"
echo ""
read -p "Apply multi-region deployment? (yes/no): " -r
if [[ $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then
terraform apply tfplan
echo ""
echo "✅ Multi-region landing zone deployment complete!"
echo ""
echo "Deployment outputs:"
terraform output
fi
cd "$PROJECT_ROOT"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ DEPLOYMENT COMPLETE ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
echo "Next steps:"
echo " 1. Review deployed resources in Azure Portal"
echo " 2. Configure application workloads"
echo " 3. Set up monitoring and alerting"
echo " 4. Review compliance status in Azure Policy"
echo ""

101
infra/terraform/aks.tf Normal file
View File

@@ -0,0 +1,101 @@
# Azure Kubernetes Service (AKS) Configuration
variable "aks_cluster_name" {
description = "Name of the AKS cluster"
type = string
default = ""
}
variable "aks_node_count" {
description = "Number of nodes in the AKS cluster"
type = number
default = 2
}
variable "aks_vm_size" {
description = "VM size for AKS nodes"
type = string
default = "Standard_B2s"
}
resource "azurerm_kubernetes_cluster" "main" {
name = var.aks_cluster_name != "" ? var.aks_cluster_name : "the-order-aks-${var.environment}"
location = var.azure_region
resource_group_name = azurerm_resource_group.main.name
dns_prefix = "the-order-${var.environment}"
kubernetes_version = "1.28" # Update to latest stable
# Use subscription_id from variable if provided
# This ensures proper Azure authentication
default_node_pool {
name = "default"
node_count = var.aks_node_count
vm_size = var.aks_vm_size
type = "VirtualMachineScaleSets"
enable_auto_scaling = var.environment != "dev"
min_count = var.environment != "dev" ? 2 : null
max_count = var.environment != "dev" ? 10 : null
os_disk_size_gb = 30
}
identity {
type = "SystemAssigned"
}
# Enable Azure RBAC
azure_active_directory_role_based_access_control {
managed = true
azure_rbac_enabled = true
admin_group_object_ids = [] # Add admin group IDs
}
# Network profile
network_profile {
network_plugin = "azure"
network_policy = "azure"
load_balancer_sku = "standard"
}
# Enable monitoring
oms_agent {
log_analytics_workspace_id = azurerm_log_analytics_workspace.main[0].id
}
tags = var.tags
}
# Log Analytics Workspace for AKS monitoring
resource "azurerm_log_analytics_workspace" "main" {
count = var.create_aks_cluster ? 1 : 0
name = "the-order-logs-${var.environment}"
location = var.azure_region
resource_group_name = azurerm_resource_group.main.name
sku = "PerGB2018"
retention_in_days = var.environment == "prod" ? 90 : 30
tags = var.tags
}
# Output AKS details
output "aks_cluster_name" {
value = var.create_aks_cluster ? azurerm_kubernetes_cluster.main[0].name : null
description = "Name of the AKS cluster"
}
output "aks_fqdn" {
value = var.create_aks_cluster ? azurerm_kubernetes_cluster.main[0].fqdn : null
description = "FQDN of the AKS cluster"
}
output "aks_kube_config" {
value = var.create_aks_cluster ? azurerm_kubernetes_cluster.main[0].kube_config_raw : null
description = "Raw Kubernetes config"
sensitive = true
}
output "log_analytics_workspace_id" {
value = azurerm_log_analytics_workspace.main[0].workspace_id
description = "Log Analytics Workspace ID"
}

View File

@@ -0,0 +1,49 @@
# Azure Provider Configuration
# Uses environment variables or terraform.tfvars for authentication
terraform {
required_version = ">= 1.5.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
# Backend configuration (uncomment and configure for remote state)
# backend "azurerm" {
# resource_group_name = "the-order-tfstate-rg"
# storage_account_name = "theordertfstate"
# container_name = "tfstate"
# key = "terraform.tfstate"
# }
}
# Configure the Azure Provider
provider "azurerm" {
features {
resource_group {
prevent_deletion_if_contains_resources = false
}
key_vault {
purge_soft_delete_on_destroy = true
}
storage {
purge_soft_delete_on_destroy = true
}
}
# Use environment variables or terraform.tfvars
# subscription_id = var.subscription_id
# tenant_id = var.tenant_id
# client_id = var.client_id
# client_secret = var.client_secret
}
# Data source for current subscription
data "azurerm_subscription" "current" {}
# Data source for current client config
data "azurerm_client_config" "current" {}

View File

@@ -26,7 +26,7 @@ resource "azurerm_storage_account" "cdn_images" {
}
}
tags = merge(local.common_tags, {
tags = merge(var.tags, {
Purpose = "CDNImages"
})
}
@@ -40,19 +40,19 @@ resource "azurerm_storage_container" "cdn_images" {
# CDN Profile
resource "azurerm_cdn_profile" "cdn_images" {
name = "${local.project_prefix}-cdn-profile"
name = var.cdn_profile_name != "" ? var.cdn_profile_name : "theorder-cdn-${var.environment}"
location = var.azure_region
resource_group_name = azurerm_resource_group.main.name
sku = "Standard_Microsoft"
tags = merge(local.common_tags, {
tags = merge(var.tags, {
Purpose = "CDNProfile"
})
}
# CDN Endpoint
resource "azurerm_cdn_endpoint" "cdn_images" {
name = "${local.project_prefix}-cdn-endpoint"
name = var.cdn_endpoint_name != "" ? var.cdn_endpoint_name : "theorder-cdn-endpoint-${var.environment}"
profile_name = azurerm_cdn_profile.cdn_images.name
location = var.azure_region
resource_group_name = azurerm_resource_group.main.name
@@ -74,7 +74,7 @@ resource "azurerm_cdn_endpoint" "cdn_images" {
}
}
tags = merge(local.common_tags, {
tags = merge(var.tags, {
Purpose = "CDNEndpoint"
})
}

119
infra/terraform/database.tf Normal file
View File

@@ -0,0 +1,119 @@
# Azure Database for PostgreSQL
# Flexible Server for production workloads
variable "database_name" {
description = "PostgreSQL database name"
type = string
default = ""
}
variable "database_admin_user" {
description = "PostgreSQL admin username"
type = string
default = "theorder_admin"
}
variable "database_sku_name" {
description = "PostgreSQL SKU (e.g., Standard_B1ms, Standard_B2s)"
type = string
default = "Standard_B1ms"
}
variable "database_storage_mb" {
description = "PostgreSQL storage in MB"
type = number
default = 32768 # 32 GB
}
resource "azurerm_postgresql_flexible_server" "main" {
name = var.database_name != "" ? var.database_name : "the-order-db-${var.environment}"
resource_group_name = azurerm_resource_group.main.name
location = var.azure_region
version = "15"
delegated_subnet_id = null # Set if using VNet integration
private_dns_zone_id = null # Set if using private DNS
administrator_login = var.database_admin_user
administrator_password = null # Set via Key Vault secret
zone = "1"
storage_mb = var.database_storage_mb
sku_name = var.database_sku_name
backup {
geo_redundant_backup_enabled = var.environment == "prod"
backup_retention_days = var.environment == "prod" ? 35 : 7
}
high_availability {
mode = var.environment == "prod" ? "ZoneRedundant" : "Disabled"
standby_availability_zone = var.environment == "prod" ? "2" : null
}
maintenance_window {
day_of_week = 0 # Sunday
start_hour = 2
start_minute = 0
}
tags = merge(var.tags, {
Purpose = "Database"
})
}
# Database
resource "azurerm_postgresql_flexible_server_database" "main" {
count = var.create_database ? 1 : 0
name = "theorder_${var.environment}"
server_id = azurerm_postgresql_flexible_server.main[0].id
charset = "UTF8"
collation = "en_US.utf8"
}
# Firewall rules - allow Azure services
resource "azurerm_postgresql_flexible_server_firewall_rule" "azure_services" {
count = var.create_database ? 1 : 0
name = "AllowAzureServices"
server_id = azurerm_postgresql_flexible_server.main[0].id
start_ip_address = "0.0.0.0"
end_ip_address = "0.0.0.0"
}
# Generate random password for database
resource "random_password" "database_password" {
count = var.create_database ? 1 : 0
length = 32
special = true
}
# Store database connection string in Key Vault
resource "azurerm_key_vault_secret" "database_url" {
count = var.create_database ? 1 : 0
name = "database-url"
value = "postgresql://${var.database_admin_user}:${random_password.database_password[0].result}@${azurerm_postgresql_flexible_server.main[0].fqdn}:5432/${azurerm_postgresql_flexible_server_database.main[0].name}?sslmode=require"
key_vault_id = azurerm_key_vault.main.id
tags = var.tags
}
# Store password in Key Vault
resource "azurerm_key_vault_secret" "database_password" {
count = var.create_database ? 1 : 0
name = "database-password"
value = random_password.database_password[0].result
key_vault_id = azurerm_key_vault.main.id
tags = var.tags
}
# Outputs
output "database_fqdn" {
value = var.create_database ? azurerm_postgresql_flexible_server.main[0].fqdn : null
description = "Fully qualified domain name of the database server"
sensitive = true
}
output "database_name" {
value = var.create_database ? azurerm_postgresql_flexible_server_database.main[0].name : null
description = "Name of the database"
}

View File

@@ -0,0 +1,57 @@
# Azure Key Vault for secrets management
resource "azurerm_key_vault" "main" {
name = var.key_vault_name != "" ? var.key_vault_name : "the-order-kv-${var.environment}"
location = var.azure_region
resource_group_name = azurerm_resource_group.main.name
tenant_id = var.tenant_id != "" ? var.tenant_id : data.azurerm_client_config.current.tenant_id
sku_name = "standard"
# Network ACLs
network_acls {
default_action = "Deny"
bypass = "AzureServices"
ip_rules = [] # Add allowed IPs for access
}
# Enable soft delete and purge protection
soft_delete_retention_days = 7
purge_protection_enabled = var.environment == "prod"
tags = merge(var.tags, {
Purpose = "SecretsManagement"
})
}
# Grant current user/service principal access
resource "azurerm_key_vault_access_policy" "current_user" {
key_vault_id = azurerm_key_vault.main.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = data.azurerm_client_config.current.object_id
key_permissions = [
"Get", "List", "Create", "Delete", "Update", "Import", "Backup", "Restore"
]
secret_permissions = [
"Get", "List", "Set", "Delete", "Recover", "Backup", "Restore", "Purge"
]
certificate_permissions = [
"Get", "List", "Create", "Delete", "Update", "Import", "Backup", "Restore"
]
}
# Output Key Vault details
output "key_vault_name" {
value = azurerm_key_vault.main.name
description = "Name of the Key Vault"
}
output "key_vault_uri" {
value = azurerm_key_vault.main.vault_uri
description = "URI of the Key Vault"
sensitive = true
}

View File

@@ -0,0 +1,137 @@
# Management Group Hierarchy for Cloud for Sovereignty
# Root: SOVEREIGN-ORDER-OF-HOSPITALLERS
variable "management_group_id" {
description = "Root management group ID"
type = string
default = "SOVEREIGN-ORDER-OF-HOSPITALLERS"
}
# Configure Azure Provider
provider "azurerm" {
features {}
}
# Data source for existing root management group
data "azurerm_management_group" "root" {
name = var.management_group_id
}
# Landing Zones Management Group
resource "azurerm_management_group" "landing_zones" {
name = "LandingZones"
display_name = "Landing Zones"
parent_management_group_id = data.azurerm_management_group.root.id
subscription_ids = []
}
# Platform Landing Zone
resource "azurerm_management_group" "platform" {
name = "Platform"
display_name = "Platform Landing Zone"
parent_management_group_id = azurerm_management_group.landing_zones.id
subscription_ids = []
}
# Sandbox Landing Zone
resource "azurerm_management_group" "sandbox" {
name = "Sandbox"
display_name = "Sandbox Landing Zone"
parent_management_group_id = azurerm_management_group.landing_zones.id
subscription_ids = []
}
# Workloads Landing Zone
resource "azurerm_management_group" "workloads" {
name = "Workloads"
display_name = "Workload Workloads"
parent_management_group_id = azurerm_management_group.landing_zones.id
subscription_ids = []
}
# Management Management Group
resource "azurerm_management_group" "management" {
name = "Management"
display_name = "Management"
parent_management_group_id = data.azurerm_management_group.root.id
subscription_ids = []
}
# Identity Management Group
resource "azurerm_management_group" "identity" {
name = "Identity"
display_name = "Identity and Access Management"
parent_management_group_id = azurerm_management_group.management.id
subscription_ids = []
}
# Security Management Group
resource "azurerm_management_group" "security" {
name = "Security"
display_name = "Security Operations"
parent_management_group_id = azurerm_management_group.management.id
subscription_ids = []
}
# Monitoring Management Group
resource "azurerm_management_group" "monitoring" {
name = "Monitoring"
display_name = "Centralized Monitoring"
parent_management_group_id = azurerm_management_group.management.id
subscription_ids = []
}
# Connectivity Management Group
resource "azurerm_management_group" "connectivity" {
name = "Connectivity"
display_name = "Connectivity"
parent_management_group_id = data.azurerm_management_group.root.id
subscription_ids = []
}
# Hub Networks Management Group
resource "azurerm_management_group" "hub_networks" {
name = "HubNetworks"
display_name = "Hub Networks"
parent_management_group_id = azurerm_management_group.connectivity.id
subscription_ids = []
}
# Spoke Networks Management Group
resource "azurerm_management_group" "spoke_networks" {
name = "SpokeNetworks"
display_name = "Spoke Networks"
parent_management_group_id = azurerm_management_group.connectivity.id
subscription_ids = []
}
# Outputs
output "management_group_hierarchy" {
description = "Management group hierarchy"
value = {
root = data.azurerm_management_group.root.id
landing_zones = azurerm_management_group.landing_zones.id
platform = azurerm_management_group.platform.id
sandbox = azurerm_management_group.sandbox.id
workloads = azurerm_management_group.workloads.id
management = azurerm_management_group.management.id
identity = azurerm_management_group.identity.id
security = azurerm_management_group.security.id
monitoring = azurerm_management_group.monitoring.id
connectivity = azurerm_management_group.connectivity.id
hub_networks = azurerm_management_group.hub_networks.id
spoke_networks = azurerm_management_group.spoke_networks.id
}
}

View File

@@ -0,0 +1,8 @@
# Variables for Management Groups
variable "management_group_id" {
description = "Root management group ID"
type = string
default = "SOVEREIGN-ORDER-OF-HOSPITALLERS"
}

View File

@@ -0,0 +1,13 @@
# Terraform and Provider Version Constraints for Management Groups
terraform {
required_version = ">= 1.5.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}

View File

@@ -0,0 +1,64 @@
# Regional Landing Zone Module
Reusable Terraform module for deploying a complete landing zone in a single Azure region, following Cloud for Sovereignty and Well-Architected Framework principles.
## Features
- **Hub-and-Spoke Network Architecture**
- Hub VNet with gateway, firewall, and management subnets
- Spoke VNet with application, database, and storage subnets
- VNet peering between hub and spoke
- **Security**
- Azure Firewall for centralized security
- Private endpoints for Key Vault and Storage
- Network security groups
- **Compliance**
- Customer-managed encryption
- Data residency tags
- Private endpoints for data sovereignty
- **Monitoring**
- Regional Log Analytics Workspace
- Application Insights ready
## Usage
```hcl
module "west_europe_landing_zone" {
source = "../../modules/regional-landing-zone"
region = "westeurope"
environment = "dev"
management_group_id = "SOVEREIGN-ORDER-OF-HOSPITALLERS"
hub_vnet_address_space = "10.0.0.0/16"
spoke_vnet_address_space = "10.1.0.0/16"
tags = {
Project = "the-order"
CostCenter = "engineering"
}
}
```
## Variables
- `region` (required): Azure region (must be non-US commercial)
- `environment` (required): dev, stage, or prod
- `management_group_id` (required): Management group ID
- `hub_vnet_address_space` (optional): Hub VNet CIDR (default: 10.0.0.0/16)
- `spoke_vnet_address_space` (optional): Spoke VNet CIDR (default: 10.1.0.0/16)
- `tags` (optional): Additional tags
## Outputs
- `resource_group_name`: Resource group name
- `hub_vnet_id`: Hub VNet ID
- `spoke_vnet_id`: Spoke VNet ID
- `firewall_id`: Azure Firewall ID
- `key_vault_id`: Key Vault ID
- `log_analytics_workspace_id`: Log Analytics Workspace ID
- `storage_account_name`: Storage account name
- `subnet_ids`: Map of subnet names to IDs

View File

@@ -0,0 +1,342 @@
# Regional Landing Zone Module
# Deploys a complete landing zone for a single Azure region
# Follows Cloud for Sovereignty and Well-Architected Framework principles
variable "region" {
description = "Azure region (e.g., westeurope, northeurope)"
type = string
validation {
condition = contains([
"westeurope",
"northeurope",
"uksouth",
"switzerlandnorth",
"norwayeast",
"francecentral",
"germanywestcentral"
], var.region)
error_message = "Region must be a non-US commercial Azure region."
}
}
variable "environment" {
description = "Environment name (dev, stage, prod)"
type = string
validation {
condition = contains(["dev", "stage", "prod"], var.environment)
error_message = "Environment must be dev, stage, or prod."
}
}
variable "management_group_id" {
description = "Management group ID for this landing zone"
type = string
}
variable "hub_vnet_address_space" {
description = "Address space for hub VNet"
type = string
default = "10.0.0.0/16"
}
variable "spoke_vnet_address_space" {
description = "Address space for spoke VNet"
type = string
default = "10.1.0.0/16"
}
variable "tags" {
description = "Tags to apply to all resources"
type = map(string)
default = {}
}
# Local values for naming
locals {
region_abbrev = {
westeurope = "we"
northeurope = "ne"
uksouth = "uk"
switzerlandnorth = "ch"
norwayeast = "no"
francecentral = "fr"
germanywestcentral = "de"
}
env_abbrev = {
dev = "dev"
stage = "stg"
prod = "prd"
}
region_short = lookup(local.region_abbrev, var.region, "we")
env_short = lookup(local.env_abbrev, var.environment, "dev")
name_prefix = "az-${local.region_short}"
common_tags = merge(var.tags, {
Region = var.region
Environment = var.environment
DataResidency = var.region
ManagedBy = "terraform"
LandingZone = "regional"
SovereigntyLevel = "high"
})
}
# Resource Group
resource "azurerm_resource_group" "landing_zone" {
name = "${local.name_prefix}-rg-${local.env_short}-lz"
location = var.region
tags = local.common_tags
}
# Hub Virtual Network
resource "azurerm_virtual_network" "hub" {
name = "${local.name_prefix}-vnet-${local.env_short}-hub"
address_space = [var.hub_vnet_address_space]
location = var.region
resource_group_name = azurerm_resource_group.landing_zone.name
tags = merge(local.common_tags, {
Purpose = "HubNetwork"
})
}
# Hub Subnets
resource "azurerm_subnet" "hub_gateway" {
name = "GatewaySubnet"
resource_group_name = azurerm_resource_group.landing_zone.name
virtual_network_name = azurerm_virtual_network.hub.name
address_prefixes = [cidrsubnet(var.hub_vnet_address_space, 8, 0)]
}
resource "azurerm_subnet" "hub_firewall" {
name = "AzureFirewallSubnet"
resource_group_name = azurerm_resource_group.landing_zone.name
virtual_network_name = azurerm_virtual_network.hub.name
address_prefixes = [cidrsubnet(var.hub_vnet_address_space, 8, 1)]
}
resource "azurerm_subnet" "hub_management" {
name = "ManagementSubnet"
resource_group_name = azurerm_resource_group.landing_zone.name
virtual_network_name = azurerm_virtual_network.hub.name
address_prefixes = [cidrsubnet(var.hub_vnet_address_space, 8, 2)]
}
# Azure Firewall
resource "azurerm_public_ip" "firewall" {
name = "${local.name_prefix}-pip-${local.env_short}-fw"
location = var.region
resource_group_name = azurerm_resource_group.landing_zone.name
allocation_method = "Static"
sku = "Standard"
tags = local.common_tags
}
resource "azurerm_firewall" "hub" {
name = "${local.name_prefix}-fw-${local.env_short}-hub"
location = var.region
resource_group_name = azurerm_resource_group.landing_zone.name
sku_name = "AZFW_VNet"
sku_tier = "Standard"
ip_configuration {
name = "configuration"
subnet_id = azurerm_subnet.hub_firewall.id
public_ip_address_id = azurerm_public_ip.firewall.id
}
tags = local.common_tags
}
# Spoke Virtual Network
resource "azurerm_virtual_network" "spoke" {
name = "${local.name_prefix}-vnet-${local.env_short}-spoke"
address_space = [var.spoke_vnet_address_space]
location = var.region
resource_group_name = azurerm_resource_group.landing_zone.name
tags = merge(local.common_tags, {
Purpose = "SpokeNetwork"
})
}
# Spoke Subnets
resource "azurerm_subnet" "spoke_app" {
name = "ApplicationSubnet"
resource_group_name = azurerm_resource_group.landing_zone.name
virtual_network_name = azurerm_virtual_network.spoke.name
address_prefixes = [cidrsubnet(var.spoke_vnet_address_space, 8, 0)]
}
resource "azurerm_subnet" "spoke_db" {
name = "DatabaseSubnet"
resource_group_name = azurerm_resource_group.landing_zone.name
virtual_network_name = azurerm_virtual_network.spoke.name
address_prefixes = [cidrsubnet(var.spoke_vnet_address_space, 8, 1)]
delegation {
name = "postgresql-delegation"
service_delegation {
name = "Microsoft.DBforPostgreSQL/flexibleServers"
actions = [
"Microsoft.Network/virtualNetworks/subnets/join/action",
]
}
}
}
resource "azurerm_subnet" "spoke_storage" {
name = "StorageSubnet"
resource_group_name = azurerm_resource_group.landing_zone.name
virtual_network_name = azurerm_virtual_network.spoke.name
address_prefixes = [cidrsubnet(var.spoke_vnet_address_space, 8, 2)]
}
# VNet Peering: Hub to Spoke
resource "azurerm_virtual_network_peering" "hub_to_spoke" {
name = "hub-to-spoke"
resource_group_name = azurerm_resource_group.landing_zone.name
virtual_network_name = azurerm_virtual_network.hub.name
remote_virtual_network_id = azurerm_virtual_network.spoke.id
allow_forwarded_traffic = true
allow_gateway_transit = true
}
resource "azurerm_virtual_network_peering" "spoke_to_hub" {
name = "spoke-to-hub"
resource_group_name = azurerm_resource_group.landing_zone.name
virtual_network_name = azurerm_virtual_network.spoke.name
remote_virtual_network_id = azurerm_virtual_network.hub.id
allow_forwarded_traffic = true
use_remote_gateways = false
}
# Data source for current client config
data "azurerm_client_config" "current" {}
# Key Vault (Regional)
resource "azurerm_key_vault" "regional" {
name = "${local.name_prefix}-kv-${local.env_short}-${local.region_short}"
location = var.region
resource_group_name = azurerm_resource_group.landing_zone.name
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "premium"
# Network ACLs - Private endpoint only
network_acls {
default_action = "Deny"
bypass = "AzureServices"
}
# Enable soft delete and purge protection
soft_delete_retention_days = 90
purge_protection_enabled = var.environment == "prod"
tags = merge(local.common_tags, {
Purpose = "RegionalSecrets"
})
}
# Private Endpoint for Key Vault
resource "azurerm_private_endpoint" "key_vault" {
name = "${local.name_prefix}-pe-${local.env_short}-kv"
location = var.region
resource_group_name = azurerm_resource_group.landing_zone.name
subnet_id = azurerm_subnet.hub_management.id
private_service_connection {
name = "kv-connection"
private_connection_resource_id = azurerm_key_vault.regional.id
subresource_names = ["vault"]
is_manual_connection = false
}
tags = local.common_tags
}
# Log Analytics Workspace (Regional)
resource "azurerm_log_analytics_workspace" "regional" {
name = "${local.name_prefix}-law-${local.env_short}-${local.region_short}"
location = var.region
resource_group_name = azurerm_resource_group.landing_zone.name
sku = "PerGB2018"
retention_in_days = var.environment == "prod" ? 90 : 30
tags = merge(local.common_tags, {
Purpose = "RegionalLogging"
})
}
# Storage Account (Regional)
resource "azurerm_storage_account" "regional" {
name = "${local.name_prefix}sa${local.env_short}${local.region_short}"
resource_group_name = azurerm_resource_group.landing_zone.name
location = var.region
account_tier = "Standard"
account_replication_type = var.environment == "prod" ? "GRS" : "LRS"
min_tls_version = "TLS1_2"
allow_blob_public_access = false
# Customer-managed encryption
identity {
type = "SystemAssigned"
}
blob_properties {
versioning_enabled = true
delete_retention_policy {
days = var.environment == "prod" ? 90 : 30
}
}
tags = merge(local.common_tags, {
Purpose = "RegionalStorage"
})
}
# Private Endpoint for Storage Account
resource "azurerm_private_endpoint" "storage" {
name = "${local.name_prefix}-pe-${local.env_short}-st"
location = var.region
resource_group_name = azurerm_resource_group.landing_zone.name
subnet_id = azurerm_subnet.spoke_storage.id
private_service_connection {
name = "storage-connection"
private_connection_resource_id = azurerm_storage_account.regional.id
subresource_names = ["blob"]
is_manual_connection = false
}
tags = local.common_tags
}
# Outputs
output "resource_group_name" {
value = azurerm_resource_group.landing_zone.name
}
output "hub_vnet_id" {
value = azurerm_virtual_network.hub.id
}
output "spoke_vnet_id" {
value = azurerm_virtual_network.spoke.id
}
output "key_vault_id" {
value = azurerm_key_vault.regional.id
}
output "log_analytics_workspace_id" {
value = azurerm_log_analytics_workspace.regional.workspace_id
}
output "storage_account_name" {
value = azurerm_storage_account.regional.name
}

View File

@@ -0,0 +1,94 @@
# Outputs for Regional Landing Zone Module
output "resource_group_name" {
description = "Name of the resource group"
value = azurerm_resource_group.landing_zone.name
}
output "resource_group_id" {
description = "ID of the resource group"
value = azurerm_resource_group.landing_zone.id
}
output "hub_vnet_id" {
description = "ID of the hub virtual network"
value = azurerm_virtual_network.hub.id
}
output "hub_vnet_name" {
description = "Name of the hub virtual network"
value = azurerm_virtual_network.hub.name
}
output "spoke_vnet_id" {
description = "ID of the spoke virtual network"
value = azurerm_virtual_network.spoke.id
}
output "spoke_vnet_name" {
description = "Name of the spoke virtual network"
value = azurerm_virtual_network.spoke.name
}
output "firewall_id" {
description = "ID of the Azure Firewall"
value = azurerm_firewall.hub.id
}
output "firewall_private_ip" {
description = "Private IP address of the Azure Firewall"
value = azurerm_firewall.hub.ip_configuration[0].private_ip_address
}
output "key_vault_id" {
description = "ID of the Key Vault"
value = azurerm_key_vault.regional.id
}
output "key_vault_name" {
description = "Name of the Key Vault"
value = azurerm_key_vault.regional.name
}
output "key_vault_uri" {
description = "URI of the Key Vault"
value = azurerm_key_vault.regional.vault_uri
}
output "log_analytics_workspace_id" {
description = "ID of the Log Analytics Workspace"
value = azurerm_log_analytics_workspace.regional.workspace_id
}
output "log_analytics_workspace_name" {
description = "Name of the Log Analytics Workspace"
value = azurerm_log_analytics_workspace.regional.name
}
output "storage_account_name" {
description = "Name of the storage account"
value = azurerm_storage_account.regional.name
}
output "storage_account_id" {
description = "ID of the storage account"
value = azurerm_storage_account.regional.id
}
output "storage_account_primary_endpoint" {
description = "Primary endpoint of the storage account"
value = azurerm_storage_account.regional.primary_blob_endpoint
}
output "subnet_ids" {
description = "Map of subnet names to IDs"
value = {
hub_gateway = azurerm_subnet.hub_gateway.id
hub_firewall = azurerm_subnet.hub_firewall.id
hub_management = azurerm_subnet.hub_management.id
spoke_app = azurerm_subnet.spoke_app.id
spoke_db = azurerm_subnet.spoke_db.id
spoke_storage = azurerm_subnet.spoke_storage.id
}
}

View File

@@ -0,0 +1,51 @@
# Variables for Regional Landing Zone Module
variable "region" {
description = "Azure region (e.g., westeurope, northeurope)"
type = string
validation {
condition = contains([
"westeurope",
"northeurope",
"uksouth",
"switzerlandnorth",
"norwayeast",
"francecentral",
"germanywestcentral"
], var.region)
error_message = "Region must be a non-US commercial Azure region."
}
}
variable "environment" {
description = "Environment name (dev, stage, prod)"
type = string
validation {
condition = contains(["dev", "stage", "prod"], var.environment)
error_message = "Environment must be dev, stage, or prod."
}
}
variable "management_group_id" {
description = "Management group ID for this landing zone"
type = string
}
variable "hub_vnet_address_space" {
description = "Address space for hub VNet"
type = string
default = "10.0.0.0/16"
}
variable "spoke_vnet_address_space" {
description = "Address space for spoke VNet"
type = string
default = "10.1.0.0/16"
}
variable "tags" {
description = "Tags to apply to all resources"
type = map(string)
default = {}
}

View File

@@ -0,0 +1,16 @@
# Terraform and Provider Version Constraints for Regional Landing Zone Module
terraform {
required_version = ">= 1.5.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}
# Configure Azure Provider (inherited from parent)
# Provider configuration should be set in the calling module

View File

@@ -0,0 +1,60 @@
# Multi-Region Landing Zone Deployment
Deploys Cloud for Sovereignty landing zones across all non-US commercial Azure regions.
## Supported Regions
1. **West Europe** (Netherlands) - Primary region
2. **North Europe** (Ireland) - Secondary region
3. **UK South** (London) - UK-specific workloads
4. **Switzerland North** (Zurich) - Swiss-specific workloads
5. **Norway East** (Oslo) - Nordic-specific workloads
6. **France Central** (Paris) - French-specific workloads
7. **Germany West Central** (Frankfurt) - German-specific workloads
## Architecture
Each region includes:
- Hub Virtual Network (gateway, firewall, management)
- Spoke Virtual Network (application, database, storage)
- Azure Firewall (centralized security)
- Key Vault (regional secrets with private endpoints)
- Log Analytics Workspace (regional logging)
- Storage Account (regional storage with private endpoints)
## Usage
### Deploy All Regions
```bash
cd infra/terraform/multi-region
terraform init
terraform plan -var="environment=dev" -var="management_group_id=SOVEREIGN-ORDER-OF-HOSPITALLERS"
terraform apply
```
### Deploy Specific Regions
```bash
terraform plan \
-var="environment=dev" \
-var="deploy_all_regions=false" \
-var='regions_to_deploy=["westeurope", "northeurope"]'
terraform apply
```
## Variables
- `environment`: dev, stage, or prod
- `management_group_id`: Root management group ID
- `deploy_all_regions`: Deploy to all supported regions (default: true)
- `regions_to_deploy`: Specific regions if deploy_all_regions is false
## Outputs
- `deployed_regions`: List of deployed regions
- `regional_resource_groups`: Resource group names per region
- `regional_key_vaults`: Key Vault names per region
- `regional_storage_accounts`: Storage account names per region
- `deployment_summary`: Complete deployment summary

View File

@@ -0,0 +1,120 @@
# Multi-Region Landing Zone Deployment
# Deploys landing zones across all non-US commercial Azure regions
# Uses the regional-landing-zone module
variable "environment" {
description = "Environment name (dev, stage, prod)"
type = string
default = "dev"
}
variable "management_group_id" {
description = "Root management group ID"
type = string
default = "SOVEREIGN-ORDER-OF-HOSPITALLERS"
}
variable "deploy_all_regions" {
description = "Deploy to all supported regions"
type = bool
default = true
}
variable "regions_to_deploy" {
description = "Specific regions to deploy (if deploy_all_regions is false)"
type = list(string)
default = []
}
# Supported non-US commercial regions
locals {
supported_regions = [
"westeurope", # Netherlands - Primary
"northeurope", # Ireland - Secondary
"uksouth", # London - UK workloads
"switzerlandnorth", # Zurich - Swiss workloads
"norwayeast", # Oslo - Nordic workloads
"francecentral", # Paris - French workloads
"germanywestcentral" # Frankfurt - German workloads
]
regions = var.deploy_all_regions ? local.supported_regions : var.regions_to_deploy
# Hub VNet address spaces per region
hub_address_spaces = {
westeurope = "10.0.0.0/16"
northeurope = "10.10.0.0/16"
uksouth = "10.20.0.0/16"
switzerlandnorth = "10.30.0.0/16"
norwayeast = "10.40.0.0/16"
francecentral = "10.50.0.0/16"
germanywestcentral = "10.60.0.0/16"
}
# Spoke VNet address spaces per region
spoke_address_spaces = {
westeurope = "10.1.0.0/16"
northeurope = "10.11.0.0/16"
uksouth = "10.21.0.0/16"
switzerlandnorth = "10.31.0.0/16"
norwayeast = "10.41.0.0/16"
francecentral = "10.51.0.0/16"
germanywestcentral = "10.61.0.0/16"
}
common_tags = {
Environment = var.environment
Project = "the-order"
ManagedBy = "terraform"
SovereigntyLevel = "high"
DataClassification = "confidential"
Compliance = "gdpr,eidas"
}
}
# Deploy regional landing zones
module "regional_landing_zones" {
source = "../modules/regional-landing-zone"
for_each = toset(local.regions)
region = each.value
environment = var.environment
management_group_id = var.management_group_id
hub_vnet_address_space = local.hub_address_spaces[each.value]
spoke_vnet_address_space = local.spoke_address_spaces[each.value]
tags = merge(local.common_tags, {
Region = each.value
})
}
# Outputs
output "deployed_regions" {
description = "List of deployed regions"
value = local.regions
}
output "regional_resource_groups" {
description = "Resource group names per region"
value = {
for region, module in module.regional_landing_zones :
region => module.resource_group_name
}
}
output "regional_key_vaults" {
description = "Key Vault IDs per region"
value = {
for region, module in module.regional_landing_zones :
region => module.key_vault_id
}
}
output "regional_storage_accounts" {
description = "Storage account names per region"
value = {
for region, module in module.regional_landing_zones :
region => module.storage_account_name
}
}

View File

@@ -0,0 +1,65 @@
# Outputs for Multi-Region Landing Zone Deployment
output "deployed_regions" {
description = "List of deployed regions"
value = local.regions
}
output "regional_resource_groups" {
description = "Resource group names per region"
value = {
for region, module in module.regional_landing_zones :
region => module.resource_group_name
}
}
output "regional_key_vaults" {
description = "Key Vault names per region"
value = {
for region, module in module.regional_landing_zones :
region => module.key_vault_name
}
}
output "regional_storage_accounts" {
description = "Storage account names per region"
value = {
for region, module in module.regional_landing_zones :
region => module.storage_account_name
}
}
output "regional_log_analytics_workspaces" {
description = "Log Analytics Workspace names per region"
value = {
for region, module in module.regional_landing_zones :
region => module.log_analytics_workspace_name
}
}
output "regional_hub_vnets" {
description = "Hub VNet names per region"
value = {
for region, module in module.regional_landing_zones :
region => module.hub_vnet_name
}
}
output "regional_spoke_vnets" {
description = "Spoke VNet names per region"
value = {
for region, module in module.regional_landing_zones :
region => module.spoke_vnet_name
}
}
output "deployment_summary" {
description = "Summary of deployment"
value = {
total_regions = length(local.regions)
regions = local.regions
environment = var.environment
management_group = var.management_group_id
}
}

View File

@@ -0,0 +1,44 @@
# Variables for Multi-Region Landing Zone Deployment
variable "environment" {
description = "Environment name (dev, stage, prod)"
type = string
default = "dev"
validation {
condition = contains(["dev", "stage", "prod"], var.environment)
error_message = "Environment must be dev, stage, or prod."
}
}
variable "management_group_id" {
description = "Root management group ID"
type = string
default = "SOVEREIGN-ORDER-OF-HOSPITALLERS"
}
variable "deploy_all_regions" {
description = "Deploy to all supported regions"
type = bool
default = true
}
variable "regions_to_deploy" {
description = "Specific regions to deploy (if deploy_all_regions is false)"
type = list(string)
default = []
validation {
condition = alltrue([
for region in var.regions_to_deploy : contains([
"westeurope",
"northeurope",
"uksouth",
"switzerlandnorth",
"norwayeast",
"francecentral",
"germanywestcentral"
], region)
])
error_message = "All regions must be non-US commercial Azure regions."
}
}

View File

@@ -0,0 +1,13 @@
# Terraform and Provider Version Constraints for Multi-Region Deployment
terraform {
required_version = ">= 1.5.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}

View File

@@ -0,0 +1,102 @@
# Azure-specific outputs for integration with Kubernetes and services
output "subscription_id" {
description = "Azure subscription ID"
value = data.azurerm_subscription.current.subscription_id
sensitive = true
}
output "tenant_id" {
description = "Azure tenant ID"
value = data.azurerm_client_config.current.tenant_id
sensitive = true
}
output "resource_group_name" {
description = "Main resource group name"
value = azurerm_resource_group.main.name
}
output "storage_account_name" {
description = "Application data storage account name"
value = azurerm_storage_account.app_data.name
}
output "storage_account_primary_key" {
description = "Storage account primary access key"
value = azurerm_storage_account.app_data.primary_access_key
sensitive = true
}
output "key_vault_name" {
description = "Key Vault name"
value = azurerm_key_vault.main.name
}
output "key_vault_uri" {
description = "Key Vault URI"
value = azurerm_key_vault.main.vault_uri
}
# CDN Outputs
output "cdn_storage_account_name" {
description = "CDN storage account name"
value = azurerm_storage_account.cdn_images.name
}
output "cdn_blob_url" {
description = "CDN blob storage URL"
value = "https://${azurerm_storage_account.cdn_images.name}.blob.core.windows.net/${azurerm_storage_container.cdn_images.name}/"
}
output "cdn_endpoint_url" {
description = "CDN endpoint URL"
value = "https://${azurerm_cdn_endpoint.cdn_images.host_name}/${azurerm_storage_container.cdn_images.name}/"
}
# Database Outputs
output "database_fqdn" {
description = "Database fully qualified domain name"
value = try(azurerm_postgresql_flexible_server.main[0].fqdn, null)
sensitive = true
}
output "database_name" {
description = "Database name"
value = try(azurerm_postgresql_flexible_server_database.main[0].name, null)
}
# AKS Outputs (if created)
output "aks_cluster_name" {
description = "AKS cluster name"
value = try(azurerm_kubernetes_cluster.main[0].name, null)
}
output "aks_fqdn" {
description = "AKS cluster FQDN"
value = try(azurerm_kubernetes_cluster.main[0].fqdn, null)
}
output "log_analytics_workspace_id" {
description = "Log Analytics Workspace ID"
value = try(azurerm_log_analytics_workspace.main[0].workspace_id, null)
}
# Export all outputs to environment variables file
output "env_file_snippet" {
description = "Environment variables snippet for .env file"
value = <<-EOT
# Azure Infrastructure Outputs (auto-generated)
AZURE_SUBSCRIPTION_ID="${data.azurerm_subscription.current.subscription_id}"
AZURE_TENANT_ID="${data.azurerm_client_config.current.tenant_id}"
AZURE_RESOURCE_GROUP="${azurerm_resource_group.main.name}"
AZURE_STORAGE_ACCOUNT="${azurerm_storage_account.app_data.name}"
AZURE_KEY_VAULT_NAME="${azurerm_key_vault.main.name}"
AZURE_KEY_VAULT_URI="${azurerm_key_vault.main.vault_uri}"
CDN_STORAGE_ACCOUNT="${azurerm_storage_account.cdn_images.name}"
CDN_BASE_URL_BLOB="https://${azurerm_storage_account.cdn_images.name}.blob.core.windows.net/${azurerm_storage_container.cdn_images.name}/"
CDN_BASE_URL_CDN="https://${azurerm_cdn_endpoint.cdn_images.host_name}/${azurerm_storage_container.cdn_images.name}/"
EOT
sensitive = true
}

View File

@@ -0,0 +1,254 @@
# Azure Policies for Cloud for Sovereignty
# Ensures compliance with data residency and sovereignty requirements
variable "management_group_id" {
description = "Root management group ID to assign policies"
type = string
default = "SOVEREIGN-ORDER-OF-HOSPITALLERS"
}
# Configure Azure Provider
provider "azurerm" {
features {}
}
# Policy: Allowed Locations (Non-US Commercial Regions Only)
resource "azurerm_policy_definition" "allowed_locations" {
name = "the-order-allowed-locations"
policy_type = "Custom"
mode = "All"
display_name = "The Order - Allowed Locations (Non-US Commercial)"
description = "Restricts resource deployment to non-US commercial Azure regions for data sovereignty"
metadata = jsonencode({
category = "Location"
})
policy_rule = jsonencode({
if = {
not = {
field = "location"
in = [
"westeurope",
"northeurope",
"uksouth",
"switzerlandnorth",
"norwayeast",
"francecentral",
"germanywestcentral"
]
}
}
then = {
effect = "Deny"
}
})
}
# Policy: Deny US Regions
resource "azurerm_policy_definition" "deny_us_regions" {
name = "the-order-deny-us-regions"
policy_type = "Custom"
mode = "All"
display_name = "The Order - Deny US Commercial and Government Regions"
description = "Explicitly denies deployment to any US Commercial or Government regions"
metadata = jsonencode({
category = "Location"
})
policy_rule = jsonencode({
if = {
field = "location"
like = "us*"
}
then = {
effect = "Deny"
}
})
}
# Policy: Require Data Residency Tags
resource "azurerm_policy_definition" "require_data_residency_tag" {
name = "the-order-require-data-residency-tag"
policy_type = "Custom"
mode = "Indexed"
display_name = "The Order - Require Data Residency Tag"
description = "Requires DataResidency tag on all resources for sovereignty tracking"
metadata = jsonencode({
category = "Tags"
})
policy_rule = jsonencode({
if = {
field = "[concat('tags[', parameters('tagName'), ']')]"
exists = "false"
}
then = {
effect = "Deny"
}
})
parameters = jsonencode({
tagName = {
type = "String"
metadata = {
displayName = "Tag Name"
description = "Name of the tag, such as 'DataResidency'"
}
defaultValue = "DataResidency"
}
})
}
# Policy: Require Encryption at Rest
resource "azurerm_policy_definition" "require_encryption_at_rest" {
name = "the-order-require-encryption-at-rest"
policy_type = "Custom"
mode = "All"
display_name = "The Order - Require Encryption at Rest"
description = "Ensures all storage accounts use encryption at rest with customer-managed keys"
metadata = jsonencode({
category = "Security"
})
policy_rule = jsonencode({
if = {
allOf = [
{
field = "type"
equals = "Microsoft.Storage/storageAccounts"
},
{
field = "Microsoft.Storage/storageAccounts/encryption.keySource"
notEquals = "Microsoft.Keyvault"
}
]
}
then = {
effect = "Deny"
}
})
}
# Policy: Require Resource Tags
resource "azurerm_policy_definition" "require_resource_tags" {
name = "the-order-require-resource-tags"
policy_type = "Custom"
mode = "Indexed"
display_name = "The Order - Require Resource Tags"
description = "Requires specific tags on all resources for governance and cost management"
metadata = jsonencode({
category = "Tags"
})
policy_rule = jsonencode({
if = {
anyOf = [
{
field = "[concat('tags[', parameters('tagName1'), ']')]"
exists = "false"
},
{
field = "[concat('tags[', parameters('tagName2'), ']')]"
exists = "false"
},
{
field = "[concat('tags[', parameters('tagName3'), ']')]"
exists = "false"
}
]
}
then = {
effect = "Deny"
}
})
parameters = jsonencode({
tagName1 = {
type = "String"
metadata = {
displayName = "Tag Name 1"
}
defaultValue = "Environment"
}
tagName2 = {
type = "String"
metadata = {
displayName = "Tag Name 2"
}
defaultValue = "Project"
}
tagName3 = {
type = "String"
metadata = {
displayName = "Tag Name 3"
}
defaultValue = "DataClassification"
}
})
}
# Policy Initiative: Cloud for Sovereignty Compliance
resource "azurerm_policy_set_definition" "sovereignty_compliance" {
name = "the-order-sovereignty-compliance"
policy_type = "Custom"
display_name = "The Order - Cloud for Sovereignty Compliance"
description = "Policy initiative ensuring compliance with Cloud for Sovereignty requirements"
metadata = jsonencode({
category = "Compliance"
})
policy_definition_reference {
policy_definition_id = azurerm_policy_definition.allowed_locations.id
}
policy_definition_reference {
policy_definition_id = azurerm_policy_definition.deny_us_regions.id
}
policy_definition_reference {
policy_definition_id = azurerm_policy_definition.require_data_residency_tag.id
}
policy_definition_reference {
policy_definition_id = azurerm_policy_definition.require_encryption_at_rest.id
}
policy_definition_reference {
policy_definition_id = azurerm_policy_definition.require_resource_tags.id
}
}
# Assign policy initiative to root management group
resource "azurerm_management_group_policy_assignment" "sovereignty_compliance" {
name = "sovereignty-compliance-assignment"
management_group_id = var.management_group_id
policy_definition_id = azurerm_policy_set_definition.sovereignty_compliance.id
identity {
type = "SystemAssigned"
}
}
# Outputs
output "policy_definitions" {
description = "Created policy definitions"
value = {
allowed_locations = azurerm_policy_definition.allowed_locations.id
deny_us_regions = azurerm_policy_definition.deny_us_regions.id
require_data_residency_tag = azurerm_policy_definition.require_data_residency_tag.id
require_encryption_at_rest = azurerm_policy_definition.require_encryption_at_rest.id
require_resource_tags = azurerm_policy_definition.require_resource_tags.id
}
}
output "policy_initiative" {
description = "Policy initiative ID"
value = azurerm_policy_set_definition.sovereignty_compliance.id
}

View File

@@ -0,0 +1,8 @@
# Variables for Sovereignty Policies
variable "management_group_id" {
description = "Root management group ID to assign policies"
type = string
default = "SOVEREIGN-ORDER-OF-HOSPITALLERS"
}

View File

@@ -0,0 +1,13 @@
# Terraform and Provider Version Constraints for Policies
terraform {
required_version = ">= 1.5.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}

View File

@@ -1,12 +1,12 @@
# Resource Groups for The Order
# Creates resource groups for each environment
# Naming: az-we-rg-dev-main (provider-region-resource-env-purpose)
# Naming: az-we-rg-dev-main (provider-region-resource-env-purpose) or custom from variable
resource "azurerm_resource_group" "main" {
name = local.rg_name
name = var.resource_group_name != "" ? var.resource_group_name : local.rg_name
location = var.azure_region
tags = merge(local.common_tags, {
tags = merge(var.tags, {
Purpose = "Main"
})
}

View File

@@ -32,9 +32,9 @@ resource "azurerm_storage_container" "terraform_state" {
}
# Storage Account for application data (object storage)
# Naming: azwesadevdata (provider+region+sa+env+purpose, alphanumeric only, max 24 chars)
# Naming: azwesadevdata (provider+region+sa+env+purpose, alphanumeric only, max 24 chars) or custom from variable
resource "azurerm_storage_account" "app_data" {
name = local.sa_data_name
name = var.storage_account_name != "" ? var.storage_account_name : local.sa_data_name
resource_group_name = azurerm_resource_group.main.name
location = var.azure_region
account_tier = "Standard"

View File

@@ -0,0 +1,48 @@
# Terraform Variables Example
# Copy this file to terraform.tfvars and fill in your values
# DO NOT commit terraform.tfvars to version control
# Azure Configuration
azure_region = "westeurope" # No US regions allowed
environment = "dev" # dev, stage, or prod
project_name = "the-order"
# Azure Subscription
subscription_id = "" # Your Azure subscription ID
tenant_id = "" # Your Azure tenant ID
# Resource Naming
resource_group_name = "the-order-rg-${environment}"
storage_account_name = "theorder${environment}" # Must be globally unique, lowercase, alphanumeric
key_vault_name = "the-order-kv-${environment}" # Must be globally unique
# Networking
domain_name = "" # Optional: your domain name
vnet_name = "the-order-vnet-${environment}"
subnet_name = "the-order-subnet-${environment}"
# Database
database_name = "theorder_${environment}"
database_admin_user = "theorder_admin"
# database_admin_password set via environment variable or Azure Key Vault
# Storage
storage_account_tier = "Standard"
storage_account_replication = "LRS" # LRS for dev, GRS for prod
# CDN (if using)
cdn_profile_name = "theorder-cdn-${environment}"
cdn_endpoint_name = "theorder-cdn-endpoint-${environment}"
# Kubernetes (AKS)
aks_cluster_name = "the-order-aks-${environment}"
aks_node_count = 2
aks_vm_size = "Standard_B2s"
# Tags
tags = {
Environment = "${environment}"
Project = "the-order"
ManagedBy = "terraform"
}

View File

@@ -40,6 +40,74 @@ variable "domain_name" {
default = ""
}
variable "subscription_id" {
description = "Azure subscription ID"
type = string
default = ""
sensitive = true
}
variable "tenant_id" {
description = "Azure tenant ID"
type = string
default = ""
sensitive = true
}
variable "client_id" {
description = "Azure service principal client ID (optional, uses Azure CLI auth if not set)"
type = string
default = ""
sensitive = true
}
variable "client_secret" {
description = "Azure service principal client secret (optional, uses Azure CLI auth if not set)"
type = string
default = ""
sensitive = true
}
variable "resource_group_name" {
description = "Azure resource group name"
type = string
default = ""
}
variable "storage_account_name" {
description = "Azure storage account name (must be globally unique)"
type = string
default = ""
}
variable "key_vault_name" {
description = "Azure Key Vault name (must be globally unique)"
type = string
default = ""
}
variable "tags" {
description = "Tags to apply to all resources"
type = map(string)
default = {
Environment = "dev"
Project = "the-order"
ManagedBy = "terraform"
}
}
variable "cdn_profile_name" {
description = "Azure CDN profile name"
type = string
default = ""
}
variable "cdn_endpoint_name" {
description = "Azure CDN endpoint name"
type = string
default = ""
}
variable "enable_monitoring" {
description = "Enable monitoring and observability"
type = bool

View File

@@ -9,6 +9,10 @@ terraform {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.0"
}
}
# Configure backend for state management