Skip to content

使用Gitlab Pipelines执行定时任务

2023年6月16日

背景

在Linux服务器下执行定时任务是一个非常常见的需求,我们可能需要定时备份数据库、定时执行程序脚本、定时清理日志等等。在旧版本Linux下,我们通常会使用crontab来执行定时任务,而新版本的Linux系统中则更推荐用systemd的timer来执行定时任务。

但是systemd的配置麻烦又罗嗦,需要先定义一个service,再定义一个timer来调用service,而且timer的配置文件中还需要指定OnCalendar来指定定时任务的执行时间,这个时间格式又不是很好记。

此外即使定义好了,管理起来也并不容易,更不要说排查问题了。

而如果机器上使用Docker等容器环境,定时任务就更麻烦了:为了应用的可移植性,希望尽可能将定时任务放到容器中运行,但容器又不能直接以宿主机的身份执行各种脚本(例如重启其它容器,可以通过一些手段实现,但更麻烦)。

此外,定制一个跑定时任务的容器也不是一件容易的事,首先需要有对应环境的基础镜像,然后基于这个镜像安装好crond之类的工具,再将定时任务的脚本刷到crond中,最后将这个容器部署到机器上。这个容器还必须有网络权限等。

总之,定时任务的配置和管理是一个非常麻烦的事情,在容器环境下尤其如此。

Gitlab Pipelines

Gitlab提供了CI/CD功能,可以自己定义Pipelines,然后在代码推送、MR合并等事件触发时执行Pipelines。利用这个功能可以很好地执行CI/CD任务。

最近发现Gitlab还有一个Pipeline Schedules的功能,可以在指定的时间点执行Pipelines。利用这个功能可以很好地解决定时任务的问题。

使用方法

首先在项目的CI/CD设置中新建一个Pipelines:

screenshot-1

可以看到,这个界面能可视化地管理Pipelines的执行时间,非常方便。

添加好之后,就可以在管理界面看到刚添加的定时任务了:

screenshot-2

但这里马上就会面临一个新问题:我们在Gitlab CI中定义的是CI/CD的任务,但定时任务却希望是另外的脚本,怎么办呢?事实上如果此时点击手工运行的话,会发现Gitlab CI会执行我们定义的CI/CD任务,而不是我们希望的定时任务。

这个问题的解决方法是,我们可以在CI/CD的任务中单独定义需要执行的定时任务,并通过条件来指定只有在定时任务触发时才执行。例如:

yaml
ci-task:
    script:
        - echo "This is a CI task"
    rules:
        - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "master"

cron-job:
    script:
        - echo "This is a cron job"
    rules:
        - if: $CI_PIPELINE_SOURCE == "schedule"
ci-task:
    script:
        - echo "This is a CI task"
    rules:
        - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "master"

cron-job:
    script:
        - echo "This is a cron job"
    rules:
        - if: $CI_PIPELINE_SOURCE == "schedule"

上述配置中,我们定义了两个任务,一个是CI任务,一个是定时任务。其中CI任务只有在pushmaster分支时才会执行,而定时任务只有在定时任务触发时才会执行。这样就可以很好地解决定时任务的执行的问题了。

详细说明可参考官方文档:https://docs.gitlab.com/ee/ci/jobs/job_control.html#run-jobs-for-scheduled-pipelines

接下来的问题是:不是说好的要解决Linux系统下的定时任务问题吗?这里的任务是在Gitlab CI中执行的,怎么执行自己服务器上的任务呢?

这就需要我们从Gitlab CI环境使用SSH连接到自己的服务器上,然后执行对应的任务脚本来实现了。

SSH连接

首先需要在Gitlab CI/CD的设置中将SSH Key作为变量添加进去,这样才能在Gitlab CI环境中获取到对应的Key,并使用SSH连接到自己的服务器上。

screenshot-3

截图中SSH_CONFIG的内容如下:

Host *
    KexAlgorithms +diffie-hellman-group1-sha1
    StrictHostKeyChecking no
Host *
    KexAlgorithms +diffie-hellman-group1-sha1
    StrictHostKeyChecking no

这里是为了解决SSH客户端与服务端使用的加密方案不同造成无法连接的情况,可视情况修改或者添加对应的配置。

对应的CI任务脚本如下:

yaml
script:
    - apk update&&apk add openssh
    - mkdir ~/.ssh
    - echo "$SSH_CONFIG">~/.ssh/config
    - echo "$SSH_KEY_PRIVATE">~/.ssh/id_rsa
    - echo "$SSH_KEY_PUBLIC">~/.ssh/id_rsa.pub
    - chmod 400 ~/.ssh/id_rsa
    - ssh root@100.1.2.3 "sh /data/scripts/db-bak.sh&&docker restart example"
script:
    - apk update&&apk add openssh
    - mkdir ~/.ssh
    - echo "$SSH_CONFIG">~/.ssh/config
    - echo "$SSH_KEY_PRIVATE">~/.ssh/id_rsa
    - echo "$SSH_KEY_PUBLIC">~/.ssh/id_rsa.pub
    - chmod 400 ~/.ssh/id_rsa
    - ssh root@100.1.2.3 "sh /data/scripts/db-bak.sh&&docker restart example"

首先安装SSH客户端,然后将SSH Key写入到对应的文件中,最后连接到服务器执行对应的脚本。

小结和可改进点

总的来说,这个方法可以让我们不用花太多心思去配置“定时”这件事情,只需要在Gitlab中添加一个定时任务,然后通过SSH连接到服务器上执行对应的脚本即可。

但这个方法依然不是特别“干净”,因为最终还是需要留一个脚本在服务器上,并不完全符合容器可移植的要求。但这个问题也有一些可改进的方向:

  1. 将脚本放到容器中,通过SSH连接到服务器后执行docker相关命令进行调用
  2. 将需要定时任务的任务通过HTTP接口等形式暴露出来,然后在Gitlab CI中通过curl等工具调用
  3. 将定时任务的脚本放到Gitlab CI中,通过SSH连接到服务器后将脚本写入到服务器上,然后执行