Skip to content

Commit 4972016

Browse files
committed
Add Terraform configuration and Azure deployment workflow for Pets Workshop
1 parent 041bc09 commit 4972016

File tree

6 files changed

+707
-0
lines changed

6 files changed

+707
-0
lines changed

.github/workflows/azure-deploy.yml

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
name: Deploy Pets Workshop to Azure
2+
3+
on:
4+
push:
5+
branches: [ main, msignite-25 ]
6+
paths:
7+
- 'terraform/**'
8+
- 'server/**'
9+
- 'client/**'
10+
- '.github/workflows/azure-deploy.yml'
11+
pull_request:
12+
branches: [ main ]
13+
paths:
14+
- 'terraform/**'
15+
- 'server/**'
16+
- 'client/**'
17+
workflow_dispatch:
18+
inputs:
19+
terraform_action:
20+
description: 'Terraform action to perform'
21+
required: true
22+
default: 'plan'
23+
type: choice
24+
options:
25+
- plan
26+
- apply
27+
- destroy
28+
29+
env:
30+
ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
31+
ARM_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
32+
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
33+
ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
34+
TF_VAR_sql_admin_password: ${{ secrets.SQL_ADMIN_PASSWORD }}
35+
36+
jobs:
37+
terraform:
38+
name: 'Terraform Infrastructure'
39+
runs-on: ubuntu-latest
40+
environment: production
41+
42+
defaults:
43+
run:
44+
shell: bash
45+
working-directory: ./terraform
46+
47+
outputs:
48+
backend_app_name: ${{ steps.terraform_output.outputs.backend_app_name }}
49+
frontend_deployment_token: ${{ steps.terraform_output.outputs.frontend_deployment_token }}
50+
resource_group_name: ${{ steps.terraform_output.outputs.resource_group_name }}
51+
static_web_app_name: ${{ steps.terraform_output.outputs.static_web_app_name }}
52+
53+
steps:
54+
- name: Checkout
55+
uses: actions/checkout@v4
56+
57+
- name: Setup Terraform
58+
uses: hashicorp/setup-terraform@v3
59+
with:
60+
terraform_version: 1.6.0
61+
terraform_wrapper: false
62+
63+
- name: Terraform Init
64+
run: terraform init
65+
66+
- name: Terraform Format Check
67+
run: terraform fmt -check
68+
69+
- name: Terraform Validate
70+
run: terraform validate
71+
72+
- name: Terraform Plan
73+
if: github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && inputs.terraform_action == 'plan')
74+
run: terraform plan -no-color
75+
continue-on-error: true
76+
77+
- name: Terraform Plan Status
78+
if: github.event_name == 'pull_request' && steps.plan.outcome == 'failure'
79+
run: exit 1
80+
81+
- name: Terraform Apply
82+
if: (github.ref == 'refs/heads/main' && github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && inputs.terraform_action == 'apply')
83+
run: terraform apply -auto-approve
84+
85+
- name: Terraform Destroy
86+
if: github.event_name == 'workflow_dispatch' && inputs.terraform_action == 'destroy'
87+
run: terraform destroy -auto-approve
88+
89+
- name: Terraform Output
90+
if: (github.ref == 'refs/heads/main' && github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && inputs.terraform_action == 'apply')
91+
id: terraform_output
92+
run: |
93+
echo "backend_app_name=$(terraform output -raw backend_app_service_name)" >> $GITHUB_OUTPUT
94+
echo "frontend_deployment_token=$(terraform output -raw frontend_deployment_token)" >> $GITHUB_OUTPUT
95+
echo "resource_group_name=$(terraform output -raw resource_group_name)" >> $GITHUB_OUTPUT
96+
echo "static_web_app_name=$(terraform output -raw frontend_static_web_app_name)" >> $GITHUB_OUTPUT
97+
98+
deploy_backend:
99+
name: 'Deploy Flask Backend'
100+
runs-on: ubuntu-latest
101+
needs: terraform
102+
if: (github.ref == 'refs/heads/main' && github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && inputs.terraform_action == 'apply')
103+
104+
steps:
105+
- name: Checkout
106+
uses: actions/checkout@v4
107+
108+
- name: Set up Python
109+
uses: actions/setup-python@v4
110+
with:
111+
python-version: '3.11'
112+
113+
- name: Install dependencies
114+
working-directory: ./server
115+
run: |
116+
python -m pip install --upgrade pip
117+
pip install -r requirements.txt
118+
119+
- name: Login to Azure
120+
uses: azure/login@v1
121+
with:
122+
creds: ${{ secrets.AZURE_CREDENTIALS }}
123+
124+
- name: Deploy to Azure Web App
125+
uses: azure/webapps-deploy@v2
126+
with:
127+
app-name: ${{ needs.terraform.outputs.backend_app_name }}
128+
package: ./server
129+
startup-command: 'gunicorn --bind=0.0.0.0 --timeout 600 app:app'
130+
131+
deploy_frontend:
132+
name: 'Deploy Astro Frontend'
133+
runs-on: ubuntu-latest
134+
needs: terraform
135+
if: (github.ref == 'refs/heads/main' && github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && inputs.terraform_action == 'apply')
136+
137+
steps:
138+
- name: Checkout
139+
uses: actions/checkout@v4
140+
141+
- name: Setup Node.js
142+
uses: actions/setup-node@v4
143+
with:
144+
node-version: '18'
145+
cache: 'npm'
146+
cache-dependency-path: './client/package-lock.json'
147+
148+
- name: Install dependencies
149+
working-directory: ./client
150+
run: npm ci
151+
152+
- name: Build Astro app
153+
working-directory: ./client
154+
run: npm run build
155+
env:
156+
# Update API endpoint to point to Azure App Service
157+
VITE_API_URL: https://${{ needs.terraform.outputs.backend_app_name }}.azurewebsites.net
158+
159+
- name: Deploy to Static Web App
160+
uses: Azure/static-web-apps-deploy@v1
161+
with:
162+
azure_static_web_apps_api_token: ${{ needs.terraform.outputs.frontend_deployment_token }}
163+
repo_token: ${{ secrets.GITHUB_TOKEN }}
164+
action: 'upload'
165+
app_location: '/client'
166+
api_location: ''
167+
output_location: 'dist'
168+
169+
database_setup:
170+
name: 'Setup Database'
171+
runs-on: ubuntu-latest
172+
needs: terraform
173+
if: (github.ref == 'refs/heads/main' && github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && inputs.terraform_action == 'apply')
174+
175+
steps:
176+
- name: Checkout
177+
uses: actions/checkout@v4
178+
179+
- name: Login to Azure
180+
uses: azure/login@v1
181+
with:
182+
creds: ${{ secrets.AZURE_CREDENTIALS }}
183+
184+
- name: Setup Database Schema
185+
run: |
186+
# Note: Add your database migration/setup commands here
187+
echo "Database setup would go here"
188+
echo "You may want to run SQL scripts to create tables and seed data"
189+
# Example: sqlcmd -S $SQL_SERVER -d $SQL_DATABASE -U $SQL_USER -P $SQL_PASSWORD -i ./database/schema.sql
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# Main Terraform configuration for Pets Workshop Azure deployment
2+
3+
# Generate random string for unique naming
4+
resource "random_string" "suffix" {
5+
length = 8
6+
special = false
7+
upper = false
8+
}
9+
10+
# Get current user context for Key Vault access policy
11+
data "azurerm_client_config" "current" {}
12+
13+
# Resource Group
14+
resource "azurerm_resource_group" "main" {
15+
name = "${var.resource_prefix}-rg-${random_string.suffix.result}"
16+
location = var.location
17+
18+
tags = var.tags
19+
}
20+
21+
# Application Insights for monitoring
22+
resource "azurerm_application_insights" "main" {
23+
name = "${var.resource_prefix}-ai-${random_string.suffix.result}"
24+
location = azurerm_resource_group.main.location
25+
resource_group_name = azurerm_resource_group.main.name
26+
application_type = "web"
27+
28+
tags = var.tags
29+
}
30+
31+
# Key Vault for storing secrets
32+
resource "azurerm_key_vault" "main" {
33+
name = "${var.resource_prefix}-kv-${random_string.suffix.result}"
34+
location = azurerm_resource_group.main.location
35+
resource_group_name = azurerm_resource_group.main.name
36+
tenant_id = data.azurerm_client_config.current.tenant_id
37+
sku_name = "standard"
38+
39+
# Enable soft delete and purge protection
40+
soft_delete_retention_days = 7
41+
purge_protection_enabled = false
42+
43+
# Network access
44+
network_acls {
45+
default_action = "Allow"
46+
bypass = "AzureServices"
47+
}
48+
49+
tags = var.tags
50+
}
51+
52+
# Key Vault access policy for current user
53+
resource "azurerm_key_vault_access_policy" "current_user" {
54+
key_vault_id = azurerm_key_vault.main.id
55+
tenant_id = data.azurerm_client_config.current.tenant_id
56+
object_id = data.azurerm_client_config.current.object_id
57+
58+
secret_permissions = [
59+
"Get",
60+
"List",
61+
"Set",
62+
"Delete",
63+
"Recover",
64+
"Backup",
65+
"Restore"
66+
]
67+
}
68+
69+
# SQL Server
70+
resource "azurerm_mssql_server" "main" {
71+
name = "${var.resource_prefix}-sql-${random_string.suffix.result}"
72+
resource_group_name = azurerm_resource_group.main.name
73+
location = azurerm_resource_group.main.location
74+
version = "12.0"
75+
administrator_login = var.sql_admin_username
76+
administrator_login_password = var.sql_admin_password
77+
78+
# Security configurations
79+
minimum_tls_version = "1.2"
80+
81+
azuread_administrator {
82+
login_username = var.sql_admin_username
83+
object_id = data.azurerm_client_config.current.object_id
84+
}
85+
86+
tags = var.tags
87+
}
88+
89+
# SQL Database
90+
resource "azurerm_mssql_database" "main" {
91+
name = "${var.resource_prefix}-sqldb-${random_string.suffix.result}"
92+
server_id = azurerm_mssql_server.main.id
93+
collation = "SQL_Latin1_General_CP1_CI_AS"
94+
license_type = "LicenseIncluded"
95+
max_size_gb = 2
96+
sku_name = var.sql_database_sku
97+
zone_redundant = false
98+
99+
tags = var.tags
100+
}
101+
102+
# SQL Server firewall rule to allow Azure services
103+
resource "azurerm_mssql_firewall_rule" "allow_azure_services" {
104+
name = "AllowAzureServices"
105+
server_id = azurerm_mssql_server.main.id
106+
start_ip_address = "0.0.0.0"
107+
end_ip_address = "0.0.0.0"
108+
}
109+
110+
# Store database connection string in Key Vault
111+
resource "azurerm_key_vault_secret" "database_connection_string" {
112+
name = "database-connection-string"
113+
value = "mssql+pyodbc://${var.sql_admin_username}:${var.sql_admin_password}@${azurerm_mssql_server.main.fully_qualified_domain_name}:1433/${azurerm_mssql_database.main.name}?driver=ODBC+Driver+17+for+SQL+Server"
114+
key_vault_id = azurerm_key_vault.main.id
115+
116+
depends_on = [azurerm_key_vault_access_policy.current_user]
117+
}
118+
119+
# App Service Plan for Flask backend
120+
resource "azurerm_service_plan" "main" {
121+
name = "${var.resource_prefix}-asp-${random_string.suffix.result}"
122+
resource_group_name = azurerm_resource_group.main.name
123+
location = azurerm_resource_group.main.location
124+
os_type = "Linux"
125+
sku_name = var.app_service_sku
126+
127+
tags = var.tags
128+
}
129+
130+
# App Service for Flask backend
131+
resource "azurerm_linux_web_app" "backend" {
132+
name = "${var.resource_prefix}-backend-${random_string.suffix.result}"
133+
resource_group_name = azurerm_resource_group.main.name
134+
location = azurerm_service_plan.main.location
135+
service_plan_id = azurerm_service_plan.main.id
136+
137+
site_config {
138+
application_stack {
139+
python_version = "3.11"
140+
}
141+
142+
# Enable CORS for frontend
143+
cors {
144+
allowed_origins = ["*"] # In production, specify your frontend domain
145+
support_credentials = false
146+
}
147+
148+
# Health check
149+
health_check_path = "/api/dogs"
150+
}
151+
152+
# Application settings
153+
app_settings = {
154+
"APPLICATIONINSIGHTS_CONNECTION_STRING" = azurerm_application_insights.main.connection_string
155+
"SCM_DO_BUILD_DURING_DEPLOYMENT" = "true"
156+
"ENABLE_ORYX_BUILD" = "true"
157+
"WEBSITES_ENABLE_APP_SERVICE_STORAGE" = "false"
158+
159+
# Database configuration will be set via Key Vault reference
160+
"@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.database_connection_string.id})" = "DATABASE_URL"
161+
}
162+
163+
# Identity for Key Vault access
164+
identity {
165+
type = "SystemAssigned"
166+
}
167+
168+
tags = var.tags
169+
}
170+
171+
# Key Vault access policy for App Service managed identity
172+
resource "azurerm_key_vault_access_policy" "app_service" {
173+
key_vault_id = azurerm_key_vault.main.id
174+
tenant_id = azurerm_linux_web_app.backend.identity[0].tenant_id
175+
object_id = azurerm_linux_web_app.backend.identity[0].principal_id
176+
177+
secret_permissions = [
178+
"Get",
179+
"List"
180+
]
181+
}
182+
183+
# Static Web App for frontend
184+
resource "azurerm_static_web_app" "frontend" {
185+
name = "${var.resource_prefix}-swa-${random_string.suffix.result}"
186+
resource_group_name = azurerm_resource_group.main.name
187+
location = var.static_web_app_location
188+
sku_tier = var.static_web_app_sku
189+
sku_size = var.static_web_app_sku
190+
191+
tags = var.tags
192+
}
193+
194+
# Static Web App custom domain (optional)
195+
# Uncomment and configure if you have a custom domain
196+
# resource "azurerm_static_web_app_custom_domain" "frontend" {
197+
# static_web_app_id = azurerm_static_web_app.frontend.id
198+
# domain_name = var.custom_domain
199+
# validation_type = "cname-delegation"
200+
# }

0 commit comments

Comments
 (0)