Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.azure
node_modules
dist
dist
infra-terraform/.terraform.lock.hcl
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ There are multiple ways to deploy this project to Azure. We provide two options
1. via template deployment and VS Code extension. This approach allows an quick deployment to Azure but does not necessarily reflect the best practices. You find more information on this option [here](documentation/DEPLOYMENT-VSCODE.md).
2. via [Azure Developer CLI](https://learn.microsoft.com/azure/developer/azure-developer-cli/overview) using `azd up`. This approach enables a quick and easy deployment and already contains best practices. It serves as a perfect starting point for a production grade setup. You find more information on this option [here](documentation/DEPLOYMENT-AZD.md).

> **Note** - If you choose to deploy to Azure via `azd` we provide two options to define the infrastructure as code, namely `Bicep` and `Terraform`. To make your life easier we provide two distinct [workspaces](https://code.visualstudio.com/docs/editor/workspaces#_multiroot-workspaces) for that to show you the relevant files and folders. If you want to go for `Bicep` you can open the workspace `workspaces/azd-bicep.code-workspace`, for Terraform go for the workspace `workspaces/azd-terraform.code-workspace`.
Copy link
Contributor

@MartinPankraz MartinPankraz Jan 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make the bicep workspace flavor default somehow? Would make things easier on first run with gh codespaces and vsc.

Seems like no easy way present at the moment: microsoft/vscode#161414

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked the documentation and did not find a way to define one workspace as default, neither via the workspace files nor via a vscode configuration we could attach.


## What's next?

You can do a lot more once the app is deployed. Curious? We go you covered with some more information [here](documentation/WHATS-NEXT.md)
Expand Down
4 changes: 4 additions & 0 deletions azure.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ services:
project: ./src/api
language: ts
host: appservice
# Remove the comments from the lines below to enable the deployment of the infrastructure via Terraform
#infra:
# provider: terraform
# path: ./infra-terraform
149 changes: 149 additions & 0 deletions documentation/DEPLOYMENT-AZD-TERRAFORM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Infrastructure Deployment via Azure Developer CLI for Terraform

> **Note** - there are known issues when using the Terraform provider in combination with GitHub Codespaces i.e., the login flow via the Azure CLI gets stuck in the callback. You find the details in this [issue](https://github.com/Azure/azure-dev/pull/1497) of the `azd` repository as well as in this [issue](https://github.com/Azure/azure-dev/pull/1496) in the Azure CLI repository. The described workaround in the second issue did not do the trick for us. Hence, we currently cannot support GitHub Codespaces in this type of infrastructure setup.

The basic steps of using `azd` to setup remain unchanged as described [here](./DEPLOYMENT-AZD.md). There are three spots that differ from a technical perspective, namely:

* The login flow via Azure CLI
* The specification of Terraform as IaC in the `azure.yaml` manifest.
* The definition of the infrastructure via Terraform files and modules

In the following sections describe the points that need to be taken in to account if you want to use Terraform.

## The login flow

To deploy the infrastructure via Terraform `azd` uses the [Azure Terraform Provider](https://github.com/hashicorp/terraform-provider-azurerm) under the hood. This provider (not `azd`) is using the Azure CLI login flow. As a consequence when using Terraform you must enforce `azd` to make use of this flow:

* Configure `azd` to make use of the Azure CLI via the command `azd config set auth.useAzCliAuth true`.
* If you are using the `devcontainer` or GitHub Codespaces, make sure to add the Azure CLI feature in the `devcontainer.json` file. We provide the corresponding template as [/templates/devcontainer.json.azcli](../templates/devcontainer.json.azcli) in this repository.

## Adjustment of manifest

The main file configuring the behavior of `azd` is the `azure.yml` file in the root repository. In order to make use of Terraform we must change it to use it for the infrastructure deployment as the default is bicep. To achieve this add the following section to the file to specify the infrastructure *provider* as well as the *path* to the Terraform files:

```yaml
infra:
provider: terraform
path: ./infra-terraform
```

> **Note** - The default path is `infra`. As we already have our `.bicep` resources defined there, we use a different path that we have to explicitly specify in the manifest.

## Infrastructure setup

The Terraform files are located in the `infra-terraform` folder. As you can see the structure contains the modules provided by the `azd` team containing the Terraform modules that represent the core building blocks of your application. These modules are use in the `main.tf` file to define the setup of our app.

In the following section we present the single files of the Terraform-based setup:

### The "provider.tf" file

This files defines what Terraform provider should be usd in order to do the setup. In addition the provider features for how to handle the purging of an Azure Key Vault as well as how deal with non-empty Resource Groups are defined in this file.

### The "variables.tf" file

This files defines the input variables for the `main.tf` file. They can be seen like function arguments and are used in the setup of the infrastructure to e.g., provide the value for a specific resource configuration.

Here we also placed our quickstarter specific variables that define the access to SAP system's OData services.

### The "output.tf" file

This file defines the output of the provisioning e.g., the URL endpoint of the application. The values correspond to function return values. They will be displayed in the CLI output when the provisioning was successfully executed.

### The "main.tfvars.json" file

This files is the so called **variable definitions** file. This is one way to set the input variables defined in the `variables.tf`.

### The "main.tf" file

This file is the core of the Terraform infrastructure configuration. Here we define the infrastructure setup of our application by combining the different building blocks provided via the Terraform modules. One example is the configuration of the module to provision the Azure App Service:

```tf
module "api" {
source = "./modules/appservicenode"
location = var.location
rg_name = azurerm_resource_group.rg.name
resource_token = local.resource_token

tags = merge(local.tags, { "azd-service-name" : "sap-cloud-sdk-api" })
service_name = "sap-cloud-sdk-api"
appservice_plan_id = module.appserviceplan.APPSERVICE_PLAN_ID
app_settings = {
"SCM_DO_BUILD_DURING_DEPLOYMENT" = "true"
"AZURE_KEY_VAULT_ENDPOINT" = module.keyvault.AZURE_KEY_VAULT_ENDPOINT
"APPLICATIONINSIGHTS_CONNECTION_STRING" = module.applicationinsights.APPLICATIONINSIGHTS_CONNECTION_STRING
"ODATA_URL" = var.oDataUrl
"ODATA_USERNAME" = var.oDataUsername
"ODATA_USERPWD" = "@Microsoft.KeyVault(SecretUri=${module.keyvault.AZURE_KEY_VAULT_ENDPOINT}secrets/${local.abbrKeyVaultVaults}secret-odata-password)"
"APIKEY" = "@Microsoft.KeyVault(SecretUri=${module.keyvault.AZURE_KEY_VAULT_ENDPOINT}secrets/${local.abbrKeyVaultVaults}secret-apikey)"
"APIKEY_HEADERNAME" = var.ApiKeyHeaderName
}

app_command_line = ""

always_on = false
identity = [{
type = "SystemAssigned"
}]
}
```

As you can see we use the variables defined in the `variables.tf` file to configure e.g., the app settings. The components are basically the same as in the Bicep flow, so we do not repeat them in detail here.

## Mind the differences

The Terraform-based setup in the `azd` was introduced a later than the one using Bicep. Due to that and due to the differences between them, some things work differently. Here are two things that we think are worth to mention.

### Purging of Azure Key Vault

When using Bicep to provide your infrastructure you get asked by the CLI if you want to purge you Azure Key Vault when deprovisioning your infrastructure. This is not the case when using Terraform. If you want to make sure that the Key Vault is purged when deprovisioning your infrastructure you must make an adjustment in the `provider.tf` file namely you must set the `purge_soft_delete_on_destroy` to `false`:

```tf
key_vault {
purge_soft_delete_on_destroy = false
}
```

The purging is automatically done once you delete your infrastructure.

> **Note** - We do not recommend the automatic purging of the Azure Key Vault in a productive environment

### Changes in the appservicenode module

In order to enable you to deploy the Azure App Service in the SKU `F1` i.e., the free plan, we had to adjust some modules delivered by the `azd` team. One parameter available in the Bicep variant is missing in the Terraform setup, namely the `site configuration` `always_on`. This is set to `true` which prevents the deployment to a free plan. To enable this we made the following adjustements:

1. Introduction of the parameter as a variable in the `infra-terraform\modules\appservicenode\appservicenode_variables.tf`

```tf
variable "always_on" {
description = "The always on setting for the app service."
type = bool
default = true
}
```

2. Exchanging the hard-coded value for the parameter with the newly defined variable in the `infra-terraform/modules/appservicenode/appservicenode.tf`file:

```tf
resource "azurerm_linux_web_app" "web" {
name = azurecaf_name.web_name.result
location = var.location
resource_group_name = var.rg_name
service_plan_id = var.appservice_plan_id
https_only = true
tags = var.tags

site_config {
always_on = var.always_on
ftps_state = "FtpsOnly"
app_command_line = var.app_command_line
application_stack {
node_version = var.node_version
}
}
```

With these changes we were able to set the variable in the definition of the Azure App Service in the `main.tf` file.

## More information

You find more information about `azd` and Terraform [here](https://learn.microsoft.com/azure/developer/azure-developer-cli/use-terraform-for-azd).
9 changes: 4 additions & 5 deletions documentation/DEPLOYMENT-AZD.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

> **Note** - The repository is compatible with version [0.5.0-beta.3](https://github.com/Azure/azure-dev/releases/tag/azure-dev-cli_0.5.0-beta.3) of the Azure Developer CLI. Make sure that you are using the same version to avoid incompatibilities due to different versions.

> **Warning** - if you are using a **personal Azure account**, the new logon flow provided by `azd` is not yet fully functional, see issue [1398](https://github.com/Azure/azure-dev/issues/1398) in the `azd` repository. You must use the fallback to the Azure CLI logon flow via `azd config set auth.useAzCliAuth true`. If you are using the `devcontainer` or GitHub Codespaces, then make sure to add the Azure CLI feature in the `devcontainer.json` file. We provide the corresponding template as [/templates/devcontainer.json.fallback](../templates/devcontainer.json.fallback) in this repository.

> **Warning** - if you are using a **personal Azure account**, the new login flow provided by `azd` is not yet fully functional, see issue [1398](https://github.com/Azure/azure-dev/issues/1398) in the `azd` repository. You must use the fallback to the Azure CLI login flow via `azd config set auth.useAzCliAuth true`. If you are using the `devcontainer` or GitHub Codespaces, then make sure to add the Azure CLI feature in the `devcontainer.json` file. We provide the corresponding template as [/templates/devcontainer.json.azcli](../templates/devcontainer.json.azcli) in this repository.

In this example we use the [Azure Developer CLI](https://github.com/Azure/azure-dev) to deploy the project. Learn more about [this tool on Microsoft learn](https://learn.microsoft.com/azure/developer/azure-developer-cli/overview)

Expand Down Expand Up @@ -173,11 +172,11 @@ As you can see from the first line of the file, the manifest is backed by a lang

Our setup follows those defaults and there no explicit configuration is necessary.

## Infrastructure as Code - alternatives
## Infrastructure as Code

In our sample project we used `Bicep` to describe the Infrastructure as code. This is a convenient way when dealing with Azure resources. You find more about `Bicep` in the [official documentation](https://learn.microsoft.com/azure/azure-resource-manager/bicep/).
We use `Bicep` as default option to describe the Infrastructure as code. This is a convenient way when dealing with Azure resources. You find more about `Bicep` in the [official documentation](https://learn.microsoft.com/azure/azure-resource-manager/bicep/).

Besides `Bicep` the `azd` also supports the definition of the infrastructure via [Terraform](https://www.terraform.io/). This is not reflected in our sample repository but feel free to explore it as described [here](https://learn.microsoft.com/azure/developer/azure-developer-cli/use-terraform-for-azd).
Besides `Bicep` the `azd` CLI also supports the definition of the infrastructure via [Terraform](https://www.terraform.io/). To give you the freedom of choice this repository also contains the infrastructure definition via Terraform modules. You find the instructions [here](./DEPLOYMENT-AZD-TERRAFORM.md)

## Cleanup

Expand Down
114 changes: 114 additions & 0 deletions infra-terraform/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
locals {
tags = { azd-env-name : var.environment_name }
sha = base64encode(sha256("${var.environment_name}${var.location}${data.azurerm_client_config.current.subscription_id}"))
resource_token = substr(replace(lower(local.sha), "[^A-Za-z0-9_]", ""), 0, 13)
abbr_key_vault_vaults = "kv-"
always_on = var.sku_name == "F1" || var.sku_name == "FREE" || var.sku_name == "SHARED" ? false : true
use_32_bit_worker = var.sku_name == "F1" || var.sku_name == "FREE" || var.sku_name == "SHARED" ? true : false
}
# ------------------------------------------------------------------------------------------------------
# Deploy resource Group
# ------------------------------------------------------------------------------------------------------
resource "azurecaf_name" "rg_name" {
name = var.environment_name
resource_type = "azurerm_resource_group"
random_length = 0
clean_input = true
}

resource "azurerm_resource_group" "rg" {
name = azurecaf_name.rg_name.result
location = var.location
tags = local.tags
}

# ------------------------------------------------------------------------------------------------------
# Deploy application insights
# ------------------------------------------------------------------------------------------------------
module "applicationinsights" {
source = "./modules/applicationinsights"
location = var.location
rg_name = azurerm_resource_group.rg.name
environment_name = var.environment_name
workspace_id = module.loganalytics.LOGANALYTICS_WORKSPACE_ID
tags = azurerm_resource_group.rg.tags
resource_token = local.resource_token
}

# ------------------------------------------------------------------------------------------------------
# Deploy log analytics
# ------------------------------------------------------------------------------------------------------
module "loganalytics" {
source = "./modules/loganalytics"
location = var.location
rg_name = azurerm_resource_group.rg.name
tags = azurerm_resource_group.rg.tags
resource_token = local.resource_token
}

# ------------------------------------------------------------------------------------------------------
# Deploy key vault
# ------------------------------------------------------------------------------------------------------
module "keyvault" {
source = "./modules/keyvault"
location = var.location
principal_id = var.principal_id
rg_name = azurerm_resource_group.rg.name
tags = azurerm_resource_group.rg.tags
resource_token = local.resource_token
access_policy_object_ids = [module.api.IDENTITY_PRINCIPAL_ID]
secrets = [
{
name = "${local.abbr_key_vault_vaults}secret-odata-password"
value = var.oDataUserpwd
},
{
name = "${local.abbr_key_vault_vaults}secret-apikey"
value = var._APIKey
}
]
}

# ------------------------------------------------------------------------------------------------------
# Deploy app service plan
# ------------------------------------------------------------------------------------------------------
module "appserviceplan" {
source = "./modules/appserviceplan"
location = var.location
rg_name = azurerm_resource_group.rg.name
tags = azurerm_resource_group.rg.tags
resource_token = local.resource_token
sku_name = var.sku_name
}

# ------------------------------------------------------------------------------------------------------
# Deploy app service api
# ------------------------------------------------------------------------------------------------------
module "api" {
source = "./modules_local/appservicenode"
location = var.location
rg_name = azurerm_resource_group.rg.name
resource_token = local.resource_token

tags = merge(local.tags, { "azd-service-name" : "sap-cloud-sdk-api" })
service_name = "sap-cloud-sdk-api"
appservice_plan_id = module.appserviceplan.APPSERVICE_PLAN_ID
app_settings = {
"SCM_DO_BUILD_DURING_DEPLOYMENT" = "true"
"AZURE_KEY_VAULT_ENDPOINT" = module.keyvault.AZURE_KEY_VAULT_ENDPOINT
"APPLICATIONINSIGHTS_CONNECTION_STRING" = module.applicationinsights.APPLICATIONINSIGHTS_CONNECTION_STRING
"ODATA_URL" = var.oDataUrl
"ODATA_USERNAME" = var.oDataUsername
"ODATA_USERPWD" = "@Microsoft.KeyVault(SecretUri=${module.keyvault.AZURE_KEY_VAULT_ENDPOINT}secrets/${local.abbr_key_vault_vaults}secret-odata-password)"
"APIKEY" = "@Microsoft.KeyVault(SecretUri=${module.keyvault.AZURE_KEY_VAULT_ENDPOINT}secrets/${local.abbr_key_vault_vaults}secret-apikey)"
"APIKEY_HEADERNAME" = var.ApiKeyHeaderName
}

app_command_line = ""
health_check_path = var.health_check_path
always_on = local.always_on
use_32_bit_worker = local.use_32_bit_worker
identity = [{
type = "SystemAssigned"
}]
}
5 changes: 5 additions & 0 deletions infra-terraform/main.tfvars.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"location": "${AZURE_LOCATION}",
"environment_name": "${AZURE_ENV_NAME}",
"principal_id": "${AZURE_PRINCIPAL_ID}"
}
Loading