diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index bf89629..950fd1e 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -18,14 +18,24 @@ jobs:
run: |
terraform init
terraform graph | dot -Tpng >graph.png
- git push
shell: bash
working-directory: terraform/azure-devops/create-service-connection
- name: Render terraform docs and push changes back to PR
uses: terraform-docs/gh-actions@main
with:
+ config-file: doc-gen/.terraform-docs.yml
working-dir: terraform/azure-devops/create-service-connection
- output-file: README.md
- output-method: inject
- git-push: "true"
\ No newline at end of file
+ output-format: markdown table
+ git-push: false
+
+ - name: Commit changes
+ run: |
+ git add graph.png
+ git add README.md
+ git config --local user.email ""
+ git config --local user.name "GitHub Actions"
+ git diff-index --quiet HEAD || git commit -m "Update docs"
+ git push
+ shell: bash
+ working-directory: terraform/azure-devops/create-service-connection
\ No newline at end of file
diff --git a/terraform/azure-devops/create-service-connection/.terraform.lock.hcl b/terraform/azure-devops/create-service-connection/.terraform.lock.hcl
index dc451aa..3656db1 100644
--- a/terraform/azure-devops/create-service-connection/.terraform.lock.hcl
+++ b/terraform/azure-devops/create-service-connection/.terraform.lock.hcl
@@ -23,22 +23,22 @@ provider "registry.terraform.io/hashicorp/azuread" {
}
provider "registry.terraform.io/hashicorp/azurerm" {
- version = "3.99.0"
+ version = "3.101.0"
constraints = "~> 3.66"
hashes = [
- "h1:dawmYJUMGlL3t1mKDyaLJc08uSxPaUBoCAb/YCbVxPM=",
- "h1:yHNaEhlR3kqlItAXFLWlIH2xxu4i7r2XzQnS04f/qBo=",
- "zh:20581c1f4c586a37af45ed4c2a86ff4d868cee79139a755bd29750d804cee3ef",
- "zh:28b3cc4e5f8bc65a595eab011d5965203a39e92aa9e26df842ffc979305ac823",
- "zh:4cb167f8bb82f9065b7b50d012be3045fce3c699b0ea0e257ad1995441227f72",
- "zh:6fa5c6fa430921a4e0fe8d44eaf12210fb90afdf3f83cedfde1c691ae36e953c",
- "zh:75eff5b0ea9fca46ed5a0425c5e33fbda470e6448917817e80ae898688568665",
- "zh:9af0aeaa74bfc764c60eec7d212d31deb70e03e970d22449f11170f75108f9cf",
- "zh:b5055767199a2927d41b543a16e905c1e0b209f14a2144c756786194e133b41d",
- "zh:c3e30b0eed068a148498ac78a9e013bc2eef0eb3cc3b4484f77421d64a797dc2",
- "zh:ce87cd35cef9e5805f921978a91a7a4e139e8cbc7674a94076cb1a20a0c2feb1",
- "zh:d87b84f144c865145bd10093ead99b653ea363fd4e7315675727659ca78544d0",
- "zh:ee5900a50d69e046aab6581f6d888014b3f8d543e5b17c50761579d3370935f2",
+ "h1:EBBVeOrjBsTGX/0l3GCBoCk0K04MtKgJ36Sf4jwPh9g=",
+ "h1:Jrkhx+qKaf63sIV/WvE8sPR53QuC16pvTrBjxFVMPYM=",
+ "zh:38b02bce5cbe83f938a71716bbf9e8b07fed8b2c6b83c19b5e708eda7dee0f1d",
+ "zh:3ed094366ab35c4fcd632471a7e45a84ca6c72b00477cdf1276e541a0171b369",
+ "zh:62bf7cde429f465173e40eebb6840f4e380dfc9dcec2d89dbcb6ce5bce379e50",
+ "zh:90761096666575f0a21275822011e08d72389a575f45e4c1c8e1d26c3b794750",
+ "zh:9494acbacc2b67cf87ae510862ca2c826d0e04662274477f8de1707cefa7c0f3",
+ "zh:9a01128004eab67ed90e9decb92271c187e95e0d6e9f136b5bbc8bf3a2189d41",
+ "zh:9e4eed599cecc2b2aff4dc334b154aad0ad80b5a07439139fc28b22fcff0c8aa",
+ "zh:a5f940e5b8b813b18d9ecd974fdda1ae989870a8a5d897fda8cff4c5368e6e24",
+ "zh:bc7c6bfad523f6c0fad7ef9f8d4c264f72cb9f29fce3a69f8483c63e70eb5085",
+ "zh:d9ba2c6bd082775e6d2d6453486ebb3ecc86ecf127e1d86eddf1a952b545c04e",
+ "zh:e288cce3c324a26d1e01a83e3fe2215537075ab897364539b6cabba298122654",
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
]
}
@@ -86,23 +86,23 @@ provider "registry.terraform.io/hashicorp/http" {
}
provider "registry.terraform.io/hashicorp/random" {
- version = "3.6.0"
+ version = "3.6.1"
constraints = "~> 3.5"
hashes = [
- "h1:I8MBeauYA8J8yheLJ8oSMWqB0kovn16dF/wKZ1QTdkk=",
- "h1:R5Ucn26riKIEijcsiOMBR3uOAjuOMfI1x7XvH4P6B1w=",
- "zh:03360ed3ecd31e8c5dac9c95fe0858be50f3e9a0d0c654b5e504109c2159287d",
- "zh:1c67ac51254ba2a2bb53a25e8ae7e4d076103483f55f39b426ec55e47d1fe211",
- "zh:24a17bba7f6d679538ff51b3a2f378cedadede97af8a1db7dad4fd8d6d50f829",
- "zh:30ffb297ffd1633175d6545d37c2217e2cef9545a6e03946e514c59c0859b77d",
- "zh:454ce4b3dbc73e6775f2f6605d45cee6e16c3872a2e66a2c97993d6e5cbd7055",
+ "h1:1OlP753r4lOKlBprL0HdZGWerm5DCabD5Mli8k8lWAg=",
+ "h1:a+Goawwh6Qtg4/bRWzfDtIdrEFfPlnVy0y4LdUQY3nI=",
+ "zh:2a0ec154e39911f19c8214acd6241e469157489fc56b6c739f45fbed5896a176",
+ "zh:57f4e553224a5e849c99131f5e5294be3a7adcabe2d867d8a4fef8d0976e0e52",
+ "zh:58f09948c608e601bd9d0a9e47dcb78e2b2c13b4bda4d8f097d09152ea9e91c5",
+ "zh:5c2a297146ed6fb3fe934c800e78380f700f49ff24dbb5fb5463134948e3a65f",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
- "zh:91df0a9fab329aff2ff4cf26797592eb7a3a90b4a0c04d64ce186654e0cc6e17",
- "zh:aa57384b85622a9f7bfb5d4512ca88e61f22a9cea9f30febaa4c98c68ff0dc21",
- "zh:c4a3e329ba786ffb6f2b694e1fd41d413a7010f3a53c20b432325a94fa71e839",
- "zh:e2699bc9116447f96c53d55f2a00570f982e6f9935038c3810603572693712d0",
- "zh:e747c0fd5d7684e5bfad8aa0ca441903f15ae7a98a737ff6aca24ba223207e2c",
- "zh:f1ca75f417ce490368f047b63ec09fd003711ae48487fba90b4aba2ccf71920e",
+ "zh:7ce41e26f0603e31cdac849085fc99e5cd5b3b73414c6c6d955c0ceb249b593f",
+ "zh:8c9e8d30c4ef08ee8bcc4294dbf3c2115cd7d9049c6ba21422bd3471d92faf8a",
+ "zh:93e91be717a7ffbd6410120eb925ebb8658cc8f563de35a8b53804d33c51c8b0",
+ "zh:982542e921970d727ce10ed64795bf36c4dec77a5db0741d4665230d12250a0d",
+ "zh:b9d1873f14d6033e216510ef541c891f44d249464f13cc07d3f782d09c7d18de",
+ "zh:cfe27faa0bc9556391c8803ade135a5856c34a3fe85b9ae3bdd515013c0c87c1",
+ "zh:e4aabf3184bbb556b89e4b195eab1514c86a2914dd01c23ad9813ec17e863a8a",
]
}
diff --git a/terraform/azure-devops/create-service-connection/README.md b/terraform/azure-devops/create-service-connection/README.md
index 0d7cbe3..462fd2e 100644
--- a/terraform/azure-devops/create-service-connection/README.md
+++ b/terraform/azure-devops/create-service-connection/README.md
@@ -1,5 +1,7 @@
-# Terraform managed Azure Service Connection
+# Terraform-managed Azure Service Connection
+
+[![Build Status](https://dev.azure.com/geekzter/Pipeline%20Playground/_apis/build/status%2Fcreate-service-connection?branchName=main&label=terraform-ci)](https://dev.azure.com/geekzter/Pipeline%20Playground/_build/latest?definitionId=5&branchName=main)
Many large customers have additional requirements around the management of the Entra ID object that a service connection creates and the permissions it is assigned to.
@@ -12,6 +14,8 @@ These are a few common requirements and constraints:
- Co-owners are required to exist for Entra ID app registrations
- The organization has an IT fulfillment process where identities are automatically created based on a service request
+## Why Terraform?
+
Terraform employs a provider model which enable all changes to be made by a single tool and configuration:
| Service | Provider | API |
@@ -20,61 +24,125 @@ Terraform employs a provider model which enable all changes to be made by a sing
| Azure DevOps | [azuredevops](https://registry.terraform.io/providers/microsoft/azuredevops/latest/docs) | [Azure DevOps REST API](https://learn.microsoft.com/rest/api/azure/devops/serviceendpoint/endpoints) |
| Entra ID | [azuread](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs) | [Microsoft Graph API](https://learn.microsoft.com/graph/use-the-api) |
-## Provisioning
-
Terraform is a declarative tool that is capable if inferring dependencies to create resources in the correct order. This is the output from `terraform graph`:
![Terraform graph](graph.png)
-Provisioning is a matter of specifying [variables](https://developer.hashicorp.com/terraform/language/values/variables) (see [inputs](#input_azdo_organization_url) below) and running `terraform apply`.
+More information:
+
+- [Overview of Terraform on Azure - What is Terraform?](https://learn.microsoft.com/azure/developer/terraform/overview)
+- [Cloud Adoption Framework Infrastructure-as-Code CI/CD security guidance](https://learn.microsoft.com/azure/cloud-adoption-framework/secure/best-practices/secure-devops)
-- To understand how the Terraform configuration can be created in automation, review
+## Provisioning
+
+Provisioning is a matter of specifying [variables](https://developer.hashicorp.com/terraform/language/values/variables) (see [inputs](#input_azdo_organization_url) below) and running `terraform apply`. To understand how the Terraform configuration can be created in automation, review
[tf_create_azurerm_service_connection.ps1](../../../scripts/azure-devops/tf_create_azurerm_service_connection.ps1) and the
[CI pipeline](azure-pipelines.yml).
-- For more information on using Terraform with Azure and other Microsoft services, see [Overview of Terraform on Azure - What is Terraform?](https://learn.microsoft.com/azure/developer/terraform/overview)
-- For infrastructure-as-code best practices, review [Securing the pipeline and CI/CD workflow](https://learn.microsoft.com/azure/cloud-adoption-framework/secure/best-practices/secure-devops).
+
+### Examples
+
+Terraform variable can be provided as a .auto.tfvars file, see [sample](config.auto.tfvars.sample).
+
+#### App registration with Federated Credential and ITSM data
+
+```hcl
+azdo_creates_identity = false
+azure_role_assignments = [
+ {
+ scope = "/subscriptions/00000000-0000-0000-0000-000000000000"
+ role = "Contributor"
+ },
+ {
+ scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg"
+ role = "Storage Blob Data Contributor"
+ }
+]
+azdo_organization_url = "https://dev.azure.com/my-organization"
+azdo_project_name = "my-project"
+create_federation = true
+create_managed_identity = false
+entra_owner_object_ids = ["00000000-0000-0000-0000-000000000000","11111111-1111-1111-1111-111111111111"]
+entra_service_management_reference = "11111111-1111-1111-1111-111111111111"
+```
+
+#### App registration with short-lived secret
+
+```hcl
+azdo_creates_identity = false
+azdo_organization_url = "https://dev.azure.com/my-organization"
+azdo_project_name = "my-project"
+azure_role_assignments = [
+ {
+ scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg"
+ role = "Reader"
+ }
+]
+create_federation = false
+create_managed_identity = false
+entra_secret_expiration_days = 0 # secret lasts 1 hour
+```
+
+#### Managed Identity with Federated Credential
+
+```hcl
+azdo_creates_identity = false
+azdo_organization_url = "https://dev.azure.com/my-organization"
+azdo_project_name = "my-project"
+azure_role_assignments = [
+ {
+ scope = "/subscriptions/00000000-0000-0000-0000-000000000000"
+ role = "Contributor"
+ },
+ {
+ scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg"
+ role = "Key Vault Secrets User"
+ }
+]
+create_federation = true
+create_managed_identity = true
+managed_identity_resource_group_id = "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/msi-rg"
+```
## Terraform Configuration
-## Providers
+Generated with [terraform-docs](https://terraform-docs.io/).
+
+### Providers
| Name | Version |
|------|---------|
-| [azurerm](#provider_azurerm) | 3.99.0 |
+| [azurerm](#provider_azurerm) | 3.101.0 |
| [external](#provider_external) | 2.3.3 |
-| [random](#provider_random) | 3.6.0 |
+| [random](#provider_random) | 3.6.1 |
| [terraform](#provider_terraform) | n/a |
-## Modules
+### Modules
| Name | Source | Version |
|------|--------|---------|
-| [azure_access](#module_azure_access) | ./modules/azure-access | n/a |
| [azure_role_assignments](#module_azure_role_assignments) | ./modules/azure-access | n/a |
| [entra_app](#module_entra_app) | ./modules/app-registration | n/a |
| [managed_identity](#module_managed_identity) | ./modules/managed-identity | n/a |
| [service_connection](#module_service_connection) | ./modules/service-connection | n/a |
-## Inputs
-
-| Name | Description | Type |
-|------|-------------|------|
-| [azdo_organization_url](#input_azdo_organization_url) | The Azure DevOps organization URL (e.g. https://dev.azure.com/contoso) | `string` |
-| [azdo_project_name](#input_azdo_project_name) | The Azure DevOps project name to create the service connection in | `string` |
-| [azdo_creates_identity](#input_azdo_creates_identity) | Let Azure DevOps create identity for service connection | `bool` |
-| [azure_role](#input_azure_role) | The Azure RBAC role to assign to the service connection's identity | `string` |
-| [azure_role_assignments](#input_azure_role_assignments) | Additional role assignments to create for the service connection's identity | `set(object({scope=string, role=string}))` |
-| [azure_scope](#input_azure_scope) | The Azure scope to assign access to | `string` |
-| [create_federation](#input_create_federation) | Use workload identity federation instead of a App Registration secret | `bool` |
-| [create_managed_identity](#input_create_managed_identity) | Creates a Managed Identity instead of a App Registration | `bool` |
-| [entra_owner_object_ids](#input_entra_owner_object_ids) | Object ids of the users that will be co-owners of the Entra ID app registration | `list(string)` |
-| [entra_secret_expiration_days](#input_entra_secret_expiration_days) | Secret expiration in days | `number` |
-| [entra_service_management_reference](#input_entra_service_management_reference) | IT Service Management Reference to add to the App Registration | `string` |
-| [managed_identity_resource_group_id](#input_managed_identity_resource_group_id) | The resource group to create the Managed Identity in | `string` |
-| [resource_prefix](#input_resource_prefix) | The prefix to put in front of resource names created | `string` |
-| [resource_suffix](#input_resource_suffix) | The suffix to append to resource names created | `string` |
-| [run_id](#input_run_id) | The ID that identifies the pipeline / workflow that invoked Terraform (used in CI/CD) | `number` |
-
-## Outputs
+### Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [azdo_organization_url](#input_azdo_organization_url) | The Azure DevOps organization URL (e.g. https://dev.azure.com/contoso) | `string` | n/a | yes |
+| [azdo_project_name](#input_azdo_project_name) | The Azure DevOps project name to create the service connection in | `string` | n/a | yes |
+| [azdo_creates_identity](#input_azdo_creates_identity) | Let Azure DevOps create identity for service connection | `bool` | `false` | no |
+| [azure_role_assignments](#input_azure_role_assignments) | Role assignments to create for the service connection's identity. If this is empty, the Contributor role will be assigned on the azurerm provider subscription. | `set(object({scope=string, role=string}))` | `[]` | no |
+| [create_federation](#input_create_federation) | Use workload identity federation instead of a App Registration secret | `bool` | `true` | no |
+| [create_managed_identity](#input_create_managed_identity) | Creates a Managed Identity instead of a App Registration | `bool` | `true` | no |
+| [entra_owner_object_ids](#input_entra_owner_object_ids) | Object ids of the users that will be co-owners of the Entra ID app registration | `list(string)` | `null` | no |
+| [entra_secret_expiration_days](#input_entra_secret_expiration_days) | Secret expiration in days | `number` | `90` | no |
+| [entra_service_management_reference](#input_entra_service_management_reference) | IT Service Management Reference to add to the App Registration | `string` | `null` | no |
+| [managed_identity_resource_group_id](#input_managed_identity_resource_group_id) | The resource group to create the Managed Identity in | `string` | `null` | no |
+| [resource_prefix](#input_resource_prefix) | The prefix to put in front of resource names created | `string` | `"demo"` | no |
+| [resource_suffix](#input_resource_suffix) | The suffix to append to resource names created | `string` | `""` | no |
+| [run_id](#input_run_id) | The ID that identifies the pipeline / workflow that invoked Terraform (used in CI/CD) | `number` | `null` | no |
+
+### Outputs
| Name | Description |
|------|-------------|
@@ -82,9 +150,7 @@ Provisioning is a matter of specifying [variables](https://developer.hashicorp.c
| [azdo_service_connection_id](#output_azdo_service_connection_id) | The Azure DevOps service connection id |
| [azdo_service_connection_name](#output_azdo_service_connection_name) | The Azure DevOps service connection name |
| [azdo_service_connection_url](#output_azdo_service_connection_url) | The Azure DevOps service connection portal URL |
-| [azure_resource_group_name](#output_azure_resource_group_name) | The name of the resource group the service connection was granted access to |
-| [azure_scope](#output_azure_scope) | The Azure scope the service connection was granted access to |
-| [azure_scope_url](#output_azure_scope_url) | The Azure scope portal URL the service connection was granted access to |
+| [azure_role_assignments](#output_azure_role_assignments) | Role assignments created for the service connection's identity |
| [azure_subscription_id](#output_azure_subscription_id) | The Azure subscription id the service connection was granted access to |
| [azure_subscription_name](#output_azure_subscription_name) | The Azure subscription name the service connection was granted access to |
| [identity_application_id](#output_identity_application_id) | The app/client id of the service connection's identity |
diff --git a/terraform/azure-devops/create-service-connection/azure-pipelines.yml b/terraform/azure-devops/create-service-connection/azure-pipelines.yml
index 9f8cba9..dd26ae2 100644
--- a/terraform/azure-devops/create-service-connection/azure-pipelines.yml
+++ b/terraform/azure-devops/create-service-connection/azure-pipelines.yml
@@ -19,7 +19,7 @@ parameters:
- name: concurrency
displayName: Concurrency
type: number
- default: 2
+ default: 1
- name: testServiceConnection
displayName: Test Service Connection(s)
type: boolean
diff --git a/terraform/azure-devops/create-service-connection/config.auto.tfvars.sample b/terraform/azure-devops/create-service-connection/config.auto.tfvars.sample
new file mode 100755
index 0000000..50d5798
--- /dev/null
+++ b/terraform/azure-devops/create-service-connection/config.auto.tfvars.sample
@@ -0,0 +1,30 @@
+# Rename to .auto.tfvars to process automatically
+
+azdo_creates_identity = false
+azdo_organization_url = "https://dev.azure.com/myorg"
+azdo_project_name = "my-organization"
+azure_role_assignments = [
+ {
+ scope = "/subscriptions/00000000-0000-0000-0000-000000000000"
+ role = "Contributor"
+ },
+ {
+ scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg"
+ role = "AcrPush"
+ },
+ {
+ scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg"
+ role = "Key Vault Secrets User"
+ },
+ {
+ scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg"
+ role = "Storage Blob Data Contributor"
+ }
+]
+create_federation = true
+create_managed_identity = true
+entra_owner_object_ids = ["00000000-0000-0000-0000-000000000000","11111111-1111-1111-1111-111111111111"]
+entra_secret_expiration_days = 0
+entra_service_management_reference = "11111111-1111-1111-1111-111111111111"
+managed_identity_resource_group_id = "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/my-service-connections"
+resource_prefix = "myalias"
diff --git a/terraform/azure-devops/create-service-connection/.terraform-docs.yml b/terraform/azure-devops/create-service-connection/doc-gen/.terraform-docs.yml
similarity index 91%
rename from terraform/azure-devops/create-service-connection/.terraform-docs.yml
rename to terraform/azure-devops/create-service-connection/doc-gen/.terraform-docs.yml
index 662ce40..a98dc3d 100644
--- a/terraform/azure-devops/create-service-connection/.terraform-docs.yml
+++ b/terraform/azure-devops/create-service-connection/doc-gen/.terraform-docs.yml
@@ -58,10 +58,10 @@ sections:
#
# {{ .Inputs }}
-# # see: https://terraform-docs.io/user-guide/configuration/output
-# output:
-# file: README.md
-# mode: inject
+# see: https://terraform-docs.io/user-guide/configuration/output
+output:
+ file: README.md
+ mode: inject
# template: |-
#
# The template can be customized with aribitrary markdown content.
@@ -85,8 +85,10 @@ sort:
# see: https://terraform-docs.io/user-guide/configuration/settings
settings:
- indent: 4
+ indent: 3
escape: false
- default: false
- required: false
+ default: true
+ lockfile: true
+ required: true
+ sensitive: true
type: true
\ No newline at end of file
diff --git a/terraform/azure-devops/create-service-connection/doc-gen/header.md b/terraform/azure-devops/create-service-connection/doc-gen/header.md
index f84be32..840db54 100644
--- a/terraform/azure-devops/create-service-connection/doc-gen/header.md
+++ b/terraform/azure-devops/create-service-connection/doc-gen/header.md
@@ -1,4 +1,6 @@
-# Terraform managed Azure Service Connection
+# Terraform-managed Azure Service Connection
+
+[![Build Status](https://dev.azure.com/geekzter/Pipeline%20Playground/_apis/build/status%2Fcreate-service-connection?branchName=main&label=terraform-ci)](https://dev.azure.com/geekzter/Pipeline%20Playground/_build/latest?definitionId=5&branchName=main)
Many large customers have additional requirements around the management of the Entra ID object that a service connection creates and the permissions it is assigned to.
@@ -11,6 +13,8 @@ These are a few common requirements and constraints:
- Co-owners are required to exist for Entra ID app registrations
- The organization has an IT fulfillment process where identities are automatically created based on a service request
+## Why Terraform?
+
Terraform employs a provider model which enable all changes to be made by a single tool and configuration:
| Service | Provider | API |
@@ -19,19 +23,84 @@ Terraform employs a provider model which enable all changes to be made by a sing
| Azure DevOps | [azuredevops](https://registry.terraform.io/providers/microsoft/azuredevops/latest/docs) | [Azure DevOps REST API](https://learn.microsoft.com/rest/api/azure/devops/serviceendpoint/endpoints) |
| Entra ID | [azuread](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs) | [Microsoft Graph API](https://learn.microsoft.com/graph/use-the-api) |
-
-## Provisioning
-
Terraform is a declarative tool that is capable if inferring dependencies to create resources in the correct order. This is the output from `terraform graph`:
![Terraform graph](graph.png)
-Provisioning is a matter of specifying [variables](https://developer.hashicorp.com/terraform/language/values/variables) (see [inputs](#input_azdo_organization_url) below) and running `terraform apply`.
+More information:
+
+- [Overview of Terraform on Azure - What is Terraform?](https://learn.microsoft.com/azure/developer/terraform/overview)
+- [Cloud Adoption Framework Infrastructure-as-Code CI/CD security guidance](https://learn.microsoft.com/azure/cloud-adoption-framework/secure/best-practices/secure-devops)
+
+## Provisioning
-- To understand how the Terraform configuration can be created in automation, review
+Provisioning is a matter of specifying [variables](https://developer.hashicorp.com/terraform/language/values/variables) (see [inputs](#input_azdo_organization_url) below) and running `terraform apply`. To understand how the Terraform configuration can be created in automation, review
[tf_create_azurerm_service_connection.ps1](../../../scripts/azure-devops/tf_create_azurerm_service_connection.ps1) and the
[CI pipeline](azure-pipelines.yml).
-- For more information on using Terraform with Azure and other Microsoft services, see [Overview of Terraform on Azure - What is Terraform?](https://learn.microsoft.com/azure/developer/terraform/overview)
-- For infrastructure-as-code best practices, review [Securing the pipeline and CI/CD workflow](https://learn.microsoft.com/azure/cloud-adoption-framework/secure/best-practices/secure-devops).
+### Examples
+
+Terraform variable can be provided as a .auto.tfvars file, see [sample](config.auto.tfvars.sample).
+
+#### App registration with Federated Credential and ITSM data
+
+```hcl
+azdo_creates_identity = false
+azure_role_assignments = [
+ {
+ scope = "/subscriptions/00000000-0000-0000-0000-000000000000"
+ role = "Contributor"
+ },
+ {
+ scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg"
+ role = "Storage Blob Data Contributor"
+ }
+]
+azdo_organization_url = "https://dev.azure.com/my-organization"
+azdo_project_name = "my-project"
+create_federation = true
+create_managed_identity = false
+entra_owner_object_ids = ["00000000-0000-0000-0000-000000000000","11111111-1111-1111-1111-111111111111"]
+entra_service_management_reference = "11111111-1111-1111-1111-111111111111"
+```
+
+#### App registration with short-lived secret
+
+```hcl
+azdo_creates_identity = false
+azdo_organization_url = "https://dev.azure.com/my-organization"
+azdo_project_name = "my-project"
+azure_role_assignments = [
+ {
+ scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg"
+ role = "Reader"
+ }
+]
+create_federation = false
+create_managed_identity = false
+entra_secret_expiration_days = 0 # secret lasts 1 hour
+```
+
+#### Managed Identity with Federated Credential
+
+```hcl
+azdo_creates_identity = false
+azdo_organization_url = "https://dev.azure.com/my-organization"
+azdo_project_name = "my-project"
+azure_role_assignments = [
+ {
+ scope = "/subscriptions/00000000-0000-0000-0000-000000000000"
+ role = "Contributor"
+ },
+ {
+ scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg"
+ role = "Key Vault Secrets User"
+ }
+]
+create_federation = true
+create_managed_identity = true
+managed_identity_resource_group_id = "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/msi-rg"
+```
+
+## Terraform Configuration
-## Terraform Configuration
\ No newline at end of file
+Generated with [terraform-docs](https://terraform-docs.io/).
\ No newline at end of file
diff --git a/terraform/azure-devops/create-service-connection/graph.png b/terraform/azure-devops/create-service-connection/graph.png
index f440f36..25c54b4 100644
Binary files a/terraform/azure-devops/create-service-connection/graph.png and b/terraform/azure-devops/create-service-connection/graph.png differ
diff --git a/terraform/azure-devops/create-service-connection/main.tf b/terraform/azure-devops/create-service-connection/main.tf
index 9df0869..07b5935 100644
--- a/terraform/azure-devops/create-service-connection/main.tf
+++ b/terraform/azure-devops/create-service-connection/main.tf
@@ -1,4 +1,8 @@
data azurerm_client_config current {}
+data azurerm_subscription current {}
+data azurerm_subscription target {
+ subscription_id = split("/",tolist(local.azure_role_assignments)[0].scope)[2]
+}
# Random resource suffix, this will prevent name collisions when creating resources in parallel
resource random_string suffix {
@@ -15,8 +19,16 @@ locals {
azdo_organization_name = split("/",var.azdo_organization_url)[3]
azdo_organization_url = replace(var.azdo_organization_url,"/\\/$/","")
azdo_project_url = "${local.azdo_organization_url}/${urlencode(var.azdo_project_name)}"
- azdo_service_connection_name = "${replace(module.azure_access.subscription_name,"/ +/","-")}-${var.azdo_creates_identity ? "aut" : "man"}-${var.create_managed_identity ? "msi" : "sp"}-${var.create_federation ? "oidc" : "secret"}${terraform.workspace == "default" ? "" : format("-%s",terraform.workspace)}-${local.resource_suffix}"
- azure_scope = var.azure_scope != null && var.azure_scope != "" ? var.azure_scope : "/subscriptions/${data.azurerm_client_config.current.subscription_id}"
+ # azdo_service_connection_name = "${replace(data.azurerm_subscription.target.display_name,"/ +/","-")}-${var.azdo_creates_identity ? "aut" : "man"}-${var.create_managed_identity ? "msi" : "sp"}-${var.create_federation ? "oidc" : "secret"}${terraform.workspace == "default" ? "" : format("-%s",terraform.workspace)}-${local.resource_suffix}"
+ azdo_service_connection_name = "${replace(data.azurerm_subscription.target.display_name,"/ +/","-")}${terraform.workspace == "default" ? "" : format("-%s",terraform.workspace)}-${local.resource_suffix}"
+ azure_role_assignments = length(var.azure_role_assignments) > 0 ? var.azure_role_assignments : [
+ {
+ # Default role assignment
+ role = "Contributor"
+ scope = data.azurerm_subscription.current.id
+ }
+ ]
+ managed_identity_subscription_id = var.create_managed_identity ? split("/", var.managed_identity_resource_group_id)[2] : null
principal_id = var.azdo_creates_identity ? null : (var.create_managed_identity ? module.managed_identity.0.principal_id : module.entra_app.0.principal_id)
principal_name = var.azdo_creates_identity ? null : (var.create_managed_identity ? module.managed_identity.0.principal_name : module.entra_app.0.principal_name)
resource_suffix = var.resource_suffix != null && var.resource_suffix != "" ? lower(var.resource_suffix) : random_string.suffix.result
@@ -30,8 +42,6 @@ locals {
runId = var.run_id
workspace = terraform.workspace
}
- managed_identity_subscription_id = var.create_managed_identity ? split("/", var.managed_identity_resource_group_id)[2] : null
- target_subscription_id = split("/", local.azure_scope)[2]
}
resource terraform_data managed_identity_validator {
@@ -80,18 +90,6 @@ module entra_app {
count = var.create_managed_identity || var.azdo_creates_identity ? 0 : 1
}
-module azure_access {
- providers = {
- azurerm = azurerm.target
- }
- source = "./modules/azure-access"
- # create_role_assignment = !var.azdo_creates_identity
- create_role_assignment = true
- identity_object_id = local.principal_id
- resource_id = local.azure_scope
- role = var.azure_role
-}
-
module azure_role_assignments {
providers = {
azurerm = azurerm.target
@@ -102,7 +100,7 @@ module azure_role_assignments {
resource_id = each.value.scope
role = each.value.role
- for_each = { for ra in var.azure_role_assignments : format("%s-%s", ra.scope, ra.role) => ra }
+ for_each = { for ra in local.azure_role_assignments : format("%s-%s", ra.scope, ra.role) => ra }
}
module service_connection {
@@ -114,6 +112,6 @@ module service_connection {
project_name = var.azdo_project_name
tenant_id = data.azurerm_client_config.current.tenant_id
service_connection_name = local.azdo_service_connection_name
- subscription_id = local.target_subscription_id
- subscription_name = module.azure_access.subscription_name
+ subscription_id = data.azurerm_subscription.target.subscription_id
+ subscription_name = data.azurerm_subscription.target.display_name
}
diff --git a/terraform/azure-devops/create-service-connection/outputs.tf b/terraform/azure-devops/create-service-connection/outputs.tf
index 6fd42f8..31339fe 100644
--- a/terraform/azure-devops/create-service-connection/outputs.tf
+++ b/terraform/azure-devops/create-service-connection/outputs.tf
@@ -14,25 +14,17 @@ output azdo_service_connection_url {
description = "The Azure DevOps service connection portal URL"
value = module.service_connection.service_connection_url
}
-output azure_resource_group_name {
- description = "The name of the resource group the service connection was granted access to"
- value = try(split("/", local.azure_scope)[4],null)
-}
-output azure_scope {
- description = "The Azure scope the service connection was granted access to"
- value = local.azure_scope
-}
-output azure_scope_url {
- description = "The Azure scope portal URL the service connection was granted access to"
- value = module.azure_access.resource_url
+output azure_role_assignments {
+ description = "Role assignments created for the service connection's identity"
+ value = local.azure_role_assignments
}
output azure_subscription_id {
description = "The Azure subscription id the service connection was granted access to"
- value = local.target_subscription_id
+ value = data.azurerm_subscription.target.subscription_id
}
output azure_subscription_name {
description = "The Azure subscription name the service connection was granted access to"
- value = module.azure_access.subscription_name
+ value = data.azurerm_subscription.target.display_name
}
output identity_application_id {
diff --git a/terraform/azure-devops/create-service-connection/terraform.tf b/terraform/azure-devops/create-service-connection/terraform.tf
index 7180134..7226977 100644
--- a/terraform/azure-devops/create-service-connection/terraform.tf
+++ b/terraform/azure-devops/create-service-connection/terraform.tf
@@ -53,5 +53,5 @@ provider azurerm {
prevent_deletion_if_contains_resources = false
}
}
- subscription_id = local.target_subscription_id
+ subscription_id = data.azurerm_subscription.target.subscription_id
}
\ No newline at end of file
diff --git a/terraform/azure-devops/create-service-connection/variables.tf b/terraform/azure-devops/create-service-connection/variables.tf
index db8bc67..e6a2f7c 100644
--- a/terraform/azure-devops/create-service-connection/variables.tf
+++ b/terraform/azure-devops/create-service-connection/variables.tf
@@ -15,23 +15,10 @@ variable azdo_project_name {
type = string
}
-variable azure_scope {
- default = null
- description = "The Azure scope to assign access to"
- type = string
-}
-
-variable azure_role {
- default = "Contributor"
- description = "The Azure RBAC role to assign to the service connection's identity"
- nullable = false
- type = string
-}
-
variable azure_role_assignments {
default = []
- description = "Additional role assignments to create for the service connection's identity"
- nullable = true
+ description = "Role assignments to create for the service connection's identity. If this is empty, the Contributor role will be assigned on the azurerm provider subscription."
+ nullable = false
type = set(object({scope=string, role=string}))
}