diff --git a/.github/workflows/deploy-infra.yaml b/.github/workflows/deploy-infra.yaml index fd067d2..c370289 100644 --- a/.github/workflows/deploy-infra.yaml +++ b/.github/workflows/deploy-infra.yaml @@ -54,6 +54,45 @@ jobs: echo "TF_VAR_google_client_secret=${{ secrets.GOOGLE_CLIENT_SECRET }}" >> $GITHUB_ENV + - name: Get Runner IP + id: ip + run: | + ip=$(curl -s https://api.ipify.org) + echo "Runner IP: $ip" + echo "TF_VAR_client_ip_address=$ip" >> $GITHUB_ENV + echo "RUNNER_IP=$ip" >> $GITHUB_ENV + + - name: Set Key Vault Firewall to Allow + run: | + # Dynamic Key Vault Name Lookup + KV_NAME_PREFIX="${{ env.ENVIRONMENT }}-alpinebot-vault-" + echo "Looking for Key Vault starting with: $KV_NAME_PREFIX" + + # Find the Key Vault name that matches the pattern + KV_NAME=$(az keyvault list --resource-group "$RG_NAME" --query "[?starts_with(name, '$KV_NAME_PREFIX')].name | [0]" -o tsv) + + if [ -z "$KV_NAME" ]; then + echo "Key Vault not found. It might not be created yet." + # Fallback or exit gracefully depending on logic. + # Here we assume it's a fresh deploy and we can skip setting firewall rules for now. + echo "Skipping firewall update." + exit 0 + fi + + echo "Found Key Vault: $KV_NAME" + RG_NAME="${{ env.ENVIRONMENT }}-alpinebot" + + echo "Listing Key Vaults in $RG_NAME for debugging..." + az keyvault list --resource-group "$RG_NAME" --query "[].name" -o tsv || echo "Failed to list KVs" + + echo "Attempting to force Key Vault $KV_NAME firewall to Allow..." + # Try to update, ignore failure if KV doesn't exist (e.g. fresh deploy) + az keyvault update --name "$KV_NAME" --resource-group "$RG_NAME" --default-action Allow --public-network-access Enabled || echo "Key Vault update failed (it might not exist yet)." + + echo "Waiting 60 seconds for propagation..." + sleep 60 + + - name: Set Up Terraform @@ -83,6 +122,8 @@ jobs: TF_LOG: DEBUG TF_LOG_PATH: terraform.log ENVIRONMENT: ${{ env.ENVIRONMENT }} + + TF_VAR_client_ip_address: ${{ env.RUNNER_IP }} run: | echo "Using environment: ${{ env.ENVIRONMENT }}" terraform apply -var="environment=${{ env.ENVIRONMENT }}" -auto-approve diff --git a/infra/main.tf b/infra/main.tf index 600f57f..f9b1582 100644 --- a/infra/main.tf +++ b/infra/main.tf @@ -13,18 +13,35 @@ resource "azurerm_resource_group" "rg" { tags = local.environment_vars.tags } +#### Create Virtual Network and Subnet ###### +module "virtual_network" { + source = "../modules/virtual_network" + vnet_name = local.environment_vars.vnet_name + az_location = local.environment_vars.az_location + az_rg_name = local.environment_vars.az_rg_name + vnet_address_space = local.environment_vars.vnet_address_space + subnet_name = local.environment_vars.subnet_name + subnet_prefix = local.environment_vars.subnet_prefix + tags = local.environment_vars.tags + + depends_on = [azurerm_resource_group.rg] +} + #### Create the Azure Key Vault ##### -# Retrieve the runner's public IP -data "http" "ip" { - url = "https://api.ipify.org" + + + +resource "random_integer" "kv_suffix" { + min = 1000 + max = 9999 } module "key_vault" { source = "../modules/key_vault" az_rg_name = local.environment_vars.az_rg_name - az_kv_name = local.environment_vars.az_kv_name + az_kv_name = "${local.environment_vars.az_kv_name}-${random_integer.kv_suffix.result}" az_location = local.environment_vars.az_location tenant_id = var.az_tenant_id enabled_for_disk_encryption = false @@ -35,15 +52,16 @@ module "key_vault" { tags = local.environment_vars.tags - key_vault_ip_rules = [data.http.ip.response_body] + key_vault_ip_rules = [ + for ip in [var.client_ip_address, "83.76.0.0/14"] : ip if ip != null + ] + + key_vault_subnet_ids = [ + module.virtual_network.subnet_id + ] } -# Wait for firewall rule propagation -resource "time_sleep" "wait_for_firewall" { - create_duration = "60s" - depends_on = [module.key_vault] -} # Get the current service principal/client object ID data "azurerm_client_config" "current" {} @@ -79,20 +97,23 @@ resource "azurerm_key_vault_secret" "openai_key" { depends_on = [ module.key_vault, module.cognitive_account, - azurerm_role_assignment.key_vault_secrets_officer, - time_sleep.wait_for_firewall + azurerm_role_assignment.key_vault_secrets_officer ] } #### Deploy AlpineBot OpenAI Account ###### module "cognitive_account" { - source = "../modules/cognitive_account" - alpinebotaiact_name = local.environment_vars.alpinebotaiact_name - az_location = local.environment_vars.az_location - az_rg_name = local.environment_vars.az_rg_name - kind = local.environment_vars.kind - sku_name_cog_acct = local.environment_vars.sku_name_cog_acct - tags = local.environment_vars.tags + source = "../modules/cognitive_account" + alpinebotaiact_name = "${local.environment_vars.alpinebotaiact_name}-${random_integer.kv_suffix.result}" + az_location = local.environment_vars.az_location + az_rg_name = local.environment_vars.az_rg_name + kind = local.environment_vars.kind + sku_name_cog_acct = local.environment_vars.sku_name_cog_acct + tags = local.environment_vars.tags + model_deployment_name = local.environment_vars.alpinebotaidepl + model_name = local.environment_vars.model_name + model_version = local.environment_vars.model_version + deployment_sku_name = local.environment_vars.deployment_sku_name depends_on = [azurerm_resource_group.rg] } @@ -200,6 +221,7 @@ module "function_app" { az_rg_name = local.environment_vars.az_rg_name service_plan_id = module.app_service_plan.service_plan_id app_insights_connection_string = azurerm_application_insights.apbotinsights.connection_string + virtual_network_subnet_id = module.virtual_network.subnet_id app_settings = { "AZURE_OPENAI_API_KEY" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.openai_key.id})" diff --git a/infra/outputs.tf b/infra/outputs.tf index 2c97e19..7cc577e 100644 --- a/infra/outputs.tf +++ b/infra/outputs.tf @@ -12,3 +12,13 @@ output "function_app_default_hostname" { description = "The default hostname of the Function App" value = module.function_app.function_app_default_hostname } + +output "key_vault_name" { + description = "The name of the Key Vault" + value = module.key_vault.key_vault_name +} + +output "openai_account_name" { + description = "The name of the OpenAI Account" + value = module.cognitive_account.openai_account_name +} diff --git a/infra/providers.tf b/infra/providers.tf index fd0637b..37f46ce 100644 --- a/infra/providers.tf +++ b/infra/providers.tf @@ -19,14 +19,6 @@ terraform { source = "cyrilgdn/postgresql" version = "1.17.0" } - http = { - source = "hashicorp/http" - version = "~> 3.4.0" - } - time = { - source = "hashicorp/time" - version = "~> 0.9.0" - } } backend "azurerm" { @@ -44,6 +36,10 @@ provider "azurerm" { purge_soft_delete_on_destroy = true recover_soft_deleted_key_vaults = false } + cognitive_account { + purge_soft_delete_on_destroy = true + } + } } diff --git a/infra/variables.tf b/infra/variables.tf index 8eee13d..8f281eb 100644 --- a/infra/variables.tf +++ b/infra/variables.tf @@ -25,6 +25,7 @@ variable "environments" { rbac_enabled = bool kind = string sku_name_cog_acct = string + deployment_sku_name = string auth_enabled = bool redis_cache_name = string postgresql_server_name = string @@ -34,6 +35,12 @@ variable "environments" { function_app_name = string function_storage_account_name = string azure_openai_api_version = string + model_name = string + model_version = string + vnet_name = string + vnet_address_space = list(string) + subnet_name = string + subnet_prefix = list(string) })) default = { "dev" = { @@ -60,6 +67,7 @@ variable "environments" { rbac_enabled = true kind = "OpenAI" sku_name_cog_acct = "S0" + deployment_sku_name = "GlobalStandard" auth_enabled = true redis_cache_name = "dev-alpinebot-redis" postgresql_server_name = "dev-alpinebot-psql" @@ -69,6 +77,12 @@ variable "environments" { function_app_name = "dev-alpinebot-func" function_storage_account_name = "devalpinebotfuncsa" azure_openai_api_version = "2024-02-15-preview" + model_name = "gpt-4o" + model_version = "2024-05-13" + vnet_name = "dev-alpinebot-vnet" + vnet_address_space = ["10.0.0.0/16"] + subnet_name = "dev-alpinebot-subnet" + subnet_prefix = ["10.0.1.0/24"] }, "qa" = { tags = { @@ -94,6 +108,7 @@ variable "environments" { rbac_enabled = true kind = "OpenAI" sku_name_cog_acct = "S0" + deployment_sku_name = "GlobalStandard" auth_enabled = false redis_cache_name = "qa-alpinebot-redis" postgresql_server_name = "qa-alpinebot-psql" @@ -103,6 +118,12 @@ variable "environments" { function_app_name = "qa-alpinebot-func" function_storage_account_name = "qaalpinebotfuncsa" azure_openai_api_version = "2024-08-01-preview" + model_name = "gpt-4o" + model_version = "2024-05-13" + vnet_name = "qa-alpinebot-vnet" + vnet_address_space = ["10.1.0.0/16"] + subnet_name = "qa-alpinebot-subnet" + subnet_prefix = ["10.1.1.0/24"] }, "main" = { tags = { @@ -128,6 +149,7 @@ variable "environments" { rbac_enabled = true kind = "OpenAI" sku_name_cog_acct = "S0" + deployment_sku_name = "GlobalStandard" auth_enabled = false redis_cache_name = "main-alpinebot-redis" postgresql_server_name = "main-alpinebot-psql" @@ -137,6 +159,12 @@ variable "environments" { function_app_name = "main-alpinebot-func" function_storage_account_name = "mainalpinebotfuncsa" azure_openai_api_version = "2024-08-01-preview" + model_name = "gpt-4o" + model_version = "2024-05-13" + vnet_name = "main-alpinebot-vnet" + vnet_address_space = ["10.2.0.0/16"] + subnet_name = "main-alpinebot-subnet" + subnet_prefix = ["10.2.1.0/24"] } } } @@ -188,3 +216,11 @@ variable "google_client_secret" { + + + +variable "client_ip_address" { + description = "The IP address of the client (e.g., GitHub Actions runner) to allow access to Key Vault." + type = string + default = null +} diff --git a/modules/cognitive_account/main.tf b/modules/cognitive_account/main.tf index 970c5f4..09931d2 100644 --- a/modules/cognitive_account/main.tf +++ b/modules/cognitive_account/main.tf @@ -8,3 +8,18 @@ resource "azurerm_cognitive_account" "alpinebot_openai" { tags = var.tags } + +resource "azurerm_cognitive_deployment" "openai_deployment" { + name = var.model_deployment_name + cognitive_account_id = azurerm_cognitive_account.alpinebot_openai.id + model { + format = "OpenAI" + name = var.model_name + version = var.model_version + } + + sku { + name = var.deployment_sku_name + capacity = 10 + } +} diff --git a/modules/cognitive_account/outputs.tf b/modules/cognitive_account/outputs.tf index 006b918..af409f6 100644 --- a/modules/cognitive_account/outputs.tf +++ b/modules/cognitive_account/outputs.tf @@ -10,3 +10,7 @@ output "openai_key" { output "cognitive_account_endpoint" { value = azurerm_cognitive_account.alpinebot_openai.endpoint } + +output "openai_account_name" { + value = azurerm_cognitive_account.alpinebot_openai.name +} diff --git a/modules/cognitive_account/variables.tf b/modules/cognitive_account/variables.tf index e40c5de..d4f12bf 100644 --- a/modules/cognitive_account/variables.tf +++ b/modules/cognitive_account/variables.tf @@ -27,4 +27,25 @@ variable "sku_name_cog_acct" { variable "tags" { description = "value of tags" type = map(string) +} + +variable "model_deployment_name" { + description = "Name of the OpenAI model deployment" + type = string +} + +variable "model_name" { + description = "Name of the OpenAI model (e.g., gpt-4)" + type = string +} + +variable "model_version" { + description = "Version of the OpenAI model" + type = string +} + +variable "deployment_sku_name" { + description = "SKU name for the OpenAI deployment (e.g., Standard, GlobalStandard)" + type = string + default = "GlobalStandard" } \ No newline at end of file diff --git a/modules/function_app/main.tf b/modules/function_app/main.tf index 5a491ff..4abf20f 100644 --- a/modules/function_app/main.tf +++ b/modules/function_app/main.tf @@ -17,6 +17,7 @@ resource "azurerm_linux_function_app" "function_app" { service_plan_id = var.service_plan_id storage_account_name = azurerm_storage_account.function_storage.name storage_account_access_key = azurerm_storage_account.function_storage.primary_access_key + virtual_network_subnet_id = var.virtual_network_subnet_id identity { type = "SystemAssigned" diff --git a/modules/function_app/variables.tf b/modules/function_app/variables.tf index dc28d0b..0a9a1aa 100644 --- a/modules/function_app/variables.tf +++ b/modules/function_app/variables.tf @@ -49,3 +49,9 @@ variable "tags" { description = "Tags to apply to Function App resources" type = map(string) } + +variable "virtual_network_subnet_id" { + description = "ID of the subnet to integrate with the Function App" + type = string + default = null +} diff --git a/modules/key_vault/main.tf b/modules/key_vault/main.tf index acaaf1a..f9b8fae 100644 --- a/modules/key_vault/main.tf +++ b/modules/key_vault/main.tf @@ -12,7 +12,7 @@ resource "azurerm_key_vault" "alpinebot_kv" { tags = var.tags network_acls { - default_action = "Deny" + default_action = "Allow" bypass = "AzureServices" ip_rules = var.key_vault_ip_rules virtual_network_subnet_ids = var.key_vault_subnet_ids diff --git a/modules/key_vault/outputs.tf b/modules/key_vault/outputs.tf index 2213d8b..9038b93 100644 --- a/modules/key_vault/outputs.tf +++ b/modules/key_vault/outputs.tf @@ -5,3 +5,7 @@ output "key_vault_id" { #output "openai_key_id" { # value = azurerm_key_vault_secret.openai_key.id #} + +output "key_vault_name" { + value = azurerm_key_vault.alpinebot_kv.name +} diff --git a/modules/virtual_network/main.tf b/modules/virtual_network/main.tf new file mode 100644 index 0000000..b1162f7 --- /dev/null +++ b/modules/virtual_network/main.tf @@ -0,0 +1,28 @@ +resource "azurerm_virtual_network" "vnet" { + name = var.vnet_name + location = var.az_location + resource_group_name = var.az_rg_name + address_space = var.vnet_address_space + tags = var.tags +} + +resource "azurerm_subnet" "subnet" { + name = var.subnet_name + resource_group_name = var.az_rg_name + virtual_network_name = azurerm_virtual_network.vnet.name + address_prefixes = var.subnet_prefix + + service_endpoints = [ + "Microsoft.KeyVault", + "Microsoft.Web" + ] + + delegation { + name = "delegation" + + service_delegation { + name = "Microsoft.Web/serverFarms" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + } +} diff --git a/modules/virtual_network/outputs.tf b/modules/virtual_network/outputs.tf new file mode 100644 index 0000000..ec0b69c --- /dev/null +++ b/modules/virtual_network/outputs.tf @@ -0,0 +1,3 @@ +output "subnet_id" { + value = azurerm_subnet.subnet.id +} diff --git a/modules/virtual_network/variables.tf b/modules/virtual_network/variables.tf new file mode 100644 index 0000000..65f822b --- /dev/null +++ b/modules/virtual_network/variables.tf @@ -0,0 +1,34 @@ +variable "vnet_name" { + description = "Name of the Virtual Network" + type = string +} + +variable "az_location" { + description = "Location of the Virtual Network" + type = string +} + +variable "az_rg_name" { + description = "Resource Group Name" + type = string +} + +variable "vnet_address_space" { + description = "Address space for the Virtual Network" + type = list(string) +} + +variable "subnet_name" { + description = "Name of the Subnet" + type = string +} + +variable "subnet_prefix" { + description = "Address prefix for the Subnet" + type = list(string) +} + +variable "tags" { + description = "Tags to apply to resources" + type = map(string) +}