CI/CD

个人理解的 CI/CD 就是持续集成和持续部署, 持续集成就是代码提交后自动构建, 持续部署就是自动部署到测试环境, 测试环境通过后自动部署到生产环境.

为了简化流程, 此项目是直接将构建后的二进制文件部署到服务器, 并执行脚本运行

新建工作流

首先选择要部署的仓库, Actions -> New workflow 新建一个工作流

选择一个合适的模板文件, 这里我们选择 go

他会为这个工作流生成一个 .github/workflows/go.yml 文件, 我们填写里面的配置文件, 在右侧的 Marketplace 我们可以搜索合适的插件

最后我的配置如下:

# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: Go

# 当有 push 或者 pull request 到 main 分支时会触发任务
on:
    push:
        branches: ["main"]
    pull_request:
        branches: ["main"]

jobs:
    build-and-deploy:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v3

            # 选择 go 的版本
            - name: Set up Go
              uses: actions/setup-go@v4
              with:
                  go-version: "1.20"

            # 构建
            - name: Build
              run: |
                  go mod tidy 
                  mkdir tmp
                  go build -o ./cmd/user-api ./app/user/cmd/api/user.go
                  go build -o ./cmd/user-rpc ./app/user/cmd/rpc/user.go
                  tar -zcvf ./cmd.tar.gz ./cmd                  

            # 部署到服务器
            # 这里使用插件 ssh-scp-ssh-pipelines
            # 详细说明见: https://github.com/marketplace/actions/ssh-scp-ssh-pipelines
            - name: Deploy To Server
              uses: cross-the-world/ssh-scp-ssh-pipelines@v1.1.4
              with:
                  host: ${{ secrets.HOST }}
                  port: ${{ secrets.PORT }}
                  user: ${{ secrets.USERNAME }}
                  pass: ${{ secrets.PASSWORD }}
                  first_ssh: |
                      cd /home/guochenxu/gtodolist/go
                      if [ -f "./cmd.tar.gz" ]; then rm ./cmd.tar.gz; fi                      
                  scp: |
                      './cmd.tar.gz' => /home/guochenxu/gtodolist/go/                      
                  last_ssh: |
                      cd /home/guochenxu/gtodolist/go
                      ./start.sh                      

上面配置文件中的 ${{ secrets.HOST }} 是仓库的环境变量, start.sh 是服务器上的启动脚本

配置仓库的环境变量

在该仓库里面选择 Settings -> Secret and variables -> Actions -> New repository secret 进行配置, 第一步的配置文件中的名字要和这里的环境变量的名字相同

编写服务器上的启动脚本

这个可以根据自己的需求自己编写

# 杀死进程
function kill_process(){
	for i do
		pid=`ps -ef | grep $i | grep -v grep | awk '{print $2}'`
		if [ -n "$pid" ]; then
		       kill -9 $pid;
		fi
 	done
}

kill_process user-rpc user-api

# 解压文件
if [ -f "./cmd.tar.gz" ]; then
	if [ -d "./cmd" ]; then
		rm -r ./cmd
	fi
	tar -zxvf ./cmd.tar.gz
	#chmod -R 755 ./cmd
	rm ./cmd.tar.gz
fi

# 启动服务
function start_service(){
	cmd=$1
	etc=$2
	logs=$3
	for file in $(ls $cmd)
	do
		bin=$cmd/$file
		config=$etc/${file}.yaml
		log=$logs/${file}.log
		nohup $bin -f $config > $log &
	done
}

start_service ./cmd ./etc ./logs

# 退出
exit 0

查看效果

到此就部署完毕, 现在只要提交代码就会自动构建, 然后将二进制文件部署到服务器上, 并运行启动脚本

服务部署成功并且正常运行

更新

因为编译后的大文件在往服务器传输的时候实在是太慢了 (平均花费半个多小时, 并且期间很容易出现网络问题), 所以改成直接上传源代码, 将编译的工作放到服务器上, 效率提高了将近 90%

事实上, 应当是把项目打包成镜像, 然后在服务器上拉去镜像运行的, 但是菜鸡不会 docker 😭

工作流更新:

换了两个新的插件, 一个传输文件 (scp 或者 rsync), 一个 ssh 登录执行命令

# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: Go

on:
    push:
        branches: ["main"]
    pull_request:
        branches: ["main"]

jobs:
    build-and-deploy:
        # 设置容器环境
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v3

            # # 设置 GO 版本
            # - name: Set up Go
            #   uses: actions/setup-go@v4
            #   with:
            #       go-version: "1.20"

            #    # 编译
            #    - name: Build
            #      run: |
            #        go mod tidy
            #        mkdir tmp
            #        go build -o ./cmd/user-api ./app/user/cmd/api/user.go
            #        go build -o ./cmd/user-rpc ./app/user/cmd/rpc/user.go
            #        go build -o ./cmd/task-api ./app/task/cmd/api/task.go
            #        go build -o ./cmd/task-rpc ./app/task/cmd/rpc/task.go
            #        tar -zcvf ./cmd.tar.gz ./cmd

            # 两种传输方式, 我的 scp 不知道为什么一直出问题, 所以就用了 rsync

            #    # 使用 scp 把文件传输到服务器上
            #    - name: Transfer to server
            #      uses: appleboy/scp-action@v0.1.4
            #      with:
            #        host: ${{ secrets.HOST }}
            #        username: ${{ secrets.USERNAME }}
            #        password: ${{ secrets.PASSWORD }}
            #        port: ${{ secrets.PORT }}
            #        source: "./cmd.tar.gz"
            #        target: /home/guochenxu/gtodolist/go/

            # 使用 rsync 把文件传输到服务器上
            - name: Transfer to server
              uses: up9cloud/action-rsync@master
              env:
                  HOST: ${{ secrets.HOST }}
                  USER: ${{ secrets.USERNAME }}
                  PORT: ${{ secrets.PORT }}
                  PASSWORD: ${{ secrets.PASSWORD }}

                  TARGET: /home/guochenxu/gtodolist/go/gtodolist
                  SOURCE: ./
                  ARGS: -avuz --progress --delete --exclude=./.git/

                  PRE_SCRIPT: |
                      echo start at:
                      date -u                      
                  POST_SCRIPT: "echo done at: && date -u"

            # 远程执行命令
            - name: Run Service On Server
              uses: fifsky/ssh-action@master
              with:
                  host: ${{ secrets.HOST }}
                  port: ${{ secrets.PORT }}
                  user: ${{ secrets.USERNAME }}
                  pass: ${{ secrets.PASSWORD }}
                  command: |
                      cd /home/guochenxu/gtodolist/go
                      ./start.sh                      

脚本更新:

之前的脚本挺多有问题的地方的, 这里做了两个更新:

  1. 把 rpc 服务和 api 服务分开放, 并且前后间隔一段时间启动, 防止出现找不到服务的问题;
  2. nohup 的错误输出重定向到标准输出的引用, 即 2>&1 (也可以选择直接丢弃输出, 2>/dev/null, 将输出重定向到黑洞文件), 防止远程 ssh 无法正常关闭.
# 杀死进程
function kill_process(){
        for i do
                pid=`ps -ef | grep $i | grep -v grep | awk '{print $2}'`
                if [ -n "$pid" ]; then
                       kill -9 $pid;
                fi
        done
}

kill_process user-api user-rpc task-api task-rpc

# 解压文件
#if [ -f "./cmd.tar.gz" ]; then
#       if [ -d "./cmd" ]; then
#               rm -r ./cmd
#       fi
#       tar -zxvf ./cmd.tar.gz
#       #chmod -R 755 ./cmd
#       rm ./cmd.tar.gz
#fi

# 编译
if [ -d "./cmd" ];then
        rm -rf ./cmd
fi
mkdir ./cmd
mkdir ./cmd/api
mkdir ./cmd/rpc

if [ -d "./gtodolist" ];then
        cd ./gtodolist
        go mod tidy

        go build -o ../cmd/api/user-api ./app/user/cmd/api/user.go
        go build -o ../cmd/rpc/user-rpc ./app/user/cmd/rpc/user.go
        go build -o ../cmd/api/task-api ./app/task/cmd/api/task.go
        go build -o ../cmd/rpc/task-rpc ./app/task/cmd/rpc/task.go

        cd ..
        rm -rf ./gtodolist
else
        echo "编译失败, 没有找到代码目录"
        exit 1
fi
echo "===== 编译完成 ====="

# 启动服务
function start_service(){
        rpc=$1
        api=$2
        etc=$3
        logs=$4

        # 先启动 rpc 服务
        for file in $(ls $rpc)
        do
                bin=$rpc/$file
                config=$etc/${file}.yaml
                log=$logs/${file}.log
                nohup $bin -f $config > $log 2>&1 &
        done

        # 睡眠 7s, 要不然 api 服务启动但是 rpc 没有启动就会报错
        sleep 7s

        # 后启动 api
        for file in $(ls $api)
        do
                bin=$api/$file
                config=$etc/${file}.yaml
                log=$logs/${file}.log
                nohup $bin -f $config > $log 2>&1 &
        done
}

start_service ./cmd/rpc ./cmd/api ./etc ./logs
echo "===== 启动完成 ====="

# 退出
echo "===== 部署成功 ! ====="
exit 0

更新后的效果

nginx 配置

最后补充一下 nginx 怎么配置后端端口转发的配置怎么写。

为了方便文件管理,我在/etc/nginx/conf.d目录下创建了todolist.conf文件,该文件就是 gtodolist 项目的 nginx 配置文件,文件内容如下:

# todolist
server {
	listen       80;
    listen       [::]:80;
    server_name todolist.chenxutalk.top;

	#把http的域名请求转成https
	return 301 https://$server_name$request_uri;
	# rewrite ^(.*) https://$server_name$1 permanent; #自动从http跳转到https

    root /home/guochenxu/gtodolist/web;
	include /etc/nginx/default.d/*.conf;

    # 前端配置
    location / {
    	root /home/guochenxu/gtodolist/web;
	    try_files $uri $uri/ /index.html; #解决页面刷新404问题
    	index  index.html index.htm;
    }

    # 后端匹配路径前端分别进行端口转发
	location /api/v1/user {
		proxy_pass  http://localhost:22301;
	}
	location /api/v1/task {
        proxy_pass  http://localhost:22302;
    }

    error_page 404 /404.html;
    location = /40x.html {
		root   /home/guochenxu/gtodolist/web;
	}
	error_page 500 502 503 504 /50x.html;
    location = /50x.html {
    }
}

server {
    listen 443 ssl;
    server_name todolist.chenxutalk.top;
    root /home/guochenxu/gtodolist/web;

    ssl_certificate /etc/nginx/cert.pem;
    ssl_certificate_key /etc/nginx/key.pem;

    ssl_session_timeout 5m;
    #请按照以下套件配置,配置加密套件,写法遵循 openssl 标准。
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    error_page 404 /404.html;
    location / {
        root /home/guochenxu/gtodolist/web;
        try_files $uri $uri/ /index.html; #解决页面刷新404问题
        index index.html index.htm;
    }

    location /api/v1/user {
        proxy_pass  http://localhost:22301;
    }
    location /api/v1/task {
        proxy_pass  http://localhost:22302;
    }

    include /etc/nginx/default.d/*.conf;
}