VSTS CI/CD with Environment Variables

Curtis Zorn
Slalom Build
Published in
5 min readOct 8, 2018

--

When using VSTS Build/Release pipelines, we can run Powershell and other scripts as part of execution. Typically we have defined Environment Variables for these pipelines that allow us to share common information between Tasks (e.g. — Environment Name, Resource Group, etc.)

But what happens if we have some generated value we want to share between Tasks? Environment Variables can help us here, but there is a lot going on behind the scenes governing how these work. This guide is intended to aid devs in avoiding common issues, and keep deploys running smoothly.

WHAT: Using Environment Variables in Powershell Tasks as part of VSTS Build/Release cycles
WHY: Listing out common pitfalls in the use of Environment Variables between Tasks/Phases/Environments
WHO: DevOps/Engineers

ACRONYMS/DEFINITIONS USED:

  • VSTS - Visual Studio Team Services
  • CI - Continuous Integration
  • CD - Continuous Deployment
  • PS - Powershell
  • SDK - Software Development Kit
  • Environment Variable - An Environment Variable explicitly defined in the “Variables” tab of a given VSTS Build/Release definition
  • Dynamic Environment Variable - An Environment Variable defined during script execution in a given VSTS Build/Release Phase. May also include “overrides” of existing Environment Variables in script.

The Default Syntax

All Environment Variables can be accessed intrinsically by Powershell scripts using the following syntax:
mySimpleVariable is referenced as $env:mySimpleVariable

If the Environment Variable contains periods, they need to be written as underscores such that:
my.complex.variableis referenced as $env:my_complex_variable

It is possible to reference and update Environment Variables using the standard assignment syntax:
$env:foobar = "update_me"

But this has some gotchas attached to it’s use. For demonstration, I’ve setup the following test in a VSTS Build job. It consists of two Powershell Tasks executing back-to-back. Task 1 modifies an Environment Variable, while Task 2 attempts to read the updated version.

Sequential Tasks with Simple PS Scripting

Code is as follows:

Task 1 Code
Task 2 Code

The Build is configured with a custom Environment Variable named PublicVariable:

Output:

Something is amiss…

As you can see in Task 2’s output, we’re still referencing the old value of PublicVariable. Any changes made by Task 1 are NOT persisted between Tasks with this syntax. Additionally, once execution is complete the $env:PublicVariable remains set to I'm a little teapot.

The Dynamic Syntax

In order to persist these changes across Tasks, we need to use an alternate syntax. There are a variety of commands that Visual Studio Online (VSO, since renamed VSTS) exposes to allow scripts to interface with it directly. We’ll invoke the command in PS withWrite-Hostto set variables as follows:
Write-Host "##vso[task.setvariable variable=TARGET_VARIABLE;]VARIABLE_VALUE"

Our Task 1 code is updated to the following, while our simple read operation Task2 code is unchanged:

Altered Task1 Code for Proper Variable settings

Resulting in the following output:

As you can see our variable is successfully updated in Task 2. However, you’ll notice Task 1 is not “up-to-date”. This is by design per language specs:

##vso[task.setvariable variable=TARGET_VARIABLE;]VARIABLE_VALUE

Sets a variable in the variable service of taskcontext. The first task can set a variable, and following tasks in the same phase are able to use the variable. The variable is exposed to the following tasks as an environment variable. When issecret is set to true, the value of the variable will be saved as secret and masked out from log. Secret variables are not passed into tasks as environment variables and must be passed as inputs.
~ VSTS Task Commands

Essentially we’re co-opting Environment Variables in future Tasks as a way to pass values around. These values can either override existing Environment Variables or generate a “dynamic” variable behind the scenes to work with.

Once execution completes, any “overridden” Environment Variable reverts to its original value. Dynamic Environment Variables only live for the lifetime of the Phase and are destroyed after execution.

Best Practices Moving Forward:

Environment Variables are useful for when we have some immutable data that needs to be shared between Phases/Tasks. Overriding Environment Variables mid-Phase makes it difficult to understand what is going on in your pipeline, and should be avoided at all costs.

Dynamic Environment Variables (as defined by shared variables only assigned inline via ##vso[task.setvariable]) are useful for when we want to share some non-deterministic data between Tasks mid-Phase.

Remember, Dynamic Environment Variables are not consumable in the Task that generates them. As such, local variables should be used to store Dynamic Environment Variables in the Task that generates them. The Task can then use the local variable for computation, and when processing is complete stash using ##vso[..] syntax for future re-use.

Ideally, Dynamic Environment Variables should be stored in some guaranteed store like Azure Key Vault by their generating Task. These values can then be referenced via explicit SDK/PS Cmdlet calls in future builds/releases. By doing so we can skip extra processing to recreate the Dynamic Environment Variable (e.g. - API Keys, etc). The retrieved values can be bound off to a Dynamic Environment Variable as we would normally once the Task completes.

In addition, Dynamic Environment Variables should use self-identifying names. Since “downstream” tasks retrieve these variables with $env:foobar we want to be able to differentiate between Environment Variables and Dynamic Environment Variables as a sanity check. For example, your Dynamically Environment Variable (e.g. - EmailApiKey) should be named and referenced something like $env:DynamicEmailApiKey.

Best Practices Demo:

For example, say we have the following Release pipeline into a Development environment:

  1. Deploy a SendGrid resource instance from an ARM Template
  2. Run a PS Task to configure SendGrid’s email API key via SendGrid API calls
  3. At the completion of step (2), store the SendGrid API key in an Azure Key Vault instance and in a Dynamic Environment Variable
  4. Run a second PS Task to query your Azure instance and assure deploy was successful. Use the API key to email the administrator.

Step 1 would deploy the ARM template based on immutable Environment Variable data such as the ResourceGroup target and SendGrid Username/PW combination

Steps 2, 3, and 4 would use Dynamic Environment Variables to easily share an API key between Tasks. Due to SendGrid API restrictions this can only be viewed in-full at the time it’s created.

By using a Key Vault in Step 3, future redeploys can simply make an SDK call to retrieve the API key from the Vault instead of creating a new key from scratch.

RESOURCES:

Github Persisting PS Variables
VSTS Task Commands
Build Variables
Consuming VSO Set Variables in Original Task

--

--