Kubernetes Inside LXD

2021 12 9, Thu

上一次提到我因为在笔记本上开虚拟机部署K8S太费资源,笔记本本身就应该是一个比较轻的长续航便携设备,要开特别多的软件削弱笔记本的便携属性不值当。所以打起了心思在LXC/LXD里面用容器。

当然这个过程不是一帆风顺的,就稍微记录一下。

原理

说起问题就得说一下原理,为什么要这样那样。这一切都得从Linux是怎么实现容器开始说起。

Linux容器,不管是系统容器还是应用容器,一般都主要是三大块,隔离,资源控制,权限限制。其实不止是容器会用这些手段,很多别的服务也会用到,比如说systemd,下来很多概念也会用systemd的服务来举例子。

隔离:Namespace

容器是一种隔离手段,或者说虚拟化手段,可以理解他就是给你隔离了一个环境出来,无论外面是CentOS还是Ubuntu,里面的环境都只和容器内部有关系。

Linux为了这个目的给出的答案叫Namespace,命名空间。命名空间有很多种,每种命名空间的范畴内,命名空间之间内部的进程看到的资源都是独立的,比如说PID啊、挂载点啊、网络啊诸如此类的。

对于容器来说,这个隔离有文件系统,我们希望程序从一个毫无关连的文件系统里面加载库、读取数据,有PID,我们希望这个程序只能看到自己和自己启动的程序,有网络,容器可以而且应该有自己的网卡和IP,有用户,我们希望容器内的用户和容器外无关、我们可以让容器带着容器的用户跟着容器跑,还有一些比如说hostname啊IPC啊时间啊什么的都可以独立于宿主。

对于systemd来说有一个很典型的应用:保护家目录(重新ro挂载一次)和独立的tmp,这就是namespace处理的。

资源限制:CGroup

对于每一个命名空间的进程,我们都肯定需要一定的机制进行资源的限制。Linux就发明了CGroup。

比如说我们可以用cgroup限制程序用哪些cpu核心,用多少的cpu核心,用多少内存多少swap,磁盘读写多快,网络的优先级如何。这些都是用cgroup控制的。

比较特殊的可以做的,一个是我们可以用cgroup对进程产生的流量打上类别的标记,从而在防火墙啊、traffic control之类的场景中对这些流量进行进一步的处理。

另一个比较特殊的可以做的是可以对cgroup下的进程统一进行暂停(freeze)和恢复(resume)。

DAC / Capabilities / AppArmor / SELinux / BPF

有了Namespace和CGroup还不够,虽然容器被隔离了,但是仍然有能力借助特权逃逸出容器,所以我们通常会借助Linux下特权限制的手段来控制权限。

其中比较常见的是DAC、AppArmor、SELinux和BPF。DAC就是Linux最简单的用户和组权限,文件系统都是用这个,Capabilities也是比较简单的标记应用能否做某类事情,比如说能不能监听比较小的网络端口。而AppArmor和SELinux则是两种稍微复杂一些的MAC类权限控制系统,AppArmor简单一些,SELinux灵活但是复杂一些,SELinux相对AppArmor更多出现在服务器上一些。BPF控制的就更细了,基本上可以看作是脚本引擎,功能强大也更灵活,但是相对来说需要写程序去处理,这里不多赘述。

在容器中常用的特权控制其实主要是AppArmor,而SELinux用的少了很多,SELinux更多用在阻止容器内进程意外访问宿主一侧的资源(没有打tag就不能访问,默认只能访问容器内)。比如说我们在容器内不能修改/proc,这就是apparmor在起作用。有兴趣可以大致看一眼Arch Wiki,对AppArmor和其他的安全机制都有描述。

问题

说了这么多,那么我们到底遇到了什么问题呢?

容器无法套容器

容器本身已经受到保护了,限制了诸多权限,所以正常情况下容器里面无法再开一个容器。

但是Kubernetes可是一个容器编排平台,要用容器的,不能套容器可不行。

没办法做一些敏感动作

受到AppArmor的保护,proc文件系统不能挂载,也不能挂载其他的文件系统。

但是kubelet和kube-proxy需要用到ipvs,访问甚至修改/proc,所以这也是个问题。

监听网络

最后一个就不是啥大问题了,容器里面套容器,无论怎么监听都只能监听到外面这层容器的IP。不过我们是测试用的集群,这个无所谓的。

解决方案

上有政策下有对策,尝试了挺久,这些是我尝试以后可行的方法。

rootless container:CGroups2委托 + Namespace + idmap

rootless container,就是不需要root用户即可创建并操作的容器,同样适用于容器内套容器的情况。其中其实需要几样东西同时正确配置才能正常用。

首先你需要使用cgroups2才可以,因为cgroups2有委托的特性,也就是可以把controller的控制权交给低权限的进程,进程对cgroups及叶子节点有完全控制权,这样不用高权限也可以管理cgroups内的资源。cgroups2在高版本systemd下默认启用,可以检查一下/sys/fs/cgroup的挂载点,如果是cgroup2的话就已经ok了。

其次命名空间需要支持User namespace,并且预先创建好用户命名空间。由于我们使用LXC,默认启用,不需要担心。

最后我们需要idmap, 就是在容器内外映射用户id的能力,参考Arch Wiki中配置rootless container的部分

特权容器

特权容器则是让容器有一定的高权限,对于LXD和Docker来说就是带上–privileged参数。只是LXD的security.privileged: true仍然是太严格了。

禁用AppArmor

实际上是设置AppArmor的profile为unconfined,类似于SELinux的unconfined,这是一个不受限制的profile,可以在容器里面做任何事情。

用户态Kubernetes:忽略一部分不重要的错误

即使设置了上面这些,有一些资源在容器里面依然是不够透明的,kubelet仍然会无法创建容器。不过好在不久前Kubernetes增加了一个FeatureGate叫KubeletInUserNamespace,这个FeatureGate会忽略掉这些失败的操作。

使用kube-proxy的iptables模式和flannel的vxlan模式

在容器内内核会拒绝所有操作ipvs的请求,无论做什么都会失败,所以在网络这方面没有太多的选择。

相比较而言我稍微倾向于kube-proxy中iptables的模式,但是userspace也是可以使用的。

而网络插件我自己目前尝试过的只有flannel,是可以正常使用的。

拓扑和规划

综合以上的这些手段,我们就可以多开几个容器,每个容器运行一个Kubernetes节点。

架构

集群架构中包括一个Keepalived + Haproxy构成的控制平面的LB,两个节点通过keepalived做HA。etcd和control plane在同一批奇数个节点上,再加上若干个worker node。

我回头研究一下kubekey是怎么不用额外的节点做高可用的。

配置

配置的过程大都是用相对比较新的软件,加上配置文件来配置的,方便记录每一步都做了什么。要配置的主要是宿主上的容器运行时、自动化安装的部分、集群初始化的配置,都尽可能使用配置文件或者脚本来进行。

好在是无论是网络还是容器还是自动化,现在很多内容都可以通过yaml定义。

实施

keepalived

下载

vrrp_script chk_haproxy {
  script "killall -0 haproxy"
  interval 2
  weight 2
}

vrrp_instance VI_1 {
  state MASTER
  interface eth0
  virtual_router_id 51
  priority 255
  advert_int 1
  virtual_ipaddress {
        10.248.21.10/24
  }
  track_script {
    chk_haproxy
  }
}

haproxy

下载

frontend apiserver
        mode tcp
        bind *:6443
        default_backend apiservers

backend apiservers
        mode tcp
        server master1 master-1:6443 check
        server master2 master-2:6443 check
        server master3 master-3:6443 check

cloud-init

主要是配置了Kubernetes的repo,安装了我常用的一些包,创建了一个sudo不需要密码的用户。同样的cloud-init配置也可以在虚拟机上用。

Kubernetes的证书直接下下来是DER的,不适合放在yaml里面,我转成了两个PEM证书。顺便一提LXD会把过长的cloud-init配置缩起来(不再折行)所以非常难读,我就另外存了一份。

  • Kubernetes(下载):
    #cloud-config
    apt:
      primary:
        - uri: http://mirrors.ustc.edu.cn/ubuntu/
        arches:
        - default
      sources:
        kubernetes.list:
          source: deb [signed-by=$KEY_FILE] https://mirrors4.tuna.tsinghua.edu.cn/kubernetes/apt kubernetes-xenial main
          key: |
            -----BEGIN PGP PUBLIC KEY BLOCK-----
    
            mQENBGA9EFkBCAC1ilzST0wns+uwZyEA5IVtYeyAuXTaQUEAd70SqIlQpDd4EyVi
            x3SCanQIu8dG9Zq3+x28WBb2OuXP9oc06ybOWdu2m7N5PY0BUT4COA36JV/YrxmN
            s+5/M+YnDHppv63jgRIOkzXzXNo6SwTsl2xG9fKB3TS0IMvBkWdw5PGrBM5GghRc
            ecgoSAAwRbWJXORHGKVwlV6tOxQZ/xqA08hPJneMfsMFPOXsitgGRHoXjlUWLVeJ
            70mmIYsC/pBglIwCzmdD8Ee39MrlSXbuXVQiz38iHfnvXYpLEmgNXKzI0DH9tKg8
            323kALzqaJlLFOLJm/uVJXRUEfKS3LhVZQMzABEBAAG0UVJhcHR1cmUgQXV0b21h
            dGljIFNpZ25pbmcgS2V5IChjbG91ZC1yYXB0dXJlLXNpZ25pbmcta2V5LTIwMjEt
            MDMtMDEtMDhfMDFfMDkucHViKYkBKAQTAQgAHAUCYD0QWQkQ/uqRaTB+oHECGwMF
            CQPDCrACGQEAAHtlCACxSWMp3yRcLmsHhxGDt59nhSNXhouWiNePSMe5vETQA/lh
            ip9Zx/NPRCa4q5jpIDBlEYOg67YanztcjSWGSI35Xblq43H4uLSxh4PtKzZMo+Uj
            8n2VNHOZXBdGcsODcU3ynF64r7eTQevUe2aU0KN2o656O3HrE4itOVKYwnnkmNsk
            G45b9b7DJnsQ6WPszUc8lNhsa2gBI6vfLl68vjj7PlWw030BM/RoMEPpoOApohHo
            sfnNhxJmE1AxwBkMEzyo2kZhPZGh85LDnDbAvjSFKqYSPReKmRFjLlo3DPVHZ/de
            Qn6noHbgUChLo21FefhlZO6tysrb283MWMIyY/YSuQENBGA9EFkBCADcdO/Aw1qu
            dZORZCNLz3vTiQSFcUFYyScfJJnwUsg8fy0kgg9olFY0GK5icT6n/shc1RlIpuqr
            OQYBZgtK3dSZfOAXE2N20HUvC+nrKKuXXX+jcM/X1kHxwX5tG6fB1fyNH0p/Qqsz
            EfYRHJu0Y4PonTYIslITnEzlN4hUN6/mx1+mWPl4P4R7/h6+p7Q2jtaClEtddF0e
            eOf16Ma5S8fff80uZCLJoVu3lOXCT22oCf7qmH2XddmqGisUScqwmbmuv30tdQed
            n+8njKo2pfpVF1Oa67CWRXdKTknuZybxI9Ipcivy8CISL2Do0uzij7SR7keVf7G1
            Q3K3iJ0wn6mDABEBAAGJAR8EGAEIABMFAmA9EFkJEP7qkWkwfqBxAhsMAAA/3Af9
            FJ2hEp2144fzgtNWHOVFv27hsrO7wYFZwoic9lHSl4iEw8mJc/3kEXdg9Vf9m1zb
            G/kZ6slmzpfv7zDAdN3h3HT0B1yrb3xXzRX0zhOYAbQSUnc6DemhDZoDWt/wVceK
            fzvebB9VTDzRBUVzxCduvY6ij0p2APZpnTrznvCPoCHkfzBMC3Zyk1FueiPTPoP1
            9M0BProMy8qDVSkFr0uX3PM54hQN6mGRQg5HVVBxUNaMnn2yOQcxbQ/T/dKlojdp
            RmvpGyYjfrvyExE8owYn8L7ly2N76GcY6kiN1CmTnCgdrbU0SPacm7XbxTYlQHwJ
            CEa9Hf4/nuiBaxwXKuc/yw==
            =PUZY
            -----END PGP PUBLIC KEY BLOCK-----
            -----BEGIN PGP PUBLIC KEY BLOCK-----
    
            mQENBF/Jfl4BCADTPUXdkNu057X+P3STVxCzJpU2Mn+tUamKdSdVambGeYFINcp/
            EGwNGhdb0a1BbHs1SWYZbzwh4d6+p3k4ABzVMO+RpMu/aBx9E5aOn5c8GzHjZ/VE
            aheqLLhSUcSCzChSZcN5jz0hTGhmAGaviMt6RMzSfbIhZPj1kDzBiGd0Qwd/rOPn
            Jr4taPruR3ecBjhHti1/BMGd/lj0F7zQnCjp7PrqgpEPBT8jo9wX2wvOyXswSI/G
            sfbFiaOJfDnYengaEg8sF+u3WOs0Z20cSr6kS76KHpTfa3JjYsfHt8NDw8w4e3H8
            PwQzNiRP9tXeMASKQz3emMj/ek6HxjihY9qFABEBAAG0umdMaW51eCBSYXB0dXJl
            IEF1dG9tYXRpYyBTaWduaW5nIEtleSAoLy9kZXBvdC9nb29nbGUzL3Byb2R1Y3Rp
            b24vYm9yZy9jbG91ZC1yYXB0dXJlL2tleXMvY2xvdWQtcmFwdHVyZS1wdWJrZXlz
            L2Nsb3VkLXJhcHR1cmUtc2lnbmluZy1rZXktMjAyMC0xMi0wMy0xNl8wOF8wNS5w
            dWIpIDxnbGludXgtdGVhbUBnb29nbGUuY29tPokBKAQTAQgAHAUCX8l+XgkQi1fF
            woNvS+sCGwMFCQPDCrACGQEAAEF6CACaekro6aUJJd3mVtrtLOOewV8et1jep5ew
            mpOrew/pajRVBeIbV1awVn0/8EcenFejmP6WFcdCWouDVIS/QmRFQV9N6YXN8Piw
            alrRV3bTKFBHkwa1cEH4AafCGo0cDvJb8N3JnM/Rmb1KSGKr7ZXpmkLtYVqr6Hgz
            l+snrlH0Xwsl5r3SyvqBgvRYTQKZpKqmBEd1udieVoLSF988kKeNDjFa+Q1SjZPG
            W+XukgE8kBUbSDx8Y8q6Cszh3VVY+5JUeqimRgJ2ADY2/3lEtAZOtmwcBlhY0cPW
            Vqga14E7kTGSWKC6W96Nfy9K7L4Ypp8nTMErus181aqwwNfMqnpnuQENBF/Jfl4B
            CADDSh+KdBeNjIclVVnRKt0QT5593yF4WVZt/TgNuaEZ5vKknooVVIq+cJIfY/3l
            Uqq8Te4dEjodtFyKe5Xuego6qjzs8TYFdCAHXpXRoUolT14m+qkJ8rhSrpN0TxIj
            WJbJdm3NlrgTam5RKJw3ShypNUxyolnHelXxqyKDCkxBSDmR6xcdft3wdQl5IkIA
            wxe6nywmSUtpndGLRJdJraJiaWF2IBjFNg3vTEYj4eoehZd4XrvEyLVrMbKZ5m6f
            1o6QURuzSrUH9JT/ivZqCmhPposClXXX0bbi9K0Z/+uVyk6v76ms3O50rIq0L0Ye
            hM8G++qmGO421+0qCLkdD5/jABEBAAGJAR8EGAEIABMFAl/Jfl4JEItXxcKDb0vr
            AhsMAAAbGggAw7lhSWElZpGV1SI2b2K26PB93fVI1tQYV37WIElCJsajF+/ZDfJJ
            2d6ncuQSleH5WRccc4hZfKwysA/epqrCnwc7yKsToZ4sw8xsJF1UtQ5ENtkdArVi
            BJHS4Y2VZ5DEUmr5EghGtZFh9a6aLoeMVM/nrZCLstDVoPKEpLokHu/gebCwfT/n
            9U1dolFIovg6eKACl5xOx+rzcAVp7R4P527jffudz3dKMdLhPrstG0w5YbyfPPwW
            MOPp+kUF45eYdR7kKKk09VrJNkEGJ0KQQ6imqR1Tn0kyu4cvkfqnCUF0rrn7CdBq
            LSCv1QRhgr6TChQf7ynWsPz5gGdVjh3tIw==
            =dsvF
            -----END PGP PUBLIC KEY BLOCK-----        
    package_upgrade: true
    packages:
    - build-essential
    - openssh-server
    - containerd
    - zsh
    - conntrack
    - curl
    - ebtables
    - socat
    - ipset
    - apt-transport-https
    - kubeadm
    locale: en_US.UTF-8
    timezone: Asia/Shanghai
    users:
    - default
    - name: <username>
        sudo: ALL=(ALL) NOPASSWD:ALL
        passwd: "<password>"
        ssh_authorized_keys:
        - "<ssh-pubkey>"
    ssh_authorized_keys:
    - "<ssh-pubkey>"
    
  • LB:
    #cloud-config
    apt:
        primary:
            - uri: http://mirrors.ustc.edu.cn/ubuntu/
            arches:
                - default
    package_upgrade: true
    packages:
    - build-essential
    - openssh-server
    - zsh
    - conntrack
    - curl
    - ebtables
    - socat
    - ipset
    - apt-transport-https
    - haproxy
    - keepalived
    locale: en_US.UTF-8
    timezone: Asia/Shanghai
    users:
    - default
    - name: <username>
        sudo: ALL=(ALL) NOPASSWD:ALL
        passwd: "<password>"
        ssh_authorized_keys:
        - "<ssh-pubkey>"
    ssh_authorized_keys:
    - "<ssh-pubkey>"
    

LXD Network

网络在规划中使用了一个10.248.21.0/24的网段启动节点,1做bridge的网关,2到99的ip空着未来做浮动ip。

lxc network create kubebr0
lxc network edit kubebr0
config:
  ipv4.address: 10.248.21.1/24
  ipv4.dhcp.ranges: 10.248.21.100-10.248.21.199
  ipv4.nat: "true"
  ipv6.address: none
description: ""
name: kubebr0
type: bridge
# ...

顺便一体同一个网络下面的主机,可以直接通过lxd的nameserver解析到另一个容器。

LXD Profile

LXD的profile实际上就是lxc的配置项。

config:
  raw.lxc: |
    lxc.apparmor.profile=unconfined
    lxc.mount.auto=proc:rw sys:rw
    lxc.cgroup.devices.allow=a
    lxc.cap.drop=    
  security.nesting: "true"
  security.privileged: "true"
  security.syscalls.intercept.mount.allowed: overlay,sysfs,tmpfs,proc,cgroup,cgroup2
  user.user-data: |
    #cloud-config
    # 这里粘贴上面cloud init的配置,需要第一行是#cloud-config    
description: Default LXD profile for project kubernetes-cluster
devices:
  eth0:
    name: eth0
    network: kubebr0
    type: nic
  root:
    path: /
    pool: default
    type: disk
name: kubernetes-node
# ...

容器内的容器运行时:containerd

容器内的容器运行时主要需要配置三方面:rootless container,镜像和代理。

rootless container

我前面已经配置好了,直接看diff吧,核心要配置的就是disable_apparmor和restrict_oom_score_adj。

需要注意containerd在快速发展中,配置可能会不完全一样,最好是打印了自己版本的默认配置后修改其中的选项。

下载

containerd config default > /etc/containerd/config-default.toml
diff /etc/containerd/config.toml /etc/containerd/config-default.toml 
44c44
<     disable_apparmor = true
---
>     disable_apparmor = false
55,56c55,56
<     restrict_oom_score_adj = true
<     sandbox_image = "registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.5"
---
>     restrict_oom_score_adj = false
>     sandbox_image = "k8s.gcr.io/pause:3.5"

镜像 / 代理

镜像就是告诉runtime一个仓库下面的image从另一个地方拉取。

containerd在早期版本中可以直接修改config.toml,但是后来这种方法改了。我使用了新的方法,比如说我要修改docker.io的,写到/etc/containerd/certs.d/docker.io/hosts.toml里面

server = "https://vhmjt3oe.mirror.aliyuncs.com" # 需要哪个换哪个

[host."https://vhmjt3oe.mirror.aliyuncs.com"]
  capabilities = ["pull", "resolve"]

[host."https://registry-1.docker.io"]
  capabilities = ["pull", "resolve"]

如果镜像麻烦,而且自己可以通过代理访问各个registry,可以考虑直接设置代理:systemctl edit containerd

Environment=HTTP_PROXY=http://127.0.0.1:8888
Environment=HTTPS_PROXY=http://127.0.0.1:8888

kubeadm

最后就是kubeadm的配置了,这个配置中etcd还是在k8s里面的。我的haproxy有一个浮动ip:10.248.21.10

下载

apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
nodeRegistration:
  criSocket: /run/containerd/containerd.sock
---
apiServer:
  timeoutForControlPlane: 4m0s
  certSANs:
    - "127.0.0.1"
    - "10.248.21.10"
    - "master-1"
    - "master-2"
    - "master-3"
apiVersion: kubeadm.k8s.io/v1beta3
imageRepository: registry.cn-hangzhou.aliyuncs.com/google_containers
kind: ClusterConfiguration
kubernetesVersion: 1.23.0
networking:
  dnsDomain: cluster.lxd
  podSubnet: 10.244.0.0/16
controlPlaneEndpoint: 10.248.21.10
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
featureGates:
  KubeletInUserNamespace: true
---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "iptables" # or "userspace"
conntrack:
# Skip setting sysctl value "net.netfilter.nf_conntrack_max"
  maxPerCore: 0
# Skip setting "net.netfilter.nf_conntrack_tcp_timeout_established"
  tcpEstablishedTimeout: 0s
# Skip setting "net.netfilter.nf_conntrack_tcp_timeout_close"
  tcpCloseWaitTimeout: 0s