diff --git a/.github/workflows/update-md-date.yml b/.github/workflows/update-md-date.yml index 5b2a19b..64d07fd 100644 --- a/.github/workflows/update-md-date.yml +++ b/.github/workflows/update-md-date.yml @@ -25,9 +25,6 @@ jobs: with: python-version: '3.x' - - name: Install dependencies - run: pip install python-dateutil - - name: Configure Git run: | git config --global user.email "github-actions[bot]@users.noreply.github.com" diff --git a/.github/workflows/update_date.py b/.github/workflows/update_date.py index ab86df5..58f97d6 100644 --- a/.github/workflows/update_date.py +++ b/.github/workflows/update_date.py @@ -1,4 +1,3 @@ -import os import subprocess from datetime import datetime, timezone @@ -44,6 +43,3 @@ def update_date_in_file(file_path): print(f"Updating file: {file_path}") # Debugging: Print the file being updated update_date_in_file(file_path) -# Add and commit changes -subprocess.run(['git', 'add', '-A']) -subprocess.run(['git', 'commit', '-m', 'Update last modified date in Markdown files']) diff --git a/.github/workflows/use-visitor-counter.yml b/.github/workflows/use-visitor-counter.yml index 8c597df..fed59dc 100644 --- a/.github/workflows/use-visitor-counter.yml +++ b/.github/workflows/use-visitor-counter.yml @@ -21,6 +21,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }} - name: Shallow clone visitor counter logic run: git clone --depth=1 https://github.com/brown9804/github-visitor-counter.git @@ -63,13 +64,12 @@ jobs: env: TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - git fetch origin - git checkout ${{ github.head_ref }} - git pull origin ${{ github.head_ref }} || echo "No merge needed" + BRANCH="${{ github.head_ref }}" + git pull origin "$BRANCH" || echo "No merge needed" git add -A git commit -m "Update visitor count" || echo "No changes to commit" git remote set-url origin https://x-access-token:${TOKEN}@github.com/${{ github.repository }} - git push origin HEAD:${{ github.head_ref }} + git push origin HEAD:"$BRANCH" # Commit and push logic for non-PR events (merge, not rebase) - name: Commit and push changes (non-PR) @@ -77,10 +77,9 @@ jobs: env: TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - git fetch origin - git checkout ${{ github.ref_name }} || git checkout -b ${{ github.ref_name }} origin/${{ github.ref_name }} - git pull origin ${{ github.ref_name }} || echo "No merge needed" + BRANCH="${{ github.ref_name }}" + git pull origin "$BRANCH" || echo "No merge needed" git add -A git commit -m "Update visitor count" || echo "No changes to commit" git remote set-url origin https://x-access-token:${TOKEN}@github.com/${{ github.repository }} - git push origin HEAD:${{ github.ref_name }} + git push origin HEAD:"$BRANCH" diff --git a/.gitignore b/.gitignore index 6349e36..ba294d5 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ crash.*.log # 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 diff --git a/README.md b/README.md index 529042d..8b5b77a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Costa Rica [![GitHub](https://img.shields.io/badge/--181717?logo=github&logoColor=ffffff)](https://github.com/) [brown9804](https://github.com/brown9804) -Last updated: 2025-07-30 +Last updated: 2026-04-07 ----------------------------- @@ -34,6 +34,8 @@ Last updated: 2025-07-30 ## Overview +> The infrastructure sample for this architecture is available in [terraform-infrastructure](terraform-infrastructure/README.md). It provisions the Azure-side resources for the documented workflow, including the Resource Group, Function App dependencies, monitoring, Logic App, Key Vault, SQL, and the AI service account. +
Centered Image @@ -73,7 +75,7 @@ Last updated: 2025-07-30
- Total views -

Refresh Date: 2025-11-13

+ Total views +

Refresh Date: 2026-04-07

diff --git a/terraform-infrastructure/README.md b/terraform-infrastructure/README.md new file mode 100644 index 0000000..5d17013 --- /dev/null +++ b/terraform-infrastructure/README.md @@ -0,0 +1,117 @@ +# Azure Infrastructure Terraform Templates + +Costa Rica + +[![GitHub](https://img.shields.io/badge/--181717?logo=github&logoColor=ffffff)](https://github.com/) +[Cloud2BR OSS - Learning Hub](https://github.com/Cloud2BR-MSFTLearningHub) + +Last updated: 2026-04-07 + +---------- + +> This approach focuses on `setting up the required infrastructure via Terraform`. It allows for source control of not only the solution code, connections, and setups `but also the infrastructure itself`. + +## Prerequisites + +- An `Azure subscription is required`. All other resources, including instructions for creating a Resource Group, are provided in this workshop. +- `Contributor role assigned or any custom role that allows`: access to manage all resources, and the ability to deploy resources within subscription. +- Please ensure that: + - [Terraform is installed on your local machine](https://developer.hashicorp.com/terraform/tutorials/azure-get-started/install-cli#install-terraform). + - [Install the Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) to work with both Terraform and Azure commands. + +## Overview + +Templates structure: + +``` +. +├── README.md +├────── main.tf +├────── variables.tf +├────── provider.tf +├────── terraform.tfvars +├────── outputs.tf +``` + +- main.tf `(Main Terraform configuration file)`: This file contains the core infrastructure code. It defines the resources you want to create, such as virtual machines, networks, and storage. It's the primary file where you describe your infrastructure in a declarative manner. +- variables.tf `(Variable definitions)`: This file is used to define variables that can be used throughout your Terraform configuration. By using variables, you can make your configuration more flexible and reusable. For example, you can define variables for resource names, sizes, and other parameters that might change between environments. +- provider.tf `(Provider configurations)`: Providers are plugins that Terraform uses to interact with cloud providers, SaaS providers, and other APIs. This file specifies which providers (e.g., AWS, Azure, Google Cloud) you are using and any necessary configuration for them, such as authentication details. +- terraform.tfvars `(Variable values)`: This file contains the actual values for the variables defined in `variables.tf`. By separating variable definitions and values, you can easily switch between different sets of values for different environments (e.g., development, staging, production) without changing the main configuration files. +- outputs.tf `(Output values)`: This file defines the output values that Terraform should return after applying the configuration. Outputs are useful for displaying information about the resources created, such as IP addresses, resource IDs, and other important details. They can also be used as inputs for other Terraform configurations or scripts. + +## How to execute it + +```mermaid +graph TD; + A[az login] --> B(terraform init) + B --> C{Terraform provisioning stage} + C -->|Review| D[terraform plan] + C -->|Order Now| E[terraform apply] + C -->|Delete Resource if needed| F[terraform destroy] +``` + +> [!IMPORTANT] +> Please modify `terraform.tfvars` with your information, then run the following flow. If you need more visual guidance, please check the video that illustrates the provisioning steps. + +1. **Login to Azure**: This command logs you into your Azure account. It opens a browser window where you can enter your Azure credentials. Once logged in, you can manage your Azure resources from the command line. + + > Go to the path where Terraform files are located: + + ```sh + cd terraform-infrastructure + ``` + + ```sh + az login + ``` + + img + + img + +2. **Initialize Terraform**: Initializes the working directory containing the Terraform configuration files. It downloads the necessary provider plugins and sets up the backend for storing the state. + + ``` sh + terraform init + ``` + + img + +3. **Terraform Provisioning Stage**: + + - **Review**: Creates an execution plan, showing what actions Terraform will take to achieve the desired state defined in your configuration files. It uses the variable values specified in `terraform.tfvars`. + + ```sh + terraform plan -var-file terraform.tfvars + ``` + + > At the end, you will see a message in green if everything was executed successfully: + + Screenshot 2025-03-18 145143 + + - **Order Now**: Applies the changes required to reach the desired state of the configuration. It prompts for confirmation before making any changes. It also uses the variable values specified in `terraform.tfvars`. + + ```sh + terraform apply -var-file terraform.tfvars + ``` + + > At the end, you will see a message in green if everything was executed successfully: + + image + + - **Remove**: Destroys the infrastructure managed by Terraform. It prompts for confirmation before deleting any resources. It also uses the variable values specified in `terraform.tfvars`. + + ```sh + terraform destroy -var-file terraform.tfvars + ``` + + > At the end, you will see a message in green if everything was executed successfully: + + image + + +
+ Total views +

Refresh Date: 2026-04-07

+
+ \ No newline at end of file diff --git a/terraform-infrastructure/main.tf b/terraform-infrastructure/main.tf new file mode 100644 index 0000000..577163d --- /dev/null +++ b/terraform-infrastructure/main.tf @@ -0,0 +1,213 @@ +locals { + prefix = lower(replace("${var.workload_name}-${var.environment}", "_", "-")) + compact_prefix = substr(replace(local.prefix, "-", ""), 0, 16) + tags = merge(var.tags, { environment = var.environment, workload = var.workload_name, managed_by = "terraform" }) + resource_group_name = "${local.prefix}-rg" + log_analytics_name = "${local.prefix}-law" + app_insights_name = "${local.prefix}-appi" + key_vault_name = substr("${local.prefix}-kv", 0, 24) + identity_name = "${local.prefix}-mi" + runtime_storage_name = substr("${local.compact_prefix}func", 0, 24) + data_storage_name = substr("${local.compact_prefix}data", 0, 24) + service_plan_name = "${local.prefix}-plan" + function_app_name = "${local.prefix}-func" + sql_server_name = substr("${local.prefix}-sql", 0, 60) + sql_database_name = "${local.prefix}-sqldb" + logic_app_name = "${local.prefix}-logic" + ai_account_name = substr("${local.prefix}-ai", 0, 64) +} + +resource "azurerm_resource_group" "resource_group" { + name = local.resource_group_name + location = var.location + tags = local.tags +} + +resource "azurerm_log_analytics_workspace" "log_analytics" { + name = local.log_analytics_name + location = var.location + resource_group_name = azurerm_resource_group.resource_group.name + sku = var.log_analytics_sku + retention_in_days = var.log_retention_in_days + tags = local.tags +} + +resource "azurerm_application_insights" "application_insights" { + name = local.app_insights_name + location = var.location + resource_group_name = azurerm_resource_group.resource_group.name + workspace_id = azurerm_log_analytics_workspace.log_analytics.id + application_type = "web" + tags = local.tags +} + +resource "azurerm_user_assigned_identity" "managed_identity" { + name = local.identity_name + location = var.location + resource_group_name = azurerm_resource_group.resource_group.name + tags = local.tags +} + +resource "azurerm_key_vault" "key_vault" { + name = local.key_vault_name + location = var.location + resource_group_name = azurerm_resource_group.resource_group.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + soft_delete_retention_days = 7 + purge_protection_enabled = false + enable_rbac_authorization = true + public_network_access_enabled = true + tags = local.tags +} + +resource "azurerm_storage_account" "storage_runtime" { + name = local.runtime_storage_name + location = var.location + resource_group_name = azurerm_resource_group.resource_group.name + account_tier = "Standard" + account_replication_type = var.storage_replication_type + account_kind = "StorageV2" + min_tls_version = "TLS1_2" + public_network_access_enabled = true + shared_access_key_enabled = true + allow_nested_items_to_be_public = false + tags = local.tags +} + +resource "azurerm_storage_account" "storage_data" { + count = var.enable_data_storage_account ? 1 : 0 + name = local.data_storage_name + location = var.location + resource_group_name = azurerm_resource_group.resource_group.name + account_tier = "Standard" + account_replication_type = var.storage_replication_type + account_kind = "StorageV2" + min_tls_version = "TLS1_2" + public_network_access_enabled = true + shared_access_key_enabled = true + allow_nested_items_to_be_public = false + tags = local.tags +} + +resource "azurerm_storage_container" "raw_recommendations" { + count = var.enable_data_storage_account ? 1 : 0 + name = "raw-recommendations" + storage_account_id = azurerm_storage_account.storage_data[0].id + container_access_type = "private" +} + +resource "azurerm_storage_container" "enriched_recommendations" { + count = var.enable_data_storage_account ? 1 : 0 + name = "enriched-recommendations" + storage_account_id = azurerm_storage_account.storage_data[0].id + container_access_type = "private" +} + +resource "azurerm_service_plan" "app_service_plan" { + name = local.service_plan_name + location = var.location + resource_group_name = azurerm_resource_group.resource_group.name + os_type = "Linux" + sku_name = var.function_plan_sku_name + tags = local.tags +} + +resource "azurerm_cognitive_account" "ai" { + count = var.enable_ai_service ? 1 : 0 + name = local.ai_account_name + location = var.location + resource_group_name = azurerm_resource_group.resource_group.name + kind = var.ai_service_kind + sku_name = var.ai_service_sku_name + custom_subdomain_name = substr(replace(local.ai_account_name, "-", ""), 0, 24) + public_network_access_enabled = true + tags = local.tags +} + +resource "azurerm_mssql_server" "sql" { + count = var.enable_sql_backend ? 1 : 0 + name = local.sql_server_name + resource_group_name = azurerm_resource_group.resource_group.name + location = var.location + version = "12.0" + administrator_login = var.sql_admin_login + administrator_login_password = var.sql_admin_password + minimum_tls_version = "1.2" + public_network_access_enabled = true + tags = local.tags +} + +resource "azurerm_mssql_database" "sql_database" { + count = var.enable_sql_backend ? 1 : 0 + name = local.sql_database_name + server_id = azurerm_mssql_server.sql[0].id + sku_name = var.sql_sku_name + tags = local.tags +} + +resource "azurerm_linux_function_app" "function_app" { + name = local.function_app_name + location = var.location + resource_group_name = azurerm_resource_group.resource_group.name + service_plan_id = azurerm_service_plan.app_service_plan.id + storage_account_name = azurerm_storage_account.storage_runtime.name + storage_account_access_key = azurerm_storage_account.storage_runtime.primary_access_key + https_only = true + tags = local.tags + + identity { + type = "UserAssigned" + identity_ids = [azurerm_user_assigned_identity.managed_identity.id] + } + + site_config { + application_insights_connection_string = azurerm_application_insights.application_insights.connection_string + application_insights_key = azurerm_application_insights.application_insights.instrumentation_key + ftps_state = "Disabled" + + application_stack { + python_version = var.function_python_version + } + } + + app_settings = merge( + { + "AzureWebJobsStorage" = "DefaultEndpointsProtocol=https;AccountName=${azurerm_storage_account.storage_runtime.name};AccountKey=${azurerm_storage_account.storage_runtime.primary_access_key};EndpointSuffix=core.windows.net" + "FUNCTIONS_EXTENSION_VERSION" = "~4" + "FUNCTIONS_WORKER_RUNTIME" = "python" + "WEBSITE_RUN_FROM_PACKAGE" = "1" + "WORKLOAD_NAME" = var.workload_name + "AZURE_CLIENT_ID" = azurerm_user_assigned_identity.managed_identity.client_id + "KEY_VAULT_NAME" = azurerm_key_vault.key_vault.name + "DATA_STORAGE_ACCOUNT_NAME" = var.enable_data_storage_account ? azurerm_storage_account.storage_data[0].name : "" + "SQL_SERVER_FQDN" = var.enable_sql_backend ? azurerm_mssql_server.sql[0].fully_qualified_domain_name : "" + "SQL_DATABASE_NAME" = var.enable_sql_backend ? azurerm_mssql_database.sql_database[0].name : "" + "AI_SERVICE_ENDPOINT" = var.enable_ai_service ? azurerm_cognitive_account.ai[0].endpoint : "" + "APPLICATIONINSIGHTS_CONNECTION_STRING" = azurerm_application_insights.application_insights.connection_string + }, + var.function_app_settings + ) +} + +resource "azurerm_logic_app_workflow" "logic_app" { + count = var.enable_logic_app ? 1 : 0 + name = local.logic_app_name + location = var.location + resource_group_name = azurerm_resource_group.resource_group.name + enabled = true + tags = local.tags +} + +resource "azurerm_role_assignment" "key_vault_secrets_user" { + scope = azurerm_key_vault.key_vault.id + role_definition_name = "Key Vault Secrets User" + principal_id = azurerm_user_assigned_identity.managed_identity.principal_id +} + +resource "azurerm_role_assignment" "data_storage_blob_contributor" { + count = var.enable_data_storage_account ? 1 : 0 + scope = azurerm_storage_account.storage_data[0].id + role_definition_name = "Storage Blob Data Contributor" + principal_id = azurerm_user_assigned_identity.managed_identity.principal_id +} diff --git a/terraform-infrastructure/outputs.tf b/terraform-infrastructure/outputs.tf new file mode 100644 index 0000000..39c6cf0 --- /dev/null +++ b/terraform-infrastructure/outputs.tf @@ -0,0 +1,65 @@ +output "resource_group_name" { + description = "Name of the resource group created for the workload." + value = azurerm_resource_group.resource_group.name +} + +output "function_app_name" { + description = "Name of the Linux Function App." + value = azurerm_linux_function_app.function_app.name +} + +output "function_app_default_hostname" { + description = "Default hostname of the Linux Function App." + value = azurerm_linux_function_app.function_app.default_hostname +} + +output "function_app_principal_id" { + description = "Principal ID of the Function App user-assigned identity." + value = azurerm_user_assigned_identity.managed_identity.principal_id +} + +output "key_vault_name" { + description = "Name of the Key Vault." + value = azurerm_key_vault.key_vault.name +} + +output "log_analytics_workspace_id" { + description = "Resource ID of the Log Analytics Workspace." + value = azurerm_log_analytics_workspace.log_analytics.id +} + +output "application_insights_connection_string" { + description = "Connection string for Application Insights." + value = azurerm_application_insights.application_insights.connection_string + sensitive = true +} + +output "runtime_storage_account_name" { + description = "Name of the Function runtime storage account." + value = azurerm_storage_account.storage_runtime.name +} + +output "data_storage_account_name" { + description = "Name of the data storage account when enabled." + value = var.enable_data_storage_account ? azurerm_storage_account.storage_data[0].name : null +} + +output "sql_server_fqdn" { + description = "Fully qualified domain name for Azure SQL Server when enabled." + value = var.enable_sql_backend ? azurerm_mssql_server.sql[0].fully_qualified_domain_name : null +} + +output "sql_database_name" { + description = "Azure SQL Database name when enabled." + value = var.enable_sql_backend ? azurerm_mssql_database.sql_database[0].name : null +} + +output "logic_app_name" { + description = "Logic App workflow name when enabled." + value = var.enable_logic_app ? azurerm_logic_app_workflow.logic_app[0].name : null +} + +output "ai_service_endpoint" { + description = "Endpoint for the Azure AI service account when enabled." + value = var.enable_ai_service ? azurerm_cognitive_account.ai[0].endpoint : null +} diff --git a/terraform-infrastructure/provider.tf b/terraform-infrastructure/provider.tf new file mode 100644 index 0000000..ef24d59 --- /dev/null +++ b/terraform-infrastructure/provider.tf @@ -0,0 +1,16 @@ +terraform { + required_version = ">= 1.7.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.26" + } + } +} + +provider "azurerm" { + features {} +} + +data "azurerm_client_config" "current" {} diff --git a/terraform-infrastructure/terraform.tfvars b/terraform-infrastructure/terraform.tfvars new file mode 100644 index 0000000..4e33a9a --- /dev/null +++ b/terraform-infrastructure/terraform.tfvars @@ -0,0 +1,23 @@ +location = "eastus" +environment = "dev" +workload_name = "arcai" +storage_replication_type = "LRS" +function_plan_sku_name = "Y1" +function_python_version = "3.11" +enable_data_storage_account = true +enable_sql_backend = true +sql_admin_login = "sqladminuser" +sql_admin_password = "ChangeMe1234!" +enable_logic_app = true +enable_ai_service = true +ai_service_kind = "AIServices" +ai_service_sku_name = "S0" + +tags = { + project = "azure-arc-recommendations-ai-agent" + owner = "platform-team" +} + +function_app_settings = { + FUNCTIONS_WORKER_RUNTIME = "python" +} diff --git a/terraform-infrastructure/variables.tf b/terraform-infrastructure/variables.tf new file mode 100644 index 0000000..dbd8c7f --- /dev/null +++ b/terraform-infrastructure/variables.tf @@ -0,0 +1,113 @@ +variable "location" { + description = "Azure region for the deployment." + type = string + default = "eastus" +} + +variable "environment" { + description = "Environment suffix used in names and tags." + type = string + default = "dev" +} + +variable "workload_name" { + description = "Short workload name used as the naming prefix." + type = string + default = "arcai" +} + +variable "tags" { + description = "Tags applied to all supported Azure resources." + type = map(string) + default = {} +} + +variable "storage_replication_type" { + description = "Replication type used by storage accounts." + type = string + default = "LRS" +} + +variable "function_plan_sku_name" { + description = "SKU for the Function App service plan. Use Y1 for consumption or EP1 for premium." + type = string + default = "Y1" +} + +variable "function_python_version" { + description = "Python version for the Linux Function App." + type = string + default = "3.11" +} + +variable "function_app_settings" { + description = "Additional Function App settings merged into the default application settings." + type = map(string) + default = {} +} + +variable "enable_data_storage_account" { + description = "Whether to create a second storage account for payloads and artifacts." + type = bool + default = true +} + +variable "enable_sql_backend" { + description = "Whether to deploy Azure SQL Server and Database for enriched recommendation persistence." + type = bool + default = true +} + +variable "sql_admin_login" { + description = "Administrator login for Azure SQL Server." + type = string + default = "sqladminuser" +} + +variable "sql_admin_password" { + description = "Administrator password for Azure SQL Server." + type = string + sensitive = true +} + +variable "sql_sku_name" { + description = "SKU for the Azure SQL Database." + type = string + default = "Basic" +} + +variable "enable_logic_app" { + description = "Whether to deploy a Logic App workflow resource." + type = bool + default = true +} + +variable "enable_ai_service" { + description = "Whether to deploy the Azure AI service account used by the solution." + type = bool + default = true +} + +variable "ai_service_kind" { + description = "Kind for the Cognitive Services account." + type = string + default = "AIServices" +} + +variable "ai_service_sku_name" { + description = "SKU for the Azure AI service account." + type = string + default = "S0" +} + +variable "log_analytics_sku" { + description = "SKU for the Log Analytics workspace." + type = string + default = "PerGB2018" +} + +variable "log_retention_in_days" { + description = "Retention period for Log Analytics in days." + type = number + default = 30 +}