上一期我整了个服务器和域名,然后请教了我们公司的运维大佬,简单的实现了前后端自动化部署,只需要把代码push上去,然后就能自动编译和部署。这里没有用到k8s或Jenkins这种重量级的CI/CD工具,只使用了github actions和docker-compose,适合个人小项目使用。
GitHub Actions是GitHub提供的一种自动化工作流程(workflow)管理工具。它可以根据特定的事件触发,执行各种操作和任务,例如编译代码、运行测试、部署应用等。
使用GitHub Actions,开发者可以定义一个或多个工作流程,每个工作流程由一系列步骤(steps)组成。每个步骤可以包含命令行脚本、调用API、运行测试等任务。这些步骤可以在不同的操作系统环境下执行,如Linux、macOS和Windows。
GitHub Actions提供了一系列预定义的事件(events),如提交代码、创建分支、打标签等,当这些事件发生时,可以触发相应的工作流程执行。同时,开发者也可以通过手动方式触发工作流程的执行。
GitHub Actions还与其他工具和服务集成,例如Docker、AWS、Azure等,可以通过这些集成实现更复杂的自动化流程。
总之,GitHub Actions是一种灵活强大的自动化工作流程工具,使开发者能够轻松地设置和管理与代码相关的自动化任务,并提高开发效率和质量。
总结:我们使用github actions就不用自己手动在本地build代码和build镜像了,只需要往main分支推送代码就行了。
如果想把前端打成Docker镜像,需要写在项目里编写Dockerfile。这里我就不介绍什么是docker了,如果对docker不了解的,可以从网上找相关资料学习了解一下docker,现在大部分公司都会选择把前后端部署到镜像里,方便维护和迁移。
Dockerfile
文件这里就不多做解释了,大家看代码中的注视吧,很简单的。
# Dockerfile
# 因为我们项目使用的是pnpm安装依赖,所以找了个支持pnpm的基础镜像,如果你们使用npm,这里可以替换成node镜像
# FROM nginx:alpine
FROM gplane/pnpm:8.4.0 as builder
# 设置工作目录
WORKDIR /data/web
# 这里有个细节,为了更好的使用node_modules缓存,我们先把这两个文件拷贝到镜像中,镜像会检测发现这两个文件没有变化,就不会去重新安装依赖了。
COPY pnpm-lock.yaml .
COPY package.json .
# 安装依赖,如果上面两个文件没有改动,就不会重现安装依赖。
RUN pnpm install
# 把当前仓库代码拷贝到镜像中
COPY . .
# 运行build命令,可以替换成 npm run build
RUN pnpm run build
# 上面我们把代码编译完成后,会在镜像里生成dist文件夹。
# 下面我们把打包出来的静态资源放到nginx中部署
# 使用nginx做基础镜像
FROM nginx:alpine as nginx
# 设置时区
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone
# 设置工作目录
WORKDIR /data/web
# 在nginx镜像中创建 /app/www文件夹
RUN mkdir -p /app/www
# 把上一步编译出来dist文件夹拷贝到刚才新建的/app/www文件夹中
COPY --from=builder /data/web/dist /app/www
# 暴露 80端口和443端口,因为我们服务监听的端口就是80,443端口是为了支持https。
EXPOSE 80
EXPOSE 443
# 如果镜像中有nginx配置,先给删了
RUN rm -rf /etc/nginx/conf.d/default.conf
# 把项目里的./nginx/config.sh shell脚本复制到ngxin镜像/root文件夹下
COPY ./nginx/config.sh /root
# 给刚复制进去的shell脚本设置权限,让镜像启动的时候可以正常运行这个shell脚本。
RUN chmod +x /root/config.sh
# 镜像启动的时候运行config.sh脚本
CMD ["/root/config.sh"]
/nginx/config.sh
文件上一步我们说了,把项目里的/nginx/config.sh复制到nginx中,所以我们需要在项目里创建这个文件,这个文件其实就是nginx配置。
#! /bin/sh -e
cat >> /etc/nginx/conf.d/default.conf <<EOF
server {
listen 80;
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
#gzip_http_version 1.0;
gzip_comp_level 2;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
gzip_vary off;
gzip_disable "MSIE [1-6].";
proxy_read_timeout 600;
location / {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
if (\$request_filename ~* .*\.(?:htm|html)$)
{
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
}
root /app/www/;
index index.html;
client_max_body_size 500m;
}
location /api {
proxy_pass $SERVER_URL;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
}
location /file/ {
proxy_pass $FILE_URL;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
}
}
EOF
echo "starting web server"
nginx -g 'daemon off;'
很基本的nginx配置,这里有两个地方需要说一下。
$SERVER_URL
和 $FILE_URL
)我们需要使用环境变量,然后只能在shell脚本中才能使用环境变量,这两个环境变量在后面docker-compose里面去配置。if (\$request_filename ~* .*\.(?:htm|html)$)
这一段配置是设置html文件不缓存,这样我们每次发布后,别人就不用清缓存后才能访问新的内容。html文件很小,所以只把html设置不缓存完全没问题,至于其他js,每次打包过后如果js文件有变化,他对应的hash也会变,如果文件内容不变,使用老的缓存也没问题。上面两步做完,如果本地装了docker,已经可以在本地编译镜像了,在项目根目录下执行下面命令。
docker build -t test:v1.0.0 .
test是镜像名,v1.0.0是版本号,后面那个点别忘记了。build成功后,我们可以使用docker desktop运行镜像也可以使用docker run -p 8000:80 test:v1.0.0
命令去执行,现在执行肯定会报错的,因为有两个环境变量还没配呢。
基于上面我们可以本地编译镜像,然后把镜像推到镜像仓库,然后在服务器上拉镜像,最后运行镜像。这样虽然可以实现,但是每一步都是手动的,太不优雅了。这时候github actions登场了,用了它我们就可以解放自己的双手了,写完一个功能,代码一推,就可以去干别的事了。
在项目根目录下,创建.github/workflows/docker-publish.yml
文件,文件名可以随便起。
name: Docker
on:
push:
branches: ['main']
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Docker buildx
uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
- name: SSH Command
uses: D3rHase/ssh-command-action@v0.2.1
with:
HOST: ${{ secrets.SERVER_IP }}
PORT: 22
USER: root
PRIVATE_SSH_KEY: ${{ secrets.SERVER_KEY }}
COMMAND: cd /root && ./run.sh
这些都是固定写法,兄弟们可以拿去直接用,有两个地方需要注意一下。
branches: ['main']
这个表示代码往main分支push后触发自动编译,可以改成别的,比如打版本,merge代码等。我们可以再github这个标签里面看到执行进度,点进去可以看到日志。
编译完镜像后,github这里会显示你的镜像。
点进去可以看到完整的镜像名称,你可以去拉这个镜像在本地或服务器上部署,后面我们在部署的时候会用到这个镜像名称。
FROM gplane/pnpm:8.4.0 as builder
WORKDIR /app
# 先复制pnpm-lock.yaml和package.json文件,上文说过为了实现缓存
COPY pnpm-lock.yaml .
COPY package.json .
# 使用pnpm安装依赖
RUN pnpm install
COPY . .
# 编译代码
RUN pnpm run build
# 我们使用pm2启动后端服务,至于为什么,可以看下神光大佬的这篇文章。https://juejin.cn/post/7229595897813712957
FROM keymetrics/pm2:16-jessie
WORKDIR /app
COPY --from=builder /app/package.json ./
COPY --from=builder /app/pnpm-lock.yaml ./
COPY --from=builder /app/node_modules ./node_modules
ENV TZ="Asia/Shanghai"
RUN npm install pnpm -g
# node项目不像前端项目,编译代码的时候,会把node_modules里的文件也编译到dist中,后端还需要保留node_modules,不过我们可以把开发环境用到的包给移除掉,减少包的体积,pnpm install加--prod可以把开发环境中用到的包给移除掉,也就是安装在devDependencies中的依赖。
RUN pnpm install --prod
# 把需要的代码复制到镜像中
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/bootstrap.js ./
COPY --from=builder /app/script ./script
COPY --from=builder /app/src/config ./src/config
COPY --from=builder /app/tsconfig.json ./
COPY --from=builder /app/src/migration ./src/migration
EXPOSE 7001
# 启动项目
CMD ["npm", "run", "start"]
在项目根目录下,创建.github/workflows/docker-publish.yml
文件,文件名可以随便起。文件内容和前端一模一样,直接把前端的拿过来就行了。
name: Docker
on:
push:
branches: ['main']
tags: ['v*.*.*']
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Docker buildx
uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
- name: SSH Command
uses: D3rHase/ssh-command-action@v0.2.1
with:
HOST: ${{ secrets.SERVER_IP }}
PORT: 22 # optional, default is 22
# user of the server
USER: root
# private ssh key registered on the server
PRIVATE_SSH_KEY: ${{ secrets.SERVER_KEY }}
# command to be executed
COMMAND: cd /root && ./run.sh
我们在本地开发的时候,一般都是开启表结构自动同步,改了实体会自动把改的地方同步到数据库。但是线上环境最好不要开这玩意,很有可能会把线上的数据搞没,因为有时候你改字段名称,他自动同步的时候,会把原来的字段删掉,然后新建一个字段,这样那一列的数据就没了,线上开自动同步风险很大,建议不要开。
那我们不开自动同步,部署到其他环境的时候,怎么把当前的表结构同步过去呢,typeorm提供解决了方案,使用migration,本地改了实体结构后,执行一个命令,会自动生成一个迁移文件,然后我们发布到线上的时候,执行一下这个文件,就能把变更的内容同步到线上数据库了。这一块midway文档中也有介绍。
node ./node_modules/@midwayjs/typeorm/cli.js migration:generate -d ./src/config/config.default.ts ./src/migration/migration
node ./node_modules/@midwayjs/typeorm/cli.js migration:run -d ./src/config/typeorm.prod.ts
node ./script/init-database && npm run migration:run && NODE_ENV=production pm2-runtime start ./bootstrap.js --name midway_app -i 4
// src/config/typeorm.prod.ts
import { env } from 'process';
export default {
typeorm: {
dataSource: {
default: {
type: 'mysql',
host: env.DB_HOST,
port: env.DB_PORT || 3306,
username: env.DB_USERNAME,
password: env.DB_PASSWORD,
database: env.DB_NAME || 'fluxy-admin',
synchronize: false, // 关闭自动同步
logging: false,
// 扫描entity文件夹
entities: ['**/entity/*{.ts,.js}'],
timezone: '+00:00',
// 迁移存在的文件路径
migrations: ['**/migration/*.ts'],
// 迁移存在的文件夹路径
cli: {
migrationsDir: 'migration',
},
},
},
},
};
npm run migration:generate
命令,会生成下图中迁移文件,up是升级执行的方法,down是回滚执行的方法。可以看到up方法中生成了一些建表的语句,后面插入管理员账号是我手动加的,typeorm migration迁移只迁移表结构,数据不会帮你迁移,所以要自己写,数据迁移这一块很复杂,后面会单独写一篇文章介绍种子数据迁移。npm run migration:generate:dev
命令,会生成一个创建完整表结构的迁移,这时候如果要初始化数据,也可以写在里面。如果线上数据库已经建好了,后面改了实体,只需要提交代码的时候执行npm run migration
命令生成迁移就行了,开发过程中不要多次执行这个命令,因为每次执行这个命令都会对比远程数据库和本地数据库的差异,生成变更,多次执行会生成重复的变更,比如加了一个表,每次执行都会生成创建表的更变,然后线上执行的时候就会报错,某某表已经存在,因为同一个表会建多次。node ./script/init-database
命令,这个是用来检查线上有没有我们用的数据库,如果没有的话,需要自动建一个,不然启动后端服务会因为无法创建数据库连接而报错。说明一下这里创建数据库不是创建数据库服务,而是连上数据库服务后创建我们用到的数据库,对应sql是CREATE DATABASE 数据库名称
。这个脚本还有一个作用,我们在使用docker-compose启动服务的时候,虽然设置了后端服务依赖数据库服务,数据库服务启动后,后端服务才会启动,但是只是让数据库服务先启动,而不是数据库服务启动完成后,再启动后端服务。所以如果数据库服务还没启动完成,这时候我们去连数据库就会失败,我在这个脚本中,写了个轮询检查数据库有没有启动完成,启动完成再去走后面的流程。// script/init-database.js
const mysql = require('mysql2');
let count = 0;
function connect() {
const host = process.env.DB_HOST;
const user = process.env.DB_USERNAME;
const password = process.env.DB_PASSWORD;
const database = process.env.DB_NAME || 'fluxy-admin';
const connection = mysql.createConnection({
host,
user,
password,
});
connection.connect(error => {
if (error) {
console.log(`host: ${host}`);
console.log(`user: ${user}`);
console.log(`password: ${password}`);
console.log('数据库连接失败,正在重新连接');
console.log(error);
setTimeout(() => {
if (count >= 60) {
console.log('数据库连接失败,请检查数据库服务是否正常启动。');
return;
}
connect();
count += 1;
}, 1000);
return;
}
connection.query(
`SELECT * FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '${database}'`,
(err, result) => {
if (err) {
console.log(err);
return;
}
if (result.length === 0) {
console.log('检测到数据库不存在,正在为你创建数据库...');
connection.query(`CREATE DATABASE \`${database}\``, () => {
console.log('数据库创建成功');
process.exit();
});
} else {
process.exit();
}
}
);
});
connection.end();
}
connect();
到此我们前后端自动编译代码和生成镜像完成了,下面我们写如何自动在服务器上部署我们的服务。
前面我们已经把前后端代码编译成了镜像,这时候我们可以在服务器上拉取镜像,然后启动镜像。前端还好,没有啥依赖,后端用到了数据库、redis、minio文件服务器,需要我们一个一个把这些服务部署好,后端才能启动。后面如果想迁移到一个新环境,还是需要一个一个部署,太麻烦了。有没有更好的方式呢,docker compose可以实现这个,我们可以把这些服务放到一个配置文件中统一管理,如果想迁移服务,直接在对应的服务器上运行一个命令就行了,是不是很方便,下面和大家说一下怎么使用。
首先需要在服务器上安装docker和docker-compose。如何安装docker我就不说了,网上太多教程了,安装docker-compose可以用下面的命令。
curl -Lo /usr/bin/docker-compose https://ghproxy.com/https://github.com/docker/compose/releases/download/v2.11.2/docker-compose-linux-x86_64
chmod +x /usr/bin/docker-compose
在服务器上/root文件夹下,创建docker-compose.yml文件
version: '3.7'
services:
web:
image: ghcr.dockerproxy.com/dbfu/fluxy-admin-web:main
container_name: web
restart: unless-stopped
environment:
SERVER_URL: http://server:7001
FILE_URL: http://file:9000/
ports:
- 8080:80
networks:
- general
server:
image: ghcr.dockerproxy.com/dbfu/fluxy-admin-server:main
container_name: server
restart: unless-stopped
environment:
DB_HOST: db
DB_PASSWORD: 123654
DB_NAME: fluxy-admin
REDIS_HOST: redis
REDIS_PASSWORD: 123654
MINIO_HOST: file
MINIO_SECRET_KEY: 123654
MINIO_PORT: 9000
networks:
- general
depends_on:
- db
- redis
- file
db:
image: mysql:latest
container_name: db
restart: unless-stopped
volumes:
- db:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: 123654
networks:
general:
ipv4_address: 172.18.0.200
redis:
image: redis:latest
container_name: redis
command: --requirepass 123654
restart: unless-stopped
volumes:
- redis:/data
networks:
general
ipv4_address: 172.18.0.201
file:
image: minio/minio:latest
container_name: file
command: server --console-address :9001 /data
restart: unless-stopped
environment:
MINIO_ROOT_USER: root
MINIO_ROOT_PASSWORD: 123654
volumes:
- file:/data
- /etc/localtime:/etc/localtime
networks:
- general
volumes:
db:
file:
redis:
上面我们编排了5个服务,web、server、db、redis、file。
ghcr.dockerproxy.com
,拉取镜像的速度就会快很多。db:/var/lib/mysql
,正常来说前面的db应该是服务器的某个路径,但是如果我们写死了一个,服务器上没有这个目录就会报错了。所以在最外面定义了一个卷有db、file、redis,这样docker-compose启动的时候,会自动创建卷对应的目录,不用担心因为目录不存在而报错了。然后我们在当前目录下执行docker-compose pull && docker-compose up --remove-orphans
命令就会自动部署服务了。如果以后我们想迁移,直接把这个docker-compose.yml
文件复制过去,然后执行当前命令,就能实现一键迁移了。每次都写那么长的命令有点麻烦,为了方便我们写个shell脚本。在当前目录下创建run.sh
文件,把上面命令复制到run.sh中,我们只需要执行sh run.sh
就行了。
如果我们改了代码,重新编译了镜像,每次都需要在服务器上手动执行sh run.sh
命令有点麻烦。不过github actions支持远程调用服务器上的命令,只需要配置服务器公网ip和密钥就行了。
上面github actions yml文件中我们已经提了这个。
在github上配置服务器ip和密钥,注意是密钥,不是密码,每个云服务器都可以生成密钥的。
如果想用你的域名访问web服务,并且想支持https,正常操作需要先申请证书,然后在nginx中配置你刚申请的证书,这些操作虽然不难,但是挺麻烦的。我请教了我们公司的运维大佬,他给我提供了个快捷方法,超级简单,这里分享给大家。
我们在docker-compose中添加两个服务,并在下面添加几个卷。这两个服务会自动申请证书,并配置nginx。
version: '3.7'
services:
nginx-proxy:
image: nginxproxy/nginx-proxy
container_name: nginx-proxy
ports:
- "80:80"
- "443:443"
volumes:
- conf:/etc/nginx/conf.d
- vhost:/etc/nginx/vhost.d
- html:/usr/share/nginx/html
- certs:/etc/nginx/certs:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
networks:
- general
acme-companion:
image: nginxproxy/acme-companion
container_name: nginx-proxy-acme
environment:
- DEFAULT_EMAIL=XXXXXX@163.com
volumes_from:
- nginx-proxy
volumes:
- certs:/etc/nginx/certs:rw
- acme:/etc/acme.sh
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- general
...
volumes:
conf:
vhost:
html:
certs:
acme:
db:
redis:
file:
如果你想用这个的话,需要把那个XXXXXX@163.com
邮箱改成自己的就行。改造一下web服务,加几个环境变量。
把上面的域名换成自己的就行了。
github action执行流程的时候,成功后不会发送通知,只有失败才会,需要我们手动开启一下。
这个默认是勾上的,去掉就行了。
好了到这里一套完整的自动化部署方案就完成了,兄弟们可以把这一套用在自己的个人项目中,还是挺方便的。
阅读量:2012
点赞量:0
收藏量:0