diff --git a/.github/workflows/tfsec.yml b/.github/workflows/tfsec.yml new file mode 100644 index 0000000..48c3900 --- /dev/null +++ b/.github/workflows/tfsec.yml @@ -0,0 +1,38 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: tfsec + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '28 14 * * 3' + +jobs: + tfsec: + name: Run tfsec sarif report + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Clone repo + uses: actions/checkout@v3 + + - name: Run tfsec + uses: aquasecurity/tfsec-sarif-action@9a83b5c3524f825c020e356335855741fd02745f + with: + sarif_file: tfsec.sarif + + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v2 + with: + # Path to SARIF file relative to the root of the repository + sarif_file: tfsec.sarif diff --git a/.infracost/pricing.gob b/.infracost/pricing.gob new file mode 100644 index 0000000..2921e6d Binary files /dev/null and b/.infracost/pricing.gob differ diff --git a/.infracost/terraform_modules/manifest-73eb4d77fdade6cec426a59518f5a40f.json b/.infracost/terraform_modules/manifest-73eb4d77fdade6cec426a59518f5a40f.json new file mode 100644 index 0000000..15fc39b --- /dev/null +++ b/.infracost/terraform_modules/manifest-73eb4d77fdade6cec426a59518f5a40f.json @@ -0,0 +1 @@ +{"Path":"d:\\Projects\\terraform-course\\120_azapi_provider","Version":"2.0","Modules":[]} \ No newline at end of file diff --git a/.infracost/terraform_modules/manifest-9a64dbcd150fd1f2eeaa11141b4ca3c4.json b/.infracost/terraform_modules/manifest-9a64dbcd150fd1f2eeaa11141b4ca3c4.json new file mode 100644 index 0000000..2fbc58c --- /dev/null +++ b/.infracost/terraform_modules/manifest-9a64dbcd150fd1f2eeaa11141b4ca3c4.json @@ -0,0 +1 @@ +{"Path":"d:\\Projects\\terraform-course\\93_import_terraform","Version":"2.0","Modules":[]} \ No newline at end of file diff --git a/.infracost/terraform_modules/manifest-c9df5a5e064cb112a7cef4f4fccd5118.json b/.infracost/terraform_modules/manifest-c9df5a5e064cb112a7cef4f4fccd5118.json new file mode 100644 index 0000000..0b16dc7 --- /dev/null +++ b/.infracost/terraform_modules/manifest-c9df5a5e064cb112a7cef4f4fccd5118.json @@ -0,0 +1 @@ +{"Path":"d:\\Projects\\terraform-course\\121_appservice_domain","Version":"2.0","Modules":[]} \ No newline at end of file diff --git a/07_kubernetes_aks/main.tf b/07_kubernetes_aks/main.tf index f51878f..c0b9ce0 100644 --- a/07_kubernetes_aks/main.tf +++ b/07_kubernetes_aks/main.tf @@ -16,7 +16,7 @@ resource "azurerm_kubernetes_cluster" "aks" { node_count = var.system_node_count vm_size = "Standard_DS2_v2" type = "VirtualMachineScaleSets" - availability_zones = [1, 2, 3] + # availability_zones = [1, 2, 3] enable_auto_scaling = false } @@ -25,7 +25,7 @@ resource "azurerm_kubernetes_cluster" "aks" { } network_profile { - load_balancer_sku = "Standard" + load_balancer_sku = "standard" network_plugin = "kubenet" # azure (CNI) } } \ No newline at end of file diff --git a/07_kubernetes_aks/providers.tf b/07_kubernetes_aks/providers.tf index 2c7c936..1ddcd8e 100644 --- a/07_kubernetes_aks/providers.tf +++ b/07_kubernetes_aks/providers.tf @@ -6,7 +6,7 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "2.78.0" + version = "3.54.0" } } } \ No newline at end of file diff --git a/07_kubernetes_aks/terraform.tfvars b/07_kubernetes_aks/terraform.tfvars index 3f9fbf9..a10a1ca 100644 --- a/07_kubernetes_aks/terraform.tfvars +++ b/07_kubernetes_aks/terraform.tfvars @@ -1,6 +1,6 @@ resource_group_name = "aks_terraform_rg" location = "West Europe" cluster_name = "terraform-aks" -kubernetes_version = "1.19.3" +kubernetes_version = "1.26.3" system_node_count = 3 node_resource_group = "aks_terraform_resources_rg" \ No newline at end of file diff --git a/120_azapi_provider/Readme.md b/120_azapi_provider/Readme.md new file mode 100644 index 0000000..d7a48d7 --- /dev/null +++ b/120_azapi_provider/Readme.md @@ -0,0 +1,38 @@ +# Using Azure Grafana and Prometheus workspace in AKS using Terraform + +## Introduction + +This lab shows how to use Terraform to provision an AKS cluster, Grafana and Monitor Workspace for Prometheus. All configured together to collect metrics from the cluster and expose it through Grafana dashboard. + + + +## Challenges + +Azure Monitor Workspace for Prometheus is a new service (in preview). +It is not yet supported with ARM template or with Terraform resource. + +So, we'll use `azapi` terraform provider to create the Monitor Workspace for Prometheus. + +And we'll use a `local-exec` to run a command line to configure AKS with Prometheus. + +AKS, Grafana and Log Analytics are suported with ARM templates and Terraform. + +## Deploying the resources using Terraform + +To deploy the Terraform configuration files, run the following commands: + +```shell +terraform init + +terraform plan -out tfplan + +terraform apply tfplan +``` + +## Cleanup resources + +To delete the creates resources, run the following command: + +```shell +terraform destroy +``` \ No newline at end of file diff --git a/120_azapi_provider/aks.tf b/120_azapi_provider/aks.tf new file mode 100644 index 0000000..b2dfb91 --- /dev/null +++ b/120_azapi_provider/aks.tf @@ -0,0 +1,29 @@ +# aks cluster +resource "azurerm_kubernetes_cluster" "aks" { + name = "aks-cluster" + location = "westeurope" + resource_group_name = "rg-aks-cluster" + dns_prefix = "aks" + kubernetes_version = "1.25.5" + + default_node_pool { + name = "default" + node_count = "3" + vm_size = "Standard_DS2_v2" + } + + identity { + type = "SystemAssigned" + } + + oms_agent { + log_analytics_workspace_id = azurerm_log_analytics_workspace.workspace.id + msi_auth_for_monitoring_enabled = true + } + + lifecycle { + ignore_changes = [ + monitor_metrics + ] + } +} diff --git a/120_azapi_provider/commands.sh b/120_azapi_provider/commands.sh new file mode 100644 index 0000000..e331d02 --- /dev/null +++ b/120_azapi_provider/commands.sh @@ -0,0 +1,7 @@ +terraform init + +terraform plan -out tfplan + +terraform apply tfplan + +terraform destroy \ No newline at end of file diff --git a/120_azapi_provider/enable_prometheus.tf b/120_azapi_provider/enable_prometheus.tf new file mode 100644 index 0000000..d3c94a0 --- /dev/null +++ b/120_azapi_provider/enable_prometheus.tf @@ -0,0 +1,22 @@ +resource "null_resource" "enable_azuremonitormetrics" { + # for windows + provisioner "local-exec" { + interpreter = ["PowerShell", "-Command"] + command = <<-EOT + + az aks update --enable-azuremonitormetrics ` + -g ${azurerm_kubernetes_cluster.aks.resource_group_name} ` + -n ${azurerm_kubernetes_cluster.aks.name} ` + --azure-monitor-workspace-resource-id ${azapi_resource.prometheus.id} + EOT + } + + triggers = { + "key" = "value1" + } + + # for linux + # provisioner "local-exec" { + # command = "az aks update --enable-azuremonitormetrics -g ${azurerm_kubernetes_cluster.aks.resource_group_name} -n ${azurerm_kubernetes_cluster.aks.name} --azure-monitor-workspace-resource-id ${azapi_resource.prometheus.id}" + # } +} diff --git a/120_azapi_provider/grafana.tf b/120_azapi_provider/grafana.tf new file mode 100644 index 0000000..66d3864 --- /dev/null +++ b/120_azapi_provider/grafana.tf @@ -0,0 +1,47 @@ +resource "azurerm_dashboard_grafana" "grafana" { + name = var.grafana_name + resource_group_name = azurerm_resource_group.rg_monitoring.name + location = azurerm_resource_group.rg_monitoring.location + api_key_enabled = true + deterministic_outbound_ip_enabled = true + public_network_access_enabled = true + sku = "Standard" + zone_redundancy_enabled = true + + azure_monitor_workspace_integrations { + resource_id = azapi_resource.prometheus.id + } + + identity { + type = "SystemAssigned" # The only possible values is SystemAssigned + } +} + +data "azurerm_client_config" "current" {} + +# assign current user as Grafana Admin +resource "azurerm_role_assignment" "role_grafana_admin" { + scope = azurerm_dashboard_grafana.grafana.id + role_definition_name = "Grafana Admin" + principal_id = data.azurerm_client_config.current.object_id +} + +resource "azurerm_role_assignment" "role_monitoring_data_reader" { + scope = azapi_resource.prometheus.id + role_definition_name = "Monitoring Data Reader" + principal_id = azurerm_dashboard_grafana.grafana.identity.0.principal_id +} + +data "azurerm_subscription" "current" {} + +# https://learn.microsoft.com/en-us/azure/azure-monitor/visualize/grafana-plugin +# (Optional) Grafana to monitor all Azure resources +resource "azurerm_role_assignment" "role_monitoring_reader" { + scope = data.azurerm_subscription.current.id + role_definition_name = "Monitoring Reader" + principal_id = azurerm_dashboard_grafana.grafana.identity.0.principal_id +} + +output "garafana_endpoint" { + value = azurerm_dashboard_grafana.grafana.endpoint +} \ No newline at end of file diff --git a/120_azapi_provider/images/architecture.png b/120_azapi_provider/images/architecture.png new file mode 100644 index 0000000..4b45bc4 Binary files /dev/null and b/120_azapi_provider/images/architecture.png differ diff --git a/120_azapi_provider/log_analytics.tf b/120_azapi_provider/log_analytics.tf new file mode 100644 index 0000000..aa536e1 --- /dev/null +++ b/120_azapi_provider/log_analytics.tf @@ -0,0 +1,20 @@ +resource "azurerm_log_analytics_workspace" "workspace" { + name = "log-analytics-workspace" + resource_group_name = azurerm_resource_group.rg_monitoring.name + location = var.resources_location + sku = "PerGB2018" # PerGB2018, Free, PerNode, Premium, Standard, Standalone, Unlimited, CapacityReservation + retention_in_days = 30 # possible values are either 7 (Free Tier only) or range between 30 and 730 +} + +resource "azurerm_log_analytics_solution" "solution" { + solution_name = "ContainerInsights" + location = azurerm_log_analytics_workspace.workspace.location + resource_group_name = azurerm_log_analytics_workspace.workspace.resource_group_name + workspace_resource_id = azurerm_log_analytics_workspace.workspace.id + workspace_name = azurerm_log_analytics_workspace.workspace.name + + plan { + publisher = "Microsoft" + product = "OMSGallery/ContainerInsights" + } +} \ No newline at end of file diff --git a/120_azapi_provider/prometheus.tf b/120_azapi_provider/prometheus.tf new file mode 100644 index 0000000..c5a21d5 --- /dev/null +++ b/120_azapi_provider/prometheus.tf @@ -0,0 +1,7 @@ +# https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/azure-monitor-workspace-overview?tabs=resource-manager#create-an-azure-monitor-workspace +resource "azapi_resource" "prometheus" { + type = "microsoft.monitor/accounts@2021-06-03-preview" + name = "monitor-workspace-aks" + parent_id = azurerm_resource_group.rg_monitoring.id + location = azurerm_resource_group.rg_monitoring.location +} \ No newline at end of file diff --git a/120_azapi_provider/provider.tf b/120_azapi_provider/provider.tf new file mode 100644 index 0000000..4563c2e --- /dev/null +++ b/120_azapi_provider/provider.tf @@ -0,0 +1,34 @@ +terraform { + + required_version = ">= 1.2.8" + + required_providers { + + azurerm = { + source = "hashicorp/azurerm" + version = "= 3.50.0" + } + + azuread = { + source = "hashicorp/azuread" + version = "= 2.36.0" + } + + azapi = { + source = "Azure/azapi" + version = "1.4.0" + } + } +} + +provider "azurerm" { + features {} +} + +# Configure the Azure Active Directory Provider +provider "azuread" { # default takes current user/identity tenant +} + +provider "azapi" { + # Configuration options +} diff --git a/120_azapi_provider/resource_group.tf b/120_azapi_provider/resource_group.tf new file mode 100644 index 0000000..276e5b7 --- /dev/null +++ b/120_azapi_provider/resource_group.tf @@ -0,0 +1,9 @@ +resource "azurerm_resource_group" "rg_aks_cluster" { + name = var.rg_aks_cluster + location = var.resources_location +} + +resource "azurerm_resource_group" "rg_monitoring" { + name = var.rg_monitoring + location = var.resources_location +} \ No newline at end of file diff --git a/120_azapi_provider/variables.tf b/120_azapi_provider/variables.tf new file mode 100644 index 0000000..e07d3e6 --- /dev/null +++ b/120_azapi_provider/variables.tf @@ -0,0 +1,29 @@ +variable "resources_location" { + type = string + default = "westeurope" +} + +variable "rg_aks_cluster" { + type = string + default = "rg-aks-cluster" +} + +variable "rg_monitoring" { + type = string + default = "rg-monitoring" +} + +variable "aks_name" { + type = string + default = "aks-cluster" +} + +variable "grafana_name" { + type = string + default = "azure-grafana-13579" +} + +variable "prometheus_name" { + type = string + default = "azure-prometheus" +} diff --git a/121_appservice_domain/Readme.md b/121_appservice_domain/Readme.md new file mode 100644 index 0000000..13a9d4b --- /dev/null +++ b/121_appservice_domain/Readme.md @@ -0,0 +1,127 @@ +# Azure App Service Domain in Terraform + +## News + +This is now available as Terraform module on Terraform Registry: https://registry.terraform.io/modules/HoussemDellai/appservice-domain/azapi/ + +## Problem + +You can create a custom domain name in Azure using App Service Domain service. +You can do that using Azure portal or Azure CLI. +But you cannot do that using Terraform for Azure provider. +Because that is not implemented yet. +Creating a custom domain in infra as code tool like Terraform might not be that much appealing for enterprises. +They would purchase their domain name manually, just once. Infra as code doesn't make lots of sense here. + +However for labs, workshops and demonstrations, this is very useful to make the lab more realistic. + +## Solution + +We'll provide a Terraform implementation for creating a custom domain name using Azure App Service Domain. +We'll use `AzApi` provider to create the resource. More info about AzApi here: https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/azapi_resource. + +The AzApi will call the REST API and pass the required JSON file containing the needed attributes. +Take a look at the REST API for App Service Domain here: https://learn.microsoft.com/en-us/rest/api/appservice/domains/create-or-update + +We also create an Azure DNS Zone to manage and configure the domain name. + +And we create an A record "test" to make sure the configuration works. + +The complete Terraform implementation is in this current folder. +But here is how to use it. + +```terraform +resource "azurerm_dns_zone" "dns_zone" { + name = var.domain_name + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azapi_resource" "appservice_domain" { + type = "Microsoft.DomainRegistration/domains@2022-09-01" + name = var.domain_name + parent_id = azurerm_resource_group.rg.id + location = "global" + schema_validation_enabled = true + + body = jsonencode({ + + properties = { + autoRenew = false + dnsType = "AzureDns" + dnsZoneId = azurerm_dns_zone.dns_zone.id + privacy = false + + consent = { + agreementKeys = ["DNRA"] + agreedBy = var.AgreedBy_IP_v6 # "2a04:cec0:11d9:24c8:8898:3820:8631:d83" + agreedAt = var.AgreedAt_DateTime # "2023-08-10T11:50:59.264Z" + } + +``` + +## Deploy the resources using Terraform + +Choose the custom domain name you want to purchase in the file `terraform.tfvars`. + +Then run the following Terraform commands from within the current folder. + +```powershell +terraform init +terraform plan -out tfplan +terraform apply tfplan +``` + +## Test the deployment + +Verify you have two resources created within the resource group. + + + +Verify that custom domain name works. +You should see the IP address we used in A record which is `1.2.3.4`. + +```powershell +nslookup test. # replace with domain name +# Server: bbox.lan +# Address: 2001:861:5e62:69c0:861e:a3ff:fea2:796c +# Non-authoritative answer: +# Name: test.houssem13.com +# Address: 1.2.3.4 +``` + +## Creating a custom domain name using Azure CLI + +In this lab we used Terraform to create the domain name. +But still you can just use Azure portal or command line. + + + +Make sure you fill the `contact_info.json` file. It is required to create domain name. + +```powershell +az group create -n rg-dns-domain -l westeurope -o table + +az appservice domain create ` + --resource-group rg-dns-domain ` + --hostname "houssem.com" ` + --contact-info=@'contact_info.json' ` + --accept-terms +``` + +## Video tutorial + +Here is a Youtube video explaining how this works: [https://www.youtube.com/watch?v=ptdAcsG2ROI](https://www.youtube.com/watch?v=ptdAcsG2ROI) + +![](https://github.com/HoussemDellai/terraform-azapi-appservice-domain/blob/main/images/youtube.png?raw=true) + +## Important notes + +You should use a Pay-As-You-Go azure subscription to be able to create Azure App Service Domain. +MSDN/VisualStudio and Free Azure subscriptions doesn't work. + +Within the terraform config file, you can change the contact info for the contactAdmin, contactRegistrant, contactBilling and contactTech. +It worked for me when reusing the same contact ! + +## What is next ? + +You can explore App Service Domain with Azure Container Apps (ACA) in this lab: https://github.com/HoussemDellai/aca-course/tree/main/14_aca_custom_domain. \ No newline at end of file diff --git a/121_appservice_domain/appservice_domain.tf b/121_appservice_domain/appservice_domain.tf new file mode 100644 index 0000000..511538d --- /dev/null +++ b/121_appservice_domain/appservice_domain.tf @@ -0,0 +1,85 @@ +# App Service Domain +# REST API reference: https://docs.microsoft.com/en-us/rest/api/appservice/domains/createorupdate +resource "azapi_resource" "appservice_domain" { + type = "Microsoft.DomainRegistration/domains@2022-09-01" + name = var.domain_name + parent_id = azurerm_resource_group.rg.id + location = "global" + schema_validation_enabled = true + + body = jsonencode({ + + properties = { + autoRenew = false + dnsType = "AzureDns" + dnsZoneId = azurerm_dns_zone.dns_zone.id + privacy = false + + consent = { + agreementKeys = ["DNRA"] + agreedBy = var.AgreedBy_IP_v6 # "2a04:cec0:11d9:24c8:8898:3820:8631:d83" + agreedAt = var.AgreedAt_DateTime # "2023-08-10T11:50:59.264Z" + } + + contactAdmin = { + nameFirst = var.contact.nameFirst + nameLast = var.contact.nameLast + email = var.contact.email + phone = var.contact.phone + + addressMailing = { + address1 = var.contact.addressMailing.address1 + city = var.contact.addressMailing.city + state = var.contact.addressMailing.state + country = var.contact.addressMailing.country + postalCode = var.contact.addressMailing.postalCode + } + } + + contactRegistrant = { + nameFirst = var.contact.nameFirst + nameLast = var.contact.nameLast + email = var.contact.email + phone = var.contact.phone + + addressMailing = { + address1 = var.contact.addressMailing.address1 + city = var.contact.addressMailing.city + state = var.contact.addressMailing.state + country = var.contact.addressMailing.country + postalCode = var.contact.addressMailing.postalCode + } + } + + contactBilling = { + nameFirst = var.contact.nameFirst + nameLast = var.contact.nameLast + email = var.contact.email + phone = var.contact.phone + + addressMailing = { + address1 = var.contact.addressMailing.address1 + city = var.contact.addressMailing.city + state = var.contact.addressMailing.state + country = var.contact.addressMailing.country + postalCode = var.contact.addressMailing.postalCode + } + } + + contactTech = { + nameFirst = var.contact.nameFirst + nameLast = var.contact.nameLast + email = var.contact.email + phone = var.contact.phone + + addressMailing = { + address1 = var.contact.addressMailing.address1 + city = var.contact.addressMailing.city + state = var.contact.addressMailing.state + country = var.contact.addressMailing.country + postalCode = var.contact.addressMailing.postalCode + } + } + } + }) +} diff --git a/121_appservice_domain/contact_info.json b/121_appservice_domain/contact_info.json new file mode 100644 index 0000000..c90b5a5 --- /dev/null +++ b/121_appservice_domain/contact_info.json @@ -0,0 +1,59 @@ +{ + "address1": { + "value": "90 avenue des Champs Elysees", + "required": true + }, + "address2": { + "value": "", + "required": false + }, + "city": { + "value": "Paris", + "required": true + }, + "country": { + "value": "FR", + "required": true, + "options": [] + }, + "postal_code": { + "value": "75008", + "required": true + }, + "state": { + "value": "Ile de France", + "required": true + }, + "email": { + "value": "houssem.dellai@email.com", + "required": true + }, + "fax": { + "value": "", + "required": false + }, + "job_title": { + "value": "", + "required": false + }, + "name_first": { + "value": "Houssem", + "required": true + }, + "name_last": { + "value": "Dellai", + "required": true + }, + "name_middle": { + "value": "", + "required": false + }, + "organization": { + "value": "", + "required": false + }, + "phone": { + "value": "+33.762954328", + "required": true + } +} \ No newline at end of file diff --git a/121_appservice_domain/dns_zone.tf b/121_appservice_domain/dns_zone.tf new file mode 100644 index 0000000..b82d056 --- /dev/null +++ b/121_appservice_domain/dns_zone.tf @@ -0,0 +1,14 @@ +# DNS Zone to configure the domain name +resource "azurerm_dns_zone" "dns_zone" { + name = var.domain_name + resource_group_name = azurerm_resource_group.rg.name +} + +# DNS Zone A record +resource "azurerm_dns_a_record" "dns_a_record" { + name = "test" + zone_name = azurerm_dns_zone.dns_zone.name + resource_group_name = azurerm_resource_group.rg.name + ttl = 300 + records = ["1.2.3.4"] # just example IP address +} diff --git a/121_appservice_domain/images/portal.png b/121_appservice_domain/images/portal.png new file mode 100644 index 0000000..0f7ef1c Binary files /dev/null and b/121_appservice_domain/images/portal.png differ diff --git a/121_appservice_domain/images/resources.png b/121_appservice_domain/images/resources.png new file mode 100644 index 0000000..7263b02 Binary files /dev/null and b/121_appservice_domain/images/resources.png differ diff --git a/121_appservice_domain/images/youtube.png b/121_appservice_domain/images/youtube.png new file mode 100644 index 0000000..6da94bd Binary files /dev/null and b/121_appservice_domain/images/youtube.png differ diff --git a/121_appservice_domain/providers.tf b/121_appservice_domain/providers.tf new file mode 100644 index 0000000..27a6e95 --- /dev/null +++ b/121_appservice_domain/providers.tf @@ -0,0 +1,23 @@ +terraform { + + required_version = ">= 1.2.8" + + required_providers { + + azurerm = { + source = "hashicorp/azurerm" + version = "= 3.85.0" + } + + azapi = { + source = "Azure/azapi" + version = "1.11.0" + } + } +} + +provider "azurerm" { + features {} +} + +provider "azapi" {} diff --git a/121_appservice_domain/rg.tf b/121_appservice_domain/rg.tf new file mode 100644 index 0000000..073ef9e --- /dev/null +++ b/121_appservice_domain/rg.tf @@ -0,0 +1,5 @@ +# Resource Group +resource "azurerm_resource_group" "rg" { + name = var.rg_name + location = var.location +} \ No newline at end of file diff --git a/121_appservice_domain/terraform.tfvars b/121_appservice_domain/terraform.tfvars new file mode 100644 index 0000000..809fda3 --- /dev/null +++ b/121_appservice_domain/terraform.tfvars @@ -0,0 +1,19 @@ +domain_name = "houssem13.com" +rg_name = "rg-dns-demo" +location = "westeurope" +AgreedBy_IP_v6 = "2a04:cec0:11d9:24c8:8898:3820:8631:d83" +AgreedAt_DateTime = "2023-08-10T11:50:59.264Z" + +contact = { + nameFirst = "Houssem" + nameLast = "Dellai" + email = "houssem.dellai@live.com" # you'll get verification email + phone = "+33.762954328" + addressMailing = { + address1 = "1 Microsoft Way" + city = "Redmond" + state = "WA" + country = "US" + postalCode = "98052" + } +} diff --git a/121_appservice_domain/variables.tf b/121_appservice_domain/variables.tf new file mode 100644 index 0000000..b197b45 --- /dev/null +++ b/121_appservice_domain/variables.tf @@ -0,0 +1,39 @@ +variable "domain_name" { + type = string + validation { + condition = length(var.domain_name) > 0 && (endswith(var.domain_name, ".com") || endswith(var.domain_name, ".net") || endswith(var.domain_name, ".co.uk") || endswith(var.domain_name, ".org") || endswith(var.domain_name, ".nl") || endswith(var.domain_name, ".in") || endswith(var.domain_name, ".biz") || endswith(var.domain_name, ".org.uk") || endswith(var.domain_name, ".co.in")) + error_message = "Available top level domains are: com, net, co.uk, org, nl, in, biz, org.uk, and co.in" + } +} + +variable "rg_name" { + type = string +} + +variable "location" { + type = string +} + +variable "AgreedBy_IP_v6" { # "2a04:cec0:11d9:24c8:8898:3820:8631:d83" + type = string +} + +variable "AgreedAt_DateTime" { # "2023-08-10T11:50:59.264Z" + type = string +} + +variable "contact" { + type = object({ + nameFirst = string + nameLast = string + email = string + phone = string + addressMailing = object({ + address1 = string + city = string + state = string + country = string + postalCode = string + }) + }) +} diff --git a/130_module_dependencies/Readme.md b/130_module_dependencies/Readme.md new file mode 100644 index 0000000..2046e9e --- /dev/null +++ b/130_module_dependencies/Readme.md @@ -0,0 +1,202 @@ +# Learn by doing: Terraform modules dependencies + +## Introduction + +In this lab, you will deep dive into learning Terraform modules dependencies. +Through examples, you will learn how Terraform manages the chaining of resource creation regarding dependencies. +This will allow you to better understand Terraform behaviour and optimize resource creation time. + +You will work with two sample modules provided under the `\modules` folder: keyvault and storage_account. +Keyvaul module creates an Azure Key vault and a Public IP resources. +Storage_account creates an Azure Storage Account and a Public IP resources. +The Public IP resource has nothing to do with the storage account or key vault. +It is needed just for demoing purposes. + +Both modules depends implicitly on the resource group created at the root resource as they reference the resource group name and location. + +## Scenario 1: no dependencies between modules + +In this scenarion, module storage account does not depend on module keyvault. +There are no explicit or implicit dependency. + +In the following example, the module doesn't depend on any other module. +It just depends on a resource group at the root configuration. + +```hcl +resource "azurerm_resource_group" "rg" { + name = "rg-prod" + location = "westeurope" +} + +module "keyvault" { + source = "./modules/keyvault" + + key_vault_name = "kv123579" + resource_group_name = azurerm_resource_group.rg.name +} + +module "storage_account" { + source = "./modules/storage_account" + + storage_account_name = "strg1235790" + resource_group_name = azurerm_resource_group.rg.name +} +``` + +Let's run two independant modules and see what will happen. +Make sure that only resource group, key vault and storage account are uncommented. + +```terraform +terraform init +terraform apply -auto-approve + +# azurerm_resource_group.rg: Creating... +# azurerm_resource_group.rg: Creation complete after 2s +# module.storage_account.azurerm_public_ip.pip: Creating... +# module.keyvault.azurerm_key_vault.keyvault: Creating... +# module.keyvault.azurerm_public_ip.pip: Creating... +# module.storage_account.azurerm_storage_account.storage: Creating... +# module.keyvault.azurerm_public_ip.pip: Creation complete after 2s +# module.storage_account.azurerm_public_ip.pip: Creation complete after 2s +# module.storage_account.azurerm_storage_account.storage: Creation complete after 22s +# module.keyvault.azurerm_key_vault.keyvault: Creation complete after 2m9s +``` + +Note from the output, because there is no dependencies between modules, the resources will be created in parallel. + +![](images/scenario-1.png) + +> Resources from independant modules will be created in parallel, with no specified order. + +Cleanup the resources before continuing with the next scenario. + +```terraform +terraform destroy -auto-approve +``` + +## Scenario 2: module depends explicitly on another module + +In this scenario, you explore a Terraform keyword called `depends_on`. +This was introduced first to set dependencies between resources. +Starting from version 0.13 of terraform, `depends_on` could be used also for setting dependencies between modules. + +Let's see how that works. +You will have two modules where the second module depends on the first one. + +The syntax in Terraform is the following. + +```hcl +module "storage_account" { + source = "./modules/storage_account" + storage_account_name = "strg1235790" + resource_group_name = azurerm_resource_group.rg.name + depends_on = [ module.keyvault ] # explicit dependency on entire module +} +``` + +To test this scanario, make sure that only resource group, key vault and storage account (scenatrio 2) are uncommented. +Then run the terraform apply command to create the resources. + +```terraform +terraform apply -auto-approve + +# azurerm_resource_group.rg: Creating... +# azurerm_resource_group.rg: Creation complete after 1s +# module.keyvault.azurerm_public_ip.pip: Creating... +# module.keyvault.azurerm_key_vault.keyvault: Creating... +# module.keyvault.azurerm_public_ip.pip: Creation complete after 2s +# module.keyvault.azurerm_key_vault.keyvault: Creation complete after 2m40s +# module.storage_account.azurerm_public_ip.pip: Creating... +# module.storage_account.azurerm_storage_account.storage: Creating... +# module.storage_account.azurerm_public_ip.pip: Creation complete after 3s +# module.storage_account.azurerm_storage_account.storage: Creation complete after 27s +``` + +Note how the resources from the storage account module are created after all the resources from the kayvault module. + +![](images/scenario-2.png) + +>The impact of explicit dependency between modules is that the resources from the dependant module will be delayed until the creation of all resources from original module. + +## Scenario 3: module depends implicitly only on a specific resource from another module + +In this scenario, you will explore an implicit dependency on a specific resource within a module. +An impilict dependency is a direct reference to a resource or data property like the name, location, tags, etc. +Let's run an experiment to see the impact. + +```hcl +module "storage_account" { + source = "./modules/storage_account" + storage_account_name = module.keyvault.key_vault_name # implicit dependency on resource from another module + resource_group_name = azurerm_resource_group.rg.name +} +``` + +Run Terraform command to create the resources. + +```terraform +terraform apply -auto-approve + +# azurerm_resource_group.rg: Creating... +# azurerm_resource_group.rg: Creation complete after 0s +# module.keyvault.azurerm_public_ip.pip: Creating... +# module.storage_account.azurerm_public_ip.pip: Creating... +# module.keyvault.azurerm_key_vault.keyvault: Creating... +# module.keyvault.azurerm_public_ip.pip: Creation complete after 3s +# module.storage_account.azurerm_public_ip.pip: Creation complete after 3s +# module.keyvault.azurerm_key_vault.keyvault: Creation complete after 2m12s +# module.storage_account.azurerm_storage_account.storage: Creating... +# module.storage_account.azurerm_storage_account.storage: Creation complete after 25s +``` + +Note frm the results of this experiment that creation of the dependant resource, which is the the storage account, started only after the creation of the key vault, as it depends implicitly on it. +Resources in both modules still could be created in parallel. +In scenario 1, terraform started by creating 2 resources in parallel. +However, in this scenario, terraform started by creating 3 resources in parallel. +This results in reducing the execution time. + +![](images/scenario-3.png) + +>Dependency on a specific resource from a module results in less execution time than dependency on the entire module. + +## Conclusion + +You learned in this lab the different options and impact for setting up dependencies between modules and resources. +The learning is that you should prefer to use implicit dependency on a specific resource rather than a dependency on an entire module. + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.4 | +| [azurerm](#requirement\_azurerm) | ~> 3.70.0 | + +## Providers + +| Name | Version | +|------|---------| +| [azurerm](#provider\_azurerm) | 3.70.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [keyvault](#module\_keyvault) | ./modules/keyvault | n/a | +| [storage\_account](#module\_storage\_account) | ./modules/storage_account | n/a | + +## Resources + +| Name | Type | +|------|------| +| [azurerm_resource_group.rg](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource | + +## Inputs + +No inputs. + +## Outputs + +No outputs. + \ No newline at end of file diff --git a/130_module_dependencies/images/scenario-1.png b/130_module_dependencies/images/scenario-1.png new file mode 100644 index 0000000..1d6cae9 Binary files /dev/null and b/130_module_dependencies/images/scenario-1.png differ diff --git a/130_module_dependencies/images/scenario-2.png b/130_module_dependencies/images/scenario-2.png new file mode 100644 index 0000000..8796905 Binary files /dev/null and b/130_module_dependencies/images/scenario-2.png differ diff --git a/130_module_dependencies/images/scenario-3.png b/130_module_dependencies/images/scenario-3.png new file mode 100644 index 0000000..18c1c21 Binary files /dev/null and b/130_module_dependencies/images/scenario-3.png differ diff --git a/130_module_dependencies/main.tf b/130_module_dependencies/main.tf new file mode 100644 index 0000000..e852c00 --- /dev/null +++ b/130_module_dependencies/main.tf @@ -0,0 +1,41 @@ +resource "azurerm_resource_group" "rg" { + name = "rg-prod" + location = "westeurope" +} + +module "keyvault" { + source = "./modules/keyvault" + + key_vault_name = "kv123579" + resource_group_name = azurerm_resource_group.rg.name +} + +# Scenario 1: module storage_account does not depend on module keyvault + +# module "storage_account" { +# source = "./modules/storage_account" + +# storage_account_name = "strg1235790" +# resource_group_name = azurerm_resource_group.rg.name +# } + +# Scenario 2: module storage_account depends explicitly on module keyvault + +# module "storage_account" { +# source = "./modules/storage_account" + +# storage_account_name = "strg1235790" +# resource_group_name = azurerm_resource_group.rg.name + +# depends_on = [ module.keyvault ] # explicit dependency on entire module +# } + +# Scenario 3: module storage_account depends implicitly on module keyvault +# Depends only on the public IP from module keyvault + +module "storage_account" { + source = "./modules/storage_account" + + storage_account_name = module.keyvault.key_vault_name # implicit dependency on key_vault_name output + resource_group_name = azurerm_resource_group.rg.name +} \ No newline at end of file diff --git a/130_module_dependencies/modules/keyvault/Readme.md b/130_module_dependencies/modules/keyvault/Readme.md new file mode 100644 index 0000000..e3baec5 --- /dev/null +++ b/130_module_dependencies/modules/keyvault/Readme.md @@ -0,0 +1,38 @@ + +## Requirements + +No requirements. + +## Providers + +| Name | Version | +|------|---------| +| [azurerm](#provider\_azurerm) | n/a | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [azurerm_key_vault.keyvault](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault) | resource | +| [azurerm_public_ip.pip](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/public_ip) | resource | +| [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [key\_vault\_name](#input\_key\_vault\_name) | Name of the key vault | `string` | n/a | yes | +| [location](#input\_location) | Location of the resources | `string` | n/a | yes | +| [resource\_group\_name](#input\_resource\_group\_name) | Name of the resource group | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [key\_vault\_id](#output\_key\_vault\_id) | n/a | +| [key\_vault\_name](#output\_key\_vault\_name) | n/a | + \ No newline at end of file diff --git a/130_module_dependencies/modules/keyvault/main.tf b/130_module_dependencies/modules/keyvault/main.tf new file mode 100644 index 0000000..85df3f5 --- /dev/null +++ b/130_module_dependencies/modules/keyvault/main.tf @@ -0,0 +1,17 @@ +data "azurerm_client_config" "current" {} + +resource "azurerm_key_vault" "keyvault" { + name = var.key_vault_name + resource_group_name = var.resource_group_name + location = "westeurope" + sku_name = "standard" + tenant_id = data.azurerm_client_config.current.tenant_id +} + +resource "azurerm_public_ip" "pip" { + name = "pip-keyvault" + location = "westeurope" + resource_group_name = var.resource_group_name + allocation_method = "Dynamic" + sku = "Basic" +} diff --git a/130_module_dependencies/modules/keyvault/output.tf b/130_module_dependencies/modules/keyvault/output.tf new file mode 100644 index 0000000..085b700 --- /dev/null +++ b/130_module_dependencies/modules/keyvault/output.tf @@ -0,0 +1,7 @@ +output "key_vault_name" { + value = azurerm_key_vault.keyvault.name +} + +output "key_vault_id" { + value = azurerm_key_vault.keyvault.id +} diff --git a/130_module_dependencies/modules/keyvault/provider.tf b/130_module_dependencies/modules/keyvault/provider.tf new file mode 100644 index 0000000..e69de29 diff --git a/130_module_dependencies/modules/keyvault/variables.tf b/130_module_dependencies/modules/keyvault/variables.tf new file mode 100644 index 0000000..0c13aa3 --- /dev/null +++ b/130_module_dependencies/modules/keyvault/variables.tf @@ -0,0 +1,9 @@ +variable resource_group_name { + description = "Name of the resource group" + type = string +} + +variable "key_vault_name" { + description = "Name of the key vault" + type = string +} \ No newline at end of file diff --git a/130_module_dependencies/modules/storage_account/Readme.md b/130_module_dependencies/modules/storage_account/Readme.md new file mode 100644 index 0000000..7c63ee4 --- /dev/null +++ b/130_module_dependencies/modules/storage_account/Readme.md @@ -0,0 +1,37 @@ + +## Requirements + +No requirements. + +## Providers + +| Name | Version | +|------|---------| +| [azurerm](#provider\_azurerm) | n/a | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [azurerm_public_ip.pip](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/public_ip) | resource | +| [azurerm_storage_account.storage](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [location](#input\_location) | Location of the resources | `string` | n/a | yes | +| [resource\_group\_name](#input\_resource\_group\_name) | Name of the resource group | `string` | n/a | yes | +| [storage\_account\_name](#input\_storage\_account\_name) | Name of the storage account | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [storage\_account\_id](#output\_storage\_account\_id) | n/a | +| [storage\_account\_name](#output\_storage\_account\_name) | n/a | + \ No newline at end of file diff --git a/130_module_dependencies/modules/storage_account/main.tf b/130_module_dependencies/modules/storage_account/main.tf new file mode 100644 index 0000000..4fe2431 --- /dev/null +++ b/130_module_dependencies/modules/storage_account/main.tf @@ -0,0 +1,15 @@ +resource "azurerm_storage_account" "storage" { + name = var.storage_account_name + resource_group_name = var.resource_group_name + location = "westeurope" + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource azurerm_public_ip "pip" { + name = "pip-storage" + resource_group_name = var.resource_group_name + location = "westeurope" + allocation_method = "Dynamic" + sku = "Basic" +} \ No newline at end of file diff --git a/130_module_dependencies/modules/storage_account/output.tf b/130_module_dependencies/modules/storage_account/output.tf new file mode 100644 index 0000000..133b58a --- /dev/null +++ b/130_module_dependencies/modules/storage_account/output.tf @@ -0,0 +1,7 @@ +output "storage_account_name" { + value = azurerm_storage_account.storage.name +} + +output "storage_account_id" { + value = azurerm_storage_account.storage.id +} \ No newline at end of file diff --git a/130_module_dependencies/modules/storage_account/provider.tf b/130_module_dependencies/modules/storage_account/provider.tf new file mode 100644 index 0000000..e69de29 diff --git a/130_module_dependencies/modules/storage_account/variables.tf b/130_module_dependencies/modules/storage_account/variables.tf new file mode 100644 index 0000000..2ee8b73 --- /dev/null +++ b/130_module_dependencies/modules/storage_account/variables.tf @@ -0,0 +1,9 @@ +variable "resource_group_name" { + description = "Name of the resource group" + type = string +} + +variable "storage_account_name" { + description = "Name of the storage account" + type = string +} \ No newline at end of file diff --git a/130_module_dependencies/provider.tf b/130_module_dependencies/provider.tf new file mode 100644 index 0000000..995f0d5 --- /dev/null +++ b/130_module_dependencies/provider.tf @@ -0,0 +1,18 @@ +# Configure the Azure provider +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 3.70.0" + } + } + required_version = ">= 1.3.4" +} + +provider "azurerm" { + features { + resource_group { + prevent_deletion_if_contains_resources = false + } + } +} diff --git a/130_module_dependencies/scenario-1.png b/130_module_dependencies/scenario-1.png new file mode 100644 index 0000000..1d6cae9 Binary files /dev/null and b/130_module_dependencies/scenario-1.png differ diff --git a/130_module_dependencies/scenario-2.png b/130_module_dependencies/scenario-2.png new file mode 100644 index 0000000..8796905 Binary files /dev/null and b/130_module_dependencies/scenario-2.png differ diff --git a/140_logic_app/README.md b/140_logic_app/README.md new file mode 100644 index 0000000..3c3c78c --- /dev/null +++ b/140_logic_app/README.md @@ -0,0 +1,203 @@ +# Deploying Logic Apps using Terraform + +## Introduction + +In this lab, you will learn how to create and deploy an Azure Logic App using Terraform. + +Logic Apps are invented to make it easy for developers and also non-developers to create applications called workflows. +It uses a visual designer where you can drag and drop actions instead of writing code. + +Behind the scenes, a JSON ARM template is generated to save the workflow configuration. + +![](images/action-send-email.png) + +You can export that ARM template and use to redeploy the same Logic App workflow. + +This approach is much more easier and faster than creating the Logic App using ARM templates without using the visual designer. +That is because the syntax of the Logic Apps takes a bit long time to understand it. + +## Issue with Terraform provider's support for Logic Apps + +The visual designer generates ARM template. What about Terraform ? + +Terraform defines resources to create a workflow, triggers and actions. +But for the actions it relies on JSON ARM template. And it is a 'headache' to configure. +You will find it much more practical to just use the exported ARM template and deploy it as it is using `azurerm_resource_group_template_deployment`. + +## Creating Logic App using Azure portal + +In order to understand how Logic App works, you will need to create the Logic App workflow manually using the visual designer. +You want to to use Logic App to send an email when you trigger it. + +Start be creating a new Logic App resource in Azure portal. Then go to `Logic app designer` section to start the creation. + +![](images/choose-action.png) + +The workflow will be composed of two components: +- A trigger of type HTTP trigger that accepts a JSON payload containing the email destination and content +- An Action to compose and send email using `outlook` connector, to the destination email from the trigger + +The `outlook` action will use your own outlook user identity to send emails on your behalf. + +The end result should be like this. + +![](images/view-code.png) + +## Deploying Logic App using Terraform and ARM template + +Now that the Logic App ARM template is generated, you can go to export it. + +![](images/view-code-workflow.png) + +You will use that ARM template to deploy it using Terraform with `azurerm_resource_group_template_deployment`. + +```hcl + +resource "azurerm_resource_group_template_deployment" "logic_app" { + name = "logic-app-deploy" + resource_group_name = azurerm_resource_group.rg.name + deployment_mode = "Incremental" # "Complete" # + + parameters_content = jsonencode({ + workflows_logic_app_name = { value = "logic-app-demo-135791555" } + connections_outlook_externalid = { value = azurerm_api_connection.api_connection_outlook.id } + connections_outlook_id = { value = data.azurerm_managed_api.managed_api_outlook.id } + }) + + template_content = <