惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Fortinet All Blogs
Microsoft Security Blog
Microsoft Security Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Vercel News
Vercel News
Application and Cybersecurity Blog
Application and Cybersecurity Blog
C
Check Point Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
W
WeLiveSecurity
The Hacker News
The Hacker News
L
LINUX DO - 热门话题
T
Tenable Blog
Hugging Face - Blog
Hugging Face - Blog
Google Online Security Blog
Google Online Security Blog
博客园 - Franky
P
Proofpoint News Feed
H
Hacker News: Front Page
P
Privacy & Cybersecurity Law Blog
月光博客
月光博客
P
Proofpoint News Feed
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
The GitHub Blog
The GitHub Blog
云风的 BLOG
云风的 BLOG
博客园_首页
www.infosecurity-magazine.com
www.infosecurity-magazine.com
C
CERT Recently Published Vulnerability Notes
Forbes - Security
Forbes - Security
I
InfoQ
Stack Overflow Blog
Stack Overflow Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Attack and Defense Labs
Attack and Defense Labs
N
News and Events Feed by Topic
博客园 - 叶小钗
T
Threat Research - Cisco Blogs
aimingoo的专栏
aimingoo的专栏
D
Darknet – Hacking Tools, Hacker News & Cyber Security
小众软件
小众软件
大猫的无限游戏
大猫的无限游戏
MongoDB | Blog
MongoDB | Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Hacker News - Newest:
Hacker News - Newest: "LLM"
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 司徒正美
O
OpenAI News
G
Google Developers Blog
Martin Fowler
Martin Fowler
罗磊的独立博客
S
SegmentFault 最新的问题
T
Tor Project blog
量子位

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角 .NET Core 原生 DI 扩展之基于名称的注入实现 - 元视角
关于 Docker 容器配置信息的渐进式思考 - 元视角
飞鸿踏雪 · 2022-12-01 · via 元视角

作为一名软件工程师,不,或许应该叫做 YAML 工程师、Markdown 工程师、Dockerfile 工程师……等等,这绝非自谦,更多的是一种自嘲。毕竟,从入行的那一天开始,追求配置上的动态灵活,就如同思想一般刻进每个程序员的 DNA 里。可当你意识到,在这个世界上,提出主张的人和解决问题的人,并不是同一群人时,你或许会心头一紧,接着便是直呼上当,我甚至不能理解,为什么程序员提交完代码,还要像运维一样折腾各种配置文件。特别是在 DevOps 的理念流行开以后,程序员们简直就是在通过各种配置文件互相折磨对方。如果程序员不能通过程序变得懒惰,那是不是说明,我们早已忘记了当初学习编程时的初心?我们都以为代码可以不用修改,可有哪一次代码能逃过面目全非的结局?每当这个时候,我就特别想怼那些主张配置文件的人,要不您来?言归正传,今天我想聊聊容器、配置文件和环境变量,为什么称为渐进式思考呢?因为它更像是一种不同人生阶段的回顾。

从何说起

故老相传,鸿蒙初开,天地混沌。上帝说,要有光。于是,盘古抄起那把传说中的开天神斧,对着虚空世界就是一通输出。那一刻,这位创世神周围就像发生了奇点大爆炸一样迅速扩张。最终,它的身体化作了世间万物,推动这个世界从无到有的进化历程。屏幕前的你,无需纠结这段融合了东/西方神话、现代物理学的表述是否严谨,因为我想说的是,在一个事物发展的初期,一定是朴素而且原始的。相信大家开始写 Dockerfile 的时候,一定没少写过下面这样的脚本:

COPY /config/nginx.conf /etc/nginx/nginx.conf

如你所见,该命令会复制主机上的配置文件到容器的指定目录,而这其实是符合我们一开始对容器的预期的,即:我们只需要将程序打包到镜像里,就可以快速地完成程序的部署。可是,我们显然忽略了一个问题,当程序部署到不同的环境中时,它需要的配置文件自然是不同的。此时,你可能会采用下面的做法:

docker exec -it <容器Id> sh
vim /etc/nginx/nginx.conf

环境变量

果然,大道至简,没有任何技巧,简直真诚到极致。常言道:智者不入爱河,这个做法辛不辛苦姑且不论,关键是容器一旦重启,你连慨叹镜花水月的时间都没有啦。所以,这个方案可谓是劳心劳力,为我所不取也!再后来,你发现容器里可以使用环境变量,于是你就灵机一动,为什么不能让这个配置文件支持动态配置呢?于是,你尝试使用下面的做法:

server {
    listen      ${NGINX_PORT};
    listen      [::]:${NGINX_PORT};
    server_name ${NGINX_HOST};

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
}

此时,我们只需要在 .env 文件或者 docker-compose.yml 文件里指定这些环境变量即可。对于这个思路,我们可以使用 envsubst 这个工具来实现:

export NGINX_PORT=80
export NGINX_HOST=xyz.com
apt-get update && apt-get install -y gettext-base
envsubst < /config/nginx.conf > /etc/nginx/nginx.conf

此时,我们会发现,它可以实现环境变量的“注入”:

环境变量的“注入” 环境变量的“注入”

当然,如果这段脚本是写在 RUN 指令后面,那么,这个改进是非常有限的。因为如果你希望更新配置,你必须要重新构建一个镜像,一个更好的做法是,将这段脚本放到 CMD 或者 ENTRYPOINT 指令里。这样,我们更新配置时只需要重启容器即可,这是不是就符合配置上的动态灵活了呢?事实上,这正是博主公司一直采用的做法。不过,运维同事大概率是没听说过 envsubst 这个工具,他使用的是更朴素的 sed 命令:

echo -e "\nReading all environment variables..."
for line in $(printenv);
do
 varname=$(echo $line | tr -dc '[:alnum:]=_' | cut -d'=' -f1)
 if [ "$varname" != "" ]
 then
   envval=$(eval "echo \"\$$varname\"")
   if [ "$envval" != "" ]
   then
    echo -e "Filling $varname into config templates..."
    escaped_envval=$(printf '%s\n' "$envval" | sed -e 's/[\/&]/\\&/g')
    sed -i "s/{{ $varname }}/$escaped_envval/g" ./config-templates/*
   fi
 fi
done

当然,我们最终还是放弃了这个方案,因为它增加了我们的维护成本,在开发和测试各有一套配置的前提下,再增加一套配置模板,同时还要保证这 3 套配置上的一致性,其难度可想而知。更重要的是,这套基于字符串替换的方案,支持的绑定语法非常有限,譬如不支持默认值或者是参数计算。如果某一个环境变量忘记配置,那么容器大概率是无法正常启动的。在放弃了配置模板的方案以后,我们不得不开始学习下面这套配置语法

export Position__Title=Environment_Editor
export Position__Name=Environment_Rick
export Logging__0__Name ToEmail
export Logging__0__Level Critical
export Logging__0__Args__FromAddress MySystem@example.com
export Logging__0__Args__ToAddress SRE@example.com
export Logging__1__Name ToConsole
export Logging__1__Level Information

这份由环境变量组成的配置信息,其等价表示为:

{
    "Position": {
        "Title": "Environment_Editor",
        "Name": "Environment_Rick"
    },
    "Logging": [{
        "Name": "ToEmail",
        "Level": "Critical",
        "Args": {
            "FromAddress": "MySystem@example.com",
            "ToAddress": "SRE@example.com"
        }
    }, {
        "Name": "ToConsole",
        "Level": "Information"
    }]
}

再后来,随着知识体系的不断完善,你发现容器内的配置文件可以挂载到主机上。此时,修改主机上的配置文件,就可以更新容器内的配置文件,其基本用法如下:

docker run -v <host-dir>:<container-dir>:<rw|wo>

或者,你可以使用 docker-compose.yml 来进行服务编排,然后通过 volumes 字段来挂载一个目录:

version: '3.5'
services:
  nextcloud_web:
    build: ./
    ports:
      - ${NEXTCLOUD_SERVER_PORT_HTTP}:80
      - ${NEXTCLOUD_SERVER_PORT_HTTPS}:443
    volumes:
      - "./data/nextcloud/config:/var/www/html/data/config"

那么,在这种模式下,是否就完美无瑕了呢?我个人觉得,这个方案对人的要求变得更高了一点,因为此前你只需要关注特定的环境变量即可,可现在你面对是一份完整的配置文件,你不得不了解每一种配置文件的细节,甚至于连 YAML 文件里的缩进都要关心,博主已经不止一次地帮别人处理开发环境 Envoy 配置文件的问题。除此之外,如果配置文件的结构频繁地发生变动,确保这两份配置文件步调一致,就再次变成了一个新的问题。

Docker Config

<html lang="en">
  <head><title>Hello Docker</title></head>
  <body>
    <p>Hello {{ env "HELLO" }}! I'm service {{ .Service.Name }}.</p>
  </body>
</html>

事实上,在 Docker Swarm 的模式下,官方提供了一种机制来管理配置文件,它最大的优点是:不用挂载目录或者注入环境变量。如上图所示,假设我们有一个名为 index.html.tmpl 的模板文件。接下来,我们只需要创建一个配置并使用这个配置即可:

# 创建一个名为 index-config 的配置
docker config create --template-driver golang index-config index.html.tmpl
# 使用 index-config 配置启动容器
docker service create \
    --name hello-world \
    --env HELLO="Docker" \
    --config source=index-config,target=/usr/share/nginx/html/index.html \
    --publish published=3000,target=80 \
    nginx:alpine

K8S 与 ConfigMap

我不想评价 Docker Swarm 和 Kubernetes 孰优孰劣,我只知道,博主公司最终还是从 Docker Swarm 退回到了 Docker Compose 。对博主而言,现在主要使用 Docker Compose,我一直不愿意触碰 Kubernetes 的原因是,我不想再多学习一种配置文件。有人说, Kubernetes 是目前容器编排的事实标准,没有什么东西能在这个领域超越它。那么, Kubernetes 是如何管理容器内的配置的呢?答案是 ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-info
data:
  baseUrl: "http://xyz.com:8080
  timeout: 86400

如图所示,这是一个最基本的 ConfigMap 示例,我们只需要通过下面的命令就可以创建一个 ConfigMap

kubectl create -f configmap.yaml

当然,它还有下面这些重载形式,可以从不同的数据源创建 ConfigMap

# 从单个文件创建
kubectl create configmap *** --from-file=file1
# 从多个文件创建
kubectl create configmap *** --from-file=file1 --from-file=file2
# 从键值对创建
kubectl create configmap *** --from-literal=value1=123 --from-literal=value2=234
# 从环境变量创建
kubectl create configmap *** --from-env-file=.env

最终,这些 ConfigMap 可以作为 Pod 级别的卷或者是在环境变量中被引用。请注意,博主并不打算动手去写一个 K8S 示例,因为比做一件事情更重要的事情是,知道为什么要做这件事情。你也许完全想象不到,我当初对环境变量这个方案是有多抗拒,因为我觉得明明配置中心更好用啊,这些配置文件你全都放到配置中心里,届时容器只需要从配置中心里拉取配置即可。我承认,这个设想非常美好,可惜对于技术选型这些事情来说,有时候,它并不是一道选择题,而是一道填空题。

本文小结

圣人有云:温故而知新,可以为师矣!通过这一系列的梳理,我们可以得出一个结论,即:容器内的配置管理,唯一正确的方向就是让配置和容器分离。不管你是用上面这些方案中的哪一种,其关键就是让配置动态地在容器内生效,而非固化到容器中。可有意思的是,K8S 里的 ConfigMap 是可以设置为 Immutable 的,这就引申出一个关乎平衡的哲学命题,变与不变该如何去抉择,一个人想要在这个世界上安身立命,到底是要遵从本性、初心不改?还是要世故圆滑、适者生存?也许,到底还是古人更聪明一点,讲究一个外圆内方。从这个角度来看的话,古代的钱币被铸成外圆内方的形制,大抵有刻意为之的嫌疑。毕竟啊,无论是在哪个朝代,一心只想搞钱这种想法都显得真实、不做作,这同样是我对于技术的态度。好了,以上就是这篇文章的全部内容,欢迎大家在评论区交换想法!