使用树莓派和电子纸显示屏开始倒计时你的下一个假期。 圆周率日 Pi Day （3 月 14 日） 来了又走，留下美好的回忆以及 许多树莓派项目 等待我们去尝试。在任何令人精神振奋、充满欢乐的假期后回到工作中都很难，圆周率日也不例外。当我们回望三月的时候，渴望那些天的快乐。但是不用害怕，亲爱的圆周率日庆祝者们，我们开始下一个节日的漫长倒计时！好了，严肃点。我做了一个圆周率日倒计时器，你也可以！不久前，我购买了一个 树莓派 Zero W，并且用它来 解决 WiFi 信号较差的原因 。我也对使用 电子纸 ePaper 来作为它的显示屏十分感兴趣。虽然我不知道该用它来干什么，但是！它看起来真的很有趣！我买了一个十分适合放在树莓派的顶部的 2.13 英寸的 WaveShare 显示器 。安装很简单：只需要将显示器接到树莓派的 GPIO 上即可。我使用 树莓派操作系统 来实现该项目，虽然其他的操作系统肯定也能完成。但是下面的 raspi-config 命令在树莓派系统上很容易使用。设置树莓派和电子纸显示屏设置树莓派和电子纸显示屏一起工作，需要你在树莓派软件中启用串行外设接口（SPI），安装 BCM2835 C 库（来访问树莓派上的博通 BCM 2835 芯片的 GPIO 功能），安装 Python GPIO 库来控制电子纸显示屏。最后，你需要安装 WaveShare 的库来使用 Python 控制这个 2.13 英寸的显示屏。下面是完成这些的步骤。启用 SPI树莓派上启用 SPI 最简单的方式是使用 raspi-config 命令。SPI 总线允许与设备进行串行数据通信——在本例中，电子纸显示：$ sudo raspi-config 从弹出的菜单中, 选择 “ 接口选项 Interfacing Options -> SPI -> 是 Yes ” 来启用 SPI 接口，然后启动。安装 BCM2835 库如上所述，BCM2835 库是用于树莓派博通 BCM2385 芯片的软件，它允许访问 GPIO 引脚来控制设备。在我写这篇文章之时，用于树莓派的最新博通 BCM2385 库版本是 v1.68 。安装此库需要下载软件压缩包然后使用 make 来安装：# 下载 BCM2853 库并解压 $ curl -sSL http://www.airspayce.com/mikem/bcm2835/bcm2835-1.68.tar.g> -o - | tar -xzf - # 进入解压后的文件夹 $ pushd bcm2835-1.68/ # 配置、检查并安装 BCM2853 库 $ sudo ./configure $ sudo make check $ sudo make install # 返回上级目录 $ popd 安装需要的 Python 库你用 Python 控制电子纸显示屏需要安装 Python 库 RPi.GPIO，还需要使用 python3-pil 包来画图。显然，PIL 包已经不行了，但 Pillow 可以作为代替方案。我还没有为该项目测试过 Pillow ，但它可行：# 安装需要的 Python 库 $ sudo apt-get update $ sudo apt-get install python3-pip python3-pil $ sudo pip3 install RPi.GPIO 注意：这些是 Python3 的指令。你可以在 WaveShare 网站查到 Python2 的指令。下载 WaveShare 示例和 Python 库Waveshare 维护了一个 Python 和 C 的 Git 库，用于使用其电子纸显示屏和一些展示如何使用它们的示例。对这个倒计时时钟而言，你需要克隆这个库并使用用于 2.13 英寸显示屏的库：# 克隆这个 WaveShare e-Paper git 库 $ git clone https://github.com/waveshare/e-Paper.gi> 如果你用不同的显示器或者其他公司产品，需要使用适配软件。Waveshare 提供了很多指导：WaveShare 电子纸设置指导WaveShare 电子纸库安装指导获得有趣的字体（选做）你可以随心所欲的使用显示器，为什么不搞点花样？找一个炫酷的字体！这有大量 开放字体许可 的字体可供选择。我十分喜爱 Bangers 字体。如果你看过 YouTube 那你见过这种字体了，它十分流行。你可以下载到本地的共享字体目录文件中，并且所有的应用都可以使用，包括这个项目：# “Bangers” 字体是 Vernon Adams 使用 Google 字体开放许可授权的字体 $ mkdir -p ~/.local/share/fonts $ curl -sSL https://github.com/google/fonts/raw/master/ofl/bangers/Bangers-Regular.ttf -o fonts/Bangers-Regular.ttf 创建一个圆周率日倒计时器现在你已经安装好了软件，可以使用带有炫酷字体的电子纸显示屏了。你可以创建一个有趣的项目：倒计时到下一个圆周率日！如果你想，你可以从该项目的 GitHub 仓库 直接下载 countdown.py 这个 Python 文件并跳到文章结尾。为了满足大家的好奇心，我将逐步讲解。导入一些库#!/usr/bin/python3 # -*- coding:utf-8 -*- import logging import os import sys import time from datetime import datetime from pathlib import Path from PIL import Image,ImageDraw,ImageFont logging.basicConfig(level=logging.INFO) basedir = Path(__file__).parent waveshare_base = basedir.joinpath('e-Paper', 'RaspberryPi_JetsonNano', 'python') libdir = waveshare_base.joinpath('lib') 开始先导入一些标准库之后脚本中用。也需要你从 PIL 添加 Image、ImageDraw 和 ImageFont，你会用到这些来画一些简单的图形。最后，为本地 lib 目录设置一些变量，该目录包含了用于 2.13 英寸显示屏的 Waveshare Python 库，稍后你可以使用这些变量从本地目录加载库。字体大小辅助函数下一部分是为你选择的 Bangers-Regular.ttf 字体建立一个修改大小的辅助函数。该函数将整型变量作为大小参数，并返回一个图形字体对象来用于显示：def set_font_size(font_size): logging.info("Loading font...") return ImageFont.truetype(f"{basedir.joinpath('Bangers-Regular.ttf').resolve()}", font_size) 倒计时逻辑接下来是计算这个项目的一个函数：距下次圆周率日还有多久。如果是在一月，那么计算剩余天数将很简单。但是你需要考虑是否今年的圆周率日是否已经过去了（允悲）。如果是的话，那么计算在你可以再次庆祝之前还有多少天：def countdown(now): piday = datetime(now.year, 3, 14) # 如果错过了就增加一年 if piday < now: piday = datetime((now.year + 1), 3, 14) days = (piday - now).days logging.info(f"Days till piday: {days}") return day 主函数最后，到了主函数，需要初始化显示屏并向它写数据。这时，你应该写一个欢迎语然后再开始倒计时。但是首先，你需要加载 Waveshare 库：def main(): if os.path.exists(libdir): sys.path.append(f"{libdir}") from waveshare_epd import epd2in13_V2 else: logging.fatal(f"not found: {libdir}") sys.exit(1) 上面的代码片段检查以确保该库已下载到倒计时脚本旁边的目录中，然后加载epd2in13_V2 库。如果你使用不同的显示屏，则需要使用不同的库。如果你愿意，也可以自己编写。我发现阅读 Waveshare 随显示屏提供的 Python 代码很有趣，它比我想象的要简单得多。下一段代码创建一个 EPD（电子纸显示屏）对象以与显示器交互并初始化硬件： logging.info("Starting...") try: # 创建一个显示对象 epd = epd2in13_V2.EPD() # 初始化并清空显示 # ePaper 保持它的状态处分更新 logging.info("Initialize and clear...") epd.init(epd.FULL_UPDATE) epd.Clear(0xFF) 关于电子纸的一个有趣之处：它仅在将像素从白色变为黑色或从黑色变为白色时才耗电。这意味着当设备断电或应用程序因任何原因停止时，屏幕上的任何内容都会保留下来。从功耗的角度来看，这很好，但这也意味着你需要在启动时清除显示，否则你的脚本只会覆盖屏幕上已有的内容。 因此，epd.Clear(0xFF) 用于在脚本启动时清除显示。接下来，创建一个“画布”来绘制剩余的显示输出： # 创建一个图形对象 # 注意："epd.heigh" 是屏幕的长边 # 注意："epd.width" 是屏幕的短边 # 真是反直觉… logging.info(f"Creating canvas - height: {epd.height}, width: {epd.width}") image = Image.new('1', (epd.height, epd.width), 255) # 255: clear the frame draw = ImageDraw.Draw(image) 这与显示器的宽度和高度相匹配——但它有点反直觉，因为显示器的短边是宽度。我认为长边是宽度，所以这只是需要注意的一点。 请注意，epd.height 和 epd.width 由 Waveshare 库设置以对应于你使用的设备。欢迎语接下来，你将开始画一些画。这涉及在你之前创建的“画布”对象上设置数据。这还没有将它绘制到电子纸显示屏上——你现在只是在构建你想要的图像。由你为这个项目绘制带有一块馅饼的图像，来创建一个庆祝圆周率日的欢迎信息：很可爱，不是吗？ logging.info("Set text text...") bangers64 = set_font_size(64) draw.text((0, 30), 'PI DAY!', font = bangers64, fill = 0) logging.info("Set BMP...") bmp = Image.open(basedir.joinpath("img", "pie.bmp")) image.paste(bmp, (150,2)) 最后，真是是最后了，你可以展示你画的图画： logging.info("Display text and BMP") epd.display(epd.getbuffer(image)) 上面那段话更新了显示屏，以显示你所画的图像。接下来，准备另一幅图像展示你的倒计时：圆周率日倒计时首先，创建一个用来展示倒计时的图像对象。也需要设置数字的字体大小： logging.info("Pi Date countdown; press CTRL-C to exit") piday_image = Image.new('1', (epd.height, epd.width), 255) piday_draw = ImageDraw.Draw(piday_image) # 设置字体大小 bangers36 = set_font_size(36) bangers64 = set_font_size(64) 为了使它显示的时候更像一个倒计时，更新图像的一部分是更加有效的手段，仅更改已经改变的显示数据部分。下面的代码准备以这样方式运行： # 准备更新显示 epd.displayPartBaseImage(epd.getbuffer(piday_image)) epd.init(epd.PART_UPDATE) 最后，需要计时，开始一个无限循环来检查据下次圆周率日还有多久，并显示在电子纸上。如果到了圆周率日，你可以输出一些庆祝短语： while (True): days = countdown(datetime.now()) unit = get_days_unit(days) # 通过绘制一个填充有白色的矩形来清除屏幕的下半部分 piday_draw.rectangle((0, 50, 250, 122), fill = 255) # 绘制页眉 piday_draw.text((10,10), "Days till Pi-day:", font = bangers36, fill = 0) if days == 0: # 绘制庆祝语 piday_draw.text((0, 50), f"It's Pi Day!", font = bangers64, fill = 0) else: # 绘制距下一次 Pi Day 的时间 piday_draw.text((70, 50), f"{str(days)} {unit}", font = bangers64, fill = 0) # 渲染屏幕 epd.displayPartial(epd.getbuffer(piday_image)) time.sleep(5) 脚本最后做了一些错误处理，包括捕获键盘中断，这样你可以使用 Ctrl + C 来结束无限循环，以及一个根据计数来打印 day 或 days 的函数： except IOError as e: logging.info(e) except KeyboardInterrupt: logging.info("Exiting...") epd.init(epd.FULL_UPDATE) epd.Clear(0xFF) time.sleep(1) epd2in13_V2.epdconfig.module_exit() exit() def get_days_unit(count): if count == 1: return "day" return "days" if __name__ == "__main__": main() 现在你已经拥有一个倒计时并显示剩余天数的脚本！这是在我的树莓派上的显示（视频经过加速，我没有足够的磁盘空间来保存一整天的视频）：安装 systemd 服务（选做）如果你希望在系统打开时运行倒计时显示，并且无需登录并运行脚本，你可以将可选的 systemd 单元安装为 systemd 用户服务。将 GitHub 上的 piday.service 文件复制到 ${HOME}/.config/systemd/user，如果该目录不存在，请先创建该目录。然后你可以启用该服务并启动它：$ mkdir -p ~/.config/systemd/user $ cp piday.service ~/.config/systemd/user $ systemctl --user enable piday.service $ systemctl --user start piday.service # Enable lingering, to create a user session at boot # and allow services to run after logout $ loginctl enable-linger $USER 该脚本将输出到 systemd 日志，可以使用 journalctl 命令查看输出。它开始看起来像是圆周率日了！这就是你的作品！一个显示在电子纸显示屏上的树莓派 Zero W 圆周率日倒计时器！并在系统启动时使用 systemd 单元文件启动！现在距离我们可以再次相聚庆祝圆周率日还有好多天的奇妙设备———树莓派。通过我们的小项目，我们可以一目了然地看到确切的天数。但实际上，每个人都可以在每一天在心中庆祝圆周率日，因此请使用自己的树莓派创建一些有趣且具有教育意义的项目吧！via: https://opensource.com/article/21/3/raspberry-pi-countdown-clock作者：Chris Collins 选题：lujun9972 译者：Donkey 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
实现一个 WiFi 扫描器玩玩～去年夏天，我和妻子变卖了家产，带着我们的两只狗移居了夏威夷。这里有美丽的阳光、温暖的沙滩、凉爽的冲浪等你能想到的一切。我们同样遇到了一些意料之外的事：WiFi 问题。不过，这不是夏威夷的问题，而是我们租住公寓的问题。我们住在一个单身公寓里，与房东的公寓仅一墙之隔。我们的租房协议中包含了免费的网络连接！好耶！只不过，它是由房东的公寓里的 WiFi 提供的，哇哦……说实话，它的效果还不错……吧？好吧，我承认它不尽如人意，并且不知道是哪里的问题。路由器明明就在墙的另一边，但我们的信号就是很不稳定，经常会自动断开连接。在家的时候，我们的 WiFi 路由器的信号能够穿过层层墙壁和地板。事实上，它所覆盖的区域比我们居住的 600 平方英尺（大约 55 平方米）的公寓还要大。在这种情况下，一个优秀的技术人员会怎么做呢？既然想知道为什么，当然是开始排查咯！幸运的是，我们在搬家之前并没有变卖掉树莓派 Zero W。它是如此小巧便携! 我当然就把它一起带来了。我有一个机智的想法：通过树莓派和它内置的 WiFi 适配器，使用 Go 语言编写一个小程序来测量并显示从路由器收到的 WiFi 信号。我打算先简单快速地把它实现出来，以后再去考虑优化。真是麻烦！我现在只想知道这个 WiFi 是怎么回事！谷歌搜索了一番后，我发现了一个比较有用的 Go 软件包 mdlayher/wifi，它专门用于 WiFi 相关操作，听起来很有希望！获取 WiFi 接口的信息我的计划是查询 WiFi 接口的统计数据并返回信号强度，所以我需要先找到设备上的接口。幸运的是，mdlayher/wifi 包有一个查询它们的方法，所以我可以创建一个 main.go 来实现它，具体代码如下：package main import ( "fmt" "github.com/mdlayher/wifi" ) func main() { c, err := wifi.New() defer c.Close() if err != nil { panic(err) } interfaces, err := c.Interfaces() for _, x := range interfaces { fmt.Printf("%+v\n", x) } } 让我们来看看上面的代码都做了什么吧！首先是导入依赖包，导入后，我就可以使用 mdlayher/wifi 模块就在 main 函数中创建一个新的客户端（类型为 *Client）。接下来，只需要调用这个新的客户端（变量名为 c）的 c.Interfaces() 方法就可以获得系统中的接口列表。接着，我就可以遍历包含接口指针的切片（变长数组），然后打印出它们的具体信息。注意到 %+v 中有一个 + 了吗？它意味着程序会详细输出 *Interface 结构体中的属性名，这将有助于我标识出我看到的东西，而不用去查阅文档。运行上面的代码后，我得到了机器上的 WiFi 接口列表：&{Index:0 Name: HardwareAddr:5c:5f:67:f3:0a:a7 PHY:0 Device:3 Type:P2P device Frequency:0} &{Index:3 Name:wlp2s0 HardwareAddr:5c:5f:67:f3:0a:a7 PHY:0 Device:1 Type:station Frequency:2412} 注意，两行输出中的 MAC 地址（HardwareAddr）是相同的，这意味着它们是同一个物理硬件。你也可以通过 PHY: 0 来确认。查阅 Go 的 wifi 模块文档，PHY 指的就是接口所属的物理设备。第一个接口没有名字，类型是 TYPE: P2P。第二个接口名为 wpl2s0，类型是 TYPE: Station。wifi 模块的文档列出了 不同类型的接口，以及它们的用途。根据文档，P2P（点对点传输） 类型表示“该接口属于点对点客户端网络中的一个设备”。我认为这个接口的用途是 WiFi 直连 ，这是一个允许两个 WiFi 设备在没有中间接入点的情况下直接连接的标准。Station（基站）类型表示“该接口是具有 控制接入点 controlling access point 的客户端设备管理的 基本服务集 basic service set （BSS）的一部分”。这是大众熟悉的无线设备标准功能：作为一个客户端来连接到网络接入点。这是测试 WiFi 质量的重要接口。利用接口获取基站信息利用该信息，我可以修改遍历接口的代码来获取所需信息：for _, x := range interfaces { if x.Type == wifi.InterfaceTypeStation { // c.StationInfo(x) returns a slice of all // the staton information about the interface info, err := c.StationInfo(x) if err != nil { fmt.Printf("Station err: %s\n", err) } for _, x := range info { fmt.Printf("%+v\n", x) } } } 首先，这段程序检查了 x.Type（接口类型）是否为 wifi.InterfaceTypeStation，它是一个基站接口（也是本练习中唯一涉及到的类型）。不幸的是名字出现了冲突，这个接口“类型”并不是 Golang 中的“类型”。事实上，我在这里使用了一个叫做 interfaceType 的 Go 类型来代表接口类型。呼，我花了一分钟才弄明白！然后，假设接口的类型正确，我们就可以调用 c.StationInfo(x) 来检索基站信息，StationInfo() 方法可以获取到关于这个接口 x 的信息。这将返回一个包含 *StationInfo 指针的切片。我不大确定这里为什么要用切片，或许是因为接口可能返回多个 StationInfo？不管怎么样，我都可以遍历这个切片，然后使用之前提到的 +%v 技巧格式化打印出 StationInfo 结构的属性名和属性值。运行上面的程序后，我得到了下面的输出：&{HardwareAddr:70:5a:9e:71:2e:d4 Connected:17m10s Inactive:1.579s ReceivedBytes:2458563 TransmittedBytes:1295562 ReceivedPackets:6355 TransmittedPackets:6135 ReceiveBitrate:2000000 TransmitBitrate:43300000 Signal:-79 TransmitRetries:2306 TransmitFailed:4 BeaconLoss:2} 我感兴趣的是 Signal（信号）部分，可能还有 TransmitFailed（传输失败）和 BeaconLoss（信标丢失）部分。信号强度是以 dBm（ 分贝-毫瓦 decibel-milliwatts ）为单位来报告的。 简短科普：如何读懂 WiFi dBm根据 MetaGeek 的说法：-30 最佳，但它既不现实也没有必要-67 非常好，它适用于需要可靠数据包传输的应用，例如流媒体-70 还不错，它是实现可靠数据包传输的底线，适用于电子邮件和网页浏览-80 很差，只是基本连接，数据包传输不可靠-90 不可用，接近“ 背景噪声 noise floor ”注意：dBm 是对数尺度，-60 比 -30 要低 1000 倍。使它成为一个真的“扫描器”所以，看着上面输出显示的我的信号：-79。哇哦，感觉不大好呢。不过单看这个结果并没有太大帮助，它只能提供某个时间点的参考，只对 WiFi 网络适配器在特定物理空间的某一瞬间有效。一个连续的读数会更有用，借助于它，我们观察到信号随着树莓派的移动而变化。我可以再次修改 main 函数来实现这一点。var i *wifi.Interface for _, x := range interfaces { if x.Type == wifi.InterfaceTypeStation { // Loop through the interfaces, and assign the station // to var x // We could hardcode the station by name, or index, // or hardwareaddr, but this is more portable, if less efficient i = x break } } for { // c.StationInfo(x) returns a slice of all // the staton information about the interface info, err := c.StationInfo(i) if err != nil { fmt.Printf("Station err: %s\n", err) } for _, x := range info { fmt.Printf("Signal: %d\n", x.Signal) } time.Sleep(time.Second) } 首先，我命名了一个 wifi.Interface 类型的变量 i。因为它在循环的范围外，所以我可以用它来存储接口信息。循环内创建的任何变量在该循环的范围外都是不可访问的。然后，我可以把这个循环一分为二。第一个遍历了 c.Interfaces() 返回的接口切片，如果元素是一个 Station 类型，它就将其存储在先前创建的变量 i 中，并跳出循环。第二个循环是一个死循环，它将不断地运行，直到我按下 Ctrl + C 来结束程序。和之前一样，这个循环内部获取接口信息、检索基站信息，并打印出信号信息。然后它会休眠一秒钟，再次运行，反复打印信号信息，直到我退出为止。运行上面的程序后，我得到了下面的输出：[chris@marvin wifi-monitor]$ go run main.go Signal: -81 Signal: -81 Signal: -79 Signal: -81 哇哦，感觉不妙。绘制公寓信号分布图不管怎么说，知道这些信息总比不知道要好。让树莓派连接上显示器或者电子墨水屏，并接上电源，我就可以让它在公寓里移动，并绘制出信号死角的位置。剧透一下：由于房东的接入点在隔壁的公寓里，对我来说最大的死角是以公寓厨房的冰箱为顶点的一个圆锥体形状区域......这个冰箱与房东的公寓靠着一堵墙！我想如果用《龙与地下城》里的黑话来说，它就是一个“ 沉默之锥 Cone of Silence ”。或者至少是一个“ 糟糕的网络连接之锥 Cone of Poor Internet ”。总之，这段代码可以直接在树莓派上运行 go build -o wifi_scanner 来编译，得到的二进制文件 wifi_scanner 可以运行在其他同样的ARM 设备上。另外，它也可以在常规系统上用正确的 ARM 设备库进行编译。祝你扫描愉快！希望你的 WiFi 路由器不在你的冰箱后面！你可以在 我的 GitHub 存储库 中找到这个项目所用的代码。via: https://opensource.com/article/21/3/troubleshoot-wifi-go-raspberry-pi作者：Chris Collins 选题：lkxed 译者：lkxed 校对：turbokernel本文由 LCTT 原创编译，Linux中国 荣誉推出
这份可下载的指南充满了有用的教程，让 SRE 和系统管理员使用 Kubernetes 获得便利。Kubernetes 是容器编排的事实标准，在基础设施管理和应用开发方面已经迅速发展成为容器环境的主导。作为一个拥有庞大的爱好者和专业人士社区的开源平台，以及作为云原生计算基金会的一部分，Kubernetes 不仅成为一个强大而令人印象深刻的编排系统本身，而且它还促进了一个庞大的相关工具和服务的生态系统，使其更容易使用，并通过更强大和复杂的组件扩展其功能。在这本新的电子书《给 SRE 和系统管理员的 Kubernetes 指导》中，Jess Cherry（Ben Finkel 也有贡献）涵盖了一系列用于管理和整合 Kubernetes 的工具和服务。Cherry 和 Finkel 提供了一些有用的 入门 指南，包括 Kubernetes 和一些工具。他们甚至还分享了面试问题，以帮助读者为在这个快速增长的大规模生态系统中工作做好准备。了解 Kubernetes如果你刚开始接触 Kubernetes 和容器，Ben Finkel 的 《Kubernetes 入门》文如其题，也是一篇对你需要了解的相关概念的出色介绍。它也是一本轻量级的快速入门指南，用于设置和使用单节点集群进行测试。没有什么比亲身体验技术并直接进入学习更好的方法了。什么是 吊舱 Pod ？ 如何在集群上部署一个应用程序？ Ben 一一为你做了介绍。与集群交互的主要方式是 kubectl 命令，这是一种 CLI 工具，它提供了一种与管理集群本身的 API 服务器交互的适合方式。例如，你可以使用 kubectl get 来列出上述的吊舱和部署，但正如你对 Kubernetes 这样复杂的东西所期望的那样，它的 CLI 界面有很强的功能和灵活性。Jess Cherry 的《9 个系统管理员需要知道的 kubectl 命令》速查表是一个很好的介绍，是使用 kubectl 的入门好方法。同样，Cherry 的《给初学者的 Kubernetes 命令空间》也很好地解释了什么是命名空间以及它们在 Kubernetes 中的使用方式。简化 Kubernetes 的工作在一个复杂的系统中工作是很困难的，尤其是使用像 kubectl 这样强大而极简的 CLI 工具。幸运的是，在围绕 Kubernetes 的生态系统中，有许多工具可用于简化事情，使扩展服务和集群管理更容易。可用于在 Kubernetes 上部署和维护应用和服务的 kubectl 命令主要使用的是 YAML 和 JSON。然而，一旦你开始管理更多应用，用 YAML 的大型仓库这样做会变得既重复又乏味。一个好的解决方案是采用一个模板化的系统来处理你的部署。Helm 就是这样一个工具，被称为 “Kubernetes 的包管理器”，Helm 提供了一种方便的方式来打包和共享应用。Cherry 写了很多关于 Helm 的有用文章：创建有效的 《Helm 海图》和有用的《Helm 命令》。kubectl 也为你提供了很多关于集群本身的信息：上面运行的是什么，以及正在发生的事件。这些信息可以通过 kubectl 来查看和交互，但有时有一个更直观的 GUI 来进行交互是有帮助的。K9s 符合这两个世界的要求。虽然它仍然是一个终端应用，但它提供了视觉反馈和一种与集群交互的方式，而不需要长长的 kubectl 命令。Cherry 也写了一份很好的《k9s 使用入门》的指南。建立在 Kubernetes 的强大和灵活性之上的扩展幸运的是，尽管 Kubernetes 是复杂而强大的，但它惊人的灵活并且开源。它专注于其核心优势：容器编排，并允许围绕它的爱好者和专业人士的社区扩展其能力，以承担不同类型的工作负载。其中一个例子是 Knative，在 Kubernetes 之上提供组件，它为无服务器和事件驱动的服务提供工具，并利用 Kubernetes 的编排能力在容器中运行最小化的微服务。事实证明，这样做非常高效，既能提供在容器中开发小型、易于测试和维护的应用的好处，又能提供仅在需要时运行这些应用的成本优势，可以在特定事件中被触发，但在其他时候处于休眠。在这本电子书中，Cherry 介绍了 Knative 和它的事件系统，以及为什么值得自己研究使用 Knative。有一个完整的世界可以探索通过 Jess Cherry 和 Ben Finkel 的这本新的电子书，可以开始了解 Kubernetes 和围绕它的生态系统。除了上述主题外，还有一些关于有用的 Kubernetes 扩展和第三方工具的文章。via: https://opensource.com/article/21/6/kubernetes-ebook作者：Chris Collins 选题：lujun9972 译者：geekpi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
将 Kubernetes 安装在多个树莓派上，实现自己的“家庭私有云”容器服务。Kubernetes 从一开始就被设计为云原生的企业级容器编排系统。它已经成长为事实上的云容器平台，并由于接受了容器原生虚拟化和无服务器计算等新技术而继续发展。从微型的边缘计算到大规模的容器环境，无论是公有云还是私有云环境，Kubernetes 都可以管理其中的容器。它是“家庭私有云”项目的理想选择，既提供了强大的容器编排，又让你有机会了解一项这样的技术 —— 它的需求如此之大，与云计算结合得如此彻底，以至于它的名字几乎就是“云计算”的代名词。没有什么比 Kubernetes 更懂“云”，也没有什么能比树莓派更合适“集群起来”！在廉价的树莓派硬件上运行本地的 Kubernetes 集群是获得在真正的云技术巨头上进行管理和开发的经验的好方法。在树莓派上安装 Kubernetes 集群本练习将在三个或更多运行 Ubuntu 20.04 的树莓派 4 上安装 Kubernetes 1.18.2 集群。Ubuntu 20.04（Focal Fossa）提供了针对 64 位 ARM（ARM64）的树莓派镜像（64 位内核和用户空间）。由于目标是使用这些树莓派来运行 Kubernetes 集群，因此运行 AArch64 容器镜像的能力非常重要：很难找到 32 位的通用软件镜像乃至于标准基础镜像。借助 Ubuntu 20.04 的 ARM64 镜像，可以让你在 Kubernetes 上使用 64 位容器镜像。AArch64 vs. ARM64；32 位 vs. 64 位；ARM vs. x86请注意，AArch64 和 ARM64 实际上是同一种东西。不同的名称源于它们在不同社区中的使用。许多容器镜像都标为 AArch64，并能在标为 ARM64 的系统上正常运行。采用 AArch64/ARM64 架构的系统也能够运行 32 位的 ARM 镜像，但反之则不然：32 位的 ARM 系统无法运行 64 位的容器镜像。这就是 Ubuntu 20.04 ARM64 镜像如此有用的原因。这里不会太深入地解释不同的架构类型，值得注意的是，ARM64/AArch64 和 x86\_64 架构是不同的，运行在 64 位 ARM 架构上的 Kubernetes 节点无法运行为 x86\_64 构建的容器镜像。在实践中，你会发现有些镜像没有为两种架构构建，这些镜像可能无法在你的集群中使用。你还需要在基于 Arch64 的系统上构建自己的镜像，或者跳过一些限制以让你的常规的 x86\_64 系统构建 Arch64 镜像。在“家庭私有云”项目的后续文章中，我将介绍如何在常规系统上构建 AArch64 镜像。为了达到两全其美的效果，在本教程中设置好 Kubernetes 集群后，你可以在以后向其中添加 x86\_64 节点。你可以通过使用 Kubernetes 的 污点 taint 和 容忍 toleration 能力，由 Kubernetes 的调度器将给定架构的镜像调度到相应的节点上运行。关于架构和镜像的内容就不多说了。是时候安装 Kubernetes 了，开始吧！前置需求这个练习的要求很低。你将需要：三台（或更多）树莓派 4（最好是 4GB 内存的型号）。在全部树莓派上安装 Ubuntu 20.04 ARM64。为了简化初始设置，请阅读《修改磁盘镜像来创建基于树莓派的家庭实验室》，在将 Ubuntu 镜像写入 SD 卡并安装在树莓派上之前，添加一个用户和 SSH 授权密钥（authorized_keys）。配置主机在 Ubuntu 被安装在树莓派上，并且可以通过 SSH 访问后，你需要在安装 Kubernetes 之前做一些修改。安装和配置 Docker截至目前，Ubuntu 20.04 在 base 软件库中提供了最新版本的 Docker，即 v19.03，可以直接使用 apt 命令安装它。请注意，包名是 docker.io。请在所有的树莓派上安装 Docker：# 安装 docker.io 软件包 $ sudo apt install -y docker.io 安装好软件包后，你需要做一些修改来启用 cgroup（控制组）。cgroup 允许 Linux 内核限制和隔离资源。实际上，这可以让 Kubernetes 更好地管理其运行的容器所使用的资源，并通过让容器彼此隔离来增加安全性。在对所有树莓派进行以下修改之前，请检查 docker info 的输出：# 检查 `docker info` # 省略了某些输出 $ sudo docker info (...) Cgroup Driver: cgroups (...) WARNING: No memory limit support WARNING: No swap limit support WARNING: No kernel memory limit support WARNING: No kernel memory TCP limit support WARNING: No oom kill disable support 上面的输出突出显示了需要修改的部分：cgroup 驱动和限制支持。首先，将 Docker 使用的默认 cgroup 驱动从 cgroups 改为 systemd，让 systemd 充当 cgroup 管理器，确保只有一个 cgroup 管理器在使用。这有助于系统的稳定性，这也是 Kubernetes 所推荐的。要做到这一点，请创建 /etc/docker/daemon.json 文件或将内容替换为：# 创建或替换 /etc/docker/daemon.json 以启用 cgroup 的 systemd 驱动 $ sudo cat > /etc/docker/daemon.json <<EOF { "exec-opts": ["native.cgroupdriver=systemd"], "log-driver": "json-file", "log-opts": { "max-size": "100m" }, "storage-driver": "overlay2" } EOF 启用 cgroup 限制支持接下来，启用限制支持，如上面的 docker info 输出中的警告所示。你需要修改内核命令行以在引导时启用这些选项。对于树莓派 4，将以下内容添加到 /boot/firmware/cmdline.txt 文件中：cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1 swapaccount=1 确保它们被添加到 cmdline.txt 文件的行末。这可以通过使用 sed 在一行中完成。# 将 cgroup 和交换选项添加到内核命令行中 # 请注意 "cgroup_enable=cpuset" 前的空格，以便在该行的最后一个项目后添加一个空格 $ sudo sed -i '$ s/$/ cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1 swapaccount=1/' /boot/firmware/cmdline.txt sed 命令匹配该行的终止符（由第一个 $ 代表），用列出的选项代替它（它实际上是将选项附加到该行）。有了这些改变，Docker 和内核应该按照 Kubernetes 的需要配置好了。重新启动树莓派，当它们重新启动后，再次检查 docker info 的输出。现在，Cgroups driver 变成了 systemd，警告也消失了。允许 iptables 查看桥接流量根据文档，Kubernetes 需要配置 iptables 来查看桥接网络流量。你可以通过修改 sysctl 配置来实现。# 启用 net.bridge.bridge-nf-call-iptables 和 -iptables6 cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 EOF $ sudo sysctl --system 安装 Ubuntu 的 Kubernetes 包由于你使用的是 Ubuntu，你可以从 Kubernetes.io 的 apt 仓库中安装 Kubernetes 软件包。目前没有 Ubuntu 20.04（Focal）的仓库，但最近的 Ubuntu LTS 仓库 Ubuntu 18.04（Xenial） 中有 Kubernetes 1.18.2。最新的 Kubernetes 软件包可以从那里安装。将 Kubernetes 软件库添加到 Ubuntu 的源列表之中：# 添加 packages.cloud.google.com 的 atp 密钥 $ curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add - # 添加 Kubernetes 软件库 cat <<EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list deb https://apt.kubernetes.io/ kubernetes-xenial main EOF 当 Kubernetes 添加了 Ubuntu 20.04（Focal）仓库时 —— 也许是在下一个 Kubernetes 版本发布时 —— 请确保切换到它。将仓库添加到源列表后，安装三个必要的 Kubernetes 包：kubelet、kubeadm 和 kubectl：# 更新 apt 缓存并安装 kubelet、kubeadm kubectl # （输出略） $ sudo apt update && sudo apt install -y kubelet kubeadm kubectl 最后，使用 apt-mark hold 命令禁用这三个包的定期更新。升级到 Kubernetes 需要比一般的更新过程更多的手工操作，需要人工关注。# 禁止（标记为保持）Kubernetes 软件包的更新 $ sudo apt-mark hold kubelet kubeadm kubectl kubelet set on hold. kubeadm set on hold. kubectl set on hold. 主机配置就到这里了! 现在你可以继续设置 Kubernetes 本身了。创建 Kubernetes 集群在安装了 Kubernetes 软件包之后，你现在可以继续创建集群了。在开始之前，你需要做一些决定。首先，其中一个树莓派需要被指定为控制平面节点（即主节点）。其余的节点将被指定为计算节点。你还需要选择一个 CIDR（无类别域间路由）地址用于 Kubernetes 集群中的 Pod。在集群创建过程中设置 pod-network-cidr 可以确保设置了 podCIDR 值，它以后可以被 容器网络接口 Container Network Interface （CNI）加载项使用。本练习使用的是 FlannelCNI。你选择的 CIDR 不应该与你的家庭网络中当前使用的任何 CIDR 重叠，也不应该与你的路由器或 DHCP 服务器管理的 CIDR 重叠。确保使用一个比你预期需要的更大的子网：总是有比你最初计划的更多的 Pod！在这个例子中，我将使用 CIDR 地址 10.244.0.0/16，但你可以选择一个适合你的。有了这些决定，你就可以初始化控制平面节点了。用 SSH 或其他方式登录到你为控制平面指定的节点。初始化控制平面Kubernetes 使用一个引导令牌来验证被加入集群的节点。当初始化控制平面节点时，需要将此令牌传递给 kubeadm init 命令。用 kubeadm token generate 命令生成一个令牌：# 生成一个引导令牌来验证加入集群的节点 $ TOKEN=$(sudo kubeadm token generate) $ echo $TOKEN d584xg.xupvwv7wllcpmwjy 现在你可以使用 kubeadm init 命令来初始化控制平面了：# 初始化控制平面 #（输出略） $ sudo kubeadm init --token=${TOKEN} --kubernetes-version=v1.18.2 --pod-network-cidr=10.244.0.0/16 如果一切顺利，你应该在输出的最后看到类似这样的东西：Your Kubernetes control-plane has initialized successfully! To start using your cluster, you need to run the following as a regular user: mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config You should now deploy a pod network to the cluster. Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at: https://kubernetes.io/docs/concepts/cluster-administration/addons/ Then you can join any number of worker nodes by running the following on each as root: kubeadm join 192.168.2.114:6443 --token zqqoy7.9oi8dpkfmqkop2p5 \ --discovery-token-ca-cert-hash sha256:71270ea137214422221319c1bdb9ba6d4b76abfa2506753703ed654a90c4982b 注意两点：第一，Kubernetes 的 kubectl 连接信息已经写入到 /etc/kubernetes/admin.conf。这个 kubeconfig 文件可以复制到用户的 ~/.kube/config 中，可以是主节点上的 root 用户或普通用户，也可以是远程机器。这样你就可以用 kubectl 命令来控制你的集群。其次，输出中以 kubernetes join 开头的最后一行是你可以运行的命令，你可以运行这些命令加入更多的节点到集群中。将新的 kubeconfig 复制到你的用户可以使用的地方后，你可以用 kubectl get nodes 命令来验证控制平面是否已经安装：# 显示 Kubernetes 集群中的节点 # 你的节点名称会有所不同 $ kubectl get nodes NAME STATUS ROLES AGE VERSION elderberry Ready master 7m32s v1.18.2 安装 CNI 加载项CNI 加载项负责 Pod 网络的配置和清理。如前所述，这个练习使用的是 Flannel CNI 加载项，在已经设置好 podCIDR 值的情况下，你只需下载 Flannel YAML 并使用 kubectl apply 将其安装到集群中。这可以用 kubectl apply -f - 从标准输入中获取数据，用一行命令完成。这将创建管理 Pod 网络所需的 ClusterRoles、ServiceAccounts 和 DaemonSets 等。下载并应用 Flannel YAML 数据到集群中：# 下载 Flannel YAML 数据并应用它 # （输出略） $ curl -sSL https://raw.githubusercontent.com/coreos/flannel/v0.12.0/Documentation/kube-flannel.yml | kubectl apply -f - 将计算节点加入到集群中有了 CNI 加载项，现在是时候将计算节点添加到集群中了。加入计算节点只需运行 kube init 命令末尾提供的 kubeadm join 命令来初始化控制平面节点。对于你想加入集群的其他树莓派，登录主机，运行命令即可：# 加入节点到集群，你的令牌和 ca-cert-hash 应各有不同 $ sudo kubeadm join 192.168.2.114:6443 --token zqqoy7.9oi8dpkfmqkop2p5 \ --discovery-token-ca-cert-hash sha256:71270ea137214422221319c1bdb9ba6d4b76abfa2506753703ed654a90c4982b 一旦你完成了每个节点的加入，你应该能够在 kubectl get nodes 的输出中看到新节点：# 显示 Kubernetes 集群中的节点 # 你的节点名称会有所不同 $ kubectl get nodes NAME STATUS ROLES AGE VERSION elderberry Ready master 7m32s v1.18.2 gooseberry Ready <none> 2m39s v1.18.2 huckleberry Ready <none> 17s v1.18.2 验证集群此时，你已经拥有了一个完全正常工作的 Kubernetes 集群。你可以运行 Pod、创建部署和作业等。你可以使用服务从集群中的任何一个节点访问集群中运行的应用程序。你可以通过 NodePort 服务或入口控制器实现外部访问。要验证集群正在运行，请创建一个新的命名空间、部署和服务，并检查在部署中运行的 Pod 是否按预期响应。此部署使用 quay.io/clcollins/kube-verify:01 镜像，这是一个监听请求的 Nginx 容器（实际上，与文章《使用 Cloud-init 将节点添加到你的私有云》中使用的镜像相同）。你可以在这里查看镜像的容器文件。为部署创建一个名为 kube-verify 的命名空间：# 创建一个新的命名空间 $ kubectl create namespace kube-verify # 列出命名空间 $ kubectl get namespaces NAME STATUS AGE default Active 63m kube-node-lease Active 63m kube-public Active 63m kube-system Active 63m kube-verify Active 19s 现在，在新的命名空间创建一个部署：# 创建一个新的部署 $ cat <<EOF | kubectl create -f - apiVersion: apps/v1 kind: Deployment metadata: name: kube-verify namespace: kube-verify labels: app: kube-verify spec: replicas: 3 selector: matchLabels: app: kube-verify template: metadata: labels: app: kube-verify spec: containers: - name: nginx image: quay.io/clcollins/kube-verify:01 ports: - containerPort: 8080 EOF deployment.apps/kube-verify created Kubernetes 现在将开始创建部署，它由三个 Pod 组成，每个 Pod 都运行 quay.io/clcollins/kube-verify:01 镜像。一分钟左右后，新的 Pod 应该运行了，你可以用 kubectl get all -n kube-verify 来查看它们，以列出新命名空间中创建的所有资源：# 检查由该部署创建的资源 $ kubectl get all -n kube-verify NAME READY STATUS RESTARTS AGE pod/kube-verify-5f976b5474-25p5r 0/1 Running 0 46s pod/kube-verify-5f976b5474-sc7zd 1/1 Running 0 46s pod/kube-verify-5f976b5474-tvl7w 1/1 Running 0 46s NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/kube-verify 3/3 3 3 47s NAME DESIRED CURRENT READY AGE replicaset.apps/kube-verify-5f976b5474 3 3 3 47s 你可以看到新的部署、由部署创建的复制子集，以及由复制子集创建的三个 Pod，以满足部署中的 replicas: 3 的要求。你可以看到 Kubernetes 内部工作正常。现在，创建一个服务来暴露在三个 Pod 中运行的 Nginx “应用程序”（在本例中是“欢迎”页面）。这将作为一个单一端点，你可以通过它连接到 Pod：# 为该部署创建服务 $ cat <<EOF | kubectl create -f - apiVersion: v1 kind: Service metadata: name: kube-verify namespace: kube-verify spec: selector: app: kube-verify ports: - protocol: TCP port: 80 targetPort: 8080 EOF service/kube-verify created 创建服务后，你可以对其进行检查并获取新服务的 IP 地址：# 检查新服务 $ kubectl get -n kube-verify service/kube-verify NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kube-verify ClusterIP 10.98.188.200 <none> 80/TCP 30s 你可以看到 kube-verify 服务已经被分配了一个 ClusterIP（仅对集群内部）10.98.188.200。这个 IP 可以从你的任何节点到达，但不能从集群外部到达。你可以通过在这个 IP 上连接到部署内部的容器来验证它们是否在工作：# 使用 curl 连接到 ClusterIP： # （简洁期间，输出有删节） $ curl 10.98.188.200 成功了！你的服务正在运行，容器内的 Nginx 正在响应你的请求。你的服务正在运行，容器内的 Nginx 正在响应你的请求。此时，你的树莓派上有一个正在运行的 Kubernetes 集群，安装了一个 CNI 加载项（Flannel），并有一个运行 Nginx Web 服务器的测试部署和服务。在大型公有云中，Kubernetes 有不同的入口控制器来与不同的解决方案交互，比如最近报道的 Skipper 项目。同样，私有云也有与硬件负载均衡器设备（如 F5 Networks 的负载均衡器）交互的入口控制器，或用于处理进入节点的流量的 Nginx 和 HAProxy 控制器。在以后的文章中，我将通过安装自己的入口控制器来解决将集群中的服务暴露给外界的问题。我还将研究动态存储供应器和 StorageClasses，以便为应用程序分配持久性存储，包括利用你在上一篇文章《将树莓派家庭实验室变成网络文件系统》中设置的 NFS 服务器来为你的 Pod 创建按需存储。去吧，Kubernetes“Kubernetes”（κυβερνήτης）在希腊语中是飞行员的意思 —— 但这是否意味着驾驶船只以及引导船只的人？诶，不是。“Kubernan”（κυβερνάω）是希腊语“驾驶”或“引导”的意思，因此，去吧，Kubernan，如果你在会议上或其它什么活动上看到我，请试着给我一个动词或名词的通行证，以另一种语言 —— 我不会说的语言。免责声明：如前所述，我不会读也不会讲希腊语，尤其是古希腊语，所以我选择相信我在网上读到的东西。你知道那是怎么一回事。我对此有所保留，放过我吧，因为我没有开“对我来说都是希腊语”这种玩笑。然而，只是提一下，虽然我可以开玩笑，但是实际上没有，所以我要么偷偷摸摸，要么聪明，要么两者兼而有之。或者，两者都不是。我并没有说这是个好笑话。所以，去吧，像专业人员一样在你的家庭私有云中用自己的 Kubernetes 容器服务来试运行你的容器吧！当你越来越得心应手时，你可以修改你的 Kubernetes 集群，尝试不同的选项，比如前面提到的入口控制器和用于持久卷的动态 StorageClasses。这种持续学习是 DevOps 的核心，持续集成和新服务交付反映了敏捷方法论，当我们学会了处理云实现的大规模扩容，并发现我们的传统做法无法跟上步伐时，我们就接受了这两种方法论。你看，技术、策略、哲学、一小段希腊语和一个可怕的原始笑话，都汇聚在一篇文章当中。via: https://opensource.com/article/20/6/kubernetes-raspberry-pi作者：Chris Collins 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
把你的树莓派家庭实验室变成一个网络文件系统
使用 NFS 服务器将共享文件系统添加到你的家庭实验室。共享文件系统是为家庭实验室增加通用性和功能性的好方法。在实验室中为客户端共享一个集中的文件系统，使得组织数据、进行备份和共享数据变得相当容易。这对于在多个服务器上进行负载均衡的 Web 应用和 Kubernetes 使用的持久化卷来说，尤其有用，因为它允许在任何数量的节点上用持久化数据来轮转 Pod。无论你的家庭实验室是由普通计算机、多余的企业服务器，还是树莓派或其他单板计算机（SBC）组成，共享文件系统都是一种有用的资产，而网络文件系统（NFS）服务器是创建共享文件系统的好方法。我之前写过关于建立“家庭私有云”的文章，这是一个由树莓派或其他 SBC 组成的家庭实验室，也许还有其他一些消费类硬件或台式 PC。NFS 服务器是这些组件之间共享数据的理想方式。由于大多数 SBC 的操作系统是通过 SD 卡运行的，所以存在一些挑战。尤其是在用作计算机的操作系统磁盘时，SD 卡的故障率会增加，它们并不是用来不断地读写的。你实际需要的是一个真正的硬盘：它们通常比 SD 卡的每 GB 价格便宜，特别是对于较大的磁盘，而且它们不太可能持续发生故障。树莓派 4 现在带有 USB 3.0 接口，而 USB 3.0 硬盘无处不在，价格也很实惠。这是一个完美的搭配。在这个项目中，我将使用一个 2TB 的 USB 3.0 外置硬盘插入到运行 NFS 服务器的树莓派 4 中。安装 NFS 服务器软件我在树莓派上运行 Fedora 服务器，但这个项目也可以在其他发行版上运行。要在 Fedora 上运行 NFS 服务器，你需要 nfs-utils 包，幸运的是它已经安装好了（至少在 Fedora 31 中是这样）。如果你打算运行 NFSv3 服务，你还需要 rpcbind 包，但它不是 NFSv4 的严格要求。如果你的系统中还没有这些软件包，请使用 dnf 命令安装它们。# 安装 nfs-utils 和 rpcbind $ sudo dnf install nfs-utils rpcbind Raspbian 是另一个与树莓派一起使用的流行操作系统，设置几乎完全相同。软件包名称不同而已，但这是唯一的主要区别。要在运行 Raspbian 的系统上安装 NFS 服务器，你需要以下软件包。nfs-common：这些文件是 NFS 服务器和客户端的通用文件。nfs-kernel-server：主要的 NFS 服务器软件包。Raspbian 使用 apt-get 来管理软件包（而不是像 Fedora 那样使用 dnf），所以用它来安装软件包。# 对于 Raspbian 系统，使用 apt-get 来安装 NFS 软件包 $ sudo apt-get install nfs-common nfs-kernel-server 准备一个 USB 硬盘作为存储设备正如我上面提到的，USB 硬盘是为树莓派或其他 SBC 提供存储的好选择，尤其是用于操作系统磁盘镜像的 SD 卡并不适合这个用途。对于家庭私有云，你可以使用廉价的 USB 3.0 硬盘进行大规模存储。插入磁盘，使用 fdisk 找出分配给它的设备 ID，就可以使用它工作了。# 使用 fdisk 找到你的硬盘 # 无关的硬盘信息已经省略 $ sudo fdisk -l Disk /dev/sda: 1.84 TiB, 2000398933504 bytes, 3907029167 sectors Disk model: BUP Slim BK Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0xe3345ae9 Device Boot Start End Sectors Size Id Type /dev/sda1 2048 3907028991 3907026944 1.8T 83 Linux 为了清楚起见，在上面的例子输出中，除了我感兴趣的那个磁盘，我省略了所有其它磁盘的信息。你可以看到我想使用的 USB 磁盘被分配了设备 /dev/sda，你可以看到一些关于型号的信息（Disk model: BUP Slim BK），这有助于我识别正确的磁盘。该磁盘已经有了一个分区，它的大小也证实了它就是我要找的磁盘。注意：请确保正确识别你的设备的磁盘和分区。它可能与上面的例子不同。在驱动器上创建的每个分区都有一个特殊的通用唯一标识符（UUID）。计算机使用 UUID 来确保它使用 /etc/fstab 配置文件将正确的分区挂载到正确的位置。你可以使用 blkid 命令检索分区的 UUID。# 获取该分区的块设备属性 # 确保你使用了合适的分区，它应该有所不同。 $ sudo blkid /dev/sda1 /dev/sda1: LABEL="backup" UUID="bd44867c-447c-4f85-8dbf-dc6b9bc65c91" TYPE="xfs" PARTUUID="e3345ae9-01" 在这里，/dev/sda1 的 UUID 是 bd44867c-447c-4f85-8dbf-dc6b9bc65c91。你的 UUID 会有所不同，所以要记下来。配置树莓派在启动时挂载这个磁盘，然后挂载它现在你已经确定了要使用的磁盘和分区，你需要告诉计算机如何挂载它，每次启动时都要这样做。现在就去挂载它。因为这是一个 USB 磁盘，可能会被拔掉，所以你还要配置树莓派在启动时如果磁盘没有插入或有其它不可用情况时不要等待。在 Linux 中，通过将分区添加到 /etc/fstab 配置文件中，包括你希望它被挂载的位置和一些参数来告诉计算机如何处理它。这个例子将把分区挂载到 /srv/nfs，所以先创建这个路径：# 创建该磁盘分区的挂载点 $ sudo mkdir -p /srv/nfs 接下来，使用以下语法格式修改 /etc/fstab 文件：<disk id> <mountpoint> <filesystem type> <options> <fs_freq> <fs_passno> 使用你之前确定的 UUID 作为磁盘 ID。正如我在上一步提到的，挂载点是 /srv/nfs。对于文件系统类型，通常最好选择其实际的文件系统，但是因为这是一个 USB 磁盘，所以使用 auto。对于选项值，使用 nosuid,nodev,nofail。关于手册页的一个旁白其实，有很多可能的选项，手册页（man）是查看它们的最好方法。查看 fstab 的手册页是一个很好的开始。# 打开 fstab 的手册页 $ man fstab 这将打开与 fstab 命令相关的手册/文档。在手册页中，每个选项都被分解成了不同的内容，以显示它的作用和常用的选择。例如，“第四个字段（fs\_mntopts）”给出了该字段中可用选项的一些基本信息，并引导你到 man 8 mount 中获取 mount 选项更深入的描述。这是有道理的，因为 /etc/fstab 文件，本质上是告诉计算机如何自动挂载磁盘，就像你手动使用 mount 命令一样。你可以从 mount 的手册页中获得更多关于你将使用的选项的信息。数字 8 表示手册页的章节。在这里，第 8 章节是系统管理工具和守护进程。你可以从 man 的手册页中得到标准章节的列表。回到挂载磁盘，让我们看看 man 8 mount。# 打开第 8 章节的 mount 手册页 $ man 8 mount 在这个手册页中，你可以查看上面列出的选项的作用。nosuid：不理会 suid/guid 位。不允许放在 U 盘上的任何文件以 root 身份执行。这是一个良好的安全实践。nodev：不识别文件系统中的字符或块特殊设备，即不理会在 U 盘上的任何设备节点。另一个良好的安全实践。nofail：如果设备不存在，不要记录任何错误。这是一个 U 盘，可能没有插入，所以在这种情况下，它将被忽略。回到你正在添加到 /etc/fstab 文件的那一行，最后还有两个选项：fs_freq 和 fs_passno。它们的值与一些过时的选项有关，大多数现代系统对这两个选项都只用 0，特别是对 USB 磁盘上的文件系统而言。fs_freq 的值与 dump 命令和文件系统的转储有关。fs_passno 的值定义了启动时要 fsck 的文件系统及其顺序，如果设置了这个值，通常根分区是 1，其他文件系统是 2，将该值设置为 0 以跳过在该分区上使用 fsck。在你喜欢的编辑器中，打开 /etc/fstab 文件，添加 U 盘上分区的条目，将这里的值替换成前面步骤中得到的值。# With sudo, or as root, add the partition info to the /etc/fstab file UUID="bd44867c-447c-4f85-8dbf-dc6b9bc65c91" /srv/nfs auto nosuid,nodev,nofail,noatime 0 0 启用并启动 NFS 服务器安装好软件包，并将分区添加到你的 /etc/fstab 文件中，现在你可以开始启动 NFS 服务器了。在 Fedora 系统中，你需要启用和启动两个服务：rpcbind 和 nfs-server。使用 systemctl 命令来完成这项工作。# 启动 NFS 服务器和 rpcbind $ sudo systemctl enable rpcbind.service $ sudo systemctl enable nfs-server.service $ sudo systemctl start rpcbind.service $ sudo systemctl start nfs-server.service 在 Raspbian 或其他基于 Debian 的发行版上，你只需要使用 systemctl 命令启用并启动 nfs-kernel-server 服务即可，方法同上。RPCBindrpcbind 工具用于将远程过程调用（RPC）服务映射到其监听的端口。根据 rpcbind 手册页：“当一个 RPC 服务启动时，它会告诉 rpcbind 它正在监听的地址，以及它准备服务的 RPC 程序号。当客户机想对给定的程序号进行 RPC 调用时，它首先与服务器机器上的 rpcbind 联系，以确定 RPC 请求应该发送到哪里的地址。”在 NFS 服务器这个案例中，rpcbind 会将 NFS 的协议号映射到 NFS 服务器监听的端口上。但是，NFSv4 不需要使用 rpcbind。如果你只使用 NFSv4 （通过从配置中删除版本 2 和版本 3），则不需要使用 rpcbind。我把它放在这里是为了向后兼容 NFSv3。导出挂载的文件系统NFS 服务器根据另一个配置文件 /etc/exports 来决定与哪些远程客户端共享（导出）哪些文件系统。这个文件只是一个 IP（或子网）与要共享的文件系统的映射，以及一些选项（只读或读写、root 去除等）。该文件的格式是：<目录> <主机>(选项) 在这个例子中，你将导出挂载到 /srv/nfs 的分区。这是“目录”部分。第二部分，主机，包括你要导出这个分区的主机。这些主机可以是单个主机：使用具有完全限定域名（FQDN）或主机名、主机的 IP 地址来指定；也可以是一组主机：使用通配符字符来匹配域（如 *.example.org）、IP 网络（如无类域间路由 CIDR 标识）或网组表示。第三部分包括应用于该导出的选项。ro/rw：将文件系统导出为只读或读写。wdelay：如果即将进行另一次写入，则推迟对磁盘的写入，以提高性能（如果你使用的是固态 USB 磁盘，这可能没有那么有用）root_squash：防止客户机上的任何 root 用户在主机上有 root 权限，并将 root UID 设置为 nfsnobody 作为安全防范措施。测试导出你挂载在 /srv/nfs 处的分区到一个客户端 —— 例如，一台笔记本电脑。确定你的客户机的 IP 地址（我的笔记本是 192.168.2.64，但你的可能会不同）。你可以把它共享到一个大的子网，但为了测试，请限制在单个 IP 地址上。这个 IP 的 CIDR 标识是 192.168.2.64/32，/32 子网代表一个 IP。使用你喜欢的编辑器编辑 /etc/exports 文件，写上你的目录、主机 CIDR 以及 rw 和 root_squash 选项。# 像这样编辑你的 /etc/exports 文件，替换为你的系统上的信息 /srv/nfs 192.168.2.64/32(rw,root_squash) 注：如果你从另一个地方复制了 /etc/exports 文件，或者用副本覆盖了原文件，你可能需要恢复该文件的 SELinux 上下文。你可以使用 restorecon 命令来恢复。# 恢复 /etc/exports 文件的 SELinux 上下文 $ sudo restorecon /etc/exports 完成后，重新启动 NFS 服务器，以接收对 /etc/exports 文件的更改。# 重新启动 NFS 服务器 $ sudo systemctl restart nfs-server.service 给 NFS 服务打开防火墙有些系统，默认不运行防火墙服务。比如 Raspbian，默认是开放 iptables 规则，不同服务打开的端口在机器外部立即就可以使用。相比之下，Fedora 服务器默认运行的是 firewalld 服务，所以你必须为 NFS 服务器（以及 rpcbind，如果你将使用 NFSv3）打开端口。你可以通过 firewall-cmd 命令来实现。检查 firewalld 使用的区域并获取默认区域。对于 Fedora 服务器，这是 FedoraServer 区域。# 列出区域 # 出于简洁省略了部分输出 $ sudo firewall-cmd --list-all-zones # R获取默认区域信息 # 记下默认区域 $ sudo firewall-cmd --get-default-zone # 永久加入 nfs 服务到允许端口列表 $ sudo firewall-cmd --add-service=nfs --permanent # 对于 NFSv3，我们需要再加一些端口： nfsv3、 rpc-mountd、 rpc-bind $ sudo firewall-cmd --add-service=(nfs3,mountd,rpc-bind) # 查看默认区域的服务，以你的系统中使用的默认区域相应替换 $ sudo firewall-cmd --list-services --zone=FedoraServer # 如果一切正常，重载 firewalld $ sudo firewall-cmd --reload 就这样，你已经成功地将 NFS 服务器与你挂载的 U 盘分区配置在一起，并将其导出到你的测试系统中进行共享。现在你可以在你添加到导出列表的系统上测试挂载它。测试 NFS 导出首先，从 NFS 服务器上，在 /srv/nfs 目录下创建一个文件来读取。# 创建一个测试文件以共享 echo "Can you see this?" >> /srv/nfs/nfs_test 现在，在你添加到导出列表中的客户端系统上，首先确保 NFS 客户端包已经安装好。在 Fedora 系统上，它是 nfs-utils 包，可以用 dnf 安装。Raspbian 系统有 libnfs-utils 包，可以用 apt-get 安装。安装 NFS 客户端包：# 用 dnf 安装 nfs-utils 软件包 $ sudo dnf install nfs-utils 一旦安装了客户端包，你就可以测试 NFS 的导出了。同样在客户端，使用带有 NFS 服务器 IP 和导出路径的 mount 命令，并将其挂载到客户端的一个位置，在这个测试中是 /mnt 目录。在这个例子中，我的 NFS 服务器的 IP 是 192.168.2.109，但你的可能会有所不同。# 挂载 NFS 服务器的输出到客户端主机 # 确保替换为你的主机的相应信息 $ sudo mount 192.168.2.109:/srv/nfs /mnt # 查看 nfs_test 文件是不是可见 $ cat /mnt/nfs_test Can you see this? 成功了！你现在已经有了一个可以工作的 NFS 服务器，可以与多个主机共享文件，允许多个读/写访问，并为你的数据提供集中存储和备份。家庭实验室的共享存储有很多选择，但 NFS 是一种古老的、高效的、可以添加到你的“家庭私有云”家庭实验室中的好选择。本系列未来的文章将扩展如何在客户端上自动挂载 NFS 共享，以及如何将 NFS 作为 Kubernetes 持久卷的存储类。via: https://opensource.com/article/20/5/nfs-raspberry-pi作者：Chris Collins 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
像主流云提供商的处理方式一样，在家中添加机器到你的私有云。Cloud-init 是一种广泛使用的行业标准方法，用于初始化云实例。云提供商使用 Cloud-init 来定制实例的网络配置、实例信息，甚至用户提供的配置指令。它也是一个可以在你的“家庭私有云”中使用的很好的工具，可以为你的家庭实验室的虚拟机和物理机的初始设置和配置添加一点自动化 —— 并了解更多关于大型云提供商是如何工作的信息。关于更多的细节和背景，请看我之前的文章《在你的树莓派家庭实验室中使用 Cloud-init》。运行 Cloud-init 的 Linux 服务器的启动过程（Chris Collins，CC BY-SA 4.0）诚然，Cloud-init 对于为许多不同客户配置机器的云提供商来说，比对于由单个系统管理员运行的家庭实验室更有用，而且 Cloud-init 解决的许多问题对于家庭实验室来说可能有点多余。然而，设置它并了解它的工作原理是了解更多关于这种云技术的好方法，更不用说它是首次启动时配置设备的好方法。本教程使用 Cloud-init 的 NoCloud 数据源，它允许 Cloud-init 在传统的云提供商环境之外使用。本文将向你展示如何在客户端设备上安装 Cloud-init，并设置一个运行 Web 服务的容器来响应客户端的请求。你还将学习如何审查客户端从 Web 服务中请求的内容，并修改 Web 服务的容器，以提供基本的、静态的 Cloud-init 服务。在现有系统上设置 Cloud-initCloud-init 可能在新系统首次启动时最有用，它可以查询配置数据，并根据指令对系统进行定制。它可以包含在树莓派和单板计算机的磁盘镜像中，也可以添加到用于 配给 provision 虚拟机的镜像中。对于测试用途来说，无论是在现有系统上安装并运行 Cloud-init，还是安装一个新系统，然后设置 Cloud-init，都是很容易的。作为大多数云提供商使用的主要服务，大多数 Linux 发行版都支持 Cloud-init。在这个例子中，我将使用 Fedora 31 Server 来安装树莓派，但在 Raspbian、Ubuntu、CentOS 和大多数其他发行版上也可以用同样的方式来完成。安装并启用云计算初始服务在你想作为 Cloud-init 客户端的系统上，安装 Cloud-init 包。如果你使用的是 Fedora：# Install the cloud-init package dnf install -y cloud-init Cloud-init 实际上是四个不同的服务（至少在 systemd 下是这样），这些服务负责检索配置数据，并在启动过程的不同阶段进行配置更改，这使得可以做的事情更加灵活。虽然你不太可能直接与这些服务进行太多交互，但在你需要排除一些故障时，知道它们是什么还是很有用的。它们是：cloud-init-local.servicecloud-init.servicecloud-config.servicecloud-final.service启用所有四个服务：# Enable the four cloud-init services systemctl enable cloud-init-local.service systemctl enable cloud-init.service systemctl enable cloud-config.service systemctl enable cloud-final.service 配置数据源以查询启用服务后，请配置数据源，客户端将从该数据源查询配置数据。有许多数据源类型，而且大多数都是为特定的云提供商配置的。对于你的家庭实验室，请使用 NoCloud 数据源，（如上所述）它是为在没有云提供商的情况下使用 Cloud-init 而设计的。NoCloud 允许以多种方式包含配置信息：以内核参数中的键/值对，用于在启动时挂载的 CD（或虚拟机中的虚拟 CD）；包含在文件系统中的文件中；或者像本例中一样，通过 HTTP 从指定的 URL（“NoCloud Net” 选项）获取配置信息。数据源配置可以通过内核参数提供，也可以在 Cloud-init 配置文件 /etc/cloud/cloud.cfg 中进行设置。该配置文件对于使用自定义磁盘镜像设置 Cloud-init 或在现有主机上进行测试非常方便。Cloud-init 还会合并在 /etc/cloud/cloud.cfg.d/ 中找到的任何 *.cfg 文件中的配置数据，因此为了保持整洁，请在 /etc/cloud/cloud.cfg.d/10_datasource.cfg 中配置数据源。Cloud-init 可以通过使用以下语法从 seedfrom 键指向的 HTTP 数据源中读取数据。seedfrom: http://ip_address:port/ IP 地址和端口是你将在本文后面创建的 Web 服务。我使用了我的笔记本电脑的 IP 和 8080 端口。这也可以是 DNS 名称。创建 /etc/cloud/cloud.cfg.d/10_datasource.cfg 文件：# Add the datasource: # /etc/cloud/cloud.cfg.d/10_datasource.cfg # NOTE THE TRAILING SLASH HERE! datasource: NoCloud: seedfrom: http://ip_address:port/ 客户端设置就是这样。现在，重新启动客户端后，它将尝试从你在 seedfrom 键中输入的 URL 检索配置数据，并进行必要的任何配置更改。下一步是设置一个 Web 服务器来侦听客户端请求，以便你确定需要提供的服务。设置网络服务器以审查客户请求你可以使用 Podman 或其他容器编排工具（如 Docker 或 Kubernetes）快速创建和运行 Web 服务器。这个例子使用的是 Podman，但同样的命令也适用于 Docker。要开始，请使用 fedora:31 容器镜像并创建一个容器文件（对于 Docker 来说，这会是一个 Dockerfile）来安装和配置 Nginx。从该容器文件中，你可以构建一个自定义镜像，并在你希望提供 Cloud-init 服务的主机上运行它。创建一个包含以下内容的容器文件：FROM fedora:31 ENV NGINX_CONF_DIR "/etc/nginx/default.d" ENV NGINX_LOG_DIR "/var/log/nginx" ENV NGINX_CONF "/etc/nginx/nginx.conf" ENV WWW_DIR "/usr/share/nginx/html" # Install Nginx and clear the yum cache RUN dnf install -y nginx \ && dnf clean all \ && rm -rf /var/cache/yum # forward request and error logs to docker log collector RUN ln -sf /dev/stdout ${NGINX_LOG_DIR}/access.log \ && ln -sf /dev/stderr ${NGINX_LOG_DIR}/error.log # Listen on port 8080, so root privileges are not required for podman RUN sed -i -E 's/(listen)([[:space:]]*)(\[\:\:\]\:)?80;$/\1\2\38080 default_server;/' $NGINX_CONF EXPOSE 8080 # Allow Nginx PID to be managed by non-root user RUN sed -i '/user nginx;/d' $NGINX_CONF RUN sed -i 's/pid \/run\/nginx.pid;/pid \/tmp\/nginx.pid;/' $NGINX_CONF # Run as an unprivileged user USER 1001 CMD ["nginx", "-g", "daemon off;"] 注：本例中使用的容器文件和其他文件可以在本项目的 GitHub 仓库中找到。上面容器文件中最重要的部分是改变日志存储方式的部分（写到 STDOUT 而不是文件），这样你就可以在容器日志中看到进入该服务器的请求。其他的一些改变使你可以在没有 root 权限的情况下使用 Podman 运行容器，也可以在没有 root 权限的情况下运行容器中的进程。在 Web 服务器上的第一个测试并不提供任何 Cloud-init 数据；只是用它来查看 Cloud-init 客户端的请求。创建容器文件后，使用 Podman 构建并运行 Web 服务器镜像：# Build the container image $ podman build -f Containerfile -t cloud-init:01 . # Create a container from the new image, and run it # It will listen on port 8080 $ podman run --rm -p 8080:8080 -it cloud-init:01 这会运行一个容器，让你的终端连接到一个伪 TTY。一开始看起来什么都没有发生，但是对主机 8080 端口的请求会被路由到容器内的 Nginx 服务器，并且在终端窗口中会出现一条日志信息。这一点可以用主机上的 curl 命令进行测试。# Use curl to send an HTTP request to the Nginx container $ curl http://localhost:8080 运行该 curl 命令后，你应该会在终端窗口中看到类似这样的日志信息：127.0.0.1 - - [09/May/2020:19:25:10 +0000] "GET / HTTP/1.1" 200 5564 "-" "curl/7.66.0" "-" 现在，有趣的部分来了：重启 Cloud-init 客户端，并观察 Nginx 日志，看看当客户端启动时， Cloud-init 向 Web 服务器发出了什么请求。当客户端完成其启动过程时，你应该会看到类似的日志消息。2020/05/09 22:44:28 [error] 2#0: *4 open() "/usr/share/nginx/html/meta-data" failed (2: No such file or directory), client: 127.0.0.1, server: _, request: "GET /meta-data HTTP/1.1", host: "instance-data:8080" 127.0.0.1 - - [09/May/2020:22:44:28 +0000] "GET /meta-data HTTP/1.1" 404 3650 "-" "Cloud-Init/17.1" "-" 注：使用 Ctrl+C 停止正在运行的容器。你可以看到请求是针对 /meta-data 路径的，即 http://ip_address_of_the_webserver:8080/meta-data。这只是一个 GET 请求 —— Cloud-init 并没有向 Web 服务器发送任何数据。它只是盲目地从数据源 URL 中请求文件，所以要由数据源来识别主机的要求。这个简单的例子只是向任何客户端发送通用数据，但一个更大的家庭实验室应该需要更复杂的服务。在这里，Cloud-init 请求的是实例元数据信息。这个文件可以包含很多关于实例本身的信息，例如实例 ID、分配实例的主机名、云 ID，甚至网络信息。创建一个包含实例 ID 和主机名的基本元数据文件，并尝试将其提供给 Cloud-init 客户端。首先，创建一个可复制到容器镜像中的 meta-data 文件。instance-id: iid-local01 local-hostname: raspberry hostname: raspberry 实例 ID（instance-id）可以是任何东西。但是，如果你在 Cloud-init 运行后更改实例 ID，并且文件被送达客户端，就会触发 Cloud-init 再次运行。你可以使用这种机制来更新实例配置，但你应该意识到它是这种工作方式。local-hostname 和 hostname 键正如其名，它们会在 Cloud-init 运行时为客户端设置主机名信息。在容器文件中添加以下行以将 meta-data 文件复制到新镜像中。# Copy the meta-data file into the image for Nginx to serve it COPY meta-data ${WWW_DIR}/meta-data 现在，用元数据文件重建镜像（使用一个新的标签以方便故障排除），并用 Podman 创建并运行一个新的容器。# Build a new image named cloud-init:02 podman build -f Containerfile -t cloud-init:02 . # Run a new container with this new meta-data file podman run --rm -p 8080:8080 -it cloud-init:02 新容器运行后，重启 Cloud-init 客户端，再次观察 Nginx 日志。127.0.0.1 - - [09/May/2020:22:54:32 +0000] "GET /meta-data HTTP/1.1" 200 63 "-" "Cloud-Init/17.1" "-" 2020/05/09 22:54:32 [error] 2#0: *2 open() "/usr/share/nginx/html/user-data" failed (2: No such file or directory), client: 127.0.0.1, server: _, request: "GET /user-data HTTP/1.1", host: "instance-data:8080" 127.0.0.1 - - [09/May/2020:22:54:32 +0000] "GET /user-data HTTP/1.1" 404 3650 "-" "Cloud-Init/17.1" "-" 你看，这次 /meta-data 路径被提供给了客户端。成功了！然而，客户端接着在 /user-data 路径上寻找第二个文件。该文件包含实例所有者提供的配置数据，而不是来自云提供商的数据。对于一个家庭实验室来说，这两个都是你自己提供的。你可以使用许多 user-data 模块来配置你的实例。对于这个例子，只需使用 write_files 模块在客户端创建一些测试文件，并验证 Cloud-init 是否工作。创建一个包含以下内容的用户数据文件：#cloud-config # Create two files with example content using the write_files module write_files: - content: | "Does cloud-init work?" owner: root:root permissions: '0644' path: /srv/foo - content: | "IT SURE DOES!" owner: root:root permissions: '0644' path: /srv/bar 除了使用 Cloud-init 提供的 user-data 模块制作 YAML 文件外，你还可以将其制作成一个可执行脚本供 Cloud-init 运行。创建 user-data 文件后，在容器文件中添加以下行，以便在重建映像时将其复制到镜像中：# Copy the user-data file into the container image COPY user-data ${WWW_DIR}/user-data 重建镜像，并创建和运行一个新的容器，这次使用用户数据信息：# Build a new image named cloud-init:03 podman build -f Containerfile -t cloud-init:03 . # Run a new container with this new user-data file podman run --rm -p 8080:8080 -it cloud-init:03 现在，重启 Cloud-init 客户端，观察 Web 服务器上的 Nginx 日志：127.0.0.1 - - [09/May/2020:23:01:51 +0000] "GET /meta-data HTTP/1.1" 200 63 "-" "Cloud-Init/17.1" "-" 127.0.0.1 - - [09/May/2020:23:01:51 +0000] "GET /user-data HTTP/1.1" 200 298 "-" "Cloud-Init/17.1" "- 成功了！这一次，元数据和用户数据文件都被送到了 Cloud-init 客户端。验证 Cloud-init 已运行从上面的日志中，你知道 Cloud-init 在客户端主机上运行并请求元数据和用户数据文件，但它用它们做了什么？你可以在 write_files 部分验证 Cloud-init 是否写入了你在用户数据文件中添加的文件。在 Cloud-init 客户端上，检查 /srv/foo 和 /srv/bar 文件的内容：# cd /srv/ && ls bar foo # cat foo "Does cloud-init work?" # cat bar "IT SURE DOES!" 成功了！文件已经写好了，并且有你期望的内容。如上所述，还有很多其他模块可以用来配置主机。例如，用户数据文件可以配置成用 apt 添加包、复制 SSH 的 authorized_keys、创建用户和组、配置和运行配置管理工具等等。我在家里的私有云中使用它来复制我的 authorized_keys、创建一个本地用户和组，并设置 sudo 权限。你接下来可以做什么Cloud-init 在家庭实验室中很有用，尤其是专注于云技术的实验室。本文所演示的简单服务对于家庭实验室来说可能并不是超级有用，但现在你已经知道 Cloud-init 是如何工作的了，你可以继续创建一个动态服务，可以用自定义数据配置每台主机，让家里的私有云更类似于主流云提供商提供的服务。在数据源稍显复杂的情况下，将新的物理（或虚拟）机器添加到家中的私有云中，可以像插入它们并打开它们一样简单。via: https://opensource.com/article/20/5/create-simple-cloud-init-service-your-homelab作者：Chris Collins 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
了解了云行业的标准，该向你的家庭实验室自动添加新设备和用户了。Cloud-init（可以说）是一个标准，云提供商用它来为云实例提供初始化和配置数据。它最常用于新实例的首次启动，以自动完成网络设置、账户创建和 SSH 密钥安装等使新系统上线所需的任何事情，以便用户可以访问它。在之前的一篇文章《修改磁盘镜像来创建基于树莓派的家庭实验室》中，我展示了如何为像树莓派这样的单板计算机定制操作系统镜像以实现类似的目标。有了 Cloud-init，就不需要向镜像中添加自定义数据。一旦在镜像中启用了它，你的虚拟机、物理服务器，甚至是小小的树莓派都可以表现得像你自己的 “家庭私有云” 中的云计算实例。新机器只需插入、打开，就可以自动成为你的家庭实验室的一部分。说实话，Cloud-init 的设计并没有考虑到家庭实验室。正如我所提到的，你可以很容易地修改给定的一套系统磁盘镜像，以启用 SSH 访问并在第一次启动后对它们进行配置。Cloud-init 是为大规模的云提供商设计的，这些提供商需要容纳许多客户，维护一组小的镜像，并为这些客户提供访问实例的机制，而无需为每个客户定制一个镜像。拥有单个管理员的家庭实验室则不会面临同样的挑战。不过，Cloud-init 在家庭实验室中也不是没有可取之处。教育是我的家庭私有云项目的目标之一，而为你的家庭实验室设置 Cloud-init 是一个很好的方式，可以获得大大小小的云提供商大量使用的技术的经验。Cloud-init 也是其他初始配置选项的替代方案之一。与其为家庭实验室中的每台设备定制每个镜像、ISO 等，并在你要进行更改时面临繁琐的更新，不如直接启用 Cloud-init。这减少了技术债务 —— 还有什么比个人技术债务更糟糕的吗？最后，在你的家庭实验室中使用 Cloud-init 可以让你的私有云实例与你拥有的或将来可能拥有的任何公有云实例表现相同 —— 这是真正的混合云。关于 Cloud-init当为 Cloud-init 配置的实例启动并且服务开始运行时（实际上是 systemd 中的四个服务，以处理启动过程中的依赖关系），它会检查其配置中的数据源，以确定其运行在什么类型的云中。每个主要的云提供商都有一个数据源配置，告诉实例在哪里以及如何检索配置信息。然后，实例使用数据源信息检索云提供商提供的配置信息（如网络信息和实例识别信息）和客户提供的配置数据（如要复制的授权密钥、要创建的用户账户以及许多其他可能的任务）。检索数据后，Cloud-init 再对实例进行配置：设置网络、复制授权密钥等，最后完成启动过程。然后，远程用户就可以访问它，准备好使用 Ansible 或 Puppet 等工具进行进一步的配置，或者准备好接收工作负载并开始分配任务。配置数据如上所述，Cloud-init 使用的配置数据来自两个潜在来源：云提供商和实例用户。在家庭实验室中，你扮演着这两种角色：作为云提供商提供网络和实例信息，作为用户提供配置信息。云提供商元数据文件在你的云提供商角色中，你的家庭实验室数据源将为你的私有云实例提供一个元数据文件。这个元数据文件包含实例 ID、云类型、Python 版本（Cloud-init 用 Python 编写并使用 Python）或要分配给主机的 SSH 公钥等信息。如果你不使用 DHCP（或 Cloud-init 支持的其他机制，如镜像中的配置文件或内核参数），元数据文件还可能包含网络信息。用户提供的用户数据文件Cloud-init 的真正价值在于用户数据文件。用户数据文件由用户提供给云提供商，并包含在数据源中，它将实例从一台普通的机器变成了用户舰队的一员。用户数据文件可以以可执行脚本的形式出现，与正常情况下脚本的工作方式相同；也可以以云服务配置 YAML 文件的形式出现，利用 Cloud-init 的模块 来执行配置任务。数据源数据源是由云提供商提供的服务，它为实例提供了元数据和用户数据文件。实例镜像或 ISO 被配置为告知实例正在使用什么数据源。例如，亚马逊 AWS 提供了一个 link-local 文件，它将用实例的自定义数据来响应实例的 HTTP 请求。其他云提供商也有自己的机制。幸运的是，对于家庭私有云项目来说，也有 NoCloud 数据源。NoCloud 数据源允许通过内核命令以键值对的形式提供配置信息，或通过挂载的 ISO 文件系统以用户数据和元数据文件的形式提供。这些对于虚拟机来说很有用，尤其是与自动化搭配来创建虚拟机。还有一个 NoCloudNet 数据源，它的行为类似于 AWS EC2 数据源，提供一个 IP 地址或 DNS 名称，通过 HTTP 从这里检索用户数据和元数据。这对于你的家庭实验室中的物理机器来说是最有帮助的，比如树莓派、NUC 或多余的服务器设备。虽然 NoCloud 可以工作，但它需要更多的人工关注 —— 这是云实例的反模式。家庭实验室的 Cloud-init我希望这能让你了解到 Cloud-init 是什么，以及它对你的家庭实验室有何帮助。它是一个令人难以置信的工具，被主要的云提供商所接受，在家里使用它可以是为了教育和乐趣，并帮助你自动向实验室添加新的物理或虚拟服务器。之后的文章将详细介绍如何创建简单的静态和更复杂的动态 Cloud-init 服务，并指导你将它们纳入你的家庭私有云。via: https://opensource.com/article/20/5/cloud-init-raspberry-pi-homelab作者：Chris Collins 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
修改磁盘镜像来创建基于树莓派的家庭实验室
使用树莓派或其它单板机创建一个“家庭私有云”。构建一个家庭实验室可以是一个有趣的方式，可以让你学习的新概念和实验新技术时还能自娱自乐。得益于以 树莓派 为首的单板计算机（SBC）的流行，在舒适的家里就可以轻松构建一个多计算机实验室。比起试图在主流的云服务商建立的相同配置，创建一个“家庭私有云”以花更少的钱来体验到云原生技术，也是一个极好的方法。这篇文章阐述如何修改树莓派或其它的单板机的磁盘镜像，预配置主机的 SSH，并禁用首次启动时强制竞选交互配置的服务。这是一个让你的设备“即启动，即运行”的极好方法，类似于云端实例。之后，你可以使用自动化的流程通过 SSH 连接来进行更专业和更深入的配置。此外, 当向你的实验室添加更多的树莓派时，修改磁盘镜像可以来让你只需要将该镜像写到一个 SD 卡、放入树莓派中就可以了！解压缩和挂载镜像对于这个项目，你需要修改一个服务器磁盘镜像。在测试期间，我使用 Fedora Server 31 ARM。在你下载该磁盘镜像并验证其校验和之后，你需要将其解压缩并挂载其到宿主机的文件系统的某个位置上，以便你可以根据需要修改它。你可以使用 xz 命令通过 --decompress 参数来解压缩 Fedora 服务器镜像：xz --decompress Fedora-Server-armhfp-X-y.z-sda.raw.xz这会留下一个解压缩后的原始磁盘镜像（它会自动地替换 .xz 压缩文件）。这个原始磁盘镜像就像它听起来的那样：一个包含格式化后安装好的磁盘上的所有数据的文件。这包含分区信息、启动分区、root 分区以及其它分区。你需要挂载你打算在其中进行修改的分区，但是要做到这一点，你需要知道磁盘镜像中的分区起始位置和扇区大小，这样你才可以挂载该文件正确的扇区。幸运的是，你可以在一个磁盘镜像上使用 fdisk 命令，就像在实际磁盘上使用一样容易。使用 --list 或 -l 参数来查看分区的列表和其信息：# 使用 fdisk 来列出原始镜像文件的分区： $ fdisk -l Fedora-Server-armhfp-31-1.9-sda.raw Disk Fedora-Server-armhfp-X-y.z-sda.raw: 3.2 GiB, 3242196992 bytes, 6332416 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0xdaad9f57 Device Boot Start End Sectors Size Id Type Fedora-Server-armhfp-X-y.z-sda.raw1 8192 163839 155648 76M c W95 F Fedora-Server-armhfp-X-y.z-sda.raw2 * 163840 1163263 999424 488M 83 Linux Fedora-Server-armhfp-X-y.z-sda.raw3 1163264 6047743 4884480 2.3G 83 Linux你需要的所有信息都可在上面的输出中获得。第 3 行表示扇区大小（包括逻辑和物理的）：512 字节 / 512 字节。设备列表显示的是原始磁盘镜像中的分区。第一个，Fedora-Server-armhfp-X-y.z-sda.raw1 毫无疑问是引导程序分区，因为它是第一个，很小（仅仅 76MB），而且类型被标识为 c，即 W95 FAT32（LBA），这是一个从 SD 卡启动的 FAT32 分区。第二个分区也不是非常大，只有 488MB。这个分区是一个 Linux 原生类型分区（Id 83），它可能是包含内核和 initramfs 的 Linux 启动分区。第三个分区可能是你需要的东西：它有 2.3GB 大小，所以在它其中应该有发行版的主要部分，并且它是一个 Linux 原生分区类型，这也是在预料之中的。这个分区应该包含了你需要修改的分区和数据。第三个分区从扇区 1163264 开始（在 fdisk 的输出中被显示为 Start 列），所以你的挂载偏移量是 595591168，计算方法是将扇区大小（512）乘以起始扇区（1163264）（即 512 * 1163264）。这意味着你需要以偏移量 595591168 挂载该文件，才能挂载到正确位置。装备了这些信息，现在你可以将第三个分区挂载到你的家目录中了：$ mkdir ~/mnt $ sudo mount -o loop,offset=595591168 Fedora-Server-armhfp-X-y.z-sda.raw ~/mnt $ ls ~/mnt直接在磁盘镜像中作业在磁盘镜像被解压缩和被挂载到宿主机上的一个位置后，就可以修改镜像以符合你的需求。在我看来，对镜像进行更改的最简单的方法是使用 chroot 来将你会话的工作根目录更改为挂载镜像的工作根目录。不过，有点棘手。在你改变了根目录后，你的会话将使用新的根目录下的二进制文件。除非你是在一个 ARM 系统做这些所有的操作，否则解压缩后的磁盘镜像的架构将与你正在使用的宿主机系统不同。即使在 chroot 环境中，宿主机系统也无法使用一个不同架构的二进制文件。至少，不能在本机使用。幸运的是，这里有一个解决方案：qemu-user-static。来自 Debian Wiki 的说明:“[qemu-user-static] 提供了用户模式的仿真二进制文件，是静态构建的。在这个模式中，QEMU 可以在一个 CPU 上启动为另一个 CPU 编译的 Linux 进程 …… 如果安装了 binfmt-support 软件包，qemu-user-static 软件包会注册提供的仿真器可以处理的二进制文件格式，以便其能够直接运行其他架构的二进制文件。”这正是你需要在 chroot 环境中非本地架构中工作所需的。如果宿主机系统是 Fedora，使用 DNF 来安装 qemu-user-static 软件包，并重新启动 systemd-binfmt.service：# 使用 DNF 启用非本地的 arch chroot 环境，添加新的二进制文件格式信息 # 输出镜像了精简 $ dnf install qemu-user-static $ systemctl restart systemd-binfmt.service使用这种方法，你一个能够更改根目录到挂载的磁盘镜像，运行 uname 命令来验证一切都在正常：sudo chroot ~/mnt/ /usr/bin/uname -a -r Linux marvin 5.5.16-200.fc31.x86_64 #1 SMP Wed Apr 8 16:43:33 UTC 2020 armv7l armv7l armv7l GNU/Linux在 chroot 环境中运行 uname 将在输出中显示 armv7l，这个原始磁盘镜像的架构， 而不是宿主机的架构。一切如预期，可以继续修改镜像了。修改磁盘镜像现在你可以直接切换到这个基于 ARM 的磁盘镜像中，并在该环境中工作了，你可以对镜像自身镜像修改了。你需要设置该镜像，以便它能够启动并可立即访问，而不需要在树莓派上做任何额外的设置。为此，你需要安装并启用 sshd（OpenSSH 守护进程），并为 SSH 访问添加授权密码。为了使其表现得更像一个云环境，实现在家里建立私有云的梦想，添加一个本地用户，给予该用户 sudo 权限，并（为了像云端的重度用户一样）允许该用户无需密码就可以使用 sudo。所以，你将做的事情是：安装并启用 SSHD（SSHD 已经在 Fedora ARM 镜像中安装并启用，但是你可能需要为你发行版手动执行这些工作）设置一个本地用户允许本地用户来使用 sudo（无需密码，可选）添加授权密钥允许 root 使用授权密码镜像 SSH（可选）我使用 GitHub 功能，它允许你上传你的 SSH 公钥，并在 https://github.com/.keys 处可访问。我发现这是一种很方便的分发公钥的方法，不过我生性多疑，我总是检查下载的密钥是否与我预期的匹配。如果你不想使用这种方法，你可以从你宿主机中复制你公钥到 chroot 环境中，或者你可以将公钥托管在你控制的 Web 服务器上以便使用相同的工作流。要开始修改磁盘镜像，再次切换根目录到挂载的磁盘镜像中，这次启动一个 shell，以便可以运行多个命令：# 为了简洁起见，省略了这些命令的输出(如果有的话) $ sudo chroot ~/mnt /bin/bash # 安装 openssh-server ，并启用它 (在 Fedora 上已经完成) $ dnf install -y openssh-server $ systemctl enable sshd.service # 允许 root 使用授权密码访问 SSH $ mkdir /root/.ssh # 下载或者另外添加授权密码文件，你的公共密码 # 将 URL 替换为你自己公共密码的路径 $ curl <https://github.com/clcollins.keys> -o /root/.ssh/authorized_keys $ chmod 700 /root/.ssh $ chmod 600 /root/.ssh/authorized_keys # 添加一个本地用户，并放置他们到 wheel 组中 # 将组和用户更改为您想要的一切 useradd -g chris -G wheel -m -u 1000 chris # 下载并添加你的授权密码 # 像你上面所做的那样更改 home 目录和URL mkdir /home/chris/.ssh curl <https://github.com/clcollins.keys> -o /home/chris/.ssh/authorized_keys chmod 700 /home/chris/.ssh chmod 600 /home/chris/.ssh/authorized_keys chown -R chris.chris /home/chris/.ssh/ # 允许 wheel 组( 使用你的本地用户) 不需要使用密码来使用 suso echo "%wheel ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/91-wheel-nopasswd这就是树莓派或其它单板机在首次启动时需要完成设置 SSH 的全部工作。不过，每个发行版都有自己的特点。例如，Raspbian 已经包含一个本地用户：pi，并且不使用 wheel 组。因此对于 Raspbian 来说，最好使用现有用户，或者删除 pi 用户，并使用另一个用户来替换它。在 Fedora ARM 的情况下，镜像会在首次引导启动时提示你完成设置。这会破坏你在上面所做的修改的目的，尤其是在设置完成之前，它会完全阻止启动。你的目标是使树莓派的功能类似于私有云的基础设施的一部分一样运行，而这个工作流程包括在主机启动时通过 SSH 远程设置主机。 禁用初始化设置，它由 initial-setup.service 控制：# 对多用户和图形目标禁用 initial-setup.service unlink /etc/systemd/system/multi-user.target.wants/initial-setup.service unlink /etc/systemd/system/graphical.target.wants/initial-setup.service当你在 chroot 环境时，你可以对你系统做任何你想做的其它更改，或者就放在那里，在第一次启动后，按照云原生的工作流通过 SSH 进行配置。重新压缩并安装修改后的镜像完成了这些更改后，剩下的就是重新压缩磁盘镜像，并将其安装其到你的树莓派的 SD 卡上。确保退出 chroot 环境，然后卸载磁盘镜像：$ sudo umount ~/mnt/就像最初解压缩镜像一样，你可以再次使用 xz 命令来压缩镜像。通过使用 --keep 参数，xz 将保留原始的镜像，而不是清理掉它。虽然这会占用更多的磁盘空间，但保留下来的未压缩镜像将允许你对正在处理的镜像进行增量更改，而不需要每次都对其进行解压缩。这对于在测试和调整镜像时节省时间是非常好的。# 压缩压缩磁盘镜像为一个 .xz 文件，但保留原始磁盘镜像 xz --compress Fedora-Server-armhfp-31-1.9-sda.raw --keep压缩过程将花费一些时间，所以趁着这个时间站起来，舒展身体，让你的血液再次流动。在压缩完成后，可以将新的、已修改过的磁盘镜像复制到 SD 卡上，以便与树莓派一起使用。标准的 dd 方法将镜像放置到 SD 卡上也很好用，但是我喜欢使用 Fedora 的 arm-image-installer，因为它因为它在处理未经编辑的镜像时提供了一些选项。它对编辑过的镜像也很好用，并且比 dd 命令更友好一些。确保检查 SD 卡在哪个磁盘驱动器上，并用 --media 参数使用它：# 使用 arm-image-installer 来复制已修改的磁盘镜像到 SD 卡上 arm-image-installer --image=Fedora-Server-armhfp-X-y.z-sda.raw.xz --target=rpi3 --media=/dev/sdc --norootpass --resizefs -y现在，你已经为树莓派或其它单板机准备好了一个新的、已修改的 Fedora Server ARM 镜像，准备好启动并立即 SSH 到你的修改镜像中。这种方法也可以用来做其它的修改，并且你也可以使用其它发行版的原始磁盘镜像，如果你更喜欢它们，而不是 Fedora 的话。这是一个开始构建家庭实验室私有云的良好基础。在以后的文章中，我将指导你使用云技术和自动化建立一个家庭实验室。延伸阅读为了学习如何做这篇文章中的事情，我做了很多研究。以下是我找到的两个对学习如何定制磁盘映像和使用非原生架构最有帮助的资料。它们对我从“不知道自己在做什么”到“我能够完成它！”非常有帮助。如何修改你的自定义 Linux 发行版的原始磁盘镜像使用 DNF 维基via: https://opensource.com/article/20/5/disk-image-raspberry-pi作者：Chris Collins 选题：lujun9972 译者：robsean 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
使用 Go 版本管理器管理多个版本的 Go 语言环境及其模块。Go 语言版本管理器（GVM）是管理 Go 语言环境的开源工具。GVM “pkgsets” 支持安装多个版本的 Go 并管理每个项目的模块。它最初由 Josh Bussdieker 开发，GVM（像它的对手 Ruby RVM 一样）允许你为每个项目或一组项目创建一个开发环境，分离不同的 Go 版本和包依赖关系，以提供更大的灵活性，防止不同版本造成的问题。有几种管理 Go 包的方式，包括内置于 Go 中的 Go 1.11 的 Modules。我发现 GVM 简单直观，即使我不用它来管理包，我还是会用它来管理 Go 不同的版本的。安装 GVM安装 GVM 很简单。GVM 存储库安装文档指示你下载安装程序脚本并将其传送到 Bash 来安装:bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer)尽管越来越多的人采用这种安装方法，但是在安装之前先看看安装程序在做什么仍然是一个很好的想法。以 GVM 为例，该安装程序脚本:检查一些相关依赖性克隆 GVM 存储库使用 shell 脚本：安装 Go 语言管理 GOPATH 环境变量向 bashrc、zshrc 或配置文件中添加一行内容如果你想确认它在做什么，你可以克隆该存储库并查看 shell 脚本，然后运行 ./binscripts/gvm-installer 这个本地脚本进行设置。注意： 因为 GVM 可以用来下载和编译新的 Go 版本，所以有一些预期的依赖关系，如 Make、Git 和 Curl。你可以在 GVM 的自述文件中找到完整的发行版列表。使用 GVM 安装和管理 GO 版本一旦安装了 GVM，你就可以使用它来安装和管理不同版本的 Go。gvm listall 命令显示可下载和编译的可用版本的 Go：[chris@marvin ]$ gvm listall gvm gos (available) go1 go1.0.1 go1.0.2 go1.0.3 <输出截断>安装特定的 Go 版本就像 gvm install <版本> 一样简单，其中 <版本> 是 gvm listall 命令返回的版本之一。假设你正在进行一个使用 Go1.12.8 版本的项目。你可以使用 gvm install go1.12.8 安装这个版本:[chris@marvin]$ gvm install go1.12.8 Installing go1.12.8... * Compiling... go1.12.8 successfully installed!输入 gvm list，你会看到 Go 版本 1.12.8 与系统 Go 版本（使用操作系统的软件包管理器打包的版本）一起并存：[chris@marvin]$ gvm list gvm gos (installed) go1.12.8 => systemGVM 仍在使用系统版本的 Go ，由 => 符号表示。你可以使用 gvm use 命令切换你的环境以使用新安装的 go1.12.8：[chris@marvin]$ gvm use go1.12.8 Now using version go1.12.8 [chris@marvin]$ go version go version go1.12.8 linux/amd64GVM 使管理已安装版本的 Go 变得极其简单，但它不止于此！使用 GVM pkgset开箱即用，Go 有一种出色而令人沮丧的管理包和模块的方式。默认情况下，如果你 go get 获取一个包，它将被下载到 $GOPATH 目录中的 src 和 pkg 目录下，然后可以使用 import 将其包含在你的 Go 程序中。这使得获得软件包变得很容易，特别是对于非特权用户，而不需要 sudo 或 root 特权（很像 Python 中的 pip install --user）。然而，在不同的项目中管理相同包的不同版本是非常困难的。有许多方法可以尝试修复或缓解这个问题，包括实验性 Go Modules（Go 1.11 版中增加了初步支持）和 Go dep（Go Modules 的“官方实验”并且持续迭代）。在我发现 GVM 之前，我会在一个 Go 项目自己的 Docker 容器中构建和测试它，以确保分离。GVM 通过使用 “pkgsets” 将项目的新目录附加到安装的 Go 版本的默认 $GOPATH 上，很好地实现了项目之间包的管理和隔离，就像 $PATH 在 Unix/Linux 系统上工作一样。想象它如何运行的。首先，安装新版 Go 1.12.9：[chris@marvin]$ echo $GOPATH /home/chris/.gvm/pkgsets/go1.12.8/global [chris@marvin]$ gvm install go1.12.9 Installing go1.12.9... * Compiling... go1.12.9 successfully installed [chris@marvin]$ gvm use go1.12.9 Now using version go1.12.9当 GVM 被告知使用新版本时，它会更改为新的 $GOPATH，默认 gloabl pkgset 应用于该版本:[chris@marvin]$ echo $GOPATH /home/chris/.gvm/pkgsets/go1.12.9/global [chris@marvin]$ gvm pkgset list gvm go package sets (go1.12.9) => global尽管默认情况下没有安装额外的包，但是全局 pkgset 中的包对于使用该特定版本的 Go 的任何项目都是可用的。现在，假设你正在启用一个新项目，它需要一个特定的包。首先，使用 GVM 创建一个新的 pkgset，名为 introToGvm:[chris@marvin]$ gvm pkgset create introToGvm [chris@marvin]$ gvm pkgset use introToGvm Now using version go1.12.9@introToGvm [chris@marvin]$ gvm pkgset list gvm go package sets (go1.12.9) global => introToGvm如上所述，pkgset 的一个新目录被添加到 $GOPATH：[chris@marvin]$ echo $GOPATH /home/chris/.gvm/pkgsets/go1.12.9/introToGvm:/home/chris/.gvm/pkgsets/go1.12.9/global将目录更改为预先设置的 introToGvm 路径，检查目录结构，这里使用 awk 和 bash 完成。[chris@marvin]$ cd $( awk -F':' '{print $1}' <<< $GOPATH ) [chris@marvin]$ pwd /home/chris/.gvm/pkgsets/go1.12.9/introToGvm [chris@marvin]$ ls overlay pkg src请注意，新目录看起来很像普通的 $GOPATH。新的 Go 包使用同样的 go get 命令下载并正常使用，且添加到 pkgset 中。例如，使用以下命令获取 gorilla/mux 包，然后检查 pkgset 的目录结构：[chris@marvin]$ go get github.com/gorilla/mux [chris@marvin]$ tree [chris@marvin introToGvm ]$ tree . ├── overlay │ ├── bin │ └── lib │ └── pkgconfig ├── pkg │ └── linux_amd64 │ └── github.com │ └── gorilla │ └── mux.a src/ └── github.com └── gorilla └── mux ├── AUTHORS ├── bench_test.go ├── context.go ├── context_test.go ├── doc.go ├── example_authentication_middleware_test.go ├── example_cors_method_middleware_test.go ├── example_route_test.go ├── go.mod ├── LICENSE ├── middleware.go ├── middleware_test.go ├── mux.go ├── mux_test.go ├── old_test.go ├── README.md ├── regexp.go ├── route.go └── test_helpers.go如你所见，gorilla/mux 已按预期添加到 pkgset $GOPATH 目录中，现在可用于使用此 pkgset 项目了。GVM 让 Go 管理变得轻而易举GVM 是一种直观且非侵入性的管理 Go 版本和包的方式。它可以单独使用，也可以与其他 Go 模块管理技术结合使用并利用 GVM Go 版本管理功能。按 Go 版本和包依赖来分离项目使得开发更加容易，并且减少了管理版本冲突的复杂性，GVM 让这变得轻而易举。via: https://opensource.com/article/19/10/introduction-gvm作者：Chris Collins 选题：lujun9972 译者：heguangzhi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
五种优化 Linux 容器大小和构建更小的镜像的方法。Docker 近几年的爆炸性发展让大家逐渐了解到容器和容器镜像的概念。尽管 Linux 容器技术在很早之前就已经出现，但这项技术近来的蓬勃发展却还是要归功于 Docker 对用户友好的命令行界面以及使用 Dockerfile 格式轻松构建镜像的方式。纵然 Docker 大大降低了入门容器技术的难度，但构建一个兼具功能强大、体积小巧的容器镜像的过程中，有很多技巧需要了解。第一步：清理不必要的文件这一步和在普通服务器上清理文件没有太大的区别，而且要清理得更加仔细。一个小体积的容器镜像在传输方面有很大的优势，同时，在磁盘上存储不必要的数据的多个副本也是对资源的一种浪费。因此，这些技术对于容器来说应该比有大量专用内存的服务器更加需要。清理容器镜像中的缓存文件可以有效缩小镜像体积。下面的对比是使用 dnf 安装 Nginx 构建的镜像，分别是清理和没有清理 yum 缓存文件的结果：# Dockerfile with cache FROM fedora:28 LABEL maintainer Chris Collins <collins.christopher@gmail.com> RUN dnf install -y nginx ----- # Dockerfile w/o cache FROM fedora:28 LABEL maintainer Chris Collins <collins.christopher@gmail.com> RUN dnf install -y nginx \ && dnf clean all \ && rm -rf /var/cache/yum ----- [chris@krang] $ docker build -t cache -f Dockerfile . [chris@krang] $ docker images --format "{{.Repository}}: {{.Size}}" | head -n 1 cache: 464 MB [chris@krang] $ docker build -t no-cache -f Dockerfile-wo-cache . [chris@krang] $ docker images --format "{{.Repository}}: {{.Size}}" | head -n 1 no-cache: 271 MB从上面的结果来看，清理缓存文件的效果相当显著。和清除了元数据和缓存文件的容器镜像相比，不清除的镜像体积接近前者的两倍。除此以外，包管理器缓存文件、Ruby gem 的临时文件、nodejs 缓存文件，甚至是下载的源码 tarball 最好都全部清理掉。层：一个潜在的隐患很不幸（当你往下读，你会发现这是不幸中的万幸），根据容器中的层的概念，不能简单地向 Dockerfile 中写一句 RUN rm -rf /var/cache/yum 就完事儿了。因为 Dockerfile 的每一条命令都以一个层的形式存储，并一层层地叠加。所以，如果你是这样写的：RUN dnf install -y nginx RUN dnf clean all RUN rm -rf /var/cache/yum你的容器镜像就会包含三层，而 RUN dnf install -y nginx 这一层仍然会保留着那些缓存文件，然后在另外两层中被移除。但缓存实际上仍然是存在的，当你把一个文件系统挂载在另外一个文件系统之上时，文件仍然在那里，只不过你见不到也访问不到它们而已。在上一节的示例中，你会看到正确的做法是将几条命令链接起来，在产生缓存文件的同一条 Dockerfile 指令里把缓存文件清理掉：RUN dnf install -y nginx \ && dnf clean all \ && rm -rf /var/cache/yum这样就把几条命令连成了一条命令，在最终的镜像中只占用一个层。这样只会浪费一点缓存的好处，稍微多耗费一点点构建容器镜像的时间，但被清理掉的缓存文件就不会留存在最终的镜像中了。作为一个折衷方法，只需要把一些相关的命令（例如 yum install 和 yum clean all、下载文件、解压文件、移除 tarball 等等）连接成一个命令，就可以在最终的容器镜像中节省出大量体积，你也能够利用 Docker 的缓存加快开发速度。层还有一个更隐蔽的特性。每一层都记录了文件的更改，这里的更改并不仅仅已有的文件累加起来，而是包括文件属性在内的所有更改。因此即使是对文件使用了 chmod 操作也会被在新的层创建文件的副本。下面是一次 docker images 命令的输出内容。其中容器镜像 layer_test_1 是在 CentOS 基础镜像中增加了一个 1GB 大小的文件后构建出来的镜像，而容器镜像 layer_test_2 是使用了 FROM layer_test_1 语句创建出来的，除了执行一条 chmod u+x 命令没有做任何改变。layer_test_2 latest e11b5e58e2fc 7 seconds ago 2.35 GB layer_test_1 latest 6eca792a4ebe 2 minutes ago 1.27 GB如你所见，layer_test_2 镜像比 layer_test_1 镜像大了 1GB 以上。尽管事实上 layer_test_1 只是 layer_test_2 的前一层，但隐藏在这第二层中有一个额外的 1GB 的文件。在构建容器镜像的过程中，如果在单独一层中进行移动、更改、删除文件，都会出现类似的结果。专用镜像和公用镜像有这么一个亲身经历：我们部门重度依赖于 Ruby on Rails，于是我们开始使用容器。一开始我们就建立了一个正式的 Ruby 的基础镜像供所有的团队使用，为了简单起见（以及在“这就是我们自己在服务器上瞎鼓捣的”想法的指导下），我们使用 rbenv 将 Ruby 最新的 4 个版本都安装到了这个镜像当中，目的是让开发人员只用这个单一的镜像就可以将使用不同版本 Ruby 的应用程序迁移到容器中。我们当时还认为这是一个虽然非常大但兼容性相当好的镜像，因为这个镜像可以同时满足各个团队的使用。实际上这是费力不讨好的。如果维护独立的、版本略微不同的镜像中，可以很轻松地实现镜像的自动化维护。同时，选择特定版本的特定镜像，还有助于在引入破坏性改变，在应用程序接近生命周期结束前提前做好预防措施，以免产生不可控的后果。庞大的公用镜像也会对资源造成浪费，当我们后来将这个庞大的镜像按照 Ruby 版本进行拆分之后，我们最终得到了共享一个基础镜像的多个镜像，如果它们都放在一个服务器上，会额外多占用一点空间，但是要比安装了多个版本的巨型镜像要小得多。这个例子也不是说构建一个灵活的镜像是没用的，但仅对于这个例子来说，从一个公共镜像创建根据用途而构建的镜像最终将节省存储资源和维护成本，而在受益于公共基础镜像的好处的同时，每个团队也能够根据需要来做定制化的配置。从零开始：将你需要的内容添加到空白镜像中有一些和 Dockerfile 一样易用的工具可以轻松创建非常小的兼容 Docker 的容器镜像，这些镜像甚至不需要包含一个完整的操作系统，就可以像标准的 Docker 基础镜像一样小。我曾经写过一篇关于 Buildah 的文章，我想在这里再一次推荐一下这个工具。因为它足够的灵活，可以使用宿主机上的工具来操作一个空白镜像并安装打包好的应用程序，而且这些工具不会被包含到镜像当中。Buildah 取代了 docker build 命令。可以使用 Buildah 将容器的文件系统挂载到宿主机上并进行交互。下面来使用 Buildah 实现上文中 Nginx 的例子（现在忽略了缓存的处理）：#!/usr/bin/env bash set -o errexit # Create a container container=$(buildah from scratch) # Mount the container filesystem mountpoint=$(buildah mount $container) # Install a basic filesystem and minimal set of packages, and nginx dnf install --installroot $mountpoint --releasever 28 glibc-minimal-langpack nginx --setopt install_weak_deps=false -y # Save the container to an image buildah commit --format docker $container nginx # Cleanup buildah unmount $container # Push the image to the Docker daemon’s storage buildah push nginx:latest docker-daemon:nginx:latest 你会发现这里使用的已经不再是 Dockerfile 了，而是普通的 Bash 脚本，而且是从框架（或空白）镜像开始构建的。上面这段 Bash 脚本将容器的根文件系统挂载到了宿主机上，然后使用宿主机的命令来安装应用程序，这样的话就不需要把软件包管理器放置到容器镜像中了。这样所有无关的内容（基础镜像之外的部分，例如 dnf）就不再会包含在镜像中了。在这个例子当中，构建出来的镜像大小只有 304 MB，比使用 Dockerfile 构建的镜像减少了 100 MB 以上。[chris@krang] $ docker images |grep nginx docker.io/nginx buildah 2505d3597457 4 minutes ago 304 MB注：这个镜像是使用上面的构建脚本构建的，镜像名称中前缀的 docker.io 只是在推送到镜像仓库时加上的。对于一个 300MB 级别的容器基础镜像来说，能缩小 100MB 已经是很显著的节省了。使用软件包管理器来安装 Nginx 会带来大量的依赖项，如果能够使用宿主机直接从源代码对应用程序进行编译然后构建到容器镜像中，节省出来的空间还可以更多，因为这个时候可以精细的选用必要的依赖项，非必要的依赖项一概不构建到镜像中。Tom Sweeney 有一篇文章《用 Buildah 构建更小的容器》，如果你想在这方面做深入的优化，不妨参考一下。通过 Buildah 可以构建一个不包含完整操作系统和代码编译工具的容器镜像，大幅缩减了容器镜像的体积。对于某些类型的镜像，我们可以进一步采用这种方式，创建一个只包含应用程序本身的镜像。使用静态链接的二进制文件来构建镜像按照这个思路，我们甚至可以更进一步舍弃容器内部的管理和构建工具。例如，如果我们足够专业，不需要在容器中进行排错调试，是不是可以不要 Bash 了？是不是可以不要 GNU 核心套件了？是不是可以不要 Linux 基础文件系统了？如果你使用的编译型语言支持静态链接库，将应用程序所需要的所有库和函数都编译成二进制文件，那么程序所需要的函数和库都可以复制和存储在二进制文件本身里面。这种做法在 Golang 社区中已经十分常见，下面我们使用由 Go 语言编写的应用程序进行展示：以下这个 Dockerfile 基于 golang:1.8 镜像构建一个小的 Hello World 应用程序镜像：FROM golang:1.8 ENV GOOS=linux ENV appdir=/go/src/gohelloworld COPY ./ /go/src/goHelloWorld WORKDIR /go/src/goHelloWorld RUN go get RUN go build -o /goHelloWorld -a CMD ["/goHelloWorld"]构建出来的镜像中包含了二进制文件、源代码以及基础镜像层，一共 716MB。但对于应用程序运行唯一必要的只有编译后的二进制文件，其余内容在镜像中都是多余的。如果在编译的时候通过指定参数 CGO_ENABLED=0 来禁用 cgo，就可以在编译二进制文件的时候忽略某些函数的 C 语言库：GOOS=linux CGO_ENABLED=0 go build -a goHelloWorld.go编译出来的二进制文件可以加到一个空白（或框架）镜像：FROM scratch COPY goHelloWorld / CMD ["/goHelloWorld"]来看一下两次构建的镜像对比：[ chris@krang ] $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE goHello scratch a5881650d6e9 13 seconds ago 1.55 MB goHello builder 980290a100db 14 seconds ago 716 MB从镜像体积来说简直是天差地别了。基于 golang:1.8 镜像构建出来带有 goHelloWorld 二进制的镜像（带有 builder 标签）体积是基于空白镜像构建的只包含该二进制文件的镜像的 460 倍！后者的整个镜像大小只有 1.55MB，也就是说，有 713MB 的数据都是非必要的。正如上面提到的，这种缩减镜像体积的方式在 Golang 社区非常流行，因此不乏这方面的文章。Kelsey Hightower 有一篇文章专门介绍了如何处理这些库的依赖关系。压缩镜像层除了前面几节中讲到的将多个命令链接成一个命令的技巧，还可以对镜像进行压缩。镜像压缩的实质是导出它，删除掉镜像构建过程中的所有中间层，然后保存镜像的当前状态为单个镜像层。这样可以进一步将镜像缩小到更小的体积。在 Docker 1.13 之前，压缩镜像层的的过程可能比较麻烦，需要用到 docker-squash 之类的工具来导出容器的内容并重新导入成一个单层的镜像。但 Docker 在 Docker 1.13 中引入了 --squash 参数，可以在构建过程中实现同样的功能：FROM fedora:28 LABEL maintainer Chris Collins <collins.christopher@gmail.com> RUN dnf install -y nginx RUN dnf clean all RUN rm -rf /var/cache/yum [chris@krang] $ docker build -t squash -f Dockerfile-squash --squash . [chris@krang] $ docker images --format "{{.Repository}}: {{.Size}}" | head -n 1 squash: 271 MB通过这种方式使用 Dockerfile 构建出来的镜像有 271MB 大小，和上面连接多条命令的方案构建出来的镜像体积一样，因此这个方案也是有效的，但也有一个潜在的问题，而且是另一种问题。“什么？还有另外的问题？”好吧，有点像以前一样的问题，以另一种方式引发了问题。过头了：过度压缩、太小太专用了容器镜像之间可以共享镜像层。基础镜像或许大小上有几 Mb，但它只需要拉取/存储一次，并且每个镜像都能复用它。所有共享基础镜像的实际镜像大小是基础镜像层加上每个特定改变的层的差异内容，因此，如果有数千个基于同一个基础镜像的容器镜像，其体积之和也有可能只比一个基础镜像大不了多少。因此，这就是过度使用压缩或专用镜像层的缺点。将不同镜像压缩成单个镜像层，各个容器镜像之间就没有可以共享的镜像层了，每个容器镜像都会占有单独的体积。如果你只需要维护少数几个容器镜像来运行很多容器，这个问题可以忽略不计；但如果你要维护的容器镜像很多，从长远来看，就会耗费大量的存储空间。回顾上面 Nginx 压缩的例子，我们能看出来这种情况并不是什么大的问题。在这个镜像中，有 Fedora 操作系统和 Nginx 应用程序，没有缓存，并且已经被压缩。但我们一般不会使用一个原始的 Nginx，而是会修改配置文件，以及引入其它代码或应用程序来配合 Nginx 使用，而要做到这些，Dockerfile 就变得更加复杂了。如果使用普通的镜像构建方式，构建出来的容器镜像就会带有 Fedora 操作系统的镜像层、一个安装了 Nginx 的镜像层（带或不带缓存）、为 Nginx 作自定义配置的其它多个镜像层，而如果有其它容器镜像需要用到 Fedora 或者 Nginx，就可以复用这个容器镜像的前两层。[ App 1 Layer ( 5 MB) ] [ App 2 Layer (6 MB) ] [ Nginx Layer ( 21 MB) ] ------------------^ [ Fedora Layer (249 MB) ] 如果使用压缩镜像层的构建方式，Fedora 操作系统会和 Nginx 以及其它配置内容都被压缩到同一层里面，如果有其它容器镜像需要使用到 Fedora，就必须重新引入 Fedora 基础镜像，这样每个容器镜像都会额外增加 249MB 的大小。[ Fedora + Nginx + App 1 (275 MB)] [ Fedora + Nginx + App 2 (276 MB) ] 当你构建了大量在功能上趋于分化的的小型容器镜像时，这个问题就会暴露出来了。就像生活中的每一件事一样，关键是要做到适度。根据镜像层的实现原理，如果一个容器镜像变得越小、越专用化，就越难和其它容器镜像共享基础的镜像层，这样反而带来不好的效果。对于仅在基础镜像上做微小变动构建出来的多个容器镜像，可以考虑共享基础镜像层。如上所述，一个镜像层本身会带有一定的体积，但只要存在于镜像仓库中，就可以被其它容器镜像复用。这种情况下，数千个镜像也许要比单个镜像占用更少的空间。[ specific app ] [ specific app 2 ] [ customizations ]--------------^ [ base layer ]一个容器镜像变得越小、越专用化，就越难和其它容器镜像共享基础的镜像层，最终会不必要地占用越来越多的存储空间。 [ specific app 1 ] [ specific app 2 ] [ specific app 3 ]总结减少处理容器镜像时所需的存储空间和带宽的方法有很多，其中最直接的方法就是减小容器镜像本身的大小。在使用容器的过程中，要经常留意容器镜像是否体积过大，根据不同的情况采用上述提到的清理缓存、压缩到一层、将二进制文件加入在空白镜像中等不同的方法，将容器镜像的体积缩减到一个有效的大小。via: https://opensource.com/article/18/7/building-container-images作者：Chris Collins 选题：lujun9972 译者：HankChow 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出