Skip to content

Commit 743d44f

Browse files
committed
feat: support MCA
1 parent 47184cf commit 743d44f

File tree

10 files changed

+219
-17
lines changed

10 files changed

+219
-17
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [v0.7.0]
11+
12+
### Added
13+
14+
- Microsoft Customer Agreement (MCA) support
15+
1016
## [v0.6.0]
1117

1218
### Added
@@ -52,10 +58,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5258

5359
- Initial Release
5460

55-
[unreleased]: https://github.com/meshcloud/terraform-azure-meshplatform/compare/v0.6.0...HEAD
61+
[unreleased]: https://github.com/meshcloud/terraform-azure-meshplatform/compare/v0.7.0...HEAD
5662
[v0.1.0]: https://github.com/meshcloud/terraform-azure-meshplatform/releases/tag/v0.1.0
5763
[v0.2.0]: https://github.com/meshcloud/terraform-azure-meshplatform/releases/tag/v0.2.0
5864
[v0.3.0]: https://github.com/meshcloud/terraform-azure-meshplatform/releases/tag/v0.3.0
5965
[v0.4.0]: https://github.com/meshcloud/terraform-azure-meshplatform/releases/tag/v0.4.0
6066
[v0.5.0]: https://github.com/meshcloud/terraform-azure-meshplatform/releases/tag/v0.5.0
6167
[v0.6.0]: https://github.com/meshcloud/terraform-azure-meshplatform/releases/tag/v0.6.0
68+
[v0.7.0]: https://github.com/meshcloud/terraform-azure-meshplatform/releases/tag/v0.7.0

README.md

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -81,20 +81,32 @@ To run this module, you need the following:
8181

8282
### Using Microsoft Customer Agreement
8383

84-
> Until <https://github.com/hashicorp/terraform-provider-azurerm/issues/15211> is resolved, MCA service principal setup can only be done manually outside of terraform.
85-
86-
1. Ensure you have permissions in the source AAD Tenant for granting access to the billing account used for subscription creation using the `Account Administrator` role
87-
2. Switch to the Tenant Directory that contains your Billing Account and follow the steps to [Register an Application](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#register-an-application) and [Add Credentials](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#add-credentials). Make sure to copy down the **Directory (tenant) ID**, **Application (client) ID**, **Object ID** and the **App Secret** value that was generated. The App Secret is only visible during the creation process.
88-
3. You must grant the Enterprise Application permissions on the Billing Account, Billing Profile, or Invoice Section so that it can generate new subscriptions. Follow the steps in [this guide](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/understand-mca-roles#manage-billing-roles-in-the-azure-portal) to grant the necessary permissions. You must grant one of the following permissions
89-
- Billing Account or Billing Profile: Owner, Contributor
90-
- Invoice Section: Owner, Contributor, Azure Subscription Creator
91-
4. Write down the Billing Scope ID that looks something like this <samp>/providers/Microsoft.Billing/billingAccounts/5e98e158-xxxx-xxxx-xxxx-xxxxxxxxxxxx:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx_xxxx-xx-xx/billingProfiles/AW4F-xxxx-xxx-xxx/invoiceSections/SH3V-xxxx-xxx-xxx</samp>
92-
5. Use the following information to configure the platform in meshStack
93-
- Billing Scope
94-
- Destination Tenant ID
95-
- Source Tenant ID
96-
- Billing Account Principal Client ID (Application Client ID that will be used to create new subscriptions)
97-
- Principal Client Secret (Application Secret created in the Source Tenant)
84+
**Prerequisites**:
85+
86+
- Ensure you have permissions in the source AAD Tenant for granting access to the billing account used for subscription creation using the `Account Administrator` role
87+
88+
**Create an MCA service principal**:
89+
90+
Add an `mca` block when calling this module.
91+
92+
e.g.:
93+
94+
```hcl
95+
module "meshplatform" {
96+
source = "meshcloud/meshplatform/azure"
97+
# required inputs
98+
99+
mca = {
100+
source_tenant = "<aad-tenant-id>"
101+
service_principal_name = "your-mca-sp-name"
102+
billing_account_name = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx_xxxx-xx-xx"
103+
billing_profile_name = "xxxx-xxxx-xxx-xxx"
104+
invoice_section_name = "xxxx-xxxx-xxx-xxx"
105+
}
106+
}
107+
```
108+
109+
> note that the source_tenant is the tenant ID of the AAD with the billing account in which you can create subscriptions. This module supports creating MCA and Replicator service principals in different AAD tenants.
98110
99111
### Using Pre-provisioned Subscriptions
100112

@@ -147,6 +159,7 @@ Before opening a Pull Request, please do the following:
147159
| Name | Version |
148160
|------|---------|
149161
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | > 1.1 |
162+
| <a name="requirement_azapi"></a> [azapi](#requirement\_azapi) | 1.13.1 |
150163
| <a name="requirement_azuread"></a> [azuread](#requirement\_azuread) | 2.46.0 |
151164
| <a name="requirement_azurerm"></a> [azurerm](#requirement\_azurerm) | 3.81.0 |
152165

@@ -161,6 +174,7 @@ Before opening a Pull Request, please do the following:
161174

162175
| Name | Source | Version |
163176
|------|--------|---------|
177+
| <a name="module_mca_service_principal"></a> [mca\_service\_principal](#module\_mca\_service\_principal) | ./modules/meshcloud-mca-service-principal | n/a |
164178
| <a name="module_metering_service_principal"></a> [metering\_service\_principal](#module\_metering\_service\_principal) | ./modules/meshcloud-metering-service-principal/ | n/a |
165179
| <a name="module_replicator_service_principal"></a> [replicator\_service\_principal](#module\_replicator\_service\_principal) | ./modules/meshcloud-replicator-service-principal/ | n/a |
166180
| <a name="module_sso_service_principal"></a> [sso\_service\_principal](#module\_sso\_service\_principal) | ./modules/meshcloud-sso/ | n/a |
@@ -183,6 +197,7 @@ Before opening a Pull Request, please do the following:
183197
| <a name="input_can_cancel_subscriptions_in_scopes"></a> [can\_cancel\_subscriptions\_in\_scopes](#input\_can\_cancel\_subscriptions\_in\_scopes) | The scopes to which Service Principal cancel subscription permission is assigned to. List of management group id of form `/providers/Microsoft.Management/managementGroups/<mgmtGroupId>/`. | `list(string)` | `[]` | no |
184198
| <a name="input_can_delete_rgs_in_scopes"></a> [can\_delete\_rgs\_in\_scopes](#input\_can\_delete\_rgs\_in\_scopes) | The scopes to which Service Principal delete resource group permission is assigned to. Only relevant when `replicator_rg_enabled`. List of subscription scopes of form `/subscriptions/<subscriptionId>`. | `list(string)` | `[]` | no |
185199
| <a name="input_create_passwords"></a> [create\_passwords](#input\_create\_passwords) | Create passwords for service principals. | `bool` | `true` | no |
200+
| <a name="input_mca"></a> [mca](#input\_mca) | n/a | <pre>object({<br> source_tenant = string<br> service_principal_name = string<br> billing_account_name = string<br> billing_profile_name = string<br> invoice_section_name = string<br> })</pre> | `null` | no |
186201
| <a name="input_metering_assignment_scopes"></a> [metering\_assignment\_scopes](#input\_metering\_assignment\_scopes) | Names or UUIDs of the Management Groups that kraken should collect costs for. | `list(string)` | n/a | yes |
187202
| <a name="input_metering_enabled"></a> [metering\_enabled](#input\_metering\_enabled) | Whether to create Metering Service Principal or not. | `bool` | `true` | no |
188203
| <a name="input_metering_service_principal_name"></a> [metering\_service\_principal\_name](#input\_metering\_service\_principal\_name) | Service principal for collecting cost data. Kraken ist the name of the meshStack component. Name must be unique per Entra ID. | `string` | `"kraken"` | no |
@@ -201,6 +216,8 @@ Before opening a Pull Request, please do the following:
201216
| Name | Description |
202217
|------|-------------|
203218
| <a name="output_azure_ad_tenant_id"></a> [azure\_ad\_tenant\_id](#output\_azure\_ad\_tenant\_id) | The Azure AD tenant id. |
219+
| <a name="output_mca_service_principal"></a> [mca\_service\_principal](#output\_mca\_service\_principal) | MCA Service Principal. |
220+
| <a name="output_mca_service_principal_password"></a> [mca\_service\_principal\_password](#output\_mca\_service\_principal\_password) | Password for MCA Service Principal. |
204221
| <a name="output_metering_service_principal"></a> [metering\_service\_principal](#output\_metering\_service\_principal) | Metering Service Principal. |
205222
| <a name="output_metering_service_principal_password"></a> [metering\_service\_principal\_password](#output\_metering\_service\_principal\_password) | Password for Metering Service Principal. |
206223
| <a name="output_replicator_service_principal"></a> [replicator\_service\_principal](#output\_replicator\_service\_principal) | Replicator Service Principal. |

main.tf

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,29 @@ terraform {
99
source = "hashicorp/azuread"
1010
version = "2.46.0"
1111
}
12+
azapi = {
13+
source = "Azure/azapi"
14+
version = "1.13.1"
15+
}
1216
}
1317
}
1418

19+
provider "azapi" {
20+
alias = "azapi_mca_source"
21+
tenant_id = var.mca.source_tenant
22+
skip_provider_registration = true
23+
}
24+
provider "azuread" {
25+
alias = "azuread_mca_source"
26+
tenant_id = var.mca.source_tenant
27+
}
28+
provider "azurerm" {
29+
features {}
30+
alias = "azurerm_mca_source"
31+
tenant_id = var.mca.source_tenant
32+
skip_provider_registration = true
33+
}
34+
1535
data "azurerm_management_group" "replicator_custom_role_scope" {
1636
name = var.replicator_custom_role_scope
1737
}
@@ -67,6 +87,23 @@ module "replicator_service_principal" {
6787
}
6888
}
6989

90+
module "mca_service_principal" {
91+
providers = {
92+
azapi = azapi.azapi_mca_source
93+
azurerm = azurerm.azurerm_mca_source
94+
azuread = azuread.azuread_mca_source
95+
}
96+
97+
count = var.mca != null ? 1 : 0
98+
source = "./modules/meshcloud-mca-service-principal"
99+
100+
service_principal_name = var.mca.service_principal_name
101+
102+
billing_account_name = var.mca.billing_account_name
103+
billing_profile_name = var.mca.billing_profile_name
104+
invoice_section_name = var.mca.invoice_section_name
105+
}
106+
70107
module "metering_service_principal" {
71108
count = var.metering_enabled ? 1 : 0
72109
source = "./modules/meshcloud-metering-service-principal/"
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# This module uses azapi as a workaround since azurerm_role_assignment does not support billing role assignment for MCA.
2+
# See https://github.com/hashicorp/terraform-provider-azurerm/issues/15211.
3+
# Once this is resolved, refactor this module.
4+
5+
terraform {
6+
required_version = "> 1.1"
7+
required_providers {
8+
azurerm = {
9+
source = "hashicorp/azurerm"
10+
version = "3.81.0"
11+
}
12+
azuread = {
13+
source = "hashicorp/azuread"
14+
version = "2.46.0"
15+
}
16+
azapi = {
17+
source = "Azure/azapi"
18+
version = "1.13.1"
19+
}
20+
}
21+
}
22+
23+
24+
data "azurerm_billing_mca_account_scope" "mca" {
25+
billing_account_name = var.billing_account_name
26+
billing_profile_name = var.billing_profile_name
27+
invoice_section_name = var.invoice_section_name
28+
}
29+
30+
resource "azuread_application" "mca" {
31+
display_name = var.service_principal_name
32+
}
33+
34+
resource "azuread_service_principal" "mca" {
35+
client_id = azuread_application.mca.client_id
36+
}
37+
38+
data "azapi_resource_list" "billing_role_definitions" {
39+
type = "Microsoft.Billing/billingAccounts/billingProfiles/invoiceSections/billingRoleDefinitions@2020-05-01"
40+
parent_id = data.azurerm_billing_mca_account_scope.mca.id
41+
response_export_values = ["*"]
42+
}
43+
44+
locals {
45+
azure_subscription_creator_role_id = jsondecode(
46+
data.azapi_resource_list.billing_role_definitions.output).value[
47+
index(jsondecode(data.azapi_resource_list.billing_role_definitions.output).value[*].properties.roleName, "Azure subscription creator")
48+
].id
49+
}
50+
51+
resource "azapi_resource_action" "add_role_assignment_subscription_creator" {
52+
type = "Microsoft.Billing/billingAccounts/billingProfiles/invoiceSections@2019-10-01-preview"
53+
resource_id = data.azurerm_billing_mca_account_scope.mca.id
54+
action = "createBillingRoleAssignment"
55+
method = "POST"
56+
when = "apply"
57+
response_export_values = ["*"]
58+
body = jsonencode({
59+
properties = {
60+
principalId = azuread_service_principal.mca.object_id
61+
roleDefinitionId = local.azure_subscription_creator_role_id
62+
}
63+
})
64+
}
65+
66+
resource "azapi_resource_action" "remove_role_assignment_subscription_creator" {
67+
type = "Microsoft.Billing/billingAccounts/billingProfiles/invoiceSections/billingRoleAssignments@2019-10-01-preview"
68+
resource_id = jsondecode(azapi_resource_action.add_role_assignment_subscription_creator.output).id
69+
method = "DELETE"
70+
when = "destroy"
71+
}
72+
73+
//---------------------------------------------------------------------------
74+
// Create new client secret and associate it with the application
75+
//---------------------------------------------------------------------------
76+
resource "time_rotating" "mca_secret_rotation" {
77+
rotation_days = 365
78+
}
79+
80+
resource "azuread_application_password" "mca" {
81+
application_id = azuread_application.mca.id
82+
rotate_when_changed = {
83+
rotation = time_rotating.mca_secret_rotation.id
84+
}
85+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
output "billing_scope" {
2+
value = data.azurerm_billing_mca_account_scope.mca.id
3+
}
4+
5+
output "credentials" {
6+
description = "Service Principal application id and object id"
7+
value = {
8+
Enterprise_Application_Object_ID = azuread_service_principal.mca.id
9+
Application_Client_ID = azuread_application.mca.client_id
10+
Client_Secret = "Execute `terraform output mca_service_principal_password` to see the password"
11+
}
12+
}
13+
14+
output "application_client_secret" {
15+
description = "Client Secret Of the Application."
16+
value = azuread_application_password.mca.value
17+
sensitive = true
18+
}
19+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
variable "service_principal_name" {
2+
type = string
3+
}
4+
5+
variable "billing_account_name" {
6+
type = string
7+
}
8+
9+
variable "billing_profile_name" {
10+
type = string
11+
}
12+
13+
variable "invoice_section_name" {
14+
type = string
15+
}

modules/meshcloud-metering-service-principal/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
|------|---------|
1414
| <a name="provider_azuread"></a> [azuread](#provider\_azuread) | 2.46.0 |
1515
| <a name="provider_azurerm"></a> [azurerm](#provider\_azurerm) | 3.81.0 |
16-
| <a name="provider_time"></a> [time](#provider\_time) | 0.11.1 |
16+
| <a name="provider_time"></a> [time](#provider\_time) | 0.11.2 |
1717

1818
## Modules
1919

modules/meshcloud-replicator-service-principal/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
| <a name="provider_azuread"></a> [azuread](#provider\_azuread) | 2.46.0 |
1515
| <a name="provider_azurerm"></a> [azurerm](#provider\_azurerm) | 3.81.0 |
1616
| <a name="provider_terraform"></a> [terraform](#provider\_terraform) | n/a |
17-
| <a name="provider_time"></a> [time](#provider\_time) | 0.11.1 |
17+
| <a name="provider_time"></a> [time](#provider\_time) | 0.11.2 |
1818

1919
## Modules
2020

outputs.tf

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ output "replicator_service_principal_password" {
99
sensitive = true
1010
}
1111

12+
output "mca_service_principal" {
13+
description = "MCA Service Principal."
14+
value = length(module.mca_service_principal) > 0 ? module.mca_service_principal[0].credentials : null
15+
}
16+
17+
output "mca_service_principal_password" {
18+
description = "Password for MCA Service Principal."
19+
value = length(module.mca_service_principal) > 0 ? module.mca_service_principal[0].application_client_secret : null
20+
sensitive = true
21+
}
22+
1223
output "metering_service_principal" {
1324
description = "Metering Service Principal."
1425
value = length(module.metering_service_principal) > 0 ? module.metering_service_principal[0].credentials : null

variables.tf

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,14 @@ variable "workload_identity_federation" {
104104
description = "Enable workload identity federation by creating federated credentials for enterprise applications. Usually you'd receive the required settings when attempting to configure a platform with workload identity federation in meshStack."
105105
type = object({ issuer = string, replicator_subject = string, kraken_subject = string })
106106
}
107+
108+
variable "mca" {
109+
type = object({
110+
source_tenant = string
111+
service_principal_name = string
112+
billing_account_name = string
113+
billing_profile_name = string
114+
invoice_section_name = string
115+
})
116+
default = null
117+
}

0 commit comments

Comments
 (0)