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 |
REQUIREMENTS:- |
---|
- 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 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 "###############################################################"
}
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"
}
Now, let me explain each part of YAML Pipeline for better understanding.
PART #1:- |
---|
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>
THIS IS HOW IT LOOKS WHEN YOU EXECUTE THE PIPELINE FROM AZURE DEVOPS:- |
---|
NOTE:- |
---|
Please Provide the Name of B2C in the Format - [NAME].onmicrosoft.com |
---|
For Example: AMTestb2ctenant005.onmicrosoft.com |
PART #2:- |
---|
BELOW FOLLOWS PIPELINE VARIABLES CODE SNIPPET:- |
---|
######################
#DECLARE VARIABLES:-
######################
variables:
AADExists: AlreadyExists
AADProvider: NotRegistered
BuildAgent: windows-latest
NOTE:- |
---|
Please feel free to change the values of the variables. |
---|
The entire YAML pipeline is build using Parameters and variables. No Values are Hardcoded. |
PART #3:- |
---|
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 "###############################################################"
}
## | 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 CASES:- |
---|
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 (main.tf):- |
---|
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
}
TERRAFORM (b2c.tf):- |
---|
## 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
}
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"
}
TERRAFORM (b2c.tfvars):- |
---|
b2c-country-code = "CH"
b2c-data-loc = "Europe"
b2c-name = "AMTestb2ctenant005"
b2c-rg = "_Admin-rg"
b2c-sku = "PremiumP1"
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 }}'
Now, let me explain each part of YAML Pipeline for better understanding.
PART #1:- |
---|
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
THIS IS HOW IT LOOKS WHEN YOU EXECUTE THE PIPELINE FROM AZURE DEVOPS:- |
---|
PART #2:- |
---|
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
NOTE:- |
---|
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. |
PART #3:- |
---|
## | 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 |
NOTE:- |
---|
- task: ms-devlabs.custom-terraform-tasks.custom-terraform-installer-task.TerraformInstaller@0
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 |
PART #4:- |
---|
## | 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 CASES:- |
---|
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 - hashicorp/terraform-provider-azurerm#14941 |
DEPLOY AZURE B2C USING TERRAFORM ONLY (By Manually Executing Terraform Init, Plan and Deploy) :- |
---|
COMMANDS:- |
---|
terraform init
terraform plan --var-file="b2c.tfvars"
terraform apply --var-file="b2c.tfvars"
OUTPUT:- |
---|
HOW DOES THE PLACEHOLDER LOOKS LIKE AFTER TERRAFORM EXECUTION :- |
---|
Hope You Enjoyed the Session!!!
Stay Safe | Keep Learning | Spread Knowledge