Consuming Environment Secrets in Reusable Workflows
One canonical use of reusable workflows is a reusable deployment job. While this is definitely possible with reusable workflows, it’s not easy to get it working. In this post I’ll show you how to do it.
- tl;dr
- Simple Steps
- Attempt 1: Specify Environment and Uses Together
- Attempt 2: Make Environment an Input
- Attempt 3: Pass the Environment and Secrets
- What Gives?
- Limitations
- Conclusion
Image from user15245033 on www.freepik.com
Last night I was about to go to bed when Damien Brady, Chris Patterson and I got into a Slack discussion about how to use environments, and specifically secrets, with reusable workflows.
The documentation explains that reusable workflows can access secrets via the secrets
keyword, and does mention environments, but it’s not very clear about how to get environment secrets into a reusable workflow.
tl;dr
If you just want the summary about how to use environment secrets in reusable workflows, then just follow these steps:
- Define secrets in your environment
- Make
environment
an input parameter to your reusable workflow - Use
environment: ${{ inputs.environment }}
inside thejob
within the reusable workflow - Declare the
secrets
your reusable workflow requires alongside theinputs
- When you call the reusable workflow, pass in the secrets
A repo with the workflows is here. For a longer explanation and some examples, keep on reading!
Simple Steps
To keep things simple, let’s consider this step as the “heart” of the reusable workflow:
steps:
- name: Dump Password
run: |
echo Password is $PASSWORD
if [[ $PASSWORD == *"PROD"* ]]; then
echo "This is the PROD password!"
else
echo "This is NOT the PROD password!"
fi
env:
PASSWORD: ${{ secrets.PASSWORD }}
Of course, in real life you’d never output any information about a password, but since secrets are masked to look like ***
in the logs, I wanted some way to confirm that we’re getting the correct passwords for the corresponding environment.
Environments
Next, let’s create two environments: dev
and prod
. I’ll navigate to Settings->Environments and create the environments. Then I create a secret called PASSWORD
in each environment, with the value DEV_PASSWORD
for dev
and PROD_PASSWORD
for prod
.
With that setup out of the way, we can attempt to use secrets in a reusable workflow!
Attempt 1: Specify Environment and Uses Together
Let’s try this for the reusable workflow:
# file: '.github/workflows/reusable-deploy.yml'
name: Deploy Template
on:
workflow_call:
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- name: Dump Password
run: |
echo Password is $PASSWORD
if [[ $PASSWORD == *"PROD"* ]]; then
echo "This is the PROD password!"
else
echo "This is NOT the PROD password!"
fi
env:
PASSWORD: ${{ secrets.PASSWORD }}
Seems reasonable. We’re executing a job and (we hope) it’s in the context of an environment, so we can access the secrets defined on the environment.
Here’s our attempt at the caller workflow:
# file: '.github/workflows/deploy-pipeline.yml'
name: Deploy Pipeline
on:
workflow_dispatch:
jobs:
deploy_dev:
name: Deploy to Dev
environment: dev
uses: colindembovsky/reusable-workflows-env-secrets/.github/workflows/reusable-deploy.yml@main
deploy_prod:
name: Deploy to Prod
needs: deploy_dev
environment: prod
uses: colindembovsky/reusable-workflows-env-secrets/.github/workflows/reusable-deploy.yml@main
Again, this seems reasonable. We’re reusing the reusable-deploy.yml
workflow file, and we’re telling Actions which environment we’re running in. Everything from there should just be wired up, right?
Not exactly. If you’re editing the workflow, you’ll see some helpful red squiggles:
That’s not cool.
At this point we could refactor the reusable workflow into a composite workflow so that we can use the environment
. Or…
Attempt 2: Make Environment an Input
Ok Actions, we’re not able to make it work “the easy way”. We’ll just pass the environment into the reusable workflow - then we’ll have access to the secrets!
Let’s try this for our new reusable workflow:
# file: '.github/workflows/reusable-deploy.yml'
name: Deploy Template
on:
workflow_call:
inputs:
environment:
type: string
description: environment to deploy to
required: true
jobs:
deploy:
name: Deploy to ${{ inputs.environment }}
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Dump Password
run: |
echo Password is $PASSWORD
if [[ $PASSWORD == *"PROD"* ]]; then
echo "This is the PROD password!"
else
echo "This is NOT the PROD password!"
fi
env:
PASSWORD: ${{ secrets.PASSWORD }}
And for the caller workflow, we’ll attempt this:
# file: '.github/workflows/deploy-pipeline.yml'
name: Deploy Pipeline
on:
workflow_dispatch:
jobs:
deploy_dev:
name: Deploy to Dev
uses: colindembovsky/reusable-workflows-env-secrets/.github/workflows/reusable-deploy.yml@main
with:
environment: dev
deploy_prod:
name: Deploy to Prod
needs: deploy_dev
uses: colindembovsky/reusable-workflows-env-secrets/.github/workflows/reusable-deploy.yml@main
with:
environment: prod
No red squiggles! Yay!
Unfortunately, when we run the workflow, the PASSWORD
secret is empty:
We’re not stuck yet - let’s press on and try to specify the secrets we need.
Attempt 3: Pass the Environment and Secrets
The documentation hints that we should be able to do this. We’re close. Let’s make one more attempt:
# file: '.github/workflows/reusable-deploy.yml'
name: Deploy Template
on:
workflow_call:
inputs:
environment:
type: string
description: environment to deploy to
required: true
secrets:
PASSWORD:
required: true
jobs:
deploy:
name: Deploy to ${{ inputs.environment }}
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Dump Password
run: |
echo Password is $PASSWORD
if [[ $PASSWORD == *"PROD"* ]]; then
echo "This is the PROD password!"
else
echo "This is NOT the PROD password!"
fi
env:
PASSWORD: ${{ secrets.PASSWORD }}
All we’re doing here is explicitly defining which secrets our reusable workflow needs.
For the caller workflow, we’ll attempt this:
# file: '.github/workflows/deploy-pipeline.yml'
name: Deploy Pipeline
on:
workflow_dispatch:
jobs:
deploy_dev:
name: Deploy to Dev
uses: colindembovsky/reusable-workflows-env-secrets/.github/workflows/reusable-deploy.yml@main
with:
environment: dev
secrets:
PASSWORD: ${{ secrets.PASSWORD }}
deploy_prod:
name: Deploy to Prod
needs: deploy_dev
uses: colindembovsky/reusable-workflows-env-secrets/.github/workflows/reusable-deploy.yml@main
with:
environment: prod
secrets:
PASSWORD: ${{ secrets.PASSWORD }}
It works!
What Gives?
What is happening here?
This is now working because we’re explicitly defining which secrets from the environment the reusable workflow is able to read. We do this by defining the secret
in the workflow_call
of the reusable workflow, as well as passing in the secret from the caller workflow.
The product team made this decision so that you would always know explicitly which secrets a reusable workflow has access to. It makes reusable workflows more secure, but makes understanding the flow and authoring a little trickier.
Another confusing notion is the meaning of the secret from the caller perspective:
jobs:
deploy_dev:
name: Deploy to Dev
uses: colindembovsky/reusable-workflows-env-secrets/.github/workflows/reusable-deploy.yml@main
with:
environment: dev
secrets:
PASSWORD: ${{ secrets.PASSWORD }}
If you look closely at the caller workflow, you’ll see we’re passing ${{ secrets.PASSWORD }}
to the PASSWORD
secret of the reusable workflow. In this context, the value is actually meaningless, since we’re not really “inside” and environment (the workflow doesn’t know what the environment
value means - it’s just a string value). What we’re really doing is passing the key to the secret that we want the reusable workflow to use.
Limitations
If you’ve got a small number of secrets (say less than 10), then this technique works. If you’ve got more secrets for your environments, then you should probably look at storing your secrets in a secret store (like Azure KeyVault or HashiVault) and add steps in your workflow to retrieve the secrets you need from the vault. Then the only secret you really need is the credential to the vault!
Conclusion
Reusable workflows are great - but understanding how environment secrets work with reusable workflows can be challenging. Hopefully this post gives you some insight into how things work so that you can start authoring reusable deployment jobs that use environment secrets!
Happy reusing!