konakona https://blog.crazyphper.com 不拘泥于平台,展现智慧的技术;与你一同分享产品道路上的坎坷与甘露。 Sat, 24 Feb 2024 09:31:26 +0000 zh-CN hourly 1 使用 Homebrew 升级 php8.2 后出现 "Library not loaded"错误 https://blog.crazyphper.com/2023/12/21/%e4%bd%bf%e7%94%a8-homebrew-%e5%8d%87%e7%ba%a7-php8-2-%e5%90%8e%e5%87%ba%e7%8e%b0-library-not-loaded%e9%94%99%e8%af%af/ Wed, 20 Dec 2023 16:52:58 +0000 https://blog.crazyphper.com/?p=6795 很久没有写php了,像往常一样先升个级。

brew install php@8.2

安装后打算修改~/.zshrc让命令行使用8.2版本,发现无法运行vim,提示如下报错信息:

➜ ~ vim
dyld[28879]: Library not loaded: /opt/homebrew/opt/libsodium/lib/libsodium.23.dylib
  Referenced from: <BA441469-20D2-3A07-ABCE-790A3BE0C6E1> /opt/homebrew/Cellar/vim/9.0.1650/bin/vim
  Reason: tried: '/opt/homebrew/opt/libsodium/lib/libsodium.23.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/opt/homebrew/opt/libsodium/lib/libsodium.23.dylib' (no such file), '/opt/homebrew/opt/libsodium/lib/libsodium.23.dylib' (no such file), '/usr/local/lib/libsodium.23.dylib' (no such file), '/usr/lib/libsodium.23.dylib' (no such file, not in dyld cache), '/opt/homebrew/Cellar/libsodium/1.0.19/lib/libsodium.23.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/opt/homebrew/Cellar/libsodium/1.0.19/lib/libsodium.23.dylib' (no such file), '/opt/homebrew/Cellar/libsodium/1.0.19/lib/libsodium.23.dylib' (no such file), '/usr/local/lib/libsodium.23.dylib' (no such file), '/usr/lib/libsodium.23.dylib' (no such file, not in dyld cache)
[1]    28879 abort      vim

第一个想法是重新安装libsodium,然而这个方向是不对的。直接用软连搞定:

ln -s  /opt/homebrew/Cellar/libsodium/1.0.19/lib/libsodium.26.dylib /opt/homebrew/opt/libsodium/lib/libsodium.23.dylib

接着运行vim会遇到新的报错:

➜ lib (stable) ✔ vim
dyld[35527]: Library not loaded: /opt/homebrew/opt/perl/lib/perl5/5.36/darwin-thread-multi-2level/CORE/libperl.dylib
  Referenced from: <BA441469-20D2-3A07-ABCE-790A3BE0C6E1> /opt/homebrew/Cellar/vim/9.0.1650/bin/vim
  Reason: tried: '/opt/homebrew/opt/perl/lib/perl5/5.36/darwin-thread-multi-2level/CORE/libperl.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/opt/homebrew/opt/perl/lib/perl5/5.36/darwin-thread-multi-2level/CORE/libperl.dylib' (no such file), '/opt/homebrew/opt/perl/lib/perl5/5.36/darwin-thread-multi-2level/CORE/libperl.dylib' (no such file), '/usr/local/lib/libperl.dylib' (no such file), '/usr/lib/libperl.dylib' (no such file, not in dyld cache)
[1]    35527 abort      vim

显然是vim使用了错误的perl版本导致的问题,这是因为在brew upgrade的过程中,perl进行了升级,而vim仍在使用指定的perl版本,这里vim官方issue中有讨论到。通过查阅issue的讨论内容,找到一个最有效的做法就是重装vim,问题解决。

brew reinstall vim 

Every time perlpython or ruby are upgraded or revision bumped, vim needs to be brew reinstall‘d. It’s a known issue. This would be a known issue even against the system Perl if Apple ever gets round to implementing another major Perl update.

看来锅是苹果的呀……
]]>
2023年成就回顾:丰盛岁月的点点滴滴 https://blog.crazyphper.com/2023/12/12/2023-achievement-awards/ Tue, 12 Dec 2023 07:57:12 +0000 https://blog.crazyphper.com/?p=6732 嗨,大家好!回首过去的一年,我的生活充实而多姿多彩。让我们一起来分享我在2023年所取得的成就吧!?

人际关系与家庭

在这一年里,我迎来了一位古灵精怪的小女友,与她共度时光,感受到了爱的温暖。

在岁月交汇中,34岁的我仿佛漫步在Ice IsLand的Reynisfjara,突然间,她闯入我的生命,如一缕清新的微风。16岁的她,带着未来的憧憬,给我的生活注入了一抹青春的色彩。我们的爱情,如同一首年轻的歌曲,在岁月的交响中轻轻荡漾,激荡着青涩而美好的旋律。在她眼中,我看到了昔日年少时的梦想,在她身边,我感受到了岁月流转中的无尽可能。这段爱情,是岁月与青春的交汇,是时光交错下的美丽契合。

同时,我们家庭成员也增添了两位新伙伴:一只黑猫,名叫”边角料”,以及一只牡丹鹦鹉,得名”鸡”。这一切让我的家庭更加温馨。

学业成就

成功完成了法律事务专业的大专学历,为未来的职业生涯奠定了坚实的基础。

旅行与探索

做为数字游民在工作之余,踏上了旅行的征程,遍历了昆明、西双版纳、广州、上海、深圳、恭城、南宁、海口、文昌、永州,甚至跨足国际,走访了马来西亚的首府-吉隆坡、亚庇和美丽的仙本那小镇进行出海潜水每一次的旅途都是一段难忘的经历。

在仙本那跟当地渔民买海鲜,无国籍小孩伸手问我们要钱(当地“特色”)
这很蔡徐坤
旅行轨迹

文化体验

生活中充满了多姿多彩的文化体验,首次现场亲临了古典音乐会、脱口秀、演唱会(杨千嬅在广州)以及马克西姆钢琴演奏会,为我的生活增色不少。

马克西姆巡回钢琴演奏会
杨千嬅在广州演唱会

兴趣拓展

为自己的兴趣爱好再添利器:一台富士X-Pro 3相机和两个X口镜头(所摄照片合集详见Google相册)。同时,开始了业余养养鱼,为生活注入了更多的乐趣。

健康与冒险

今年特种兵一般的行程让“年迈的我”被迫提早面对一个女性issue——乳腺肿物。面对健康问题我进行了乳腺结节旋切微创术(因为结节较大,局麻效果有限,有几刀切的太深,挺疼的),成功度过手术的考验。同时,迎来了新的冒险,取得了摩托车驾照,开始了巡航车的骑行之旅

QJ Motor 闪150 巡航太子,12月遇上搞活动 1,2000¥ 落地

未来展望

展望明年,我的计划包括参加学为贵雅思封闭班学习、深造摄影技能以及探索新的学习方向。期待未来更多的挑战和成就!

]]>
统计Nginx 1小时内访问最多的URL https://blog.crazyphper.com/2023/09/06/%e7%bb%9f%e8%ae%a1nginx-1%e5%b0%8f%e6%97%b6%e5%86%85%e8%ae%bf%e9%97%ae%e6%9c%80%e5%a4%9a%e7%9a%84url/ Wed, 06 Sep 2023 07:57:28 +0000 https://blog.crazyphper.com/?p=6695 要找出最近1小时内访问最多的站点和完整网址,你可以使用Nginx的日志文件来分析访问情况。首先,你需要确保Nginx已经配置为记录访问日志而不是access_log off

通常,Nginx的访问日志位于/var/log/nginx/access.log或类似的位置,具体路径取决于你的Nginx配置。

接下来,你可以使用一些命令行工具和技巧来分析这些日志文件。以下是一种可能的方法:

# 使用grep和awk命令来提取最近1小时内的日志条目
now=$(date +"%Y-%m-%d %H:%M:%S")
one_hour_ago=$(date -d '1 hour ago' +"%Y-%m-%d %H:%M:%S")
grep -E "$one_hour_ago|$now" /var/log/nginx/access.log | awk '{print $7}' | sort | uniq -c | sort -nr

这个命令会在日志文件中搜索最近1小时内的日志条目,提取出访问的URL(在Nginx日志中通常位于第7列),然后对其进行计数并按访问次数进行降序排序,返回一个包含站点和访问次数的列表。

这些命令假定Nginx的访问日志采用默认格式。如果你的Nginx配置使用了不同的日志格式,就需要相应地调整命令中的字段位置了。


通常,一台Nginx服务器上往往有多个站点( Virtual Host ),我们创建一个analyze_logs.sh脚本来存放要执行的命令以分析这些日志中访问最多的站点和网址:

#!/bin/bash

# 定义日志文件路径
LOG_DIR="/data/wwwlogs/"
# 定义时间间隔(秒)
TIME_INTERVAL=3600

# 获取当前时间戳
CURRENT_TIME=$(date +%s)

# 遍历日志目录下的所有日志文件
for LOG_FILE in "${LOG_DIR}"*_nginx.log; do
  # 获取文件名(站点名称)
  SITE_NAME=$(basename "${LOG_FILE}" _nginx.log)

  # 计算文件的修改时间
  FILE_MOD_TIME=$(stat -c %Y "${LOG_FILE}")

  # 计算时间间隔
  TIME_DIFF=$((CURRENT_TIME - FILE_MOD_TIME))

  # 检查是否在指定时间间隔内
  if [ ${TIME_DIFF} -le ${TIME_INTERVAL} ]; then
    # 输出站点名称
    echo "站点: ${SITE_NAME}"

    # 使用 awk 分析日志文件并统计访问次数最多的前 50 个 URL
    awk '{print $7}' "${LOG_FILE}" | grep -vE '(\.gif|\.jpg|\.jpeg|\.png|\.ico|\.css|\.js)' | sort | uniq -c | sort -nr | head -n 50 | while read COUNT URL; do
      echo "访问次数: ${COUNT}"
      echo "网址: ${URL}"
      echo "======================"
    done

    echo
  fi
done
# 赋执行权限
chmod +x analyze_logs.sh
# 执行脚本
./analyze_logs.sh

这个命令会遍历/data/wwwlogs/目录下的每个日志文件,找到每个站点里访问次数最多的URL,并显示站点名称、最多访问的URL以及访问次数。


接下来我们还可以增加时间条件以便查询出一小时内的访问信息,并且输出成csv格式的纯文本信息方便导入excel查看:

#!/bin/bash

# 获取Nginx日志文件列表
log_files=$(ls /data/wwwlogs/*_nginx.log)

# 获取当前时间的1小时前的时间戳
start_time=$(date -d '1 hour ago' '+%d/%b/%Y:%H:%M:%S')

# 添加标题行
echo "日志名,路径,1小时内访问总数"

# 循环处理日志文件
for log_file in $log_files; do
    # 获取日志文件名
    log_name=$(basename $log_file)

    # 使用awk处理日志文件并输出结果
    awk -v log_name="$log_name" -v start_time="$start_time" '$4 > start_time {urls[$7]++} END {for (url in urls) {print log_name "," url "," urls[url]}}' $log_file
done | sort -t',' -k2 -n -r | head -n 50

这个命令会遍历/data/wwwlogs/目录下的每个日志文件,找到每个站点最近1小时内访问次数最多的URL,并按照标题行约定返回csv格式的纯文本。

大功告成,感谢阅读与支持! ?

]]>
64位键盘按下cmd+ESC时变成cmd+` https://blog.crazyphper.com/2022/05/26/%e8%ae%a9%e4%bd%a0%e7%9a%8464%e4%bd%8d%e9%94%ae%e7%9b%98%e6%8c%89%e4%b8%8bcmdesc%e6%97%b6%e5%8f%98%e6%88%90cmd%ef%bc%88%e5%88%87%e6%8d%a2%e5%bd%93%e5%89%8d%e5%ba%94%e7%94%a8%ef%bc%89/ Thu, 26 May 2022 03:29:24 +0000 https://blog.crazyphper.com/?p=6669 MacOS下切换当前应用窗口的快捷键是cmd+`,但在64位小键盘里,` 键需要靠组合键fn+ECS才能敲打出来,而应用切换需要同时按下cmd+fn+ECS

我想向使用全键盘那样通过cmd+ECS(全键盘`的和ECS当然是分开的啦)就能实现应用内切换窗口。在MacOS下有一款开源的软件可以实现组合键映射,今天试了一下相当给力。

前往官网下载dmg安装包 https://karabiner-elements.pqrs.org ,安装好后运行Karabiner-Elements APP。

看一下使用复杂的规则教程,通过导入小脚本来实现组合映射键,官网团队收集了很多不同键盘的不同样式需求的小脚本方便大家使用。小脚本长这样:

Keychron K6 Escape Key/ `/~ modifications
- Shift + Esc to ~
- Control + Esc to `
- Command + Esc to Command + Tilda

访问这里,找到keychron K6 escape key /` /~,点击import。会唤起Karabiner-Elements APP,剩下的1-2个步骤就按照上方的教程说明的那样,点击一下Enable 按钮就好了,立刻就生效。

还有很多不同的脚本,根据你的键盘寻找合适的脚本来达到目的吧!Have fun!

]]>
使用Lego v4.0.1 和NGINX 实现openSSL https://blog.crazyphper.com/2020/11/07/%e4%bd%bf%e7%94%a8lego-v4-0-1-%e5%92%8cnginx-%e5%ae%9e%e7%8e%b0openssl/ Sat, 07 Nov 2020 10:14:59 +0000 http://blog.crazyphper.com/?p=6545 本文基于ACME v2规则,使用 lego 的CLI模式 chanllenge HTTP-01。除了使用lego,还有更多的ACME客户端供选择噢。

为了在您的网站上启用HTTPS,您需要从证书颁发机构(CA)获取证书。Let’s Encrypt 是一个证书颁发机构(CA)。要从 Let’s Encrypt 获取您网站域名的证书,您必须证明您对域名的实际控制权。您可以在您的 Web 主机上运行使用 ACME 协议的软件来获取 Let’s Encrypt 证书。

https://letsencrypt.org/zh-cn/getting-started/

docker安装Lego

docker run goacme/lego -h

如果不想用Docker方式运行lego,官方手册还提及到其他安装方式

Nginx配置

server {
    listen 80;
    server_name example.com; 

    location /.well-known/acme-challenge {  # lego ACME文件验证服务转发
        proxy_pass http://127.0.0.1:81;
        proxy_set_header Host $host;
    }
}

给需要申请证书的域名(或子域名)server块 中添加/.well-known/acme-challenge规则,NGINX会将该路径的访问请求转发给lego。

订购证书

创建用于存储证书的路径,这里我用/var/www/letsencrypt

mkdir -p /var/www/letsencrypt
# 申请证书
docker run \
-v /var/www/letsencrypt:/.lego/certificates/ \
-p 81:81 \
goacme/lego \
--email="mail@gmail.com" \
--domains="api.example.com" \
--domains="example.com" \
--http --http.port :81 \
--accept-tos run

当执行这条lego命令时,我们指定81端口为生成ACME文件的服务器,在NGINX的配置中实现了路径访问的服务转发。由此,当Let’s Encrypt服务器回应我们的请求后,会访问例如http://example.com/.well-known/acme-challenge/Rd9xe0xPPocVFcUgp0vH5IIU8KGC3R_DK2BOMBHZGlI这样的路径来验证网站的服务器是否真实且拥有实际的控制权。

执行结果

参数说明

  • –email 站长的Email,可以不存在。email被视作ACME账户。
  • –accept-tos 同意TOS规则,在执行run命令时必须同意(携带)。
  • –http 使用HTTP-01 challenge。
  • –http.port 创建ACME文件服务器,并listen 81端口的请求。
  • –domains 可以指定一到多个域名同时申请证书。实际上这个地方有bug(lego v4.0.1),一条命令虽然响应了多个域名的申请,但是最终只会生成一个域名的证书(见上方的“执行结果”一图)。如果需要生成多个域名的证书,需要执行多次此命令。

速率限制

要知道Let’s Encrypt是有速率限制的,如果你正在调试lego或者其他ACME的客户端,请使用测试环境而不是生产环境的 API。

Let’s Encrypt 对证书颁发进行速率限制以确保尽可能多的人能合理使用我们的服务。我们相信这些速率限制在大多数情况下足以满足用户的需求。同时续期证书几乎不受速率限制的影响,所以大型组织可以逐步增加他们可以发布的证书数量,而无需Let’s Encrypt的干预。

我们要注意的限制有这些:

  • 每个注册域名每周最多可申请50份证书
  • 每个账户每小时每域名有最多验证失败 5 次的限制
  • 每个IP地址每3小时最多可以创建10个账户
  • 每个账户最多可以有300个待验证授权

更新Nginx配置

证书生成后会出现在/var/www/letsencrypt/certificates/下,根据申请时的–domains命名。接下来在Nginx配置中增加443监听:

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate /var/www/letsencrypt/certificates/example.com.crt;
    ssl_certificate_key /var/www/letsencrypt/certificates/example.com.key;
    ssl_protocols TLSv1 TLSv1.2;
}

执行nginx-T检查下配置书写是否正确,最后别忘了让nginx重新加载配置:

service nginx reload

续约证书

Let’s Encrypt证书有效期为90天(3个月),之后需要使用renew命令申请续约。这个命令所需要带的参数跟lego run一样。

docker run \
-v /var/www/letsencrypt:/.lego/certificates/ \
-p 81:81 \
goacme/lego \
--email="mail@example.com" \
--domains="example.com" \
--http --http.port :81 
renew --days 90 

renew后的证书文件会变,nginx需要重新加载,执行srevice nginx reload

我们可以将这个任务交给cron,让它每隔3个月(也就是每4个月执行一次)执行一次。

创建一个.sh文件(我存放在 /home/acme.sh),保存如下内容:

<span class="token shebang important">#!/bin/bash</span>
<span class="token shebang important">source</span> /etc/profile/usr/bin/docker run -v /var/www/letsencrypt:/.lego/certificates/ -p 81:81 goacme/lego --email<span class="token operator">="admin@example.com"</span> --domains<span class="token operator">="example.com"</span> --http --http.port :81 renew --days 90 <span class="token operator">></span> /home/crontab_logs/acme.log
service nginx reload

执行crontab -e编辑当前用户的计划任务。

0 0 1 */4 * /bin/bash /home/acme.sh

如果加了sudo那么计划任务挂在root用户下,建议统一由一个用户管理计划任务。

]]>
使用jetbrains调试docker中的PHP https://blog.crazyphper.com/2020/09/12/%e4%bd%bf%e7%94%a8idea-phpstorm%e5%9c%a8php-docker%e5%ae%b9%e5%99%a8%e4%b8%ad%e8%bf%9b%e8%a1%8cxdebug%e8%b0%83%e8%af%95/ Sat, 12 Sep 2020 07:42:46 +0000 http://blog.crazyphper.com/?p=6474 前言

最近开发中需要使用到多个PHP版本,固采用容器化的方式运行PHP环境来开发PHP。为了能够进行断点单步调试,需要让IntelliJ IDEA / PhpStorm访问到PHP容器中运行的Xdebug服务。而这个过程有一些坑,为了避免再次爬坑,决定总结下。

Dockerfile文件修改

首先,我们需要在PHP容器中安装并激活xdebug。在镜像FROM php:7.4-fpm-alpine中使用pecl安装并启用之后,将配置一些xdebug的属性存放入php.ini中,这一部分的Dockerfile内容参考:

# Install xdebug extensions
RUN pecl install xdebug

# Enable xdebug extensions
RUN docker-php-ext-enable  xdebug

#xdebug configure
RUN echo 'xdebug.remote_port=9000' >> /usr/local/etc/php/php.ini
RUN echo 'xdebug.remote_enable=1' >> /usr/local/etc/php/php.ini

这里使用的是9000端口是xdebug和PhpStorm默认使用的端口号。

不使用xdebug.remote_connect_back=1

如若启用此参数,xdebug会根据请求容器的源$_SERVER['REMOTE_ADDR']来返回数据,而这个IP地址段是随机的,不可穿透的,xdebug.log如下:

[17] Log opened at 2020-09-12 06:35:10
[17] I: Checking remote connect back address.
[17] I: Checking header 'HTTP_X_FORWARDED_FOR'.
[17] I: Checking header 'REMOTE_ADDR'.
[17] I: Remote address found, connecting to 172.29.0.1:9000.
[17] W: Creating socket for '172.29.0.1:9000', poll success, but error: Operation in progress (29).
[17] E: Could not connect to client. :-(
[17] Log closed at 2020-09-12 06:35:10
对比前次log中记录的访问ip可以发现,每次从宿主机访问容器内的服务,IP都不同

docker-compose.yml 文件

通过使用环境变量XDEBUG_CONFIGxdebug.remote_host设置宿主机的IP地址实现传递数据。

macOS可以使用docker.for.mac.host.internal在容器内解析宿主机IP(好像是1.18提供的特性)。如果是linux主机可以使用docker.host.internal

DNS name docker.for.mac.host.internal should be used instead of docker.for.mac.localhost (still valid) for host resolution from containers, since since there is an RFC banning the use of subdomains of localhost. See https://tools.ietf.org/html/draft-west-let-localhost-be-localhost-06.

尽量不要用docker.for.mac.localhost,过时了
version: "3.8"

services:
  web:
    build: .
    ports:
      - "8088:8088"
    container_name: php
    environment:
      XDEBUG_CONFIG: "remote_host=docker.for.mac.host.internal  remote_port=9000 idekey=PHPSTORM remote_log=/var/www/xdebug.log"
    volumes:
      - "../src:/var/www"

由于我使用的后端程序需要暴露端口为8088,切勿完全照抄,可按照自己的实际情况做出设定。
建议设置remote_log将日志输出在程序目录下,方便随时查看。

启动容器

#编译Dockerfile获得镜像
docker-compose build
#运行容器
docker-compose up -d
#运行程序
docker-compose exec web php artisan serve --port 8088 --host 0.0.0.0

打开浏览器访问http://localhost:8088,此时WEB服务已经running。

到这一步,容器方面的操作已经结束,接下来说说PhpStorm的操作。

PhpStorm配置

我使用的PhpStorm版本为2020.1.1

创建服务

cmd+`打开「Preferences」窗口,找到PHP->Servers,创建Servers。

  • Name:随意就好。
  • Host:我使用的Docker network drive是默认的brige,因此这里我直接使用localhost可以访问到。
  • Port:输入容器中映射给宿主机使用的端口,参见docker-compose.yml中的ports,我设置为8088。如果你设置的是8000:80,那么宿主机访问容器服务的端口就是8000,此处设置为8000
  • path mappings:在File/Directory列中,选择宿主机本地程序所在的位置,在Absolute path on the server列中,输入容器中程序的位置。

创建PHP调试类型的运行配置

在菜单栏点击Run->Edit Configurations...,创建一个PHP remote Debug类型的调试配置,该配置通过remote xdebug服务器连接到PHP容器。

开始愉快的调试吧~

]]>
Gitlab Runner+Helm实现PHP程序自动化构建与部署最佳实践 https://blog.crazyphper.com/2020/04/24/gitlab-runnerhelm%e5%ae%9e%e7%8e%b0php%e7%a8%8b%e5%ba%8f%e5%9c%a8k8s%e7%9a%84%e8%87%aa%e5%8a%a8%e5%8c%96%e6%9e%84%e5%bb%ba%e4%b8%8e%e9%83%a8%e7%bd%b2/ Fri, 24 Apr 2020 05:04:01 +0000 http://blog.crazyphper.com/?p=6268 为了让研发团队快速持续迭代PHP项目,采用Dockerfile(Nginx+PHP7.2+supervisor)+Helm部署的方式实现CICD。

软件情况说明:

  • Harbor企业级镜像中心:使用docker-compose部署,版本 1.10.0
  • GitLab:使用docker部署,版本 12.5.2
  • Gitlab-Runner:使用docker部署,版本 latest
  • Helm:kubernetes包管理工具,版本 3.0.0-rc2

重点讲一些场景要点和实现过程,Harbor(安装说明)和Gitlab安装方式此处省略。


注意事项

由于PHP的依赖项较多,Kubernetes的DNS(kube-dns / CoreDNS)服务并不稳定,在Pipeline中会引起严重的超时导致构建失败,Gitlab-Runner建议使用Docker部署在服务器上,而非使用Helm部署Gitlab-Runner到集群中。

在Helm部署的Runner中不断访问阿里云镜像,有40%的失败概率

由于Helm3 不再需要Tiller,因此Gitlab UI 安装Helm Tiller会失败(因为是Helm2的功能),致使Gitlab-Runner没有访问集群的权限,本文采用的是将kubeconfig放在config.toml[[runners]]中映射到环境变量里。


Runner配置

可以将gitlab-ci.yml常用的一些环境变量设置在config.toml[[runners]]下的environment里,不用每次创建项目时都跑去Setting->里设置variables。比如docker login要用到的Harbor镜像地址和账号密码就可以添加后省事了。

[[runners]]
environment = ["DOCKER_TLS_CERTDIR=","HUB_DEV_ADDR=hub.***.com","HUB_DEV_PASSWORD=***","HUB_DEV_USERNAME=***","DOCKER_DRIVER=overlay2","GIT_DEPTH=10","KUBECONFIG_C="***"]

DOCKER_TLS_CERTDIR=空代表不使用TLS;
DOCKER_DRIVER是overlay2与服务器设置要一致;
GIT_DEPTH=10代表浅克隆;
KUBECONFIG_C的值是base64编码后的kubeconfig,使用下边这条命令:

cat ~/.kube/config | base64

再次强调是[[runners]]而非[runners.docker],很多搜索结果都说是放[runners.docker]是错误的。

WARRNING already in use by container

有时你可能会遇到already in use by containerWARRNING级别错误,常发生在有多个stage时,错误内容如下:

ERROR: Preparation failed: Error response from daemon: Conflict. The container name "/runner-465639a0-project-44-concurrent-3-build" is already in use by container "109a38a9e6ef215f4518992c652e04572e5977b5bfbef72d038a0cf1bb662946". You have to remove (or rename) that container to be able to reuse that name.

这个问题困扰了一部分Gitlab用户,我在这个issue里发现了大量的讨论,持续近2年的官方团队跟进,定义为P2级Bug(对产品影响比较大,如果发布给用户将会产生麻烦),在3个星期前刚刚修复,需要在[runners.docker]中指定helper_image

[[runners]]
  name = "my runner"
  ...
  [runners.docker]
    helper_image = "gitlab/gitlab-runner-helper:x86_64-1b659122"

文件结构介绍

在PHP程序Git目录下需要准备一些配置文件。

> $ tree  -L 1 -f                                                                                                                                                                    
 
.
├── ./Charts
├── ./Config
├── ./Dockerfile
├── ./src
├── ./.gitlab-ci.yml
└── ./README.md

3 directories, 3 files

/Charts用于存放Helm部署所需配置文件
/Config用于存放Dockerfile创建的容器内的配置文件
/src用于存放需要部署的程序

完整的样例已上传Github

Helm Charts

需要为验收环境和生产环境准备不通的数据库信息、CDN等等,charts的配置方式每个人的习惯和用法都不一样,所以只谈下与环境变量有关的配置好了。

Deployment中,需要配置PHP程序需要的env

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  template:
    spec:
 containers:
      - name: frontend
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        imagePullPolicy: {{ .Values.image.pullPolicy }}
        env:
        - name: APP_DEFAULT_TIMEZONE
          value: "Asia/Shanghai"
        - name: APP_DEBUG
          value: {{ .Values.backend.debug | quote }}
        - name: APPLICATION_ENV
          value: {{ .Values.backend.active }}
        - name: DATABASE_URL
          value: {{ .Values.backend.datasource.url }}
        - name: DATABASE_HOSTNAME
          value: {{ .Values.backend.datasource.host }}
        - name: DATABASE_DATABASE
          value: {{ .Values.backend.datasource.database }}
        - name: DATABASE_USERNAME
          value: {{ .Values.backend.datasource.username }}
        - name: DATABASE_PASSWORD
          value: {{ .Values.backend.datasource.password }}

gitlab-ci.yml

先来说一说CICD配置部分,需求是提供验收和生产环境。


stages:
  - build
  - deploy

services:
- docker:18.09-dind

variables:
  #CI_DEBUG_TRACE: "true"
  GIT_DEPTH: 10
  PROJECT_NAME: oa-domain-net
  REPO_NAME: frontend
  IMAGE_TAG: ${CI_COMMIT_SHA}
  DEVELOP_IMAGE: ${HUB_DEV_ADDR}/${PROJECT_NAME}/${REPO_NAME}:${CI_COMMIT_SHA}
  PRODUCTION_IMAGE: ${HUB_DEV_ADDR}/${PROJECT_NAME}-production/${REPO_NAME}:${CI_COMMIT_TAG}
  

编译(Staging):
  services:
  - docker:18.09-dind
  stage: build
  image: docker:stable
  script:
  - echo "start to build"
  - docker build -t ${DEVELOP_IMAGE} .
  - docker login -u $HUB_DEV_USERNAME -p $HUB_DEV_PASSWORD $HUB_DEV_ADDR
  - docker push ${DEVELOP_IMAGE}
  only:
  - master

部署(Staging):
  stage: deploy
  image: 
    name: alpine/helm:3.0.0-rc.2
    entrypoint: [""]
  script:
  - init_helm
  - helm upgrade --namespace ${PROJECT_NAME}-staging -f Charts/staging_values.yaml --set image.tag=${CI_COMMIT_SHA} --set image.repository=${HUB_DEV_ADDR}/${PROJECT_NAME}/${REPO_NAME} ${REPO_NAME} Charts/
  only:
  - master

编译(Production):
  stage: build
  image: docker:stable
  script:
  - docker build -t ${PRODUCTION_IMAGE} .
  - docker login -u $HUB_DEV_USERNAME -p $HUB_DEV_PASSWORD $HUB_DEV_ADDR
  - docker push ${PRODUCTION_IMAGE}
  only:
  - tags

部署(Production):
  stage: deploy
  image: 
    name: alpine/helm:3.0.0-rc.2
    entrypoint: [""]
  script:
  - init_helm
  - helm upgrade --namespace ${PROJECT_NAME}-production -f Charts/production_values.yaml --set image.tag=${CI_COMMIT_TAG} --set image.repository=${HUB_DEV_ADDR}/${PROJECT_NAME}/${REPO_NAME} ${REPO_NAME} Charts/
  only:
  - tags


.functions: &functions |
    # Functions
    function init_helm() {
        echo $KUBECONFIG_C | base64 -d > ./kubeconfig
        export KUBECONFIG="./kubeconfig"
    }

before_script:
- *functions

在进行部署之前会先使用init_helm方法将Runner容器中含有kubeconfig编码内容的环境变量解码成$KUBECONFIG环境变量来实现集群访问。

Dockerfile

使用nginx+php+supervisord(进程管理)。

这里使用php:7.2-fpm-alpine部署一份ThinkPHP6.0的程序。

# FROM php:7.2-fpm
FROM daocloud.io/php:7.2-fpm-alpine
ENV TZ=Asia/Shanghai
LABEL maintainer="konakona@crazyphper.com"

#阿里云镜像
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories

# 阿里云的内网DNS,如果不需要可以注释掉
RUN echo 'nameserver 100.100.2.136 \n \
nameserver 100.100.2.138' >> /etc/resolv.conf

RUN apk update && apk add --no-cache --virtual .build-deps \
        $PHPIZE_DEPS \
        curl-dev \
        imagemagick-dev \
        libtool \
        libxml2-dev \
        postgresql-dev \
        #sqlite-dev \
	libmcrypt-dev \
        freetype-dev \
        libjpeg-turbo-dev \
        libpng-dev \
    && apk add --no-cache \
        curl \
        git \
        imagemagick \
        mysql-client \
        postgresql-libs \
        nodejs \
        nodejs-npm \
    # 配置npm中国镜像
    && npm config set registry https://registry.npm.taobao.org \
    && pecl install imagick \
    && pecl install mcrypt-1.0.1 \
    && docker-php-ext-enable mcrypt \
    && docker-php-ext-enable imagick \
    && docker-php-ext-install \
        curl \
        mbstring \
        pdo \
        pdo_mysql \
        pdo_pgsql \
        #pdo_sqlite \
        pcntl \
        #tokenizer \
        xml \
        zip \
	&& docker-php-ext-install -j"$(getconf _NPROCESSORS_ONLN)" iconv \
    && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
    && docker-php-ext-install -j"$(getconf _NPROCESSORS_ONLN)" gd \
    && pecl install -o -f redis \
    && rm -rf /tmp/pear \
    && docker-php-ext-enable redis

RUN apk add --no-cache nginx supervisor procps

ENV COMPOSER_ALLOW_SUPERUSER=1
ENV COMPOSER_NO_INTERACTION=1
ENV COMPOSER_HOME=/usr/local/share/composer

RUN mkdir -p /usr/local/share/composer \
	&& curl -o /tmp/composer-setup.php https://getcomposer.org/installer \
	&& php /tmp/composer-setup.php --no-ansi --install-dir=/usr/local/bin --filename=composer --snapshot \
	&& rm -f /tmp/composer-setup.* \
    # 配置composer中国全量镜像
    && composer config -g repo.packagist composer https://packagist.phpcomposer.com

RUN mkdir /run/nginx
RUN rm /etc/nginx/conf.d/*
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
COPY Config/nginx/default.conf /etc/nginx/conf.d/default.conf
# COPY docker/config/nginx/default /etc/nginx/sites-available/default
COPY Config/php/php-fpm.d/docker.conf /usr/local/etc/php-fpm.d/zz-docker.conf
COPY Config/php/php.ini /usr/local/etc/php/php.ini
COPY Config/supervisor/supervisord.conf /etc/supervisor/supervisord.conf
COPY Config/start.sh /usr/local/bin/start


COPY src /var/www
WORKDIR /var/www

RUN chown -R www-data:www-data /var/www/ \
    && chmod +x /usr/local/bin/start 
    
RUN chmod -R 755 public \
 && chmod -R 777 runtime \
 && composer selfupdate --no-plugins

CMD ["/usr/local/bin/start"]

另外把一些常用的镜像也一起放在这里方便大家自取:

#中科大镜像
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories

#163镜像
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.163.com/g' /etc/apk/repositories

# 清华大学镜像,慢死你
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories

/Config/目录下有针对php、nginx、supervisord的各项配置。

nginx/default.conf

server{
    listen 80;
    listen [::]:80;
    index index.php index.html;
    server_name _;
    charset utf-8;
    error_log /var/log/nginx/error.log;
    access_log /var/log/nginx/access.log;
    root /var/www/public;

    location = /robots.txt {
        log_not_found off;
        access_log off;
    }

    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }

    if (!-e $request_filename) {
       rewrite  ^(.*)$  /index.php?s=/$1  last;
       break;
    }

    location ~ \.php$ {
        try_files $uri = 404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/usr/local/var/run/php-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
}

php/php.ini

只需要写入部分配置即可,不需要完整的php.ini文件。

由于自动构建多环境的需求,我们不能够再依赖.env来为程序提供所需的环境变量,而是要让PHP的getenv()$_ENV能够读取到系统环境变量,所以需要修改variables_order的设置。同时Helm Chart Deployment部分也需要定义好对应的环境变量。

# php.ini
variables_order = "EGPCS"
extension=pdo_pgsql.so
extension=pdo_mysql.so

php/php-fpm.d/docker.conf

[global]
daemonize=no
pid=run/php-fpm.pid

[www]
listen=/usr/local/var/run/php-fpm.sock
listen.owner=www-data
listen.group=www-data
listen.mode=0660

supervisor/supervisord.conf

[supervisord]
nodaemon=true
pidfile=/var/run/supervisord.pidfile
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

[program:nginx]
autostart=true
autorestart=true
command=nginx -g "daemon off;"
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

[program:php-fpm]
command=php-fpm
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

有了这些准备,我们还需要做一件事,那就是先试一试。

docker build -t name .
docker run -p 8855:80 name 
curl http://localhost:8855

当能够正常访问到程序后,说明Dockerfile和/Config配置正确无误。

接下来就是根据情况调整Helm Charts后上传到Gitlab看看跑的结果。

]]>
Helm Gitlab Runner pipeline curl: (6) Could not resolve host https://blog.crazyphper.com/2020/04/16/helm-gitlab-runner-pipeline-curl-6-could-not-resolve-host/ Thu, 16 Apr 2020 10:13:35 +0000 http://blog.crazyphper.com/?p=6166 最近遇到了一个奇妙的问题,在Kubernetes中的Gitlab runner网络不稳定。

pipeline(docker:bind)在stage执行docker build时,从阿里云镜像获取依赖巨慢无比,一次都没有完成过。

从构建中的runner pod 内 ping阿里云镜像的地址mirrors.aliyun.com是Bad address。这种奇怪的现象让我头大了2天。最后发现一切与/etc/resolv.conf里的ndots有关……小小的参数,杀伤力怎么那么大呢?

#Dockerfile

FROM daocloud.io/php:7.2-fpm-alpine
# 阿里镜像
# RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories

apk update ...
pipeline执行1小时都做不完PHP的环境部署,不规律的不稳定,有的时候卡在拿gcc,有的时候卡在拿gd

起初我怀疑是集群内kube-dns运作出现了问题,准备了一个busybox试验。

# 安装一个旧版busybox,注意新版有bug
kubectl run busybox --image=busybox:1.28.3

# 让其检查各种不通的域名解析如何
kubectl get pod | grep busybox |awk '{system("kubectl exec "$1" nslookup mirrors.aliyun.com")}' 

kubectl get pod | grep busybox |awk '{system("kubectl exec "$1" nslookup blog.crazyphper.com")}' 
busybox vs pipeline

执行结果对比,busybox OK,说明集群本身没有问题。

我在宿主机的/etc/resolv.conf里增加2枚阿里云内网DNSIP,无效。

我在宿主机的/etc/docker/daemon.json里增加了DNS,无效。

vim /etc/docker/daemon.json

#增加dnspod的dns IP
"dns": ["119.29.29.29"]

vim /etc/resolv.conf

# 增加了这2行阿里云内网DNS
nameserver 100.100.2.138
nameserver 100.100.2.136


# 更新配置
systemctl daemon-reload

CoreDNS默认会将pod所在宿主机上的/etc/resolv.conf拿去使用(kube-dns同理),这次怎么没起作用呢?

使用docker exec -ti [runner容器id] --user root sh 以root权限进入容器排查:

ping kube-dns 10.96.0.10(失败)
ping 自己的博客(成功)
ping 阿里镜像(失败)

我惊了。

我将阿里云内网2枚DNS IP加入到/etc/resolv.conf中,再次ping 阿里镜像,发现可以了。

问题进入白热化……Gitlab本身应该没有对runner起什么作用,因为Gitlab是用docker-compose安装在服务器上,runner使用helm安装在集群内部,由集群进行编排。

Gitlab docs中搜索dns、nameserver关键字没有任何结果或者配置项,这也是为何我在Gitlab这个方向上毫无进展的原因。

经过大量的搜索,在下边这些链接中,我找到了原因:

发现了“罪魁祸首”——ndots。runner我设置为hostNetwork:true将优先请求集群所在域查询。

#runner-value.yaml
hostNetwork: true

在Kubernetes中,可以针对每个Pod设置DNS的策略dnsPolicy字段可以指定相应的策略,目前支持的策略如下:

  • Default: Pod继承所在宿主机的设置,也就是直接将宿主机的/etc/resolv.conf内容挂载到容器中。
  • ClusterFirst: 默认的配置,所有请求会优先在集群所在域查询,如果没有才会转发到上游DNS。
  • ClusterFirstWithHostNet: 和ClusterFirst一样,不过是Pod运行在hostNetwork:true的情况下强制指定的。
  • None: 1.9版本引入的一个新值,这个配置忽略所有配置,以Pod的dnsConfig字段为准。

kube-dns默认会给pod内的/etc/resolv.conf 设置ndots:5。这意味着任何包含少于5个.(点)的解析请求都将在所有搜索域中循环并试图解析。整个ndots就像一个神秘花园,扰乱了请求。

想要知道一个域名有几个.使用host domain命令。

可以看到阿里镜像只有3个点的报文

这也是为什么在runner构建的容器中可以ping通我的博客,却ping不到阿里镜像的原因,之一。

这与我安装的runner镜像版本有关image:gitlab/gitlab-runner:alpine-v12.5.0。alpine轻便,但是它对于遵循FQDN标准要求更加严格。可以使用尾随点.来绕过/etc/resolv.conf 的ndots

虽然绕过了ndots,仍然有解析问题,而且成功率低

我对这个解决办法并不满意,一个Dockerfile会从很多地方获取依赖,有些地方还是隐式的,无法修改。

可以通过安装非alpine版runner来解决,v12.1.0这个版本亲测可行,安装最新版会出现各种权限问题。

# 编辑runner的chart
vim values.yaml

#image: gitlab/gitlab-runner:alpine-v12.5.0
#使用这一镜像
image: gitlab/gitlab-runner:v12.1.0
#如果仍然不行,可以加上这个配置,让pod直接挂载宿主机的/etc/resolv.conf
#dnsPolicy: "Default"

#保存退出后,更新部署
helm upgrade --namespace kube-system -f gitlab-runner/values.yaml gitlab-runner gitlab/gitlab-runner

问题终于解决。

但是效率仍然不够友好,我希望在10分钟内完成一次CICD。

归根结底,应该是Kubernetes+Gitlab(Docker)+Gitlab Runner(Helm)这套组合对于PHP这类依赖编译项多的程序不够友好。这套组合对于JAVA却非常适合,因为依赖极少,在我之前的JAVA程序为首的集群中运行良好。

之后,我又对GitLab Runner进行了调试与优化,将它改成docker run的形式运作,每次CICD的时间平均稳定在8-10分钟。


]]>
Gitlab CICD 构建失败剖析 RPC failed原因 https://blog.crazyphper.com/2020/04/15/gitlab-cicd-%e6%9e%84%e5%bb%ba%e5%a4%b1%e8%b4%a5%e5%89%96%e6%9e%90-rpc-failed%e5%8e%9f%e5%9b%a0/ Wed, 15 Apr 2020 06:30:21 +0000 http://blog.crazyphper.com/?p=6093 最近发现有的仓库在运行CICD时报出error: RPC failed; curl 18 transfer closed with outstanding read data remaining。而简单的项目(静态项目)则不会出现这个问题。

出问题的仓库的info

Curl 18错误是CURLE_PARTIAL_FILEvia),引发这个问题的可能性有:

  1. Git clone期间磁盘空间不足问题
  2. Git浅克隆有关
  3. Git+HTTP之间的通信问题,分块传输编码有关(我是这个原因)
  4. Nginx代理缓冲问题

推荐按照这个顺序排查问题。


一、Git Clone期间磁盘空间不足

在CICD中,Git Clone由Runner管理。因cache目录磁盘空间不足引起这个情况时,可以通过重新部署runner到一个更大的分区来解决。

由于我使用Helm安装的runner,因此修改yaml文件中RUNNER_CACHE_DIR RUNNER_BUILDS_DIR部分。

envVars:
     value: /data/k8s/gitlab/cache/runner
   - name: RUNNER_BUILDS_DIR
     value: /data/k8s/gitlab/cache/builds
   - name: DOCKER_DRIVER

其他安装方式请借鉴官网文档

二、Git浅克隆有关(Shallow cloning)

当遇到很大的仓库(比如几个G)时,我们必须优化Gitlab。

默认情况下,Gitlab和Runner始终执行完整克隆。就相当于把所有Tag、Branch这些都Clone到Runner cache中,对于文件体积大仓库或者历史冗长的仓库会造成很大的性能浪费。

理想情况下,应该始终使用GIT_DEPTH小于10的配置,这意味着Runner会执行浅克隆。

#gitlab-ci.yaml
variables:
  GIT_DEPTH: 10

其实每个人都遇到过这个报错了,这不仅仅是Gitlab上会遇到的。在加入一个Team时,准备Clone一个不断迭代的项目也会遇到,太常见了。

在网上找了张图

值得一提的是,我使用Kubernetes运行Runner的环境是docker+machine,Gitlab默认采用GIT_STRATEGY: fetch策略对此是有局限性的,可以设置为GIT_STRATEGY: none策略,这会禁止Gitlab完成任何fetch和checkout命令,而原本的这些工作由你在gitlab-ci.yaml中做实现。

https://docs.gitlab.com/ee/ci/large_repositories/

只有在很大的仓库时才需要考虑这么做。

三、Git+HTTP之间的通信问题

如果以上2个方式依然无法解决报错,那么通常情况下,可以认定是由于分块传输引起,而处理这一事务的是Nginx。

依然报错

首先确保Nginx反代理配置不会产生多余的步骤,比如我一开始手残写成来proxy_pass https://:真实域名,结果造成来多次连接……修正后,我的配置如下:

#仅供参考

upstream app_name {
    server 127.0.0.1:8444;
}
server {
    listen 443  ssl;
    server_name git.app_name.com;
    #SSL
    ...
    client_max_body_size 0;

    location / {
	proxy_pass https://app_name;
    }
}

另一个要注意的就是磁盘空间大小变化。简单粗暴一点就是看df -h,可以用一个shell命令来观察:

while true; do df; sleep 1; done

通过观察CICD runner 克隆过程中,磁盘空间使用率的增加表明nginx正在进行某些操作。有可能是nginx响应缓冲proxy_buffering(默认开启)引发的问题,可以根据自己的理解判断是否将其关闭。

启用缓冲后,nginx会尽快从代理服务器收到响应,并将其保存到proxy_buffer_size和proxy_buffers指令设置的缓冲区中。如果整个响应都无法容纳到内存中,则可以将一部分响应保存到磁盘上的临时文件中。写入临时文件由proxy_max_temp_file_size和proxy_temp_file_write_size指令控制。

via: http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering

除此意外,还可以检查Gitlab Rails应用程序的日志(production.log,独角兽日志)和Gitlab Workhouse日志。借此来找到有针对性的问题根源。

# 进入容器
docker exec -ti gitlab /bin/bash

# 输出日志
sudo gitlab-ctl tail 
# 查看某一个程序的日志
sudo gitlab-ctl tail gitlab-rails/api_json.log

本文至此结束,感谢大家的阅读,有描述不对的地方还望大家留言指出。

]]>
Traefik 2.0 两层Traffic应用实践 https://blog.crazyphper.com/2020/03/09/traefik-2-0-traffic/ Mon, 09 Mar 2020 04:26:34 +0000 http://blog.crazyphper.com/?p=5556
  • 你有没有遇到过串项目的问题?
    • 访问任意指向集群的域名都能请求到最后一次部署的应用,不管有没有该域名的IngressRoute。
  • 你有没有遇到过CRD权限问题?
    • ClusterRoleBindingServiceAccounts跟NameSpace走,导致ACME在新建的NameSpace里不起作用。
  • —— 明明看起来Traefik部署是好的。

    本文将提供最佳Traefikv2.0在Kubernetes中的实践(我可是折腾了很久啊)。


    概念讲解

    首先,我们会使用官方提供的CRD(自定义资源定义)文件配置对Kuberntes集群访问的权限。

    然后,我们就可以使用一个单例的Traefik了。Traefik在单个实例中启用LetsEncrypt时,并不会遇到任何问题,因为你只需要修改ClusterRoleBindingServiceAccounts里的Namespace与Traefik实例一致,就可以让LetsEncrypt正常运行。

    然而,当我们想要在多个Traefik实例中同时启用LetsEncrypt,就会出现无法正确接收质询请求了。

    When using a single instance of Traefik with LetsEncrypt, no issues should be encountered, however this could be a single point of failure. Unfortunately, it is not possible to run multiple instances of Traefik 2.0 with LetsEncrypt enabled, because there is no way to ensure that the correct instance of Traefik will receive the challenge request, and subsequent responses.

    https://docs.traefik.io/providers/kubernetes-crd/

    为了解决这一类权限问题,我们要嵌套Traefik。让所有的外部访问优先抵达第一层的Traefik,由它去分发服务。


    还记得官方的这张图例吗?这只龅牙鼠其实应该理解为有2只哦。两只分别代表两层Traefik:

    • 第一层(最外层),响应Internet请求。让所有的外部访问优先抵达这一层的Traefik,由它去分发下一步的转发。同时,它还可以集中去做LetsEncrypt这些事情;
    • 第二层,就是每个单例的Traefik,Belongs Private Network。

    看到这里,大家是不是已经隐约感觉到是第一层的Traefik包裹着所有单例Traefik呢?可以这样理解,但也不完全对。因为Traefik是无状态的,唯有靠Service服务通过端口判断来区分内网和外网,才能达到服务分发的作用。


    第一层的Traefik

    一个Traefik实例由Deployment和Service组成,接下来我们准备第一层的Deployment部分。

    Deployment

    注意serviceAccountNameserviceAccount应与CRD创建时设定一致,默认就是traefik-ingress-controller
    注意namespace应与serviceAccount创建的namesapce一致,默认是default,我这里指定的是kube-system,因为CRD中我已修改default为kube-system。
    启用了LetsEncrypt,并挂载了一个traefik-acme-data的PVC配置,用于将ACME数据存放在宿主机上。

    # traefik-ds.yaml
    
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      namespace: kube-system
      name: traefik
      labels:
        app: traefik
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: traefik
      template:
        metadata:
          labels:
            app: traefik
        spec:
          serviceAccountName: traefik-ingress-controller
          serviceAccount: traefik-ingress-controller
    
          volumes:
          - name: data
            persistentVolumeClaim:
              claimName: traefik-acme-data
          containers:
            - args:
              - --api
              - --accesslog
              - --global.sendanonymoususage
              - --entrypoints.web.address=:8000
              - --entrypoints.websecure.address=:4443
              - --providers.kubernetescrd
              - --certificatesresolvers.default.acme.tlschallenge
              - --certificatesresolvers.default.acme.email=konakona.xiong@gmail.com
              - --certificatesresolvers.default.acme.httpChallenge.entryPoint=web
              - --certificatesresolvers.default.acme.storage=/data/acme.json
              image: traefik:v2.0
              imagePullPolicy: IfNotPresent
              name: traefik
              ports:
              - containerPort: 8000
                name: web
                protocol: TCP
              - containerPort: 4443
                name: websecure
                protocol: TCP
              volumeMounts:
                - name: data
                  mountPath: /data
    

    PV/PVC

    创建一个PV,将ACME数据存放至宿主机的/data/k8s/kube-system/traefik/acme-data目录下,并声明1G的空间,理论上来说10M就够了。

    # traefik-pv.yaml
    
    kind: PersistentVolume
    apiVersion: v1
    metadata:
      name: traefik-acme-data
    spec:
      capacity:
        storage: "1Gi"
      accessModes:
        - ReadWriteOnce
      persistentVolumeReclaimPolicy: Retain
      hostPath:
        path: "/data/k8s/kube-system/traefik/acme-data"
    ---
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: traefik-acme-data
      namespace: kube-system
    spec:
      volumeName: traefik-acme-data
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: "1Gi"

    Service

    分别创建对外的service:traefik,以及对内的service:traefik-internal

    # traefik-service.yaml
    
    apiVersion: v1
    kind: Service
    metadata:
      name: traefik
      namespace: kube-system
    spec:
      ports:
      - name: web
        nodePort: 32001
        port: 8000
        protocol: TCP
        targetPort: 8000
      - name: websecure
        nodePort: 32002
        port: 4443
        protocol: TCP
        targetPort: 4443
      selector:
        app: traefik
      type: NodePort
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: traefik-internal
      namespace: kube-system
    spec:
      ports:
      - name: web
        port: 8000
        protocol: TCP
        targetPort: 80
      - name: websecure
        port: 4443
        protocol: TCP
        targetPort: 443
      selector:
        app: traefik
      type: ClusterIP
    

    service:traefik负责将所有外部访问32001和32002端口的请求转发给service:traefik-internal的8000和4443。


    执行kubectl apply -f . 吧,如若配置无误,将可以看到各项服务。

    至此,第一层部署结束。接下来,我们需要将所有单例Traefik中LetsEncrypt的部分去除,就可以享受到第一层主动为你去申请LetsEncrypt的功劳了。


    单例Traefik 借鉴

    如果好奇我的单例Traefik是如何部署的,那么接下来我将进一步展示它们。

    Deployment

    spec:
        containers:
            - name: traefik
              image: traefik:v2.0
              imagePullPolicy: {{ .Values.image.pullPolicy }}
              args:
                - --api
                - --accesslog
                - --global.sendanonymoususage
                - --entrypoints.web.address=:80
              ports:
                - name: http
                  containerPort: 80
                  protocol: TCP

    Services

    apiVersion: v1
    kind: Service
    metadata:
      name: traefik
    spec:
      type: ClusterIP
      ports:
        - port: 80
          targetPort: http
          protocol: TCP
          name: http
      selector:
        app.kubernetes.io/name: traefik
        app.kubernetes.io/instance: traefik

    因第一层的Traefik下的service:traefik-internal会将所有Traefik的80转发到8000端口,再由service:traefik将8000转发到NodePort:32001供外部访问。

    开发者无需修改自己docker-compose文件去适应部署的需要,直接按照最初的80提供给CICD,保证了大部分代码的部署是无感知、无改动的。

    同时,LetsEncrypt的工作已经完整的交由第一层Traefik处理,单例的Traefik无需再写args cli,除非你不需要。

    traefik 获取访问者的真实IP

    从Kubernetes 1.5 开始,默认情况下,发送到 Type=NodePort 的服务数据包来自 DNAT 。 traefik的Service默认使用的 externalTrafficPolicy:Cluster,代表其他节点的Pod流量可以转发过来,这意味着traefik Pod拿到的是转发过来的ClusterIP,并非访问者的真实IP。就好比张三在访问网站,但是服务器以为是李四来了。

    我们可以通过设置 externalTrafficPolicy: Local 将请求代理到本地端点,而不是其他节点Pod转发来的ClusterIP,以保留最原始的请求IP地址。

    当你的traefik是单部署的时候,在 Server 中 spec externalTrafficPolicy: Local 就大功告成了:

    apiVersion: v1
    kind: Service
    metadata:
      name: traefik
      namespace: kube-system
    spec:
      externalTrafficPolicy: Local  # <-- 此处
      type:NodePort # 或LoadBalancer才可以设置externalTrafficPolicy

    当使用的是两层traefik,不仅要在第一层的traefik server中设置 externalTrafficPolicy: Local ,还需要在应用层的单例traefik Deployment中增加command(用args啊!喂)来传递X-Forwarded-For的IP:

    args:
      - --entryPoints.web.forwardedHeaders.trustedIPs=0.0.0.0/0

    应用层的traefik Service是ClusterIP,所以不需要改变也无法改变 externalTrafficPolicy

    到这里,文章结束,感谢大家的阅读,欢迎留言。

    ]]>