Monday, 17 October 2022

CICD pipeline for Azure Blob storage with CDN - Part 6 Set up Continues Deployment of our Hero Images

This is more of a continuation of the previous post in which we configured our initial release pipeline and configured our Continuous Deployment trigger. In this post we are going to simply: 

  1. unzip our hero images, 
  2. then copy them over to our Blob storage 

Lets start by adding our delete content form our blob storage task.


Once you've opened the stage view start by renaming your stage to something more readable "Unzip and upload hero images"


once, that's done, click the "+" to add a task to the stage, search for extract files and add the "Extract files" task


Now we are going to have to configure our extract tasks job.


There's only two things you have to do and a third nice to have,
  1. set the display name to something more meaningful.
  2. set destination path: "$(System.DefaultWorkingDirectory)/_pav.content.cdn/unzip"
  3. set Overwrite existing files to true
  4. finally click the Save button
Next let's add another task that will let us copy our hero images to our blob store, click the add task button and search for "Azure file copy"


Add the azure file copy task, here we are going to finally copy our hero images to our azure file storage.


Now set all of your File copy properties. 


save your pipeline and run it, and more likely than not your CD pipeline will fails, with the following descriptive error.



Upload to container: 'container-public-pav-master' in storage account: 'stpavmaster' with blob prefix: '' failed with error: 'AzCopy.exe exited with non-zero exit code while uploading files to blob storage.' For more info please refer to https://aka.ms/azurefilecopyreadme

my favorite parts is "AzCopy.exe exited with non-zero exit code while uploading files to blob storage", luckily for you after struggling with this for days, I learned that it's a permission issue.

Click on the gear at the bottom left and select service connections,


Click on the service connection that you are using in your pipeline.



Next click on the "Manage service Principle" link button. notice that the arrow is pointing to the wrong link button.



Take note of the display name, you're going to have to grant permissions in your azure environment to upload to your blob storage.

Next you'll have to open Access control in your azure portal for your subscription, 



This will open a drop down list, form it choose "Add role assignment" which will now let you add the Blob storage contributor role to your application service connection.


with that selected, hit the next button




Here you'll have to select the member and you'll have to manually paste in the name from a few steps above, select the member and assign the "Storage Blob Data Contributor role" this will let your dev ops site push files to your blob storage.

Now with your permissions set correctly you can create your release and you should be able to push your hero images to your Blob storage.

The blob storage URL for your images should be something along the lines of

https://stpavmaster.blob.core.windows.net/container-public-pav-master/hassan-pasha-AwssoaXCosQ-unsplash.jpg

and you should be able to access that exact same file using your cdn end point

https://cdn-endpoint-pav-master-global.azureedge.net/container-public-pav-master/hassan-pasha-AwssoaXCosQ-unsplash.jpg

and that's it, for the most part, there's two steps I'm leaving out, because I'm just too exhausted.

one you should delete all of the files in your blob storage before pushing them, to ensure that you do not host useless data.

and two you need to purge your cdn endpoint, so that your users aren't being hosted stale data.

Thursday, 13 October 2022

CICD pipeline for Azure Blob storage with CDN - Part 5 Set up Continues Deployment of our Hero Images

One of the saddest parts of Continuous Deployment is that it can't be scripted and we have to go through Microsoft anything is possible, it's just conveniently hidden behind 35 clicks and 17 modals. To get started log into your Azure Devops portal. 

Once you have to portal open click on the rocket ship and go to releases


Next click the "New pipeline" button. This will let you choose from some predefined Continuous deployment configurations, however we are going to start from a blank slate, so just choose "Empty job"


Next the we'll be prompted to start with the first stage of our continuous deployment, but before we get started, well select our artifact from our Continuous Integration pipeline, in this case the zip file in the drop folder. 

First close the stage panel, we'll get back to that later.


With the stage panel closed, click the "add artifact" area of the Artifacts section.


Now you'll be asked to choose the source pipeline, that's the YAML file we built previously, it defaults to our project name, however we could have given it a more specific name.


Once you choose your source pipeline everything else should just auto select for you.


Once you have everything selected, click the "Add" button, now that you've added your artifact (the output from the Continuous integration pipeline, the "zip" file in this case). You'll have to add the trigger for this pipeline, click the little lightning bolt.


You should be looking at the following, enable the continuous deployment trigger, and then close the panel, you could add a branch filter, but we only have a master branch in this example.


With the Continuous deployment trigger flyout panel collapsed, we're going to change the name of our Pipeline, just to make it extra clear at what it does. at the top center, click on "New release pipeline" and give it a new name' I'm calling it "Hero Image deployment pipeline" Once you've changed your Pipeline name, save your progress.


Once you click save, you'll be asked to which folder you would want to save to, for now just leave it as the root and hit the "OK" button.


That's it for now we can take a break, next we'll configure our actual pipeline.

Tuesday, 11 October 2022

CICD pipeline for Azure Blob storage with CDN - Part 4 Zip up our assets for deployment to our Blob storage.

Now that we've pushed our assets to our git repo and created our Azure infrastructure, we are now going to finish the Continuous Integration part of our DevOps pipeline. Our goal is going to be simple, zip up our assets and get them ready for deployment.

Open up your YAML file and add the following section to zip up our assets.

  - taskArchiveFiles@2
    displayName'Zip hero images'
    inputs:
      rootFolderOrFile'heroImages'
      includeRootFolderfalse
      archiveType'zip'
      archiveFile'$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
      replaceExistingArchivetrue

The above will simply create a zip file with the contents of our heroImages folder, if we where to check off includeRootFolder, our zip would contain the heroImages folder, but with it set to false it will simply be a build number.zip file with the contents of heroImages.

Next we have to move our Zip file to a staging area, we can do this with the following

taskPublishBuildArtifacts@1
  displayName'Publish zipped assets for deployment'
  inputs:
    PathtoPublish'$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
    ArtifactName'drop'
    publishLocation'Container'

The above takes our zipped file and makes it available in a folder called drop for our Continuous Deployment step, otherwise known as CD.

For good measure, here's our YAML file start to finish.

# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml

trigger:
dev
test
master

variables:
  ${{ if startsWith(variables['Build.SourceBranch'], 'refs/heads/') }}:
    branchName$[ replace(variables['Build.SourceBranch'], 'refs/heads/', '') ]
  ${{ if startsWith(variables['Build.SourceBranch'], 'refs/pull/') }}:
    branchName$[ replace(variables['System.PullRequest.TargetBranch'], 'refs/heads/', '') ]
  name'pav'
  location'westeurope'
  azureSubscription'Pavs Subscription(0000000000-0000-0000-0000-000000000000)'

pool:
  vmImage'windows-latest'

steps
script: |
    echo BranchName = '$(branchName)'
    echo name = '$(name)'
    echo location = '$(location)'
    echo azureSubscription = '$(azureSubscription)'
  displayName'List YAML variables'

taskAzurePowerShell@5
  displayNameBuild azure cdn infrastructure
  inputs:
    azureSubscription'$(azureSubscription)'
    ScriptType'FilePath'
    ScriptPath'$(Build.SourcesDirectory)/AzureInfrastructure.ps1'
    ScriptArguments
      -location$(location)
      -name$(name)
      -env$(branchName)
    errorActionPreference'continue'
    azurePowerShellVersion'LatestVersion'
taskArchiveFiles@2
  displayName'Zip up assets'
  inputs:
    rootFolderOrFile'heroImages'
    includeRootFolderfalse
    archiveType'zip'
    archiveFile'$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
    replaceExistingArchivetrue
taskPublishBuildArtifacts@1
  displayName'Publish zipped assets for deployment'
  inputs:
    PathtoPublish'$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
    ArtifactName'drop'
    publishLocation'Container'
    

Keep in mind this will not deploy anything, it will simply get it ready for deployment, if you do push this new YAML file, go to the job once it's finished you can click on the artifact created, download it and inspect it to see the contents of your heroImages folder.


That's is for this post, in the next one we'll set up the Continuous Deployment part of our CICD pipeline, that's where we will move our images into our azure Blob storage.

Sunday, 9 October 2022

Parallel Programming 02

Previously we made a simple winRT App using the await/async keywords that updated progress bars and textboxs, let's use it again

create a simple event handler for the click event of the button.

async void Start_Button_Click(object sender, RoutedEventArgs e)
{
    //start the first long running task in parallel
    await LongRunningTask(Progress00_ProgresBar);

    //print something to the textbox
    TextBoxOne.Text = "First Progress Bar Complete";

    //show a message to the user
    await new MessageDialog("Good Times").ShowAsync();

    //start the second long running task in parallel
    await LongRunningTask(Progress01_ProgresBar);

    //print something to the second textbox
    TextBoxTwo.Text = "Second Progress Bar Complete";
}

and for our LongRunnting Task we simply made

public void LongRunningTask(ProgressBar pb)
{
    for (var i = 0; i <= 100; i+=20)
    {
        Task.Delay(1000).Wait();
        pb.Value = i;
    }
}

Now that worked great, that is unless you clicked the start button again before the whole process finished, kinda bunk what we need to do is be able to cancel our tasks if the start button is clicked. to do this we need to add a CancellationTokenSource, let's go ahead and create a property right on the page.

public sealed partial class MainPage : Page
{

    CancellationTokenSource cts { get; set; }
    //Our Code ...
}


with that done let's modify our start event handler

async void Start_Button_Click(object sender, RoutedEventArgs e)
{
    //Check if cancellation token source is instantiated
    if (this.cts != null) {
        //if it is, that means we're running and cancel
        cts.Cancel(true);
    }
    else
    {
        cts = new CancellationTokenSource();

        try
        {
            //start the first long running task in parallel
            await LongRunningTask(Progress00_ProgresBar, cts.Token);

            //print something to the textbox
            TextBoxOne.Text = "First Progress Bar Complete";

            //show a message to the user
            await new MessageDialog("Good Times").ShowAsync();

            //start the second long running task in parallel
            await LongRunningTask(Progress01_ProgresBar, cts.Token);

            //print something to the second textbox
            TextBoxTwo.Text = "Second Progress Bar Complete";
        }
        //catch cancellation exception from LongRunning function
        catch (OperationCanceledException ocex)
        {
            new MessageDialog("Operation Canceled, click start again").ShowAsync();
        }
        finally
        {
            //canceled or finished set the cancellation token source to null
            this.cts = null;
        }
    }
}

Next we modify our long running function to accept the cancellation token and use it to terminate the task if it's signaled to cancel.

async Task LongRunningTask(ProgressBar pb, CancellationToken token)
{
    for (var i = 0; i <= 100; i += 20)
    {
        await Task.Delay(1000);

        //if cancel signal sent, break the loop
        if (token.IsCancellationRequested)
            break;
        await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => pb.Value = i);
    }
    //throw cancelation exception
    token.ThrowIfCancellationRequested();

}

we pass in our token and then if we signal it, our long running task will stop and throw an cancellation request winch will halt our start event handler.

Thursday, 6 October 2022

CICD pipeline for Azure Blob storage with CDN - Part 3 Powershell to provision Azure infrastructure

Now the meat let's set up our AzureInfrastructure.ps1 script, open it in your ms Code editor, and let's start by setting up some input parameters and creating a simple function to create a resource group in azure for us.

Param (
  [Parameter()][String]$location,
  [Parameter()][String]$name,
  [Parameter()][String]$env)

#create resource group
function CreateResourceGroup {
    Param(
        [Parameter(Mandatory=$true)][String]$name,
        [Parameter(Mandatory=$true)][String]$location,
        [Parameter(Mandatory=$true)][String]$env)

    #setup resource Group name using prefix
    $rgName = ("rg-$name-$env").ToLower()

    #check if resource group already exists
    $resourceGroup = Get-AzResourceGroup `
        -Name $rgName `
        -ErrorAction SilentlyContinue
    if($resourceGroup){
        Write-host "Resource group '$rgName' already exists" -foregroundcolor yellow
        return $rgName
    }

    #create resource group
    try {
        Write-Host "Createing '$rgName' resource group in '$location'" -ForegroundColor Magenta
        $resourceGroup = New-AzResourceGroup `
            -Name $rgName `
            -Location $location
        Write-host "Resource group '$rgName' created  in '$location'" -foregroundcolor Green
        return $rgName
    }
    catch {
        #failed to create resourcec group  
         Write-error "Resource group '$rgName' NOT created" -ErrorAction Stop  
    }
}


$rgName = CreateResourceGroup $name $location $env

These will let our YAML file input parameters to our PowerShell which will make our lives easier in the long run, our powershell then will use those parameters to build our infrastructure. 

Now that we have parameterized our PowerShell script, let's jump back to our YAML file and pass those parameters to our script

# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml

trigger:
- dev
- test
- master

variables:
  ${{ if startsWith(variables['Build.SourceBranch'], 'refs/heads/') }}:
    branchName: $[ replace(variables['Build.SourceBranch'], 'refs/heads/', '') ]
  ${{ if startsWith(variables['Build.SourceBranch'], 'refs/pull/') }}:
    branchName: $[ replace(variables['System.PullRequest.TargetBranch'], 'refs/heads/', '') ]
  name: 'pav'
  location: 'westeurope'
  azureSubscription: 'Pavs Subscription(6e72246e-0000-0000-0000-000000000000)'

pool:
  vmImage: 'windows-latest'

steps:
- script: |
    echo BranchName = '$(branchName)'
    echo name = '$(name)'
    echo location = '$(location)'
    echo azureSubscription = '$(azureSubscription)'
  displayName: 'List YAML variables'

- task: AzurePowerShell@5
  inputs:
    azureSubscription: '$(azureSubscription)'
    ScriptType: 'FilePath'
    ScriptPath: '$(Build.SourcesDirectory)/AzureInfrastructure.ps1'
    ScriptArguments: >
      -location: $(location)
      -name: $(name)
      -env: $(branchName)
    errorActionPreference: 'continue'
    FailOnStandardError: true
    azurePowerShellVersion: 'LatestVersion'

with those two changes complete and SAVED, let's test our work, in theory all we have to do now, is commit our changes and push our code, Devops should pick them up, connect to azure and create our resource group.

the command we execute in our command line are

  1. Add our changes to our repo: git add .
  2. commit our changes : git commit -m 'a comment for our source control'
  3. push our changes: git push
and that's it, now we can go back to our azure devops portal to see if our pipeline ran correctly.



from the looks of our job, it was a success, now lets do one final check and login to our Azure portal and see if in fact we have a resource group called 'rg-pav-master'




and sure enough there it is our resource group, alright next step is to update our powershell to create a Blob storage in our resource group.

let's create a powershell function that does just that.

#create Blob storage account
function CreateStorageAcccount {
    param (
        [Parameter(Mandatory=$true)][String]$name,
        [Parameter(Mandatory=$true)][String]$rgName,
        [Parameter(Mandatory=$true)][String]$location,
        [Parameter(Mandatory=$true)][String]$env)  
   
    $stName = "st$name$env".ToLower();

    $storageAccount = Get-AzStorageAccount `
        -ResourceGroupName $rgName `
        -Name $stName `
        -ErrorAction SilentlyContinue

    if($storageAccount){
        Write-host "Sorage account '$stName' already exists in resouce group '$rgName'" -foregroundcolor yellow
        return $stName
    }

     #create storage account
     try {
        Write-Host "Createing storage account '$stName' in resource group in '$rgname'" -ForegroundColor Magenta
        $storageAccount =  New-AzStorageAccount `
            -ResourceGroupName $rgName `
            -Name $stName `
            -Location $location `
            -SkuName Standard_LRS `
            -Kind BlobStorage `
            -AccessTier Hot
   
        Write-host "Storage account created" -foregroundcolor Green
        return $stName
    }
    catch {
        #failed to create storagte account  
         Write-error "storage account '$stName' NOT created in resouce group '$rgName'" -ErrorAction Stop  -foregroundcolor red
    }
}

notice the section in the try catch statement, here we create a new storage account, we set it to blob storage and to 'Locally-redundant storage (LRS)' because that's the cheapest option.

Now that we have our Blob storage set up, we have to configure a public container for our files, this is where they will be hosted.

#create Blob storage container
function CreateStorageContainer {
    param (
        [Parameter(Mandatory=$true)][String]$name,
        [Parameter(Mandatory=$true)][String]$rgName,
        [Parameter(Mandatory=$true)][String]$env,
        [Parameter(Mandatory=$true)][String]$stName)

    $strContainerName = "container-public-$name-$env".ToLower()

    $key = Get-AzStorageAccountKey `
        -ResourceGroupName $rgName `
        -Name $stName

    $stContext = New-AzStorageContext `
        -StorageAccountName $stName `
        -StorageAccountKey $key[0].Value
    $strContainer = Get-AzStorageContainer `
        -Name $strContainerName `
        -Context $stContext `
        -ErrorAction SilentlyContinue

    if($strContainer){
        Write-host "storage container '$strContainerName' already exists" -foregroundcolor yellow
        return $strContainer
    }

     # Create new storage container
     try {
        Write-Host "Creating storage container $strContainerName" -ForegroundColor Magenta
        $strContainer = New-AzStorageContainer `
            -Name $strContainerName `
            -Permission Blob `
            -Context $stContext
        Write-host "storage container '$strContainerName' created" -foregroundcolor Green
        return $strContainer
    }
    catch {
        #failed to create resource group  
        Write-error "CDN profile '$cdnProfileName' NOT created in '$rgName'" -ErrorAction Stop  
    }
}

with that set up, now we will start building our CDN, that is what is going to cache our images all over the world, so that if our site is hosted in West Europe, but our users are in Australia they will still receive optimal performance.

Let's create a cdn profile function.

# create a CDN profile
function CreateCdnProfile{
    param (
        [Parameter(Mandatory=$true)][String]$name,
        [Parameter(Mandatory=$true)][String]$rgName,
        [Parameter(Mandatory=$true)][String]$location,
        [Parameter(Mandatory=$true)][String]$env)  

        $cdnProfileName = "cdn-profile-$name-$env"
        $cdnProfile = Get-AzCdnProfile `
            -ProfileName $cdnProfileName `
            -ResourceGroupName $rgName `
            -ErrorAction SilentlyContinue

        if($cdnProfile){
            Write-host "CDN '$cdnProfileName' already exists in resource group '$rgName'" -foregroundcolor yellow
            return $cdnProfileName
        }

        # Create a new cdn profile
        try {
            Write-Host "Createing cdn profile $cdnProfileName in resource group in $rgname" -ForegroundColor Magenta
            $cdnProfile =  New-AzCdnProfile `
                -ProfileName $cdnProfileName `
                -ResourceGroupName $rgName `
                -Sku Standard_Microsoft `
                -Location $location
            Write-host "Cdn profile '$cdnProfileName' created in '$rgName'" -foregroundcolor Green
            return $cdnProfileName
        }
        catch {
            #failed to create resourcec group  
            Write-error "CDN profile '$cdnProfileName' NOT created in '$rgName'" -ErrorAction Stop  
        }
}

With that complete, we now create a cdn endpoint.

#create a cdn endpoint
function CreateCdnEndPoint{
    param (
        [Parameter(Mandatory=$true)][String]$cdnProfileName,
        [Parameter(Mandatory=$true)][String]$name,
        [Parameter(Mandatory=$true)][String]$rgName,
        [Parameter(Mandatory=$true)][String]$location,
        [Parameter(Mandatory=$true)][String]$stName,
        [Parameter(Mandatory=$true)][String]$env)  

    $cdnEndPointName = "cdn-endpoint-$name-$env-$location"
    Write-Host "-ResourceGroupName $rgName -ProfileName $cdnProfileName -Name $cdnEndPointName "
   
   $cdnEndpoint = Get-AzCdnEndpoint `
        -ResourceGroupName $rgName `
        -ProfileName $cdnProfileName `
        | Where-Object {$_.Name -eq $cdnEndPointName}

   if($cdnEndpoint){
       Write-host "cdn endpoint '$cdnEndpointName' already exists in resource group $rgName" -foregroundcolor yellow
       return $cdnEndpoint
   }

    # Create new cdn endpoint
    try {
        Write-Host "Createing CDN endPoint $cdnEndPointName in resource group $rgName" -ForegroundColor Magenta
        $originHostHeader = "$stName.blob.core.windows.net"
       
        $origin = @{
            Name = $originHostHeader.Replace('.','-')
            HostName = $originHostHeader
        };
        New-AzCdnEndpoint `
            -ResourceGroupName $rgName `
            -ProfileName $cdnProfileName `
            -OriginHostHeader  $originHostHeader `
            -Name $cdnEndPointName `
            -Location $location `
            -OptimizationType "GeneralWebDelivery" `
            -Origin $origin
           
        Write-host "cdn endpoint '$cdnEndPointName' created in '$rgName'" -foregroundcolor Green
        return $cdnEndpoint
    }
    catch {
        #failed to create cdn endpoint
        Write-error "CDN endpoint '$cdnEndPointName' NOT created in '$rgName'" -ErrorAction Stop  
    }  
}

With all of our function written let's call them at the end of our script.

$rgName = CreateResourceGroup $name $location $env
$stName = CreateStorageAcccount $name $rgName $location $env
$stContainer = CreateStorageContainer $name $rgName $env $stName
$cdnProfileName = CreateCdnProfile $name $rgName $location $env
CreateCdnEndPoint $name $rgName $location $stName $cdnProfileName $env

and for good measure, here's our script in it's entirety 

Param (
    [Parameter()][String]$location,
    [Parameter()][String]$name,
    [Parameter()][String]$env)

    $name= 'pav'
    $location= 'westeurope'
    $env = "master"
    $tenantId= '00000000-0000-00000-00000-0000000000000'
    $subscriptionId= '000000000-00000-0000-0000-000000000000'
#create resource group
function CreateResourceGroup {
    Param(
        [Parameter(Mandatory=$true)][String]$name,
        [Parameter(Mandatory=$true)][String]$location,
        [Parameter(Mandatory=$true)][String]$env)

    #setup resource Group name using prefix
    $rgName = ("rg-$name-$env").ToLower()

    #check if resource group already exists
    $resourceGroup = Get-AzResourceGroup `
        -Name $rgName `
        -ErrorAction SilentlyContinue

    if($resourceGroup){
        Write-host "Resource group '$rgName' already exists" -foregroundcolor yellow
        return $rgName
    }

    #create resource group
    try {
        Write-Host "Createing '$rgName' resource group in '$location'" -ForegroundColor Magenta
        $resourceGroup = New-AzResourceGroup `
            -Name $rgName `
            -Location $location
        Write-host "Resource group '$rgName' created  in '$location'" -foregroundcolor Green
        return $rgName
    }
    catch {
        #failed to create resourcec group  
         Write-error "Resource group '$rgName' NOT created" -ErrorAction Stop  
    }
}

#create Blob storage account
function CreateStorageAcccount {
    param (
        [Parameter(Mandatory=$true)][String]$name,
        [Parameter(Mandatory=$true)][String]$rgName,
        [Parameter(Mandatory=$true)][String]$location,
        [Parameter(Mandatory=$true)][String]$env)  
   
    $stName = "st$name$env";
   
    Write-Host "Create storage account with $stName $rgName $env"

    $storageAccount = Get-AzStorageAccount -ResourceGroupName $rgName -Name $stName -ErrorAction SilentlyContinue

    if($storageAccount){
        Write-host "Sorage account '$stName' already exists in resouce group '$rgName'" -foregroundcolor yellow
        return $stName
    }

     #create storage account
     try {
        Write-Host "Createing storage account '$stName' in resource group in '$rgname'" -ForegroundColor Magenta
        $storageAccount =  New-AzStorageAccount `
            -ResourceGroupName $rgName `
            -Name $stName `
            -Location $location `
            -SkuName Standard_LRS `
            -Kind BlobStorage `
            -AccessTier Hot
   
        Write-host "Storage account created" -foregroundcolor Green
        return $stName
    }
    catch {
        #failed to create storagte account  
         Write-error "storage account '$stName' NOT created in resouce group '$rgName'" -ErrorAction Stop  -foregroundcolor red
    }
}

#create Blob storage container
function CreateStorageContainer {
    param (
        [Parameter(Mandatory=$true)][String]$name,
        [Parameter(Mandatory=$true)][String]$rgName,
        [Parameter(Mandatory=$true)][String]$env,
        [Parameter(Mandatory=$true)][String]$stName)

    $strContainerName = "container-public-$name-$env".ToLower()
    Write-Host "$strContainerName $rgName $env $stName"
    $key = Get-AzStorageAccountKey `
        -ResourceGroupName $rgName `
        -Name $stName

    $stContext = New-AzStorageContext `
        -StorageAccountName $stName `
        -StorageAccountKey $key[0].Value

    $strContainer = Get-AzStorageContainer `
        -Name $strContainerName `
        -Context $stContext `
        -ErrorAction SilentlyContinue

    if($strContainer){
        Write-host "storage container '$strContainerName' already exists" -foregroundcolor yellow
        return $strContainer
    }

     # Create new storage coantainer
     try {
        Write-Host "Createing storage container $strContainerName" -ForegroundColor Magenta
        $strContainer = New-AzStorageContainer `
            -Name $strContainerName `
            -Permission Blob `
            -Context $stContext
        Write-host "storage container '$strContainerName' created" -foregroundcolor Green
        return $strContainer
    }
    catch {
        #failed to create resourcec group  
        Write-error "CDN profile '$cdnProfileName' NOT created in '$rgName'" -ErrorAction Stop  
    }
}

# create a CDN profile
function CreateCdnProfile{
    param (
        [Parameter(Mandatory=$true)][String]$name,
        [Parameter(Mandatory=$true)][String]$rgName,
        [Parameter(Mandatory=$true)][String]$location,
        [Parameter(Mandatory=$true)][String]$env)  

        $cdnProfileName = "cdn-profile-$name-$env"
        $cdnProfile = Get-AzCdnProfile `
            -ProfileName $cdnProfileName `
            -ResourceGroupName $rgName `
            -ErrorAction SilentlyContinue

        if($cdnProfile){
            Write-host "CDN '$cdnProfileName' already exists in resource group '$rgName'" -foregroundcolor yellow
            return $cdnProfileName
        }

        # Create a new cdn profile
        try {
            Write-Host "Createing cdn profile $cdnProfileName in resource group in $rgname" -ForegroundColor Magenta
            $cdnProfile =  New-AzCdnProfile `
                -ProfileName $cdnProfileName `
                -ResourceGroupName $rgName `
                -Sku Standard_Microsoft `
                -Location $location
            Write-host "Cdn profile '$cdnProfileName' created in '$rgName'" -foregroundcolor Green
            return $cdnProfileName
        }
        catch {
            #failed to create resourcec group  
            Write-error "CDN profile '$cdnProfileName' NOT created in '$rgName'" -ErrorAction Stop  
        }
}

#create a cdn endpoint
function CreateCdnEndPoint{
    param (
        [Parameter(Mandatory=$true)][String]$cdnProfileName,
        [Parameter(Mandatory=$true)][String]$name,
        [Parameter(Mandatory=$true)][String]$rgName,
        [Parameter(Mandatory=$true)][String]$location,
        [Parameter(Mandatory=$true)][String]$stName,
        [Parameter(Mandatory=$true)][String]$env)  

    $cdnEndPointName = "cdn-endpoint-$name-$env-$location"
    Write-Host "-ResourceGroupName $rgName -ProfileName $cdnProfileName -Name $cdnEndPointName "
   
   $cdnEndpoint = Get-AzCdnEndpoint `
        -ResourceGroupName $rgName `
        -ProfileName $cdnProfileName `
        | Where-Object {$_.Name -eq $cdnEndPointName}

   if($cdnEndpoint){
       Write-host "cdn endpoint '$cdnEndpointName' already exists in resource group $rgName" -foregroundcolor yellow
       return $cdnEndpoint
   }

    # Create new cdn endpoint
    try {
        Write-Host "Createing CDN endPoint $cdnEndPointName in resource group $rgName" -ForegroundColor Magenta
        $originHostHeader = "$stName.blob.core.windows.net"
       
        $origin = @{
            Name = $originHostHeader.Replace('.','-')
            HostName = $originHostHeader
        };
        New-AzCdnEndpoint `
            -ResourceGroupName $rgName `
            -ProfileName $cdnProfileName `
            -OriginHostHeader  $originHostHeader `
            -Name $cdnEndPointName `
            -Location $location `
            -OptimizationType "GeneralWebDelivery" `
            -Origin $origin
           
        Write-host "cdn endpoint '$cdnEndPointName' created in '$rgName'" -foregroundcolor Green
        return $cdnEndpoint
    }
    catch {
        #failed to create cdn endpoint
        Write-error "CDN endpoint '$cdnEndPointName' NOT created in '$rgName'" -ErrorAction Stop  
    }  
}

Register-AzResourceProvider -ProviderNamespace "Microsoft.Cdn"

$rgName = CreateResourceGroup $name $location $env
$stName = CreateStorageAcccount $name $rgName $location $env
$stContainer = CreateStorageContainer $name $rgName $env $stName
$cdnProfileName = CreateCdnProfile $name $rgName $location $env
$cdnEndPoint = CreateCdnEndPoint $cdnProfileName $name $rgName $location $stName $env


we are done, provisioning our azure infrastructure, now if you do a git add . , git commit -m 'comment', and a git push, our Continuous Integration pipeline, should execute our script and build our Azure infrastructure.

there is one problem, the script will fail to create an endpoint, you'll have to do it manually.