
Kubernetes Inside LXD
上一次提到我因为在笔记本上开虚拟机部署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