如何通过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 feed或via email,以获取自动通知当下一部分出来时:)
目录
介绍
ci对于 c ost tinuule i ntegration来说,对我来说主要是指在孤立的环境中运行代码质量工具和测试 >(最好是自动)。在团队中工作时,这一点尤其重要,因为 CI系统在将功能或错误文件合并到主分支中之前是最终的守门人。
。我将脚趾固定在开源水中时,最初了解了CI系统。回到那天,我使用Travis CI进行自己的项目,并在某个时候用Github Actions代替它。在您周围,我们从一个自托管的Jenkins服务器开始,然后作为一个完全管理的解决方案移至Gitlab CI(尽管我们使用custom runners)。
推荐阅读
本教程在前一个部分之上。我会尽最大努力在必要时跨引用相应的文章,但我仍然建议您进行一些预先阅读:
- general folder structure, update of the koude9 directory和引入 koude10 directory
- the general usage of koude0 和it's evolution以及 connection to koude12 commands
- docker containers and the koude12 setup的概念
,作为一个不错的知识:
- PhpUnit for the koude14 make target的设置以及 koude15 make target
- usage of koude16 to handle secret values
方法
在本教程中,我将解释如何使我们现有的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开始:
- 结帐分支part-7-ci-pipeline-docker-php-gitlab-github
- initialize
make
- 运行
.local-ci.sh
脚本
这应该为您提供与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
前缀。
GPG_PASSWORD=12345678
是Add 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
- 对于Docker设置,我们从 initializing the environment for koude3
# 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
- 一旦运行了
application
容器,QA工具将通过调用 koude15 make target
# 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动作的设置
- Repository (branch koude73)
- CI/CD overview (Actions)
- Example of a successful job
- Example of a failed job
如果您是GitHub Actions的新手,我建议您从official Quickstart Guide for GitHub Actions和Understanding 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上所述的基本相同。一些其他注释:
- 我希望只有在我时才自动触发操作
push to branch koude73
或创建拉动请求时(通过
pull_request
)。另外,我想 trigger the Action manually on any branch (通过Cold82)。
on:
push:
branches:
- part-7-ci-pipeline-docker-php-gitlab-github
pull_request: {}
workflow_dispatch: {}
对于一个真实的项目,我会让动作仅在
之类的悠久分支上自动运行
main
或develop
。如果您只想测试当前的工作
,手动触发器将很有帮助
不提起审查。 注意:有一个
known issue that "hides" the "Trigger workflow" button to trigger the action manually.
- 每个
run:
指令都会启动一个新的外壳,因此我们必须将计时器存储在“全局”中 environment variable koude86
- name: start timer
run: |
echo "START_TOTAL=$(date +%s)" > $GITHUB_ENV
这将是我们使用的唯一计时器,因为该作业使用了定时的多个步骤
自动 - 因此我们不需要手动进行时间戳:
-
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
-
ubuntu-latest
图像不包含docker compose
插件,因此我们需要 install it manually
- 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
- 对于
make
初始化,我们需要第二个名为GPG_PASSWORD
的秘密 - 在我们的情况下为12345678
配置为12345678
,请参阅 Add a password-protected secret gpg key
- name: SETUP DOCKER
run: |
make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=${{ secrets.GPG_PASSWORD }}"
- 由于跑步者将在跑步后关闭,因此我们需要将构建工件移至 永久位置,使用 actions/upload-artifact@v3 action
- name: upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build-artifacts
path: ./.build
你可以
download the artifacts in the Run overview UI
Gitlab管道的设置
- Repository (branch koude73)
- CI/CD overview (Pipelines)
- Example of a successful job
- Example of a failed job
如果您是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
- 我希望仅在我时自动触发管道 push to branch koude73 或when a pull request is created Triggering the Pipeline manually on any branch is possible by default.
rules:
- if: '($CI_PIPELINE_SOURCE == "merge_request_event" || $CI_COMMIT_BRANCH == "part-7-ci-pipeline-docker-php-gitlab-github")'
- 由于我们要构建和运行Docker映像,因此我们需要使用Docker Base Image并激活
docker:dind
服务。参见Use Docker to build Docker images: Use Docker-in-Docker
image: docker:20.10.12
services:
- name: docker:dind
- 我们将Secret
gpg
密钥存储为秘密文件(使用 "file" type) CI/CD variables configuration of the Gitlab repository 并将其移至./secret.gpg
,以便以后解密秘密
## 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
- Docker Base Image并不附带所有必需的工具,因此我们需要安装
缺失(
make
,bash
,curl
和docker 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"
因此,它不会显示在任何日志中
- 最后,我们存储the job artifacts
artifacts:
when: always
paths:
- ".build/*"
expire_in: 1 week
可以在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 build and test #83" Github Action run
- "Pipeline #511355192" Gitlab Pipeline run
- “没有本地图像的
bash .local-ci.sh
” - 根本没有本地图像
- “带有缓存图像的本地图像”通过
bash .local-ci.sh
,带有mysql
和redis
的缓存图像 - “带有缓存图像 +图层的本地”,通过
bash .local-ci.sh
,带有mysql
的缓存图像和redis
和"warm" layer cache for the koude7 image
优化该教程的性能不超出范围,但我至少会记录我当前的发现。
CI的缓存问题
大部分时间通常用于构建Docker Images 。我们尽最大努力通过利用层缓存和使用缓存安装座来优化该过程(请参阅Build stage koude3 in the koude5 image节)。但是这些步骤在CI系统上是徒劳的,因为相应的跑步者将对每个CI跑步 - 即 都没有局部缓存它们可以使用。结果,整个Docker设置也在每次运行中都“从头开始”。
有一些方法可以减轻例如。
- 将图像推向容器注册表并在构建图像之前将其拉动
通过koude128 option的层缓存
docker compose
- 通过 koude131和 koude132, 将它们存储在内置的缓存中 Github 或Gitlab
- 使用koude133 和koude134选项 koude135
但是:这些都不适合我的盒子:(我们将在即将到来的教程中仔细看一下。到目前为止,我发现的一些阅读材料:
- Caching Docker builds in GitHub Actions: Which approach is the fastest? 🤔 A research.
- Caching strategies for CI systems
- Build images on GitHub Actions with Docker layer caching
- Faster CI Builds with Docker Layer Caching and BuildKit
- Image rebase and improved remote cache support in new BuildKit
Docker更改
作为第一步,我们需要决定需要哪些容器和如何提供代码库。
由于我们的目标是运行QA工具和测试,因此我们只需要application
PHP容器。测试还需要一个数据库和一个队列,即mysql
和redis
容器也需要 - 而nginx
,php-fpm
和php-worker
不需要。我们将通过仅包含必要服务的专用docker compose
配置文件来处理。这在Compose file updates节中更详细地进行了解释。
在我们的本地设置中,我们拥有光泽的主机系统和Docker - 主要是因为我们希望我们的更改立即在Docker中进行反映。
这对于CI 用例不是必需的。实际上,我们希望我们的 ci图像尽可能靠近生产图像 - 这些图像应该“包含所有独立运行”的图像。 IE。 代码库应生活在图像中 - 不在主机系统上。这将在Use the whole codebase as build context节中进行解释。
撰写文件更新
我们不仅将在CI Docker设置和本地Docker设置(=不同的容器)之间存在一些差异,还将在单个服务的配置中存在一些差异。为此,我们将在.docker/docker-compose/
目录中使用以下docker compose
配置文件:
-
docker-compose.local.ci.yml
:- 保留对
local
和ci
有效的配置,试图保留配置文件 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节稍后解释逻辑。简而言之:
docker-compose.local.yml
将ci
与local
进行比较时,对于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"
- 我们不需要本地开发工具的任何设置,例如
xdebug
或strace
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
秘密文件本身被烘烤到图像中,但是解密它们的关键将是
仅在运行时提供和
- 与共享一个
.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
声明了一项检查,该检查以确定该服务的容器是否“健康”。
由于此healthcheck
对local
也“有效”,所以我在组合的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_healthy
的koude183 property,以便docker compose
只有一旦mysql
服务才能启动容器:
application:
depends_on:
mysql:
condition: service_healthy
但是,这将“阻止” make docker-up
,直到mysql
实际启动并运行为止。在我们的情况下,这是不可取的,因为同时我们可以做“其他事情”(即:运行qa
检查,因为它们不需要数据库),从而在每个CI运行中保存几秒钟。 P>
构建目标: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
-
.make/.env
文件包含在主Makefile
中,使ENV
变量可用 到make
-
configure a koude202 variable
将
ENV
作为环境变量传递,即通过
ENV=$(ENV) docker-compose
- 在
docker compose
配置文件中使用ENV
变量来确定build.target
属性。例如。在.docker/docker-compose/docker-compose-php-base.yml
中
php-base:
build:
target: ${ENV?}
- 在服务的
Dockerfile
中,将ENV
定义为构建阶段。例如。在.docker/images/php/base/Dockerfile
FROM base as ci
# ...
因此,要启用新的ci
环境,我们需要修改php-base
和application
image的dockerfiles。
在php-base
图像中构建stage ci
将整个代码库用作构建上下文
如Docker changes节所述,我们想将代码库“烘烤”到php-base
容器的ci
图像中。
因此,我们必须更改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。
最终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-builds是good 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)
-
allocate a koude262 via koude263
在
docker-compose.local.ci.yml
文件中
application:
tty: true
.dockerignore
koude232 file位于存储库的根部,并确保将某些文件放在Docker build context
之外。这将
- 加快构建(因为需要将更少的文件传输到Docker守护程序)
- 保持较小的图像(因为将不相关的文件放在图像之外)
语法与.gitignore
文件非常相似 - 实际上,我发现.gitignore
文件的内容是.dockerignore
文件的子集。这有点意义,因为您通常不希望从存储库中排除的文件最终进入Docker Image (例如,未加密的秘密文件)。其他人也注意到了这一点,请参见例如
- Reddit: Any way to copy .gitignore contents to .dockerignore
- SO: Should .dockerignore typically be a superset of .gitignore?
,但据我所知,目前有(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
文件仍然是.gitignore
d,可以使用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
代码库更改
为加密文件添加测试
我们已经在上一个教程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 address和re-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添加到test
的phpunit
配置中,以创建.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。