Make Azure Keyvault secrets available in entire pipeline - azure

In order to access my secret from the keyvault, I run
- task: AzureKeyVault#2
inputs:
azureSubscription: $(KEYVAULT_SC_DEV)
KeyVaultName: $(KEYVAULT_NAME_DEV)
SecretsFilter: APICREDENTIALS
RunAsPreJob: true
which works fine.
However, I have multiple jobs and am now facing the trouble of having to repeat these lines too many times.
So, is there a way to tell Azure Devops that this secret should be set globally for each job/stage/step.. etc?

If you want to make Azure Keyvault secrets available across multiple jobs or stages with AzureKeyVault#2task, you can use outputs in a different stages.
For example, I’ve set secret password in my KeyVault.
Across multiple jobs:
variables:
# map the output variable from A into this job
password-job-b: $[ dependencies.A.outputs['ouputvariable.mypassword'] ]
Across multiple stage:
variables:
# map the output variable from A into this job
password-stage-two: $[ stageDependencies.One.A.outputs['ouputvariable.mypassword'] ]
Across whole job :
- task: AzureKeyVault#2
RunAsPreJob: true ## Make the secret(s) available to the whole job
Full yaml sample:
trigger:
- none
pool:
vmImage: ubuntu-latest
stages:
- stage: One
jobs:
- job: A
steps:
- task: AzureKeyVault#2
inputs:
azureSubscription: ‘your subscription‘
KeyVaultName: ‘your keyvault name’
SecretsFilter: '*'
RunAsPreJob: true
- task: Bash#3
inputs:
targetType: 'inline'
script: 'echo "##vso[task.setvariable variable=mypassword;isOutput=true]$(password)"'
name : ouputvariable
- job: B
dependsOn : A
variables:
# map the output variable from A into this job
password-job-b: $[ dependencies.A.outputs['ouputvariable.mypassword'] ]
steps:
- script: echo this is password :$(password-job-b) # this step uses the mapped-in variable
- stage: Two
variables:
# map the output variable from A into this job
password-stage-two: $[ stageDependencies.One.A.outputs['ouputvariable.mypassword'] ]
jobs:
- job: C
steps:
- script: echo this is password :$(password-stage-two) # this step uses the mapped-in variable
Result across multiple jobs:
Result across multiple stages:
UPDATE
When issecret is set to true, the value of the variable will be saved as secret .
script: 'echo "##vso[task.setvariable variable=mypassword;isOutput=true;issecret=true]$(password)"'

If you want these secrets available to multiple pipelines one way would be to use the library variables
And reference these in your pipeline
https://learn.microsoft.com/en-us/azure/devops/pipelines/library/variable-groups?view=azure-devops&tabs=yaml#use-a-variable-group
If you want these secrets available to multiple stages/jobs/steps within the same pipeline one way would be to create a pipeline variable
variables:
secretValue: ''
jobs:
- job: RetrieveSecret
steps:
- task: AzureKeyVault#2
inputs:
azureSubscription: $(KEYVAULT_SC_DEV)
KeyVaultName: $(KEYVAULT_NAME_DEV)
SecretsFilter: APICREDENTIALS
OutputVariable: secretValue
Here the RetrieveSecret job retrieves the secret from the Key Vault and stores it in the secretValue pipeline variable.Once the secret has been stored in the pipeline variable, you can reference it from any job or task in your pipeline by using the $(pipelineVariableName) syntax.
The caveat here is that pipeline variables are scoped to a specific job, if you wanted to use the same variable across different jobs then you need to pass this value to the next job sort of like below
jobs:
- job: Job1
steps:
- task: AzureKeyVault#2
inputs:
azureSubscription: $(KEYVAULT_SC_DEV)
KeyVaultName: $(KEYVAULT_NAME_DEV)
SecretsFilter: APICREDENTIALS
OutputVariable: secretValue
- job: Job2
inputs:
secretInput: $(secretValue)
steps:
- task: SomeTask
inputs:
secret: $(secretInput)

We can use "variable groups" to pass the values into a YAML pipeline, which we can make available across all.
Steps1:
Store Key vault key values into Variable Groups
how to use keyvault
Step2:
Use that Variable group into any pipelines
Here is the reference: tutorial from Thomas Thornton

Related

Key Vault secret values are displayed as plain text in Bash script

AzureKeyVault#1 task retrieves all the secrets, some of the secrets are displayed as *** whereas some newly created ones are shown as plain text.
A part of my pipeline:
steps:
- task: AzureKeyVault#1
displayName: Download secrets from KeyVault
inputs:
azureSubscription: azure_sub
KeyVaultName: key_vault
SecretsFilter: '*'
RunAsPreJob: true
- task: PipAuthenticate#1
displayName: Authentication step
inputs:
artifactFeeds: organization
onlyAddExtraIndex: true
- script: |
echo "##vso[task.setvariable variable=keyvault_variable;isOutput=true]$(keyvault_variable)"
displayName: Set environment variables
name: SetVariables
- stage: Stage2
jobs:
- job: check_if_encrypted
steps:
- task: CmdLine#2
displayName: Write secrets
inputs:
script: |
echo keyvault_variable
Is there any changes to the Azure Key Vault or wrong with the pipeline?
Thanks
You're creating an unencrypted copy of the secret value with echo "##vso[task.setvariable variable=keyvault_variable;isOutput=true]$(keyvault_variable)". You should specify isSecret=true if you want it to continue to be a secret.
Refer to the documentation for more details.
It seems we have to explicitly mention : issecret=true in the
echo "##vso[task.setvariable variable=keyvault_variable;isOutput=true]$(keyvault_variable)" script. Only then it masks.
What is not clear is why this has to be set for certain for certain sercrets whereas for others it worked without explicitly mentioning.

How to loop through user-defined variables in a YAML pipeline?

I am trying to loop through user-defined variables in an Azure DevOps YAML pipeline.
The variables have been created through the UI:
Below the YAML pipeline code that I'm using:
trigger:
- dev
- main
pr:
- dev
pool:
vmImage: ubuntu-latest
stages:
- stage:
jobs:
- job: TestVars
steps:
- ${{ each var in variables }}:
- script: |
echo ${{ var.key }}
echo ${{ var.value }}
displayName: ${{ var.key }}
When running the above pipeline only system and build variables are listed (e.g. system, system.hostType, build.queuedBy, etc.).
Any help to loop through user-defined variables would be much appreciated.
Unfortunately, no luck fetching the variables defined in UI. However, if your variables are non-secrets, you can bring them over into the YAML, and they will show up in the loop.
- stage:
variables:
myyamlvar: 1000 # this will show up in the loop
jobs:
- job: TestVars
steps:
- ${{ each var in variables }}:
- script: |
echo ${{ var.key }}
echo ${{ var.value }}
displayName: ${{ var.key }}
Alternatively, instead of using a compile time expression, you can list variables using a runtime construct, for example:
- job: TestRuntimeVars
steps:
- script: |
for var in $(compgen -e); do
echo $var ${!var};
done
This will list all variables including ones defined in the UI.
From the Microsoft docs link you provided, it specifies that:
"Unlike a normal variable, they are not automatically decrypted into
environment variables for scripts. You need to explicitly map secret
variables."
However, one workaround could potentially be to run an azure cli task and get the pipeline variables using az pipelines variable list
Assuming your intention is to get the actual values, in which case maybe that won't suffice. Having said that, you should consider a variable group even if you're not using them in other pipelines since the group can be linked to an Azure KeyVault and map the secrets as variables. You can store your sensitive values in a KeyVault and link it to the variable group which can be used like regular variables in your pipeline.
Or you can access KeyVault secrets right from the AzureKeyVault pipeline task.
To expand on the awnser below. It is a bit round about but you can use the azure devopps CLI. This may be a bit overkill but it does do the job.
trigger:
- main
pool:
vmImage: ubuntu-latest
steps:
- bash: az --version
displayName: 'Show Azure CLI version'
- bash: az devops configure --defaults organization=$(System.TeamFoundationCollectionUri) project=$(System.TeamProject) --use-git-aliases true
displayName: 'Set default Azure DevOps organization and project'
- bash: |
az pipelines variable list --pipeline-id $(System.DefinitionId)
displayName: 'Show build list varibales'
env:
AZURE_DEVOPS_EXT_PAT: $(System.AccessToken)
This approach was taken from a combination of:
https://learn.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#list-variables
and
https://learn.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#list-variables
If the agent is self hosted you may need to install the dev opps cli.

Referencing Azure Key Vault secrets from CI/CD YAML

We have a multi-stage YAML pipeline that does CI/CD to an existing set of Azure Resources
The stages are
Build
Deploy to Development and Run Tests
If Previous succeeded - Deploy to Production and Run Tests
We use the AzureRmWebAppDeployment task during the deployment stages and we use the AppSettings argument to that task to specify environment-specific settings. For example
- task: AzureRmWebAppDeployment#4
displayName: 'Deploy Azure App Service'
inputs:
azureSubscription: '$(azureSubscriptionEndpoint)'
appType: '$(webAppKind)'
WebAppName: 'EXISTING__AZURE_RESOURCENAME-DEV'
Package: '$(Pipeline.Workspace)/**/*.zip'
AppSettings: >
-AzureAd:CallbackPath /signin-oidc
-AzureAd:ClientId [GUID was here]
-AzureAd:Domain [domain was here]
-AzureAd:Instance https://login.microsoftonline.com/
-AzureAd:TenantId [Id was here]
-EmailServer:SMTPPassword SECRETPASSWORD
-EmailServer:SMTPUsername SECRETUSERNAME
There are two settings in that set, EmailServer: SMTPUsername and EmailServer: SMTPPassword that I want to pull from an Azure KeyVault. I know how to reference the KV secret from Azure Portal using the syntax
#Microsoft.KeyVault(SecretUri=https://our.vault.azure.net/secrets/SendGridPassword/ReferenceGuidHere)
but how do I reference the value from the YAML pipeline so it is set in Azure?
As pointed out by Thomas in this comment, Referencing Azure Key Vault secrets from CI/CD YAML
I can explicitly set the value in the YAML file like this:
-EmailServer:SMTPPassword #Microsoft.KeyVault(SecretUri=https://our.vault.azure.net/secrets/SendGridPassword/ReferenceGuidHere)
You need to set an AzureKeyVault#1 task with RunAsPreJob to true, this will make your key vault values available as CI/CD jobs environment variables so you can use it as $(KEY-OF-SECRET-VALUE) on the rest of your stages in the job.
The following piece of yaml file is a working example.
We set for python unittest a set of env variable provided from Azure key-vault
trigger:
batch: true # disable concurrent build for pipeline
branches:
include:
- '*' # CI start for all branches
pool:
vmImage: ubuntu-16.04
stages:
- stage: Test
jobs:
- job: sample_test_stage
steps:
- task: AzureKeyVault#1
inputs:
azureSubscription: 'YOUR SUBSCRIPTION HERE'
KeyVaultName: 'THE-KEY-VAULT-NAME'
SecretsFilter: '*'
RunAsPreJob: true
- task: UsePythonVersion#0
inputs:
versionSpec: '3.7'
- script : python -m unittest discover -v -s tests
displayName: 'Execute python unittest'
env: { MY-ENV-VAL-1: $(SECRET-VALUE-1), MY-ENV-VAL-2: $(SECRET-VALUE-2)}
Note that sometimes you need to approve connection beetween AzureDevops and another Azure service like KeyVault

Changing Azure YAML Pipeline causes authorization to be lost to Resources

I've had this happen to me a number of times where I have a working Azure Pipeline written in YAML. Then I change the Pipeline and then I get the error that There was a resource authorization issue. Typically I delete the pipeline, re-create it, and then it works. However, now it is not working and I continuously get the following error:
So, I click the little button, and it pops up saying, Resources have been Authorized. I attempt to run the pipeline again, and I get the same error.
I am an Account/Collection/Organization Administrator, and created the Library Group originally where it is set to have Access to all Pipelines enabled. I've tried renaming the Pipeline and re-creating it a few times to the same error. short of reverting the pipeline back to it original state what should I do?
--EDIT--
Simply resetting the branch to an earlier version of the pipeline worked. Still no clue on why moving the steps into Stages and Jobs failed though.
--EDIT--
Below will is the YAML I used originally and the updated version. When the updated version gave Resource Authorization issues, I performed a git log and took the commit id of the previous commit that worked, and did a git reset $commitId. Pushed the reset branch back up to Azure DevOps, then it just magically worked.
Original Azure Pipeline YAML:
---
trigger: none
variables:
- name: ProjectFolder
value: tf-datafactory
- group: 'Deploy_Terraform_Library_Group'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: replacetokens#3
displayName: Replace tokens
inputs:
targetFiles: '$(System.DefaultWorkingDirectory)/$(ProjectFolder)/variables.tf'
encoding: 'auto'
writeBOM: true
verbosity: 'detailed'
actionOnMissing: 'warn'
keepToken: false
tokenPrefix: '#{{'
tokenSuffix: '}}#'
- task: AzureCLI#2
displayName: Get the storage account key
inputs:
azureSubscription: '$(ARM.SubscriptionEndpoint)'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
export ContainerAccessKeyExport=$(az storage account keys list \
--resource-group $(StorageResourceGroupName) \
--account-name $(StorageAccountName) \
--query "[0].value")
echo "##vso[task.setvariable variable=ContainerAccessKey]$ContainerAccessKeyExport"
...
I then moved these steps into stages and jobs.
---
parameters:
- name: TerraformAction
displayName: 'Will Terraform Create or Destroy?'
type: 'string'
default: 'create'
values:
- 'create'
- 'destroy'
trigger: none
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: 'Terraform'
displayName: 'Terraform Stage'
variables:
- name: 'TerraformAction'
value: '${{ parameters.TerraformAction }}'
- name: ProjectFolder
value: tf-datafactory
jobs:
- job: 'DeployTerraform'
displayName: 'Terraform Deploy Data Factory'
condition: eq(variables['TerraformAction'], 'create')
variables:
- group: 'Deploy_Terraform_Library_Group'
steps:
- task: replacetokens#3
displayName: Replace tokens
inputs:
targetFiles: '$(System.DefaultWorkingDirectory)/$(ProjectFolder)/variables.tf'
encoding: 'auto'
writeBOM: true
verbosity: 'detailed'
actionOnMissing: 'warn'
keepToken: false
tokenPrefix: '#{{'
tokenSuffix: '}}#'
- task: AzureCLI#2
displayName: Get the storage account key
inputs:
azureSubscription: '$(ARM.SubscriptionEndpoint)'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
export ContainerAccessKeyExport=$(az storage account keys list \
--resource-group $(StorageResourceGroupName) \
--account-name $(StorageAccountName) \
--query "[0].value")
echo "##vso[task.setvariable variable=ContainerAccessKey]$ContainerAccessKeyExport"
...
--EDIT--
What I get from this: https://aka.ms/yamlauthz is that you either need to start with Stages and Jobs from the get go, otherwise you have to stick with the original Pipeline that was created. Most people that let Azure DevOps create their initial pipeline won't know to use the Stages and Jobs because the pipeline generator doesn't do that for them and only starts with Steps.

How to pass environment specific values to Azure pipeline?

I am deploying Service Fabric Application packages and I have several (~15) devtest environments, any one of which can be used to test a code fix. I can pass in the Service Connection so deploying the final package is not the issue. What I can't figure out is how to set the other environment specific variables based on the target environment.
I tried using the Service Connection name to pick one of several variable template files:
variables:
- name: envTemplateFileTest
${{ if eq( variables['DevConnection'], 'Environ01' ) }}:
value: ../Templates/DEV01-Variables-Template.yml
${{ if eq( variables['DevConnection'], 'Environ02' ) }}:
value: ../Templates/DEV02-Variables-Template.yml
... (snip) ...
variables:
- template: ${{ variables.envTemplateFile }}
But UI variables are not set at compile time. So the template expressions see blank values and fail.
I could use a pipeline variable but then QA would have to make a file change and check it in each time they want to deploy to a different environment than last time.
What I currently have is an empty variable template and a powershell script that sets the values based on different script names.
- task: PowerShell#2
inputs:
targetType: 'filePath'
filePath: '$(Build.ArtifactStagingDirectory)\drop\Deployment\Code\Scripts\Set-$(DevConnection)Variables.ps1'
#arguments: # Optional
displayName: Set environment variables
There has got to be a better way than this. Please.
There is not a direct way to achieve this, as the template expression is parsed at compile time.
However I have workaround which no need to write additional ps script and avoid making a file change and check it in to your repo each time.
Since all your devtest environments has the same deployment steps. Then you can create steps template yaml to hold the deployment steps.
Then you can modify your azure-pipelines.yml like below example:
jobs:
- job: A
pool:
vmImage: 'windows-latest'
steps:
- powershell: |
$con = "$(connection)"
if($con -eq "environ1"){echo "##vso[task.setvariable variable=variablegroup;isOutput=true]environ1"}
if($con -eq "environ2"){echo "##vso[task.setvariable variable=variablegroup;isOutput=true]environ2"}
name: setvarStep
- script: echo '$(setvarStep.variablegroup)'
- job: environ1
pool:
vmImage: 'windows-latest'
dependsOn: A
condition: eq(dependencies.A.outputs['setvarStep.variablegroup'], 'environ1')
variables:
- template: environ1.yaml
steps:
- template: deploy-jobs.yaml
- job: environ2
pool:
vmImage: 'windows-latest'
dependsOn: A
condition: eq(dependencies.A.outputs['setvarStep.variablegroup'], 'environ2')
variables:
- template: environ2.yml
steps:
- template: deploy-jobs.yaml
Above yml pipeline use depenpencies and condition. The first job A will output a variable according to the variable (eg.$(connection)) you specify when running the pipeline. In the following jobs, there are conditions to evaluate the output variable. If condition is satisfied then the job will be executed, the job will be skipped if failed on condition.
What we decided to do was add a Powershell script step that sets the variables based on a string passed in.
- task: PowerShell#2
inputs:
targetType: 'filePath'
filePath: $(Build.ArtifactStagingDirectory)\drop\Deployment\Code\Scripts\Set-DefaultValues.ps1
displayName: Set default pipeline variables
Then we load the appropriate file and loop through the variables, setting each in turn.
param(
[string]
$EnvironmentName
)
$environmentValues = #{}
switch ($EnvironmentName) {
'DEV98' { . '.\Dev98-Values.ps1'}
'DEV99' { . '.\Dev99-Values.ps1'}
}
foreach ($keyName in $environmentValues.Keys) {
Write-Output "##vso[task.setvariable variable=$($keyName)]$($environmentValues[$keyName])"
}
This allows us to put the environment specific variables in a plain PSCustom object file and dot import it.
$environmentValues = #{
currentYear = '2020';
has_multiple_nodetypes = 'false';
protocol = 'http';
endpoint = 'vm-dev98.cloudapp.com';
... snip ...
}
So QA has an easier time maintaining the different environment files.
Hope this helps others out there.

Resources