Greetings my fellow Technology Advocates and Specialists.
In this Session, I will demonstrate -
How to Validate Pre-Requisites of Azure B2C Tenant using DevOps.
If Azure B2C Tenant Deployment is Possible using Terraform and DevOps.
LIVE RECORDED SESSION:-
LIVE DEMO was Recorded as part of my Presentation in AZURE BACK TO SCHOOL - 2022 Forum/Platform
Duration of My Demo = 49 Mins 17 Secs
VIDEO
Azure Subscription.
Azure DevOps Organisation and Project.
Service Principal with Delegated Graph API Rights and Required RBAC (Typically Contributor on Subscription or Resource Group)
Azure Resource Manager Service Connection in Azure DevOps.
Microsoft DevLabs Terraform Extension Installed in Azure DevOps.
CODE REPOSITORY:-
CAN WE DEPLOY AZ B2C USING TERRAFORM AND DEVOPS ?
CAN WE DEPLOY AZ B2C USING TERRAFORM AND DEVOPS ?
Greetings my fellow Technology Advocates and Specialists.
In this Session, I will demonstrate -
How to Validate Pre-Requisites of Azure B2C Tenant using DevOps.
If Azure B2C Tenant Deployment is Possible using Terraform and DevOps.
LIVE RECORDED SESSION:-
LIVE DEMO was Recorded as part of my Presentation in AZURE BACK TO SCHOOL - 2022 Forum/Platform
Duration of My Demo = 49 Mins 17 Secs
Azure Subscription.
Azure DevOps Organisation and Project.
Service Principal with Delegated Graph API Rights and Required RBAC (Typically Contributor on Subscription or Resource Group)
Azure Resource Manager Service Connection in Azure DevOps.
Microsoft DevLabs Terraform Extension Installed in Azure DevOps.
USE CASE #1:-
Validate Pre-Requisites of Azure B2C Tenant using DevOps
PIPELINE DETAILS FOLLOW BELOW:-
This is a Single Stage Pipeline with 3 Runtime Variables - 1) Subscription ID 2) Service Connection Name 3) Name…
USE CASE #1:-
Validate Pre-Requisites of Azure B2C Tenant using DevOps
PIPELINE DETAILS FOLLOW BELOW:-
This is a Single Stage Pipeline with 3 Runtime Variables - 1) Subscription ID 2) Service Connection Name 3) Name of Azure B2C Tenant (This is the Only User Input Runtime Variable)
The Stage Checks for 2 Conditions: 1)If the Provider is Registered in the Subscription 2)If the B2C Name Provided by the user is Globally Unique . If Both Conditions are NOT met, Pipeline Fails, else the pipeline succeeds confirming that the Azure B2C Tenant Name can be used for Deployment.
HOW DOES MY CODE PLACEHOLDER LOOKS LIKE:-
AZURE DEVOPS YAML PIPELINE (azure-pipelines-B2C-v1.0.yml):-
trigger:
none
######################
#DECLARE PARAMETERS:-
######################
parameters:
- name: SubscriptionID
displayName: Subscription ID Details Follow Below:-
default: 210e66cb-55cf-424e-8daa-6cad804ab604
values:
- 210e66cb-55cf-424e-8daa-6cad804ab604
- name: ServiceConnection
displayName: Service Connection Name Follows Below:-
default: amcloud-cicd-service-connection
values:
- amcloud-cicd-service-connection
- name: AADB2CName
displayName: Please Provide the AAD B2C Tenant Name:-
type: object
default: <Please Provide the Name of AAD B2C>
######################
#DECLARE VARIABLES:-
######################
variables:
AADExists: AlreadyExists
AADProvider: NotRegistered
BuildAgent: windows-latest
#########################
# Declare Build Agents:-
#########################
pool:
vmImage: $(BuildAgent)
###################
# Declare Stages:-
###################
stages:
- stage: VALIDATE_AAD_B2C_PROVIDER_AND_NAME
jobs:
- job: IF_AAD_B2C_PROVIDER_AND_NAME_EXISTS
displayName: IF AAD B2C PROVIDER AND NAME EXISTS
steps:
- task: AzureCLI@2
displayName: CHECK AAD B2C PROVIDER AND NAME
inputs:
azureSubscription: ${{ parameters.ServiceConnection }}
scriptType: ps
scriptLocation: inlineScript
inlineScript: |
az --version
az account set --subscription ${{ parameters.SubscriptionID }}
az account show
$B2CJSON = @{
countryCode = "CH"
name = "${{ parameters.AADB2CName }}"
}
$infile = "B2CDetails.json"
Set-Content -Path $infile -Value ($B2CJSON | ConvertTo-Json)
$i = az provider show --namespace "Microsoft.AzureActiveDirectory" --query "registrationState" -o tsv
$j = az rest --method POST --url https://management.azure.com/subscriptions/${{ parameters.SubscriptionID }}/providers/Microsoft.AzureActiveDirectory/checkNameAvailability?api-version=2019-01-01-preview --body "@B2CDetails.json" --query 'reason' -o tsv
if ($i -eq "$(AADProvider)" -and $j -eq "$(AADExists)") {
echo "###############################################################"
echo "Provider $(AADProvider) and Name $(AADExists)"
echo "###############################################################"
exit 1
}
elseif ($i -eq "$(AADProvider)" -or $j -eq "$(AADExists)") {
echo "###############################################################"
echo "Either Name $(AADExists) or Provider $(AADProvider)"
echo "###############################################################"
exit 1
}
else {
echo "###############################################################"
echo "MOVE TO NEXT STAGE - DEPLOY AZURE AAD B2C"
echo "###############################################################"
}
Enter fullscreen mode
Exit fullscreen mode
POWERSHELL MODULE (ValidateAADB2C.ps1): IF ANYONE INTENDS TO VALIDATE USING POWERSHELL ONLY (MINUS DEVOPS PIPELINE):-
$AADExists = "AlreadyExists"
$AADProvider = "NotRegistered"
$AADB2CCountryCode = "CH"
$AADB2CName = "AMTestb2ctenant005.onmicrosoft.com"
$AADB2CRest = "https://management.azure.com/subscriptions/210e66cb-55cf-424e-8daa-6cad804ab604/providers/Microsoft.AzureActiveDirectory/checkNameAvailability?api-version=2019-01-01-preview"
$B2CJSON = @{
countryCode = "$AADB2CCountryCode"
name = "$AADB2CName"
}
$infile = "B2CDetails.json"
Set-Content -Path $infile -Value ($B2CJSON | ConvertTo-Json)
$i = az rest --method POST --url $AADB2CRest --body "@B2CDetails.json" --query 'reason' -o tsv
$j = az provider show --namespace "Microsoft.AzureActiveDirectory" --query "registrationState" -o tsv
if ($i -eq "$AADExists" -and $j -eq "$AADProvider") {
Write-Output "Name $AADExists and Provider $AADProvider"
}
ElseIf ($i -eq "$AADExists" -or $j -eq "$AADProvider") {
Write-Output "Either Name $AADExists or Provider $AADProvider"
}
Else {
Write-Output "MOVE TO NEXT STAGE - DEPLOY AZURE AAD B2C"
}
Enter fullscreen mode
Exit fullscreen mode
Now, let me explain each part of YAML Pipeline for better understanding.
BELOW FOLLOWS PIPELINE RUNTIME VARIABLES CODE SNIPPET:-
######################
#DECLARE PARAMETERS:-
######################
parameters:
- name: SubscriptionID
displayName: Subscription ID Details Follow Below:-
default: 210e66cb-55cf-424e-8daa-6cad804ab604
values:
- 210e66cb-55cf-424e-8daa-6cad804ab604
- name: ServiceConnection
displayName: Service Connection Name Follows Below:-
default: amcloud-cicd-service-connection
values:
- amcloud-cicd-service-connection
- name: AADB2CName
displayName: Please Provide the AAD B2C Tenant Name:-
type: object
default: <Please Provide the Name of AAD B2C>
Enter fullscreen mode
Exit fullscreen mode
THIS IS HOW IT LOOKS WHEN YOU EXECUTE THE PIPELINE FROM AZURE DEVOPS:-
Please Provide the Name of B2C in the Format - [NAME].onmicrosoft.com
For Example: AMTestb2ctenant005.onmicrosoft.com
BELOW FOLLOWS PIPELINE VARIABLES CODE SNIPPET:-
######################
#DECLARE VARIABLES:-
######################
variables:
AADExists: AlreadyExists
AADProvider: NotRegistered
BuildAgent: windows-latest
Enter fullscreen mode
Exit fullscreen mode
Please feel free to change the values of the variables.
The entire YAML pipeline is build using Parameters and variables. No Values are Hardcoded.
BELOW FOLLOWS PIPELINE STAGE VALIDATE_AAD_B2C_PROVIDER_AND_NAME CODE SNIPPET:-
###################
# Declare Stages:-
###################
stages:
- stage: VALIDATE_AAD_B2C_PROVIDER_AND_NAME
jobs:
- job: IF_AAD_B2C_PROVIDER_AND_NAME_EXISTS
displayName: IF AAD B2C PROVIDER AND NAME EXISTS
steps:
- task: AzureCLI@2
displayName: CHECK AAD B2C PROVIDER AND NAME
inputs:
azureSubscription: ${{ parameters.ServiceConnection }}
scriptType: ps
scriptLocation: inlineScript
inlineScript: |
az --version
az account set --subscription ${{ parameters.SubscriptionID }}
az account show
$B2CJSON = @{
countryCode = "CH"
name = "${{ parameters.AADB2CName }}"
}
$infile = "B2CDetails.json"
Set-Content -Path $infile -Value ($B2CJSON | ConvertTo-Json)
$i = az provider show --namespace "Microsoft.AzureActiveDirectory" --query "registrationState" -o tsv
$j = az rest --method POST --url https://management.azure.com/subscriptions/${{ parameters.SubscriptionID }}/providers/Microsoft.AzureActiveDirectory/checkNameAvailability?api-version=2019-01-01-preview --body "@B2CDetails.json" --query 'reason' -o tsv
if ($i -eq "$(AADProvider)" -and $j -eq "$(AADExists)") {
echo "###############################################################"
echo "Provider $(AADProvider) and Name $(AADExists)"
echo "###############################################################"
exit 1
}
elseif ($i -eq "$(AADProvider)" -or $j -eq "$(AADExists)") {
echo "###############################################################"
echo "Either Name $(AADExists) or Provider $(AADProvider)"
echo "###############################################################"
exit 1
}
else {
echo "###############################################################"
echo "MOVE TO NEXT STAGE - DEPLOY AZURE AAD B2C"
echo "###############################################################"
}
Enter fullscreen mode
Exit fullscreen mode
##
CONDITIONS APPLIED IN VALIDATE STAGE
1.
Firstly, it validates whether the Provider Microsoft.AzureActiveDirectory is Registered in the Subscription. If the Value returned is NotRegistered it means that condition is Not Met to Deploy B2C. az cli is used to validate the Registration of the Provider in the Subscription.
2.
Secondly, it validates whether the B2C Name Provided by the User is Globally Unique. If the Value returned is AlreadyExists it means that the condition is Not Met to Deploy B2C. REST API together with AZ REST is used to validate B2C Globally Unique Name.
3.
Expected value for Provider and B2C Name are: Registered and Null
TEST CASE #1: B2C NAME IS GLOBALLY NOT UNIQUE AND PROVIDER REGISTERED IN THE SUBSCRIPTION :-
Desired Output: VALIDATE Stage FAILS
PIPELINE RUNTIME VARIABES:-
PIPELINE RESULTS:-
TEST CASE #2: B2C NAME IS GLOBALLY UNIQUE AND PROVIDER REGISTERED IN THE SUBSCRIPTION:-
Desired Output: VALIDATE Stage Executes SUCCESSFULLY .
PIPELINE RUNTIME VARIABES:-
PIPELINE RESULTS:-
USE CASE #2:-
Validate If Azure B2C Tenant Deployment is Possible using Terraform and DevOps
QUICK ANSWER:-
Azure B2C Tenant Deployment is Not Possible to deploy using Terraform and DevOps Together.
Azure B2C Tenant Deployment is Possible to deploy using Terraform only (By Manually Executing Terraform Init , Plan and Deploy )
PIPELINE DETAILS FOLLOW BELOW:-
This is a Two Stage Pipeline with 2 Runtime Variables - 1) Subscription ID 2) Service Connection Name
The Stages Performs Terraform INIT , PLAN and DEPLOY
HOW DOES MY CODE PLACEHOLDER LOOKS LIKE:-
DETAILS AND ALL CODE SNIPPETS FOLLOWS BELOW:-
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.2"
}
azuread = {
source = "hashicorp/azuread"
version = "~> 2.20.0"
}
}
}
provider "azurerm" {
features {}
skip_provider_registration = true
}
Enter fullscreen mode
Exit fullscreen mode
## AAD B2C:-
resource "azurerm_aadb2c_directory" "Az_B2c" {
country_code = var.b2c-country-code
data_residency_location = var.b2c-data-loc
display_name = var.b2c-name
domain_name = "${var.b2c-name}.onmicrosoft.com"
resource_group_name = var.b2c-rg
sku_name = var.b2c-sku
}
Enter fullscreen mode
Exit fullscreen mode
TERRAFORM (variables.tf):-
variable "b2c-name" {
type = string
description = "Name of the B2C Tenant"
}
variable "b2c-country-code" {
type = string
description = "Country Code of B2C"
}
variable "b2c-data-loc" {
type = string
description = "Data Residency Location of B2C"
}
variable "b2c-rg" {
type = string
description = "Resource Group of B2C"
}
variable "b2c-sku" {
type = string
description = "Resource Group of B2C"
}
Enter fullscreen mode
Exit fullscreen mode
b2c-country-code = "CH"
b2c-data-loc = "Europe"
b2c-name = "AMTestb2ctenant005"
b2c-rg = "_Admin-rg"
b2c-sku = "PremiumP1"
Enter fullscreen mode
Exit fullscreen mode
NOTE:-
You may have noticed that I have put "b2c-name " as AMTestb2ctenant005 . Please Refer Use Case #1, Test Case #2 , where I have Validated this Name.
AZURE DEVOPS YAML PIPELINE (azure-pipelines-B2C-v1.1.yml):-
trigger:
none
######################
#DECLARE PARAMETERS:-
######################
parameters:
- name: SubscriptionID
displayName: Subscription ID Details Follow Below:-
default: 210e66cb-55cf-424e-8daa-6cad804ab604
values:
- 210e66cb-55cf-424e-8daa-6cad804ab604
- name: ServiceConnection
displayName: Service Connection Name Follows Below:-
default: amcloud-cicd-service-connection
values:
- amcloud-cicd-service-connection
######################
#DECLARE VARIABLES:-
######################
variables:
ResourceGroup: tfpipeline-rg
StorageAccount: tfpipelinesa
Container: terraform
TfstateFile: B2C/b2cdeploy.tfstate
BuildAgent: windows-latest
WorkingDir: $(System.DefaultWorkingDirectory)/B2C-Terraform
Target: $(build.artifactstagingdirectory)/AMTF
Environment: NonProd
Artifact: AM
#########################
# Declare Build Agents:-
#########################
pool:
vmImage: $(BuildAgent)
###################
# Declare Stages:-
###################
stages:
- stage: PLAN
jobs:
- job: PLAN
displayName: PLAN
steps:
# Install Terraform Installer in the Build Agent:-
- task: ms-devlabs.custom-terraform-tasks.custom-terraform-installer-task.TerraformInstaller@0
displayName: INSTALL TERRAFORM VERSION - LATEST
inputs:
terraformVersion: 'latest'
# Terraform Init:-
- task: TerraformTaskV2@2
displayName: TERRAFORM INIT
inputs:
provider: 'azurerm'
command: 'init'
workingDirectory: '$(workingDir)' # Az DevOps can find the required Terraform code
backendServiceArm: '${{ parameters.ServiceConnection }}'
backendAzureRmResourceGroupName: '$(ResourceGroup)'
backendAzureRmStorageAccountName: '$(StorageAccount)'
backendAzureRmContainerName: '$(Container)'
backendAzureRmKey: '$(TfstateFile)'
# Terraform Validate:-
- task: TerraformTaskV2@2
displayName: TERRAFORM VALIDATE
inputs:
provider: 'azurerm'
command: 'validate'
workingDirectory: '$(workingDir)'
environmentServiceNameAzureRM: '${{ parameters.ServiceConnection }}'
# Terraform Plan:-
- task: TerraformTaskV2@2
displayName: TERRAFORM PLAN
inputs:
provider: 'azurerm'
command: 'plan'
workingDirectory: '$(workingDir)'
commandOptions: "--var-file=b2c.tfvars --out=tfplan"
environmentServiceNameAzureRM: '${{ parameters.ServiceConnection }}'
# Copy Files to Artifacts Staging Directory:-
- task: CopyFiles@2
displayName: COPY FILES ARTIFACTS STAGING DIRECTORY
inputs:
SourceFolder: '$(workingDir)'
Contents: |
**/*.tf
**/*.tfvars
**/*tfplan*
TargetFolder: '$(Target)'
# Publish Artifacts:-
- task: PublishBuildArtifacts@1
displayName: PUBLISH ARTIFACTS
inputs:
targetPath: '$(Target)'
artifactName: '$(Artifact)'
- stage: DEPLOY
condition: succeeded()
dependsOn: PLAN
jobs:
- deployment:
displayName: Deploy
environment: $(Environment)
pool:
vmImage: '$(BuildAgent)'
strategy:
runOnce:
deploy:
steps:
# Download Artifacts:-
- task: DownloadBuildArtifacts@0
displayName: DOWNLOAD ARTIFACTS
inputs:
buildType: 'current'
downloadType: 'single'
artifactName: '$(Artifact)'
downloadPath: '$(System.ArtifactsDirectory)'
# Install Terraform Installer in the Build Agent:-
- task: ms-devlabs.custom-terraform-tasks.custom-terraform-installer-task.TerraformInstaller@0
displayName: INSTALL TERRAFORM VERSION - LATEST
inputs:
terraformVersion: 'latest'
# Terraform Init:-
- task: TerraformTaskV2@2
displayName: TERRAFORM INIT
inputs:
provider: 'azurerm'
command: 'init'
workingDirectory: '$(System.ArtifactsDirectory)/$(Artifact)/AMTF/' # Az DevOps can find the required Terraform code
backendServiceArm: '${{ parameters.ServiceConnection }}'
backendAzureRmResourceGroupName: '$(ResourceGroup)'
backendAzureRmStorageAccountName: '$(StorageAccount)'
backendAzureRmContainerName: '$(Container)'
backendAzureRmKey: '$(TfstateFile)'
# Terraform Apply:-
- task: TerraformTaskV2@2
displayName: TERRAFORM APPLY # The terraform Plan stored earlier is used here to apply only the changes.
inputs:
provider: 'azurerm'
command: 'apply'
workingDirectory: '$(System.ArtifactsDirectory)/$(Artifact)/AMTF'
commandOptions: '--var-file=b2c.tfvars' # The terraform Plan stored earlier is used here to apply.
environmentServiceNameAzureRM: '${{ parameters.ServiceConnection }}'
Enter fullscreen mode
Exit fullscreen mode
Now, let me explain each part of YAML Pipeline for better understanding.
BELOW FOLLOWS PIPELINE RUNTIME VARIABLES CODE SNIPPET:-
######################
#DECLARE PARAMETERS:-
######################
parameters:
- name: SubscriptionID
displayName: Subscription ID Details Follow Below:-
default: 210e66cb-55cf-424e-8daa-6cad804ab604
values:
- 210e66cb-55cf-424e-8daa-6cad804ab604
- name: ServiceConnection
displayName: Service Connection Name Follows Below:-
default: amcloud-cicd-service-connection
values:
- amcloud-cicd-service-connection
Enter fullscreen mode
Exit fullscreen mode
THIS IS HOW IT LOOKS WHEN YOU EXECUTE THE PIPELINE FROM AZURE DEVOPS:-
BELOW FOLLOWS PIPELINE VARIABLES CODE SNIPPET:-
######################
#DECLARE VARIABLES:-
######################
variables:
ResourceGroup: tfpipeline-rg
StorageAccount: tfpipelinesa
Container: terraform
TfstateFile: B2C/b2cdeploy.tfstate
BuildAgent: windows-latest
WorkingDir: $(System.DefaultWorkingDirectory)/B2C-Terraform
Target: $(build.artifactstagingdirectory)/AMTF
Environment: NonProd
Artifact: AM
Enter fullscreen mode
Exit fullscreen mode
Please feel free to change the values of the variables.
The entire YAML pipeline is build using Parameters and variables. No Values are Hardcoded.
"Working Directory " Path should be based on your Code Placeholder.
"Environment " here refers to Pipeline Environment Name where Approval Gate is configured.
##
TASKS PERFORMED UNDER PLAN STAGE
1.
Install Latest Version of Terraform in Build Agent
2.
Terraform Init
3.
Terraform Validate
4.
Terraform Plan
5.
Copy Files to Artifacts Staging Directory
6.
Publish Artifacts
- task: ms-devlabs.custom-terraform-tasks.custom-terraform-installer-task.TerraformInstaller@0
Enter fullscreen mode
Exit fullscreen mode
Explanation:-
Instead of using TerraformInstaller@0 YAML Task, I have specified the Full Name. This is because I have Multiple Terraform Extensions in my DevOps Organisation and with each of the terraform Extension exists the Terraform Install Task
##
TASKS PERFORMED UNDER DEPLOY STAGE
1.
Previous Stage PLAN should complete Successfully in order for this Stage DEPLOY to Proceed. Otherwise, the Stage will get skipped
2.
Download Published Artifacts
3.
Terraform Init
4.
Terraform Apply
TEST CASE #1: B2C NAME IS GLOBALLY NOT UNIQUE AND PROVIDER REGISTERED IN THE SUBSCRIPTION :-
Desired Output: PLAN Stage is SUCCESSFUL but DEPLOY Stage FAILS
PIPELINE RESULTS:-
Waiting for Approval
DEPLOY Stage FAILED
ERROR ENCOUNTERED:-
REASON:-
It occurs when using a Service Principal. When creating an Azure B2C directory, the user who creates it becomes the owner of the new directory by default. This is achieved by the user account being added to the B2C directory as an External Member from the parent directory.
Service Principals cannot be added as external members of other directories, therefore it's NOT POSSIBLE for a Service Principal to create a B2C directory
The Issue is Recorded in Github - https://github.com/hashicorp/terraform-provider-azurerm/issues/14941
DEPLOY AZURE B2C USING TERRAFORM ONLY (By Manually Executing Terraform Init, Plan and Deploy) :-
terraform init
Enter fullscreen mode
Exit fullscreen mode
terraform plan --var-file="b2c.tfvars"
Enter fullscreen mode
Exit fullscreen mode
terraform apply --var-file="b2c.tfvars"
Enter fullscreen mode
Exit fullscreen mode
OUTPUT:-
HOW DOES THE PLACEHOLDER LOOKS LIKE AFTER TERRAFORM EXECUTION :-
Hope You Enjoyed the Session!!!
Stay Safe | Keep Learning | Spread Knowledge
Top comments (2)
This is great! Thanks for the insights on the service connection / principal issue not being able to add itself as the B2C owner, and for providing the issue tracking GitHub link. ❤️
Thank you Marcel 😊