diff --git a/Makefile b/Makefile index ef4c304a..18a63605 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,7 @@ lint: upgrade: @make -C cicd/helm upgrade + @make -C cicd/terraform upgrade @make -C src/conversation-api upgrade @make -C src/conversation-ui upgrade diff --git a/cicd/terraform/.gitignore b/cicd/terraform/.gitignore new file mode 100644 index 00000000..aae9cbba --- /dev/null +++ b/cicd/terraform/.gitignore @@ -0,0 +1,43 @@ +# Local Terraform tfvars file +.tfvars.json + +# Created by https://www.toptal.com/developers/gitignore/api/terraform +# Edit at https://www.toptal.com/developers/gitignore?templates=terraform + +### Terraform ### +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +# End of https://www.toptal.com/developers/gitignore/api/terraform diff --git a/cicd/terraform/.terraform.lock.hcl b/cicd/terraform/.terraform.lock.hcl new file mode 100644 index 00000000..b0e87047 --- /dev/null +++ b/cicd/terraform/.terraform.lock.hcl @@ -0,0 +1,82 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/azuread" { + version = "2.46.0" + constraints = "~> 2.0" + hashes = [ + "h1:ZlHAGqxOj/YJHNX5i/NKfAbqBD2e09UlXwiOubJDXfk=", + "zh:1b9bfe88b02684dca86591ed595d2d9e2ca4fdd146812cbf2baea4260aa1cf45", + "zh:1c3e89cf19118fc07d7b04257251fc9897e722c16e0a0df7b07fcd261f8c12e7", + "zh:1d13a97907cab6cbc104686f2c50b9d9f90baf3f0aaf767dc7b09e2d6890c519", + "zh:2295c497feb55bd614b7ec81944c62f615dbea14bbab13fa5bc942a172435601", + "zh:30b289c45f2917c48d3b299ca57fe3c60f1d23c711db1ad02eb1c3397ead6cfc", + "zh:3c8f8c95e05c8bf20f54db4d8e69c4a50256547440fec951afabe405cf9732bd", + "zh:47a643662a336e35015e9582bd0ceb75980ba229f75a3dc45b7819c3b831c9a4", + "zh:82cd21a53d40967e83789fa0cb2d6853b8b2f76aab13fad7a859dc50f2e7101c", + "zh:aded4409a899d4f4be43021f672f762ecf99e90c852f1d58886edcbbf0d7200e", + "zh:d089206976d986a44ef6d43d883a1aba9828f64db6f7a9427c74152d0a8ea8dc", + "zh:e40e35454c5f1074b7642de11069ad1221b6ddba3d56e1d1e24fc459481f7ea3", + "zh:eabea628fe8062c912626d33d221b261f2e657a0423d45f09ea2e36119201a17", + ] +} + +provider "registry.terraform.io/hashicorp/azurerm" { + version = "3.82.0" + constraints = "~> 3.0" + hashes = [ + "h1:wIfGA8icS9CbbMfHQFRAUddF2/emdmvJbkTZxfEJTXg=", + "zh:2042c5485476b0b9dbcebfc01d95e1cec50b37b2c443ffd9824a4fc6a7b293bd", + "zh:3fc6753c039bac1866b90f5faf5f72edc7470cb64c1e84f830e71931dfced865", + "zh:4760c4595a5e8a07c5eef08304877909f88252c1536432e443211ae668456459", + "zh:4886aadcafcd88d036c3e36019ca3b0b39a6b7cfbd34f88fe8a544ca337af14f", + "zh:631602a5e38cb3ee8f8a2a2257e669f41ff05766a774eb19933d54ae1832c100", + "zh:6c03c113c729614598cb197415a3dd7b7d0fcb0aec9055b0491f02274e244582", + "zh:95390bd4037f695329a38ff1a736e4f03c134097d19201f89c5e6c08a11becb9", + "zh:ac1a1dd5559c53f72f89320f0cd9aeb8fc84ad1351b6578f99efc07c16486a2a", + "zh:b96d4ca8a05a1903accd5c1b16109444d05868198a4698e31c17ef7ab95c4541", + "zh:ccc5895c0579e4f5b0dc2d358579d417c21281104591f1877525d87f31079f96", + "zh:e0c496ab8f07ea381bbed86eefde47480b9b156bb022b2215c94cb01779c7076", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.5.1" + constraints = "~> 3.0" + hashes = [ + "h1:IL9mSatmwov+e0+++YX2V6uel+dV6bn+fC/cnGDK3Ck=", + "zh:04e3fbd610cb52c1017d282531364b9c53ef72b6bc533acb2a90671957324a64", + "zh:119197103301ebaf7efb91df8f0b6e0dd31e6ff943d231af35ee1831c599188d", + "zh:4d2b219d09abf3b1bb4df93d399ed156cadd61f44ad3baf5cf2954df2fba0831", + "zh:6130bdde527587bbe2dcaa7150363e96dbc5250ea20154176d82bc69df5d4ce3", + "zh:6cc326cd4000f724d3086ee05587e7710f032f94fc9af35e96a386a1c6f2214f", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:b6d88e1d28cf2dfa24e9fdcc3efc77adcdc1c3c3b5c7ce503a423efbdd6de57b", + "zh:ba74c592622ecbcef9dc2a4d81ed321c4e44cddf7da799faa324da9bf52a22b2", + "zh:c7c5cde98fe4ef1143bd1b3ec5dc04baf0d4cc3ca2c5c7d40d17c0e9b2076865", + "zh:dac4bad52c940cd0dfc27893507c1e92393846b024c5a9db159a93c534a3da03", + "zh:de8febe2a2acd9ac454b844a4106ed295ae9520ef54dc8ed2faf29f12716b602", + "zh:eab0d0495e7e711cca367f7d4df6e322e6c562fc52151ec931176115b83ed014", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.0.5" + constraints = "~> 4.0" + hashes = [ + "h1:zeG5RmggBZW/8JWIVrdaeSJa0OG62uFX5HY1eE8SjzY=", + "zh:01cfb11cb74654c003f6d4e32bbef8f5969ee2856394a96d127da4949c65153e", + "zh:0472ea1574026aa1e8ca82bb6df2c40cd0478e9336b7a8a64e652119a2fa4f32", + "zh:1a8ddba2b1550c5d02003ea5d6cdda2eef6870ece86c5619f33edd699c9dc14b", + "zh:1e3bb505c000adb12cdf60af5b08f0ed68bc3955b0d4d4a126db5ca4d429eb4a", + "zh:6636401b2463c25e03e68a6b786acf91a311c78444b1dc4f97c539f9f78de22a", + "zh:76858f9d8b460e7b2a338c477671d07286b0d287fd2d2e3214030ae8f61dd56e", + "zh:a13b69fb43cb8746793b3069c4d897bb18f454290b496f19d03c3387d1c9a2dc", + "zh:a90ca81bb9bb509063b736842250ecff0f886a91baae8de65c8430168001dad9", + "zh:c4de401395936e41234f1956ebadbd2ed9f414e6908f27d578614aaa529870d4", + "zh:c657e121af8fde19964482997f0de2d5173217274f6997e16389e7707ed8ece8", + "zh:d68b07a67fbd604c38ec9733069fbf23441436fecf554de6c75c032f82e1ef19", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/cicd/terraform/Makefile b/cicd/terraform/Makefile new file mode 100644 index 00000000..58fe8f79 --- /dev/null +++ b/cicd/terraform/Makefile @@ -0,0 +1,13 @@ +.PHONY: install deploy dry-run + +install: + terraform init + +upgrade: + terraform init -upgrade + +deploy: + terraform apply -auto-approve -var-file=.tfvars.json + +dry-run: + terraform plan -var-file=.tfvars.json diff --git a/cicd/terraform/main.tf b/cicd/terraform/main.tf new file mode 100644 index 00000000..f1c478cb --- /dev/null +++ b/cicd/terraform/main.tf @@ -0,0 +1,719 @@ +locals { + tags = { + app = "Private GPT" + sources = "https://github.com/clemlesne/private-gpt" + } + custom_domain_verification_id = uuid() +} + +data "azurerm_client_config" "current" {} + +data "azuread_client_config" "current" {} + +resource "random_string" "dns_prefix" { + length = 8 + lower = true + numeric = false + special = false + upper = false +} + +resource "azurerm_user_assigned_identity" "identity" { + location = azurerm_resource_group.this.location + name = var.resourcePrefix + resource_group_name = azurerm_resource_group.this.name + + tags = local.tags +} + +resource "azuread_application" "this" { + description = "Deploy smart and secure conversational agents for your employees, using Azure. Able to use both private and public data." + display_name = "Private GPT (${var.resourcePrefix})" + logo_image = filebase64("${path.root}/../../logo-512.png") + owners = [data.azuread_client_config.current.object_id] + sign_in_audience = "AzureADandPersonalMicrosoftAccount" + support_url = "https://github.com/clemlesne/private-gpt/issues" + + api { + requested_access_token_version = 2 + } + + single_page_application { + redirect_uris = ["https://${azurerm_public_ip.this.fqdn}/"] + } + + web { + homepage_url = "https://${azurerm_public_ip.this.fqdn}" + + implicit_grant { + id_token_issuance_enabled = true + } + } + + required_resource_access { + resource_app_id = "00000003-0000-0000-c000-000000000000" # Microsoft Graph + + resource_access { + id = "14dad69e-099b-42c9-810b-d002981feec1" + type = "Scope" + } + + resource_access { + id = "e1fe6dd8-ba31-4d61-89e7-88639da4683d" + type = "Scope" + } + + resource_access { + id = "37f7f235-527c-4136-accd-4a02d197296e" + type = "Scope" + } + + resource_access { + id = "64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0" + type = "Scope" + } + } +} + +resource "azurerm_resource_group" "this" { + location = var.location + name = var.resourcePrefix + tags = local.tags +} + +resource "azurerm_public_ip" "this" { + location = azurerm_resource_group.this.location + name = var.resourcePrefix + resource_group_name = azurerm_resource_group.this.name + + allocation_method = "Static" + domain_name_label = random_string.dns_prefix.result + ip_version = "IPv4" + + tags = local.tags +} + +resource "azurerm_dns_zone" "this" { + name = azurerm_public_ip.this.fqdn + resource_group_name = azurerm_resource_group.this.name + + tags = local.tags +} + +resource "azurerm_search_service" "search" { + location = azurerm_resource_group.this.location + name = var.resourcePrefix + resource_group_name = azurerm_resource_group.this.name + + sku = "basic" + + tags = local.tags +} + +resource "azurerm_cognitive_account" "form_recognizer" { + location = azurerm_resource_group.this.location + name = "${var.resourcePrefix}-form-recognizer" + resource_group_name = azurerm_resource_group.this.name + + custom_subdomain_name = "${var.resourcePrefix}-form-recognizer" + kind = "FormRecognizer" # Only one free account is allowed + sku_name = "S0" + + tags = local.tags +} + +# data "azapi_resource" "bing" { +# # name = "${var.resourcePrefix}-bing" +# name = "private-gpt" +# parent_id = azurerm_resource_group.this.id +# type = "Microsoft.Bing/accounts" +# } + +# resource "azapi_resource" "bing" { +# location = "global" +# name = "${var.resourcePrefix}-bing" +# parent_id = azurerm_resource_group.this.id +# type = "Microsoft.Bing/accounts" + +# body = jsonencode({ +# kind = "Bing.Search.v7" +# sku = { +# name = "F1" +# } +# }) +# } + +# resource "azurerm_cognitive_account" "bing" { +# location = "global" +# name = "${var.resourcePrefix}-bing" +# resource_group_name = azurerm_resource_group.this.name + +# kind = "Bing.Search.v7" +# sku_name = "F1" + +# tags = local.tags +# } + +resource "azurerm_cognitive_account" "openai" { + location = azurerm_resource_group.this.location + name = "${var.resourcePrefix}-openai" + resource_group_name = azurerm_resource_group.this.name + + custom_subdomain_name = "${var.resourcePrefix}-openai" + kind = "OpenAI" + sku_name = "S0" + + tags = local.tags +} + +resource "azurerm_cognitive_deployment" "openai_ada" { + name = "ada" + + cognitive_account_id = azurerm_cognitive_account.openai.id + + model { + format = "OpenAI" + name = "text-embedding-ada-002" + version = "2" + } + + scale { + type = "Standard" + } +} + +resource "azurerm_cognitive_deployment" "openai_gpt" { + name = "gpt" + + cognitive_account_id = azurerm_cognitive_account.openai.id + + model { + format = "OpenAI" + name = "gpt-35-turbo-16k" + version = "0613" + } + + scale { + type = "Standard" + } +} + +resource "azurerm_role_assignment" "openai_contributor" { + principal_id = azurerm_user_assigned_identity.identity.principal_id + role_definition_name = "Cognitive Services OpenAI Contributor" + scope = azurerm_cognitive_account.openai.id +} + +resource "azurerm_cognitive_account" "content_safety" { + location = azurerm_resource_group.this.location + name = "${var.resourcePrefix}-content-safety" + resource_group_name = azurerm_resource_group.this.name + + custom_subdomain_name = "${var.resourcePrefix}-content-safety" + kind = "ContentModerator" + sku_name = "S0" + + tags = local.tags +} + +resource "azurerm_redis_cache" "this" { + location = azurerm_resource_group.this.location + name = var.resourcePrefix + resource_group_name = azurerm_resource_group.this.name + + capacity = 0 + family = "C" + sku_name = "Basic" + + tags = local.tags +} + +resource "azurerm_role_assignment" "redis_contributor" { + principal_id = azurerm_user_assigned_identity.identity.principal_id + role_definition_name = "Contributor" + scope = azurerm_redis_cache.this.id +} + +resource "azurerm_cosmosdb_account" "this" { + location = azurerm_resource_group.this.location + name = var.resourcePrefix + resource_group_name = azurerm_resource_group.this.name + + local_authentication_disabled = true + offer_type = "Standard" + + consistency_policy { + consistency_level = "ConsistentPrefix" + } + + capabilities { + name = "EnableServerless" + } + + geo_location { + location = azurerm_resource_group.this.location + failover_priority = 0 + } + + tags = local.tags +} + +resource "azurerm_cosmosdb_sql_database" "this" { + account_name = azurerm_cosmosdb_account.this.name + name = "db" + resource_group_name = azurerm_resource_group.this.name +} + +resource "azurerm_cosmosdb_sql_container" "conversation" { + account_name = azurerm_cosmosdb_account.this.name + database_name = azurerm_cosmosdb_sql_database.this.name + name = "conversation" + resource_group_name = azurerm_resource_group.this.name + + partition_key_path = "/user_id" +} + +resource "azurerm_cosmosdb_sql_container" "message" { + account_name = azurerm_cosmosdb_account.this.name + database_name = azurerm_cosmosdb_sql_database.this.name + name = "message" + resource_group_name = azurerm_resource_group.this.name + + partition_key_path = "/conversation_id" +} + +resource "azurerm_cosmosdb_sql_container" "usage" { + account_name = azurerm_cosmosdb_account.this.name + database_name = azurerm_cosmosdb_sql_database.this.name + name = "usage" + resource_group_name = azurerm_resource_group.this.name + + partition_key_path = "/user_id" +} + +resource "azurerm_cosmosdb_sql_container" "user" { + account_name = azurerm_cosmosdb_account.this.name + database_name = azurerm_cosmosdb_sql_database.this.name + name = "user" + resource_group_name = azurerm_resource_group.this.name + + partition_key_path = "/dummy" +} + +// Using built-in role definition +// See: https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#built-in-role-definitions +data "azurerm_cosmosdb_sql_role_definition" "data_contributor" { + account_name = azurerm_cosmosdb_account.this.name + resource_group_name = azurerm_resource_group.this.name + role_definition_id = "00000000-0000-0000-0000-000000000002" +} + +resource "azurerm_cosmosdb_sql_role_assignment" "this" { + account_name = azurerm_cosmosdb_account.this.name + resource_group_name = azurerm_resource_group.this.name + scope = azurerm_cosmosdb_account.this.id + + principal_id = azurerm_user_assigned_identity.identity.principal_id + role_definition_id = data.azurerm_cosmosdb_sql_role_definition.data_contributor.id +} + +resource "azurerm_log_analytics_workspace" "this" { + location = azurerm_resource_group.this.location + name = var.resourcePrefix + resource_group_name = azurerm_resource_group.this.name + + retention_in_days = 30 + sku = "PerGB2018" + + tags = local.tags +} + +resource "azurerm_application_insights" "this" { + location = azurerm_resource_group.this.location + name = var.resourcePrefix + resource_group_name = azurerm_resource_group.this.name + + application_type = "web" + workspace_id = azurerm_log_analytics_workspace.this.id + + tags = local.tags +} + +resource "azurerm_key_vault" "this" { + location = azurerm_resource_group.this.location + name = var.resourcePrefix + resource_group_name = azurerm_resource_group.this.name + tenant_id = data.azurerm_client_config.current.tenant_id + + enable_rbac_authorization = true + sku_name = "standard" +} + +resource "azurerm_role_assignment" "key_vault_administrator" { + principal_id = azurerm_user_assigned_identity.identity.principal_id + role_definition_name = "Key Vault Administrator" + scope = azurerm_key_vault.this.id +} + +resource "azurerm_key_vault_certificate" "this" { + name = "certificate" + key_vault_id = azurerm_key_vault.this.id + + certificate_policy { + issuer_parameters { + name = "Self" + } + + key_properties { + exportable = true + key_size = 4096 + key_type = "RSA" + reuse_key = true + } + + lifetime_action { + action { + action_type = "AutoRenew" + } + + trigger { + lifetime_percentage = 90 + } + } + + secret_properties { + content_type = "application/x-pem-file" + } + + x509_certificate_properties { + extended_key_usage = ["1.3.6.1.5.5.7.3.1"] # Server authentication + + key_usage = [ + "cRLSign", + "dataEncipherment", + "digitalSignature", + "keyAgreement", + "keyCertSign", + "keyEncipherment", + ] + + subject_alternative_names { + dns_names = [ + "*.${azurerm_public_ip.this.fqdn}", + azurerm_public_ip.this.fqdn, + ] + } + + subject = "CN=${azurerm_public_ip.this.fqdn}" + validity_in_months = 12 # 1 year + } + } +} + +data "azurerm_key_vault_secret" "this" { + key_vault_id = azurerm_key_vault.this.id + name = azurerm_key_vault_certificate.this.name + version = azurerm_key_vault_certificate.this.version +} + +resource "azurerm_container_app_environment" "this" { + location = azurerm_resource_group.this.location + name = var.resourcePrefix + resource_group_name = azurerm_resource_group.this.name + + log_analytics_workspace_id = azurerm_log_analytics_workspace.this.id + + tags = local.tags +} + +resource "azurerm_container_app_environment_certificate" "this" { + container_app_environment_id = azurerm_container_app_environment.this.id + name = "default" + + certificate_blob_base64 = base64encode(data.azurerm_key_vault_secret.this.value) + certificate_password = "" +} + +resource "azurerm_dns_a_record" "container_app" { + resource_group_name = azurerm_resource_group.this.name + zone_name = azurerm_dns_zone.this.name + + name = "@" + records = [azurerm_container_app_environment.this.static_ip_address] + ttl = 3600 +} + +resource "azurerm_container_app" "conversation_ui" { + container_app_environment_id = azurerm_container_app_environment.this.id + name = "conversation-ui" + resource_group_name = azurerm_resource_group.this.name + + revision_mode = "Single" + + template { + container { + cpu = 0.25 + image = "ghcr.io/clemlesne/private-gpt/conversation-ui:develop" + memory = "0.5Gi" + name = "conversation-ui" + + liveness_probe { + initial_delay = 5 + interval_seconds = 5 + path = "/health/liveness" + port = 8080 + transport = "HTTP" + } + + startup_probe { + failure_count_threshold = 10 + interval_seconds = 5 + port = 8080 + transport = "TCP" + } + + volume_mounts { + name = "tmp" + path = "/tmp" + } + + env { + name = "API_HOST" + value = "https://${azurerm_public_ip.this.fqdn}/api" + } + + env { + name = "PWA_HOST" + value = "https://${azurerm_public_ip.this.fqdn}" + } + + env { + name = "OIDC_API_AUDIENCE" + value = azuread_application.this.application_id + } + + env { + name = "AZURE_APP_INSIGHTS_CONNECTION_STR" + value = azurerm_application_insights.this.instrumentation_key + } + } + + volume { + name = "tmp" + storage_type = "EmptyDir" + } + } + + ingress { + external_enabled = true + target_port = 8080 + + custom_domain { + certificate_id = azurerm_container_app_environment_certificate.this.id + name = azurerm_public_ip.this.fqdn + } + + traffic_weight { + percentage = 100 + } + } + + registry { + password_secret_name = "ghcr" + server = "ghcr.io" + username = "clemlesne" + } + + secret { + name = "ghcr" + value = var.ghcrToken + } + + identity { + type = "UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.identity.id, + ] + } + + tags = local.tags +} + +resource "azurerm_container_app" "conversation_api" { + container_app_environment_id = azurerm_container_app_environment.this.id + name = "conversation-api" + resource_group_name = azurerm_resource_group.this.name + + revision_mode = "Single" + + template { + container { + cpu = 0.25 + image = "ghcr.io/clemlesne/private-gpt/conversation-api:develop" + memory = "0.5Gi" + name = "conversation-api" + + liveness_probe { + initial_delay = 5 + interval_seconds = 5 + path = "/health/liveness" + port = 8080 + transport = "HTTP" + } + + startup_probe { + failure_count_threshold = 10 + interval_seconds = 5 + port = 8080 + transport = "TCP" + } + + readiness_probe { + interval_seconds = 15 + path = "/health/readiness" + port = 8080 + timeout = 5 + transport = "HTTP" + } + + volume_mounts { + name = "tmp" + path = "/tmp" + } + + env { + name = "TMPDIR" + value = "/tmp" + } + + env { + name = "PG_CONFIG_JSON" + value = jsonencode({ + api = { + root_path = "/api" + } + oidc = { + algorithms = [ + "RS256" + ] + api_audience = azuread_application.this.application_id + issuers = [ + "https://login.microsoftonline.com/${data.azuread_client_config.current.tenant_id}/v2.0" + ] + jwks = "https://login.microsoftonline.com/common/discovery/v2.0/keys" + } + monitoring = { + logging = { + app_level = "DEBUG" + sys_level = "WARN" + } + azure_app_insights = { + connection_str = azurerm_application_insights.this.instrumentation_key + } + } + persistence = { + cache = { + type = "redis" + config = { + db = 0 + host = azurerm_redis_cache.this.hostname + } + } + search = { + type = "qdrant" + config = { + host = "[host]" + } + } + store = { + type = "cosmos" + config = { + url = azurerm_cosmosdb_account.this.endpoint + database = azurerm_cosmosdb_sql_database.this.name + } + } + stream = { + type = "redis" + config = { + db = 0 + host = azurerm_redis_cache.this.hostname + } + } + } + ai = { + openai = { + ada_deploy_id = azurerm_cognitive_deployment.openai_ada.name + ada_max_tokens = 2049 + api_base = azurerm_cognitive_account.openai.endpoint + gpt_deploy_id = azurerm_cognitive_deployment.openai_gpt.name + gpt_max_tokens = 4096 + } + azure_content_safety = { + api_base = azurerm_cognitive_account.content_safety.endpoint + api_token = azurerm_cognitive_account.content_safety.primary_access_key + max_length = 1000 + } + } + tools = { + azure_form_recognizer = { + api_base = azurerm_cognitive_account.form_recognizer.endpoint + api_token = azurerm_cognitive_account.form_recognizer.primary_access_key + } + bing = { + search_url = var.bingSearchUrl + subscription_key = var.bingSubscriptionKey + } + tmdb = { + bearer_token = var.tmdbBearerToken + } + news = { + api_key = var.newsApiKey + } + listen_notes = { + api_key = var.listenNotesApiKey + } + } + }) + } + } + + volume { + name = "tmp" + storage_type = "EmptyDir" + } + } + + ingress { + external_enabled = true + target_port = 8080 + + custom_domain { + certificate_id = azurerm_container_app_environment_certificate.this.id + name = "api.${azurerm_public_ip.this.fqdn}" + } + + traffic_weight { + percentage = 100 + } + } + + registry { + password_secret_name = "ghcr" + server = "ghcr.io" + username = "clemlesne" + } + + secret { + name = "ghcr" + value = var.ghcrToken + } + + identity { + type = "UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.identity.id + ] + } + + tags = local.tags +} diff --git a/cicd/terraform/outputs.tf b/cicd/terraform/outputs.tf new file mode 100644 index 00000000..e69de29b diff --git a/cicd/terraform/provider.tf b/cicd/terraform/provider.tf new file mode 100644 index 00000000..d2b2d7e8 --- /dev/null +++ b/cicd/terraform/provider.tf @@ -0,0 +1,28 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 3.0" + } + azuread = { + source = "hashicorp/azuread" + version = "~> 2.0" + } + tls = { + source = "hashicorp/tls" + version = "~> 4.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.0" + } + } +} + +provider "azurerm" { + features { + resource_group { + prevent_deletion_if_contains_resources = false + } + } +} diff --git a/cicd/terraform/variables.tf b/cicd/terraform/variables.tf new file mode 100644 index 00000000..5e364a4e --- /dev/null +++ b/cicd/terraform/variables.tf @@ -0,0 +1,49 @@ +variable "resourcePrefix" { + description = "Resources prefix" + validation { + condition = can(regex("^[a-z][a-zA-Z0-9-]{2,}$", var.resourcePrefix)) + error_message = "Resource prefix must be at least 3 characters long and start with a lowercase letter" + } +} + +variable "location" { + // TODO: Add description +} + +variable "tmdbBearerToken" { + // TODO: Add description + sensitive = true +} + +variable "newsApiKey" { + // TODO: Add description + sensitive = true +} + +variable "listenNotesApiKey" { + // TODO: Add description + sensitive = true +} + +variable "openWeatherMapApiKey" { + // TODO: Add description + sensitive = true +} + +variable "oidcApiAudience" { + // TODO: Add description +} + +variable "ghcrToken" { + // TODO: Add description + sensitive = true +} + +variable "bingSearchUrl" { + // TODO: Add description +} + +variable "bingSubscriptionKey" { + // TODO: Add description + sensitive = true +}