DevOps 之 Laravel 基于分支的多环境部署
前言⌗
在我们之前的 DevOps Workflow 中,只有一套测试环境,也就是说我们如果要测试功能需要将 feature/* 分支的代码合并到 develop 分支,然后 Gitlab CI 执行部署。
因为我们的主要语言是 PHP 框架是 Laravel,处于成本考虑,没有采用打包容器的方式进行项目的部署。而是采用 Deployer SSH 到远端服务器进行部署。
如果没有并行的需求开发,这样也没什么问题。但经常会出现一些优先级较高的需求,需要穿插在当前已经在开发的功能中。之前我们只能等 develop 分支上的代码发布了以后才能将 feature/* 的代码合并到 develop 分支上进行测试。
而我们想实现的是,在测试服务器上基于分支部署多个测试环境,然后通过请求头区分要访问的测试分支。
例如开发者 A 创建了 feature/sso
, 那么当该分支被推送到 Gitlab 时,则执行部署流程,测试人员可以通过在请求头中附加 X-Branch: feature/sso
来访问指定分支的测试环境。
为了实现这一功能,我调研了诸多方案,将在下面罗列并分析优劣,以供参考!
Gitlab CI 改造⌗
将原来的仅监听 develop 分支的变化并执行 CI 流程,改为监听除 master 分支以外的所有流程。
stages:
- review
- staging
- production
review:deploy:
image: betterde/deployer:7.0.2
only:
refs:
- branches
tags:
- backend
stage: review
except:
refs:
- master
- develop
before_script:
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" >> ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
script:
- deployer deploy --root "$PROJECT_BASE_ROOT" --branch "$CI_COMMIT_REF_NAME" --env_file "$STAGING_ENV" --gitlab_domain "$GITLAB_DOMAIN" --gitlab_personal_token "$PERSONAL_TOKEN" $VERBOSE staging
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://laravel.example.com
on_stop: review:stop
resource_group: review
review:stop:
image: betterde/deployer:7.0.2
when: manual
only:
refs:
- branches
tags:
- backend
stage: review
except:
refs:
- master
- develop
before_script:
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" >> ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
script:
- deployer review:stop --root "$PROJECT_BASE_ROOT" --branch "$CI_COMMIT_REF_NAME" --gitlab_domain "$GITLAB_DOMAIN" --gitlab_personal_token "$PERSONAL_TOKEN" $VERBOSE staging
variables:
GIT_STRATEGY: none
environment:
name: review/$CI_COMMIT_REF_NAME
action: stop
resource_group: review
staging:deploy:
image: betterde/deployer:7.0.2
only:
refs:
- develop
stage: staging
tags:
- backend
before_script:
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" >> ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
script:
- deployer deploy --root "$PROJECT_BASE_ROOT" --branch "$CI_COMMIT_REF_NAME" --env_file "$STAGING_ENV" --gitlab_domain "$GITLAB_DOMAIN" --gitlab_personal_token "$PERSONAL_TOKEN" $VERBOSE staging
environment:
name: staging
url: https://laravel.example.com
resource_group: staging
production:deploy:
image: betterde/deployer:7.0.2
only:
refs:
- tags
when: manual
tags:
- backend
stage: production
before_script:
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" >> ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
script:
- deployer deploy --root "$PROJECT_BASE_ROOT" --tag "$CI_COMMIT_TAG" --gitlab_domain "$GITLAB_DOMAIN" --gitlab_personal_token "$PERSONAL_TOKEN" $VERBOSE stage=production
environment:
name: production
url: $PRODUCTION_URL
resource_group: production
production:rollback:
image: betterde/deployer:7.0.2
only:
refs:
- tags
when: manual
stage: production
tags:
- backend
before_script:
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" >> ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
script:
- deployer rollback --root "$PROJECT_BASE_ROOT" $VERBOSE production
resource_group: production
上面的配置中,核心在于 review:deploy
和 review:stop
两个配置,以及 script
的脚本。当除了 master
和 develop
分支以外的分支触发了流程时,会通过 deployer 部署 Review 环境。
注意 review:stop
配置中的如下内容:
variables:
GIT_STRATEGY: none
这本配置可以实现触发流程时不会拉取分支代码,因为当我们在合并 feature 分支时,如果勾选删除分支的话,没有这个配置会导致后面执行 review:stop
失败,因为分支在合并时已经被删除了!
最后一条指令中用到了 –gitlab_domain 和 –gitlab_personal_token 是我们自定义的参数,因为在我们的项目中使用到了基于 Gitlab 的私有 Composer 扩展包,所以需要提供 Gitlab 身份认证信息,来拉取扩展包。
下面就分析一下 deploy.php 的配置,如何实现基于分支的多环境部署。
Deployer 配置⌗
<?php
namespace Deployer;
use Symfony\Component\Console\Input\InputOption;
require 'contrib/rsync.php';
require 'recipe/laravel.php';
option('gitlab_domain', null, InputOption::VALUE_OPTIONAL, 'Define Gitlab domain name');
option('gitlab_personal_token', null, InputOption::VALUE_OPTIONAL, 'Define gitlab personal token');
option('env', null, InputOption::VALUE_OPTIONAL, 'Define env file path');
option('root', null, InputOption::VALUE_REQUIRED, 'Define root path of the project');
// Keep releases version count
set('keep_releases', 1);
// Set permission of project dircotry
set('writable_chmod_mode', '0777');
// Set change permission mode
set('writable_mode', 'chmod');
// Not use sudo to set permission
set('writable_use_sudo', false);
// Set project repository
set('repository', '[email protected]:backend/laravel.git');
set('deploy_path', function () {
$branch = input()->getOption('branch');
if ($branch) {
return input()->getOption('root') . '/' . $branch;
}
return input()->getOption('root');
});
// Install private composer package from private gitlab server
set('gitlab_domain', function () {
if (input()->hasOption('gitlab_domain') && !empty(input()->getOption('gitlab_domain'))) {
return input()->getOption('gitlab_domain');
}
return 'gitlab.shenjumiaosuan.com';
});
// Install private composer package from private gitlab server
set('gitlab_personal_token', function () {
if (input()->hasOption('gitlab_personal_token') && !empty(input()->getOption('gitlab_personal_token'))) {
return input()->getOption('gitlab_personal_token');
}
return '';
});
set('bin/php', 'docker exec fpm bash -c "cd {{release_path}} && php"');
// Staging host configuration
host('staging')
->set('port', 22)
->set('hostname', 'xxx.xxx.xxx.xxx')
->set('labels', ['stage' => 'staging'])
->set('http_user', 'www-data')
->set('remote_user', 'root')
->set('keep_releases', 1)
->set('identity_file', '~/.ssh/id_rsa')
->set('forward_agent', true)
->set('ssh_multiplexing', true)
->set('update_code_strategy', 'clone')
->set('ssh_arguments', ['-o UserKnownHostsFile=/dev/null', '-o StrictHostKeyChecking=no'])
->set('composer_install', 'COMPOSER=composer.dev.json composer install --verbose --prefer-dist --no-progress --no-interaction --optimize-autoloader --no-suggest --no-dev');
// Production host configuration
host('production-01')
->set('port', 22)
->set('hostname', 'xxx.xxx.xxx.xxx')
->set('labels', ['stage' => 'production'])
->set('http_user', 'www-data')
->set('remote_user', 'root')
->set('keep_releases', 3)
->set('identity_file', '~/.ssh/id_rsa')
->set('forward_agent', true)
->set('ssh_multiplexing', true)
->set('git_ssh_command', 'ssh') // When the server openssh version less-than 7.4 this config is required
->set('update_code_strategy', 'clone')
->set('ssh_arguments', ['-o UserKnownHostsFile=/dev/null', '-o StrictHostKeyChecking=no'])
->set('composer_install', 'composer install --verbose --prefer-dist --no-progress --no-interaction --optimize-autoloader --no-suggest --no-dev');
host('production-02')
->set('port', 22)
->set('hostname', 'xxx.xxx.xxx.xxx')
->set('labels', ['stage' => 'production'])
->set('http_user', 'www-data')
->set('remote_user', 'root')
->set('keep_releases', 3)
->set('identity_file', '~/.ssh/id_rsa')
->set('forward_agent', true)
->set('ssh_multiplexing', true)
->set('git_ssh_command', 'ssh') // When the server openssh version less-than 7.4 this config is required
->set('update_code_strategy', 'clone')
->set('ssh_arguments', ['-o UserKnownHostsFile=/dev/null', '-o StrictHostKeyChecking=no'])
->set('composer_install', 'composer install --verbose --prefer-dist --no-progress --no-interaction --optimize-autoloader --no-suggest --no-dev');
desc('Composer auth');
task('composer:auth', function () {
run('docker exec fpm bash -c "cd {{release_path}} && composer config gitlab-token.{{gitlab_domain}} {{gitlab_personal_token}}"');
});
//option('tag', null, InputOption::VALUE_REQUIRED, 'Tag to deploy');
desc('Installing vendors');
task('deploy:vendors', function () {
run('docker exec fpm bash -c "cd {{release_path}} && {{composer_install}}"');
});
// Upload .env file to remote server
desc('Upload environment file');
task('env:upload', function () {
$path = input()->getOption('env');
if ($path) {
upload(input()->getOption('env'), '{{deploy_path}}/shared/.env');
}
});
desc('Stop review environment');
task('review:stop', function () {
run('rm -rf {{deploy_path}}');
});
before('deploy:vendors', 'composer:auth');
after('deploy:vendors', 'env:upload');
after('deploy:failed', 'deploy:unlock');
CI 在执行上述的 deploy 配置后,会在远端服务器上的 $PROJECT_BASE_ROOT
目录下基于分支创建同名的目录。例如分支为 feature/sso,那么最终的部署路径就是 $PROJECT_BASE_ROOT/feature/sso
。
到这一步就完成了服务的部署,接下来还需要通过对 Nginx 的配置实现根据 X-Branch 头访问到对应的项目分支。
Nginx 配置⌗
server {
listen 80;
server_name laravel.example.com;
access_log off;
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name laravel.example.com;
# 设置默认分支,当用户没有在请求头中附加 X-Branch 请求头时,则默认访问 develop 分支
set $branch develop;
if ($http_x_branch) {
# 如果检测到 X-Branch 请求头时,则将值作为分支
set $branch $http_x_branch;
}
# 将项目分支作为 root 的路径参数
set $base /usr/wwwroot/laravel/$branch/current;
root $base/public;
index index.php;
access_log off;
include components/ssl.conf;
include components/security.conf;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# additional config
include components/general.conf;
# handle .php
location ~ \.php$ {
root $base/public;
fastcgi_pass fpm:9000;
include components/fastcgi.conf;
}
}
通过上述配置,就可以实现 Nginx 将分支路由到指定分支的源码了,且新增分支无需修改 Nginx 的配置。这种好处是基于请求头进行路由,配置简单,不用改变当前环境。
还有一种是使用 Nginx Unit + PHP-CLI 来实现 PHP 服务,因为 Unit 本身支持通过 API 进行配置热更新,所以灵活性更高。
但是对于我们来说有两个问题:
- 将现有的 PHP-FPM 架构变更为 Unit
- 我们的项目是前后端混编,所以访问是比较依赖域名和端口的(代码中有较多硬编码,所以不敢轻易动)
基于上述考虑,我们最终还是选择了 Nginx + PHP-FPM 的架构。
请求⌗
为了实现分支的切换,需要再浏览器中安装 Header Editor 插件,用来修改请求头。
经过这一番操作下来,就可以在测试服实现基于分支的多环境部署和测试了。
对于纯 API 项目而言,我们后面可能会考虑选择 Unit 这种方式或者直接使用 Go 开发,并使用 API Gateway 基于服务发现,自动路由多分支测试环境。
I hope this is helpful, Happy hacking…