diff --git a/terraform/implementation/aca/README.md b/terraform/implementation/aca/README.md new file mode 100644 index 000000000..9887fa274 --- /dev/null +++ b/terraform/implementation/aca/README.md @@ -0,0 +1,61 @@ +# Data Integration Building Blocks Azure Templates + +# Overview + +The Data Integration Building Blocks (DIBBs) project is an effort to help state, local, territorial, and tribal public health departments better make sense of and utilize their data. You can read more about the project on the [main DIBBs repository](https://github.com/CDCgov/phdi/blob/main/README.md). + +This repository is intended to provide an operational starting point for instantiating a fully-incorporated, self-sustained Azure environment that will run the DIBBs eCR Viewer pipeline. It can be deployed in full for users who wish to start an environment from scratch, or its several parts can be used to augment an existing Azure environment. + +# Prerequisites + +## Worker/Agent Prerequisites +The machine or deployment agent that will run this code must have the following tools installed: +* Terraform version 1.7.4 or later. +* Microsoft Azure CLI +* Docker Engine with standard Unix socket configuration (custom locations will require modification of configuration files) +* Git + +## Cloud Prerequisites +The target Azure subscription must have the following: +* An existing Resource Group and Storage Account to store Terraform state files +* A Service Principal with Contributor access to the Resource Group (or an engineer with equivalent or greater access, if running from a personal machine) +* Active Azure resource providers for the services being deployed (e.g., Azure App Gateway, Azure Container Apps, etc.) + +# Deployment +User-modifiable code exists in the `implementation_aca` folder. Be sure to review `_config.tf` and `main.tf` for variables and inputs that can be customized to fit your installation. + +Before you deploy, ensure that you have the prerequisites installed and configured. Then, run the following commands in the `implementation_aca` folder: + +```bash +terraform init +terraform plan +``` +Ensure there are no configuration errors, and review the plan output to confirm that the resources to be created are as expected. If everything looks good, run: + +```bash +terraform apply +``` + +# Deployment Sample +The `dev` folder contains a working sample of a fully-configured environment. You can use this as a reference for your own deployment. + + +# Notices and Disclaimers + +This repository constitutes a work of the United States Government and is not +subject to domestic copyright protection under 17 USC § 105. This repository is in +the public domain within the United States, and copyright and related rights in +the work worldwide are waived through the [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/). +All contributions to this repository will be released under the CC0 dedication. By +submitting a pull request you are agreeing to comply with this waiver of +copyright interest. + +This source code in this repository is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the Apache Software License for more details. + +This repository is not a source of government records, but is a copy to increase collaboration and collaborative potential. All government records will be published through the [CDC web site](http://www.cdc.gov). + +Contributions to and use of this repository are governed by the [Code of Conduct](code-of-conduct.md), [License](LICENSE), and [Contributing](CONTRIBUTING.md) documents. See [DISLAIMER.md](DISLAIMER.md) for additional disclaimers and legal information. + +Please refer to [CDC's Template Repository](https://github.com/CDCgov/template) for more information about [contributing to this repository](https://github.com/CDCgov/template/blob/main/CONTRIBUTING.md), [public domain notices and disclaimers](https://github.com/CDCgov/template/blob/main/DISCLAIMER.md), and [code of conduct](https://github.com/CDCgov/template/blob/main/code-of-conduct.md). \ No newline at end of file diff --git a/terraform/implementation/aca/create_prereq_aca.md b/terraform/implementation/aca/create_prereq_aca.md new file mode 100644 index 000000000..a3be159ad --- /dev/null +++ b/terraform/implementation/aca/create_prereq_aca.md @@ -0,0 +1,89 @@ +# Azure Console Setup Instructions for Terraform Deployment + +To prepare your Azure subscription for Terraform deployments, follow the steps below to create and configure the necessary resources. + +--- + +## ✅ Prerequisites + +Ensure that the following are in place: + +- Access to the Azure portal: https://portal.azure.com +- Appropriate permissions to create and manage Azure resources +- Azure CLI (optional, but useful for automation): [Install Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) + +--- + +## 1. Create a Resource Group and Storage Account + +These will be used to store Terraform state files. + +### Step 1: Create a Resource Group + +1. Navigate to **Resource groups**. +2. Click **+ Create**. +3. Provide: + - **Subscription**: Your target subscription + - **Resource Group Name**: e.g., `terraform-rg` + - **Region**: e.g., `East US` +4. Click **Review + Create**, then **Create**. + +### Step 2: Create a Storage Account + +1. Go to **Storage accounts**. +2. Click **+ Create**. +3. Provide: + - **Subscription**: Same as above + - **Resource Group**: Select the one created earlier + - **Storage account name**: e.g., `tfstate12345` (must be globally unique) + - **Region**: Same as the resource group + - **Performance**: Standard + - **Redundancy**: Locally-redundant storage (LRS) +4. Click **Review + Add Create**, then **Create**. + +### Step 3: Create a Blob Container + +1. Open the created storage account. +2. Navigate to **Containers** under **Data storage**. +3. Click **+ Container**. +4. Enter a name (e.g., `tfstate`) and set **Public access level** to **Private**. +5. Click **Create**. + +--- + +## 2. Engineer Access + +This allows Terraform to authenticate against Azure. + +### Use an Existing Engineer Account + +If running Terraform locally, ensure your Azure account has at least **Contributor** access to the target resource group. + +--- + +## 3. Register Required Azure Resource Providers + +Ensure that all necessary resource providers are registered, especially if deploying services like App Gateway or Container Apps. + +### Steps: + +1. Go to **Subscriptions** in the Azure portal. +2. Select your subscription. +3. Click **Resource providers** in the left menu. +4. Search for and register the following (examples): + - `Microsoft.App` + - `Microsoft.ContainerInstance` + - `Microsoft.Network` + - `Microsoft.OperationalInsights` +5. Click **Register** for each as needed. + +--- + +## ✅ Summary + +You should now have: +- A resource group and storage account for Terraform state +- An Engineer that has access with Contributor rights +- All required resource providers registered + +You’re ready to deploy with Terraform! diff --git a/terraform/implementation/aca/example_deployment/_config.tf b/terraform/implementation/aca/example_deployment/_config.tf new file mode 100644 index 000000000..376a0e0d9 --- /dev/null +++ b/terraform/implementation/aca/example_deployment/_config.tf @@ -0,0 +1,20 @@ +terraform { + backend "azurerm" { + resource_group_name = "" + storage_account_name = "dibbsstatestorage" + container_name = "ce-tfstate" + key = "dev/terraform.tfstate" + } + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 3.116.0" + } + } + required_version = "~> 1.7.4" +} + +provider "azurerm" { + features {} + skip_provider_registration = true +} \ No newline at end of file diff --git a/terraform/implementation/aca/example_deployment/_data.tf b/terraform/implementation/aca/example_deployment/_data.tf new file mode 100644 index 000000000..bb087fd19 --- /dev/null +++ b/terraform/implementation/aca/example_deployment/_data.tf @@ -0,0 +1 @@ +data "azurerm_client_config" "current" {} \ No newline at end of file diff --git a/terraform/implementation/aca/example_deployment/main.tf b/terraform/implementation/aca/example_deployment/main.tf new file mode 100644 index 000000000..b6138cb77 --- /dev/null +++ b/terraform/implementation/aca/example_deployment/main.tf @@ -0,0 +1,62 @@ +locals { + team = "qc-client" //Update this to match your chosen prefix + project = "qc" + env = "dev" + location = "eastus" //Update this to match your chosen region +} + +module "foundations" { + source = "../resources/foundations" + team = local.team + project = local.project + env = local.env + location = local.location +} + +module "networking" { + source = "../resources/networking" + team = local.team + project = local.project + env = local.env + location = local.location + resource_group_name = module.foundations.resource_group_name + + //These can be configured to match your network requirements. + //We recommend /24 at minumum for the network address space, + //and /25 for the ACA subnet. (Allows for 58 individual nodes) + network_address_space = ["10.30.0.0/24"] + aca_subnet_address_prefixes = ["10.30.0.0/25"] + app_gateway_subnet_address_prefixes = ["10.30.0.128/26"] +} + +module "container_apps" { + source = "../resources/aca" + team = local.team + project = local.project + env = local.env + location = local.location + resource_group_name = module.foundations.resource_group_name + + aca_subnet_id = module.networking.subnet_aca_id + appgw_subnet_id = module.networking.subnet_appgw_id + vnet_id = module.networking.network.id + + acr_url = module.foundations.acr_url + acr_username = module.foundations.acr_admin_username //TODO: Change to an ACA-specific password + acr_password = module.foundations.acr_admin_password //TODO: Change to an ACA-specific password + + qc_version = "0.9.1" + + azure_storage_connection_string = module.foundations.azure_storage_connection_string + azure_container_name = module.foundations.azure_container_name + + + nextauth_url = "https://${local.team}-${local.project}-${local.env}.${local.location}.cloudapp.azure.com/ecr-viewer/api/auth" + + key_vault_id = "" //Update this to match your target key vault. Follows the longform format: "/subscriptions/{subscription-id}/resourceGroups/{resource-group-name}/providers/Microsoft.KeyVault/vaults/{key-vault-name}" + + + use_ssl = false //Set this to false if you do not want to use SSL for the ACA gateway. + + pre_assigned_identity_id = "" //Set to the ID of a user-assigned managed identity for your gateway if you want to use one. Follows the longform format: "/subscriptions/{subscription-id}/resourceGroups/{resource-group-name}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identity-name}" +} \ No newline at end of file diff --git a/terraform/implementation/aca/example_deployment/setup.sh b/terraform/implementation/aca/example_deployment/setup.sh new file mode 100644 index 000000000..8ca9d4075 --- /dev/null +++ b/terraform/implementation/aca/example_deployment/setup.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# If you're running this code from an M-series processor or other ARM variant, you may need to specify the container platform manually. +export DOCKER_DEFAULT_PLATFORM=linux/amd64 + +# This section provides an example of targeted deployments, if required for your installation. +terraform apply -target=module.foundations -target=module.networking +terraform apply -target=module.container_apps \ No newline at end of file diff --git a/terraform/implementation/aca/implementation_aca/_config.tf b/terraform/implementation/aca/implementation_aca/_config.tf new file mode 100644 index 000000000..29022a1f7 --- /dev/null +++ b/terraform/implementation/aca/implementation_aca/_config.tf @@ -0,0 +1,27 @@ +/* + * The Terraform Azure backend requires a pre-existing Azure Storage Account and a container to store the state file. If you do not already + * have a pre-configured resource for this, we recommend setting one up manually. + * + * WARNING: Make sure you don't use the same resource group for your state storage and your DIBBs resources to avoid accidental deletion. + * DIBBs resources should be deployed in their own resource group. + */ +terraform { + backend "azurerm" { + resource_group_name = "qc-aca-rg" // TODO: Change this to match the resource group that contains your storage account for Terraform state storage. + storage_account_name = "qcacastorageaccount" // TODO: Change this to match the storage account that contains/will contain your Terraform state files. + container_name = "tfstate" // We recommend leaving this alone, to keep DIBBs state files separate from the rest of your resources. + key = "dev/terraform.tfstate" // TODO: Change the prefix to match the environment you are working in. + } + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 3.116.0" + } + } + required_version = "~> 1.9.8" +} + +provider "azurerm" { + features {} + skip_provider_registration = true +} \ No newline at end of file diff --git a/terraform/implementation/aca/implementation_aca/_data.tf b/terraform/implementation/aca/implementation_aca/_data.tf new file mode 100644 index 000000000..bb087fd19 --- /dev/null +++ b/terraform/implementation/aca/implementation_aca/_data.tf @@ -0,0 +1 @@ +data "azurerm_client_config" "current" {} \ No newline at end of file diff --git a/terraform/implementation/aca/implementation_aca/main.tf b/terraform/implementation/aca/implementation_aca/main.tf new file mode 100644 index 000000000..8b07d84e6 --- /dev/null +++ b/terraform/implementation/aca/implementation_aca/main.tf @@ -0,0 +1,54 @@ +locals { + team = "" // TODO: Change this to match your implementation team's name or internal asset code. + project = "dibbs" + env = "dev" // TODO: Change this to match your desired environment (e.g., dev, test, prod). Make sure to vary this between environments. + location = "eastus" // TODO: Change this to match your desired Azure region. +} + +module "foundations" { + source = "../resources/foundations" + team = local.team + project = local.project + env = local.env + location = local.location +} + +module "networking" { + source = "../resources/networking" + team = local.team + project = local.project + env = local.env + location = local.location + resource_group_name = module.foundations.resource_group_name + + network_address_space = ["10.30.0.0/24"] + aca_subnet_address_prefixes = ["10.30.0.0/25"] + app_gateway_subnet_address_prefixes = ["10.30.0.128/26"] +} + +module "container_apps" { + source = "../resources/aca" + team = local.team + project = local.project + env = local.env + location = local.location + resource_group_name = module.foundations.resource_group_name + + aca_subnet_id = module.networking.subnet_aca_id + appgw_subnet_id = module.networking.subnet_appgw_id + vnet_id = module.networking.network.id + + acr_url = module.foundations.acr_url + acr_username = module.foundations.acr_admin_username + acr_password = module.foundations.acr_admin_password + + qc_version = "0.9.1" + + azure_storage_connection_string = module.foundations.azure_storage_connection_string + azure_container_name = module.foundations.azure_container_name + + nextauth_url = "https://${local.team}-${local.project}-${local.env}.${local.location}.cloudapp.azure.com/ecr-viewer/api/auth" + + key_vault_id = "" //Update this to match your target key vault. Follows the longform format: "/subscriptions/{subscription-id}/resourceGroups/{resource-group-name}/providers/Microsoft.KeyVault/vaults/{key-vault-name}" + +} \ No newline at end of file diff --git a/terraform/implementation/aca/resources/aca/_data.tf b/terraform/implementation/aca/resources/aca/_data.tf new file mode 100644 index 000000000..4c090c3f0 --- /dev/null +++ b/terraform/implementation/aca/resources/aca/_data.tf @@ -0,0 +1,51 @@ +data "azurerm_client_config" "current" {} + +# data "azurerm_key_vault" "global_key_vault" { +# name = "skylightdibbsglobalkv" +# resource_group_name = "skylight-dibbs-global" +# } + +data "azurerm_key_vault_secret" "query_connector_db_username" { + name = "query-connector-demo-db-user" + key_vault_id = resource.azurerm_key_vault.kv.id +} + +data "azurerm_key_vault_secret" "query_connector_db_password" { + name = "query-connector-demo-db-password" + key_vault_id = resource.azurerm_key_vault.kv.id +} + +# data "azurerm_key_vault" "demo_key_vault" { +# name = "skylightdibbsdemokv" +# resource_group_name = "skylight-dibbs-global" +# } + +data "azurerm_key_vault_secret" "query_connector_umls_api_key" { + name = "query-connector-umls-api-key" + key_vault_id = resource.azurerm_key_vault.kv.id +} + +data "azurerm_key_vault_secret" "query_connector_ersd_api_key" { + name = "query-connector-ersd-api-key" + key_vault_id = resource.azurerm_key_vault.kv.id +} + +data "azurerm_key_vault_secret" "query_connector_auth_secret" { + name = "query-connector-auth-secret" + key_vault_id = resource.azurerm_key_vault.kv.id +} + +data "azurerm_key_vault_secret" "query_connector_tenant" { + name = "query-connector-${local.env}-azuread-tenant-id" + key_vault_id = resource.azurerm_key_vault.kv.id +} + +data "azurerm_key_vault_secret" "query_connector_auth_client_id" { + name = "query-connector-${local.env}-client-id" + key_vault_id = resource.azurerm_key_vault.kv.id +} + +data "azurerm_key_vault_secret" "query_connector_auth_client_secret" { + name = "query-connector-${local.env}-client-secret" + key_vault_id = resource.azurerm_key_vault.kv.id +} \ No newline at end of file diff --git a/terraform/implementation/aca/resources/aca/_output.tf b/terraform/implementation/aca/resources/aca/_output.tf new file mode 100644 index 000000000..e69de29bb diff --git a/terraform/implementation/aca/resources/aca/_var.tf b/terraform/implementation/aca/resources/aca/_var.tf new file mode 100644 index 000000000..8ebf011c6 --- /dev/null +++ b/terraform/implementation/aca/resources/aca/_var.tf @@ -0,0 +1,190 @@ +variable "team" { + description = "One-word identifier for this project's custodial team." + type = string +} + +variable "project" { + description = "One-word identifier or code name for this project." + type = string +} + +variable "env" { + description = "One-word identifier for the target environment (e.g. dev, test, prod)." + type = string +} + + +variable "resource_group_name" { + description = "The name of the resource group to deploy to" + type = string +} + +variable "aca_subnet_id" { + type = string + description = "The ID of the subnet to connect the Azure Container Apps environment to" +} + +variable "appgw_subnet_id" { + type = string + description = "The ID of the subnet to connect the App Gateway to" +} + +variable "acr_url" { + description = "The URL of the Azure Container Registry" + type = string +} + +variable "acr_username" { + description = "The username for the Azure Container Registry" + type = string +} + +variable "acr_password" { + description = "The password for the Azure Container Registry" + type = string +} + +variable "vnet_id" { + description = "The name of the virtual network to which the ACA gateway will be assigned" + type = string +} + +variable "autoscale_min" { + description = "Value for the minimum number of load balancer instances to run." + default = 0 +} +variable "autoscale_max" { + description = "Value for the maximum number of load balancer instances to run." + default = 4 +} + +variable "tags" { + description = "A map of tags to apply to all resources." + type = map(string) + default = {} +} + +variable "qc_version" { + description = "The version of the Query Connector services to deploy. Can be overridden if building blocks are on different versions." + type = string +} + +variable "ghcr_string" { + description = "The string to use for the source GitHub Container Registry" + type = string + default = "ghcr.io/cdcgov/dibbs-query-connector/" +} + +variable "azure_storage_connection_string" { + description = "The connection string for the Azure Storage account for eCR processing" + type = string +} + +variable "azure_container_name" { + description = "The name of the Azure Storage container for eCR processing" + type = string +} + + +variable "nextauth_url" { + description = "The URL for the auth service" + type = string +} + +variable "key_vault_id" { + description = "The ID of the key vault to use for secrets" + type = string +} + + + +variable "use_ssl" { + description = "Boolean to determine if SSL should be used for the eCR Viewer resources. Required for Entra/Azure Active Directory use." + type = bool + default = false +} + +variable "pre_assigned_identity_id" { + description = "The ID of the pre-assigned managed identity to use for the ACA environment" + type = string + default = "" +} + + +#ADDED QC VARAIBLES FROM dibbs-tf-envs + +variable "app_version" { + type = string + default = "0.9.1" +} + +variable "location" { + description = "The Azure region in which the associated resources will be created." + type = string + default = "eastus" +} + +variable "fhir_url" { + type = string + description = "URL for FHIR server" + default = "undefined" +} + +variable "cred_manager" { + type = string + description = "URL for Credentials Manager" + default = "undefined" +} + + +variable "auth_provider" { + type = string + description = "Which auth provider to use (microsoft-entra-id or keycloak)" + default = "microsoft-entra-id" +} + +variable "auth_client_id" { + type = string + description = "Client ID" + default = "query-connector" +} + +variable "auth_issuer" { + type = string + description = "URL for the Auth issuer (https://login.microsoftonline.com/your-tenant-id/v2.0 or keycloak)" + default = "https://login.microsoftonline.com/your-tenant-id/v2.0" +} + +variable "container_type" { + description = "Type of container workload to deploy. Must be one of: aci or aca." + type = string + default = "aci" + validation { + condition = contains(["aci", "aca"], var.container_type) + error_message = "container_type must be either 'aci' or 'aca'." + } +} + +variable "database_endpoint" { + description = "Provides the database URI for the database connection string" + type = string + default = "dibbs-global-postgres.postgres.database.azure.com" +} + +variable "database_name" { + description = "Database name" + type = string + default = "query_connector_demo" +} + +variable "database_port" { + description = "Database Port" + type = string + default = "5432" +} + +variable "server_name" { + description = "Server name" + type = string + default = "dibbs-global-postgres" +} \ No newline at end of file diff --git a/terraform/implementation/aca/resources/aca/docker.tf b/terraform/implementation/aca/resources/aca/docker.tf new file mode 100644 index 000000000..61a8c43c9 --- /dev/null +++ b/terraform/implementation/aca/resources/aca/docker.tf @@ -0,0 +1,7 @@ +resource "time_static" "now" {} + +resource "dockerless_remote_image" "dibbs" { + for_each = local.building_block_definitions + source = "${var.ghcr_string}${each.key}:${each.value.app_version}" + target = "${var.acr_url}/${each.value.name}:${each.value.app_version}" +} \ No newline at end of file diff --git a/terraform/implementation/aca/resources/aca/firewall.tf b/terraform/implementation/aca/resources/aca/firewall.tf new file mode 100644 index 000000000..b425b82cf --- /dev/null +++ b/terraform/implementation/aca/resources/aca/firewall.tf @@ -0,0 +1,38 @@ +resource "azurerm_web_application_firewall_policy" "aca_waf_policy" { + name = "${local.name}-wafpolicy" + resource_group_name = var.resource_group_name + location = var.location + + custom_rules { + name = "USOnly" + priority = 1 + rule_type = "MatchRule" + + match_conditions { + match_variables { + variable_name = "RemoteAddr" + } + operator = "GeoMatch" + negation_condition = false + match_values = [ + "US" //United States + ] + } + action = "Allow" + } + + managed_rules { + managed_rule_set { + type = "OWASP" + version = "3.2" + } + } + + policy_settings { + enabled = true + mode = "Detection" //Can use "Detection" for testing, to see which requests would be blocked. "Prevention" turns on active blocking. + request_body_check = true + file_upload_limit_in_mb = 100 + max_request_body_size_in_kb = 1024 //Can go to 2000 in modern provider version. Proposed is 1024. + } +} \ No newline at end of file diff --git a/terraform/implementation/aca/resources/aca/gateway.tf b/terraform/implementation/aca/resources/aca/gateway.tf new file mode 100644 index 000000000..d1f6e34aa --- /dev/null +++ b/terraform/implementation/aca/resources/aca/gateway.tf @@ -0,0 +1,285 @@ +resource "azurerm_private_dns_zone" "aca_zone" { + name = azurerm_container_app_environment.ce_apps.default_domain + resource_group_name = var.resource_group_name +} + +resource "azurerm_private_dns_a_record" "aca_record" { + name = "*" + zone_name = azurerm_private_dns_zone.aca_zone.name + resource_group_name = var.resource_group_name + ttl = 300 + records = [azurerm_container_app_environment.ce_apps.static_ip_address] +} + +resource "azurerm_private_dns_zone_virtual_network_link" "aca_vnet_link" { + name = "${local.name}-vnet-link" + resource_group_name = var.resource_group_name + private_dns_zone_name = azurerm_private_dns_zone.aca_zone.name + virtual_network_id = var.vnet_id +} + +resource "azurerm_public_ip" "aca_ingress" { + name = "${local.name}-aca-gateway-ip" + location = var.location + resource_group_name = var.resource_group_name + allocation_method = "Static" + sku = "Standard" + + domain_name_label = local.name +} + +resource "azurerm_user_assigned_identity" "gateway" { + count = var.pre_assigned_identity_id == "" ? 1 : 0 // Only create if not provided + name = "dibbs-${var.env}-gateway" + location = var.location + resource_group_name = var.resource_group_name + + tags = var.tags +} + +resource "azurerm_application_gateway" "load_balancer" { + name = "${local.name}-app-gateway" + resource_group_name = var.resource_group_name + location = var.location + + sku { + name = "WAF_v2" + tier = "WAF_v2" + } + + autoscale_configuration { + min_capacity = var.autoscale_min + max_capacity = var.autoscale_max + } + + gateway_ip_configuration { + name = "${local.name}-gateway-ip-config" + subnet_id = var.appgw_subnet_id + } + + identity { + type = "UserAssigned" + identity_ids = [try(coalesce(var.pre_assigned_identity_id, azurerm_user_assigned_identity.gateway[0].id), var.pre_assigned_identity_id)] + } + + frontend_ip_configuration { + name = local.frontend_config + public_ip_address_id = azurerm_public_ip.aca_ingress.id + } + + # --- HTTP Listener + frontend_port { + name = local.http_listener + port = 80 + } + + http_listener { + name = local.http_listener + frontend_ip_configuration_name = local.frontend_config + frontend_port_name = local.http_listener + protocol = "Http" + } + + # --- HTTPS Listener (only if SSL is enabled) + dynamic "frontend_port" { + for_each = var.use_ssl ? [1] : [] + content { + name = local.https_listener + port = 443 + } + } + + + dynamic "http_listener" { + for_each = var.use_ssl ? [1] : [] + content { + name = local.https_listener + frontend_ip_configuration_name = local.frontend_config + frontend_port_name = local.https_listener + protocol = "Https" + ssl_certificate_name = "dibbs-site-cert" + } + } + + + dynamic "ssl_certificate" { + for_each = var.use_ssl ? [1] : [] + + content { + name = "dibbs-site-cert" + key_vault_secret_id = data.azurerm_key_vault_certificate.dibbs_site_cert[0].secret_id + } + } + + # --- Container Environment Pool + + backend_address_pool { + name = local.aca_backend_pool + ip_addresses = [azurerm_container_app_environment.ce_apps.static_ip_address] + } + + backend_http_settings { + name = local.aca_backend_http_setting + cookie_based_affinity = "Disabled" + path = "/" + port = 80 + protocol = "Http" + request_timeout = 60 + host_name = azurerm_container_app_environment.ce_apps.default_domain + } + + + # --- eCR Viewer Settings + + backend_address_pool { + name = local.ecr_viewer_backend_pool + fqdns = [azurerm_container_app.aca_apps["ecr-viewer"].ingress[0].fqdn] + } + + backend_http_settings { + name = local.ecr_viewer_backend_http_setting + cookie_based_affinity = "Disabled" + port = 80 + protocol = "Http" + request_timeout = 60 + host_name = azurerm_container_app.aca_apps["ecr-viewer"].ingress[0].fqdn + probe_name = "ecr-viewer-probe" + } + + probe { + host = azurerm_container_app.aca_apps["ecr-viewer"].ingress[0].fqdn + name = "ecr-viewer-probe" + protocol = "Http" + path = "/ecr-viewer/api/health-check" + interval = 30 + timeout = 30 + unhealthy_threshold = 3 + + match { + status_code = ["200"] + } + + } + + # ------- Routing ------------------------- + + request_routing_rule { + name = "${local.name}-routing-http" + priority = 200 + rule_type = "PathBasedRouting" + http_listener_name = local.http_listener + backend_address_pool_name = local.aca_backend_pool + backend_http_settings_name = local.aca_backend_http_setting + url_path_map_name = "${local.name}-urlmap" + } + + dynamic "request_routing_rule" { + for_each = var.use_ssl ? [1] : [] + content { + name = "${local.name}-routing-https" + priority = 100 + rule_type = "PathBasedRouting" + http_listener_name = local.https_listener + backend_address_pool_name = local.aca_backend_pool + backend_http_settings_name = local.aca_backend_http_setting + url_path_map_name = "${local.name}-urlmap" + } + } + + url_path_map { + name = "${local.name}-urlmap" + default_backend_address_pool_name = local.aca_backend_pool + default_backend_http_settings_name = local.aca_backend_http_setting + + + + path_rule { + name = "ecr-viewer" + paths = ["/ecr-viewer/*", "/ecr-viewer"] + backend_address_pool_name = local.ecr_viewer_backend_pool + backend_http_settings_name = local.ecr_viewer_backend_http_setting + } + } + + depends_on = [ + azurerm_public_ip.aca_ingress, + //azurerm_key_vault_access_policy.gateway + ] + + firewall_policy_id = azurerm_web_application_firewall_policy.aca_waf_policy.id + + // Consider uncommenting this line if your tags will be managed exclusively by Terraform. + // tags = var.tags + + // Consider removing this block if your tags will be managed exclusively by Terraform. + lifecycle { + ignore_changes = [ + tags + ] + } +} + +resource "azurerm_key_vault" "kv" { + name = "${var.team}${var.project}${var.env}kv" + location = var.location + resource_group_name = var.resource_group_name + enabled_for_disk_encryption = true + tenant_id = data.azurerm_client_config.current.tenant_id + soft_delete_retention_days = 7 + purge_protection_enabled = false + + sku_name = "standard" + + //It's recommended to set an access control list for the key vault. The network_acls block can be removed if your needs require it. + network_acls { + bypass = "AzureServices" + default_action = "Deny" + virtual_network_subnet_ids = [var.aca_subnet_id, var.appgw_subnet_id] + } + +} + +resource "azurerm_key_vault_access_policy" "gateway" { + key_vault_id = azurerm_key_vault.kv.id + object_id = azurerm_user_assigned_identity.gateway[0].principal_id + tenant_id = data.azurerm_client_config.current.tenant_id + + secret_permissions = ["Get"] +} + + +// Gateway analytics +resource "azurerm_monitor_diagnostic_setting" "logs_metrics" { + name = "${local.name}-gateway-logs-metrics" + target_resource_id = azurerm_application_gateway.load_balancer.id + log_analytics_workspace_id = azurerm_log_analytics_workspace.aca_analytics.id + + dynamic "enabled_log" { + for_each = [ + "ApplicationGatewayAccessLog", + "ApplicationGatewayPerformanceLog", + "ApplicationGatewayFirewallLog", + ] + content { + category = enabled_log.value + + retention_policy { + enabled = false + } + } + } + + dynamic "metric" { + for_each = [ + "AllMetrics", + ] + content { + category = metric.value + + retention_policy { + enabled = false + } + } + } +} + diff --git a/terraform/implementation/aca/resources/aca/locals.tf b/terraform/implementation/aca/resources/aca/locals.tf new file mode 100644 index 000000000..69cc90140 --- /dev/null +++ b/terraform/implementation/aca/resources/aca/locals.tf @@ -0,0 +1,126 @@ +locals { + env = "dev" // TODO: Change this to match your desired environment (e.g., dev, test, prod). Make sure to vary this between environments. + name = "${var.team}-${var.project}-${var.env}" + + workload_profile = "qc-profile" + + registry = { + server = var.acr_url + username = var.acr_username + password = var.acr_password + } + + // Configuration and environment variables for the building blocks are reflected below. + // CPU and memory settings can be adjusted as necessary within the bounds of your workload profile. + building_block_definitions = { + + message-parser = { + name = "message-parser" + cpu = 0.5 + memory = "1Gi" + + + is_public = false + + env_vars = [] + + target_port = 8080 + } + + + query-connector = { + name = "query-connector" + cpu = 0.5 + memory = "1.5Gi" + qc_version= var.qc_version + + is_public = true + + env_vars = [ + { + name = "location", + value = var.location + }, + { + name = "fhir_url", + value = var.fhir_url + }, + { + name = "cred_manager", + value = var.cred_manager + }, + { + name = "DATABASE_URL" + value = "postgres://${data.azurerm_key_vault_secret.query_connector_db_username.value}:${data.azurerm_key_vault_secret.query_connector_db_password.value}@${var.database_endpoint}:${var.database_port}/${var.database_name}?sslmode=require" + }, + { + name = "FLYWAY_URL", + value = "jdbc:postgresql://${var.database_endpoint}:5432/${var.database_name}" + }, + { + name = "FLYWAY_PASSWORD", + value = "${data.azurerm_key_vault_secret.query_connector_db_password.value}" + }, + { + name = "UMLS_API_KEY", + value = "${data.azurerm_key_vault_secret.query_connector_umls_api_key.value}" + }, + { + name = "ERSD_API_KEY", + value = "${data.azurerm_key_vault_secret.query_connector_ersd_api_key.value}" + }, + { + name = "NEXT_PUBLIC_AUTH_PROVIDER", + value = var.auth_provider + }, + { + name = "AUTH_SECRET", + value = "${data.azurerm_key_vault_secret.query_connector_auth_secret.value}" + }, + { + name = "AUTH_CLIENT_ID", + value = "${data.azurerm_key_vault_secret.query_connector_auth_client_id.value}" + }, + { + name = "AUTH_CLIENT_SECRET", + value = "${data.azurerm_key_vault_secret.query_connector_auth_client_secret.value}" + }, + { + name = "AUTH_ISSUER", + value = "https://login.microsoftonline.com/${data.azurerm_key_vault_secret.query_connector_tenant.value}/v2.0" + }, + { + name = "AUTH_DISABLED", + value = "true" + }, + { + name = "ENTRA_TENANT_ID", + value = "${data.azurerm_key_vault_secret.query_connector_tenant.value}" + }, + { + name = "AUTH_URL", + value = "https://connector.dibbs.tools" + }, + { + name = "APP_HOSTNAME", + value = "connector.dibbs.tools" + } + ] + + target_port = 3000 + } +} + http_listener = "${local.name}-http" + https_listener = "${local.name}-https" + frontend_config = "${local.name}-config" + redirect_rule = "${local.name}-redirect" + + aca_backend_pool = "${local.name}-be-aca" + aca_backend_http_setting = "${local.name}-be-aca-http" + orchestration_backend_pool = "${local.name}-be-orchestration" + orchestration_backend_http_setting = "${local.name}-be-orchestration-http" + orchestration_backend_https_setting = "${local.name}-be-orchestration-https" + ecr_viewer_backend_pool = "${local.name}-be-ecr_viewer" + ecr_viewer_backend_http_setting = "${local.name}-be-api-ecr_viewer-http" + ecr_viewer_backend_https_setting = "${local.name}-be-api-ecr_viewer-https" + } \ No newline at end of file diff --git a/terraform/implementation/aca/resources/aca/main.tf b/terraform/implementation/aca/resources/aca/main.tf new file mode 100644 index 000000000..ee5868f74 --- /dev/null +++ b/terraform/implementation/aca/resources/aca/main.tf @@ -0,0 +1,96 @@ +resource "azurerm_log_analytics_workspace" "aca_analytics" { + name = "${local.name}-aca-logs" + location = var.location + resource_group_name = var.resource_group_name + sku = "PerGB2018" + retention_in_days = 30 + + daily_quota_gb = 5 +} + +resource "azurerm_container_app_environment" "ce_apps" { + name = "${local.name}-apps" + location = var.location + resource_group_name = var.resource_group_name + log_analytics_workspace_id = azurerm_log_analytics_workspace.aca_analytics.id + + infrastructure_resource_group_name = "${local.name}-apps-rg" + infrastructure_subnet_id = var.aca_subnet_id + + /* + * Can create additional profiles for FHIR converter, etc. if needed. + * Be sure to adjust the value for workload_profile_type if your building blocks + * hit the resource cap. + */ + workload_profile { + name = local.workload_profile + workload_profile_type = "D4" + maximum_count = 10 + minimum_count = 1 + } + + internal_load_balancer_enabled = true +} + +/* + * Due to internal timings within Azure, the container registry needs extra time to process the presence + * of the images before they are available to be read by the Azure Container Apps environment. + */ +resource "time_sleep" "wait_for_app_images" { + depends_on = [dockerless_remote_image.dibbs] + create_duration = "60s" +} + +resource "azurerm_container_app" "aca_apps" { + for_each = local.building_block_definitions + + name = each.value.name + container_app_environment_id = azurerm_container_app_environment.ce_apps.id + resource_group_name = var.resource_group_name + revision_mode = "Single" + + template { + container { + name = each.value.name + image = "${var.acr_url}/${each.value.name}:${each.value.app_version}" + cpu = each.value.cpu + memory = each.value.memory + + dynamic "env" { + for_each = each.value.env_vars + + content { + name = env.value.name + value = env.value.value + } + } + } + } + + ingress { + allow_insecure_connections = true + external_enabled = each.value.is_public + target_port = each.value.target_port + transport = "auto" + + traffic_weight { + latest_revision = true + percentage = 100 + } + } + + registry { + server = var.acr_url + username = var.acr_username + password_secret_name = "acr-password-secret" + } + + secret { + name = "acr-password-secret" + value = var.acr_password + } + + workload_profile_name = local.workload_profile + + depends_on = [time_sleep.wait_for_app_images] +} \ No newline at end of file diff --git a/terraform/implementation/aca/resources/aca/provider.tf b/terraform/implementation/aca/resources/aca/provider.tf new file mode 100644 index 000000000..2638de3c7 --- /dev/null +++ b/terraform/implementation/aca/resources/aca/provider.tf @@ -0,0 +1,17 @@ +terraform { + required_providers { + dockerless = { + source = "nullstone-io/dockerless" + version = "~> 0.1.1" + } + } +} + +provider "dockerless" { + registry_auth = { + "${var.acr_url}" = { + username = var.acr_username + password = var.acr_password + } + } +} \ No newline at end of file diff --git a/terraform/implementation/aca/resources/foundations/_data.tf b/terraform/implementation/aca/resources/foundations/_data.tf new file mode 100644 index 000000000..bb087fd19 --- /dev/null +++ b/terraform/implementation/aca/resources/foundations/_data.tf @@ -0,0 +1 @@ +data "azurerm_client_config" "current" {} \ No newline at end of file diff --git a/terraform/implementation/aca/resources/foundations/_output.tf b/terraform/implementation/aca/resources/foundations/_output.tf new file mode 100644 index 000000000..1f5c837ba --- /dev/null +++ b/terraform/implementation/aca/resources/foundations/_output.tf @@ -0,0 +1,36 @@ +output "resource_group_name" { + value = azurerm_resource_group.rg.name +} + +output "resource_group_location" { + value = azurerm_resource_group.rg.location +} + +output "resource_group_id" { + value = azurerm_resource_group.rg.id +} + +output "acr_name" { + value = azurerm_container_registry.acr.name +} + +output "acr_url" { + value = azurerm_container_registry.acr.login_server +} + +output "acr_admin_username" { + value = azurerm_container_registry.acr.admin_username +} + +output "acr_admin_password" { + value = azurerm_container_registry.acr.admin_password + sensitive = true +} + +output "azure_storage_connection_string" { + value = azurerm_storage_account.app.primary_connection_string +} + +output "azure_container_name" { + value = azurerm_storage_container.ecr_data.name +} \ No newline at end of file diff --git a/terraform/implementation/aca/resources/foundations/_var.tf b/terraform/implementation/aca/resources/foundations/_var.tf new file mode 100644 index 000000000..3f8a12f6d --- /dev/null +++ b/terraform/implementation/aca/resources/foundations/_var.tf @@ -0,0 +1,19 @@ +variable "team" { + description = "One-word identifier for this project's custodial team." + type = string +} + +variable "project" { + description = "One-word identifier or code name for this project." + type = string +} + +variable "env" { + description = "One-word identifier for the target environment (e.g. dev, test, prod)." + type = string +} + +variable "location" { + description = "The Azure region in which the associated resources will be created." + type = string +} \ No newline at end of file diff --git a/terraform/implementation/aca/resources/foundations/main.tf b/terraform/implementation/aca/resources/foundations/main.tf new file mode 100644 index 000000000..ead2dd4ec --- /dev/null +++ b/terraform/implementation/aca/resources/foundations/main.tf @@ -0,0 +1,37 @@ +resource "azurerm_resource_group" "rg" { + name = "${var.team}-${var.project}-${var.env}" + location = var.location + + lifecycle { + ignore_changes = [ + tags + ] + } +} + +resource "azurerm_container_registry" "acr" { + location = var.location + name = "${var.team}${var.project}${var.env}acr" + resource_group_name = azurerm_resource_group.rg.name + sku = "Standard" + admin_enabled = true +} + +resource "azurerm_storage_account" "app" { + account_replication_type = "GRS" # Cross-regional redundancy + account_tier = "Standard" + account_kind = "StorageV2" + name = "${var.team}${var.project}${var.env}sa" + resource_group_name = azurerm_resource_group.rg.name + location = var.location + https_traffic_only_enabled = true + min_tls_version = "TLS1_2" + allow_nested_items_to_be_public = false + cross_tenant_replication_enabled = false +} + +resource "azurerm_storage_container" "ecr_data" { + name = "ecr-data" + storage_account_name = azurerm_storage_account.app.name + container_access_type = "private" +} \ No newline at end of file diff --git a/terraform/implementation/aca/resources/networking/_output.tf b/terraform/implementation/aca/resources/networking/_output.tf new file mode 100644 index 000000000..9050e203f --- /dev/null +++ b/terraform/implementation/aca/resources/networking/_output.tf @@ -0,0 +1,11 @@ +output "subnet_appgw_id" { + value = azurerm_subnet.appgw.id +} + +output "subnet_aca_id" { + value = azurerm_subnet.aca.id +} + +output "network" { + value = azurerm_virtual_network.vnet +} diff --git a/terraform/implementation/aca/resources/networking/_var.tf b/terraform/implementation/aca/resources/networking/_var.tf new file mode 100644 index 000000000..2fab1c6b8 --- /dev/null +++ b/terraform/implementation/aca/resources/networking/_var.tf @@ -0,0 +1,42 @@ +variable "team" { + description = "One-word identifier for this project's custodial team." + type = string +} + +variable "project" { + description = "One-word identifier or code name for this project." + type = string +} + +variable "env" { + description = "One-word identifier for the target environment (e.g. dev, test, prod)." + type = string +} + +variable "location" { + description = "The Azure region in which the associated resources will be created." + type = string +} + +variable "resource_group_name" { + description = "The name of the resource group to deploy to" + type = string +} + +variable "network_address_space" { + description = "The desired address space for the full virtual network" + type = list(string) + default = ["10.30.0.0/24"] +} + +variable "aca_subnet_address_prefixes" { + type = list(string) + description = "Load balancer subnet IP address space." + default = ["10.30.0.0/25"] +} + +variable "app_gateway_subnet_address_prefixes" { + type = list(string) + description = "App gateway subnet server IP address space." + default = ["10.30.0.128/25"] +} \ No newline at end of file diff --git a/terraform/implementation/aca/resources/networking/main.tf b/terraform/implementation/aca/resources/networking/main.tf new file mode 100644 index 000000000..1a6e622e5 --- /dev/null +++ b/terraform/implementation/aca/resources/networking/main.tf @@ -0,0 +1,41 @@ +locals { + name = "${var.team}-${var.project}-${var.env}" +} + +resource "azurerm_virtual_network" "vnet" { + name = "${local.name}-network" + resource_group_name = var.resource_group_name + location = var.location + address_space = var.network_address_space +} + +resource "azurerm_subnet" "appgw" { + name = "${local.name}-appgw-subnet" + resource_group_name = var.resource_group_name + virtual_network_name = azurerm_virtual_network.vnet.name + address_prefixes = var.app_gateway_subnet_address_prefixes + service_endpoints = [ + "Microsoft.Web", + "Microsoft.Storage", + "Microsoft.KeyVault" + ] +} + +resource "azurerm_subnet" "aca" { + name = "${local.name}-aca" + resource_group_name = var.resource_group_name + virtual_network_name = azurerm_virtual_network.vnet.name + address_prefixes = var.aca_subnet_address_prefixes + service_endpoints = [ + "Microsoft.KeyVault" + ] + + delegation { + name = "aca_delegation" + + service_delegation { + name = "Microsoft.App/environments" + actions = ["Microsoft.Network/virtualNetworks/subnets/join/action"] + } + } +}