Chris Collins 发布的文章

使用树莓派和电子纸显示屏开始倒计时你的下一个假期。

 title=

圆周率日 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 提供了很多指导:

获得有趣的字体(选做)

你可以随心所欲的使用显示器,为什么不搞点花样?找一个炫酷的字体!

这有大量 开放字体许可 的字体可供选择。我十分喜爱 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 添加 ImageImageDrawImageFont,你会用到这些来画一些简单的图形。最后,为本地 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.heightepd.width 由 Waveshare 库设置以对应于你使用的设备。

欢迎语

接下来,你将开始画一些画。这涉及在你之前创建的“画布”对象上设置数据。这还没有将它绘制到电子纸显示屏上——你现在只是在构建你想要的图像。由你为这个项目绘制带有一块馅饼的图像,来创建一个庆祝圆周率日的欢迎信息:

 title=

很可爱,不是吗?

    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 来结束无限循环,以及一个根据计数来打印 daydays 的函数:

    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()

现在你已经拥有一个倒计时并显示剩余天数的脚本!这是在我的树莓派上的显示(视频经过加速,我没有足够的磁盘空间来保存一整天的视频):

 title=

安装 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: Stationwifi 模块的文档列出了 不同类型的接口,以及它们的用途。根据文档,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 获得便利。

 title=

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 &amp;&amp; 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    &lt;none&gt;   2m39s   v1.18.2
huckleberry   Ready    &lt;none&gt;   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   &lt;none&gt;        80/TCP    30s

你可以看到 kube-verify 服务已经被分配了一个 ClusterIP(仅对集群内部)10.98.188.200。这个 IP 可以从你的任何节点到达,但不能从集群外部到达。你可以通过在这个 IP 上连接到部署内部的容器来验证它们是否在工作:

# 使用 curl 连接到 ClusterIP:
# (简洁期间,输出有删节)
$ curl 10.98.188.200
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>

成功了!你的服务正在运行,容器内的 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 中。

 title=

安装 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_freqfs_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 系统中,你需要启用和启动两个服务:rpcbindnfs-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 服务即可,方法同上。

RPCBind

rpcbind 工具用于将远程过程调用(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 以及 rwroot_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》。

 title=

运行 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-init

Cloud-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.service
  • cloud-init.service
  • cloud-config.service
  • cloud-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-hostnamehostname 键正如其名,它们会在 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中国 荣誉推出