Terraform Modules — HISD Full-Stack
Overview
The infrastructure-as-code lives in eshars-ops/develop/infrastructure/ and uses OpenTofu (Terraform-compatible). A single tofu apply provisions everything needed to run the HISD web portal in a new environment.
infrastructure/├── main.tf # Wires all modules together├── variables.tf # Input variables├── locals.tf # Environment-specific logic├── outputs.tf # Resource outputs├── environments/│ ├── dev.tfvars│ ├── qa.tfvars│ ├── uat.tfvars│ ├── staging.tfvars│ ├── prod.tfvars│ ├── training.tfvars│ └── regression.tfvars└── modules/ ├── app-service-plan/ # (pre-existing) ├── key-vault/ # (pre-existing) ├── sql-server/ # (pre-existing) ├── private-dns/ # (pre-existing) ├── monitoring/ # (pre-existing) ├── app-insights/ # Log Analytics + Application Insights ├── sql-database/ # 3 databases + optional elastic pool ├── storage/ # Storage Account + 6 blob containers ├── redis/ # Redis Cache ├── web-app/ # Windows App Service + staging slot ├── function-app/ # Windows Function App └── key-vault-secrets/ # Seeds KV with connection stringsSQL Server Topology
Not every environment gets its own SQL Server. The topology balances cost and isolation:
| SQL Server | Environments | Mode |
|---|---|---|
eshars-nonprod-sql | Dev, QA, Training | Elastic pool (50 eDTU shared) |
eshars-uat-sql | UAT | Dedicated (standalone SKUs) |
eshars-regression-sql | Regression | Dedicated (standalone SKUs) |
eshars-prod-sql | Prod, Staging | Dedicated (standalone SKUs) |
Each environment gets 3 databases on its assigned server:
| Database | Purpose |
|---|---|
db-eshars-{env} | Core — students, visits, clinicians, Quartz scheduler |
db-log-{env} | Logs — ELMAH, job execution logs |
db-eshars-{env}-audit | Audit trail |
Resources Per Environment
graph TD
RG["Resource Group<br/>eshars-{env}-rg"]
RG --> ASP["App Service Plan"]
RG --> WA["Web App<br/>wa-eshars-{env}"]
RG --> SLOT["Staging Slot"]
RG --> FA["Function App<br/>func-eshars-{env}"]
RG --> SQL["SQL Server"]
SQL --> DB1["Core DB"]
SQL --> DB2["Log DB"]
SQL --> DB3["Audit DB"]
RG --> SA["Storage Account<br/>saeshars{env}"]
SA --> C1["6 blob containers"]
RG --> REDIS["Redis Cache"]
RG --> AI["Application Insights"]
AI --> LAW["Log Analytics"]
RG --> KV["Key Vault"]
KV --> SEC["Secrets"]
WA --> |"managed identity"| KV
FA --> |"managed identity"| KV
SLOT --> |"managed identity"| KV
Module Details
app-insights
Creates a Log Analytics Workspace and Application Insights resource connected to it.
| Output | Description |
|---|---|
instrumentation_key | App Insights instrumentation key (sensitive) |
connection_string | App Insights connection string (sensitive) |
workspace_id | Log Analytics workspace resource ID |
sql-database
Creates 3 SQL databases (Core, Log, Audit). Supports two modes:
- Elastic pool (
elastic_pool_enabled = true) — all 3 DBs share a pool. Used by dev/qa/training. - Standalone (
elastic_pool_enabled = false) — each DB gets its own SKU. Used by uat/regression/prod.
storage
Creates a Storage Account with 6 blob containers:
| Container | Purpose |
|---|---|
default | General storage |
certificate-attachments | Clinician certificate files |
provider-rx-sign-details | Provider prescription signatures |
eu-university | EU university documents |
iep | IEP documents |
log-files | Application log files (auto-deleted after 90 days) |
redis
Redis Cache for session state and output cache. TLS 1.2 enforced, non-SSL port disabled.
web-app
Windows App Service running .NET Framework 4.8. Includes:
- Staging slot for zero-downtime swap deployments
- System-assigned managed identity with Key Vault Secrets User role
- App settings include Key Vault name, App Insights keys, timezone (CST)
- HTTPS-only, TLS 1.2, FTP disabled
function-app
Windows Function App on the same App Service Plan. Same managed identity + Key Vault pattern as the web app.
key-vault-secrets
Seeds Key Vault with operational secrets:
| Secret | Source |
|---|---|
ESHARS | Core DB connection string (constructed from SQL Server FQDN) |
ESHARSLOGS | Log DB connection string |
ESHARSAUDIT | Audit DB connection string |
RedisConnectionString | Redis module output |
AzureBlobStorageConnectionString | Storage module output |
applicationInsightKey | App Insights module output |
SendGridApiKey | Placeholder — update post-provisioning |
EncryptionKey | Placeholder — update post-provisioning |
EncryptionkeyByte | Placeholder — update post-provisioning |
masterPassword | Placeholder — update post-provisioning |
MvcReportViewerUsername | Placeholder — update post-provisioning |
MvcReportViewerPassword | Placeholder — update post-provisioning |
EchoSignAppKey | Placeholder — update post-provisioning |
HandShakingKey | Placeholder — update post-provisioning |
SKU Map
| Resource | dev/qa/training | uat | regression | staging | prod |
|---|---|---|---|---|---|
| App Service Plan | B1 / S1 / S1 | S1 | S1 | S2 | P1v3 |
| SQL | ElasticPool (50 eDTU) | P1 | P1 | P1 | P1 |
| Redis | Basic/C0 | Standard/C0 | Basic/C0 | Standard/C0 | Standard/C1 |
| Storage replication | LRS | LRS | LRS | LRS | GRS |
| Backup retention | 7 days | 7 days | 7 days | 7 days | 35 days |
Deploying a New Environment
Prerequisites
- OpenTofu >= 1.6.0
- Azure CLI authenticated (
az login) - Backend storage account for tfstate (configured via
-backend-config)
Steps
cd eshars-ops/develop/infrastructure
# 1. Initialize (first time or after module changes)tofu init -backend-config=backends/{env}.hcl
# 2. Set sensitive variablesexport TF_VAR_sql_admin_login="..."export TF_VAR_sql_admin_password="..."export TF_VAR_quartz_connection_string="..."export TF_VAR_logs_connection_string="..."
# 3. Plantofu plan -var-file=environments/{env}.tfvars -out=tfplan
# 4. Review the plan carefully, then applytofu apply tfplan
# 5. Post-provisioning: update placeholder secrets in Key Vaultaz keyvault secret set --vault-name eshars-{env}-kv \ --name SendGridApiKey --value "actual-key-here"# ... repeat for all placeholder secretsAdding a New Environment
- Create
environments/{env}.tfvarswith appropriate SKUs - Add the environment name to the
validationblock invariables.tf - Create a backend config
backends/{env}.hclpointing to the tfstate container - Follow the deploy steps above
Sensitive Data
All sensitive values are passed via TF_VAR_ environment variables — never stored in .tfvars files:
| Variable | Description |
|---|---|
TF_VAR_sql_admin_login | SQL Server admin username |
TF_VAR_sql_admin_password | SQL Server admin password |
TF_VAR_quartz_connection_string | Core DB connection string (for monitoring module) |
TF_VAR_logs_connection_string | Log DB connection string (for monitoring module) |
TF_VAR_teams_webhook_url | Teams webhook for alert notifications |