使用Github&Gitlab [教程第7部分]的Dockerized PHP应用程序的CI管道
#github #php #docker #cicd

如何通过GitHub Action和Gitlab Pipelines设置用于Dockerized PHP应用程序的CI(连续集成)管道

本文首先出现在CI Pipelines for dockerized PHP Apps with Github & Gitlab [Tutorial Part 7]https://www.pascallandau.com/


在该教程系列的第七部分中,我们将在Docker上开发PHP,我们将设置CI(连续集成)管道,以运行代码质量工具和测试GitHub Action和Gitlab Pipelines 。

所有代码样本均在我的Docker PHP Tutorial repository on Github中公开。您可以在part-7-ci-pipeline-docker-php-gitlab-github上找到本教程的分支。

所有Docker PHP教程的部分已发表的部分均在Docker PHP Tutorial的专用页面下收集。前一部分是Use git-secret to encrypt secrets in the repository,以下是A primer on GCP Compute Instance VMs for dockerized Apps

如果您想关注,请订阅RSS feedvia email,以获取自动通知当下一部分出来时:)

目录

介绍

ci对于 c ost tinuule i ntegration来说,对我来说主要是指在孤立的环境中运行代码质量工具和测试 >(最好是自动)。在团队中工作时,这一点尤其重要,因为 CI系统在将功能或错误文件合并到主分支中之前是最终的守门人

我将脚趾固定在开源水中时,最初了解了CI系统。回到那天,我使用Travis CI进行自己的项目,并在某个时候用Github Actions代替它。在您周围,我们从一个自托管的Jenkins服务器开始,然后作为一个完全管理的解决方案移至Gitlab CI(尽管我们使用custom runners)。

推荐阅读

本教程在前一个部分之上。我会尽最大努力在必要时跨引用相应的文章,但我仍然建议您进行一些预先阅读:

,作为一个不错的知识:

方法

在本教程中,我将解释如何使我们现有的Docker设置与GitHub Action和Gitlab CI/CD Pipelines 一起使用。由于我是“渐进式增强”方法的忠实拥护者,我们将确保所有必要的步骤都可以通过make 在本地执行。这具有保留单个真理来源(Makefile)的其他好处,当我们在两个不同的提供商(Github和Gitlab)上设置CI系统时,它将派上用场。

一般过程看起来与本地开发的过程非常相似:

  • 构建Docker设置
  • 启动Docker设置
  • 运行QA工具
  • 运行测试

您可以在CI setup部分中查看最终结果,包括混凝土yml文件和链接到存储库,请参见

在代码级别上,我们将将CI视为环境,通过ENV变量ENV配置。到目前为止,我们只使用了ENV=local,我们还将将其扩展到使用ENV=ci。在各节中的混凝土CI设置说明

中解释了必要的更改

自己尝试

要了解发生的事情,您可以从executing the local CI run开始:

这应该为您提供与Execution example中介绍的类似输出。

git checkout part-7-ci-pipeline-docker-php-gitlab-github

# Initialize make
make make-init

# Execute the local CI run
bash .local-ci.sh

CI设置

通用CI注意

初始化make的CI

作为第一步,我们需要“配置”代码库以为ci环境操作。这是通过make-init目标完成的,如稍后在Makefile changes部分中通过
更详细地解释

make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=12345678"
$ make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=12345678"
Created a local .make/.env file

ENV=ci确保我们

TAG=latest现在只是一个简化,因为我们还没有对图像做任何事情。在接下来的教程中,我们将将它们推到容器注册表中以供以后在生产部署中使用,然后将TAG设置为更有意义的东西(例如构建号)。

EXECUTE_IN_CONTAINER=true强制使用koude35 setup在容器中运行的每个make命令。这很重要,因为 GitLab跑步者实际上将在Docker容器本身中运行。但是,这将导致任何受影响的目标省略$(DOCKER_COMPOSER) exec前缀

Execute all targets in the application docker container

GPG_PASSWORD=12345678Add a password-protected secret koude8 key中提到的秘密gpg键的密码。

等待服务

我将解释“容器正在启动和运行,但基础服务不是mysql服务的问题,以及我们如何在本文Adding a health check for koude2的本文中通过健康检查解决。有目的,我们不希望docker compose照顾好等待,因为我们可以“更好地利用等待时间”,而是通过位于.docker/scripts/wait-for-service.sh的简单bash脚本自己实现它:

#!/bin/bash

name=$1
max=$2
interval=$3

[ -z "$1" ] && echo "Usage example: bash wait-for-service.sh mysql 5 1"
[ -z "$2" ] && max=30
[ -z "$3" ] && interval=1

echo "Waiting for service '$name' to become healthy, checking every $interval second(s) for max. $max times"

while true; do 
  ((i++))
  echo "[$i/$max] ..."; 
  status=$(docker inspect --format "{{json .State.Health.Status }}" "$(docker ps --filter name="$name" -q)")
  if echo "$status" | grep -q '"healthy"'; then 
   echo "SUCCESS";
   break
  fi
  if [ $i == $max ]; then 
    echo "FAIL"; 
    exit 1
  fi 
  sleep $interval; 
done

此脚本等待docker $service成为docker inspect命令的checking the koude45 info的“健康”。

注意:脚本使用$(docker ps --filter name="$name" -q)来确定容器的ID,即它将“匹配”所有运行容器与$name的匹配 - 如果有多个匹配的容器,这将失败! IE。您必须确保$name足够具体以唯一识别一个单个容器。

脚本将在$interval秒的间隔内检查到$max次。请参阅“我如何在脚本中编写重试逻辑以继续重试以运行5次的these answers?”实施重试逻辑的问题。要检查mysql服务的健康5次,每次尝试之间使用1秒钟,可以通过
来调用

bash wait-for-service.sh mysql 5 1

输出

$ bash wait-for-service.sh mysql 5 1
Waiting for service 'mysql' to become healthy, checking every 1 second(s) for max. 5 times
[1/5] ...
[2/5] ...
[3/5] ...
[4/5] ...
[5/5] ...
FAIL

# OR

$ bash wait-for-service.sh mysql 5 1
Waiting for service 'mysql' to become healthy, checking every 1 second(s) for max. 5 times
[1/5] ...
[2/5] ...
SUCCESS

“容器依赖关系”的问题并不是新事物,并且已经存在一些现有的解决方案,例如

,但不幸的是,所有这些都通过检查host:port组合的可用性,而对于mysql的可用性无济于事,因为容器已上升,端口是可触及的,但是容器中的mysql服务没有。<<<<<<<<<<<<<<<<<<<<<<<<<< /p>

“本地” CI运行的设置

Approach所述,我们希望能够在本地执行所有必要的步骤,并且我在.local-ci.sh上创建了一个相应的脚本:

#!/bin/bash
# fail on any error 
# @see https://stackoverflow.com/a/3474556/413531
set -e

make docker-down ENV=ci || true

start_total=$(date +%s)

# STORE GPG KEY
cp secret-protected.gpg.example secret.gpg

# DEBUG
docker version
docker compose version
cat /etc/*-release || true

# SETUP DOCKER
make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=12345678"
start_docker_build=$(date +%s)
make docker-build
end_docker_build=$(date +%s)
mkdir -p .build && chmod 777 .build

# START DOCKER
start_docker_up=$(date +%s)
make docker-up
end_docker_up=$(date +%s)
make gpg-init
make secret-decrypt-with-password

# QA
start_qa=$(date +%s)
make qa || FAILED=true
end_qa=$(date +%s)

# WAIT FOR CONTAINERS
start_wait_for_containers=$(date +%s)
bash .docker/scripts/wait-for-service.sh mysql 30 1
end_wait_for_containers=$(date +%s)

# TEST
start_test=$(date +%s)
make test || FAILED=true
end_test=$(date +%s)

end_total=$(date +%s)

# RUNTIMES
echo "Build docker:        " `expr $end_docker_build - $start_docker_build`
echo "Start docker:        " `expr $end_docker_up - $start_docker_up  `
echo "QA:                  " `expr $end_qa - $start_qa`
echo "Wait for containers: " `expr $end_wait_for_containers - $start_wait_for_containers`
echo "Tests:               " `expr $end_test - $start_test`
echo "---------------------"
echo "Total:               " `expr $end_total - $start_total`

# CLEANUP
# reset the default make variables
make make-init
make docker-down ENV=ci || true

# EVALUATE RESULTS
if [ "$FAILED" == "true" ]; then echo "FAILED"; exit 1; fi

echo "SUCCESS"

运行详细信息

  • 作为准备步骤,我们首先确保没有过时的ci容器正在运行(这是 只有在本地必要的,因为远程CI系统上的跑步者将开始“从头开始”)
  make docker-down ENV=ci || true
  • 我们需要一些时间测量来了解某些部分通过多长时间
  start_total=$(date +%s)

存储当前的时间戳

  • 我们需要秘密的gpg密钥才能解密秘密并简单地复制 password-protected example key (在实际的CI系统中,密钥将被配置为注入中的秘密值 运行)
  # STORE GPG KEY
  cp secret-protected.gpg.example secret.gpg
  • 我喜欢打印一些调试信息,以了解哪些确切情况 我们正在处理(TBH,在设置CI系统或制作时,这大多是重要的 修改它)
  # DEBUG
  docker version
  docker compose version
  cat /etc/*-release || true
  # SETUP DOCKER
  make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=12345678"

然后构建Docker设置

  make docker-build

,最后将一个.build/目录添加到
collect the build artifacts

  mkdir -p .build && chmod 777 .build
  • 然后,启动了Docker设置
  # START DOCKER
  make docker-up

gpg是初始化的,以便
the secrets can be decrypted

  make gpg-init
  make secret-decrypt-with-password

我们不需要将GPG_PASSWORD传递给secret-decrypt-with-password,因为我们已经设置了
通过make-init

作为默认值,它在上一步中提高
  # QA
  make qa || FAILED=true

|| FAILED=true零件确保如果支票失败,则不会终止脚本。
相反,发生故障的事实是在FAILED变量中“记录”的,以便我们
可以在最后进行评估。我们不希望脚本停在这里,因为我们想要
也要执行以下步骤(例如测试)。

  # WAIT FOR CONTAINERS
  bash .docker/scripts/wait-for-service.sh mysql 30 1
  • 一旦准备就绪,我们可以通过 koude14 make target和 应用与QA工具相同的|| FAILED=true解决方法
  # TEST
  make test || FAILED=true
  • 最后,所有计时器均已打印
  # RUNTIMES
  echo "Build docker:        " `expr $end_docker_build - $start_docker_build`
  echo "Start docker:        " `expr $end_docker_up - $start_docker_up  `
  echo "QA:                  " `expr $end_qa - $start_qa`
  echo "Wait for containers: " `expr $end_wait_for_containers - $start_wait_for_containers`
  echo "Tests:               " `expr $end_test - $start_test`
  echo "---------------------"
  echo "Total:               " `expr $end_total - $start_total`
  • 我们清理资源(这只有在本地运行时才需要,因为 无论如何,CI系统将被关闭)
  # CLEANUP
  make make-init
  make docker-down ENV=ci || true
  • 最终评估运行QA工具或测试时是否发生任何错误
  # EVALUATE RESULTS
  if [ "$FAILED" == "true" ]; then echo "FAILED"; exit 1; fi

  echo "SUCCESS"

执行示例

通过
执行脚本

bash .local-ci.sh

产生以下(缩短)输出:

$ bash .local-ci.sh
Container dofroscra_ci-redis-1  Stopping
# Stopping all other `ci` containers ...
# ...

Client:
 Cloud integration: v1.0.22
 Version:           20.10.13
# Print more debugging info ...
# ...

Created a local .make/.env file
ENV=ci TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker compose -p dofroscra_ci --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose-php-base.yml build php-base
#1 [internal] load build definition from Dockerfile
# Output from building the docker containers 
# ...

ENV=ci TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker compose -p dofroscra_ci --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.local.ci.yml -f ./.docker/docker-compose/docker-compose.ci.yml up -d
Network dofroscra_ci_network  Creating
# Starting all `ci` containers ...
# ...

"C:/Program Files/Git/mingw64/bin/make" -s gpg-import GPG_KEY_FILES="secret.gpg"
gpg: directory '/home/application/.gnupg' created
gpg: keybox '/home/application/.gnupg/pubring.kbx' created
gpg: /home/application/.gnupg/trustdb.gpg: trustdb created
gpg: key D7A860BBB91B60C7: public key "Alice Doe protected <alice.protected@example.com>" imported
# Output of importing the secret and public gpg keys
# ...

"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="reveal -f -p 12345678"
git-secret: done. 1 of 1 files are revealed.
"C:/Program Files/Git/mingw64/bin/make" -j 8 -k --no-print-directory --output-sync=target qa-exec NO_PROGRESS=true
phplint                             done   took 4s
phpcs                               done   took 4s
phpstan                             done   took 8s
composer-require-checker            done   took 8s
Waiting for service 'mysql' to become healthy, checking every 1 second(s) for max. 30 times
[1/30] ...
SUCCESS
PHPUnit 9.5.19 #StandWithUkraine

........                                                            8 / 8 (100%)

Time: 00:03.077, Memory: 28.00 MB

OK (8 tests, 15 assertions)
Build docker:         12
Start docker:         2
QA:                   9
Wait for containers:  3
Tests:                5
---------------------
Total:                46
Created a local .make/.env file

Container dofroscra_ci-application-1  Stopping
Container dofroscra_ci-mysql-1  Stopping
# Stopping all other `ci` containers ...
# ...

SUCCESS

github动作的设置

Github Action example

如果您是GitHub Actions的新手,我建议您从official Quickstart Guide for GitHub ActionsUnderstanding GitHub Actions文章开始。简而言之:

  • github动作基于所谓的工作流程
    • 工作流程是在特殊.github/workflows目录中使用的yaml文件 存储库
  • 工作流可以包含多个作业
  • 每个工作都由一系列步骤组成
  • 每个步骤都需要一个代表由新shell执行的命令的run:元素

    • 应该使用相同外壳的多行命令写为
      - run : |
            echo "line 1"
            echo "line 2"
    

    另请参见difference between "run |" and multiple runs in github actions

工作流文件

github操作会根据.github/workflows目录中的文件自动触发。我添加了文件.github/workflows/ci.yml,其中包含以下内容:

name: CI build and test

on:
  # automatically run for pull request and for pushes to branch "part-7-ci-pipeline-docker-php-gitlab-github"
  # @see https://stackoverflow.com/a/58142412/413531
  push:
    branches:
      - part-7-ci-pipeline-docker-php-gitlab-github
  pull_request: {}
  # enable to trigger the action manually
  # @see https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/
  # CAUTION: there is a known bug that makes the "button to trigger the run" not show up
  # @see https://github.community/t/workflow-dispatch-workflow-not-showing-in-actions-tab/130088/29
  workflow_dispatch: {}

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v1

      - name: start timer
        run: |
          echo "START_TOTAL=$(date +%s)" > $GITHUB_ENV

      - name: STORE GPG KEY
        run: |
          # Note: make sure to wrap the secret in double quotes (")
          echo "${{ secrets.GPG_KEY }}" > ./secret.gpg

      - name: SETUP TOOLS
        run : |
          DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
          # install docker compose
          # @see https://docs.docker.com/compose/cli-command/#install-on-linux
          # @see https://github.com/docker/compose/issues/8630#issuecomment-1073166114
          mkdir -p $DOCKER_CONFIG/cli-plugins 
          curl -sSL https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-linux-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose
          chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose

      - name: DEBUG
        run: |
          docker compose version
          docker --version
          cat /etc/*-release

      - name: SETUP DOCKER
        run: |
          make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=${{ secrets.GPG_PASSWORD }}"
          make docker-build
          mkdir .build && chmod 777 .build

      - name: START DOCKER
        run: |
          make docker-up
          make gpg-init
          make secret-decrypt-with-password

      - name: QA
        run: |
          # Run the tests and qa tools but only store the error instead of failing immediately
          # @see https://stackoverflow.com/a/59200738/413531
          make qa || echo "FAILED=qa" >> $GITHUB_ENV

      - name: WAIT FOR CONTAINERS
        run: |
          # We need to wait until mysql is available.
          bash .docker/scripts/wait-for-service.sh mysql 30 1 

      - name: TEST
        run: |
          make test || echo "FAILED=test $FAILED" >> $GITHUB_ENV

      - name: RUNTIMES
        run: |
          echo `expr $(date +%s) - $START_TOTAL`

      - name: EVALUATE
        run: |
          # Check if $FAILED is NOT empty
          if [ ! -z "$FAILED" ]; then echo "Failed at $FAILED" && exit 1; fi

      - name: upload build artifacts
        uses: actions/upload-artifact@v3
        with:
          name: build-artifacts
          path: ./.build

这些步骤与Run details for the local run上所述的基本相同。一些其他注释:

  on:
    push:
      branches:
        - part-7-ci-pipeline-docker-php-gitlab-github
    pull_request: {}
    workflow_dispatch: {}

对于一个真实的项目,我会让动作仅在
之类的悠久分支上自动运行 maindevelop。如果您只想测试当前的工作
,手动触发器将很有帮助 不提起审查。 注意:有一个
known issue that "hides" the "Trigger workflow" button to trigger the action manually.

      - name: start timer
      run: |
        echo "START_TOTAL=$(date +%s)" > $GITHUB_ENV 

这将是我们使用的唯一计时器,因为该作业使用了定时的多个步骤
自动 - 因此我们不需要手动进行时间戳:
Github Action step times

  • gpg密钥被配置为 encrypted secret命名 GPG_KEY,存储在./secret.gpg中。值是 secret-protected.gpg.example文件
      - name: STORE GPG KEY
        run: |
          echo "${{ secrets.GPG_KEY }}" > ./secret.gpg

秘密在
Settings > Secrets > Actions下的github存储库中配置

  https://github.com/$user/$repository/settings/secrets/actions

  e.g.

  https://github.com/paslandau/docker-php-tutorial/settings/secrets/actions

Github Action Secrets UI

      - name: SETUP TOOLS
      run : |
        DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
        mkdir -p $DOCKER_CONFIG/cli-plugins 
        curl -sSL https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-linux-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose
        chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose
      - name: SETUP DOCKER
        run: |
          make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=${{ secrets.GPG_PASSWORD }}"
      - name: upload build artifacts
        uses: actions/upload-artifact@v3
        with:
          name: build-artifacts
          path: ./.build

你可以
download the artifacts in the Run overview UI
Github Actions: Run overview UI shows build-artifacts

Gitlab管道的设置

Gitlab Pipeline example

如果您是Gitlab管道的新手,我建议您从official Get started with GitLab CI/CD guide开始。简而言之:

  • gitlab管道的核心概念是管道
    • 它是在yaml文件中定义的,该文件生活在存储库的根部
  • 管道可以包含多个阶段
  • 每个阶段都由一系列工作组成
  • 每个工作都包含一个koude100 section
  • script部分由一系列壳命令组成

.gitlab-ci.yml管道文件

gitlab管道是根据位于存储库根的.gitlab-ci.yml文件自动触发的。它具有以下内容:

stages:
  - build_test

QA and Tests:
  stage: build_test

  rules:
    # automatically run for pull request and for pushes to branch "part-7-ci-pipeline-docker-php-gitlab-github"
    - if: '($CI_PIPELINE_SOURCE == "merge_request_event" || $CI_COMMIT_BRANCH == "part-7-ci-pipeline-docker-php-gitlab-github")'

  # see https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-in-docker
  image: docker:20.10.12

  services:
    - name: docker:dind

  script:
    - start_total=$(date +%s)

    ## STORE GPG KEY
    - cp $GPG_KEY_FILE ./secret.gpg

    ## SETUP TOOLS
    - start_install_tools=$(date +%s)
    # "curl" is required to download docker compose
    - apk add --no-cache make bash curl
    # install docker compose
    # @see https://docs.docker.com/compose/cli-command/#install-on-linux
    - mkdir -p ~/.docker/cli-plugins/
    - curl -sSL https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose
    - chmod +x ~/.docker/cli-plugins/docker-compose
    - end_install_tools=$(date +%s)

    ## DEBUG
    - docker version
    - docker compose version
    # show linux distro info
    - cat /etc/*-release

    ## SETUP DOCKER
    # Pass default values to the make-init command - otherwise we would have to pass those as arguments to every make call
    - make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=$GPG_PASSWORD"
    - start_docker_build=$(date +%s)
    - make docker-build
    - end_docker_build=$(date +%s)
    - mkdir .build && chmod 777 .build

    ## START DOCKER
    - start_docker_up=$(date +%s)
    - make docker-up
    - end_docker_up=$(date +%s)
    - make gpg-init
    - make secret-decrypt-with-password

    ## QA
    # Run the tests and qa tools but only store the error instead of failing immediately
    # @see https://stackoverflow.com/a/59200738/413531
    - start_qa=$(date +%s)
    - make qa ENV=ci || FAILED=true
    - end_qa=$(date +%s)

    ## WAIT FOR CONTAINERS
    # We need to wait until mysql is available.
    - start_wait_for_containers=$(date +%s)
    - bash .docker/scripts/wait-for-service.sh mysql 30 1
    - end_wait_for_containers=$(date +%s)

    ## TEST
    - start_test=$(date +%s)
    - make test ENV=ci || FAILED=true
    - end_test=$(date +%s)

    - end_total=$(date +%s)

    # RUNTIMES
    - echo "Tools:" `expr $end_install_tools - $start_install_tools`
    - echo "Build docker:" `expr $end_docker_build - $start_docker_build`
    - echo "Start docker:" `expr $end_docker_up - $start_docker_up  `
    - echo "QA:" `expr $end_qa - $start_qa`
    - echo "Wait for containers:" `expr $end_wait_for_containers - $start_wait_for_containers`
    - echo "Tests:" `expr $end_test - $start_test`
    - echo "Total:" `expr $end_total - $start_total`

    # EVALUATE RESULTS
    # Use if-else constructs in Gitlab pipelines
    # @see https://stackoverflow.com/a/55464100/413531
    - if [ "$FAILED" == "true" ]; then exit 1; fi

  # Save the build artifact, e.g. the JUNIT report.xml file, so we can download it later
  # @see https://docs.gitlab.com/ee/ci/pipelines/job_artifacts.html
  artifacts:
    when: always
    paths:
      # the quotes are required
      # @see https://stackoverflow.com/questions/38009869/how-to-specify-wildcard-artifacts-subdirectories-in-gitlab-ci-yml#comment101411265_38055730
      - ".build/*"
    expire_in: 1 week

这些步骤基本与Run details for the local run下的所述。一些其他注释:

  • 我们首先定义管道的阶段 - 尽管目前只是一个(build_test
  stages:
    - build_test
  • 然后我们定义了QA and Tests的作业,并将其分配到build_test阶段
  QA and Tests:
    stage: build_test
    rules:
    - if: '($CI_PIPELINE_SOURCE == "merge_request_event" || $CI_COMMIT_BRANCH == "part-7-ci-pipeline-docker-php-gitlab-github")'
  image: docker:20.10.12

  services:
    - name: docker:dind
    ## STORE GPG KEY
    - cp $GPG_KEY_FILE ./secret.gpg

秘密可以在
Settings > CI/CD > Variables下进行配置

  https://gitlab.com/$project/$repository/-/settings/ci_cd

  e.g.

  https://gitlab.com/docker-php-tutorial/docker-php-tutorial/-/settings/ci_cd

Gitlab CI/CD Variables UI

  • Docker Base Image并不附带所有必需的工具,因此我们需要安装 缺失(makebashcurldocker compose
      ## SETUP TOOLS
      - apk add --no-cache make bash curl
      - mkdir -p ~/.docker/cli-plugins/
      - curl -sSL https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose
      - chmod +x ~/.docker/cli-plugins/docker-compose
  • 对于make的初始化,我们使用了我们在 CI/CD设置
   ## SETUP DOCKER
    - make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=$GPG_PASSWORD"

注意:我有marked the variable as "masked"
因此,它不会显示在任何日志中

  artifacts:
    when: always
    paths:
      - ".build/*"
    expire_in: 1 week 

可以在Pipeline overview UI中访问它们 Gitlab Pipeline overview UI

表现

性能现在不是问题,因为CI运行仅需约1分钟(github操作)和〜2分钟(gitlab pipelines),但这主要是因为我们只运送一个当事情变得更加复杂时,超级最小的应用程序,这些时间将上升。对于本地设置,我使用了笔记本电脑的所有8个内核。时间分解大致如下:

步骤 gitlab github 本地
没有缓存
local
带有缓存的图像
local
带有缓存的图像 +层
设置工具 1 0 0 0 0
设置Docker 33 17 39 39 5
开始码头 17 11 34 2 1
QA 17 5 10 13 1
等待容器 5 5 3 2 13
测试 3 1 3 6 3
Total
(Depl。RunnerStart)
78 43 97 70 36
Total
(包括Runner启动)
139 54 97 70 36

中取出的时间

优化该教程的性能不超出范围,但我至少会记录我当前的发现。

CI的缓存问题

大部分时间通常用于构建Docker Images 。我们尽最大努力通过利用层缓存和使用缓存安装座来优化该过程(请参阅Build stage koude3 in the koude5 image节)。但是这些步骤在CI系统上是徒劳的,因为相应的跑步者将对每个CI跑步 - 即 都没有局部缓存它们可以使用。结果,整个Docker设置也在每次运行中都“从头开始”。

有一些方法可以减轻例如。

但是:这些都不适合我的盒子:(我们将在即将到来的教程中仔细看一下。到目前为止,我发现的一些阅读材料:

Docker更改

作为第一步,我们需要决定需要哪些容器如何提供代码库

由于我们的目标是运行QA工具和测试,因此我们只需要application PHP容器。测试还需要一个数据库和一个队列,即mysqlredis容器也需要 - 而nginxphp-fpmphp-worker不需要。我们将通过仅包含必要服务的专用docker compose配置文件来处理。这在Compose file updates节中更详细地进行了解释。

Build images for CI

在我们的本地设置中,我们拥有光泽的主机系统和Docker - 主要是因为我们希望我们的更改立即在Docker中进行反映。

Share the codebase between host system and docker container

这对于CI 用例不是必需的。实际上,我们希望我们的 ci图像尽可能靠近生产图像 - 这些图像应该“包含所有独立运行”的图像。 IE。 代码库应生活在图像中 - 不在主机系统上。这将在Use the whole codebase as build context节中进行解释。

Add the codebase in the docker image

撰写文件更新

我们不仅将在CI Docker设置和本地Docker设置(=不同的容器)之间存在一些差异,还将在单个服务的配置中存在一些差异。为此,我们将在.docker/docker-compose/目录中使用以下docker compose配置文件:

  • docker-compose.local.ci.yml
    • 保留对localci有效的配置,试图保留配置文件 DRY
  • Cold148:
    • 保留仅适用于ci的配置
  • docker-compose.local.yml
    • 保留仅适用于local的配置

使用docker compose时,我们需要确保仅包含所需的文件,例如对于ci

docker compose -f docker-compose.local.ci.yml -f docker-compose.ci.yml

我将在ENV based docker compose config节稍后解释逻辑。简而言之:

Assemble docker-compose config files for CI

docker-compose.local.yml

cilocal进行比较时,对于ci

  • 我们不需要与主机系统共享代码库
    application:
      volumes:
      - ${APP_CODE_PATH_HOST?}:${APP_CODE_PATH_CONTAINER?}
  • 我们不需要持续的卷 Redis和MySQL数据
    mysql:
      volumes:
        - mysql:/var/lib/mysql

    redis:
      volumes:
        - redis:/data
  • 我们不需要与主机系统共享端口
    application:
      ports:
        - "${APPLICATION_SSH_HOST_PORT:-2222}:22"

    redis:
      ports:
        - "${REDIS_HOST_PORT:-6379}:6379"
  • 我们不需要本地开发工具的任何设置,例如xdebugstrace
    application:
      environment:
        - PHP_IDE_CONFIG=${PHP_IDE_CONFIG?}
      cap_add:
        - "SYS_PTRACE"
      security_opt:
        - "seccomp=unconfined"
      extra_hosts:
        - host.docker.internal:host-gateway  

因此,所有这些配置值仅在docker-compose.local.yml文件中使用。

docker-compose.ci.yml

实际上,ci只有两件事local没有:

  • 仅与application容器的绑定安装
    application:
      volumes:
        - ${APP_CODE_PATH_HOST?}/secret.gpg:${APP_CODE_PATH_CONTAINER?}/secret.gpg:ro

这个
required to decrypt the secrets

[...]私钥必须命名为secret.gpg并放在代码库的根部,
因此,可以使用make Targets

来简化导入

秘密文件本身被烘烤到图像中,但是解密它们的关键将是
仅在运行时提供和
Add the codebase in the docker image and share a secret key file

  • 共享一个.build文件夹的绑定安装,用于用application容器
    application:
      volumes:
        - ${APP_CODE_PATH_HOST?}/.build:${APP_CODE_PATH_CONTAINER?}/.build

这将用于收集我们要从构建中保留的任何文件(例如,代码覆盖范围
信息,日志文件等)

添加mysql的健康检查

第一次在CI系统上运行测试时,我注意到与数据库有关的一些奇怪错误:

1) Tests\Feature\App\Http\Controllers\HomeControllerTest::test___invoke with data set "default" (array(), '    <li><a href="?dispatch=fo...></li>')
PDOException: SQLSTATE[HY000] [2002] Connection refused

事实证明,mysql容器本身正在启动并运行 - 但是内的mysql Process 该容器尚未准备好接受连接。在本地,这不是问题,因为启动容器后我们通常不会“立即”进行测试 - 但是在CI上是这种情况。

幸运的是,docker compose在这里涵盖了我们,并提供了一个koude171 configuration option

healthcheck声明了一项检查,该检查以确定该服务的容器是否“健康”。

由于此healthchecklocal也“有效”,所以我在组合的docker-compose.local.ci.yml文件中定义了它:

  mysql:
    healthcheck:
      # Only mark the service as healthy if mysql is ready to accept connections
      # Check every 2 seconds for 30 times, each check has a timeout of 1s
      test: mysqladmin ping -h 127.0.0.1 -u $$MYSQL_USER --password=$$MYSQL_PASSWORD
      timeout: 1s
      retries: 30
      interval: 2s

test中的脚本取自SO: Docker-compose check if mysql connection is ready

启动Docker设置时,docker ps现在将为STATUS添加健康信息:

$ make docker-up

$ docker ps
CONTAINER ID   IMAGE                            STATUS                           NAMES
b509eb2f99c0   dofroscra/application-ci:latest  Up 1 seconds                     dofroscra_ci-application-1
503e52fd9e68   mysql:8.0.28                     Up 1 seconds (health: starting)  dofroscra_ci-mysql-1

# a couple of seconds later

$ docker ps
CONTAINER ID   IMAGE                            STATUS                   NAMES
b509eb2f99c0   dofroscra/application-ci:latest  Up 13 seconds            dofroscra_ci-application-1
503e52fd9e68   mysql:8.0.28                     Up 13 seconds (healthy)  dofroscra_ci-mysql-1

请注意(health: starting)(healthy) Infos for mysql服务。

我们还可以从docker inspect(我们的wait-for-service.sh script使用)获得此信息:

$ docker inspect --format "{{json .State.Health.Status }}" dofroscra_ci-mysql-1
"healthy"

fyi:我们还可以在application容器上使用condition: service_healthykoude183 property,以便docker compose只有一旦mysql服务才能启动容器:

application:
  depends_on:
    mysql: 
      condition: service_healthy

但是,这将“阻止” make docker-up,直到mysql实际启动并运行为止。在我们的情况下,这是不可取的,因为同时我们可以做“其他事情”(即:运行qa检查,因为它们不需要数据库),从而在每个CI运行中保存几秒钟。

构建目标:ci

我们已经在Environments and build targets中引入了构建目标,以及如何“选择”它们through koude0 with the koude20 variable defined in a shared koude194 file。简短回顾:

  • 通过make make-init创建一个.make/.env文件,其中包含ENV,例如
  ENV=ci
  ENV=$(ENV) docker-compose

Initialize make to run docker commands with ENV=ci

  • docker compose配置文件中使用ENV变量来确定 build.target属性。例如。在.docker/docker-compose/docker-compose-php-base.yml
    php-base:
      build:
        target: ${ENV?}

Build images for CI

  • 在服务的Dockerfile中,将ENV定义为构建阶段。例如。在 .docker/images/php/base/Dockerfile
  FROM base as ci
  # ...

因此,要启用新的ci环境,我们需要修改php-baseapplication image的dockerfiles。

php-base图像中构建stage ci

将整个代码库用作构建上下文

Docker changes节所述,我们想将代码库“烘烤”到php-base容器的ci图像中。

Add the codebase in the docker image

因此,我们必须更改context .docker/docker-compose/docker-compose-php-base.yml 中的属性,不仅使用.docker/目录,还使用整个代码库。 IE。 “不要使用../,而是../../”:

# File: .docker/docker-compose/docker-compose-php-base.yml

  php-base:
    build:
      # pass the full codebase to docker for building the image
      context: ../../

建立依赖关系

作曲家依赖项也必须在图像中设置,因此我们在.docker/images/php/base/Dockerfile中引入了一个新的阶段。最琐碎的解决方案看起来像这样:

  • 复制整个代码库
  • 运行composer install
FROM base as ci

COPY . /codebase

RUN composer install --no-scripts --no-plugins --no-progress -o

但是,这种方法有一些弊端:

  • 如果在代码库中更改中的任何文件,则COPY . /codebase层将无效。 IE。 docker可以不使用使用layer cache 这也意味着之后每一层也无法使用缓存。结果 composer install每次运行 - 即使composer.json文件不更改。
  • koude228 itself uses a cache 在本地存储依赖关系,因此它不必下载尚未更改的依赖项。 但是,由于我们在Docker中运行composer install ,因此每次都会“扔掉”此缓存 构建完成。为了减轻这种情况,我们可以使用 koude230 定义Docker将在构建之间重复使用的目录: >缓存目录的内容在构建器调用之间持续存在,而无需无效 >指令缓存。

牢记这些要点,我们最终得到以下说明:

# File: .docker/images/php/base/Dockerfile
# ...

FROM base as ci

# By only copying the composer files required to run composer install
# the layer will be cached and only invalidated when the composer dependencies are changed
COPY ./composer.json /dependencies/
COPY ./composer.lock /dependencies/

# use a cache mount to cache the composer dependencies
# this is essentially a cache that lives in Docker BuildKit (i.e. has nothing to do with the host system) 
RUN --mount=type=cache,target=/tmp/.composer \
    cd /dependencies && \
    # COMPOSER_HOME=/tmp/.composer sets the home directory of composer that
    # also controls where composer looks for the cache 
    # so we don't have to download dependencies again (if they are cached)
    COMPOSER_HOME=/tmp/.composer composer install --no-scripts --no-plugins --no-progress -o 

# copy the full codebase
COPY . /codebase

RUN mv /dependencies/vendor /codebase/vendor && \
    cd /codebase && \
    # remove files we don't require in the image to keep the image size small
    rm -rf .docker/ && \
    # we need a git repository for git-secret to work (can be an empty one)
    git init

fyi:COPY . /codebase步骤实际上并未复制“存储库中的所有内容”,因为我们还引入了一个.dockerignore文件,以将某些文件排除在构建上下文中 - 请参见koude232

e节。

最终RUN步骤的一些注释:

  • rm -rf .docker/在当前设置中并没有真正保存“那么多” - 请更多 作为删除最终图像中不应最终文件的任何文件的示例(例如 生产图像”)
  • 需要git init部分,因为我们以后需要解密秘密 - git-secret需要一个git存储库(可以是空的)。我们不能解密秘密 在构建过程中,因为我们不希望解密的秘密文件最终出现在图像中。

在本地测试时,琐碎解决方案与使用层缓存的解决方案之间的差异为〜35秒,请参见Performance节中的结果。

创建最终图像

作为最后一步,我们将重命名为codebase的当前阶段,并将“构建伪像”从该阶段复制到我们的最终ci构建阶段:

FROM base as codebase

# build the composer dependencies and clean up the copied files
# ...

FROM base as ci

COPY --from=codebase --chown=$APP_USER_NAME:$APP_GROUP_NAME /codebase $APP_CODE_PATH

为什么我们不仅直接将上一个阶段用作ci

因为使用multistage-buildsgood practice to keep the final layers of an image to a minimum:上一个codebase阶段中“忘记”的所有事件都将被“忘记”,即不会以层为单位。

不仅可以为我们节省一些图层,而且还允许我们摆脱.docker/目录等文件。我们需要在构建上下文中的目录,因为在Dockerfile的其他部分(例如PHP INI文件)中需要某些文件,因此我们无法通过.dockerignore将其排除。但是我们可以在codebase阶段将其删除 - 因此它不会被复制到最终图像中。如果我们没有codebase阶段,则该文件夹将是COPY从构建上下文中的所有文件并通过rm -rf .docker/删除它时创建的图层的一部分,对图像大小没有影响。

目前,这并不重要,因为建筑步骤非常简单(只是composer install) - 但是在不断增长且更复杂的代码库中,您可以轻松节省几个MB。

要具体,多阶段构建具有31层,包含代码库的最终层的大小为 65.1MB

$ docker image history -H dofroscra/application-ci
IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
d778c2ee8d5e   17 minutes ago   COPY /codebase /var/www/app # buildkit          65.1MB    buildkit.dockerfile.v0
                                                                                ^^^^^^
<missing>      17 minutes ago   WORKDIR /var/www/app                            0B        buildkit.dockerfile.v0
<missing>      17 minutes ago   COPY /usr/bin/composer /usr/local/bin/compos…   2.36MB    buildkit.dockerfile.v0
<missing>      17 minutes ago   COPY ./.docker/images/php/base/.bashrc /root…   395B      buildkit.dockerfile.v0
<missing>      17 minutes ago   COPY ./.docker/images/php/base/.bashrc /home…   395B      buildkit.dockerfile.v0
<missing>      17 minutes ago   COPY ./.docker/images/php/base/conf.d/zz-app…   196B      buildkit.dockerfile.v0
<missing>      17 minutes ago   COPY ./.docker/images/php/base/conf.d/zz-app…   378B      buildkit.dockerfile.v0
<missing>      17 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   1.28kB    buildkit.dockerfile.v0
<missing>      17 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   41MB      buildkit.dockerfile.v0
<missing>      18 minutes ago   ADD https://php.hernandev.com/key/php-alpine…   451B      buildkit.dockerfile.v0
<missing>      18 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   62.1MB    buildkit.dockerfile.v0
<missing>      18 minutes ago   ADD https://gitsecret.jfrog.io/artifactory/a…   450B      buildkit.dockerfile.v0
<missing>      18 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   4.74kB    buildkit.dockerfile.v0
<missing>      18 minutes ago   ENV ENV=ci                                      0B        buildkit.dockerfile.v0
<missing>      18 minutes ago   ENV ALPINE_VERSION=3.15                         0B        buildkit.dockerfile.v0
<missing>      18 minutes ago   ENV TARGET_PHP_VERSION=8.1                      0B        buildkit.dockerfile.v0
<missing>      18 minutes ago   ENV APP_CODE_PATH=/var/www/app                  0B        buildkit.dockerfile.v0
<missing>      18 minutes ago   ENV APP_GROUP_NAME=application                  0B        buildkit.dockerfile.v0
<missing>      18 minutes ago   ENV APP_USER_NAME=application                   0B        buildkit.dockerfile.v0
<missing>      18 minutes ago   ENV APP_GROUP_ID=10001                          0B        buildkit.dockerfile.v0
<missing>      18 minutes ago   ENV APP_USER_ID=10000                           0B        buildkit.dockerfile.v0
<missing>      18 minutes ago   ARG ENV                                         0B        buildkit.dockerfile.v0
<missing>      18 minutes ago   ARG ALPINE_VERSION                              0B        buildkit.dockerfile.v0
<missing>      18 minutes ago   ARG TARGET_PHP_VERSION                          0B        buildkit.dockerfile.v0
<missing>      18 minutes ago   ARG APP_CODE_PATH                               0B        buildkit.dockerfile.v0
<missing>      18 minutes ago   ARG APP_GROUP_NAME                              0B        buildkit.dockerfile.v0
<missing>      18 minutes ago   ARG APP_USER_NAME                               0B        buildkit.dockerfile.v0
<missing>      18 minutes ago   ARG APP_GROUP_ID                                0B        buildkit.dockerfile.v0
<missing>      18 minutes ago   ARG APP_USER_ID                                 0B        buildkit.dockerfile.v0
<missing>      2 days ago       /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
<missing>      2 days ago       /bin/sh -c #(nop) ADD file:5d673d25da3a14ce1…   5.57MB

非培养基构建具有32层,包含代码库的最终层的组合大小为 65.15MB (60.3MB + 4.85MB)。

$ docker image history -H dofroscra/application-ci
IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
94ba50438c9a   2 minutes ago    RUN /bin/sh -c COMPOSER_HOME=/tmp/.composer …   60.3MB    buildkit.dockerfile.v0
<missing>      2 minutes ago    COPY . /var/www/app # buildkit                  4.85MB    buildkit.dockerfile.v0
                                                                                ^^^^^^
<missing>      31 minutes ago   WORKDIR /var/www/app                            0B        buildkit.dockerfile.v0
<missing>      31 minutes ago   COPY /usr/bin/composer /usr/local/bin/compos…   2.36MB    buildkit.dockerfile.v0
<missing>      31 minutes ago   COPY ./.docker/images/php/base/.bashrc /root…   395B      buildkit.dockerfile.v0
<missing>      31 minutes ago   COPY ./.docker/images/php/base/.bashrc /home…   395B      buildkit.dockerfile.v0
<missing>      31 minutes ago   COPY ./.docker/images/php/base/conf.d/zz-app…   196B      buildkit.dockerfile.v0
<missing>      31 minutes ago   COPY ./.docker/images/php/base/conf.d/zz-app…   378B      buildkit.dockerfile.v0
<missing>      31 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   1.28kB    buildkit.dockerfile.v0
<missing>      31 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   41MB      buildkit.dockerfile.v0
<missing>      31 minutes ago   ADD https://php.hernandev.com/key/php-alpine…   451B      buildkit.dockerfile.v0
<missing>      31 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   62.1MB    buildkit.dockerfile.v0
<missing>      31 minutes ago   ADD https://gitsecret.jfrog.io/artifactory/a…   450B      buildkit.dockerfile.v0
<missing>      31 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   4.74kB    buildkit.dockerfile.v0
<missing>      31 minutes ago   ENV ENV=ci                                      0B        buildkit.dockerfile.v0
<missing>      31 minutes ago   ENV ALPINE_VERSION=3.15                         0B        buildkit.dockerfile.v0
<missing>      31 minutes ago   ENV TARGET_PHP_VERSION=8.1                      0B        buildkit.dockerfile.v0
<missing>      31 minutes ago   ENV APP_CODE_PATH=/var/www/app                  0B        buildkit.dockerfile.v0
<missing>      31 minutes ago   ENV APP_GROUP_NAME=application                  0B        buildkit.dockerfile.v0
<missing>      31 minutes ago   ENV APP_USER_NAME=application                   0B        buildkit.dockerfile.v0
<missing>      31 minutes ago   ENV APP_GROUP_ID=10001                          0B        buildkit.dockerfile.v0
<missing>      31 minutes ago   ENV APP_USER_ID=10000                           0B        buildkit.dockerfile.v0
<missing>      31 minutes ago   ARG ENV                                         0B        buildkit.dockerfile.v0
<missing>      31 minutes ago   ARG ALPINE_VERSION                              0B        buildkit.dockerfile.v0
<missing>      31 minutes ago   ARG TARGET_PHP_VERSION                          0B        buildkit.dockerfile.v0
<missing>      31 minutes ago   ARG APP_CODE_PATH                               0B        buildkit.dockerfile.v0
<missing>      31 minutes ago   ARG APP_GROUP_NAME                              0B        buildkit.dockerfile.v0
<missing>      31 minutes ago   ARG APP_USER_NAME                               0B        buildkit.dockerfile.v0
<missing>      31 minutes ago   ARG APP_GROUP_ID                                0B        buildkit.dockerfile.v0
<missing>      31 minutes ago   ARG APP_USER_ID                                 0B        buildkit.dockerfile.v0
<missing>      2 days ago       /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
<missing>      2 days ago       /bin/sh -c #(nop) ADD file:5d673d25da3a14ce1…   5.57MB

再次:预计差异并不大,因为唯一的尺寸节省来自.docker/目录,大小约为70kb。

$ du -hd 0 .docker
73K     .docker

最后,我们还使用koude252 option of the koude234 instruction来确保文件具有正确的权限。

application图像中构建stage ci

实际上在这里无需完成“无所事事”。我们不再需要SSH,因为它仅需要SSH Configuration of PhpStorm。因此,构建阶段只是“空”:

ARG BASE_IMAGE
FROM ${BASE_IMAGE} as base

FROM base as ci

FROM base as local
# ...

尽管有一件事要牢记:在local图像中,我们使用sshd作为入口点,即我们有一个很长的运行过程,可以使容器运行。要保持ci应用程序容器运行,我们必须

  • 通过docker compose-d标志启动(已经在make docker-up目标中完成)
  .PHONY: docker-up
  docker-up: validate-docker-variables
      $(DOCKER_COMPOSE) up -d $(DOCKER_SERVICE_NAME) 
    application:
      tty: true

.dockerignore

koude232 file位于存储库的根部,并确保将某些文件放在Docker build context之外。这将

  • 加快构建(因为需要将更少的文件传输到Docker守护程序)
  • 保持较小的图像(因为将不相关的文件放在图像之外)

语法与.gitignore文件非常相似 - 实际上,我发现.gitignore文件的内容是.dockerignore文件的子集。这有点意义,因为您通常不希望从存储库中排除的文件最终进入Docker Image (例如,未加密的秘密文件)。其他人也注意到了这一点,请参见例如

,但据我所知,目前有(2022-04-24)无法“将两个文件保持同步”。

小心:两个文件之间的行为并不相同!文档说

使用Go s Filepath.Match规则进行匹配。预处理步骤删除了领先和
落后于空格并消除。和..使用Go s Filepath.Clean的元素。预处理后空白的线被忽略。

超越GO的FILEPATH。匹配规则,Docker还支持一个特殊的通配符**

匹配任意数量的目录(包括零)。例如,**/*。go将排除所有
在所有目录中找到的以.go结尾的文件,包括构建上下文的根。

线以! (感叹号)可用于使异常以排除。

请注意有关**\*.go的零件:在.gitignore中,编写.go以匹配包含.go的任何 ,无论目录如何。在.dockerignore中,您必须将其指定为**/*.go

在我们的情况下,.dockerignore文件的内容如下:

# gitignore
!.env.example
**/*.env .idea .phpunit.result.cache vendor/
secret.gpg .gitsecret/keys/random_seed .gitsecret/keys/pubring.kbx~
!*.secret passwords.txt .build

# additionally ignored files .git ```



<!-- generated -->
<a id='makefile-changes'> </a>
<!-- /generated -->

## Makefile changes

<!-- generated -->
<a id='initialize-the-shared-variables'> </a>
<!-- /generated -->

### Initialize the shared variables

We have introduced the concept of [shared variables via `.make/.env`](https://www.pascallandau.com/blog/docker-from-scratch-for-php-applications-in-2022/#shared-variables-make-env)
previously. It allows us to **define variables in one place** (=single source 
of truth) that are then used as "defaults" so we **don't have to define them explicitly** when 
invoking certain `make` targets (like `make docker-build`). We'll make use of this concept by 
setting the environment to `ci`via`ENV=ci` and thus making sure that all docker commands use 
`ci` "automatically" as well.

[![Initialize make to run docker commands with ENV=ci](https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/make-init-ci-docker-commands.PNG)](https://www.pascallandau.com/img/ci-pipeline-docker-php-gitlab-github/make-init-ci-docker-commands.PNG)

In addition, I made a small modification by **introducing a second file at `.make/variables.env`** 
that is also included in the main `Makefile` and **holds the "default" shared variables**. Those 
are neither "secret" nor are they likely to be changed for environment adjustments. The file 
is NOT ignored by `.gitignore` and is basically just the previous `.make/.env.example` file without 
the environment specific variables:



```text
# File .make/variables.env

DOCKER_REGISTRY=docker.io
DOCKER_NAMESPACE=dofroscra
APP_USER_NAME=application
APP_GROUP_NAME=application

.make/.env文件仍然是.gitignored,可以使用make-init初始化 使用ENVS变量的目标:

make make-init ENVS="ENV=ci SOME_OTHER_DEFAULT_VARIABLE=foo"

它将用内容创建一个.make/.env文件

ENV=ci SOME_OTHER_DEFAULT_VARIABLE=foo

如有必要,我们还可以在.make/variables.env文件中定义的变量 ,因为.make/.env最后包含在Makefile中:

# File: Makefile
# ...

# include the default variables
include .make/variables.env
# include the local variables
-include .make/.env

ENVS的默认值是ENV=local TAG=latest,以保留与省略ENVS时相同的默认行为。相应的make-init目标是在主Makefile中定义的,现在看起来像这样:

ENVS?=ENV=local TAG=latest
.PHONY: make-init
make-init: ## Initializes the local .makefile/.env file with ENV variables for make. Use via ENVS="KEY_1=value1 KEY_2=value2"
    @$(if $(ENVS),,$(error ENVS is undefined))
    @rm -f .make/.env
    for variable in $(ENVS); do \
      echo $$variable | tee -a .make/.env > /dev/null 2>&1; \
    done
    @echo "Created a local .make/.env file" 

基于env的docker compose配置

Compose file updates节所述,我们需要根据ENV值选择“正确” docker compose配置文件。这是在.make/02-00-docker.mk中完成的:

# File .make/02-00-docker.mk

# ...

DOCKER_COMPOSE_DIR:=...
DOCKER_COMPOSE_COMMAND:=...

DOCKER_COMPOSE_FILE_LOCAL_CI:=$(DOCKER_COMPOSE_DIR)/docker-compose.local.ci.yml
DOCKER_COMPOSE_FILE_CI:=$(DOCKER_COMPOSE_DIR)/docker-compose.ci.yml
DOCKER_COMPOSE_FILE_LOCAL:=$(DOCKER_COMPOSE_DIR)/docker-compose.local.yml

# we need to "assemble" the correct combination of docker-compose.yml config files
ifeq ($(ENV),ci)
    DOCKER_COMPOSE_FILES:=-f $(DOCKER_COMPOSE_FILE_LOCAL_CI) -f $(DOCKER_COMPOSE_FILE_CI)
else ifeq ($(ENV),local)
    DOCKER_COMPOSE_FILES:=-f $(DOCKER_COMPOSE_FILE_LOCAL_CI) -f $(DOCKER_COMPOSE_FILE_LOCAL)
endif

DOCKER_COMPOSE:=$(DOCKER_COMPOSE_COMMAND) $(DOCKER_COMPOSE_FILES)

现在,当我们使用docker目标的ENV=ci(例如docker-up)时,我们可以看到完整的食谱时,我们可以看到选择了正确的文件,例如

$ make docker-up ENV=ci -n
ENV=ci TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker compose -p dofroscra_ci --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.local.ci.yml -f ./.docker/docker-compose/docker-compose.ci.yml up -d

# =>
# -f ./.docker/docker-compose/docker-compose.local.ci.yml 
# -f ./.docker/docker-compose/docker-compose.ci.yml

Assemble docker-compose config files for CI

代码库更改

为加密文件添加测试

我们已经在上一个教程Use git-secret to encrypt secrets in the repository中介绍了git-secret,并使用它存储了代码库中加密的文件passwords.txt。为了确保解密在CI系统上的预期工作,我在tests/Feature/EncryptionTest.php上添加了一个测试,以检查文件是否存在以及内容是否正确。

class EncryptionTest extends TestCase
{
    public function test_ensure_that_the_secret_passwords_file_was_decrypted()
    {
        $pathToSecretFile = __DIR__."/../../passwords.txt";

        $this->assertFileExists($pathToSecretFile);

        $expected = "my_secret_password\n";
        $actual   = file_get_contents($pathToSecretFile);

        $this->assertEquals($expected, $actual);
    }
}

当然,这在“现实世界情景”中没有意义,因为现在的秘密价值将在测试中暴露出来 - 但现在足以证明工作秘密解密的证明。

添加一个受密码保护的秘密gpg

我在Scenario: Decrypt file中提到,也可以使用受密码保护的秘密gpg键进行额外的安全层。我创建了这样的钥匙,并将其存储在secret-protected.gpg.example的存储库中(在“现实世界情景”中,我不会这样做 - 但是由于这是一个公共教程,我希望您能够完全跟随)。该键的密码是12345678

相应的公钥位于.dev/gpg-keys/alice-protected-public.gpg,属于电子邮件地址alice.protected@example.com。我已经通过
added this email addressre-encrypted the secrets

make gpg-init
make secret-add-user EMAIL="alice.protected@example.com"
make secret-encrypt

现在,当我导入secret-protected.gpg.example键时,我可以解密秘密,尽管我不能使用通常的secret-decrypt target,而必须使用secret-decrypt-with-password

make secret-decrypt-with-password GPG_PASSWORD=12345678

或将GPG_PASSWORD存储在.make/.env文件中,当它初始化为ci

make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=12345678"
make secret-decrypt-with-password

从Phpunit创建JUNIT报告

我已经将koude311 option添加到testphpunit配置中,以创建.make/01-02-application-qa.mk文件中的.build/目录中的XML报告:

# File: .make/01-02-application-qa.mk
# ...

PHPUNIT_CMD=php vendor/bin/phpunit
PHPUNIT_ARGS= -c phpunit.xml --log-junit .build/report.xml

即。现在,测试的每次运行都将在.build/report.xml上创建Junit XML report。该文件用作构建工件的示例,即从CI运行中的“我们想保留的东西”。

包起来

恭喜,您做到了!如果目前尚不清楚某些事情,请随时发表评论。现在,您应该有一个用于GitHub(通过GitHub操作)和/或Gitlab(通过Gitlab Pipelines)的CI管道,该管道自动在每个推动下自动运行。

在本教程的下一部分中,我们将create a VM on GCP and provision it to run dockerized applications

请订阅RSS feedvia email,以获取下一部分出现时自动通知:)