Moby: Configurações de ENV e ARG persistentes para todos os estágios posteriores em compilações de vários estágios

Criado em 26 jun. 2018  ·  20Comentários  ·  Fonte: moby/moby

Descrição

Afaik, não há como compartilhar variáveis ​​entre os estágios (corrija-me se eu estiver errado, por favor). Para compartilhar de um estágio de construção para o próximo, a única opção é COPY arquivos do diretório de um estágio para o estágio atual. Podemos construir algo como arquivos JSON com um despejo de variáveis ​​de tempo de construção, mas acho que esse problema é muito frequente e inundará nossas construções de vários estágios com todos os tipos de problemas malucos de análise JSON. Podemos automatizar isso?

Etapas para reproduzir o problema:

FROM alpine:latest as base1
ARG v1=test
ENV v1=$v1
RUN echo ${v1}

FROM alpine:latest as base2
RUN echo ${v1}

Descreva os resultados que você recebeu:

docker build --no-cache  .
Step 4/6 : RUN echo ${v1}
---> Running in b60a3079864b
test

...

Step 5/6 : FROM alpine:latest as base2
---> 3fd9065eaf02
Step 6/6 : RUN echo ${v1}
---> Running in 1147977afd60

Descreva os resultados que você esperava:

docker build --no-cache --multistage-share-env --multistage-share-arg .
Step 4/6 : RUN echo ${v1}
---> Running in b60a3079864b
test

...

Step 5/6 : FROM alpine:latest as base2
---> 3fd9065eaf02
Step 6/6 : RUN echo ${v1}
---> Running in 1147977afd60
test

Ou talvez possamos usar algo como namespaces para interpolar ${base1.v1}

arebuilder kinquestion

Comentários muito úteis

As instruções corretas do Dockerfile, incluindo ENV vars e ARG têm o escopo definido por estágio de compilação e não serão preservadas no próximo estágio; isso ocorre por design.

Você _pode_, entretanto, definir um _global_ ARG (definido antes do primeiro estágio de compilação) e usar esse valor em cada estágio de compilação;

ARG version_default=v1

FROM alpine:latest as base1
ARG version_default
ENV version=$version_default
RUN echo ${version}
RUN echo ${version_default}

FROM alpine:latest as base2
ARG version_default
RUN echo ${version_default}

Ao construir sem um conjunto --build-arg ;

docker build --no-cache -<<'EOF'
ARG version_default=v1

FROM alpine:latest as base1
ARG version_default
ENV version=$version_default
RUN echo ${version}
RUN echo ${version_default}

FROM alpine:latest as base2
ARG version_default
RUN echo ${version_default}
EOF

Isso produz:

Sending build context to Docker daemon  2.048kB
Step 1/9 : ARG version_default=v1
Step 2/9 : FROM alpine:latest as base1
 ---> 3fd9065eaf02
Step 3/9 : ARG version_default
 ---> Running in 702c05d6f294
Removing intermediate container 702c05d6f294
 ---> 1b2cac6e7585
Step 4/9 : ENV version=$version_default
 ---> Running in 6fb73bc8cdb9
Removing intermediate container 6fb73bc8cdb9
 ---> 656d82ccb6d7
Step 5/9 : RUN echo ${version}
 ---> Running in 403c720d0031
v1
Removing intermediate container 403c720d0031
 ---> d6071c5bd329
Step 6/9 : RUN echo ${version_default}
 ---> Running in d5c76d7d3aaa
v1
Removing intermediate container d5c76d7d3aaa
 ---> 554df1d8584b
Step 7/9 : FROM alpine:latest as base2
 ---> 3fd9065eaf02
Step 8/9 : ARG version_default
 ---> Running in 92400e85c722
Removing intermediate container 92400e85c722
 ---> 5f0cb12f4448
Step 9/9 : RUN echo ${version_default}
 ---> Running in f38802f0d690
v1
Removing intermediate container f38802f0d690
 ---> 4b8caab7870a
Successfully built 4b8caab7870a

E _com_ um --build-arg ;

docker build --no-cache --build-arg version_default=v2 -<<'EOF'
ARG version_default=v1

FROM alpine:latest as base1
ARG version_default
ENV version=$version_default
RUN echo ${version}
RUN echo ${version_default}

FROM alpine:latest as base2
ARG version_default
RUN echo ${version_default}
EOF
Sending build context to Docker daemon  2.048kB
Step 1/9 : ARG version_default=v1
Step 2/9 : FROM alpine:latest as base1
 ---> 3fd9065eaf02
Step 3/9 : ARG version_default
 ---> Running in 7f5dd5885859
Removing intermediate container 7f5dd5885859
 ---> 482ffb014095
Step 4/9 : ENV version=$version_default
 ---> Running in b6c6e9aa3489
Removing intermediate container b6c6e9aa3489
 ---> 83f1c0b82986
Step 5/9 : RUN echo ${version}
 ---> Running in 0805ec04fd20
v2
Removing intermediate container 0805ec04fd20
 ---> ef39d4bd6306
Step 6/9 : RUN echo ${version_default}
 ---> Running in f8747a5bfeeb
v2
Removing intermediate container f8747a5bfeeb
 ---> 72d497d25306
Step 7/9 : FROM alpine:latest as base2
 ---> 3fd9065eaf02
Step 8/9 : ARG version_default
 ---> Running in 57aa2e097787
Removing intermediate container 57aa2e097787
 ---> 45e167d234ce
Step 9/9 : RUN echo ${version_default}
 ---> Running in 8615cd6f6ab6
v2
Removing intermediate container 8615cd6f6ab6
 ---> 1674ad8d3b88
Successfully built 1674ad8d3b88

Todos 20 comentários

As instruções corretas do Dockerfile, incluindo ENV vars e ARG têm o escopo definido por estágio de compilação e não serão preservadas no próximo estágio; isso ocorre por design.

Você _pode_, entretanto, definir um _global_ ARG (definido antes do primeiro estágio de compilação) e usar esse valor em cada estágio de compilação;

ARG version_default=v1

FROM alpine:latest as base1
ARG version_default
ENV version=$version_default
RUN echo ${version}
RUN echo ${version_default}

FROM alpine:latest as base2
ARG version_default
RUN echo ${version_default}

Ao construir sem um conjunto --build-arg ;

docker build --no-cache -<<'EOF'
ARG version_default=v1

FROM alpine:latest as base1
ARG version_default
ENV version=$version_default
RUN echo ${version}
RUN echo ${version_default}

FROM alpine:latest as base2
ARG version_default
RUN echo ${version_default}
EOF

Isso produz:

Sending build context to Docker daemon  2.048kB
Step 1/9 : ARG version_default=v1
Step 2/9 : FROM alpine:latest as base1
 ---> 3fd9065eaf02
Step 3/9 : ARG version_default
 ---> Running in 702c05d6f294
Removing intermediate container 702c05d6f294
 ---> 1b2cac6e7585
Step 4/9 : ENV version=$version_default
 ---> Running in 6fb73bc8cdb9
Removing intermediate container 6fb73bc8cdb9
 ---> 656d82ccb6d7
Step 5/9 : RUN echo ${version}
 ---> Running in 403c720d0031
v1
Removing intermediate container 403c720d0031
 ---> d6071c5bd329
Step 6/9 : RUN echo ${version_default}
 ---> Running in d5c76d7d3aaa
v1
Removing intermediate container d5c76d7d3aaa
 ---> 554df1d8584b
Step 7/9 : FROM alpine:latest as base2
 ---> 3fd9065eaf02
Step 8/9 : ARG version_default
 ---> Running in 92400e85c722
Removing intermediate container 92400e85c722
 ---> 5f0cb12f4448
Step 9/9 : RUN echo ${version_default}
 ---> Running in f38802f0d690
v1
Removing intermediate container f38802f0d690
 ---> 4b8caab7870a
Successfully built 4b8caab7870a

E _com_ um --build-arg ;

docker build --no-cache --build-arg version_default=v2 -<<'EOF'
ARG version_default=v1

FROM alpine:latest as base1
ARG version_default
ENV version=$version_default
RUN echo ${version}
RUN echo ${version_default}

FROM alpine:latest as base2
ARG version_default
RUN echo ${version_default}
EOF
Sending build context to Docker daemon  2.048kB
Step 1/9 : ARG version_default=v1
Step 2/9 : FROM alpine:latest as base1
 ---> 3fd9065eaf02
Step 3/9 : ARG version_default
 ---> Running in 7f5dd5885859
Removing intermediate container 7f5dd5885859
 ---> 482ffb014095
Step 4/9 : ENV version=$version_default
 ---> Running in b6c6e9aa3489
Removing intermediate container b6c6e9aa3489
 ---> 83f1c0b82986
Step 5/9 : RUN echo ${version}
 ---> Running in 0805ec04fd20
v2
Removing intermediate container 0805ec04fd20
 ---> ef39d4bd6306
Step 6/9 : RUN echo ${version_default}
 ---> Running in f8747a5bfeeb
v2
Removing intermediate container f8747a5bfeeb
 ---> 72d497d25306
Step 7/9 : FROM alpine:latest as base2
 ---> 3fd9065eaf02
Step 8/9 : ARG version_default
 ---> Running in 57aa2e097787
Removing intermediate container 57aa2e097787
 ---> 45e167d234ce
Step 9/9 : RUN echo ${version_default}
 ---> Running in 8615cd6f6ab6
v2
Removing intermediate container 8615cd6f6ab6
 ---> 1674ad8d3b88
Successfully built 1674ad8d3b88

outra maneira é usar o contêiner básico para vários estágios:

FROM alpine:latest as base
ARG version_default
ENV version=$version_default

FROM base
RUN echo ${version}

FROM base
RUN echo ${version}
docker build --build-arg=version_default=123 --no-cache .   
Sending build context to Docker daemon  92.67kB
Step 1/7 : FROM alpine:latest as base
 ---> 3fd9065eaf02
Step 2/7 : ARG version_default
 ---> Running in a1ebfdf79f07
Removing intermediate container a1ebfdf79f07
 ---> 3e78800ed9ea
Step 3/7 : ENV version=$version_default
 ---> Running in 105d94baac3f
Removing intermediate container 105d94baac3f
 ---> a14276ddc77b
Step 4/7 : FROM base
 ---> a14276ddc77b
Step 5/7 : RUN echo ${version}
 ---> Running in d92f9b48a6cc
123
Removing intermediate container d92f9b48a6cc
 ---> 6505fe2a14bb
Step 6/7 : FROM base
 ---> a14276ddc77b
Step 7/7 : RUN echo ${version}
 ---> Running in 1b748eea4ef3
123
Removing intermediate container 1b748eea4ef3
 ---> f3311d3ad27e
Successfully built f3311d3ad27e
Time: 0h:00m:04s

Deixe-me encerrar este problema, porque isso é intencional, mas espero que os exemplos acima ajudem você ainda mais; também fique à vontade para continuar a conversa

Se for intencional, o design está errado. Sem uma forma concisa de compartilhar variáveis ​​entre os estágios, é impossível secar. Existem situações inevitáveis ​​e não elimináveis ​​em que muitos estágios precisarão de acesso às mesmas variáveis. A duplicação de definições está sujeita a erros e, portanto, a duplicação do boilerplate para hackear as definições compartilhadas em cada estágio.

A abordagem de "contêiner de base" limita severamente o poder expressivo porque corrige o contêiner de base para cada estágio, embora existam casos de uso válidos em que cada um dos estágios intermediários precisa usar uma imagem de base mínima diferente que fornece uma ferramenta necessária para o estágio.

@thaJeztah
Este projeto pode ser reconsiderado?

Tenho 11 imagens de base que estão disponíveis apenas internamente e são usadas principalmente para nossas várias compilações. Estou tentando fazer uma imagem base “DO zero”, que essas 11 imagens usarão como parte de uma compilação de vários estágios, porque parte da lógica é a mesma em todas as 11, e isso inclui variáveis ​​de ambiente. Portanto, tenho variáveis ​​de ambiente que precisam ser definidas em cada imagem e quero defini-las em minha imagem de base para que a mesma lógica não precise ser replicada em todas as outras imagens.

@ DMaxfield-BDS O escopo de ARG não mudará, mas você pode considerar ter um alvo de compilação separado que crie a imagem base "FROM scratch"; coloque-o no registro e use-o para as outras imagens;

FROM scratch AS scratch-base
ENV foo=bar
ENV bar=baz

construí-lo e enviá-lo para seu registro (interno), e essas 11 imagens de base poderiam usá-lo como base

FROM scratch-base
RUN your stuff

Há outra proposta de EXPORT/IMPORT , que pode se adequar a alguns outros casos de uso; https://github.com/moby/moby/issues/32100

@thaJeztah

Obrigado pela sua resposta. A única coisa que não indiquei é que essas outras 11 imagens têm imagens de base diferentes (python, npm, postgres, openjdk, etc). Portanto, o que estou procurando fazer é colocar toda a configuração / preparação comum em uma imagem de base, incluindo a configuração das variáveis ​​de ambiente necessárias usadas pelo aplicativo da minha empresa, como o seguinte:

FROM scratch AS scratch-base
ARG JAR_DIR='/Input/jars/'
ENV JAR_DOWNLOAD_DIR=$JAR_DIR

Empurre para o meu registro interno. Então, um exemplo de uma das outras 11 imagens faria o seguinte:

FROM scratch-base AS scratch-base
FROM openjdk:8-jdk
COPY --from=scratch-base $JAR_DOWNLOAD_DIR .
RUN <stuff that also uses the JAR_DOWNLOAD_DIR environment variable>

Se eu conseguir usar as variáveis ​​de ambiente definidas na 1ª imagem, não preciso definir essas mesmas configurações em cada uma das outras 11 imagens. Isso me permite um ponto de configuração e permite melhor automação.

Como vou fazer algo assim?

RUN COMMIT_HASH = $ (git rev-parse --short HEAD)

Como posso usar a variável COMMIT_HASH nas etapas RUN subsequentes? Sou forçado a definir a variável e usar tudo em uma única etapa?

Na minha opinião, este design é muito limitado. Talvez um pudesse:

RUN COMMIT_HASH = $ (git rev-parse --short HEAD) AS commit-hash
ARG --from = commit-hash COMMIT_HASH
RUN echo $ {COMMIT_HASH}

@ dnk8n isto é o que eu faço para contornar isso:

No estágio de construção eu faço:

RUN git rev-parse HEAD > commit_hash

em seguida, no outro estágio, copio o arquivo com os dados & e defino a variável de ambiente antes de executar meu aplicativo que o consome:

COPY --from=builder /<build_folder>/commit_hash /<other_stage_folder>/commit_hash

CMD export COMMIT_HASH=$(cat /<other_stage_folder>/commit_hash); java -jar myApp.jar

Você deve ser capaz de fazer algo semelhante para definir o ARG.

No mínimo, devemos ser capazes de fazer algo assim para copiar a variável BAR do outro estágio / imagem para o escopo de construção atual.

ENV --from=stage1 FOO=$BAR

Isso é ainda mais importante ao considerar o uso de uma imagem externa como um palco, porque pode haver metadados importantes nas variáveis ​​de ambiente.

ENV --from=nginx:latest FOO=$BAR

Estou tentando salvar o ambiente em um arquivo em cada estágio, usando este padrão:

FROM base1:version1 AS BUILDER1
WORKDIR /build
RUN env | sort > env.BUILDER1

FROM base2:version2 AS BUILDER2
WORKDIR /build
RUN env | sort > env.BUILDER2

FROM finalbase:version AS FINAL
WORKDIR /build
ENV LANG=C.UTF-8
COPY --from=BUILDER1 /build/env.BUILDER1 .
COPY --from=BUILDER2 /build/env.BUILDER2 .
# Use ". ./env.BUILDER" instead of "source" if /bin/sh is true-POSIX
RUN set -eux ; \
        source ./env.BUILDER1 ; \
        source ./env.BUILDER2 ; \
        env | sort

Como isso pode finalmente ser 'salvo' no 'docker' de forma que fique visível ao executar docker inspect Não tenho certeza, mas poderia ir para / etc / profile, por exemplo.

image

Visto que o último comentário foi tão recente, só me dá uma ideia de quantos desenvolvedores estão questionando as decisões de design por trás do dockerfile. Aparentemente, "DRY" não estava entre as habilidades dominadas. Se você quiser tentar, adicione outra camada e comece a modelar os dockerfiles ...

Para ser honesto, estou muito surpreso com isso ... o "argumento global" não parece funcionar para mim. Se eu mover os argumentos de construção para o topo antes de qualquer coisa, é como se eles não existissem nas etapas seguintes após FROM.

Edit: Eu não tinha percebido que tenho que repetir de qualquer maneira as linhas ARG sem valores após cada FROM ... funciona, mas não é ideal.

A maneira explícita de copiar (obter) a variável de ambiente de outro estágio é obrigatória. Eu tenho um caso de uso válido para isso.
Aqui está um exemplo simplificado:

FROM node:lts-alpine AS node
FROM php:7-fpm-alpine AS php

# Here goes some build instructions for PHP image, then

# Install nodejs, npm, yarn
COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
COPY --from=node /opt /opt

# Create symlinks to npm, yarn binaries
RUN \
    ln -s "/usr/local/lib/node_modules/npm/bin/npm-cli.js" /usr/local/bin/npm \
    && ln -s "/usr/local/lib/node_modules/npm/bin/npx-cli.js" /usr/local/bin/npx \
    && ln -s /opt/yarn-v1.??.?/bin/yarn /usr/local/bin/yarn \
    && ln -s /opt/yarn-v1.??.?/bin/yarnpkg /usr/local/bin/yarnpkg

# Some other instructions

Agora, como vou saber a versão do fio se ela está definida na imagem do nó ( YARN_VERSION ) na variável de ambiente?

A construção de vários estágios é acomodada com enormes benefícios em nossos pipelines de CI. Um recurso como "herança de ambiente" iria empurrá-lo para outro nível de manutenção e também inteligente.

Estou tendo cerca de 5 camadas com uso intenso de env vars e cada atualização é um pesadelo. O lado bom (?), Prefiro pensar duas vezes antes de apresentar uma nova etapa.

A implementação ARG atual apresenta alguns comportamentos curiosos:

FROM python
ARG X=os    # or ENV, the same  
RUN echo import ${X:-sys}   # Proof that the syntax works with default SHELL
SHELL ["python", "-c"]
RUN import os
RUN import ${X}  # Fails
RUN import ${X:-sys}  # Fails too, but this is a documented syntax (for ENV)

ou

FROM alpine
ARG X=ab.cd
RUN echo ${X%.*}  # it's ok but it looks like magic 
WORKDIR ${X%.*}  # Fails here 

Isso está ligado ao fato de que os ARGs são variáveis ​​de ambiente por estágio, não modelos.

Eu sei que você (e eu) provavelmente nunca vimos isso na vida real (manipulação de strings, talvez ...).
Mas isso destaca o fato de que a sintaxe do docker é baseada na sintaxe do shell do underlayer. Isso faz com que a documentação minta: (consulte https://docs.docker.com/engine/reference/builder/#environment-replacement e contra-exemplo " RUN import ${X:-sys} ")

Por que RUN não interpreta ARG antes de lançar o comando como o legado 'make' faz?

$ cat Makefile
SHELL=python
X=os
test:
    import os
    import ${X}
$ make test
import os
import os

24 horas acabaram =)

_Aviso_ esta solução não funciona, embora eu desejasse que funcionasse!

Grave o env var em um arquivo no primeiro estágio e copie esse arquivo no segundo estágio? etc? como isso:

FROM golang:1.14 as cm_base
ARG commit_id
RUN echo "$commit_id" > /tmp/env.json
FROM golang:1.14
COPY --from=cm_base "/tmp/env.json" "/tmp/env.json"
ENV cm_cc_commit_id="$(cat /tmp/env.json)"

boom, exceto que não funciona, uma vez que cm_cc_commit_id se torna o literal $(cat /tmp/env.json) , a última expressão não é avaliada, ela permanece literal. Portanto, configurar variáveis ​​env dinâmicas parece impossível?

No entanto, o que funciona é gravar em um arquivo, transformar seu ponto de entrada em um arquivo bash e fazer o seguinte:

#!/usr/bin/env bash
echo "the docker entrypoint args:" "$@"
export cm_cc_commit_id="$(cat /tmp/env.json)"
"$@"

Pense mais assim:

O Dockerfile não tem um meio de declarar variáveis, exceto por meio de ARG global. Isso não se propaga automaticamente para cada estágio porque tem efeitos no armazenamento em cache.
O construtor pode ser mais inteligente e determinar se você está tentando usar um ARG em um estágio específico e permitir isso, mas então ele tem que adivinhar se você deseja um literal ou não.

Então, sim, você precisa importar explicitamente ARG s globais em cada estágio em que deseja usá-lo. Talvez o construtor ou um linter possa implementar avisos para casos em que parece que você está tentando usar um ARG que você não importou.
Isso não é interrompido por design, é uma escolha explícita para escolher o cache ideal em menos linhas de código.

É definitivamente entendido que o uso de ARG s globais é inicialmente confuso.

Esta página foi útil?
0 / 5 - 0 avaliações