Skip to content

Commit

Permalink
feat: support MCA
Browse files Browse the repository at this point in the history
  • Loading branch information
malhussan committed Jun 6, 2024
1 parent 47184cf commit 743d44f
Show file tree
Hide file tree
Showing 10 changed files with 219 additions and 17 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [v0.7.0]

### Added

- Microsoft Customer Agreement (MCA) support

## [v0.6.0]

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

- Initial Release

[unreleased]: https://github.com/meshcloud/terraform-azure-meshplatform/compare/v0.6.0...HEAD
[unreleased]: https://github.com/meshcloud/terraform-azure-meshplatform/compare/v0.7.0...HEAD
[v0.1.0]: https://github.com/meshcloud/terraform-azure-meshplatform/releases/tag/v0.1.0
[v0.2.0]: https://github.com/meshcloud/terraform-azure-meshplatform/releases/tag/v0.2.0
[v0.3.0]: https://github.com/meshcloud/terraform-azure-meshplatform/releases/tag/v0.3.0
[v0.4.0]: https://github.com/meshcloud/terraform-azure-meshplatform/releases/tag/v0.4.0
[v0.5.0]: https://github.com/meshcloud/terraform-azure-meshplatform/releases/tag/v0.5.0
[v0.6.0]: https://github.com/meshcloud/terraform-azure-meshplatform/releases/tag/v0.6.0
[v0.7.0]: https://github.com/meshcloud/terraform-azure-meshplatform/releases/tag/v0.7.0
45 changes: 31 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,32 @@ To run this module, you need the following:

### Using Microsoft Customer Agreement

> Until <https://github.com/hashicorp/terraform-provider-azurerm/issues/15211> is resolved, MCA service principal setup can only be done manually outside of terraform.
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
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.
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
- Billing Account or Billing Profile: Owner, Contributor
- Invoice Section: Owner, Contributor, Azure Subscription Creator
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>
5. Use the following information to configure the platform in meshStack
- Billing Scope
- Destination Tenant ID
- Source Tenant ID
- Billing Account Principal Client ID (Application Client ID that will be used to create new subscriptions)
- Principal Client Secret (Application Secret created in the Source Tenant)
**Prerequisites**:

- 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

**Create an MCA service principal**:

Add an `mca` block when calling this module.

e.g.:

```hcl
module "meshplatform" {
source = "meshcloud/meshplatform/azure"
# required inputs
mca = {
source_tenant = "<aad-tenant-id>"
service_principal_name = "your-mca-sp-name"
billing_account_name = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx_xxxx-xx-xx"
billing_profile_name = "xxxx-xxxx-xxx-xxx"
invoice_section_name = "xxxx-xxxx-xxx-xxx"
}
}
```

> 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.
### Using Pre-provisioned Subscriptions

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

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

| Name | Source | Version |
|------|--------|---------|
| <a name="module_mca_service_principal"></a> [mca\_service\_principal](#module\_mca\_service\_principal) | ./modules/meshcloud-mca-service-principal | n/a |
| <a name="module_metering_service_principal"></a> [metering\_service\_principal](#module\_metering\_service\_principal) | ./modules/meshcloud-metering-service-principal/ | n/a |
| <a name="module_replicator_service_principal"></a> [replicator\_service\_principal](#module\_replicator\_service\_principal) | ./modules/meshcloud-replicator-service-principal/ | n/a |
| <a name="module_sso_service_principal"></a> [sso\_service\_principal](#module\_sso\_service\_principal) | ./modules/meshcloud-sso/ | n/a |
Expand All @@ -183,6 +197,7 @@ Before opening a Pull Request, please do the following:
| <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 |
| <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 |
| <a name="input_create_passwords"></a> [create\_passwords](#input\_create\_passwords) | Create passwords for service principals. | `bool` | `true` | no |
| <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 |
| <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 |
| <a name="input_metering_enabled"></a> [metering\_enabled](#input\_metering\_enabled) | Whether to create Metering Service Principal or not. | `bool` | `true` | no |
| <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 |
Expand All @@ -201,6 +216,8 @@ Before opening a Pull Request, please do the following:
| Name | Description |
|------|-------------|
| <a name="output_azure_ad_tenant_id"></a> [azure\_ad\_tenant\_id](#output\_azure\_ad\_tenant\_id) | The Azure AD tenant id. |
| <a name="output_mca_service_principal"></a> [mca\_service\_principal](#output\_mca\_service\_principal) | MCA Service Principal. |
| <a name="output_mca_service_principal_password"></a> [mca\_service\_principal\_password](#output\_mca\_service\_principal\_password) | Password for MCA Service Principal. |
| <a name="output_metering_service_principal"></a> [metering\_service\_principal](#output\_metering\_service\_principal) | Metering Service Principal. |
| <a name="output_metering_service_principal_password"></a> [metering\_service\_principal\_password](#output\_metering\_service\_principal\_password) | Password for Metering Service Principal. |
| <a name="output_replicator_service_principal"></a> [replicator\_service\_principal](#output\_replicator\_service\_principal) | Replicator Service Principal. |
Expand Down
37 changes: 37 additions & 0 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,29 @@ terraform {
source = "hashicorp/azuread"
version = "2.46.0"
}
azapi = {
source = "Azure/azapi"
version = "1.13.1"
}
}
}

provider "azapi" {
alias = "azapi_mca_source"
tenant_id = var.mca.source_tenant
skip_provider_registration = true
}
provider "azuread" {
alias = "azuread_mca_source"
tenant_id = var.mca.source_tenant
}
provider "azurerm" {
features {}
alias = "azurerm_mca_source"
tenant_id = var.mca.source_tenant
skip_provider_registration = true
}

data "azurerm_management_group" "replicator_custom_role_scope" {
name = var.replicator_custom_role_scope
}
Expand Down Expand Up @@ -67,6 +87,23 @@ module "replicator_service_principal" {
}
}

module "mca_service_principal" {
providers = {
azapi = azapi.azapi_mca_source
azurerm = azurerm.azurerm_mca_source
azuread = azuread.azuread_mca_source
}

count = var.mca != null ? 1 : 0
source = "./modules/meshcloud-mca-service-principal"

service_principal_name = var.mca.service_principal_name

billing_account_name = var.mca.billing_account_name
billing_profile_name = var.mca.billing_profile_name
invoice_section_name = var.mca.invoice_section_name
}

module "metering_service_principal" {
count = var.metering_enabled ? 1 : 0
source = "./modules/meshcloud-metering-service-principal/"
Expand Down
85 changes: 85 additions & 0 deletions modules/meshcloud-mca-service-principal/module.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# This module uses azapi as a workaround since azurerm_role_assignment does not support billing role assignment for MCA.
# See https://github.com/hashicorp/terraform-provider-azurerm/issues/15211.
# Once this is resolved, refactor this module.

terraform {
required_version = "> 1.1"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "3.81.0"
}
azuread = {
source = "hashicorp/azuread"
version = "2.46.0"
}
azapi = {
source = "Azure/azapi"
version = "1.13.1"
}
}
}


data "azurerm_billing_mca_account_scope" "mca" {
billing_account_name = var.billing_account_name
billing_profile_name = var.billing_profile_name
invoice_section_name = var.invoice_section_name
}

resource "azuread_application" "mca" {
display_name = var.service_principal_name
}

resource "azuread_service_principal" "mca" {
client_id = azuread_application.mca.client_id
}

data "azapi_resource_list" "billing_role_definitions" {
type = "Microsoft.Billing/billingAccounts/billingProfiles/invoiceSections/billingRoleDefinitions@2020-05-01"
parent_id = data.azurerm_billing_mca_account_scope.mca.id
response_export_values = ["*"]
}

locals {
azure_subscription_creator_role_id = jsondecode(
data.azapi_resource_list.billing_role_definitions.output).value[
index(jsondecode(data.azapi_resource_list.billing_role_definitions.output).value[*].properties.roleName, "Azure subscription creator")
].id
}

resource "azapi_resource_action" "add_role_assignment_subscription_creator" {
type = "Microsoft.Billing/billingAccounts/billingProfiles/invoiceSections@2019-10-01-preview"
resource_id = data.azurerm_billing_mca_account_scope.mca.id
action = "createBillingRoleAssignment"
method = "POST"
when = "apply"
response_export_values = ["*"]
body = jsonencode({
properties = {
principalId = azuread_service_principal.mca.object_id
roleDefinitionId = local.azure_subscription_creator_role_id
}
})
}

resource "azapi_resource_action" "remove_role_assignment_subscription_creator" {
type = "Microsoft.Billing/billingAccounts/billingProfiles/invoiceSections/billingRoleAssignments@2019-10-01-preview"
resource_id = jsondecode(azapi_resource_action.add_role_assignment_subscription_creator.output).id
method = "DELETE"
when = "destroy"
}

//---------------------------------------------------------------------------
// Create new client secret and associate it with the application
//---------------------------------------------------------------------------
resource "time_rotating" "mca_secret_rotation" {
rotation_days = 365
}

resource "azuread_application_password" "mca" {
application_id = azuread_application.mca.id
rotate_when_changed = {
rotation = time_rotating.mca_secret_rotation.id
}
}
19 changes: 19 additions & 0 deletions modules/meshcloud-mca-service-principal/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
output "billing_scope" {
value = data.azurerm_billing_mca_account_scope.mca.id
}

output "credentials" {
description = "Service Principal application id and object id"
value = {
Enterprise_Application_Object_ID = azuread_service_principal.mca.id
Application_Client_ID = azuread_application.mca.client_id
Client_Secret = "Execute `terraform output mca_service_principal_password` to see the password"
}
}

output "application_client_secret" {
description = "Client Secret Of the Application."
value = azuread_application_password.mca.value
sensitive = true
}

15 changes: 15 additions & 0 deletions modules/meshcloud-mca-service-principal/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
variable "service_principal_name" {
type = string
}

variable "billing_account_name" {
type = string
}

variable "billing_profile_name" {
type = string
}

variable "invoice_section_name" {
type = string
}
2 changes: 1 addition & 1 deletion modules/meshcloud-metering-service-principal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
|------|---------|
| <a name="provider_azuread"></a> [azuread](#provider\_azuread) | 2.46.0 |
| <a name="provider_azurerm"></a> [azurerm](#provider\_azurerm) | 3.81.0 |
| <a name="provider_time"></a> [time](#provider\_time) | 0.11.1 |
| <a name="provider_time"></a> [time](#provider\_time) | 0.11.2 |

## Modules

Expand Down
2 changes: 1 addition & 1 deletion modules/meshcloud-replicator-service-principal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
| <a name="provider_azuread"></a> [azuread](#provider\_azuread) | 2.46.0 |
| <a name="provider_azurerm"></a> [azurerm](#provider\_azurerm) | 3.81.0 |
| <a name="provider_terraform"></a> [terraform](#provider\_terraform) | n/a |
| <a name="provider_time"></a> [time](#provider\_time) | 0.11.1 |
| <a name="provider_time"></a> [time](#provider\_time) | 0.11.2 |

## Modules

Expand Down
11 changes: 11 additions & 0 deletions outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ output "replicator_service_principal_password" {
sensitive = true
}

output "mca_service_principal" {
description = "MCA Service Principal."
value = length(module.mca_service_principal) > 0 ? module.mca_service_principal[0].credentials : null
}

output "mca_service_principal_password" {
description = "Password for MCA Service Principal."
value = length(module.mca_service_principal) > 0 ? module.mca_service_principal[0].application_client_secret : null
sensitive = true
}

output "metering_service_principal" {
description = "Metering Service Principal."
value = length(module.metering_service_principal) > 0 ? module.metering_service_principal[0].credentials : null
Expand Down
11 changes: 11 additions & 0 deletions variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,14 @@ variable "workload_identity_federation" {
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."
type = object({ issuer = string, replicator_subject = string, kraken_subject = string })
}

variable "mca" {
type = object({
source_tenant = string
service_principal_name = string
billing_account_name = string
billing_profile_name = string
invoice_section_name = string
})
default = null
}

0 comments on commit 743d44f

Please sign in to comment.