5

Working on a CI/CD pipeline for a Django based API. I need to inject an environmental variable during the build stage to successfully build the image.

  • python manage.py collectstatic is run in the Dockerfile when the image is being built
  • collectstatic needs the SECRET_KEY in the settings.py to run
  • SECRET_KEY is empty during this stage because it is read in with os.environ['SECRET_KEY]
  • Because it is empty, the build fails
  • Thus, I need to set the environment to have this variable

I am having trouble putting that much together, so wanted to see if anyone could assist me.

This is what I have so far:

Azure Pipeline

enter image description here

azure-pipelines.yml

trigger:
  branches:
    include:
    - master

resources:
- repo: self

variables:
  # Container registry service connection established during pipeline creation
  secretKey: $(SECRET_KEY)

  # Agent VM image name
  vmImageName: 'ubuntu-latest'

stages:
- stage: Build
  displayName: Build and push stage
  jobs:  
  - job: Build
    displayName: Build
    pool:
      vmImage: $(vmImageName)
    steps:
    - bash:
      env:
        SECRET_KEY: $(secretKey)
    - task: Docker@2
      displayName: Build and push api image to container registry
      inputs:
        command: buildAndPush
        repository: $(imageRepository)-api
        dockerfile: $(dockerfilePath)/api/Dockerfile
        containerRegistry: $(dockerRegistryServiceConnection)
        tags: |
          $(tag)
    - upload: manifests
      artifact: manifests

Dockerfile

FROM python:3.7-slim
ENV PYTHONUNBUFFERED 1
WORKDIR /app
EXPOSE 5000
COPY requirements*.txt ./
RUN pip install -r requirements.txt
COPY . .
RUN python manage.py collectstatic
CMD ["gunicorn", "-b", ":5000", "--log-level", "info", "config.wsgi:application"]

Build fail log (probably not helpful because I know why it failed... SECRET_KEY isn't in env vars)

Step 8/18 : RUN python manage.py collectstatic
 ---> Running in 1f42a5c062aa
Traceback (most recent call last):
  File "manage.py", line 21, in <module>
    main()
  File "manage.py", line 17, in main
    execute_from_command_line(sys.argv)
  File "/usr/local/lib/python3.7/site-packages/django/core/management/__init__.py", line 381, in execute_from_command_line
    utility.execute()
  File "/usr/local/lib/python3.7/site-packages/django/core/management/__init__.py", line 325, in execute
    settings.INSTALLED_APPS
  File "/usr/local/lib/python3.7/site-packages/django/conf/__init__.py", line 79, in __getattr__
    self._setup(name)
  File "/usr/local/lib/python3.7/site-packages/django/conf/__init__.py", line 66, in _setup
    self._wrapped = Settings(settings_module)
  File "/usr/local/lib/python3.7/site-packages/django/conf/__init__.py", line 157, in __init__
    mod = importlib.import_module(self.SETTINGS_MODULE)
  File "/usr/local/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 728, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/app/config/settings.py", line 26, in <module>
    SECRET_KEY = os.environ['SECRET_KEY']
  File "/usr/local/lib/python3.7/os.py", line 679, in __getitem__
    raise KeyError(key) from None
KeyError: 'SECRET_KEY'
The command '/bin/sh -c python manage.py collectstatic' returned a non-zero code: 1
##[error]The command '/bin/sh -c python manage.py collectstatic' returned a non-zero code: 1
##[error]The process '/usr/bin/docker' failed with exit code 1

I'm just not sure how to accomplish getting the environmental variable in there. My approach could be totally wrong from the beginning.

So how should I go about:

  1. Declaring the environmental variables in the pipeline securely?
  2. Passing said environmental variables into the build stage?

Thanks!

cjones
  • 8,384
  • 17
  • 81
  • 175
  • Not get your latest information, is the workaround helpful for you? Or if you have any concern, feel free to share it here. – Hugh Lin Jan 14 '20 at 10:11
  • @HughLin-MSFT Well, I was hoping there was a way to do this with the `azure-pipelines.yml` and Azure DevOps Pipelines. Option 1. provided below was what I Had in mind if this was not doable. – cjones Jan 14 '20 at 16:01
  • @HughLin-MSFT Would `arguments: --secret` be the means of handling this in the `azure-pipelines.yml`? Are there security issues related to it with the secrets embedded in the image? – cjones Jan 15 '20 at 00:14
  • Meant to include link: https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/build/docker?view=azure-devops#other-commands-and-arguments – cjones Jan 15 '20 at 00:30
  • @HughLin-MSFT Is this just not doable with Variables in Azure Pipelines and in the `azure-pipelines.yml`? Messed with it several more hours and no luck. – cjones Jan 16 '20 at 12:09
  • Please view Levi's answer below. – Hugh Lin Jan 17 '20 at 09:52
  • 1
    Wouldn't it work to have a dummy-key for the build-phase: `RUN SECRET_KEY=dummy django-admin collectstatic`? – mandrake Jan 13 '21 at 19:58
  • @mandrake It would. There's a production example in https://github.com/wagtail/bakerydemo/blob/main/Dockerfile. But if you use `os.environ['YOUR_KEY']` (without the fallback) I believe `collectstatic` will throw on every such env var access occassion. If you have multiple variables it can quickly become ugly. But its' doable. – lexeme Oct 09 '22 at 20:19

3 Answers3

9

Your python manage.py collectstatic is running in the docker container, and its os.environ['SECRET_KEY] will try to get the environment variable of the container where it is running. But the environment variable SECRET_KEY you were setting in your pipeline is for build agent.

You can try following below steps to pass your pipeline env variable to the docker container.

1, I add an ARG and an ENV in your dockerfile ARG SECRET; ENV SECRET_KEY $SECRET

ENV SECRET_KEY refers to the value of ARG SECRET

FROM python:3.7-slim

ARG SECRET
ENV SECRET_KEY $SECRET

ENV PYTHONUNBUFFERED 1
WORKDIR /app
EXPOSE 5000
COPY requirements*.txt ./
RUN pip install -r requirements.txt
COPY . .
RUN python manage.py collectstatic
CMD ["gunicorn", "-b", ":5000", "--log-level", "info", "config.wsgi:application"]

2, I separate docker buildandpush task to dock build and dock push, as buildandpush command cannot accept arguments.

In docker build task. I passed the variable secretKey to the arguments field --build-arg SECRET=$(secretKey). So that when docker run build, ARG SECRET will be replaced by secretKey. And it will be passed to the ENV SECRET_KEY as defined in above dockerfile. So that SECRET_KEY will be set to the docker container's Environment variable.

Then you python code should be able to get the environment variable's value using os.environ['SECRET_KEY]

stages:
- stage: Build
  displayName: Build and push stage
  jobs:  
  - job: Build
    displayName: Build
    pool:
      vmImage: $(vmImageName)
    steps:
    - task: Docker@2
      displayName: Build
      inputs:
        command: build
        repository: $(imageRepository)-api
        dockerfile: $(dockerfilePath)/api/Dockerfile
        containerRegistry: $(dockerRegistryServiceConnection)

        arguments: --build-arg SECRET=$(secretKey)

        tags: |
          $(tag)

    - task: Docker@2
      displayName: Push
      inputs:
        command: push
        repository: $(imageRepository)-api
        containerRegistry: $(dockerRegistryServiceConnection)
        tags: |
          $(tag)


- upload: manifests
  artifact: manifests
Levi Lu-MSFT
  • 27,483
  • 2
  • 31
  • 43
  • 1
    This does work! However, the problem I'm running into now is security. While doing something like `RUN echo ${SECRET_KEY}` doesn't print out the value, when I pull the image and run `docker history ` it does print out a portion of the `SECRET_KEY`. Instead of using `arguments: --build-arg:`, I tried to use `arguments: --secret`, but the values do not get passed into the Dockerfile. Is there a more secure way than using `--build-arg`? – cjones Jan 17 '20 at 19:49
  • If you want to use docker secrets, You might need to use docker compose and set the secret to env variable for your container. You can refer to [this example](https://medium.com/@adrian.gheorghe.dev/using-docker-secrets-in-your-environment-variables-7a0609659aab). You can aslo use a powershell task to execute your docker command in your docker file in pure powershell commands, and set your pipelince secret to the env variable for your container using powershell script. – Levi Lu-MSFT Jan 20 '20 at 03:25
  • Works flawlessly! Thank you – Uma Oct 06 '21 at 19:38
  • @cjones Yes. Use BuildKit and secrets. See my answer below. – Andrew Marshall Dec 10 '21 at 20:04
  • THIS IS SECURITY LEAK !!! NEVER SHARE IMAGE BUILD LIKE THIS !!! `docker history --no-trunc --format "{{.CreatedBy}}" IMAGE_NAME` – CallMarl Sep 22 '22 at 15:09
1

You can inject env variables using --build-arg and corresponding ARG command in Dockerfile, but this is not secure. The key will become embedded in the image!

Your options if you want to be secure:

  1. Download whatever stuff you need to using the secret key outside of Docker build, and just copy that in.
  2. Use the (experimental, but less buggy than in the past) BuildKit backend for Docker, which supports passing in secrets securely.
  3. Run a network server in another container, run your Docker build with --network container:<thatcontainerid> and now you can send queries to that other container to get the secrets. Or similarly run webserver on host, do --network host.

Longer writeup, which also mentions some other alternatives, like copying via multi-stage build: https://pythonspeed.com/articles/docker-build-secrets/

Itamar Turner-Trauring
  • 3,430
  • 1
  • 13
  • 17
1

The way to do this securely in Docker 18.09+ is to use Buildkit and secrets.

Modify your pipeline as follows:

variables:
  DOCKER_BUILDKIT: 1

steps:
  - bash: |
      echo ${SECRET_KEY} > ${PIPELINE_WORKSPACE}/secret_key.txt
    displayName: Store secret
    env:
      SECRET_KEY: $(secretKey)

  - task: Docker@2
    displayName: Build
    inputs:
      command: build
      arguments: --secret id=secret_key,src=$(Pipeline.Workspace)/secret_key.txt

And in your Dockerfile:

RUN --mount=type=secret,id=secret_key \
  export SECRET_KEY=$(cat /run/secrets/secret_key) && \
  python manage.py collectstatic
CMD ["gunicorn", "-b", ":5000", "--log-level", "info", "config.wsgi:application"]
Andrew Marshall
  • 1,469
  • 13
  • 14