背景
一次偶然的机会,我尝试通过 puppet 利用 archive
module 从 s3 中下载文件到指定的目录,结果掉坑了。
服务器中 puppet 设置了定时任务自动更新,并监控其运行状态;做法是在 /etc/cron.d
创建了对应的 cronjob 任务。
$ 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,问题变得有点迷惑。
问题分析
目前已知情况
- puppet 代码配置应该没有问题,因为手工登录任意一个机器,使用 puppet 更新命令
sudo puppet agent -t
可以看到更新成功,而且文件也下载成功 - 自动化 puppet 更新有问题,登录有问题的服务器,看到自动化更新的返回码是 6,也有看到是 4 的,文件没有成功被下载。
- puppet 状态码代表 puppet 更新只有部分成功。
$ 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 的例子改写的
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 声明依赖。
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 信息会有什么差异。
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
环境变量:
$ 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
环境变量
$ 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
的信息
$ 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
的内容
$ 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 。
参考链接
- https://unix.stackexchange.com/questions/479522/how-does-cron-set-the-environment-variables-in-etc-cron-d-and-etc-cron-d
- https://superuser.com/questions/664169/what-is-the-difference-between-etc-environment-and-etc-profile
- https://puppet.com/docs/puppet/6/lang_defined_types.html
- https://puppet.com/docs/puppet/7/lang_relationships.html#lang_rel_require
- https://groups.google.com/g/puppet-users/c/mEaP_PciA_Y/m/bOWi6qTcGwAJ?pli=1