当使用 kaniko 缓存 运行ning docker 构建时,npm 运行 构建不会被缓存

npm run build is not cached when running docker build with kaniko cache

我正在尝试加快我的 Google 用于 React 应用程序的 Cloud Build(github repo). Therefor I started using Kaniko Cache 如官方 Cloud Build 文档中所建议。

看来我构建过程的 npm install 部分现在确实被缓存了。但是,我原以为 npm run build 在源文件未更改时也会被缓存。

我的 Dockerfile:

# Base image has ubuntu, curl, git, openjdk, node & firebase-tools installed
FROM gcr.io/team-timesheets/builder as BUILDER   

## Install dependencies for functions first
WORKDIR /functions
COPY functions/package*.json ./

RUN npm ci

## Install app dependencies next
WORKDIR /
COPY package*.json ./

RUN npm ci

# Copy all app source files
COPY . .

# THIS SEEMS TO BE NEVER CACHED, EVEN WHEN SOURCE FILES HAVENT CHANGED
RUN npm run build:refs \
    && npm run build:production

ARG VCS_COMMIT_ID
ARG VCS_BRANCH_NAME
ARG VCS_PULL_REQUEST
ARG CI_BUILD_ID
ARG CODECOV_TOKEN

ENV VCS_COMMIT_ID=$VCS_COMMIT_ID
ENV VCS_BRANCH_NAME=$VCS_BRANCH_NAME
ENV VCS_PULL_REQUEST=$VCS_PULL_REQUEST
ENV CI_BUILD_ID=$CI_BUILD_ID
ENV CODECOV_TOKEN=$CODECOV_TOKEN

RUN npm run test:cloudbuild \
    && if [ "$CODECOV_TOKEN" != "" ]; \
        then curl -s https://codecov.io/bash | bash -s - -X gcov -X coveragepy -X fix -s coverage; \
    fi

WORKDIR /functions

RUN npm run build

WORKDIR /

ARG FIREBASE_PROJECT_ID
ARG FIREBASE_TOKEN

RUN if [ "$FIREBASE_TOKEN" != "" ]; \
       then firebase deploy --project $FIREBASE_PROJECT_ID --token $FIREBASE_TOKEN; \
    fi

构建输出:

BUILD
Pulling image: gcr.io/kaniko-project/executor:latest
latest: Pulling from kaniko-project/executor
Digest: sha256:b9eec410fa32cd77cdb7685c70f86a96debb8b087e77e63d7fe37eaadb178709
Status: Downloaded newer image for gcr.io/kaniko-project/executor:latest
gcr.io/kaniko-project/executor:latest
INFO[0000] Resolved base name gcr.io/team-timesheets/builder to builder 
INFO[0000] Using dockerignore file: /workspace/.dockerignore 
INFO[0000] Retrieving image manifest gcr.io/team-timesheets/builder 
INFO[0000] Retrieving image gcr.io/team-timesheets/builder 
INFO[0000] Retrieving image manifest gcr.io/team-timesheets/builder 
INFO[0000] Retrieving image gcr.io/team-timesheets/builder 
INFO[0000] Built cross stage deps: map[]                
INFO[0000] Retrieving image manifest gcr.io/team-timesheets/builder 
INFO[0000] Retrieving image gcr.io/team-timesheets/builder 
INFO[0000] Retrieving image manifest gcr.io/team-timesheets/builder 
INFO[0000] Retrieving image gcr.io/team-timesheets/builder 
INFO[0001] Executing 0 build triggers                   
INFO[0001] Resolving srcs [functions/package*.json]...  
INFO[0001] Checking for cached layer gcr.io/team-timesheets/app/cache:9307850446a7754b17d62c95be0c1580672377c1231ae34b1e16fc284d43833a... 
INFO[0001] Using caching version of cmd: RUN npm ci     
INFO[0001] Resolving srcs [package*.json]...            
INFO[0001] Checking for cached layer gcr.io/team-timesheets/app/cache:7ca523b620323d7fb89afdd0784f1169c915edb933e1d6df493f446547c30e74... 
INFO[0001] Using caching version of cmd: RUN npm ci     
INFO[0001] Checking for cached layer gcr.io/team-timesheets/app/cache:1fd7153f10fb5ed1de3032f00b9fb904195d4de9dec77b5bae1a3cb0409e4530... 
INFO[0001] No cached layer found for cmd RUN npm run build:refs     && npm run build:production 
INFO[0001] Unpacking rootfs as cmd COPY functions/package*.json ./ requires it. 
INFO[0026] WORKDIR /functions                           
INFO[0026] cmd: workdir                                 
INFO[0026] Changed working directory to /functions      
INFO[0026] Creating directory /functions                
INFO[0026] Taking snapshot of files...                  
INFO[0026] Resolving srcs [functions/package*.json]...  
INFO[0026] COPY functions/package*.json ./              
INFO[0026] Resolving srcs [functions/package*.json]...  
INFO[0026] Taking snapshot of files...                  
INFO[0026] RUN npm ci                                   
INFO[0026] Found cached layer, extracting to filesystem 
INFO[0029] WORKDIR /                                    
INFO[0029] cmd: workdir                                 
INFO[0029] Changed working directory to /               
INFO[0029] No files changed in this command, skipping snapshotting. 
INFO[0029] Resolving srcs [package*.json]...            
INFO[0029] COPY package*.json ./                        
INFO[0029] Resolving srcs [package*.json]...            
INFO[0029] Taking snapshot of files...                  
INFO[0029] RUN npm ci                                   
INFO[0029] Found cached layer, extracting to filesystem 
INFO[0042] COPY . .                                     
INFO[0043] Taking snapshot of files...                  
INFO[0043] RUN npm run build:refs     && npm run build:production 
INFO[0043] Taking snapshot of full filesystem...        
INFO[0061] cmd: /bin/sh                                 
INFO[0061] args: [-c npm run build:refs     && npm run build:production] 
INFO[0061] Running: [/bin/sh -c npm run build:refs     && npm run build:production] 

> thdk-timesheets-app@1.2.16 build:refs /
> tsc -p common


> thdk-timesheets-app@1.2.16 build:production /
> webpack --env=prod

Hash: e33e0aec56687788a186
Version: webpack 4.43.0
Time: 81408ms
Built at: 12/04/2020 6:57:57 AM
....

现在,由于缓存系统的开销,似乎甚至没有速度优势。

我对 Dockerfiles 比较陌生,所以希望我只是在这里遗漏了一个简单的行。

简答:缓存失效很难。

在 Dockerfile 的 RUN 部分中,任何 命令都可以是 运行。一般来说,docker(使用本地缓存时)或 Kaniko 现在已经决定是否可以缓存此步骤。这通常通过检查输出是否是确定性的来确定 - 换句话说:如果相同的命令再次 运行 ,它是否产生与以前相同的文件更改(相对于最后一个图像)?

现在,这种简单的观点不足以确定可缓存的命令,因为任何命令都可能具有不影响本地文件系统的副作用 - 例如,网络流量。如果您 运行 curl -XPOST https://notify.example.com/build/XYZ 到 post 某个通知 API 的成功或失败的构建,这应该 被缓存。也许您的命令正在为管理员用户生成一个随机密码并将其保存到外部数据库——这一步也不应该被缓存。

另一方面,完全可重现的 npm run build 仍然可能导致两个不同的捆绑包,因为缩小器和捆绑器的工作方式 - 例如其中缩小版和丑化版具有不同的短变量名称。尽管生成的构建在语义上相同,但它们不在字节级别 - 因此虽然这一步可以被缓存,docker 或 kaniko 无法识别。

区分可缓存和不可缓存的行为基本上是不可能的,因此您会一次又一次地在缓存中遇到误报或漏报形式的问题行为。

当我在构建管道时咨询客户时,如果 docker 在某个步骤中判断错误,我通常会将 Dockerfile 分成多个阶段或将缓存未命中或命中逻辑放入脚本中。

拆分 Dockerfile 时,您有一个基础映像(其中包含所有依赖项和其他准备步骤),并将自定义可缓存部分拆分到它自己的 Dockerfile 中——后者随后引用前一个基础映像。这通常意味着,你必须有某种形式的模板(例如,在开始时有一个 FROM ${BASE_IMAGE},然后通过 envsubst 或更复杂的系统如 helm 呈现)。

如果这不适合您的用例,您可以选择自己在脚本中实现逻辑。要找出哪些文件发生了变化,您可以使用 git diff --name-only HEAD HEAD~1。通过将其与更多逻辑相结合,您可以自定义脚本行为以仅在一组特定文件发生更改时执行某些逻辑:

#!/usr/bin/env bash
# only rebuild, if something changed in 'app/'
if [[ ! -z "$(git diff --name-only HEAD HEAD~1 | grep -e '^(app/|package.*)')" ]]; then
  npm run build:ref
  curl -XPOST https://notify.api/deploy/$(git rev-parse --short HEAD)
  // ... further steps ...
fi

您可以轻松地将此逻辑扩展到您的确切需求,并自己完全控制缓存逻辑 - 但您应该只针对涉及 docker 或 kaniko 的误报或漏报的步骤执行此操作,因为不确定的行为将不会缓存以下所有步骤。