From 120a940779fde6cdb084e2171304b1181ed2e65c Mon Sep 17 00:00:00 2001 From: DJSaunders1997 Date: Wed, 14 Jan 2026 19:51:04 +0000 Subject: [PATCH] Add Terraform Infrastructure as Code setup - Add Terraform configuration for Azure Container Apps infrastructure - Configure Resource Group, Container App Environment, and Container App - Set up secure secret management for API keys - Add comprehensive README with deployment instructions - Update .gitignore to exclude terraform state files - Organize terraform directory under backend/ for better structure Future enhancement: Parameterize container image reference for dynamic deployments --- .gitignore | 6 + backend/terraform/README.md | 145 +++++++++++++++++++++ backend/terraform/main.tf | 108 +++++++++++++++ backend/terraform/outputs.tf | 14 ++ backend/terraform/providers.tf | 12 ++ backend/terraform/terraform.tfvars.example | 12 ++ backend/terraform/variables.tf | 60 +++++++++ 7 files changed, 357 insertions(+) create mode 100644 backend/terraform/README.md create mode 100644 backend/terraform/main.tf create mode 100644 backend/terraform/outputs.tf create mode 100644 backend/terraform/providers.tf create mode 100644 backend/terraform/terraform.tfvars.example create mode 100644 backend/terraform/variables.tf diff --git a/.gitignore b/.gitignore index 901b4f3..95e3eaa 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,12 @@ coverage.xml *.cover *.py,cover .hypothesis/ + +# Terraform +terraform.tfvars +terraform.tfstate* +.terraform/ + .pytest_cache/ cover/ diff --git a/backend/terraform/README.md b/backend/terraform/README.md new file mode 100644 index 0000000..da4f689 --- /dev/null +++ b/backend/terraform/README.md @@ -0,0 +1,145 @@ +# GPTeasers Terraform Infrastructure + +This directory contains the Infrastructure as Code (IaC) configuration for deploying the GPTeasers quiz application to Azure Container Apps. + +## 📁 Directory Contents + +- `main.tf` - Main infrastructure configuration (Resource Group, Container App Environment, Container App) +- `variables.tf` - Input variables for the infrastructure +- `outputs.tf` - Output values from the deployed infrastructure +- `providers.tf` - Terraform provider configurations +- `terraform.tfvars` - Actual values for sensitive variables (not committed to git) +- `terraform.tfvars.example` - Example file showing required variable structure +- `terraform.tfstate` - Current state of deployed infrastructure (not committed to git) +- `graph.dot` - Infrastructure dependency graph in DOT format +- `infrastructure.png` & `infrastructure.svg` - Visual representations of the infrastructure + +## 🚀 Deploying the Infrastructure + +### Prerequisites + +1. **Azure CLI installed and authenticated**: + ```bash + az login + ``` + +2. **Terraform installed** (v1.14.3+ recommended) + +3. **Configure your secrets**: + ```bash + cp terraform.tfvars.example terraform.tfvars + # Edit terraform.tfvars with your actual API keys and tokens + ``` + +### Deployment Commands + +1. **Initialize Terraform** (first time only): + ```bash + cd backend/terraform + terraform init + ``` + +2. **Review the planned changes**: + ```bash + cd backend/terraform + terraform plan + ``` + +3. **Deploy the infrastructure**: + ```bash + cd backend/terraform + terraform apply + ``` + +4. **Get the deployed app URL**: + ```bash + cd backend/terraform + terraform output container_app_url + ``` + +### Cleanup + +To destroy all resources: +```bash +cd backend/terraform +terraform destroy +``` + +## ⚠️ Important Notes + +### Hardcoded Container Image Concern + +**I'm a bit confused and concerned that the container image is hardcoded in `main.tf`**: + +```terraform +image = "ghcr.io/djsaunders1997/gpteasers:7aa9ecacb7f655e25c149d704108690263ec26b4" +``` + +This means: +- The infrastructure is tied to a specific image version/tag +- Updates require manual Terraform changes +- CI/CD pipelines can't automatically update the infrastructure +- Risk of the image being deleted or becoming unavailable + +**Future Enhancement Idea**: This would be a cool project to parameterize the image reference: +- Add an `image_tag` variable to `variables.tf` +- Make the image reference dynamic: `"ghcr.io/djsaunders1997/gpteasers:${var.image_tag}"` +- Allow CI/CD to pass the latest image tag during deployment +- Enable blue/green deployments with different image versions + +For now, the infrastructure works as-is, but image updates require manual intervention. This could be a fun Terraform parameterization project for the future! 🎯 + +## 🔧 Configuration Details + +### Resources Created + +- **Resource Group**: `ContainerApps` (UK West region) +- **Container App Environment**: `container-app-environment` +- **Container App**: `gpteasers` with: + - 0.25 CPU cores, 0.5Gi memory + - External ingress on port 8000 + - GitHub Container Registry integration + - Environment variables for all AI provider API keys + +### Secrets Management + +The following secrets are managed via Terraform: +- OpenAI API Key +- Azure AI API Key & Base URL +- Gemini API Key +- DeepSeek API Key +- GitHub Container Registry Token + +**Security Note**: Secrets are defined in `terraform.tfvars` (gitignored) and managed through Terraform's lifecycle rules to prevent accidental overwrites during CI/CD deployments. + +### Network Configuration + +- **Ingress**: External enabled, auto transport +- **Traffic**: 100% to latest revision +- **Scaling**: 0-1 replicas (manual scaling) + +## 📊 Infrastructure Visualization + +View the infrastructure dependencies: +```bash +# Generate DOT graph +cd backend/terraform +terraform graph > graph.dot + +# Convert to PNG (requires GraphViz) +dot -Tpng graph.dot -o infrastructure.png +``` + +## 🔍 Troubleshooting + +- **State issues**: `terraform refresh` to sync with Azure +- **Import existing resources**: `terraform import . ` +- **Debug mode**: `TF_LOG=DEBUG terraform apply` + +## 📝 Development Workflow + +1. Make changes to `.tf` files +2. Run `terraform plan` to preview +3. Run `terraform apply` to deploy +4. Use `terraform output` to get URLs/endpoints +5. Commit changes (excluding `.tfstate` and `.tfvars`) \ No newline at end of file diff --git a/backend/terraform/main.tf b/backend/terraform/main.tf new file mode 100644 index 0000000..6407587 --- /dev/null +++ b/backend/terraform/main.tf @@ -0,0 +1,108 @@ +# Resource Group +resource "azurerm_resource_group" "gpteasers" { + name = var.resource_group_name + location = var.location +} + +# Container App Environment +resource "azurerm_container_app_environment" "gpteasers" { + name = var.container_app_environment_name + location = azurerm_resource_group.gpteasers.location + resource_group_name = azurerm_resource_group.gpteasers.name + # Remove log_analytics_workspace_id - the existing environment doesn't have one +} + +# Container App +resource "azurerm_container_app" "gpteasers" { + name = var.container_app_name + container_app_environment_id = azurerm_container_app_environment.gpteasers.id + resource_group_name = azurerm_resource_group.gpteasers.name + revision_mode = "Single" + + template { + container { + name = "gpteasers" + image = "ghcr.io/djsaunders1997/gpteasers:7aa9ecacb7f655e25c149d704108690263ec26b4" + cpu = 0.25 + memory = "0.5Gi" + + env { + name = "OPENAI_API_KEY" + secret_name = "openai-api-key-secret" + } + env { + name = "AZURE_AI_API_KEY" + secret_name = "azure-ai-api-key-secret" + } + env { + name = "GEMINI_API_KEY" + secret_name = "gemini-api-key-secret" + } + env { + name = "DEEPSEEK_API_KEY" + secret_name = "deepseek-api-key-secret" + } + env { + name = "AZURE_AI_API_BASE" + secret_name = "azure-ai-api-base-secret" + } + } + + min_replicas = 0 + max_replicas = 1 + } + + ingress { + external_enabled = true + target_port = 8000 + transport = "auto" # Changed from "Auto" to "auto" to match existing + traffic_weight { + latest_revision = true + percentage = 100 + } + } + + registry { + server = "ghcr.io" + username = "DJSaunders1997" + password_secret_name = "ghcrio-djsaunders1997" + } + + # Secrets are managed externally - these placeholders prevent Terraform from removing them + secret { + name = "openai-api-key-secret" + value = var.openai_api_key + } + + secret { + name = "azure-ai-api-key-secret" + value = var.azure_ai_api_key + } + + secret { + name = "gemini-api-key-secret" + value = var.gemini_api_key + } + + secret { + name = "deepseek-api-key-secret" + value = var.deepseek_api_key + } + + secret { + name = "azure-ai-api-base-secret" + value = var.azure_ai_api_base + } + + secret { + name = "ghcrio-djsaunders1997" + value = var.ghcr_token + } + + lifecycle { + ignore_changes = [ + secret, # Don't update secrets - they're managed by CI/CD or manually + template[0].container[0].image, # Allow image updates from CI/CD + ] + } +} \ No newline at end of file diff --git a/backend/terraform/outputs.tf b/backend/terraform/outputs.tf new file mode 100644 index 0000000..8d72034 --- /dev/null +++ b/backend/terraform/outputs.tf @@ -0,0 +1,14 @@ +output "container_app_url" { + description = "URL of the deployed container app" + value = azurerm_container_app.gpteasers.latest_revision_fqdn +} + +output "resource_group_name" { + description = "Name of the resource group" + value = azurerm_resource_group.gpteasers.name +} + +output "container_app_environment_name" { + description = "Name of the container app environment" + value = azurerm_container_app_environment.gpteasers.name +} \ No newline at end of file diff --git a/backend/terraform/providers.tf b/backend/terraform/providers.tf new file mode 100644 index 0000000..a613eb1 --- /dev/null +++ b/backend/terraform/providers.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 3.0" + } + } +} + +provider "azurerm" { + features {} +} \ No newline at end of file diff --git a/backend/terraform/terraform.tfvars.example b/backend/terraform/terraform.tfvars.example new file mode 100644 index 0000000..efdb858 --- /dev/null +++ b/backend/terraform/terraform.tfvars.example @@ -0,0 +1,12 @@ +# Copy this file to terraform.tfvars and fill in your actual values +# DO NOT commit terraform.tfvars to git - it contains sensitive information + +# API Keys +openai_api_key = "your-openai-api-key-here" +gemini_api_key = "your-gemini-api-key-here" +azure_ai_api_key = "your-azure-ai-api-key-here" +azure_ai_api_base = "your-azure-ai-api-base-url-here" +deepseek_api_key = "your-deepseek-api-key-here" + +# GitHub Container Registry +ghcr_token = "your-github-personal-access-token-here" \ No newline at end of file diff --git a/backend/terraform/variables.tf b/backend/terraform/variables.tf new file mode 100644 index 0000000..6bb9084 --- /dev/null +++ b/backend/terraform/variables.tf @@ -0,0 +1,60 @@ +variable "resource_group_name" { + description = "Name of the resource group" + type = string + default = "ContainerApps" +} + +variable "location" { + description = "Azure region" + type = string + default = "UK West" +} + +variable "container_app_name" { + description = "Name of the container app" + type = string + default = "gpteasers" +} + +variable "container_app_environment_name" { + description = "Name of the container app environment" + type = string + default = "container-app-environment" +} + +# Secret variables - values loaded from terraform.tfvars (not committed) +variable "openai_api_key" { + description = "OpenAI API key" + type = string + sensitive = true +} + +variable "gemini_api_key" { + description = "Gemini API key" + type = string + sensitive = true +} + +variable "azure_ai_api_key" { + description = "Azure AI API key" + type = string + sensitive = true +} + +variable "azure_ai_api_base" { + description = "Azure AI API base URL" + type = string + sensitive = true +} + +variable "deepseek_api_key" { + description = "DeepSeek API key" + type = string + sensitive = true +} + +variable "ghcr_token" { + description = "GitHub Container Registry token" + type = string + sensitive = true +} \ No newline at end of file