Moby: Conservación de la configuración de ENV y ARG en todas las etapas posteriores en compilaciones de varias etapas

Creado en 26 jun. 2018  ·  20Comentarios  ·  Fuente: moby/moby

Descripción

Afaik, no hay forma de compartir variables entre etapas (corríjanme si me equivoco, por favor). Para compartir de una etapa de compilación a la siguiente, la única opción es COPY archivos del directorio de una etapa a la etapa actual. Podemos crear algo como archivos JSON con una gran cantidad de variables de tiempo de compilación, pero creo que este problema es demasiado frecuente e inundará nuestras compilaciones de múltiples etapas con todo tipo de problemas de análisis JSON locos. ¿Podemos automatizar esto?

Pasos para reproducir el problema:

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

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

Describe los resultados que recibiste:

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

Describe los resultados que esperabas:

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

O tal vez podamos usar algo como espacios de nombres para interpolar ${base1.v1}

arebuilder kinquestion

Comentario más útil

Las instrucciones correctas de Dockerfile, incluidas ENV vars y ARG tienen un alcance por etapa de compilación y no se conservarán en la siguiente etapa; esto es por diseño.

Sin embargo, _puede_ establecer un _global_ ARG (establecido antes de la primera etapa de compilación) y usar ese valor en cada etapa de compilación;

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}

Al construir sin un --build-arg set;

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

Esto produce:

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

Y _con_ un --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 comentarios

Las instrucciones correctas de Dockerfile, incluidas ENV vars y ARG tienen un alcance por etapa de compilación y no se conservarán en la siguiente etapa; esto es por diseño.

Sin embargo, _puede_ establecer un _global_ ARG (establecido antes de la primera etapa de compilación) y usar ese valor en cada etapa de compilación;

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}

Al construir sin un --build-arg set;

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

Esto produce:

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

Y _con_ un --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

otra forma es usar el contenedor base para múltiples etapas:

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

Permítanme cerrar este tema, porque esto es por diseño, pero espero que los ejemplos anteriores lo ayuden más; también siéntete libre de continuar la conversación

Si esto es por diseño, entonces el diseño es incorrecto. Sin una forma concisa de compartir variables entre etapas es imposible SECAR. Hay situaciones inevitables e imposibles de eliminar en las que muchas etapas necesitarán acceder a las mismas variables. La duplicación de definiciones es propensa a errores, al igual que la duplicación del texto estándar para piratear las definiciones compartidas en cada etapa.

El enfoque del "contenedor base" limita severamente el poder expresivo porque fija el contenedor base para cada etapa, mientras que hay casos de uso válidos donde las etapas intermedias necesitan usar una imagen base mínima diferente que proporciona una herramienta requerida para el escenario.

@thaJeztah
¿Se puede reconsiderar este diseño?

Tengo 11 imágenes base que solo están disponibles internamente y se utilizan principalmente para nuestras diversas compilaciones. Estoy tratando de hacer una imagen base "DESDE cero", que esas 11 imágenes usarán como parte de una compilación de múltiples etapas, porque parte de la lógica es la misma en las 11, y esto incluye variables de entorno. Entonces, tengo variables de entorno que deben configurarse dentro de cada imagen y quiero configurarlas dentro de mi imagen base para que no sea necesario replicar la misma lógica en todas las demás imágenes.

@ DMaxfield-BDS El alcance de ARG no cambiará, pero podría considerar tener un objetivo de compilación separado que cree la imagen base "DESDE cero"; enviarlo al registro y usarlo para sus otras imágenes;

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

compruébelo y envíelo a su registro (interno), y esas 11 imágenes base podrían usarlo como base

FROM scratch-base
RUN your stuff

Hay otra propuesta para tener EXPORT/IMPORT , que podría ajustarse a otros casos de uso; https://github.com/moby/moby/issues/32100

@thaJeztah

Gracias por su respuesta. Lo único que no señalé es que estas otras 11 imágenes tienen imágenes base diferentes (python, npm, postgres, openjdk, etc.). Entonces, lo que estoy buscando hacer es poner toda la configuración / preparación común en una imagen base, incluida la configuración de las variables de entorno necesarias utilizadas por la aplicación de mi empresa, como la siguiente:

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

Empuje a mi registro interno. Entonces, un ejemplo de una de las otras 11 imágenes haría lo siguiente:

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>

Si puedo usar las variables de entorno establecidas en la primera imagen, no tengo que configurar estos mismos ajustes en cada una de las otras 11 imágenes. Esto me permite un punto de configuración y permite una mejor automatización.

¿Cómo se supone que haga algo como esto?

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

¿Cómo se me permite usar la variable COMMIT_HASH en los siguientes pasos de RUN? ¿Me veo obligado a definir la variable y usarlo todo en un solo paso?

En mi opinión, este diseño es muy limitado. Quizás uno podría:

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

@ dnk8n esto es lo que hago para

En la etapa de construcción hago:

RUN git rev-parse HEAD > commit_hash

luego, en la otra etapa, copio el archivo con los datos y configuro la variable de entorno antes de ejecutar mi aplicación que lo consume:

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

Debería poder hacer algo similar para configurar el ARG.

Como mínimo, deberíamos poder hacer algo como esto para copiar la variable BAR de la otra etapa / imagen en el ámbito de compilación actual.

ENV --from=stage1 FOO=$BAR

Esto es aún más importante cuando se considera el uso de una imagen externa como escenario porque puede haber metadatos importantes en las variables de entorno.

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

Estoy trabajando guardando el entorno en un archivo en cada etapa, usando este patrón:

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

No estoy seguro de cómo se puede 'guardar' finalmente en 'docker' de modo que sea visible cuando se ejecuta docker inspect , pero podría ir a / etc / profile, por ejemplo.

image

Visto que el último comentario es tan reciente, solo me da una idea de cuántos desarrolladores están cuestionando las decisiones de diseño detrás del dockerfile. Aparentemente, "DRY" no estaba entre las habilidades dominadas. Si quieres probar, agrega otra capa y comienza a crear plantillas para los dockerfiles ...

Para ser honesto, estoy muy sorprendido por esto ... el "argumento global" no parece funcionar para mí. Si muevo los argumentos de compilación a la parte superior antes que nada, es como si no existieran en los siguientes pasos después de FROM.

Editar: No me había dado cuenta de que tenía que repetir de todos modos las líneas ARG sin valores después de cada FROM ... funciona pero no es ideal.

La forma explícita de copiar (obtener) la variable de entorno de otra etapa es imprescindible. Tengo un caso de uso válido para ello.
Aquí hay un ejemplo 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

Ahora, ¿cómo se supone que debo saber la versión del hilo si está definida en la imagen del nodo ( YARN_VERSION ) en la variable de entorno?

La construcción de múltiples etapas se acomoda a lo largo de enormes beneficios en nuestras tuberías de CI. Una característica como la "herencia del entorno" lo llevaría a otro nivel de mantenimiento y funcionalidad también.

Tengo alrededor de 5 capas con un uso intensivo de env vars y cada actualización es una pesadilla. El lado bueno (?), Prefiero pensarlo dos veces antes de presentar una nueva etapa.

La implementación ARG actual da un comportamiento curioso:

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)

o

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

Esto está vinculado por el hecho de que los ARG son variables de entorno por etapa, no plantillas.

Sé que tú (y yo) probablemente nunca hemos visto eso en la vida real (manipulación de cuerdas tal vez ...).
Pero eso resalta el hecho de que la sintaxis de la ventana acoplable se basa en la sintaxis del shell subyacente. Eso hace que la documentación mienta: (ver https://docs.docker.com/engine/reference/builder/#environment-replacement y contraejemplo " RUN import ${X:-sys} ")

¿Por qué RUN no interpreta ARG antes de lanzar el comando como lo hace el legado 'make'?

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

Se acabaron las 24 horas =)

_Advertencia_ esta solución no funciona, ¡aunque me gustaría que lo hiciera!

¿Escribir el env var en un archivo en la primera etapa y copiar ese archivo en la segunda etapa? etc? Me gusta esto:

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, excepto que no funciona, ya que cm_cc_commit_id convierte en el literal $(cat /tmp/env.json) , la última expresión no se evalúa, permanece literal. Entonces, ¿establecer variables env dinámicas parece imposible?

Sin embargo, lo que sí funciona es escribir en un archivo, convertir su punto de entrada en un archivo bash y luego hacer esto:

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

Piense en ello más así:

Dockerfile no tiene un medio para declarar variables excepto a través de ARG globales. Esto no se propaga automáticamente a cada etapa porque tiene efectos sobre el almacenamiento en caché.
El constructor podría ser más inteligente y determinar si está tratando de usar un ARG en una etapa en particular y permitirlo, pero luego tiene que adivinar si desea un literal o no.

Entonces, sí, debe importar explícitamente ARG s globales en cada etapa en la que desea usarlo. Tal vez el constructor o un linter podrían implementar advertencias para los casos en los que parece que podría estar intentando usar un ARG que no ha importado.
Esto no está roto por diseño, es una elección explícita elegir el almacenamiento en caché óptimo en menos líneas de código.

Definitivamente se entiende que el uso de ARG s globales es inicialmente confuso.

¿Fue útil esta página
0 / 5 - 0 calificaciones