puppet 定时执行的陷阱

2022-06-02 16:45:20 浏览数 (2)

背景

一次偶然的机会,我尝试通过 puppet 利用 archive module 从 s3 中下载文件到指定的目录,结果掉坑了。

服务器中 puppet 设置了定时任务自动更新,并监控其运行状态;做法是在 /etc/cron.d 创建了对应的 cronjob 任务。

代码语言:shell复制
$ cd /etc/cron.d
$ cat mypuppetjob 
3 23    * * *   root    /opt/puppetlabs/bin/puppet agent -t >/dev/null; echo $? | tee /tmp/puppet.status

一次用户需求,需要到 s3 下载文件到指定的服务器中。

为此,编写 puppet 代码,使用 archive module,最后手工执行 puppet 更新后,文件下载成功。

验证没问题后,接着尝试全量更新;主要方式是:更新 puppetsever,让所有 client 通过定时任务更新 puppet,达到全量更新的目的。

后续通过监控查看到,大部分服务更新后,/tmp/puppet.status 状态码返回是 6 而非正常的 2 或者 0,问题变得有点迷惑。

问题分析

目前已知情况

  1. puppet 代码配置应该没有问题,因为手工登录任意一个机器,使用 puppet 更新命令 sudo puppet agent -t 可以看到更新成功,而且文件也下载成功
  2. 自动化 puppet 更新有问题,登录有问题的服务器,看到自动化更新的返回码是 6,也有看到是 4 的,文件没有成功被下载。
  3. puppet 状态码代表 puppet 更新只有部分成功。
代码语言:shell复制
$ sudo puppet agent --help

...

'--test' runs once in the foreground with verbose logging, then exits.
It also exits if it can't get a valid catalog. `--test` includes the '--detailed-exitcodes' option by default and exits with one of the following exit codes:

* 0: The run succeeded with no changes or failures; the system was already in the desired state.
* 1: The run failed, or wasn't attempted due to another run already in progress.
* 2: The run succeeded, and some resources were changed.
* 4: The run succeeded, and some resources failed.
* 6: The run succeeded, and included both changes and failures.
...

逐步分析

1. 代码写错了吗?

首先,感觉是 puppet 的代码编写有问题,毕竟整个 s3 相关下载的 class 是参照 archive module 的例子改写的

代码语言:text复制
class abc::role::s3::global_so {
  file { "/opt/abcaccount":
        ensure => directory,
        mode   => "0755",
        owner  => 'tom',
        group  => 'tom',
  }
  abc::profile::s3::download {"lib_hello_1.0.0.tar.gz":
    ensure => present,
    s3_src_path        => "/abc_account/lib_hello_1.0.0.tar.gz",
    dst_path           => "/opt/abcaccount",
    exist_test_path    => "/opt/abcaccount/llib_hello_1.0.0.version",
    user               => 'tom',
    group              => 'tom',
    require            => File['/opt/imoaccount'],
  }
}

这里自定义的 resource 可能无法使用 require 保留字,查了一轮官方文档也没有找到答案,保守起见先把这里改了,改为在 File 中使用 before 声明依赖。

代码语言:text复制
class abc::role::s3::global_so {
  file { "/opt/abcaccount":
        ensure => directory,
        mode   => "0755",
        owner  => 'tom',
        group  => 'tom',
        before => Imo::Profile::S3::Download["lib_hello_1.0.0.tar.gz"],

  }
  abc::profile::s3::download {"lib_hello_1.0.0.tar.gz":
    ensure => present,
    s3_src_path        => "/abc_account/lib_hello_1.0.0.tar.gz",
    dst_path           => "/opt/abcaccount",
    exist_test_path    => "/opt/abcaccount/llib_hello_1.0.0.version",
    user               => 'tom',
    group              => 'tom',
  }
}

通过测试发现,修改前后效果一样,手工执行成功,cron 自动执行失败。

即自动更新失败于代码编写没有关系。

2. cron 执行有问题?

第一感觉:cron 的任务不应该有问题。<u>毕竟,cron 任务已经使用两年多了,没有修改过,而这种固定思维也为后面艰难排错埋下了伏笔。</u>

首先,我们需要确认 cron job 它是在特定时间被执行了,通过查看 /var/log/cron.log 可以看到,定时任务执行没有问题(想想也知道没有问题,因为我们修改的是 puppet 逻辑代码,这个不会影响 cron 的自动执行 :) )

接着,想到 puppet 更新时可以开启 debug 模式,因此对 cron 任务打开 debug,输出到指定文件中:

代码语言:shell复制
$ cd /etc/cron.d
$ cat mypuppetjob 
3 23    * * *   root    /opt/puppetlabs/bin/puppet agent -t --debug >/tmp/puppet-debug.log; echo $? | tee /tmp/puppet.status

查看 debug 信息:

代码语言:txt复制
Debug: Caching connection for https://puppet:8140
Debug: Abc::Profile::S3::Download[lib_hello_1.0.0.tar.gz]: Resource is being skipped, unscheduling all events
Debug: Class[Abc::Role::S3::Global_so]: Resource is being skipped, unscheduling all events
Debug: Stage[main]: Resource is being skipped, unscheduling all events
Debug: Finishing transaction 49174140
Debug: Storing state
Debug: Pruned old state cache entries in 0.00 seconds
Debug: Stored state in 0.16 seconds
Notice: Applied catalog in 31.53 seconds

这里可以看到,Debug: Abc::Profile::S3::Download[lib_hello_1.0.0.tar.gz]: Resource is being skipped, unscheduling all events ,貌似是 download 这个 resource 没有被执行,重新走读代码,对一些可能有问题的点做了修改,发现问题依然存在。

所以,可能问题不在代码上。

由于没有其他办法,只能手工运行 sudo puppet agent -t --debug 命令,看看两者 debug 信息会有什么差异。

代码语言:txt复制
Debug: Caching connection for https://puppet:8140
Debug: Executing: '/usr/local/bin/aws s3 cp s3://abc_backend_lib/abc_account/lib_hello_1.0.0.tar.gz /tmp/lib_hello_1.0.0.tar.gz_20220531-3980-ynce3l --endpoint-url https://abc.com/ --profile abcrs'
Debug: Archive extracting /tmp/lib_hello_1.0.0.tar.gz in /opt/abcaccount: tar xzf /tmp/lib_hello_1.0.0.tar.gz
Debug: Executing with uid=amy gid=amy: 'tar xzf /tmp/lib_hello_1.0.0.tar.gz'
Debug: Cleanup archive /tmp/lib_hello_1.0.0.tar.gz
Notice: /Stage[main]/Abc::Role::S3::Global_so/Abc::Profile::S3::Download[lib_hello_1.0.0.tar.gz]/Archive[lib_hello_1.0.0.tar.gz]/ensure: download archive from s3://abc_backend_lib/abc_account/lib_hello_1.0.0.tar.gz to /tmp/lib_hello_1.0.0.tar.gz and extracted in /opt/abcaccount with cleanup (corrective)
Debug: /Stage[main]/Abc::Role::S3::Global_so/Abc::Profile::S3::Download[lib_hello_1.0.0.tar.gz]/Archive[lib_hello_1.0.0.tar.gz]: The container Abc::Profile::S3::Download[lib_hello_1.0.0.tar.gz] will propagate my refresh event
Debug: Abc::Profile::S3::Download[lib_hello_1.0.0.tar.gz]: The container Class[Abc::Role::S3::Global_so] will propagate my refresh event
Debug: Class[Abc::Role::S3::Global_so]: The container Stage[main] will propagate my refresh event
Debug: Finishing transaction 43366640
Debug: Storing state
Debug: Pruned old state cache entries in 0.00 seconds
Debug: Stored state in 0.21 seconds
Notice: Applied catalog in 41.37 seconds

可以看到,download 相关的 resource 被执行了。

这里的主要差异点在于:cron job 的 debug 信息报了 Debug: Abc::Profile::S3::Download[lib_hello_1.0.0.tar.gz]: Resource is being skipped, unscheduling all events 的错误,而手工在 shell 执行时没有;通过搜索引擎查询相关信息,但没有找到这个错误具体原因详细的解释。

正当我继续苦恼换着搜索引擎查询解决方案时,我突然想到了一个问题。回头仔细看正确执行的代码信息,Debug: Executing: '/usr/local/bin/aws s3 cp s3://abc_backend_lib/abc_account/lib_hello_1.0.0.tar.gz /tmp/lib_hello_1.0.0.tar.gz_20220531-3980-ynce3l --endpoint-url https://abc.com/ --profile abcrs',这里错误原因就是这个 s3 命令没有被执行,会不会是 /usr/local/bin/aws 不存在?不对,这些配置已经提前设置好了,确认过有问题的机器都是有对应项 bin 执行文件的。

bin 文件存在,但执行失败,我再想到的,是可能环境变量出问题了。

我尝试在 cron job 中添加 PATH 环境变量:

代码语言:txt复制
 $ cat mypuppetjob
 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
3 23    * * *   root    /opt/puppetlabs/bin/puppet agent -t --debug >/tmp/puppet-debug.log; echo $? | tee /tmp/puppet.status

调整任务时间使其执行,OK,问题解决了~

原因分析

默认情况下,cron 设置了很多默认变量,例如 SHELL 默认为 /bin/sh,PATH 默认为 /usr/bin:/bin

通过 man 5 crontab,我们可以看到

crontab 有默认的环境变量

Several environment variables are set up automatically by the cron(8) daemon. SHELL is set to /bin/sh, and LOGNAME and HOME are set from the /etc/passwd line of the crontab's owner. PATH is set to "/usr/bin:/bin". HOME, SHELL, and PATH may be overridden by settings in the crontab; LOGNAME is the user that the job is running from, and may not be changed.

若想变更默认的 PATH 环境变量,需要在 crontab file 里面声明

On the Debian GNU/Linux system, cron supports the pam_env module, and loads the environment specified by /etc/environment and /etc/security/pam_env.conf. It also reads locale information from /etc/default/locale. However, the PAM settings do NOT override the settings described above nor any settings in the crontab file itself. Note in particular that if you want a PATH other than "/usr/bin:/bin", you will need to set it in the crontab file.

当在 /etc/cron.d 配置 puppet 定时执行时,实际 puppet 命令也受上述命令的影响;其 fork 出来的子进程若想执行类似 /usr/local/bin/aws 的命令,并且命令写成相对路径的格式 aws <aws_command>,这时候命令执行就会失败,因为在环境变量中无法找到 aws 这个 bin 文件。

测试

获取 /etc/cron.d 下,root 用户 cron job 默认的 PATH 环境变量

代码语言:shell复制
$ cat /etc/cron.d/get_path 
33 23    * * *   root   echo $PATH > /tmp/test_path_1.log 
$ cat /tmp/test_path_1.log 
/usr/bin:/bin

获取当前用户下,crontab 的环境变量 PATH 的信息

代码语言:shell复制
$ crontab -l |grep -v "^#"
42 23    * * *   echo $PATH > /tmp/test_path_2.log
$ cat /tmp/test_path_2.log 
/usr/bin:/bin

测试的环境是 Linux Ubuntu 16.04,结果可以知道,无论 crontab 还是 /etc/cron.d ,其任务都是使用了默认的 PATH 环境变量

参照这里有更详细的解释

解决方案

参照这里可知,linux 的全局环境变量依赖 /etc/environment/etc/profile,查看 /etc/environment 的内容

代码语言:shell复制
$ cat /etc/environment 
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"

environment 的已经罗列了大部分常用的 PATH。因此,如无特殊需要,可以直接使用environment 中的 PATH 作为 crontab file 的默认 PATH

最终修改方案如下:

代码语言:shell复制
$ cat mypuppetjob
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
3 23    * * *   root    /opt/puppetlabs/bin/puppet agent -t >/dev/null; echo $? | tee /tmp/puppet.status

所有 puppet 自动更新任务执行无异常,返回码为 2 。

参考链接

  1. https://unix.stackexchange.com/questions/479522/how-does-cron-set-the-environment-variables-in-etc-cron-d-and-etc-cron-d
  2. https://superuser.com/questions/664169/what-is-the-difference-between-etc-environment-and-etc-profile
  3. https://puppet.com/docs/puppet/6/lang_defined_types.html
  4. https://puppet.com/docs/puppet/7/lang_relationships.html#lang_rel_require
  5. https://groups.google.com/g/puppet-users/c/mEaP_PciA_Y/m/bOWi6qTcGwAJ?pli=1

0 人点赞