├── .gitignore
├── Makefile
├── README.md
├── babel.config.js
├── codeblock
├── cni
│ └── ptp.json
├── containerization
│ ├── alpine-tz.dockerfile
│ ├── centos-tz.dockerfile
│ ├── maven-jar-pom.xml
│ ├── mount-tz.yaml
│ ├── tz-env.yaml
│ └── ubuntu-tz.dockerfile
├── graceful
│ └── update-strategy.yaml
├── handle-sigterm
│ ├── sigterm.go
│ ├── sigterm.java
│ ├── sigterm.js
│ ├── sigterm.py
│ └── sigterm.sh
├── hello.go
├── home-network
│ ├── 10-router.conf
│ ├── alist.yaml
│ ├── aria2.yaml
│ ├── clean.sh
│ ├── ddns.yaml
│ ├── dnsmasq.conf
│ ├── dnsmasq.yaml
│ ├── filebrowser.yaml
│ ├── home-assistant.yaml
│ ├── homepage.yaml
│ ├── ikev2.yaml
│ ├── jellyfin.yaml
│ ├── netplan-config-bypass-route.yaml
│ ├── netplan-config-main-route.yaml
│ ├── nfs.yaml
│ ├── nftables-firewall.conf
│ ├── nftables-tproxy.conf
│ ├── nftables.yaml
│ ├── nginx.yaml
│ ├── qbittorrent.yaml
│ ├── radvd.conf
│ ├── radvd.yaml
│ ├── router.json
│ ├── samba.yaml
│ ├── set-rules.sh
│ ├── setup-pppoe.sh
│ └── vpn.env
├── llama
│ ├── download-llama3-8b.yaml
│ ├── llama3-cpu-8b.yaml
│ ├── llama3-gpu-70b.yaml
│ ├── ollama-nodeselector.yaml
│ ├── ollama.yaml
│ └── open-webui.yaml
├── prometheus
│ ├── kubernetes-pods.yaml
│ └── kubernetes-service-endpoints.yaml
├── rbac
│ ├── cluster-admin.yaml
│ ├── limit-sa.yaml
│ ├── readonly-all.yaml
│ ├── readonly-exclude-secret.yaml
│ ├── secret-reader-for-manager-group.yaml
│ └── test-admin-istio-system-readonly.yaml
├── sysctl
│ ├── set=sysctl-init-containers.yaml
│ └── set=sysctl-security-context.yaml
├── test
│ ├── ab.yaml
│ ├── hey.yaml
│ ├── httpbin.yaml
│ ├── nginx.yaml
│ └── wrk.yaml
└── webhook
│ ├── check-annotation.yaml
│ └── object-selector.yaml
├── content
├── README.md
├── appendix
│ ├── faq
│ │ ├── ipvs-conn-reuse-mode.md
│ │ └── why-enable-bridge-nf-call-iptables.md
│ ├── kubectl
│ │ ├── build.md
│ │ ├── get-raw.md
│ │ ├── node.md
│ │ └── pod.md
│ ├── terraform
│ │ ├── tke-serverless.md
│ │ └── tke-vpc-cni.md
│ └── yaml
│ │ ├── rbac.md
│ │ └── test.md
├── apps
│ └── set-sysctl.md
├── basics
│ └── README.md
├── best-practices
│ ├── autoscaling
│ │ ├── hpa-velocity.md
│ │ ├── hpa-with-custom-metrics
│ │ │ ├── keda.md
│ │ │ └── prometheus-adapter.md
│ │ └── keda
│ │ │ ├── cron.md
│ │ │ ├── install.md
│ │ │ ├── overview.md
│ │ │ ├── prometheus.md
│ │ │ └── workload.md
│ ├── configure-healthcheck.md
│ ├── containerization
│ │ ├── crontab.md
│ │ ├── golang.md
│ │ ├── java.md
│ │ ├── logrotate.md
│ │ ├── systemd.md
│ │ ├── thin-image.md
│ │ └── timezone.md
│ ├── dns
│ │ ├── customize-dns-resolution.md
│ │ └── optimize-coredns-performance.md
│ ├── graceful-shutdown
│ │ ├── intro.md
│ │ ├── lb-to-pod-directly.md
│ │ ├── long-connection.md
│ │ ├── pod-termination-proccess.md
│ │ ├── prestop.md
│ │ ├── sigterm.md
│ │ └── update-strategy.md
│ ├── ha
│ │ ├── pod-split-up-scheduling.md
│ │ └── smooth-upgrade.md
│ ├── logging.md
│ ├── long-connection.md
│ ├── ops
│ │ ├── batch-operate-node-with-ansible.md
│ │ ├── etcd-optimization.md
│ │ ├── large-scale-cluster-optimization.md
│ │ ├── securely-maintain-or-offline-node.md
│ │ └── securely-modify-container-root-dir.md
│ ├── performance-optimization
│ │ ├── cpu.md
│ │ └── network.md
│ └── request-limit.md
├── cases
│ ├── devcontainer
│ │ ├── deploy.md
│ │ ├── dind.md
│ │ ├── dockerfile.md
│ │ ├── host.md
│ │ ├── lang.md
│ │ ├── other.md
│ │ ├── overview.md
│ │ ├── packages.md
│ │ ├── ssh.md
│ │ └── sync-config.md
│ ├── home-network
│ │ ├── alist.md
│ │ ├── aria2.md
│ │ ├── containerized-nftables.md
│ │ ├── ddns.md
│ │ ├── dnsmasq.md
│ │ ├── filebrowser.md
│ │ ├── gitops.md
│ │ ├── home-assistant.md
│ │ ├── homepage.md
│ │ ├── ikev2.md
│ │ ├── intro.md
│ │ ├── jellyfin.md
│ │ ├── monitoring.md
│ │ ├── network-config.md
│ │ ├── nfs.md
│ │ ├── prepare.md
│ │ ├── qbittorrent.md
│ │ ├── radvd.md
│ │ ├── samba.md
│ │ └── tproxy.md
│ ├── llama3.md
│ └── nextcloud.md
├── certs
│ ├── sign-certs-with-cfssl.md
│ ├── sign-free-certs-for-dnspod.md
│ └── sign-free-certs-with-cert-manager.md
├── deploy
│ ├── aws
│ │ ├── aws-load-balancer-controller.md
│ │ └── eks.md
│ ├── k3s
│ │ ├── install.md
│ │ └── offline.md
│ ├── kubespray
│ │ ├── install.md
│ │ └── offline.md
│ └── terraform.md
├── dev
│ └── kubebuilder
│ │ ├── init-before-start.md
│ │ ├── multi-version.md
│ │ ├── quickstart.md
│ │ ├── reconcile-trigger.md
│ │ ├── reconcile.md
│ │ ├── remove-api.md
│ │ └── webhook.md
├── gitops
│ └── argocd
│ │ ├── cluster-and-repo.md
│ │ ├── install.md
│ │ └── project.md
├── images
│ ├── podman.md
│ └── sync-images-with-skopeo.md
├── kubectl
│ ├── build.md
│ ├── kubectl-aliases.md
│ ├── kubie.md
│ ├── merge-kubeconfig-with-kubecm.md
│ └── quick-switch-with-kubectx.md
├── monitoring
│ ├── dcgm-exporter.md
│ ├── grafana
│ │ └── ha.md
│ ├── prometheus
│ │ └── annotation-discovery.md
│ └── victoriametrics
│ │ └── install-with-operator.md
├── rbac
│ └── create-user-using-csr-api.md
└── sidebars.ts
├── coscli.log
├── docusaurus.config.ts
├── giscus.json
├── package-lock.json
├── package.json
├── src
├── components
│ ├── Comment.tsx
│ └── FileBlock.tsx
├── css
│ └── custom.scss
├── theme
│ ├── DocCategoryGeneratedIndexPage
│ │ └── index.tsx
│ ├── DocItem
│ │ └── Layout
│ │ │ └── index.tsx
│ └── MDXComponents.tsx
└── utils
│ └── prismDark.ts
├── static
├── img
│ └── logo.svg
├── js
│ └── BigPicture.min.js
└── manifest.json
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | /node_modules
3 |
4 | # Production
5 | /build
6 |
7 | # Generated files
8 | .docusaurus
9 | .cache-loader
10 |
11 | # Misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SHELL := /bin/bash
2 |
3 | start:
4 | npm run start
5 | install:
6 | npm install
7 | outdated:
8 | npm outdated
9 | init: install
10 | git clone --depth=1 git@gitee.com:imroc/kubernetes-guide.git build
11 | gen:
12 | npx docusaurus build --out-dir=./build/out
13 | push:
14 | cd build && git add -A && git commit -m update && git push
15 | update: install gen push
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Kubernetes 实践指南
2 |
3 | 本书将介绍 Kubernetes 相关实战经验与总结,助你成为一名云原生老司机 😎。
4 |
5 | ## 关于本书
6 |
7 | 本书为电子书形式,内容为本人多年的云原生与 Kubernetes 实战经验进行系统性整理的结果,不废话,纯干货。
8 |
9 | ## 在线阅读
10 |
11 | 地址:https://imroc.cc/kubernetes
12 |
13 | ## 评论与互动
14 |
15 | 本书已集成 [giscus](https://giscus.app/zh-CN) 评论系统,欢迎对感兴趣的文章进行评论与交流。
16 |
17 | ## 贡献
18 |
19 | 本书使用 [docusaurus](https://docusaurus.io/) 构建,已集成自动构建和发布,欢迎 Fork 并 PR 来贡献干货内容 (点击左下角 `编辑此页` 按钮可快速修改文章)。
20 |
21 | ## 许可证
22 |
23 | 您可以使用 [署名 - 非商业性使用 - 相同方式共享 4.0 (CC BY-NC-SA 4.0)](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh) 协议共享。
24 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
3 | };
4 |
--------------------------------------------------------------------------------
/codeblock/cni/ptp.json:
--------------------------------------------------------------------------------
1 | {
2 | "cniVersion": "0.3.1",
3 | "name": "ptp",
4 | "plugins": [
5 | {
6 | "ipMasq": false,
7 | "ipam": {
8 | "dataDir": "/run/cni-ipam-state",
9 | "ranges": [
10 | [
11 | {
12 | "subnet": "10.244.2.0/24"
13 | }
14 | ]
15 | ],
16 | "routes": [
17 | {
18 | "dst": "0.0.0.0/0"
19 | }
20 | ],
21 | "type": "host-local"
22 | },
23 | "mtu": 1500,
24 | "type": "ptp"
25 | },
26 | {
27 | "capabilities": {
28 | "portMappings": true
29 | },
30 | "snat": false,
31 | "externalSetMarkChain": "KUBE-MARK-MASQ",
32 | "type": "portmap"
33 | }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/codeblock/containerization/alpine-tz.dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:latest
2 |
3 | # 安装 tzdata,复制里面的时区文件后删除 tzdata,保持精简
4 | RUN apk add --no-cache tzdata && \
5 | cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
6 | apk del tzdata && \
7 | echo "Asia/Shanghai" > /etc/timezone
8 |
--------------------------------------------------------------------------------
/codeblock/containerization/centos-tz.dockerfile:
--------------------------------------------------------------------------------
1 | FROM centos:latest
2 |
3 | RUN rm -f /etc/localtime \
4 | && ln -sv /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
5 | && echo "Asia/Shanghai" > /etc/timezone
6 |
--------------------------------------------------------------------------------
/codeblock/containerization/maven-jar-pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | org.example
8 | http
9 | 1.0-SNAPSHOT
10 |
11 |
12 |
13 | 11
14 | 11
15 |
16 |
17 |
18 |
19 | app
20 |
21 |
22 |
23 | org.apache.maven.plugins
24 | maven-jar-plugin
25 |
26 |
27 |
28 |
29 | org.example.http.HttpTest
30 |
31 | true
32 |
33 | ./lib/
34 |
35 | false
36 |
37 |
38 |
39 |
40 |
41 |
42 | org.apache.maven.plugins
43 | maven-dependency-plugin
44 |
45 |
46 | copy
47 | package
48 |
49 | copy-dependencies
50 |
51 |
52 | ${project.build.directory}/lib
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | org.apache.httpcomponents.client5
63 | httpclient5
64 | 5.1.3
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/codeblock/containerization/mount-tz.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: app
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: app
10 | template:
11 | metadata:
12 | labels:
13 | app: app
14 | spec:
15 | containers:
16 | - name: app
17 | image: app
18 | volumeMounts:
19 | # highlight-start
20 | - name: tz
21 | mountPath: /etc/localtime
22 | # highlight-end
23 | volumes:
24 | # highlight-start
25 | - name: tz
26 | hostPath:
27 | path: /etc/localtime
28 | # highlight-end
29 |
--------------------------------------------------------------------------------
/codeblock/containerization/tz-env.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: app
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: app
10 | template:
11 | metadata:
12 | labels:
13 | app: app
14 | spec:
15 | containers:
16 | - name: app
17 | image: app
18 | env:
19 | # highlight-start
20 | - name: TZ
21 | value: Asia/Shanghai
22 | # highlight-end
23 |
--------------------------------------------------------------------------------
/codeblock/containerization/ubuntu-tz.dockerfile:
--------------------------------------------------------------------------------
1 | FROM ubuntu:latest
2 |
3 | RUN apt update -y && \
4 | DEBIAN_FRONTEND="noninteractive" apt -y install tzdata
5 | RUN ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
6 | dpkg-reconfigure -f noninteractive tzdata
7 |
--------------------------------------------------------------------------------
/codeblock/graceful/update-strategy.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: nginx
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: nginx
10 | strategy:
11 | # highlight-start
12 | type: RollingUpdate
13 | rollingUpdate: # 单个串行升级,等新副本 ready 后才开始销毁旧副本
14 | maxUnavailable: 0
15 | maxSurge: 1
16 | # highlight-end
17 | template:
18 | metadata:
19 | labels:
20 | app: nginx
21 | spec:
22 | containers:
23 | - name: nginx
24 | image: nginx:latest
25 | startupProbe:
26 | httpGet:
27 | path: /
28 | port: 80
29 | # highlight-next-line
30 | successThreshold: 5 # 新副本启动时,连续探测成功多次后才交给 readinessProbe 探测
31 | periodSeconds: 5
32 | readinessProbe:
33 | httpGet:
34 | path: /
35 | port: 80
36 | # highlight-next-line
37 | successThreshold: 1 # 运行过程中探测 1 次成功就认为 ready,可在抖动导致异常后快速恢复服务
38 | periodSeconds: 5
39 |
--------------------------------------------------------------------------------
/codeblock/handle-sigterm/sigterm.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "os/signal"
7 | "syscall"
8 | )
9 |
10 | func main() {
11 | sigs := make(chan os.Signal, 1)
12 | done := make(chan bool, 1)
13 | // registers the channel
14 | signal.Notify(sigs, syscall.SIGTERM)
15 |
16 | go func() {
17 | sig := <-sigs
18 | fmt.Println("Caught SIGTERM, shutting down")
19 | // Finish any outstanding requests, then...
20 | done <- true
21 | }()
22 |
23 | fmt.Println("Starting application")
24 | // Main logic goes here
25 | <-done
26 | fmt.Println("exiting")
27 | }
28 |
--------------------------------------------------------------------------------
/codeblock/handle-sigterm/sigterm.java:
--------------------------------------------------------------------------------
1 | import sun.misc.Signal;
2 | import sun.misc.SignalHandler;
3 |
4 | public class ExampleSignalHandler {
5 | public static void main(String... args) throws InterruptedException {
6 | final long start = System.nanoTime();
7 | Signal.handle(new Signal("TERM"), new SignalHandler() {
8 | public void handle(Signal sig) {
9 | System.out.format("\nProgram execution took %f seconds\n", (System.nanoTime() - start) / 1e9f);
10 | System.exit(0);
11 | }
12 | });
13 | int counter = 0;
14 | while(true) {
15 | System.out.println(counter++);
16 | Thread.sleep(500);
17 | }
18 | }
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/codeblock/handle-sigterm/sigterm.js:
--------------------------------------------------------------------------------
1 | process.on('SIGTERM', () => {
2 | console.log('The service is about to shut down!');
3 |
4 | // Finish any outstanding requests, then...
5 | process.exit(0);
6 | });
7 |
8 |
--------------------------------------------------------------------------------
/codeblock/handle-sigterm/sigterm.py:
--------------------------------------------------------------------------------
1 | import signal, time, os
2 |
3 | def shutdown(signum, frame):
4 | print('Caught SIGTERM, shutting down')
5 | # Finish any outstanding requests, then...
6 | exit(0)
7 |
8 | if __name__ == '__main__':
9 | # Register handler
10 | signal.signal(signal.SIGTERM, shutdown)
11 | # Main logic goes here
12 |
13 |
--------------------------------------------------------------------------------
/codeblock/handle-sigterm/sigterm.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | ## Redirecting Filehanders
4 | ln -sf /proc/$$/fd/1 /log/stdout.log
5 | ln -sf /proc/$$/fd/2 /log/stderr.log
6 |
7 | ## Pre execution handler
8 | pre_execution_handler() {
9 | ## Pre Execution
10 | # TODO: put your pre execution steps here
11 | : # delete this nop
12 | }
13 |
14 | ## Post execution handler
15 | post_execution_handler() {
16 | ## Post Execution
17 | # TODO: put your post execution steps here
18 | : # delete this nop
19 | }
20 |
21 | ## Sigterm Handler
22 | sigterm_handler() {
23 | if [ $pid -ne 0 ]; then
24 | # the above if statement is important because it ensures
25 | # that the application has already started. without it you
26 | # could attempt cleanup steps if the application failed to
27 | # start, causing errors.
28 | kill -15 "$pid"
29 | wait "$pid"
30 | post_execution_handler
31 | fi
32 | exit 143 # 128 + 15 -- SIGTERM
33 | }
34 |
35 | ## Setup signal trap
36 | # on callback execute the specified handler
37 | trap 'sigterm_handler' SIGTERM
38 |
39 | ## Initialization
40 | pre_execution_handler
41 |
42 | ## Start Process
43 | # run process in background and record PID
44 | "$@" >/log/stdout.log 2>/log/stderr.log &
45 | pid="$!"
46 | # Application can log to stdout/stderr, /log/stdout.log or /log/stderr.log
47 |
48 | ## Wait forever until app dies
49 | wait "$pid"
50 | return_code="$?"
51 |
52 | ## Cleanup
53 | post_execution_handler
54 | # echo the return code of the application
55 | exit $return_code
56 |
--------------------------------------------------------------------------------
/codeblock/hello.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | func main() {
8 | for i := 0; i < 10; i++ {
9 | fmt.Println("hello world", i)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/codeblock/home-network/10-router.conf:
--------------------------------------------------------------------------------
1 |
2 | # 作为路由器,启用 ip 转发 (ipv6默认关闭)
3 | net.ipv4.ip_forward=1
4 | net.ipv6.conf.all.forwarding=1
5 | net.ipv6.conf.default.forwarding=1
6 |
7 | # 接收来自运营商(ISP) 的 ipv6 RA (路由通告),以获取公网 ipv6 地址
8 | #net.ipv6.conf.all.accept_ra=2
9 | #net.ipv6.conf.enp1s0.accept_ra=2
10 | #net.ipv6.conf.default.accept_ra=2
11 | #net.ipv6.conf.all.accept_ra_rt_info_max_plen=128
12 | #net.ipv6.conf.default.accept_ra_rt_info_max_plen=128
13 | #net.ipv6.conf.enp1s0.accept_ra_rt_info_max_plen=128
14 |
15 | # 禁用ipv6
16 | net.ipv6.conf.all.accept_ra=0
17 | net.ipv6.conf.enp1s0.accept_ra=0
18 | net.ipv6.conf.default.accept_ra=0
19 | net.ipv6.conf.all.accept_ra_rt_info_max_plen=0
20 | net.ipv6.conf.default.accept_ra_rt_info_max_plen=0
21 | net.ipv6.conf.enp1s0.accept_ra_rt_info_max_plen=0
22 |
23 | # 可选。暂时没发现什么场景一定要禁用 rp_filter,以防万一,先禁用
24 | net.ipv4.conf.all.rp_filter=0
25 | net.ipv4.conf.default.rp_filter=0
26 |
27 | # 允许容器ns的流量被拦截
28 | net.bridge.bridge-nf-call-iptables=0
29 | net.bridge.bridge-nf-call-ip6tables=0
--------------------------------------------------------------------------------
/codeblock/home-network/alist.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: DaemonSet
3 | metadata:
4 | labels:
5 | app: alist
6 | name: alist
7 | namespace: default
8 | spec:
9 | selector:
10 | matchLabels:
11 | app: alist
12 | template:
13 | metadata:
14 | labels:
15 | app: alist
16 | spec:
17 | terminationGracePeriodSeconds: 1
18 | hostAliases:
19 | - hostnames:
20 | - api-cf.nn.ci
21 | ip: 104.21.30.209
22 | containers:
23 | - image: docker.io/xhofe/alist:v3.32.0
24 | imagePullPolicy: IfNotPresent
25 | name: alist
26 | env:
27 | - name: PUID
28 | value: "0"
29 | - name: PGID
30 | value: "0"
31 | - name: UMASK
32 | value: "022"
33 | volumeMounts:
34 | - mountPath: /opt/alist/data
35 | name: data
36 | dnsPolicy: Default
37 | hostNetwork: true
38 | restartPolicy: Always
39 | volumes:
40 | - name: data
41 | hostPath:
42 | path: /data/alist
43 | type: DirectoryOrCreate
44 | updateStrategy:
45 | rollingUpdate:
46 | maxSurge: 0
47 | maxUnavailable: 1
48 | type: RollingUpdate
49 |
--------------------------------------------------------------------------------
/codeblock/home-network/aria2.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: DaemonSet
3 | metadata:
4 | labels:
5 | app: aria2
6 | name: aria2
7 | namespace: default
8 | spec:
9 | selector:
10 | matchLabels:
11 | app: aria2
12 | template:
13 | metadata:
14 | labels:
15 | app: aria2
16 | spec:
17 | terminationGracePeriodSeconds: 1
18 | containers:
19 | - image: docker.io/p3terx/aria2-pro:202209060423
20 | imagePullPolicy: IfNotPresent
21 | name: aria2
22 | env:
23 | - name: LISTEN_PORT
24 | value: "16881"
25 | - name: RPC_PORT
26 | value: "6800"
27 | - name: RPC_SECRET
28 | value: "111111"
29 | - name: PUID
30 | value: "0"
31 | - name: PGID
32 | value: "0"
33 | - name: IPV6_MODE
34 | value: "true"
35 | - name: TZ
36 | value: "Asia/Shanghai"
37 | - name: SPECIAL_MODE
38 | value: "move"
39 | volumeMounts:
40 | - mountPath: /config
41 | name: config
42 | - mountPath: /downloads
43 | name: media
44 | - image: docker.io/p3terx/ariang:latest
45 | imagePullPolicy: IfNotPresent
46 | name: ariang
47 | dnsPolicy: Default
48 | hostNetwork: true
49 | restartPolicy: Always
50 | volumes:
51 | - name: config
52 | hostPath:
53 | path: /data/aria2/config
54 | type: DirectoryOrCreate
55 | - name: media
56 | hostPath:
57 | path: /data/media/downloads
58 | type: DirectoryOrCreate
59 | updateStrategy:
60 | rollingUpdate:
61 | maxSurge: 0
62 | maxUnavailable: 1
63 | type: RollingUpdate
64 |
--------------------------------------------------------------------------------
/codeblock/home-network/clean.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -x
4 |
5 | nft delete table inet proxy # 关闭时清理 nftables,避免拉不了镜像
6 | ip rule delete fwmark 0x1 table 100
7 | ip route delete local 0.0.0.0/0 dev lo table 100
8 | ip -6 rule delete fwmark 0x1 table 100
9 | ip -6 route delete local ::/0 dev lo table 100
10 |
11 | exit 0
12 |
--------------------------------------------------------------------------------
/codeblock/home-network/ddns.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: DaemonSet
3 | metadata:
4 | labels:
5 | app: ddns
6 | name: ddns
7 | namespace: default
8 | spec:
9 | selector:
10 | matchLabels:
11 | app: ddns
12 | template:
13 | metadata:
14 | labels:
15 | app: ddns
16 | spec:
17 | terminationGracePeriodSeconds: 1
18 | containers:
19 | - image: newfuture/ddns:v2.13.3
20 | imagePullPolicy: IfNotPresent
21 | name: ddns
22 | securityContext:
23 | privileged: true
24 | volumeMounts:
25 | - mountPath: /config.json
26 | subPath: config.json
27 | name: config
28 | dnsPolicy: Default
29 | hostNetwork: true
30 | restartPolicy: Always
31 | volumes:
32 | - configMap:
33 | name: ddns-config
34 | name: config
35 | updateStrategy:
36 | rollingUpdate:
37 | maxSurge: 0
38 | maxUnavailable: 1
39 | type: RollingUpdate
40 |
41 |
--------------------------------------------------------------------------------
/codeblock/home-network/dnsmasq.conf:
--------------------------------------------------------------------------------
1 | log-queries=extra
2 | no-resolv
3 | no-poll
4 | server=61.139.2.69
5 | strict-order
6 | log-dhcp
7 | cache-size=2000
8 | dhcp-range=10.10.10.15,10.10.10.254,255.255.255.0,12h
9 | dhcp-authoritative
10 | dhcp-leasefile=/var/lib/dnsmasq/dnsmasq.leases
11 | dhcp-option=option:router,10.10.10.2
12 | dhcp-option=option:dns-server,61.139.2.69,218.6.200.139
13 |
--------------------------------------------------------------------------------
/codeblock/home-network/dnsmasq.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: DaemonSet
3 | metadata:
4 | labels:
5 | app: dnsmasq
6 | name: dnsmasq
7 | namespace: default
8 | spec:
9 | selector:
10 | matchLabels:
11 | app: dnsmasq
12 | template:
13 | metadata:
14 | labels:
15 | app: dnsmasq
16 | spec:
17 | terminationGracePeriodSeconds: 3
18 | containers:
19 | - image: imroc/dnsmasq:2.90
20 | imagePullPolicy: IfNotPresent
21 | name: dnsmasq
22 | securityContext:
23 | privileged: true
24 | volumeMounts:
25 | - mountPath: /etc/dnsmasq.conf
26 | name: dnsmasq-config
27 | subPath: dnsmasq.conf
28 | - mountPath: /var/lib/dnsmasq
29 | name: lease
30 | hostNetwork: true
31 | restartPolicy: Always
32 | volumes:
33 | - configMap:
34 | name: dnsmasq-config
35 | name: dnsmasq-config
36 | - name: lease
37 | hostPath:
38 | path: /data/lease
39 | type: DirectoryOrCreate
40 | updateStrategy:
41 | rollingUpdate:
42 | maxSurge: 0
43 | maxUnavailable: 1
44 | type: RollingUpdate
45 |
46 |
--------------------------------------------------------------------------------
/codeblock/home-network/filebrowser.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: DaemonSet
3 | metadata:
4 | labels:
5 | app: filebrowser
6 | name: filebrowser
7 | namespace: default
8 | spec:
9 | selector:
10 | matchLabels:
11 | app: filebrowser
12 | template:
13 | metadata:
14 | labels:
15 | app: filebrowser
16 | spec:
17 | terminationGracePeriodSeconds: 1
18 | containers:
19 | - image: filebrowser/filebrowser:v2.27.0
20 | args:
21 | - "-p"
22 | - "8567"
23 | - "-r"
24 | - "/data"
25 | - "--username=roc"
26 | - "--password=$2a$10$q/0NjHYLYvP/rcB1VdRBxeVg/AnaPILgMJYyrEnOpw6mhimhsgjeG" # 111111
27 | imagePullPolicy: IfNotPresent
28 | name: filebrowser
29 | volumeMounts:
30 | - mountPath: /data
31 | name: data
32 | dnsPolicy: Default
33 | hostNetwork: true
34 | restartPolicy: Always
35 | volumes:
36 | - name: data
37 | hostPath:
38 | path: /data
39 | type: DirectoryOrCreate
40 | updateStrategy:
41 | rollingUpdate:
42 | maxSurge: 0
43 | maxUnavailable: 1
44 | type: RollingUpdate
45 |
--------------------------------------------------------------------------------
/codeblock/home-network/home-assistant.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: DaemonSet
3 | metadata:
4 | labels:
5 | app: home-assistant
6 | name: home-assistant
7 | namespace: default
8 | spec:
9 | selector:
10 | matchLabels:
11 | app: home-assistant
12 | template:
13 | metadata:
14 | labels:
15 | app: home-assistant
16 | spec:
17 | terminationGracePeriodSeconds: 1
18 | containers:
19 | - image: homeassistant/home-assistant:stable
20 | imagePullPolicy: IfNotPresent
21 | name: home-assistant
22 | ports:
23 | - containerPort: 8123
24 | name: web
25 | env:
26 | - name: TZ
27 | value: Asia/Shanghai
28 | volumeMounts:
29 | - mountPath: /config
30 | name: config
31 | - mountPath: /run/dbus
32 | name: dbus
33 | readOnly: true
34 | dnsPolicy: Default
35 | hostNetwork: true
36 | restartPolicy: Always
37 | volumes:
38 | - name: config
39 | hostPath:
40 | path: /data/home-assistant/config
41 | type: DirectoryOrCreate
42 | - name: dbus
43 | hostPath:
44 | path: /run/dbus
45 | updateStrategy:
46 | rollingUpdate:
47 | maxSurge: 0
48 | maxUnavailable: 1
49 | type: RollingUpdate
50 |
--------------------------------------------------------------------------------
/codeblock/home-network/homepage.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: DaemonSet
3 | metadata:
4 | labels:
5 | app: homepage
6 | name: homepage
7 | namespace: default
8 | spec:
9 | selector:
10 | matchLabels:
11 | app: homepage
12 | template:
13 | metadata:
14 | labels:
15 | app: homepage
16 | spec:
17 | terminationGracePeriodSeconds: 1
18 | containers:
19 | - image: docker.io/imroc/homepage:v0.8.8
20 | imagePullPolicy: IfNotPresent
21 | name: homepage
22 | env:
23 | - name: PORT
24 | value: "80"
25 | volumeMounts:
26 | - mountPath: /app/config/services.yaml
27 | name: config
28 | subPath: services.yaml
29 | - mountPath: /app/config/settings.yaml
30 | name: config
31 | subPath: settings.yaml
32 | - mountPath: /app/config/widgets.yaml
33 | name: config
34 | subPath: widgets.yaml
35 | - mountPath: /app/config/bookmarks.yaml
36 | name: config
37 | subPath: bookmarks.yaml
38 | dnsPolicy: Default
39 | hostNetwork: true
40 | restartPolicy: Always
41 | volumes:
42 | - name: config
43 | configMap:
44 | name: homepage-config
45 | updateStrategy:
46 | rollingUpdate:
47 | maxSurge: 0
48 | maxUnavailable: 1
49 | type: RollingUpdate
50 |
--------------------------------------------------------------------------------
/codeblock/home-network/ikev2.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: DaemonSet
3 | metadata:
4 | labels:
5 | app: ikev2
6 | name: ikev2
7 | namespace: default
8 | spec:
9 | selector:
10 | matchLabels:
11 | app: ikev2
12 | template:
13 | metadata:
14 | labels:
15 | app: ikev2
16 | spec:
17 | containers:
18 | - image: imroc/ipsec-vpn-server:4.12
19 | imagePullPolicy: IfNotPresent
20 | name: ikev2
21 | ports:
22 | - containerPort: 500
23 | protocol: UDP
24 | hostPort: 500
25 | - containerPort: 4500
26 | protocol: UDP
27 | hostPort: 4500
28 | envFrom:
29 | - secretRef:
30 | name: ikev2-secret
31 | securityContext:
32 | privileged: true
33 | volumeMounts:
34 | - mountPath: /etc/ipsec.d
35 | name: ikev2-vpn-data
36 | - mountPath: /lib/modules
37 | name: mod
38 | readOnly: true
39 | dnsPolicy: Default
40 | restartPolicy: Always
41 | volumes:
42 | - hostPath:
43 | path: /lib/modules
44 | type: Directory
45 | name: mod
46 | - secret:
47 | secretName: ikev2-vpn-data
48 | name: ikev2-vpn-data
49 | updateStrategy:
50 | rollingUpdate:
51 | maxSurge: 0
52 | maxUnavailable: 1
53 | type: RollingUpdate
54 |
--------------------------------------------------------------------------------
/codeblock/home-network/jellyfin.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: DaemonSet
3 | metadata:
4 | labels:
5 | app: jellyfin
6 | name: jellyfin
7 | namespace: default
8 | spec:
9 | selector:
10 | matchLabels:
11 | app: jellyfin
12 | template:
13 | metadata:
14 | labels:
15 | app: jellyfin
16 | spec:
17 | terminationGracePeriodSeconds: 1
18 | containers:
19 | - image: jellyfin/jellyfin:latest
20 | imagePullPolicy: IfNotPresent
21 | name: jellyfin
22 | resources:
23 | limits:
24 | cpu: "1"
25 | memory: 1Gi
26 | requests:
27 | cpu: 1m
28 | memory: 16Mi
29 | env:
30 | - name: TZ
31 | value: Asia/Shanghai
32 | securityContext:
33 | runAsUser: 0
34 | runAsGroup: 0
35 | privileged: true
36 | volumeMounts:
37 | - mountPath: /dev/dri
38 | name: dri
39 | - mountPath: /data/media
40 | name: media
41 | - mountPath: /config
42 | name: config
43 | - mountPath: /cache
44 | name: cache
45 | dnsPolicy: Default
46 | hostNetwork: true
47 | restartPolicy: Always
48 | volumes:
49 | - name: dri
50 | hostPath:
51 | path: /dev/dri
52 | - name: config
53 | hostPath:
54 | path: /data/jellyfin/config
55 | type: DirectoryOrCreate
56 | - name: cache
57 | hostPath:
58 | path: /data/jellyfin/cache
59 | type: DirectoryOrCreate
60 | - name: media
61 | hostPath:
62 | path: /data/media
63 | type: DirectoryOrCreate
64 | updateStrategy:
65 | rollingUpdate:
66 | maxSurge: 0
67 | maxUnavailable: 1
68 | type: RollingUpdate
69 |
--------------------------------------------------------------------------------
/codeblock/home-network/netplan-config-bypass-route.yaml:
--------------------------------------------------------------------------------
1 | network:
2 | version: 2
3 | renderer: networkd
4 | ethernets:
5 | enp1s0: # 主网口,默认路由指向主路由 IP
6 | optional: true
7 | accept-ra: false
8 | dhcp4: no
9 | dhcp6: no
10 | addresses:
11 | - 10.10.10.2/24
12 | - fddd:dddd:dddd:dddd::2/64
13 | routes:
14 | - to: default
15 | via: 10.10.10.14
16 | enp6s0: # 预留的管理网口,极端情况下用网线连管理网口登录 ssh
17 | optional: true
18 | accept-ra: false
19 | dhcp4: no
20 | dhcp6: no
21 | addresses:
22 | - 10.10.11.1/24
23 |
--------------------------------------------------------------------------------
/codeblock/home-network/netplan-config-main-route.yaml:
--------------------------------------------------------------------------------
1 | network:
2 | version: 2
3 | renderer: networkd
4 | ethernets:
5 | enp1s0: # 拨号网口
6 | optional: true
7 | accept-ra: false
8 | dhcp4: no
9 | dhcp6: no
10 | addresses:
11 | - 192.168.11.1/24
12 | routes: # k3s 需要默认路由,不然会报错,导致 dhcp 服务起不来,从而导致局域网连不上,这里随便设置一个(启动会被pppoe拨号覆盖,这里设置用于没有拨号的情况下也连上机器)
13 | - to: default
14 | via: 192.168.11.1
15 | enp2s0:
16 | optional: true
17 | accept-ra: false
18 | dhcp4: no
19 | dhcp6: no
20 | addresses: # 固定网卡所拥有的内网IP地址段
21 | - 10.10.10.2/24
22 | - fddd:dddd:dddd:dddd::2/64
23 | enp6s0: # 预留的管理网口,极端情况下用网线连管理网口登录 ssh
24 | optional: true
25 | accept-ra: false
26 | dhcp4: no
27 | dhcp6: no
28 | addresses:
29 | - 10.10.11.1/24
30 |
--------------------------------------------------------------------------------
/codeblock/home-network/nfs.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: DaemonSet
3 | metadata:
4 | labels:
5 | app: nfs
6 | name: nfs
7 | namespace: default
8 | spec:
9 | selector:
10 | matchLabels:
11 | app: nfs
12 | template:
13 | metadata:
14 | labels:
15 | app: nfs
16 | spec:
17 | terminationGracePeriodSeconds: 1
18 | containers:
19 | - image: docker.io/erichough/nfs-server:2.2.1
20 | imagePullPolicy: IfNotPresent
21 | name: nfs
22 | securityContext:
23 | privileged: true
24 | volumeMounts:
25 | - mountPath: /data/media
26 | name: media
27 | - mountPath: /lib/modules
28 | name: mod
29 | readOnly: true
30 | - mountPath: /etc/exports
31 | name: exports
32 | subPath: exports
33 | dnsPolicy: Default
34 | hostNetwork: true
35 | restartPolicy: Always
36 | volumes:
37 | - name: media
38 | hostPath:
39 | path: /data/media
40 | type: DirectoryOrCreate
41 | - name: mod
42 | hostPath:
43 | path: /lib/modules
44 | - name: exports
45 | configMap:
46 | name: nfs-exports
47 | updateStrategy:
48 | rollingUpdate:
49 | maxSurge: 0
50 | maxUnavailable: 1
51 | type: RollingUpdate
52 |
--------------------------------------------------------------------------------
/codeblock/home-network/nftables-tproxy.conf:
--------------------------------------------------------------------------------
1 | #!/usr/sbin/nft -f
2 |
3 | table inet proxy
4 | delete table inet proxy
5 |
6 | table inet proxy {
7 |
8 | # 保留网段,参考:https://zh.wikipedia.org/zh-sg/%E4%BF%9D%E7%95%99IP%E5%9C%B0%E5%9D%80
9 | set byp4 {
10 | typeof ip daddr
11 | flags interval
12 | elements = {
13 | 0.0.0.0/8,
14 | 10.0.0.0/8,
15 | 100.64.0.0/10,
16 | 127.0.0.0/8,
17 | 169.254.0.0/16,
18 | 172.16.0.0/12,
19 | 192.0.0.0/24,
20 | 192.0.2.0/24,
21 | 192.88.99.0/24,
22 | 192.168.0.0/16,
23 | 198.18.0.0/15,
24 | 198.51.100.0/24,
25 | 203.0.113.0/24,
26 | 224.0.0.0/4,
27 | 240.0.0.0/4
28 | }
29 | }
30 | set byp6 {
31 | typeof ip6 daddr
32 | flags interval
33 | elements = {
34 | ::,
35 | ::1,
36 | ::ffff:0:0:0/96,
37 | 100::/64,
38 | 64:ff9b::/96,
39 | 2001::/32,
40 | 2001:10::/28,
41 | 2001:20::/28,
42 | 2001:db8::/32,
43 | 2002::/16,
44 | fc00::/7,
45 | fe80::/10,
46 | ff00::/8
47 | }
48 | }
49 |
50 | chain prerouting {
51 | type filter hook prerouting priority filter; policy accept;
52 |
53 | # 只拦截内网部分设备(10.10.10.0/28 用于拦截内网部分指定设备的流量,10.42.0.0/16 用于拦截容器网络流量)
54 | fib saddr type != local ip saddr != { 10.10.10.0/28, 10.42.0.0/16 } counter return
55 |
56 | # 放行发往 local 的。如果连接已被 tproxy 拦截,后续流量会被认为是 local 的(代理会在本机监听目标地址),流量可以直接转发给代理进程,无需拦截,直接 return 避免再次被 tproxy 拦截
57 | fib daddr type local counter return
58 |
59 | # 放行 reply 方向的
60 | ct direction reply counter return
61 |
62 | # 将局域网设备访问公网的流量标记为需要拦截
63 | meta l4proto {tcp,udp} ct mark != 1 ct state new,related counter jump do_proxy
64 |
65 | # 拦截已经标记要拦截的流量(连接的第一个包在这里被 tproxy 到代理进程,然后代理进程会监听目标地址,后续的包无需拦截就可以被转发给代理进程)
66 | meta l4proto {tcp,udp} ct mark 1 tproxy to :12345 meta mark set 1 counter
67 | }
68 |
69 | chain do_proxy {
70 |
71 | # 放行内网
72 | ip daddr @byp4 counter return
73 | ip6 daddr @byp6 counter return
74 |
75 | # 其余全部打上 mark,标记流量需要拦截
76 | ct mark set 1 counter
77 | }
78 |
79 | chain output {
80 | type route hook output priority filter; policy accept;
81 |
82 | # 放行发往 local 的
83 | fib daddr type local counter return
84 |
85 | # 放行 reply 方向的
86 | ct direction reply counter return
87 |
88 | # 放行代理发出的包。
89 | meta skgid eq 23333 counter return
90 |
91 | # 尝试给需要拦截的连接进行 mark
92 | meta l4proto {tcp,udp} ct state new,related counter jump do_proxy
93 |
94 | # 让报文走策略路由重新进入 PREROUTING,以便被代理。
95 | ct mark 1 meta mark set 1 counter
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/codeblock/home-network/nftables.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: DaemonSet
3 | metadata:
4 | labels:
5 | app: nftables
6 | name: nftables
7 | spec:
8 | selector:
9 | matchLabels:
10 | app: nftables
11 | template:
12 | metadata:
13 | labels:
14 | app: nftables
15 | spec:
16 | terminationGracePeriodSeconds: 3
17 | containers:
18 | - image: imroc/nftables:ubuntu-24.04
19 | imagePullPolicy: IfNotPresent
20 | name: nftables
21 | securityContext:
22 | privileged: true
23 | command: ["/script/entrypoint.sh"]
24 | volumeMounts:
25 | - mountPath: /etc/nftables
26 | name: nftables-config
27 | - mountPath: /script
28 | name: nftables-script
29 | hostNetwork: true
30 | volumes:
31 | - configMap:
32 | name: nftables-config
33 | name: nftables-config
34 | - configMap:
35 | name: nftables-script
36 | defaultMode: 0755
37 | name: nftables-script
38 | updateStrategy:
39 | rollingUpdate:
40 | maxSurge: 0
41 | maxUnavailable: 1
42 | type: RollingUpdate
43 |
--------------------------------------------------------------------------------
/codeblock/home-network/nginx.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: DaemonSet
3 | metadata:
4 | labels:
5 | app: nginx
6 | name: nginx
7 | namespace: default
8 | spec:
9 | selector:
10 | matchLabels:
11 | app: nginx
12 | template:
13 | metadata:
14 | labels:
15 | app: nginx
16 | spec:
17 | terminationGracePeriodSeconds: 1
18 | containers:
19 | - image: nginx:latest
20 | imagePullPolicy: IfNotPresent
21 | name: nginx
22 | securityContext:
23 | privileged: true
24 | volumeMounts:
25 | - mountPath: /etc/nginx
26 | name: config
27 | dnsPolicy: Default
28 | hostNetwork: true
29 | restartPolicy: Always
30 | volumes:
31 | - configMap:
32 | name: nginx-config
33 | name: config
34 | updateStrategy:
35 | rollingUpdate:
36 | maxSurge: 0
37 | maxUnavailable: 1
38 | type: RollingUpdate
39 |
--------------------------------------------------------------------------------
/codeblock/home-network/qbittorrent.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: DaemonSet
3 | metadata:
4 | labels:
5 | app: qbittorrent
6 | name: qbittorrent
7 | namespace: default
8 | spec:
9 | selector:
10 | matchLabels:
11 | app: qbittorrent
12 | template:
13 | metadata:
14 | labels:
15 | app: qbittorrent
16 | spec:
17 | terminationGracePeriodSeconds: 1
18 | containers:
19 | - image: docker.io/linuxserver/qbittorrent:5.0.0
20 | imagePullPolicy: IfNotPresent
21 | name: qbittorrent
22 | env:
23 | - name: PUID
24 | value: "0"
25 | - name: PGID
26 | value: "0"
27 | - name: TZ
28 | value: "Asia/Shanghai"
29 | - name: WEBUI_PORT
30 | value: "9367"
31 | - name: TORRENTING_PORT
32 | value: "16991"
33 | volumeMounts:
34 | - mountPath: /config
35 | name: config
36 | - mountPath: /downloads
37 | name: media
38 | dnsPolicy: Default
39 | hostNetwork: true
40 | restartPolicy: Always
41 | volumes:
42 | - name: config
43 | hostPath:
44 | path: /data/qbittorrent/config
45 | type: DirectoryOrCreate
46 | - name: media
47 | hostPath:
48 | path: /data/media
49 | type: DirectoryOrCreate
50 | updateStrategy:
51 | rollingUpdate:
52 | maxSurge: 0
53 | maxUnavailable: 1
54 | type: RollingUpdate
55 |
--------------------------------------------------------------------------------
/codeblock/home-network/radvd.conf:
--------------------------------------------------------------------------------
1 | interface enp2s0 {
2 | # 网卡启用路由通告(RA)
3 | AdvSendAdvert on;
4 | # 启用 Home Agent(iOS、macOS等移动设备加入网络时发送Home Agent请求获取ipv6信息)
5 | AdvHomeAgentFlag on;
6 | AdvHomeAgentInfo on;
7 | MinRtrAdvInterval 10;
8 | MaxRtrAdvInterval 60;
9 | prefix fddd:dddd:dddd:dddd::2/64 {
10 | AdvOnLink on;
11 | AdvAutonomous on;
12 | AdvRouterAddr on;
13 | };
14 | route fddd:dddd:dddd:dddd::2/64 {
15 | AdvRoutePreference high;
16 | AdvRouteLifetime 3600;
17 | RemoveRoute off;
18 | };
19 | };
20 |
--------------------------------------------------------------------------------
/codeblock/home-network/radvd.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: DaemonSet
3 | metadata:
4 | labels:
5 | app: radvd
6 | name: radvd
7 | namespace: default
8 | spec:
9 | selector:
10 | matchLabels:
11 | app: radvd
12 | template:
13 | metadata:
14 | labels:
15 | app: radvd
16 | spec:
17 | initContainers:
18 | - image: imroc/radvd:2.18
19 | imagePullPolicy: IfNotPresent
20 | name: sysctl
21 | securityContext:
22 | privileged: true
23 | command:
24 | - sh
25 | - -c
26 | - |
27 | sysctl -w net.ipv6.conf.all.accept_ra_rt_info_max_plen=128
28 | sysctl -w net.ipv6.conf.default.accept_ra_rt_info_max_plen=128
29 | sysctl -w net.ipv6.conf.all.forwarding=1
30 | sysctl -w net.ipv6.conf.default.forwarding=1
31 | containers:
32 | - image: imroc/radvd:2.18
33 | imagePullPolicy: IfNotPresent
34 | name: radvd
35 | securityContext:
36 | privileged: true
37 | args: ["--debug", "5"]
38 | volumeMounts:
39 | - mountPath: /etc/radvd.d
40 | name: radvd-config
41 | hostNetwork: true
42 | restartPolicy: Always
43 | volumes:
44 | - configMap:
45 | name: radvd-config
46 | name: radvd-config
47 | updateStrategy:
48 | rollingUpdate:
49 | maxSurge: 0
50 | maxUnavailable: 1
51 | type: RollingUpdate
52 |
--------------------------------------------------------------------------------
/codeblock/home-network/samba.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: DaemonSet
3 | metadata:
4 | labels:
5 | app: samba
6 | name: samba
7 | namespace: default
8 | spec:
9 | selector:
10 | matchLabels:
11 | app: samba
12 | template:
13 | metadata:
14 | labels:
15 | app: samba
16 | spec:
17 | terminationGracePeriodSeconds: 1
18 | containers:
19 | - image: docker.io/dperson/samba:latest
20 | args:
21 | - "-u"
22 | - "roc;111111"
23 | - "-s"
24 | - "data;/data/;yes;no;yes;all;all;all"
25 | - "-w"
26 | - "WORKGROUP"
27 | - "-g"
28 | - "force user=roc"
29 | - "-g"
30 | - "guest account=roc"
31 | - "-S"
32 | imagePullPolicy: IfNotPresent
33 | name: samba
34 | securityContext:
35 | privileged: true
36 | volumeMounts:
37 | - mountPath: /data
38 | name: data
39 | dnsPolicy: Default
40 | hostNetwork: true
41 | restartPolicy: Always
42 | volumes:
43 | - name: data
44 | hostPath:
45 | path: /data
46 | type: DirectoryOrCreate
47 | updateStrategy:
48 | rollingUpdate:
49 | maxSurge: 0
50 | maxUnavailable: 1
51 | type: RollingUpdate
52 |
--------------------------------------------------------------------------------
/codeblock/home-network/set-rules.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -x
4 |
5 | while :; do
6 | export https_proxy=http://127.0.0.1:10809 # 假设代理会启动 10809 端口的 HTTP 代理
7 | code=$(curl -I -o /dev/null -m 5 -s -w %{http_code} https://www.google.com | xargs) # 通过代理探测目标地址是否正常响应
8 | if [[ "${code}" == "200" ]]; then # 探测成功就退出探测循环,准备设置策略路由
9 | break
10 | fi
11 | echo "bad code:${code};" >/tmp/error.log
12 | sleep 5s
13 | done
14 |
15 | echo "proxy is ready, set up rules"
16 | ip rule list | grep "from all fwmark 0x1 lookup 100"
17 | if [ $? -ne 0 ]; then
18 | echo "add rule and route"
19 | ip rule add fwmark 0x1 table 100
20 | ip route add local 0.0.0.0/0 dev lo table 100
21 | ip -6 rule add fwmark 0x1 table 100
22 | ip -6 route add local ::/0 dev lo table 100
23 | fi
24 |
25 | nft -f /etc/nftables/nftables.conf # 设置 nftables 规则
26 | exit 0
27 |
28 |
--------------------------------------------------------------------------------
/codeblock/home-network/setup-pppoe.sh:
--------------------------------------------------------------------------------
1 | #!/bin/env sh
2 |
3 | # interface to pppoe workload
4 | INTERFACE=enp1s0
5 |
6 | if [ "${IFACE}" = "${INTERFACE}" ] ; then
7 | echo "running pon ${INTERFACE}..."
8 | pon dsl-provider
9 | fi
--------------------------------------------------------------------------------
/codeblock/home-network/vpn.env:
--------------------------------------------------------------------------------
1 | # 定义 VPN 配置文件的密码,以及登录 VPN 的用户名和密
2 | VPN_IPSEC_PSK=123456
3 | VPN_USER=roc
4 | VPN_PASSWORD=123456
5 |
6 | # Define additional VPN users
7 | # - DO NOT put "" or '' around values, or add space around =
8 | # - DO NOT use these special characters within values: \ " '
9 | # - Usernames and passwords must be separated by spaces
10 | # VPN_ADDL_USERS=additional_username_1 additional_username_2
11 | # VPN_ADDL_PASSWORDS=additional_password_1 additional_password_2
12 |
13 | # 改成家里公网 IP 对应的域名
14 | VPN_DNS_NAME=home.yourdomain.com
15 |
16 | # Specify a name for the first IKEv2 client
17 | # - Use one word only, no special characters except '-' and '_'
18 | # - The default is 'vpnclient' if not specified
19 | VPN_CLIENT_NAME=roc
20 |
21 | # 可以改成家里宽带使用的 DNS 地址
22 | VPN_DNS_SRV1=61.139.2.69
23 | VPN_DNS_SRV2=218.6.200.139
24 |
25 | # Protect IKEv2 client config files using a password
26 | # - By default, no password is required when importing IKEv2 client configuration
27 | # - Uncomment if you want to protect these files using a random password
28 | VPN_PROTECT_CONFIG=yes
29 |
30 |
--------------------------------------------------------------------------------
/codeblock/llama/download-llama3-8b.yaml:
--------------------------------------------------------------------------------
1 | initContainers:
2 | - name: pull
3 | image: ollama/ollama:latest
4 | tty: true
5 | stdin: true
6 | command:
7 | - bash
8 | - -c
9 | - |
10 | # highlight-next-line
11 | model="llama3:8b" # 替换需要使用的模型,模型库列表: https://ollama.com/library/llama3
12 | ollama serve &
13 | sleep 5 # 等待 ollama server 就绪,就绪后才能执行 ollama cli 工具的命令
14 | result=`ollama list | grep $model`
15 | if [ "$result" == "" ]; then
16 | echo "downloading model $model"
17 | ollama pull $model
18 | else
19 | echo "model $model already been downloaded"
20 | fi
21 | volumeMounts:
22 | - name: ollama-volume
23 | mountPath: /root/.ollama
24 |
--------------------------------------------------------------------------------
/codeblock/llama/llama3-cpu-8b.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: StatefulSet
3 | metadata:
4 | name: ollama
5 | namespace: llama
6 | spec:
7 | serviceName: "ollama"
8 | replicas: 1
9 | selector:
10 | matchLabels:
11 | app: ollama
12 | template:
13 | metadata:
14 | labels:
15 | app: ollama
16 | spec:
17 | # highlight-start
18 | initContainers:
19 | - name: pull
20 | image: ollama/ollama:latest
21 | tty: true
22 | stdin: true
23 | command:
24 | - bash
25 | - -c
26 | - |
27 | # highlight-next-line
28 | model="llama3:8b" # 替换需要使用的模型,模型库列表: https://ollama.com/library/llama3
29 | ollama serve &
30 | sleep 5 # 等待 ollama server 就绪,就绪后才能执行 ollama cli 工具的命令
31 | result=`ollama list | grep $model`
32 | if [ "$result" == "" ]; then
33 | echo "downloading model $model"
34 | ollama pull $model
35 | else
36 | echo "model $model already been downloaded"
37 | fi
38 | volumeMounts:
39 | - name: ollama-volume
40 | mountPath: /root/.ollama
41 | # highlight-end
42 | containers:
43 | - name: ollama
44 | image: ollama/ollama:latest
45 | ports:
46 | - containerPort: 11434
47 | resources:
48 | requests:
49 | cpu: "2000m"
50 | memory: "2Gi"
51 | nvidia.com/gpu: "0" # 如果要用 Nvidia GPU,这里声明下 GPU 卡
52 | limits:
53 | cpu: "4000m"
54 | memory: "4Gi"
55 | volumeMounts:
56 | - name: ollama-volume
57 | mountPath: /root/.ollama
58 | tty: true
59 | volumeClaimTemplates:
60 | - metadata:
61 | name: ollama-volume
62 | spec:
63 | accessModes: ["ReadWriteOnce"]
64 | resources:
65 | requests:
66 | storage: 200Gi # 注意要确保磁盘容量能够容纳得下模型的体积
67 | ---
68 | apiVersion: v1
69 | kind: Service
70 | metadata:
71 | name: ollama
72 | namespace: llama
73 | labels:
74 | app: ollama
75 | spec:
76 | type: ClusterIP
77 | ports:
78 | - port: 11434
79 | protocol: TCP
80 | targetPort: 11434
81 | selector:
82 | app: ollama
83 |
--------------------------------------------------------------------------------
/codeblock/llama/llama3-gpu-70b.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: StatefulSet
3 | metadata:
4 | name: ollama
5 | namespace: llama
6 | spec:
7 | serviceName: "ollama"
8 | replicas: 1
9 | selector:
10 | matchLabels:
11 | app: ollama
12 | template:
13 | metadata:
14 | labels:
15 | app: ollama
16 | spec:
17 | initContainers:
18 | - name: pull
19 | image: ollama/ollama:latest
20 | tty: true
21 | stdin: true
22 | command:
23 | - bash
24 | - -c
25 | - |
26 | # highlight-next-line
27 | model="llama3:70b" # 替换需要使用的模型,模型库列表: https://ollama.com/library/llama3
28 | ollama serve &
29 | sleep 5 # 等待 ollama server 就绪,就绪后才能执行 ollama cli 工具的命令
30 | result=`ollama list | grep $model`
31 | if [ "$result" == "" ]; then
32 | echo "downloading model $model"
33 | ollama pull $model
34 | else
35 | echo "model $model already been downloaded"
36 | fi
37 | volumeMounts:
38 | - name: ollama-volume
39 | mountPath: /root/.ollama
40 | containers:
41 | - name: ollama
42 | image: ollama/ollama:latest
43 | ports:
44 | - containerPort: 11434
45 | resources:
46 | requests:
47 | cpu: "2000m"
48 | memory: "2Gi"
49 | # highlight-next-line
50 | nvidia.com/gpu: "1" # 声明使用一张 N 卡
51 | limits:
52 | cpu: "4000m"
53 | memory: "4Gi"
54 | volumeMounts:
55 | - name: ollama-volume
56 | mountPath: /root/.ollama
57 | tty: true
58 | volumeClaimTemplates:
59 | - metadata:
60 | name: ollama-volume
61 | spec:
62 | accessModes: ["ReadWriteOnce"]
63 | resources:
64 | requests:
65 | # highlight-next-line
66 | storage: 200Gi # 注意要确保磁盘容量能够容纳得下模型的体积
67 | ---
68 | apiVersion: v1
69 | kind: Service
70 | metadata:
71 | name: ollama
72 | namespace: llama
73 | labels:
74 | app: ollama
75 | spec:
76 | type: ClusterIP
77 | ports:
78 | - port: 11434
79 | protocol: TCP
80 | targetPort: 11434
81 | selector:
82 | app: ollama
83 |
--------------------------------------------------------------------------------
/codeblock/llama/ollama-nodeselector.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: StatefulSet
3 | metadata:
4 | name: ollama
5 | namespace: llama
6 | spec:
7 | serviceName: "ollama"
8 | replicas: 1
9 | selector:
10 | matchLabels:
11 | app: ollama
12 | template:
13 | metadata:
14 | labels:
15 | app: ollama
16 | spec:
17 | # highlight-start
18 | nodeSelector:
19 | gpu: v100
20 | # highlight-end
21 | containers:
22 | - name: ollama
23 | image: ollama/ollama:latest
24 | ports:
25 | - containerPort: 11434
26 | resources:
27 | requests:
28 | cpu: "2000m"
29 | memory: "2Gi"
30 | nvidia.com/gpu: "4"
31 | limits:
32 | cpu: "4000m"
33 | memory: "4Gi"
34 | volumeMounts:
35 | - name: ollama-volume
36 | mountPath: /root/.ollama
37 | tty: true
38 | volumeClaimTemplates:
39 | - metadata:
40 | name: ollama-volume
41 | spec:
42 | accessModes: ["ReadWriteOnce"]
43 | resources:
44 | requests:
45 | storage: 200Gi
46 |
--------------------------------------------------------------------------------
/codeblock/llama/ollama.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: StatefulSet
3 | metadata:
4 | name: ollama
5 | namespace: llama
6 | spec:
7 | serviceName: "ollama"
8 | replicas: 1
9 | selector:
10 | matchLabels:
11 | app: ollama
12 | template:
13 | metadata:
14 | labels:
15 | app: ollama
16 | spec:
17 | containers:
18 | - name: ollama
19 | image: ollama/ollama:latest
20 | ports:
21 | - containerPort: 11434
22 | resources:
23 | requests:
24 | cpu: "2000m"
25 | memory: "2Gi"
26 | # highlight-next-line
27 | nvidia.com/gpu: "0" # 如果要用 Nvidia GPU,这里声明下 GPU 卡
28 | limits:
29 | cpu: "4000m"
30 | memory: "4Gi"
31 | volumeMounts:
32 | - name: ollama-volume
33 | mountPath: /root/.ollama
34 | tty: true
35 | volumeClaimTemplates:
36 | - metadata:
37 | name: ollama-volume
38 | spec:
39 | accessModes: ["ReadWriteOnce"]
40 | resources:
41 | requests:
42 | # highlight-next-line
43 | storage: 200Gi # 注意要确保磁盘容量能够容纳得下模型的体积
44 | ---
45 | apiVersion: v1
46 | kind: Service
47 | metadata:
48 | name: ollama
49 | namespace: llama
50 | labels:
51 | app: ollama
52 | spec:
53 | type: ClusterIP
54 | ports:
55 | - port: 11434
56 | protocol: TCP
57 | targetPort: 11434
58 | selector:
59 | app: ollama
60 |
--------------------------------------------------------------------------------
/codeblock/llama/open-webui.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: PersistentVolumeClaim
3 | metadata:
4 | name: webui-pvc
5 | namespace: llama
6 | labels:
7 | app: webui
8 | spec:
9 | accessModes: ["ReadWriteOnce"]
10 | resources:
11 | requests:
12 | storage: 2Gi
13 |
14 | ---
15 | apiVersion: apps/v1
16 | kind: Deployment
17 | metadata:
18 | name: webui
19 | namespace: llama
20 | spec:
21 | replicas: 1
22 | selector:
23 | matchLabels:
24 | app: webui
25 | template:
26 | metadata:
27 | labels:
28 | app: webui
29 | spec:
30 | containers:
31 | - name: webui
32 | # highlight-next-line
33 | image: imroc/open-webui:main # docker hub 中的 mirror 镜像,长期自动同步,可放心使用
34 | env:
35 | - name: OLLAMA_BASE_URL
36 | # highlight-next-line
37 | value: http://ollama:11434 # ollama 的地址
38 | tty: true
39 | ports:
40 | - containerPort: 8080
41 | resources:
42 | requests:
43 | cpu: "500m"
44 | memory: "500Mi"
45 | limits:
46 | cpu: "1000m"
47 | memory: "1Gi"
48 | volumeMounts:
49 | - name: webui-volume
50 | mountPath: /app/backend/data
51 | volumes:
52 | - name: webui-volume
53 | persistentVolumeClaim:
54 | claimName: webui-pvc
55 |
56 | ---
57 | apiVersion: v1
58 | kind: Service
59 | metadata:
60 | name: webui
61 | namespace: llama
62 | labels:
63 | app: webui
64 | spec:
65 | type: ClusterIP
66 | ports:
67 | - port: 8080
68 | protocol: TCP
69 | targetPort: 8080
70 | selector:
71 | app: webui
72 |
--------------------------------------------------------------------------------
/codeblock/prometheus/kubernetes-pods.yaml:
--------------------------------------------------------------------------------
1 | - job_name: "kubernetes-pods"
2 |
3 | kubernetes_sd_configs:
4 | - role: pod
5 |
6 | relabel_configs:
7 | # prometheus relabel to scrape only pods that have
8 | # `prometheus.io/scrape: "true"` annotation.
9 | - source_labels:
10 | - __meta_kubernetes_pod_annotation_prometheus_io_scrape
11 | action: keep
12 | regex: true
13 |
14 | # prometheus relabel to customize metric path based on pod
15 | # `prometheus.io/path: ` annotation.
16 | - source_labels:
17 | - __meta_kubernetes_pod_annotation_prometheus_io_path
18 | action: replace
19 | target_label: __metrics_path__
20 | regex: (.+)
21 |
22 | # prometheus relabel to scrape only single, desired port for the pod
23 | # based on pod `prometheus.io/port: ` annotation.
24 | - source_labels:
25 | - __address__
26 | - __meta_kubernetes_pod_annotation_prometheus_io_port
27 | action: replace
28 | regex: ([^:]+)(?::\d+)?;(\d+)
29 | replacement: $1:$2
30 | target_label: __address__
31 | - action: labelmap
32 | regex: __meta_kubernetes_pod_label_(.+)
33 | - source_labels: [__meta_kubernetes_namespace]
34 | action: replace
35 | target_label: namespace
36 | - source_labels: [__meta_kubernetes_pod_name]
37 | action: replace
38 | target_label: pod
39 |
--------------------------------------------------------------------------------
/codeblock/prometheus/kubernetes-service-endpoints.yaml:
--------------------------------------------------------------------------------
1 | - job_name: "kubernetes-service-endpoints"
2 |
3 | kubernetes_sd_configs:
4 | - role: endpoints
5 |
6 | relabel_configs:
7 | # prometheus relabel to scrape only endpoints that have
8 | # `prometheus.io/scrape: "true"` annotation.
9 | - source_labels:
10 | - __meta_kubernetes_service_annotation_prometheus_io_scrape
11 | action: keep
12 | regex: true
13 |
14 | # prometheus relabel to customize metric path based on endpoints
15 | # `prometheus.io/path: ` annotation.
16 | - source_labels:
17 | - __meta_kubernetes_service_annotation_prometheus_io_path
18 | action: replace
19 | target_label: __metrics_path__
20 | regex: (.+)
21 |
22 | # prometheus relabel to scrape only single, desired port for the service based
23 | # on endpoints `prometheus.io/port: ` annotation.
24 | - source_labels:
25 | - __address__
26 | - __meta_kubernetes_service_annotation_prometheus_io_port
27 | action: replace
28 | regex: ([^:]+)(?::\d+)?;(\d+)
29 | replacement: $1:$2
30 | target_label: __address__
31 |
32 | # prometheus relabel to configure scrape scheme for all service scrape targets
33 | # based on endpoints `prometheus.io/scheme: ` annotation.
34 | - source_labels:
35 | - __meta_kubernetes_service_annotation_prometheus_io_scheme
36 | action: replace
37 | target_label: __scheme__
38 | regex: (https?)
39 | - action: labelmap
40 | regex: __meta_kubernetes_service_label_(.+)
41 | - source_labels: [__meta_kubernetes_namespace]
42 | action: replace
43 | target_label: namespace
44 | - source_labels: [__meta_kubernetes_service_name]
45 | action: replace
46 | target_label: service
47 |
--------------------------------------------------------------------------------
/codeblock/rbac/cluster-admin.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ServiceAccount
3 | metadata:
4 | name: cluster-admin
5 | namespace: kube-system
6 |
7 | ---
8 | apiVersion: rbac.authorization.k8s.io/v1
9 | kind: ClusterRole
10 | metadata:
11 | name: cluster-admin
12 | rules:
13 | - apiGroups: ["*"]
14 | resources: ["*"]
15 | verbs: ["*"]
16 |
17 | ---
18 | apiVersion: rbac.authorization.k8s.io/v1
19 | kind: ClusterRoleBinding
20 | metadata:
21 | name: cluster-admin
22 | subjects:
23 | - kind: ServiceAccount
24 | name: cluster-admin
25 | namespace: kube-system
26 | roleRef:
27 | kind: ClusterRole
28 | name: cluster-admin
29 | apiGroup: rbac.authorization.k8s.io
30 |
--------------------------------------------------------------------------------
/codeblock/rbac/limit-sa.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ServiceAccount
3 | metadata:
4 | name: build-robot
5 | namespace: build
6 |
7 | ---
8 | apiVersion: rbac.authorization.k8s.io/v1
9 | kind: Role
10 | metadata:
11 | namespace: build
12 | name: pod-reader
13 | rules:
14 | - apiGroups: [""]
15 | resources: ["pods", "pods/log"]
16 | verbs: ["get", "list"]
17 |
18 | ---
19 | apiVersion: rbac.authorization.k8s.io/v1
20 | kind: RoleBinding
21 | metadata:
22 | name: read-pods
23 | namespace: build
24 | subjects:
25 | - kind: ServiceAccount
26 | name: build-robot
27 | namespace: build
28 | roleRef:
29 | kind: Role
30 | name: pod-reader
31 | apiGroup: rbac.authorization.k8s.io
32 |
--------------------------------------------------------------------------------
/codeblock/rbac/readonly-all.yaml:
--------------------------------------------------------------------------------
1 | kind: ClusterRole
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | metadata:
4 | name: readonly
5 | rules:
6 | - apiGroups: ["*"]
7 | resources: ["*"]
8 | verbs: ["get", "watch", "list"]
9 |
10 | ---
11 | kind: ClusterRoleBinding
12 | apiVersion: rbac.authorization.k8s.io/v1
13 | metadata:
14 | name: readonly-to-roc
15 | subjects:
16 | - kind: User
17 | name: roc
18 | apiGroup: rbac.authorization.k8s.io
19 | roleRef:
20 | kind: ClusterRole
21 | name: readonly
22 | apiGroup: rbac.authorization.k8s.io
23 |
--------------------------------------------------------------------------------
/codeblock/rbac/readonly-exclude-secret.yaml:
--------------------------------------------------------------------------------
1 | kind: ClusterRole
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | metadata:
4 | name: readonly
5 | rules:
6 | - apiGroups: [""]
7 | resources:
8 | - bindings
9 | - componentstatuses
10 | - configmaps
11 | - endpoints
12 | - events
13 | - limitranges
14 | - namespaces
15 | - nodes
16 | - persistentvolumeclaims
17 | - persistentvolumes
18 | - pods
19 | - podtemplates
20 | - replicationcontrollers
21 | - resourcequotas
22 | - serviceaccounts
23 | - services
24 | verbs: ["get", "list"]
25 | - apiGroups:
26 | - cert-manager.io
27 | - admissionregistration.k8s.io
28 | - apiextensions.k8s.io
29 | - apiregistration.k8s.io
30 | - apps
31 | - authentication.k8s.io
32 | - autoscaling
33 | - batch
34 | - certificaterequests.cert-manager.io
35 | - certificates.cert-manager.io
36 | - certificates.k8s.io
37 | - cloud.tencent.com
38 | - coordination.k8s.io
39 | - discovery.k8s.io
40 | - events.k8s.io
41 | - extensions
42 | - install.istio.io
43 | - metrics.k8s.io
44 | - monitoring.coreos.com
45 | - networking.istio.io
46 | - node.k8s.io
47 | - policy
48 | - rbac.authorization.k8s.io
49 | - scheduling.k8s.io
50 | - security.istio.io
51 | - storage.k8s.io
52 | resources: ["*"]
53 | verbs: ["get", "list"]
54 |
55 | ---
56 | apiVersion: rbac.authorization.k8s.io/v1
57 | kind: ClusterRoleBinding
58 | metadata:
59 | name: roc
60 | roleRef:
61 | apiGroup: rbac.authorization.k8s.io
62 | kind: ClusterRole
63 | name: readonly
64 | subjects:
65 | - apiGroup: rbac.authorization.k8s.io
66 | kind: User
67 | name: roc
68 |
--------------------------------------------------------------------------------
/codeblock/rbac/secret-reader-for-manager-group.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: rbac.authorization.k8s.io/v1
2 | kind: ClusterRole
3 | metadata:
4 | name: secret-reader
5 | rules:
6 | - apiGroups: [""]
7 | resources: ["secrets"]
8 | verbs: ["get", "watch", "list"]
9 | ---
10 | apiVersion: rbac.authorization.k8s.io/v1
11 | kind: ClusterRoleBinding
12 | metadata:
13 | name: read-secrets-global
14 | subjects:
15 | - kind: Group
16 | name: manager
17 | apiGroup: rbac.authorization.k8s.io
18 | roleRef:
19 | kind: ClusterRole
20 | name: secret-reader
21 | apiGroup: rbac.authorization.k8s.io
22 |
--------------------------------------------------------------------------------
/codeblock/rbac/test-admin-istio-system-readonly.yaml:
--------------------------------------------------------------------------------
1 | kind: Role
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | metadata:
4 | name: admin
5 | namespace: test
6 | rules:
7 | - apiGroups: ["*"]
8 | resources: ["*"]
9 | verbs: ["*"]
10 |
11 | ---
12 | kind: RoleBinding
13 | apiVersion: rbac.authorization.k8s.io/v1
14 | metadata:
15 | name: admin-to-roc
16 | namespace: test
17 | subjects:
18 | - kind: User
19 | name: roc
20 | apiGroup: rbac.authorization.k8s.io
21 | roleRef:
22 | kind: Role
23 | name: admin
24 | apiGroup: rbac.authorization.k8s.io
25 |
26 | ---
27 | kind: Role
28 | apiVersion: rbac.authorization.k8s.io/v1
29 | metadata:
30 | name: readonly
31 | namespace: istio-system
32 | rules:
33 | - apiGroups: ["*"]
34 | resources: ["*"]
35 | verbs: ["get", "watch", "list"]
36 |
37 | ---
38 | kind: RoleBinding
39 | apiVersion: rbac.authorization.k8s.io/v1
40 | metadata:
41 | name: readonly-to-roc
42 | namespace: istio-system
43 | subjects:
44 | - kind: User
45 | name: roc
46 | apiGroup: rbac.authorization.k8s.io
47 | roleRef:
48 | kind: Role
49 | name: readonly
50 | apiGroup: rbac.authorization.k8s.io
51 |
--------------------------------------------------------------------------------
/codeblock/sysctl/set=sysctl-init-containers.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: nginx
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: nginx
10 | template:
11 | metadata:
12 | labels:
13 | app: nginx
14 | spec:
15 | # highlight-add-start
16 | initContainers:
17 | - image: busybox
18 | command:
19 | - sh
20 | - -c
21 | - |
22 | sysctl -w net.core.somaxconn=65535
23 | sysctl -w net.ipv4.ip_local_port_range="1024 65535"
24 | sysctl -w net.ipv4.tcp_tw_reuse=1
25 | sysctl -w fs.file-max=1048576
26 | imagePullPolicy: Always
27 | name: setsysctl
28 | securityContext:
29 | privileged: true
30 | # highlight-add-end
31 | containers:
32 | - name: nginx
33 | image: nginx:latest
34 |
--------------------------------------------------------------------------------
/codeblock/sysctl/set=sysctl-security-context.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: nginx
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: nginx
10 | template:
11 | metadata:
12 | labels:
13 | app: nginx
14 | spec:
15 | # highlight-add-start
16 | securityContext:
17 | sysctls:
18 | - name: net.core.somaxconn
19 | value: "1024"
20 | - name: net.core.somaxconn
21 | value: "1024"
22 | # highlight-add-end
23 | containers:
24 | - name: nginx
25 | image: nginx:latest
26 |
--------------------------------------------------------------------------------
/codeblock/test/ab.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: ab
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: ab
10 | template:
11 | metadata:
12 | labels:
13 | app: ab
14 | spec:
15 | containers:
16 | - image: imroc/ab:latest
17 | name: ab
18 | command: ["sleep", "infinity"]
19 |
--------------------------------------------------------------------------------
/codeblock/test/hey.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: hey
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: hey
10 | template:
11 | metadata:
12 | labels:
13 | app: hey
14 | spec:
15 | containers:
16 | - image: imroc/hey:latest
17 | name: hey
18 | command: ["sleep", "infinity"]
19 |
--------------------------------------------------------------------------------
/codeblock/test/httpbin.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | labels:
5 | app: httpbin
6 | name: httpbin
7 | spec:
8 | ports:
9 | - port: 80
10 | protocol: TCP
11 | targetPort: 80
12 | selector:
13 | app: httpbin
14 | type: ClusterIP
15 |
16 | ---
17 | apiVersion: apps/v1
18 | kind: Deployment
19 | metadata:
20 | name: httpbin
21 | spec:
22 | replicas: 1
23 | selector:
24 | matchLabels:
25 | app: httpbin
26 | template:
27 | metadata:
28 | labels:
29 | app: httpbin
30 | spec:
31 | containers:
32 | - image: kennethreitz/httpbin:latest
33 | name: httpbin
34 |
--------------------------------------------------------------------------------
/codeblock/test/nginx.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | labels:
5 | app: nginx
6 | name: nginx
7 | spec:
8 | ports:
9 | - port: 80
10 | protocol: TCP
11 | targetPort: 80
12 | selector:
13 | app: nginx
14 | type: ClusterIP
15 |
16 | ---
17 | apiVersion: apps/v1
18 | kind: Deployment
19 | metadata:
20 | name: nginx
21 | spec:
22 | replicas: 1
23 | selector:
24 | matchLabels:
25 | app: nginx
26 | template:
27 | metadata:
28 | labels:
29 | app: nginx
30 | spec:
31 | containers:
32 | - image: nginx:latest
33 | name: nginx
34 |
--------------------------------------------------------------------------------
/codeblock/test/wrk.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: wrk
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: wrk
10 | template:
11 | metadata:
12 | labels:
13 | app: wrk
14 | spec:
15 | containers:
16 | - image: imroc/wrk:latest
17 | name: wrk
18 | command: ["sleep", "infinity"]
19 |
--------------------------------------------------------------------------------
/codeblock/webhook/check-annotation.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: admissionregistration.k8s.io/v1
2 | kind: ValidatingWebhookConfiguration
3 | metadata:
4 | name: validating-webhook-configuration
5 | webhooks:
6 | - name: vpod-v1.kb.io
7 | # highlight-add-start
8 | matchConditions:
9 | - name: check-annotation
10 | expression: |
11 | 'annotations' in object.metadata && 'networking.cloud.tencent.com/enable-clb-port-mapping' in object.metadata.annotations
12 | # highlight-add-end
13 |
--------------------------------------------------------------------------------
/codeblock/webhook/object-selector.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: admissionregistration.k8s.io/v1
2 | kind: ValidatingWebhookConfiguration
3 | metadata:
4 | name: validating-webhook-configuration
5 | webhooks:
6 | - name: vpod-v1.kb.io
7 | # highlight-add-start
8 | objectSelector:
9 | matchExpressions:
10 | - key: sidecar.istio.io/inject
11 | operator: NotIn
12 | values:
13 | - "false"
14 | # highlight-add-end
15 |
--------------------------------------------------------------------------------
/content/README.md:
--------------------------------------------------------------------------------
1 | # Kubernetes 实践指南
2 |
3 | 本书将介绍 Kubernetes 相关实战经验与总结,助你成为一名云原生老司机 😎。
4 |
5 | ## 关于本书
6 |
7 | 本书为电子书形式,内容为本人多年的云原生与 Kubernetes 实战经验进行系统性整理的结果,不废话,纯干货。
8 |
9 | ## 评论与互动
10 |
11 | 本书已集成 [giscus](https://giscus.app/zh-CN) 评论系统,欢迎对感兴趣的文章进行评论与交流。
12 |
13 | ## 贡献
14 |
15 | 本书使用 [docusaurus](https://docusaurus.io/) 构建,已集成自动构建和发布,欢迎 Fork 并 PR 来贡献干货内容 (点击左下角 `编辑此页` 按钮可快速修改文章)。
16 |
17 | ## 许可证
18 |
19 | 您可以使用 [署名 - 非商业性使用 - 相同方式共享 4.0 (CC BY-NC-SA 4.0)](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh) 协议共享。
20 |
--------------------------------------------------------------------------------
/content/appendix/faq/why-enable-bridge-nf-call-iptables.md:
--------------------------------------------------------------------------------
1 | # 为什么要开 bridge-nf-call-iptables?
2 |
3 | Kubernetes 环境中,很多时候都要求节点内核参数开启 `bridge-nf-call-iptables`:
4 |
5 | ```bash
6 | sysctl -w net.bridge.bridge-nf-call-iptables=1
7 | ```
8 |
9 | > 参考官方文档 [Network Plugin Requirements](https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/#network-plugin-requirements)
10 |
11 | 如果不开启或中途因某些操作导致参数被关闭了,就可能造成一些奇奇怪怪的网络问题,排查起来非常麻烦。
12 |
13 | 为什么要开启呢?本文就来跟你详细掰扯下。
14 |
15 | ## 基于网桥的容器网络
16 |
17 | Kubernetes 集群网络有很多种实现,有很大一部分都用到了 Linux 网桥:
18 |
19 | 
20 |
21 | * 每个 Pod 的网卡都是 veth 设备,veth pair 的另一端连上宿主机上的网桥。
22 | * 由于网桥是虚拟的二层设备,同节点的 Pod 之间通信直接走二层转发,跨节点通信才会经过宿主机 eth0。
23 |
24 | ## Service 同节点通信问题
25 |
26 | 不管是 iptables 还是 ipvs 转发模式,Kubernetes 中访问 Service 都会进行 DNAT,将原本访问 ClusterIP:Port 的数据包 DNAT 成 Service 的某个 Endpoint (PodIP:Port),然后内核将连接信息插入 conntrack 表以记录连接,目的端回包的时候内核从 conntrack 表匹配连接并反向 NAT,这样原路返回形成一个完整的连接链路:
27 |
28 | 
29 |
30 | 但是 Linux 网桥是一个虚拟的二层转发设备,而 iptables conntrack 是在三层上,所以如果直接访问同一网桥内的地址,就会直接走二层转发,不经过 conntrack:
31 | 1. Pod 访问 Service,目的 IP 是 Cluster IP,不是网桥内的地址,走三层转发,会被 DNAT 成 PodIP:Port。
32 | 2. 如果 DNAT 后是转发到了同节点上的 Pod,目的 Pod 回包时发现目的 IP 在同一网桥上,就直接走二层转发了,没有调用 conntrack,导致回包时没有原路返回 (见下图)。
33 |
34 | 
35 |
36 | 由于没有原路返回,客户端与服务端的通信就不在一个 "频道" 上,不认为处在同一个连接,也就无法正常通信。
37 |
38 | 常见的问题现象就是偶现 DNS 解析失败,当 coredns 所在节点上的 pod 解析 dns 时,dns 请求落到当前节点的 coredns pod 上时,就可能发生这个问题。
39 |
40 | ## 开启 bridge-nf-call-iptables
41 |
42 | 如果 Kubernetes 环境的网络链路中走了 bridge 就可能遇到上述 Service 同节点通信问题,而 Kubernetes 很多网络实现都用到了 bridge。
43 |
44 | `bridge-nf-call-iptables` 这个内核参数 (置为 1),表示 bridge 设备在二层转发时也去调用 iptables 配置的三层规则 (包含 conntrack),所以开启这个参数就能够解决上述 Service 同节点通信问题,这也是为什么在 Kubernetes 环境中,大多都要求开启 `bridge-nf-call-iptables` 的原因。
45 |
--------------------------------------------------------------------------------
/content/appendix/kubectl/build.md:
--------------------------------------------------------------------------------
1 | # 自行编译 kubectl
2 |
3 | 如果对 kubectl 有些特殊需求,进行了二次开发,可通过下面的方式编译:
4 |
5 | ```bash
6 | make kubectl KUBE_BUILD_PLATFORMS=darwin/arm64
7 | ```
8 |
9 | - `KUBE_BUILD_PLATFORMS` 指定要编译的目标平台。
10 |
--------------------------------------------------------------------------------
/content/appendix/kubectl/pod.md:
--------------------------------------------------------------------------------
1 | # Pod 相关
2 |
3 | ## 清理 Pod
4 |
5 |
6 |
7 |
8 | ``` bash
9 | kubectl get pod -o wide --all-namespaces --no-headers | awk '{if($4=="Evicted"){cmd="kubectl -n "$1" delete pod "$2; system(cmd)}}'
10 | ```
11 |
12 |
13 |
14 |
15 |
16 | ``` bash
17 | kubectl get pod -o wide --all-namespaces --no-headers | awk '{if($4!="Running"){cmd="kubectl -n "$1" delete pod "$2; system(cmd)}}'
18 | ```
19 |
20 |
21 |
22 |
23 | ## 升级镜像
24 |
25 | ``` bash
26 | export NAMESPACE="kube-system"
27 | export WORKLOAD_TYPE="daemonset"
28 | export WORKLOAD_NAME="ip-masq-agent"
29 | export CONTAINER_NAME="ip-masq-agent"
30 | export IMAGE="ccr.ccs.tencentyun.com/library/ip-masq-agent:v2.5.0"
31 | ```
32 |
33 | ``` bash
34 | kubectl -n $NAMESPACE patch $WORKLOAD_TYPE $WORKLOAD_NAME --patch '{"spec": {"template": {"spec": {"containers": [{"name": "$CONTAINER_NAME","image": "$IMAGE" }]}}}}'
35 | ```
36 |
37 | ## 查看某命名空间的镜像列表
38 |
39 | ```bash
40 | kubectl -n kube-system get pod -ojsonpath='{range .items[*]}{range .spec.containers[*]}{"\n"}{.image}{end}{end}' | sort | uniq
41 | ```
42 |
--------------------------------------------------------------------------------
/content/appendix/terraform/tke-serverless.md:
--------------------------------------------------------------------------------
1 | # TKE Serverless 集群
2 |
3 | ```hcl title="main.tf"
4 | terraform {
5 | required_providers {
6 | tencentcloud = {
7 | source = "tencentcloudstack/tencentcloud"
8 | version = "1.80.4"
9 | }
10 | }
11 | }
12 |
13 | provider "tencentcloud" {
14 | secret_id = "************************************" # 云 API 密钥 SecretId
15 | secret_key = "********************************" # 云 API 密钥 SecretKey
16 | region = "ap-shanghai" # 地域,完整可用地域列表参考: https://cloud.tencent.com/document/product/213/6091
17 | }
18 |
19 |
20 | data "tencentcloud_vpc_instances" "myvpc" {
21 | name = "myvpc" # 指定 VPC 名称
22 | }
23 |
24 | data "tencentcloud_vpc_subnets" "mysubnet" {
25 | vpc_id = data.tencentcloud_vpc_instances.myvpc.instance_list.0.vpc_id
26 | name = "mysubnet" # 指定子网名称
27 | }
28 |
29 | resource "tencentcloud_eks_cluster" "myserverless" {
30 | cluster_name = "roc-test-serverless" # 指定 serverless 集群名称
31 | k8s_version = "1.24.4" # 指定 serverless 集群版本
32 |
33 | public_lb {
34 | enabled = true # 打开公网访问 (kubectl 远程操作集群)
35 | allow_from_cidrs = ["0.0.0.0/0"]
36 | }
37 |
38 | vpc_id = data.tencentcloud_vpc_instances.roctest.instance_list.0.vpc_id
39 | subnet_ids = [
40 | data.tencentcloud_vpc_subnets.mysubnet.instance_list.0.subnet_id
41 | ]
42 | cluster_desc = "roc test cluster" # 集群描述
43 | service_subnet_id = data.tencentcloud_vpc_subnets.mysubnet.instance_list.0.subnet_id
44 | enable_vpc_core_dns = true
45 | need_delete_cbs = true
46 | }
47 | ```
48 |
49 |
--------------------------------------------------------------------------------
/content/appendix/yaml/rbac.md:
--------------------------------------------------------------------------------
1 | # RBAC 相关
2 |
3 | ## 给 roc 授权 test 命名空间所有权限,istio-system 命名空间的只读权限
4 |
5 |
6 |
7 | ## 给 roc 授权整个集群的只读权限
8 |
9 |
10 |
11 | ## 给 manager 用户组里所有用户授权 secret 读权限
12 |
13 |
14 |
15 | ## 给 roc 授权集群只读权限 (secret读权限除外)
16 |
17 | Secret 读权限比较敏感,不要轻易放开,k8s 的 Role/ClusterRole 没有提供类似 "某资源除外" 的能力,secret 在 core group 下,所以只排除 secret 读权限的话需要列举其它所有 core 下面的资源,另外加上其它所有可能的 group 所有资源(包括CRD):
18 |
19 |
20 |
21 | > 可以借助 `kubectl api-resources -o name` 来列举。
22 |
23 | ## 限制 ServiceAccount 权限
24 |
25 | 授权 `build-robot` 这个 ServiceAccount 读取 build 命名空间中 Pod 的信息和 log 的权限:
26 |
27 |
28 |
29 | ## ServiceAccount 最高权限
30 |
31 |
32 |
--------------------------------------------------------------------------------
/content/appendix/yaml/test.md:
--------------------------------------------------------------------------------
1 | # 测试服务
2 |
3 | ## 压测客户端
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ## 测试服务端
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/content/apps/set-sysctl.md:
--------------------------------------------------------------------------------
1 | # 为 Pod 设置内核参数
2 |
3 | 本文介绍为 Pod 设置内核参数的几种方式。
4 |
5 | ## 在 securityContext 中指定 sysctl
6 |
7 | 自 k8s 1.12 起,[sysctls](https://kubernetes.io/docs/tasks/administer-cluster/sysctl-cluster/) 特性 beta 并默认开启,允许用户在 pod 的 `securityContext` 中设置内核参数,用法示例:
8 |
9 |
10 |
11 | 不过使用该方法,默认情况下有些认为是 unsafe 的参数是不能改的,需要将其配到 kubelet 的 `--allowed-unsafe-sysctls` 中才可以用。
12 |
13 | ## 使用 initContainers 设置 sysctl
14 |
15 | 如果希望设置内核参数更简单通用,可以在 initContainer 中设置,不过这个要求给 initContainer 打开 `privileged` 权限。示例:
16 |
17 |
18 |
19 | > 这里用了 privileged 容器,只是为了让这个 container 有权限修改当前容器网络命名空间中的内核参数,只要 Pod 没使用 hostNetwork,内核参数的修改是不会影响 Node 上的内核参数的,两者是隔离的,所以不需要担心会影响 Node 上其它 Pod 的内核参数 (hostNetwork 的 Pod 就不要在 Pod 上修改内核参数了)。
20 |
21 | ## 使用 tuning CNI 插件统一设置 sysctl
22 |
23 | 如果想要为所有 Pod 统一配置某些内核参数,可以使用 [tuning](https://github.com/containernetworking/plugins/tree/master/plugins/meta/tuning) 这个 CNI 插件来做:
24 |
25 | ```json
26 | {
27 | "name": "mytuning",
28 | "type": "tuning",
29 | "sysctl": {
30 | "net.core.somaxconn": "500",
31 | "net.ipv4.tcp_tw_reuse": "1"
32 | }
33 | }
34 | ```
35 |
36 | ## 最佳实践
37 |
38 | 一般不建议使用 CNI 插件统一设置 sysctl,根据业务需求按需设置即可。如果 securityContext 支持你所需要设置的内核参数,直接用 securityContext 设置,可避免特权容器,如果 securityContext 不支持你所需要的内核参数,或者对安全要求不高的话,可以考虑用 initContainers 特权容器来设置 sysctl。
39 |
40 | ## 参考资料
41 |
42 | * [Using sysctls in a Kubernetes Cluster](https://kubernetes.io/docs/tasks/administer-cluster/sysctl-cluster/)
43 | * [tuning 插件文档](https://www.cni.dev/plugins/current/meta/tuning/)
44 |
--------------------------------------------------------------------------------
/content/basics/README.md:
--------------------------------------------------------------------------------
1 | # Kubernetes 基础实践
2 |
3 | 分享一些 Kubernetes 的基础实践。
4 |
--------------------------------------------------------------------------------
/content/best-practices/autoscaling/hpa-with-custom-metrics/keda.md:
--------------------------------------------------------------------------------
1 | # KEDA 方案
2 |
3 | ## 概述
4 |
5 | KEDA 是一个开源的 Kubernetes 基于事件驱动的弹性伸缩器(参考 [KEDA 介绍](../keda/overview))),官方内置了几十种常用的触发器,其中也包含 prometheus 触发器,即可以根据 proemtheus 中的监控数据进行伸缩。
6 |
7 | ## Prometheus 触发器使用方法
8 |
9 | 参考 [KEDA 基于 Prometheus 自定义指标的弹性伸缩](../keda/prometheus) 。
10 |
11 | ## 与 prometheus-adapter 对比
12 |
13 | [prometheus-adapter](https://github.com/kubernetes-sigs/prometheus-adapter) 也支持相同的能力,即根据 Prometheus 中的监控指标数据进行伸缩,但相比 KEDA 的方案有以下不足:
14 |
15 | * 每次新增自定义指标,都要改动 `prometheus-adapter` 的配置,且改配置是集中式管理的,不支持通过 CRD 管理,配置维护起来比较麻烦,而 KEDA 方案则只需要配置 `ScaledObject` 或 `ScaledJob` 这种 CRD,不同业务使用不同的 YAML 文件维护,利于配置维护。
16 | * `prometheus-adapter` 的配置语法晦涩难懂,不能直接写 `PromQL`,需要学习一下 `prometheus-adapter` 的配置语法,有一定的学习成本,而 KEDA 的 prometheus 配置则非常简单,指标可以直接写 `PromQL` 查询语句,简单明了。
17 | * `prometheus-adapter` 只支持根据 Prometheus 监控数据进行伸缩,而对于 KEDA 来说,Prometheus 只是众多触发器中的一种。
18 |
19 | 综上,推荐使用 KEDA 方案。
20 |
--------------------------------------------------------------------------------
/content/best-practices/autoscaling/keda/cron.md:
--------------------------------------------------------------------------------
1 | # 定时水平伸缩 (Cron 触发器)
2 |
3 | ## Cron 触发器
4 |
5 | KEDA 支持 Cron 触发器,即使用 Cron 表达式来配置周期性的定时扩缩容,用法参考 [KEDA Scalers: Cron](https://keda.sh/docs/latest/scalers/cron/)。
6 |
7 | Cron 触发器适用于有周期性特征的业务,比如业务流量有固定的周期性波峰和波谷特征。
8 |
9 | ## 案例:每天固定时间点的秒杀活动
10 |
11 | 秒杀活动的特征是时间比较固定,可以在活动开始前提前扩容,以下展示了 `ScaledObject` 配置示例。
12 |
13 | ```yaml showLineNumbers
14 | apiVersion: keda.sh/v1alpha1
15 | kind: ScaledObject
16 | metadata:
17 | name: seckill
18 | spec:
19 | scaleTargetRef:
20 | apiVersion: apps/v1
21 | kind: Deployment
22 | name: seckill
23 | pollingInterval: 15
24 | minReplicaCount: 2 # 至少保留 2 个副本
25 | maxReplicaCount: 1000
26 | advanced:
27 | horizontalPodAutoscalerConfig:
28 | behavior: # 控制扩缩容行为,使用比较保守的策略,快速扩容,缓慢缩容
29 | scaleDown: # 缓慢缩容:至少冷却 10 分钟才能缩容
30 | stabilizationWindowSeconds: 600
31 | selectPolicy: Min #
32 | scaleUp: # 快速扩容:每 15s 最多允许扩容 5 倍
33 | policies:
34 | - type: Percent
35 | value: 500
36 | periodSeconds: 15
37 | triggers:
38 | # highlight-start
39 | - type: cron # 每天早上 10 点秒杀活动,确保前后半小时内至少有 200 个副本
40 | metadata:
41 | timezone: Asia/Shanghai
42 | start: 30 9 * * *
43 | end: 30 10 * * *
44 | desiredReplicas: "200"
45 | - type: cron # 每天晚上 6 点秒杀活动,确保前后半小时内至少有 200 个副本
46 | metadata:
47 | timezone: Asia/Shanghai
48 | start: 30 17 * * *
49 | end: 30 18 * * *
50 | desiredReplicas: "200"
51 | # highlight-end
52 | - type: memory # CPU 利用率超过 60% 扩容
53 | metricType: Utilization
54 | metadata:
55 | value: "60"
56 | - type: cpu # 内存利用率超过 60% 扩容
57 | metricType: Utilization
58 | metadata:
59 | value: "60"
60 | ```
61 |
62 | ## 注意事项
63 |
64 | 通常触发器不能只配置 Cron,还需和其它触发器一起配合使用,因为如果在 cron 的 start 和 end 区间之外的时间段,如果没有其它触发器活跃,副本数就会降到 `minReplicaCount`,可能并不是我们想要的。
65 |
--------------------------------------------------------------------------------
/content/best-practices/autoscaling/keda/install.md:
--------------------------------------------------------------------------------
1 | # 使用 helm 部署 KEDA
2 |
3 | ## 添加 helm repo
4 |
5 | ```bash
6 | helm repo add kedacore https://kedacore.github.io/charts
7 | helm repo update
8 | ```
9 |
10 | ## 准备 values.yaml
11 |
12 | 先查看默认的 values.yaml (看看有哪些可以自定义的配置项)
13 |
14 | ```bash
15 | helm show values kedacore/keda
16 | ```
17 |
18 | 默认的依赖镜像在国内环境拉取不了,可以替换为使用 docker hub 上的 mirror 镜像,配置 `values.yaml`:
19 |
20 | ```yaml
21 | image:
22 | keda:
23 | repository: docker.io/imroc/keda
24 | metricsApiServer:
25 | repository: docker.io/imroc/keda-metrics-apiserver
26 | webhooks:
27 | repository: docker.io/imroc/keda-admission-webhooks
28 | ```
29 |
30 | > 以上 mirror 镜像长期自动同步,可放心使用和更新版本。
31 |
32 | ## 安装
33 |
34 | ```bash
35 | helm upgrade --install keda kedacore/keda \
36 | --namespace keda --create-namespace \
37 | -f values.yaml
38 | ```
39 |
40 | ## 版本与升级
41 |
42 | 每个 KEDA 的版本都有对应适配的 K8S 版本区间,如果你的 TKE 集群版本不是特别新,安装最新版的 KEDA 可能无法兼容,可查看 [KEDA Kubernetes Compatibility](https://keda.sh/docs/latest/operate/cluster/#kubernetes-compatibility) 来确认当前集群版本能兼容的 KEDA 版本。
43 |
44 | 比如 TKE 集群版本是 1.26,对应能兼容的 KEDA 最新版本是 v2.12,再查询到 KEDA v2.12 (APP VERSION) 对应的 Chart 版本 (CHART VERSION) 最高版本是 2.12.1:
45 |
46 | ```bash
47 | $ helm search repo keda --versions
48 | NAME CHART VERSION APP VERSION DESCRIPTION
49 | kedacore/keda 2.13.2 2.13.1 Event-based autoscaler for workloads on Kubernetes
50 | kedacore/keda 2.13.1 2.13.0 Event-based autoscaler for workloads on Kubernetes
51 | kedacore/keda 2.13.0 2.13.0 Event-based autoscaler for workloads on Kubernetes
52 | # highlight-next-line
53 | kedacore/keda 2.12.1 2.12.1 Event-based autoscaler for workloads on Kubernetes
54 | kedacore/keda 2.12.0 2.12.0 Event-based autoscaler for workloads on Kubernetes
55 | kedacore/keda 2.11.2 2.11.2 Event-based autoscaler for workloads on Kubernetes
56 | kedacore/keda 2.11.1 2.11.1 Event-based autoscaler for workloads on Kubernetes
57 | ```
58 |
59 | 安装 KEDA 时指定版本:
60 |
61 | ```bash
62 | helm upgrade --install keda kedacore/keda \
63 | --namespace keda --create-namespace \
64 | # highlight-next-line
65 | --version 2.12.1 \
66 | -f values.yaml
67 | ```
68 |
69 | 后续升级版本时可复用上面的安装命令,只需修改下版本号即可。
70 |
71 | **注意**:在升级 TKE 集群前也用这里的方法先确认下升级后的集群版本能否兼容当前版本的 KEDA,如果不能,请提前升级 KEDA 到当前集群版本所能兼容的最新 KEDA 版本。
72 |
73 | ## 卸载
74 |
75 | 参考 [官方卸载说明](https://keda.sh/docs/latest/deploy/#uninstall)。
76 |
77 | ## 参考资料
78 |
79 | * [KEDA 官方文档:Deploying KEDA](https://keda.sh/docs/latest/deploy/)
80 |
--------------------------------------------------------------------------------
/content/best-practices/autoscaling/keda/overview.md:
--------------------------------------------------------------------------------
1 | # 认识 KEDA
2 |
3 | ## 什么是 KEDA ?
4 |
5 | KEDA (Kubernetes-based Event-Driven Autoscaler) 是在 Kubernetes 中事件驱动的弹性伸缩器,功能非常强大。不仅支持根据基础的 CPU 和内存指标进行伸缩,还支持根据各种消息队列中的长度、数据库中的数据统计、QPS、Cron 定时计划以及您可以想象的任何其他指标进行伸缩,甚至还可以将副本缩到 0。
6 |
7 | 该项目于 2020.3 被 CNCF 接收,2021.8 开始孵化,最后在 2023.8 宣布毕业,目前已经非常成熟,可放心在生产环境中使用。
8 |
9 | ## 为什么需要 KEDA ?
10 |
11 | HPA 是 Kubernetes 自带的 Pod 水平自动伸缩器,只能根据监控指标对工作负载自动扩缩容,指标主要是工作负载的 CPU 和内存的利用率(Resource Metrics),如果需要支持其它自定义指标,一般是安装 [prometheus-adapter](https://github.com/kubernetes-sigs/prometheus-adapter) 来作为 HPA 的 Custom Metrics 和 External Metrics 的实现来将 Prometheus 中的监控数据作为自定义指标提供给 HPA。理论上,用 HPA + prometheus-adapter 也能实现 KEDA 的功能,但实现上会非常麻烦,比如想要根据数据库中任务表里记录的待执行的任务数量统计进行伸缩,就需要编写并部署 Exporter 应用,将统计结果转换为 Metrics 暴露给 Prometheus 进行采集,然后 prometheus-adapter 再从 Prometheus 查询待执行的任务数量指标来决定是否伸缩。
12 |
13 | KEDA 的出现主要是为了解决 HPA 无法基于灵活的事件源进行伸缩的这个问题,内置了几十种常见的 [Scaler](https://keda.sh/docs/latest/scalers/) ,可直接跟各种第三方应用对接,比如各种开源和云托管的关系型数据库、时序数据库、文档数据库、键值存储、消息队列、事件总线等,也可以使用 Cron 表达式进行定时自动伸缩,常见的伸缩常见基本都涵盖了,如果发现有不支持的,还可以自己实现一个外部 Scaler 来配合 KEDA 使用。
14 |
15 | ## KEDA 的原理
16 |
17 | KEDA 并不是要替代 HPA,而是作为 HPA 的补充或者增强,事实上很多时候 KEDA 是配合 HPA 一起工作的,这是 KEDA 官方的架构图:
18 |
19 | 
20 |
21 | * 当要将工作负载的副本数缩到闲时副本数,或从闲时副本数扩容时,由 KEDA 通过修改工作负载的副本数实现(闲时副本数小于 `minReplicaCount`,包括 0,即可以缩到 0)。
22 | * 其它情况下的扩缩容由 HPA 实现,HPA 由 KEDA 自动管理,HPA 使用 External Metrics 作为数据源,而 External Metrics 后端的数据由 KEDA 提供。
23 | * KEDA 各种 Scalers 的核心其实就是为 HPA 暴露 External Metrics 格式的数据,KEDA 会将各种外部事件转换为所需的 External Metrics 数据,最终实现 HPA 通过 External Metrics 数据进行自动伸缩,直接复用了 HPA 已有的能力,所以如果还想要控制扩缩容的行为细节(比如快速扩容,缓慢缩容),可以直接通过配置 HPA 的 `behavior` 字段来实现 (要求 Kubernetes 版本 >= 1.18)。
24 |
25 | 除了工作负载的扩缩容,对于任务计算类场景,KEDA 还可以根据排队的任务数量自动创建 Job 来实现对任务的及时处理:
26 |
27 | 
28 |
29 | ## 哪些场景适合使用 KEDA ?
30 |
31 | 下面罗列下适合使用 KEDA 的场景。
32 |
33 | ### 微服务多级调用
34 |
35 | 在微服务中,基本都存在多级调用的业务场景,压力是逐级传递的,下面展示了一个常见的情况:
36 |
37 | 
38 |
39 | 如果使用传统的 HPA 根据负载扩缩容,用户流量进入集群后:
40 | 1. `Deploy A` 负载升高,指标变化迫使 `Deploy A` 扩容。
41 | 2. A 扩容之后,吞吐量变大,B 受到压力,再次采集到指标变化,扩容 `Deploy B`。
42 | 3. B 吞吐变大,C 受到压力,扩容 `Deploy C`。
43 |
44 | 这个逐级传递的过程不仅缓慢,还很危险:每一级的扩容都是直接被 CPU 或内存的飙高触发的,被 “冲垮” 的可能性是普遍存在的。这种被动、滞后的方式,很明显是有问题的。
45 |
46 | 此时,我们可以利用 KEDA 来实现多级快速扩容:
47 | * `Deploy A` 可根据自身负载或网关记录的 QPS 等指标扩缩容。
48 | * `Deploy B` 和 `Deploy C` 可根据 `Deploy A` 副本数扩缩容(各级服务副本数保持一定比例)。
49 |
50 | ### 任务执行(生产者与消费者)
51 |
52 | 如果有需要长时间执行的计算任务,如数据分析、ETL、机器学习等场景,从消息队列或数据库中取任务进行执行,需要根据任务数量来伸缩,使用 HPA 不太合适,用 KEDA 就非常方便,可以让 KEDA 根据排队中的任务数量对工作负载进行伸缩,也可以自动创建 Job 来消费任务。
53 |
54 | 
55 |
56 | ### 周期性规律
57 |
58 | 如果业务有周期性的波峰波谷特征,可以使用 KEDA 配置定时伸缩,在波峰来临之前先提前扩容,结束之后再缓慢缩容。
59 |
--------------------------------------------------------------------------------
/content/best-practices/autoscaling/keda/prometheus.md:
--------------------------------------------------------------------------------
1 | # 基于 Prometheus 自定义指标的弹性伸缩
2 |
3 | ## Prometheus 触发器
4 |
5 | KEDA 支持 `prometheus` 类型的触发器,即根据自定义的 PromQL 查询到的 Prometheus 指标数据进行伸缩,完整配置参数参考 [KEDA Scalers: Prometheus](https://keda.sh/docs/latest/scalers/prometheus/),本文将给出使用案例。
6 |
7 | ## 案例:基于 istio 的 QPS 指标伸缩
8 |
9 | 如果你使用 isito,业务 Pod 注入了 sidecar,会自动暴露一些七层的监控指标,最常见的是 `istio_requests_total`,可以通过这个指标计算 QPS。
10 |
11 | 假设这种场景:A 服务需要根据 B 服务处理的 QPS 进行伸缩。
12 |
13 | ```yaml showLineNumbers
14 | apiVersion: keda.sh/v1alpha1
15 | kind: ScaledObject
16 | metadata:
17 | name: b-scaledobject
18 | namespace: prod
19 | spec:
20 | scaleTargetRef:
21 | apiVersion: apps/v1
22 | kind: Deployment
23 | name: a # 对 A 服务进行伸缩
24 | pollingInterval: 15
25 | minReplicaCount: 1
26 | maxReplicaCount: 100
27 | triggers:
28 | # highlight-start
29 | - type: prometheus
30 | metadata:
31 | serverAddress: http://monitoring-kube-prometheus-prometheus.monitoring.svc.cluster.local:9090 # 替换 Prometheus 的地址
32 | query: | # 计算 B 服务 QPS 的 PromQL
33 | sum(irate(istio_requests_total{reporter=~"destination",destination_workload_namespace=~"prod",destination_workload=~"b"}[1m]))
34 | threshold: "100" # A服务副本数=ceil(B服务QPS/100)
35 | # highlight-end
36 | ```
37 |
38 | ## 相比 prometheus-adapter 的优势
39 |
40 | [prometheus-adapter](https://github.com/kubernetes-sigs/prometheus-adapter) 也支持相同的能力,即根据 Prometheus 中的监控指标数据进行伸缩,但相比 KEDA 的方案有以下不足:
41 |
42 | * 每次新增自定义指标,都要改动 `prometheus-adapter` 的配置,且改配置是集中式管理的,不支持通过 CRD 管理,配置维护起来比较麻烦,而 KEDA 方案则只需要配置 `ScaledObject` 或 `ScaledJob` 这种 CRD,不同业务使用不同的 YAML 文件维护,利于配置维护。
43 | * `prometheus-adapter` 的配置语法晦涩难懂,不能直接写 `PromQL`,需要学习一下 `prometheus-adapter` 的配置语法,有一定的学习成本,而 KEDA 的 prometheus 配置则非常简单,指标可以直接写 `PromQL` 查询语句,简单明了。
44 | * `prometheus-adapter` 只支持根据 Prometheus 监控数据进行伸缩,而对于 KEDA 来说,Prometheus 只是众多触发器中的一种。
45 |
--------------------------------------------------------------------------------
/content/best-practices/autoscaling/keda/workload.md:
--------------------------------------------------------------------------------
1 | # 多级服务同步水平伸缩 (Workload 触发器)
2 |
3 | ## Workload 触发器
4 |
5 | KEDA 支持 Kubernetes Workload 触发器,即可以根据的一个或多个工作负载的 Pod 数量来扩缩容,在多级服务调用的场景下很有用,具体用法参考 [KEDA Scalers: Kubernetes Workload](https://keda.sh/docs/2.13/scalers/kubernetes-workload/)。
6 |
7 | ## 案例:多级服务同时扩容
8 |
9 | 比如下面这种多级微服务调用:
10 |
11 | 
12 |
13 | * A、B、C 这一组服务通常有比较固定的数量比例。
14 | * A 的压力突增,迫使扩容,B 和 C 也可以用 KEDA 的 Kubernetes Workload 触发器实现与 A 几乎同时扩容,而无需等待压力逐级传导才缓慢迫使扩容。
15 |
16 | 首先配置 A 的扩容,根据 CPU 和内存压力扩容:
17 |
18 | ```yaml showLineNumbers
19 | apiVersion: keda.sh/v1alpha1
20 | kind: ScaledObject
21 | metadata:
22 | name: a
23 | spec:
24 | scaleTargetRef:
25 | apiVersion: apps/v1
26 | kind: Deployment
27 | name: a
28 | pollingInterval: 15
29 | minReplicaCount: 10
30 | maxReplicaCount: 1000
31 | triggers:
32 | - type: memory
33 | metricType: Utilization
34 | metadata:
35 | value: "60"
36 | - type: cpu
37 | metricType: Utilization
38 | metadata:
39 | value: "60"
40 | ```
41 |
42 | 然后配置 B 和 C 的扩容,假设固定比例 A:B:C = 3:3:2。
43 |
44 |
45 |
46 |
47 | ```yaml showLineNumbers
48 | apiVersion: keda.sh/v1alpha1
49 | kind: ScaledObject
50 | metadata:
51 | name: b
52 | spec:
53 | scaleTargetRef:
54 | apiVersion: apps/v1
55 | kind: Deployment
56 | name: b
57 | pollingInterval: 15
58 | minReplicaCount: 10
59 | maxReplicaCount: 1000
60 | triggers:
61 | # highlight-start
62 | - type: kubernetes-workload
63 | metadata:
64 | podSelector: 'app=a' # 选中 A 服务
65 | value: '1' # A/B=3/3=1
66 | # highlight-end
67 | ```
68 |
69 |
70 |
71 |
72 |
73 | ```yaml showLineNumbers
74 | apiVersion: keda.sh/v1alpha1
75 | kind: ScaledObject
76 | metadata:
77 | name: c
78 | spec:
79 | scaleTargetRef:
80 | apiVersion: apps/v1
81 | kind: Deployment
82 | name: c
83 | pollingInterval: 15
84 | minReplicaCount: 3
85 | maxReplicaCount: 340
86 | triggers:
87 | # highlight-start
88 | - type: kubernetes-workload
89 | metadata:
90 | podSelector: 'app=a' # 选中 A 服务
91 | value: '1.5' # A/C=3/2=1.5
92 | # highlight-end
93 | ```
94 |
95 |
96 |
97 |
98 | 通过以上配置,当A的压力增加时,A、B和C将几乎同时进行扩容,而无需等待压力逐级传导。这样可以更快地适应压力变化,提高系统的弹性和性能。
99 |
--------------------------------------------------------------------------------
/content/best-practices/containerization/crontab.md:
--------------------------------------------------------------------------------
1 | # 在容器中使用 crontab
2 |
3 | ## 准备 crontab 配置文件
4 |
5 | 新建一个名为 `crontab` 的配置文件,写定时任务规则:
6 |
7 | ```txt
8 | * * * * * echo "Crontab is working" > /proc/1/fd/1
9 | ```
10 |
11 | > `/proc/1/fd/1` 表示输出到容器主进程的标准输出,这样我们可以利用 `kubectl logs` 来查看到执行日志。
12 |
13 | ## 准备 Dockerfile
14 |
15 | ### CentOS 镜像
16 |
17 | ```dockerfile
18 | FROM docker.io/centos:7
19 |
20 | RUN yum -y install crontabs && rm -rf /etc/cron.*/*
21 |
22 | ADD crontab /etc/crontab
23 | RUN chmod 0644 /etc/crontab
24 | RUN crontab /etc/crontab
25 |
26 | CMD ["crond", "-n"]
27 | ```
28 |
29 | ### Ubuntu 镜像
30 |
31 | ```dockerfile
32 | FROM docker.io/ubuntu:22.04
33 |
34 | RUN apt-get update && apt-get install -y cron && rm -rf /etc/cron.*/*
35 |
36 | ADD crontab /etc/crontab
37 | RUN chmod 0644 /etc/crontab
38 | RUN crontab /etc/crontab
39 |
40 | CMD ["cron", "-f", "-l", "2"]
41 | ```
42 |
43 | ## 打包镜像
44 |
45 | ```bash
46 | docker build -t docker.io/imroc/crontab:latest -f Dockerfile .
47 | # podman build -t docker.io/imroc/crontab:latest -f Dockerfile .
48 | ```
49 |
--------------------------------------------------------------------------------
/content/best-practices/containerization/golang.md:
--------------------------------------------------------------------------------
1 | # Go 应用容器化
2 |
3 | ## 使用多阶段构建编译
4 |
5 | 可以使用 golang 的官方镜像进行编译,建议使用静态编译,因为 golang 官方镜像默认使用的基础镜像是 debian,如果使用默认的编译,会依赖依赖一些动态链接库,当业务镜像使用了其它发行版基础镜像,且动态链接库不一样的话 (比如 alpine),就会导致程序启动时发现依赖的动态链接库找不到而无法启动:
6 |
7 | ```txt
8 | standard_init_linux.go:211: exec user process caused "no such file or directory"
9 | ```
10 |
11 | 以下是多阶段构建静态编译 golang 程序的 Dockerfile 示例:
12 |
13 | ```dockerfile
14 | FROM golang:latest as builder
15 | COPY . /build
16 | WORKDIR /build
17 | RUN CGO_ENABLED=0 go build -trimpath -ldflags='-s -w -extldflags=-static' -o /app
18 |
19 | FROM ubuntu:22.10
20 | COPY --from=builder /app /
21 | CMD ["/app"]
22 | ```
23 |
24 | 如果希望最小化镜像,可以用空基础镜像,让镜像中只包含一个静态编译后 go 二进制:
25 |
26 | ```dockerfile
27 | FROM golang:latest as builder
28 | COPY . /build
29 | WORKDIR /build
30 | RUN CGO_ENABLED=0 go build -trimpath -ldflags='-s -w -extldflags=-static' -o /app
31 |
32 | FROM scratch
33 | COPY --from=builder /app /
34 | CMD ["/app"]
35 | ```
36 |
37 | :::tip
38 |
39 | 建议 k8s 1.23 及其以上版本使用 scratch 基础镜像,即使镜像中不包含 bash 等调试工具,也可以 [使用临时容器来进行调试](https://kubernetes.io/zh-cn/docs/tasks/debug/debug-application/debug-running-pod/#ephemeral-container)。
40 |
41 | :::
42 |
43 | ## 利用 go module 缓存加速构建
44 |
45 | 如果在固定的机器上编译镜像,可以考虑在 Dockerfile 中为 go modules 缓存单独使用一个阶段构建,具体思路是将项目中的 `go.mod` 和 `go.sum` 先单独拷贝过去,然后执行以下 `go mod download` 来下载 go modules 缓存,只要这两个文件没有变动,下次构建镜像时就可以直接复用之前下载好的 go modules 缓存依赖。
46 |
47 | 示例:
48 |
49 | ```dockerfile
50 | FROM golang:alpine AS mod
51 | RUN apk add --no-cache git
52 | WORKDIR /workspace
53 | # highlight-start
54 | COPY go.mod .
55 | COPY go.sum .
56 | RUN go mod download
57 | # highlight-end
58 |
59 | FROM mod AS build
60 | COPY . .
61 | RUN CGO_ENABLED=0 go build -o app -ldflags '-w -extldflags "-static"' .
62 |
63 | FROM alpine:latest
64 | RUN apk add --no-cache tzdata ca-certificates
65 | COPY --from=build /workspace/app /app
66 | CMD ["/app"]
67 | ```
68 |
--------------------------------------------------------------------------------
/content/best-practices/containerization/java.md:
--------------------------------------------------------------------------------
1 | # Java 应用容器化
2 |
3 | 本文介绍 Java 应用容器化相关注意事项。
4 |
5 | ## 避免低版本 JDK
6 |
7 | JDK 低版本对容器不友好,感知不到自己在容器内:
8 | 1. 不知道被分配了多少内存,很容易造成消耗过多内容而触发 Cgroup OOM 被杀死。
9 | 2. 不知道被分配了多少 CPU,认为可用 CPU 数量就是宿主机的 CPU 数量,导致 JVM 创建过多线程,容易高负载被 Cgroup CPU 限流(throttle)。
10 |
11 | 在高版本的 JDK 中 (JDK10) 对容器进行了很好的支持,同时也 backport 到了低版本 (JDK8):
12 | 1. 如果使用的 `Oracle JDK`,确保版本大于等于 `8u191`。
13 | 2. 如果使用的 `OpenJDK`,确保版本大于等于 `8u212`。
14 |
15 | ## 常见问题
16 |
17 | ### 相同镜像在部分机器上跑有问题
18 |
19 | * 现象: 经常会有人说,我的 java 容器镜像,在 A 机器上跑的好好的,在 B 机器上就有问题,都是用的同一个容器镜像啊。
20 | * 根因:java 类加载的顺序问题,如果有不同版本的重复 jar 包,只会加载其中一个,并且不保证顺序。
21 | * 解决方案:业务去掉重复的 jar 包。
22 | * 类似 case 的分析文章:[关于Jar加载顺序的问题分析](https://www.jianshu.com/p/dcad5330b06f)
23 |
24 | ### java 默认线程池的线程数问题
25 |
26 | * 现象:java 应用创建大量线程。
27 | * 根因:低版本 jdk,无法正确识别 cgroup 的 limit,所以 cpu 的数量及内存的大小是直接从宿主机获取的,跟 cgroup 里的 limit 不一致。
28 | * 解决方案:业务升级 jdk 版本。
29 |
30 | ## 使用 Maven 构建 Java 容器镜像
31 |
32 | 本文介绍如果在容器环境将 Maven 项目构建成 Java 容器镜像,完整示例源码请参考 Github [maven-docker-example](https://github.com/imroc/maven-docker-example)。
33 |
34 | ### pom.xml
35 |
36 | 以下是 maven `pom.xml` 示例:
37 |
38 |
39 |
40 | 关键点:
41 | * 利用 `maven-dependency-plugin` 插件将所有依赖 jar 包拷贝到 `./lib` 下。
42 | * 利用 `maven-jar-plugin` 插件在打包 jar 时指定 main 函数所在 Class,让 jar 可执行;将依赖包放到 jar 包相对路径的 `./lib` 下并自动加上 `CLASSPATH`。
43 |
44 | ### Dockerfile
45 |
46 | 以下是用于构建镜像的 `Dockerfile` 示例:
47 |
48 | ```dockerfile
49 | FROM docker.io/library/maven:3.8-jdk-11 AS build
50 |
51 | COPY src /app/src
52 | COPY pom.xml /app
53 |
54 | RUN mvn -f /app/pom.xml clean package
55 |
56 | FROM openjdk:11-jre-slim
57 | COPY --from=build /app/target/app.jar /app/app.jar
58 | COPY --from=build /app/target/lib /app/lib
59 | ENTRYPOINT ["java","-jar","/app/app.jar"]
60 | ```
61 |
62 | 关键点:
63 | * 利用多阶段构建,只将生成的 jar 包及其依赖拷贝到最终镜像中,减小镜像体积。
64 | * 镜像指定启动命令,给 `java` 指定要运行的 jar 包。
65 |
66 | ## 参考资料
67 |
68 | * [JDK 8u191 Update Release Notes ](https://www.oracle.com/java/technologies/javase/8u191-relnotes.html)
69 | * [Docker support in Java 8 — finally!](https://blog.softwaremill.com/docker-support-in-new-java-8-finally-fd595df0ca54)
70 | * [Better Containerized JVMs in JDK10](http://blog.gilliard.lol/2018/01/10/Java-in-containers-jdk10.html)
71 | * [JVM in a Container](https://merikan.com/2019/04/jvm-in-a-container/#java-8u131-and-java-9)
72 | * [14 best practices for containerising your Java applications](https://www.tutorialworks.com/docker-java-best-practices/)
73 | * [Best Practices: Java Memory Arguments for Containers](https://dzone.com/articles/best-practices-java-memory-arguments-for-container)
74 |
--------------------------------------------------------------------------------
/content/best-practices/containerization/logrotate.md:
--------------------------------------------------------------------------------
1 | # 使用 sidecar 轮转日志
2 |
3 | ## 背景
4 |
5 | 业务程序如果将日志写到日志文件,如果没有自动轮转,可能会撑爆磁盘导致业务异常,甚至可能影响节点上其它 Pod。
6 |
7 | 如果所使用的日志框架不支持日志轮转,或者不想改动业务代码,可以通过 sidecar 来对业务日志进行自动轮转,本文介绍如何基于 [docker-logrotate](https://github.com/imroc/docker-logrotate) 来实现日志轮转。
8 |
9 | ## docker-logrotate 介绍
10 |
11 | [docker-logrotate](https://github.com/imroc/docker-logrotate) 是一个将 logrotate 容器化的开源项目,该项目自动构建出的容器镜像 [imroc/logrotate](https://hub.docker.com/r/imroc/logrotate) 是基于 alpine,预装了 logrotate,且支持多 CPU 架构的容器镜像,还可以通过环境变量的方式控制 logrotate 配置。
12 |
13 | ## 示例一:自动轮转 nginx ingress 的日志
14 |
15 | 配置 [ingress-nginx](https://github.com/kubernetes/ingress-nginx/tree/main/charts/ingress-nginx) helm chart 的 `values.yaml`:
16 |
17 | ```yaml
18 | controller:
19 | config:
20 | access-log-path: /var/log/nginx/nginx_access.log
21 | error-log-path: /var/log/nginx/nginx_error.log
22 | extraVolumes:
23 | - name: log
24 | emptyDir: {}
25 | extraVolumeMounts:
26 | - name: log
27 | mountPath: /var/log/nginx
28 | extraContainers: # logrotate sidecar
29 | - name: logrotate
30 | image: imroc/logrotate:latest
31 | imagePullPolicy: IfNotPresent
32 | env:
33 | - name: LOGROTATE_FILE_PATTERN
34 | value: "/var/log/nginx/nginx_*.log"
35 | - name: LOGROTATE_FILESIZE
36 | value: "20M"
37 | - name: LOGROTATE_FILENUM
38 | value: "10"
39 | - name: CRON_EXPR
40 | value: "*/1 * * * *"
41 | - name: CROND_LOGLEVEL
42 | value: "7"
43 | volumeMounts:
44 | - name: log # share log directory
45 | mountPath: /var/log/nginx
46 | ```
47 |
48 | ## 示例二:自动轮转 nginx 日志
49 |
50 | ```yaml
51 | apiVersion: apps/v1
52 | kind: Deployment
53 | metadata:
54 | name: nginx
55 | spec:
56 | replicas: 1
57 | selector:
58 | matchLabels:
59 | app: nginx
60 | template:
61 | metadata:
62 | labels:
63 | app: nginx
64 | spec:
65 | containers:
66 | - name: nginx
67 | image: nginx:latest
68 | volumeMounts:
69 | - name: log # share log directory
70 | mountPath: /var/log/nginx
71 | - name: logrotate
72 | image: imroc/logrotate:latest
73 | env:
74 | - name: LOGROTATE_FILE_PATTERN
75 | value: "/var/log/nginx/*.log"
76 | - name: LOGROTATE_FILESIZE
77 | value: "20M"
78 | - name: LOGROTATE_FILENUM
79 | value: "10"
80 | - name: CRON_EXPR
81 | value: "*/1 * * * *"
82 | - name: CROND_LOGLEVEL
83 | value: "7"
84 | volumeMounts:
85 | - name: log # share log directory
86 | mountPath: /var/log/nginx
87 | volumes:
88 | - name: log
89 | emptyDir: {}
90 | ```
91 |
--------------------------------------------------------------------------------
/content/best-practices/containerization/thin-image.md:
--------------------------------------------------------------------------------
1 | # 精简 Docker 镜像
2 |
3 | 在生产环境中,往往需要精简容器镜像,即让 Dockerfile 构建出来的镜像体积足够小,本文介绍如何优雅的为 Docker 镜像瘦身。
4 |
5 | ## 精简镜像的好处
6 |
7 | * 减少构建时间。
8 | * 减少磁盘使用量。
9 | * 减少下载时间,加快容器启动速度,在需要快速扩容的场景下显得尤为重要。
10 | * 降低带宽压力。在大规模扩容的场景下,大量镜像并发拉取,镜像仓库或任意节点达到带宽瓶颈都会影响扩容速度,精简的镜像会降低带宽压力,从而降低达到带宽瓶颈的概率和时长。
11 | * 提高安全性,减少攻击面积。无用的依赖少,从而大大减少被攻击目标。
12 |
13 | ## 使用精简的基础镜像
14 |
15 | ### alpine
16 |
17 | Alpine一个基于 musl libc 和 busybox、面向安全的轻量级 Linux 发行版,压缩体积只有 3M 左右,很多流行的镜像都有基于 alpine 的制作的基础镜像。
18 |
19 | ### scratch
20 |
21 | scratch 是一个空镜像,如果你的应用是一个包含所有依赖的二进制(不依赖动态链接库),可以使用 scratch 作为基础镜像,这样镜像的体积几乎就等于你 COPY 进去的二进制的体积。
22 |
23 | 示例:
24 |
25 | ```dockerfile showLineNumbers
26 | FROM scratch
27 | COPY app /app
28 | CMD ["/app"]
29 | ```
30 |
31 | ### busybox
32 |
33 | 如果你希望镜像里可以包含一些常用的 Linux 工具,busybox 镜像是个不错选择,它集成了一百多个最常用 Linux 命令和工具的软件工具箱,镜像压缩体积只有不到 1M,非常便于构建小镜像。
34 |
35 | ### distroless
36 |
37 | distroless 镜像,它仅包含您的应用程序及其运行时依赖项。它们不包含您希望在标准 Linux 发行版中找到的包管理器、shell或任何其他程序。
38 | 由于Distroless是原始操作系统的精简版本,不包含额外的程序。容器里并没有Shell!如果黑客入侵了我们的应用程序并获取了容器的访问权限,他也无法造成太大的损害。也就是说,程序越少则尺寸越小也越安全。不过,代价是调试更麻烦。
39 |
40 | :::tip[注意]
41 |
42 | 我们不应该在生产环境中,将Shell附加到容器中进行调试,而应依靠正确的日志和监控。
43 |
44 | :::
45 |
46 | 示例:
47 |
48 | ```dockerfile showLineNumbers
49 | FROM node:8 as build
50 |
51 | WORKDIR /app
52 | COPY package.json index.js ./
53 | RUN npm install
54 |
55 | FROM gcr.io/distroless/nodejs
56 |
57 | COPY --from=build /app /
58 | EXPOSE 3000
59 | CMD ["index.js"]
60 | ```
61 |
62 | ### Distroless vs Alpine
63 |
64 | 如果是在生产环境中运行,并且注重安全性, Distroless 镜像可能会更合适。
65 |
66 | Docker镜像中每增加一个二进制程序,就会给整个应用程序带来一定的风险。在容器中只安装一个二进制程序即可降低整体风险。
67 |
68 | 举个例子,如果黑客在运行于Distroless的应用中发现了一个漏洞,他也无法在容器中创建Shell,因为根本就没有。
69 |
70 | 如果更在意要是大小,则可以换成Alpine基础镜像。
71 |
72 | 这两个都很小,代价是兼容性。Alpine用了一个稍稍有点不一样的C标准库————muslc,时不时会碰到点兼容性的问题。
73 |
74 | :::tip
75 |
76 | 原生基础镜像非常适合用于测试和开发。它的尺寸比较大,不过用起来就像你主机上安装的Ubuntu一样。并且,你能访问该操作系统里有的所有二进制程序。
77 |
78 | :::
79 |
80 | ## 清理包管理器缓存
81 |
82 | 在 Dockerfile 中使用包管理器安装依赖的软件包时,往往会产生一些缓存数据,可以清理掉以减少镜像体积。
83 |
84 | ### Alpine
85 |
86 | 如果使用 alpine 基础镜像,可以在用 `apk add` 安装软件包时加 `--no-cache`:
87 |
88 | ```dockerfile showLineNumbers
89 | FROM alpine:latest
90 |
91 | # highlight-next-line
92 | RUN apk add --no-cache tzdata ca-certificates
93 | ```
94 |
95 | ### Ubuntu/Debian
96 |
97 | ```dockerfile showLineNumbers
98 | FROM ubuntu:latest
99 |
100 | RUN apt update -y && apt install -y curl
101 |
102 | # highlight-start
103 | RUN apt-get clean autoclean && \
104 | apt-get autoremove --yes && \
105 | rm -rf /var/lib/{apt,dpkg,cache,log}/
106 | # highlight-end
107 | ```
108 |
109 | ## 使用多阶段构建
110 |
111 | Dockerfile 支持多阶段构建,即有多个 `FROM` 指令,最终构建出的镜像由由最后一个 `FROM` 之后的指令决定,通常可以把这之前的指令用作编译,之后的指令用作打包,打包阶段可以将编译阶段产生的文件拷贝过来,这样可以实现在最终镜像中只保留运行程序所需要的内容。
112 |
113 | 下面是使用 Golang 静态编译二进制,然后 COPY 到 scratch 镜像的 Dockerfile 示例:
114 |
115 | ```dockerfile showLineNumbers
116 | FROM golang:latest AS build
117 | WORKDIR /workspace
118 | COPY . .
119 | # 静态编译二进制
120 | RUN CGO_ENABLED=0 go build -o app -ldflags '-w -extldflags "-static"' .
121 |
122 | FROM scratch
123 | # 拷贝二进制到空镜像
124 | COPY --from=build /workspace/app /usr/local/bin/app
125 | CMD ["app"]
126 | ```
127 |
--------------------------------------------------------------------------------
/content/best-practices/containerization/timezone.md:
--------------------------------------------------------------------------------
1 | # 解决容器内时区不一致问题
2 |
3 | ## 背景
4 |
5 | 业务程序在使用时间的时候(比如打印日志),没有指定时区,使用的系统默认时区,而基础镜像一般默认使用 UTC 时间,程序输出时间戳的时候,就与国内的时间相差 8 小时,如何使用国内的时间呢?本文教你如何解决。
6 |
7 | ## 方案一:指定 TZ 环境变量
8 |
9 | 很多编程语言都支持 `TZ` 这个用于设置时区的环境变量,可以在部署工作负载的时候,为容器指定该环境变量,示例:
10 |
11 |
12 |
13 | ## 方案二:Dockerfile 里设置时区
14 |
15 | 下面给出在一些常见的基础镜像里设置时区的示例:
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | ## 方案三:挂载主机时区配置到容器(不推荐)
30 |
31 | 最后一种思路是将 Pod 所在节点的时区文件挂载到容器内 `/etc/localtime`,这种方式对 Pod 有一定侵入性,而且依赖主机内时区配置,在不得已的情况下不推荐使用。
32 |
33 | 下面是 YAML 示例:
34 |
35 |
36 |
--------------------------------------------------------------------------------
/content/best-practices/dns/customize-dns-resolution.md:
--------------------------------------------------------------------------------
1 | # 自定义域名解析
2 |
3 | 本文介绍在 kubernetes 上如何自定义集群 CoreDNS 的域名解析。
4 |
5 | ## 添加全局自定义域名解析
6 |
7 | 可以为 coredns 配置 hosts 来实现为 kubernetes 集群添加全局的自定义域名解析:
8 |
9 | 编辑 coredns 配置:
10 |
11 | ```bash
12 | kubectl -n kube-system edit configmap coredns
13 | ```
14 |
15 | 加入 hosts:
16 |
17 | ```txt
18 | hosts {
19 | 10.10.10.10 harbor.example.com
20 | 10.10.10.11 grafana.example.com
21 | fallthrough
22 | }
23 | ```
24 |
25 | 
26 |
27 | > 参考 [CoreDNS hosts 插件说明](https://coredns.io/plugins/hosts/)
28 |
29 | 如果是想解析到集群内的 Service,也可以配置下 rewrite:
30 |
31 | ```txt
32 | rewrite name harbor.example.com harbor.harbor.svc.cluster.local
33 | ```
34 |
35 |
36 | 
37 |
38 | > 参考 [CoreDNS rewrite 插件说明](https://coredns.io/plugins/rewrite/)
39 |
40 | ## 为部分 Pod 添加自定义域名解析
41 |
42 | 如果有部分 Pod 对特定的域名解析有依赖,在不希望配置 dns 解析的情况下,可以使用 K8S 提供的 `hostAliases` 来为部分工作负载添加 hosts:
43 |
44 | ```yaml
45 | spec:
46 | hostAliases:
47 | - hostnames: [ "harbor.example.com" ]
48 | ip: "10.10.10.10"
49 | ```
50 |
51 | 
52 |
53 | 添加后在容器内可以看到 hosts 被添加到了 `/etc/hosts` 中:
54 |
55 | ```bash
56 | $ cat /etc/hosts
57 | ...
58 | # Entries added by HostAliases.
59 | 10.10.10.10 harboar.example.com
60 | ```
61 |
--------------------------------------------------------------------------------
/content/best-practices/dns/optimize-coredns-performance.md:
--------------------------------------------------------------------------------
1 | # CoreDNS 性能优化
2 |
3 | CoreDNS 作为 Kubernetes 集群的域名解析组件,如果性能不够可能会影响业务,本文介绍几种 CoreDNS 的性能优化手段。
4 |
5 | ## 合理控制 CoreDNS 副本数
6 |
7 | 考虑以下几种方式:
8 | 1. 根据集群规模预估 coredns 需要的副本数,直接调整 coredns deployment 的副本数:
9 | ```bash
10 | kubectl -n kube-system scale --replicas=10 deployment/coredns
11 | ```
12 | 2. 为 coredns 定义 HPA 自动扩缩容。
13 | 3. 安装 [cluster-proportional-autoscaler](https://github.com/kubernetes-sigs/cluster-proportional-autoscaler) 以实现更精确的扩缩容(推荐)。
14 |
15 | ## 禁用 ipv6
16 |
17 | 如果 K8S 节点没有禁用 IPV6 的话,容器内进程请求 coredns 时的默认行为是同时发起 IPV4 和 IPV6 解析,而通常我们只需要用到 IPV4,当容器请求某个域名时,coredns 解析不到 IPV6 记录,就会 forward 到 upstream 去解析,如果到 upstream 需要经过较长时间(比如跨公网,跨机房专线),就会拖慢整个解析流程的速度,业务层面就会感知 DNS 解析慢。
18 |
19 | CoreDNS 有一个 [template](https://coredns.io/plugins/template/) 的插件,可以用它来禁用 IPV6 的解析,只需要给 CoreDNS 加上如下的配置:
20 |
21 | ```txt
22 | template ANY AAAA {
23 | rcode NXDOMAIN
24 | }
25 | ```
26 |
27 | > 这个配置的含义是:给所有 IPV6 的解析请求都响应空记录,即无此域名的 IPV6 记录。
28 |
29 | ## 优化 ndots
30 |
31 | 默认情况下,Kubernetes 集群中的域名解析往往需要经过多次请求才能解析到。查看 pod 内 的 `/etc/resolv.conf` 可以知道 `ndots` 选项默认为 5:
32 |
33 | 
34 |
35 | 意思是: 如果域名中 `.` 的数量小于 5,就依次遍历 `search` 中的后缀并拼接上进行 DNS 查询。
36 |
37 | 举个例子,在 debug 命名空间查询 `kubernetes.default.svc.cluster.local` 这个 service:
38 | 1. 域名中有 4 个 `.`,小于 5,尝试拼接上第一个 search 进行查询,即 `kubernetes.default.svc.cluster.local.debug.svc.cluster.local`,查不到该域名。
39 | 2. 继续尝试 `kubernetes.default.svc.cluster.local.svc.cluster.local`,查不到该域名。
40 | 3. 继续尝试 `kubernetes.default.svc.cluster.local.cluster.local`,仍然查不到该域名。
41 | 4. 尝试不加后缀,即 `kubernetes.default.svc.cluster.local`,查询成功,返回响应的 ClusterIP。
42 |
43 | 可以看到一个简单的 service 域名解析需要经过 4 轮解析才能成功,集群中充斥着大量无用的 DNS 请求。
44 |
45 | 怎么办呢?我们可以设置较小的 ndots,在 Pod 的 dnsConfig 中可以设置:
46 |
47 | 
48 |
49 | 然后业务发请求时尽量将 service 域名拼完整,这样就不会经过 search 拼接造成大量多余的 DNS 请求。
50 |
51 | 不过这样会比较麻烦,有没有更好的办法呢?有的!请看下面的 autopath 方式。
52 |
53 | ## 启用 autopath
54 |
55 | 启用 CoreDNS 的 autopath 插件可以避免每次域名解析经过多次请求才能解析到,原理是 CoreDNS 智能识别拼接过 search 的 DNS 解析,直接响应 CNAME 并附上相应的 ClusterIP,一步到位,可以极大减少集群内 DNS 请求数量。
56 |
57 | 启用方法是修改 CoreDNS 配置:
58 |
59 | ```bash
60 | kubectl -n kube-system edit configmap coredns
61 | ```
62 |
63 | 修改红框中圈出来的配置:
64 |
65 | 
66 |
67 | * 加上 `autopath @kubernetes`。
68 | * 默认的 `pods insecure` 改成 `pods verified`。
69 |
70 | 需要注意的是,启用 autopath 后,由于 coredns 需要 watch 所有的 pod,会增加 coredns 的内存消耗,根据情况适当调节 coredns 的 memory request 和 limit。
71 |
72 | ## 部署 NodeLocal DNSCache
73 |
74 | 参考 k8s 官方文档 [Using NodeLocal DNSCache in Kubernetes clusters](https://kubernetes.io/docs/tasks/administer-cluster/nodelocaldns/)
75 |
76 | 如果是使用 TKE 并且 kube-proxy 转发模式为 iptables,可以直接在扩展组件中安装此扩展组件,扩展组件说明请参考 [TKE 官方文档](https://cloud.tencent.com/document/product/457/49423);如果使用的 ipvs 模式,可以参考 [IPVS 模式安装 NodeLocalDNS](https://imroc.cc/tke/networking/install-localdns-with-ipvs)。
77 |
78 | ## 使用 DNSAutoscaler
79 |
80 | 社区有开源的 [cluster-proportional-autoscaler](https://github.com/kubernetes-sigs/cluster-proportional-autoscaler) ,可以根据集群规模自动扩缩容,支持比较灵活的扩缩容算法。
81 |
82 | 如果使用的是 TKE,已经将其产品化成 `DNSAutoscaler 扩展组件`,在扩展组件中直接安装即可,组件说明请参考 [TKE 官方文档](https://cloud.tencent.com/document/product/457/49305)。
83 |
84 |
--------------------------------------------------------------------------------
/content/best-practices/graceful-shutdown/intro.md:
--------------------------------------------------------------------------------
1 | # 优雅终止介绍
2 |
3 | > 本文视频教程: [https://www.bilibili.com/video/BV1fu411m73C](https://www.bilibili.com/video/BV1fu411m73C)
4 |
5 | 所谓优雅终止,就是保证在销毁 Pod 的时候保证对业务无损,比如在业务发版时,让工作负载能够平滑滚动更新。 Pod 在销毁时,会停止容器内的进程,通常在停止的过程中我们需要执行一些善后逻辑,比如等待存量请求处理完以避免连接中断,或通知相关依赖进行清理等,从而实现优雅终止目的。
6 |
7 | 本节将介绍在 Kubernetes 场景下,实现 Pod 优雅终止的最佳实践。
8 |
--------------------------------------------------------------------------------
/content/best-practices/graceful-shutdown/lb-to-pod-directly.md:
--------------------------------------------------------------------------------
1 | # LB 直通 Pod 场景
2 |
3 | ## 传统 NodePort 场景
4 |
5 | K8S 服务对外暴露传统方案是 LB 绑定 Service 的 NodePort 流量从 LB 打到 NodePort 之后再由 kube-proxy 生成的 ipvs 或 iptables 规则进行转发:
6 |
7 | 
8 |
9 | 这样当滚动更新时,LB 绑定的 NodePort 一般无需变动,也就不需要担心 LB 解绑导致对业务有损。
10 |
11 | ## LB 直通 Pod 场景
12 |
13 | 现在很多云厂商也都支持了 LB 直通 Pod,即 LB 直接将流量转发给 Pod,不需要再经过集群内做一次转发:
14 |
15 | 
16 |
17 | 当滚动更新时,LB 就需要解绑旧 Pod,绑定新 Pod,如果 LB 到旧 Pod 上的存量连接的存量请求还没处理完,直接解绑的话就可能造成请求异常;我们期望的是,等待存量请求处理完,LB 才真正解绑旧 Pod。
18 |
19 | ## 解决方案
20 |
21 | ### TKE
22 |
23 | 腾讯云 TKE 官方针对四层 Service 和七层 Ingress 都提供了解决方案。
24 |
25 | 如果是四层 Service,在 Service 上加上这样的注解即可(前提是 Service 用了 CLB 直通 Pod 模式):
26 |
27 | ```yaml
28 | service.cloud.tencent.com/enable-grace-shutdown: "true"
29 | ```
30 |
31 | > 参考官方文档 [Service 优雅停机](https://cloud.tencent.com/document/product/457/60064)
32 |
33 | 如果是七层 CLB 类型 Ingress,在 Ingress 上加上这样的注解即可(前提是 Service 用了 CLB 直通 Pod 模式):
34 |
35 | ```yaml
36 | ingress.cloud.tencent.com/enable-grace-shutdown: "true"
37 | ```
38 |
39 | > 参考官方文档 [Ingress 优雅停机](https://cloud.tencent.com/document/product/457/60065)
40 |
41 | :::tip[说明]
42 |
43 | TKE 新版的 service/ingress 控制器(2.2.1 以上)已默认强制开启该功能,注解已废弃。可分别通过以下命令查看控制器版本(`VERSION` 字段):
44 |
45 | ```bash
46 | kubectl -n kube-system get cm tke-service-controller-config -o yaml
47 | kubectl -n kube-system get cm tke-ingress-controller-config -o yaml
48 | ```
49 |
50 | :::
51 |
52 | ### ACK
53 |
54 | 阿里云 ACK 目前只针对四层 Service 提供了解决方案,通过注解开启优雅中断与设置中断超时时间:
55 |
56 | ```yaml
57 | service.beta.kubernetes.io/alibaba-cloud-loadbalancer-connection-drain: "on"
58 | service.beta.kubernetes.io/alibaba-cloud-loadbalancer-connection-drain-timeout: "900"
59 | ```
60 |
61 | > 参考官方文档 [通过Annotation配置负载均衡](https://help.aliyun.com/document_detail/86531.html)
62 |
--------------------------------------------------------------------------------
/content/best-practices/graceful-shutdown/long-connection.md:
--------------------------------------------------------------------------------
1 | # 长连接场景
2 |
3 | 如果业务是长链接场景,比如游戏、会议、直播等,客户端与服务端会保持着长链接:
4 |
5 | 
6 |
7 | 销毁 Pod 时需要的优雅终止的时间通常比较长 (preStop + 业务进程停止超过 30s),有的极端情况甚至可能长达数小时,这时候可以根据实际情况自定义 `terminationGracePeriodSeconds`,避免过早的被 `SIGKILL` 杀死,示例:
8 |
9 | 
10 |
11 | 具体设置多大可以根据业务场景最坏的情况来预估,比如对战类游戏场景,同一房间玩家的客户端都连接的同一个服务端 Pod,一轮游戏最长半个小时,那么我们就设置 `terminationGracePeriodSeconds` 为 1800。
12 |
13 | 如果不好预估最坏的情况,最好在业务层面优化下,比如 Pod 销毁时的优雅终止逻辑里面主动通知下客户端,让客户端连到新的后端,然后客户端来保证这两个连接的平滑切换。等旧 Pod 上所有客户端连接都连切换到了新 Pod 上,才最终退出
14 |
--------------------------------------------------------------------------------
/content/best-practices/graceful-shutdown/pod-termination-proccess.md:
--------------------------------------------------------------------------------
1 | # Pod 终止流程
2 |
3 | 我们先了解下容器在 Kubernetes 环境中的终止流程:
4 |
5 | 
6 |
7 | 1. Pod 被删除,状态变为 `Terminating`。从 API 层面看就是 Pod metadata 中的 deletionTimestamp 字段会被标记上删除时间。
8 | 2. kube-proxy watch 到了就开始更新转发规则,将 Pod 从 service 的 endpoint 列表中摘除掉,新的流量不再转发到该 Pod。
9 | 3. kubelet watch 到了就开始销毁 Pod。
10 |
11 | 3.1. 如果 Pod 中有 container 配置了 [preStop Hook](https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/) ,将会执行。
12 |
13 | 3.2. 发送 `SIGTERM` 信号给容器内主进程以通知容器进程开始优雅停止。
14 |
15 | 3.3. 等待 container 中的主进程完全停止,如果在 `terminationGracePeriodSeconds` 内 (默认 30s) 还未完全停止,就发送 `SIGKILL` 信号将其强制杀死。
16 |
17 | 3.4. 所有容器进程终止,清理 Pod 资源。
18 |
19 | 3.5. 通知 APIServer Pod 销毁完成,完成 Pod 删除。
20 |
--------------------------------------------------------------------------------
/content/best-practices/graceful-shutdown/prestop.md:
--------------------------------------------------------------------------------
1 | # 合理使用 preStop
2 |
3 | 若你的业务代码中没有处理 `SIGTERM` 信号,或者你无法控制使用的第三方库或系统来增加优雅终止的逻辑,也可以尝试为 Pod 配置下 preStop,在这里面实现优雅终止的逻辑,示例:
4 |
5 | ```yaml
6 | lifecycle:
7 | preStop:
8 | exec:
9 | command:
10 | - /clean.sh
11 | ```
12 |
13 | > 参考 [Kubernetes API 文档](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#lifecycle-1)
14 |
15 | 在某些极端情况下,Pod 被删除的一小段时间内,仍然可能有新连接被转发过来,因为 kubelet 与 kube-proxy 同时 watch 到 pod 被删除,kubelet 有可能在 kube-proxy 同步完规则前就已经停止容器了,这时可能导致一些新的连接被转发到正在删除的 Pod,而通常情况下,当应用收到 `SIGTERM` 后都不再接受新连接,只保持存量连接继续处理,所以就可能导致 Pod 删除的瞬间部分请求失败。
16 |
17 | 这种情况下,我们也可以利用 preStop 先 sleep 一小下,等待 kube-proxy 完成规则同步再开始停止容器内进程:
18 |
19 | ```yaml
20 | lifecycle:
21 | preStop:
22 | exec:
23 | command:
24 | - sleep
25 | - 5s
26 | ```
27 |
--------------------------------------------------------------------------------
/content/best-practices/graceful-shutdown/update-strategy.md:
--------------------------------------------------------------------------------
1 | # 配置保守的更新策略
2 |
3 | ## 保守更新策略
4 |
5 | 如果对稳定性要求较高,可以设置比较保守的滚动更新策略:
6 | 1. 保持足够多的可用副本数量。避免在滚动时可以正常处理请求的 Pod 数量减少导致部分请求因后端 Pod 处理不过来而异常。
7 | 2. 减缓发版速度。一方面可以避免新版应用引入难以发现的问题快速扩散,方便发现后及时回滚恢复;另一方面,如果使用 LB 直通 Pod,更新过程中,云厂商的 `service-controller` 或 `cloud-controller-manager` 组件会更新 LB 的后端 rs,这个过程是异步的,在某些极端场景下,可能出现 LB 后端的 rs 还没更新,旧的 Pod 副本已经被销毁了,从而导致流量转发到已销毁的 Pod 而引发异常。
8 | 3. 给新副本留预热时间。新副本启动时,多给应用一些时间进行准备,避免某些应用虽然探测接口返回就绪,但实际处理能力还没跟上,过早转发请求过来可能导致异常。
9 |
10 | ## 配置示例
11 |
12 |
13 |
--------------------------------------------------------------------------------
/content/best-practices/ha/smooth-upgrade.md:
--------------------------------------------------------------------------------
1 | # 工作负载平滑升级
2 |
3 | 解决了服务单点故障和驱逐节点时导致的可用性降低问题后,我们还需要考虑一种可能导致可用性降低的场景,那就是滚动更新。为什么服务正常滚动更新也可能影响服务的可用性呢?别急,下面我来解释下原因。
4 |
5 | ## 业务有损滚动更新
6 |
7 | 假如集群内存在服务间调用:
8 |
9 | 
10 |
11 | 当 server 端发生滚动更新时:
12 |
13 | 
14 |
15 | 发生两种尴尬的情况:
16 | 1. 旧的副本很快销毁,而 client 所在节点 kube-proxy 还没更新完转发规则,仍然将新连接调度给旧副本,造成连接异常,可能会报 "connection refused" (进程停止过程中,不再接受新请求) 或 "no route to host" (容器已经完全销毁,网卡和 IP 已不存在)。
17 | 2. 新副本启动,client 所在节点 kube-proxy 很快 watch 到了新副本,更新了转发规则,并将新连接调度给新副本,但容器内的进程启动很慢 (比如 Tomcat 这种 java 进程),还在启动过程中,端口还未监听,无法处理连接,也造成连接异常,通常会报 "connection refused" 的错误。
18 |
19 | ## 最佳实践
20 |
21 | 针对第一种情况,可以给 container 加 preStop,让 Pod 真正销毁前先 sleep 等待一段时间,等待 client 所在节点 kube-proxy 更新转发规则,然后再真正去销毁容器。这样能保证在 Pod Terminating 后还能继续正常运行一段时间,这段时间如果因为 client 侧的转发规则更新不及时导致还有新请求转发过来,Pod 还是可以正常处理请求,避免了连接异常的发生。听起来感觉有点不优雅,但实际效果还是比较好的,分布式的世界没有银弹,我们只能尽量在当前设计现状下找到并实践能够解决问题的最优解。
22 |
23 | 针对第二种情况,可以给 container 加 ReadinessProbe (就绪检查),让容器内进程真正启动完成后才更新 Service 的 Endpoint,然后 client 所在节点 kube-proxy 再更新转发规则,让流量进来。这样能够保证等 Pod 完全就绪了才会被转发流量,也就避免了链接异常的发生。
24 |
25 | 最佳实践 yaml 示例:
26 |
27 | ``` yaml
28 | readinessProbe:
29 | httpGet:
30 | path: /healthz
31 | port: 80
32 | httpHeaders:
33 | - name: X-Custom-Header
34 | value: Awesome
35 | initialDelaySeconds: 10
36 | timeoutSeconds: 1
37 | lifecycle:
38 | preStop:
39 | exec:
40 | command: ["/bin/bash", "-c", "sleep 10"]
41 | ```
42 |
43 | 最后,业务本身也需要实现优雅终止,避免被销毁时中断业务,参考 [优雅终止最佳实践](../graceful-shutdown)
44 |
--------------------------------------------------------------------------------
/content/best-practices/logging.md:
--------------------------------------------------------------------------------
1 | # 日志采集
2 |
3 | 本文介绍 Kubernetes 中,日志采集的最佳实践。
4 |
5 | ## 落盘文件还是标准输出?
6 |
7 | 在上 K8S 的过程中,往往会遇到一个问题:业务日志是输出到日志文件,还是输出到标准输出?哪种方式更好?
8 |
9 | 如果输出到日志文件,日志轮转就需要自己去完成,要么业务日志框架支持,要么用其它工具去轮转(比如 sidecar 与业务容器共享日志目录,然后 sidecar 中 crontab + logrotate 之类的工具去轮转)。
10 |
11 | 如果输出到标准输出(前提是容器主进程是业务进程),日志轮转则是由 K8S 自动完成,业务不需要关心,对于非 docker 的运行时(比如 containerd),日志轮转由 kubelet 完成,每个容器标准输出的日志轮转规则由 kubelet 以下两个参数决定:
12 |
13 | ```txt
14 | --container-log-max-files int32 Set the maximum number of container log files that can be present for a container. The number must be >= 2. This flag can only be used with --container-runtime=remote. (default 5)
15 | --container-log-max-size string Set the maximum size (e.g. 10Mi) of container log file before it is rotated. This flag can only be used with --container-runtime=remote. (default "10Mi")
16 | ```
17 |
18 | > 日志默认最多存储 5 个文件,每个最大 10Mi。
19 |
20 | 对于 docker 运行时,没有实现 CRI 接口,日志轮转由 docker 自身完成,在配置文件 `/etc/docker/daemon.json` 中配置:
21 |
22 | ``` json
23 | {
24 | "log-driver": "json-file",
25 | "log-opts": {
26 | "max-size": "500m",
27 | "max-file": "3"
28 | }
29 | }
30 | ```
31 |
32 | 输出到标准输出还有一些其它好处:
33 |
34 | 1. 日志内容可以通过标准 K8S API 获取到,比如使用 `kubectl logs` 或一些 K8S 管理平台的可视化界面查看(比如 Kubernetes Dashboard,KubeSphere, Rancher 以及云厂商的容器服务控制台等)。
35 | 2. 运维无需关注业务日志文件路径,可以更方便的使用统一的采集规则进行采集,减少运维复杂度。
36 |
37 | **最佳实践**
38 |
39 | 如果你的应用已经足够云原生了,符合"单进程模型",不再是富容器,那么应尽量将日志输出到标准输出,业务不需要关心日志轮转,使用日志采集工具采集容器标准输出。有一种例外的情况是,对于非 docker 运行时,如果你有单个容器的日志输出过快,速率持续超过 `30MB/s` 的话,kubelet 在轮转压缩的时候,可能会 "追不上",迟迟读不到 EOF,轮转失败,最终可能导致磁盘爆满,这种情况还是建议输出到日志文件,自行轮转。
40 |
41 | 其它情况,可以先将日志落盘到文件,并自行轮转下。
42 |
--------------------------------------------------------------------------------
/content/best-practices/long-connection.md:
--------------------------------------------------------------------------------
1 | # 长连接服务
2 |
3 | ## 负载不均问题
4 |
5 | 对于长连接的服务,可能会存在负载不均的问题,下面介绍两种场景。
6 |
7 | ### 滚动更新负载不均
8 |
9 | 在连接数比较固定或波动不大的情况下,滚动更新时,旧 Pod 上的连接逐渐断掉,重连到新启动的 Pod 上,越先启动的 Pod 所接收到的连接数越多,造成负载不均:
10 |
11 | 
12 |
13 | ### rr 策略负载不均
14 |
15 | 假如长连接服务的不同连接的保持时长差异很大,而 ipvs 转发时默认是 rr 策略转发,如果某些后端 Pod "运气较差",它们上面的连接保持时间比较较长,而由于是 rr 转发,它们身上累计的连接数就可能较多,节点上通过 `ipvsadm -Ln -t CLUSTER-IP:PORT` 查看某个 service 的转发情况:
16 |
17 | 
18 |
19 | 部分 Pod 连接数高,意味着相比连接数低的 Pod 要同时处理更多的连接,着消耗的资源也就相对更多,从而造成负载不均。
20 |
21 | 将 kube-proxy 的 ipvs 转发模式设置为 lc (Least-Connection) ,即倾向转发给连接数少的 Pod,可能会有所缓解,但也不一定,因为 ipvs 的负载均衡状态是分散在各个节点的,并没有收敛到一个地方,也就无法在全局层面感知哪个 Pod 上的连接数少,并不能真正做到 lc。可以尝试设置为 sh (Source Hashing),并且这样可以保证即便负载均衡状态没有收敛到同一个地方,也能在全局尽量保持负载均衡。
22 |
23 | ## 扩容失效问题
24 |
25 | 在连接数比较固定或波动不大的情况下,工作负载在 HPA 自动扩容时,由于是长链接,连接数又比较固定,所有连接都 "固化" 在之前的 Pod 上,新扩出的 Pod 几乎没有连接,造成之前的 Pod 高负载,而扩出来的 Pod 又无法分担压力,导致扩容失效:
26 |
27 | 
28 |
29 | ## 最佳实践
30 |
31 | 1. 业务层面自动重连,避免连接 "固化" 到某个后端 Pod 上。比如周期性定时重连,或者一个连接中处理的请求数达到阈值后自动重连。
32 | 2. 不直接请求后端,通过七层代理访问。比如 gRPC 协议,可以 [使用 nginx ingress 转发 gRPC](https://kubernetes.github.io/ingress-nginx/examples/grpc/),也可以 [使用 istio 转发 gRPC](https://istiobyexample.dev/grpc/),这样对于 gRPC 这样多个请求复用同一个长连接的场景,经过七层代理后,可以自动拆分请求,在请求级别负载均衡。
33 | 3. kube-proxy 的 ipvs 转发策略设置为 sh (`--ipvs-scheduler=sh`)。如果用的腾讯云 EKS 弹性集群,没有节点,看不到 kube-proxy,可以通过 `eks.tke.cloud.tencent.com/ipvs-scheduler: 'sh'` 这样的注解来设置,另外还支持将端口号也加入到 hash 的 key,更利于负载均衡,需再设置下 `eks.tke.cloud.tencent.com/ipvs-sh-port: "true"`,参考 [Serverless 注解](https://cloud.tencent.com/document/product/457/44173#.E8.AE.BE.E7.BD.AE-ipvs-.E5.8F.82.E6.95.B0)。
34 |
35 |
--------------------------------------------------------------------------------
/content/best-practices/ops/batch-operate-node-with-ansible.md:
--------------------------------------------------------------------------------
1 | # 使用 Ansible 批量操作节点
2 |
3 | ## 原理介绍
4 |
5 | Ansible 是一款流行的开源运维工具,可以直接通过 SSH 协议批量操作机器,无需事先进行手动安装依赖等操作,十分便捷。我们可以针对需要批量操作的节点,使用 ansbile 批量对节点执行指定的脚本。
6 |
7 | ## 准备 Ansible 控制节点
8 |
9 | 1. 选取实例作为 Ansible 的控制节点,通过此节点批量发起对存量 TKE 节点的操作。可选择与集群所在私有网络 VPC 中任意实例作为控制节点(包括 TKE 节点)。
10 | 2. 选定控制节点后,选择对应方式安装 Ansible:
11 |
12 | - Ubuntu 操作系统安装方式:
13 | ```bash
14 | sudo apt update && sudo apt install software-properties-common -y && sudo apt-add-repository --yes --update ppa:ansible/ansible && sudo apt install ansible -y
15 | ```
16 |
17 | - CentOS 操作系统安装方式:
18 | ```bash
19 | sudo yum install ansible -y
20 | ```
21 |
22 | ## 准备配置文件
23 |
24 | 将所有需要进行配置操作的节点内网 IP 配置到 `host.ini` 文件中,每行一个 IP。示例如下:
25 |
26 | ```txt
27 | 10.0.3.33
28 | 10.0.2.4
29 | ```
30 |
31 | 如需操作所有节点,可通过以下命令一键生成 `hosts.ini` 文件。
32 |
33 | ```bash
34 | kubectl get nodes -o jsonpath='{.items[*].status.addresses[?(@.type=="InternalIP")].address}' | tr ' ' '\n' > hosts.ini
35 | ```
36 |
37 | ## 准备批量执行脚本
38 |
39 | 将需批量执行的操作写入脚本,并保存为脚本文件,下面举个例子。
40 |
41 | 自建镜像仓库后没有权威机构颁发证书,直接使用 HTTP 或 HTTPS 自签发的证书,默认情况下 dockerd 拉取镜像时会报错。此时可通过批量修改节点的 dockerd 配置,将自建仓库地址添加到 dockerd 配置的 `insecure-registries` 中使 dockerd 忽略证书校验。脚本文件 `modify-dockerd.sh` 内容如下:
42 |
43 | ```bash
44 | # yum install -y jq # centos
45 | apt install -y jq # ubuntu
46 | cat /etc/docker/daemon.json | jq '."insecure-registries" += ["myharbor.com"]' > /tmp/daemon.json
47 | cp /tmp/daemon.json /etc/docker/daemon.json
48 | systemctl restart dockerd
49 | ```
50 |
51 | ## 使用 Ansible 批量执行脚本
52 |
53 | 通常 TKE 节点在新增时均指向一个 SSH 登录密钥或密码。请按照实际情况执行以下操作:
54 |
55 | ### 使用密钥
56 |
57 | 1. 准备密钥文件,例如 `tke.key`。
58 | 2. 执行以下命令,授权密钥文件:
59 | ```bash
60 | chmod 0600 tke.key
61 | ```
62 |
63 | 3. 批量执行脚本:
64 | - Ubuntu 操作系统节点批量执行示例如下:
65 | ```bash
66 | ansible all -i hosts.ini --ssh-common-args="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --user ubuntu --become --become-user=root --private-key=tke.key -m script -a "modify-dockerd.sh"
67 | ```
68 | - 其他操作系统节点批量执行示例如下:
69 | ```bash
70 | ansible all -i hosts.ini --ssh-common-args="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --user root -m script -a "modify-dockerd.sh"
71 | ```
72 |
73 |
74 | ### 使用密码
75 |
76 | 1. 执行以下命令,将密码输入至 PASS 变量。
77 | ```bash
78 | read -s PASS
79 | ```
80 |
81 | 2. 批量执行脚本:
82 | - Ubuntu 操作系统节点的 SSH 用户名默认为 ubuntu,批量执行示例如下:
83 | ```bash
84 | ansible all -i hosts.ini --ssh-common-args="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --user ubuntu --become --become-user=root -e "ansible_password=$PASS" -m script -a "modify-dockerd.sh"
85 | ```
86 |
87 | - 其他系统节点的 SSH 用户名默认为 root,批量执行示例如下:
88 | ```bash
89 | ansible all -i hosts.ini --ssh-common-args="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --user root -e "ansible_password=$PASS" -m script -a "modify-dockerd.sh"
90 | ```
91 |
--------------------------------------------------------------------------------
/content/best-practices/ops/etcd-optimization.md:
--------------------------------------------------------------------------------
1 | # ETCD 优化
2 |
3 | ## 高可用部署
4 |
5 | 部署一个高可用 ETCD 集群可以参考官方文档 [Clustering Guide](https://etcd.io/docs/v3.5/op-guide/clustering/)。
6 |
7 | > 如果是 self-host 方式部署的集群,可以用 etcd-operator 部署 etcd 集群;也可以使用另一个小集群专门部署 etcd (使用 etcd-operator)
8 |
9 | ## 提高磁盘 IO 性能
10 |
11 | ETCD 对磁盘写入延迟非常敏感,对于负载较重的集群建议磁盘使用 SSD 固态硬盘。可以使用 diskbench 或 fio 测量磁盘实际顺序 IOPS。
12 |
13 | ## 提高 ETCD 的磁盘 IO 优先级
14 |
15 | 由于 ETCD 必须将数据持久保存到磁盘日志文件中,因此来自其他进程的磁盘活动可能会导致增加写入时间,结果导致 ETCD 请求超时和临时 leader 丢失。当给定高磁盘优先级时,ETCD 服务可以稳定地与这些进程一起运行:
16 |
17 | ``` bash
18 | sudo ionice -c2 -n0 -p $(pgrep etcd)
19 | ```
20 |
21 | ## 提高存储配额
22 |
23 | 默认 ETCD 空间配额大小为 2G,超过 2G 将不再写入数据。通过给 ETCD 配置 `--quota-backend-bytes` 参数增大空间配额,最大支持 8G。
24 |
25 | ## 分离 events 存储
26 |
27 | 集群规模大的情况下,集群中包含大量节点和服务,会产生大量的 event,这些 event 将会对 etcd 造成巨大压力并占用大量 etcd 存储空间,为了在大规模集群下提高性能,可以将 events 存储在单独的 ETCD 集群中。
28 |
29 | 配置 kube-apiserver:
30 |
31 | ``` bash
32 | --etcd-servers="http://etcd1:2379,http://etcd2:2379,http://etcd3:2379" --etcd-servers-overrides="/events#http://etcd4:2379,http://etcd5:2379,http://etcd6:2379"
33 | ```
34 |
35 | ## 减小网络延迟
36 |
37 | 如果有大量并发客户端请求 ETCD leader 服务,则可能由于网络拥塞而延迟处理 follower 对等请求。在 follower 节点上的发送缓冲区错误消息:
38 |
39 | ``` bash
40 | dropped MsgProp to 247ae21ff9436b2d since streamMsg's sending buffer is full
41 | dropped MsgAppResp to 247ae21ff9436b2d since streamMsg's sending buffer is full
42 | ```
43 |
44 | 可以通过在客户端提高 ETCD 对等网络流量优先级来解决这些错误。在 Linux 上,可以使用 tc 对对等流量进行优先级排序:
45 |
46 | ``` bash
47 | $ tc qdisc add dev eth0 root handle 1: prio bands 3
48 | $ tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip sport 2380 0xffff flowid 1:1
49 | $ tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip dport 2380 0xffff flowid 1:1
50 | $ tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip sport 2379 0xffff flowid 1:1
51 | $ tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip dport 2379 0xffff flowid 1:1
52 | ```
53 |
--------------------------------------------------------------------------------
/content/best-practices/ops/large-scale-cluster-optimization.md:
--------------------------------------------------------------------------------
1 | # 大规模集群优化
2 |
3 | Kubernetes 自 v1.6 以来,官方就宣称单集群最大支持 5000 个节点。不过这只是理论上,在具体实践中从 0 到 5000,还是有很长的路要走,需要见招拆招。
4 |
5 | 官方标准如下:
6 |
7 | * 不超过 5000 个节点
8 | * 不超过 150000 个 pod
9 | * 不超过 300000 个容器
10 | * 每个节点不超过 100 个 pod
11 |
12 | ## Master 节点配置优化
13 |
14 | GCE 推荐配置:
15 |
16 | * 1-5 节点: n1-standard-1
17 | * 6-10 节点: n1-standard-2
18 | * 11-100 节点: n1-standard-4
19 | * 101-250 节点: n1-standard-8
20 | * 251-500 节点: n1-standard-16
21 | * 超过 500 节点: n1-standard-32
22 |
23 | AWS 推荐配置:
24 |
25 | * 1-5 节点: m3.medium
26 | * 6-10 节点: m3.large
27 | * 11-100 节点: m3.xlarge
28 | * 101-250 节点: m3.2xlarge
29 | * 251-500 节点: c4.4xlarge
30 | * 超过 500 节点: c4.8xlarge
31 |
32 | 对应 CPU 和内存为:
33 |
34 | * 1-5 节点: 1vCPU 3.75G内存
35 | * 6-10 节点: 2vCPU 7.5G内存
36 | * 11-100 节点: 4vCPU 15G内存
37 | * 101-250 节点: 8vCPU 30G内存
38 | * 251-500 节点: 16vCPU 60G内存
39 | * 超过 500 节点: 32vCPU 120G内存
40 |
41 | ## kube-apiserver 优化
42 |
43 | ### 高可用
44 |
45 | * 方式一: 启动多个 kube-apiserver 实例通过外部 LB 做负载均衡。
46 | * 方式二: 设置 `--apiserver-count` 和 `--endpoint-reconciler-type`,可使得多个 kube-apiserver 实例加入到 Kubernetes Service 的 endpoints 中,从而实现高可用。
47 |
48 | 不过由于 TLS 会复用连接,所以上述两种方式都无法做到真正的负载均衡。为了解决这个问题,可以在服务端实现限流器,在请求达到阀值时告知客户端退避或拒绝连接,客户端则配合实现相应负载切换机制。
49 |
50 | ### 控制连接数
51 |
52 | kube-apiserver 以下两个参数可以控制连接数:
53 |
54 | ``` bash
55 | --max-mutating-requests-inflight int The maximum number of mutating requests in flight at a given time. When the server exceeds this, it rejects requests. Zero for no limit. (default 200)
56 | --max-requests-inflight int The maximum number of non-mutating requests in flight at a given time. When the server exceeds this, it rejects requests. Zero for no limit. (default 400)
57 | ```
58 |
59 | 节点数量在 1000 - 3000 之间时,推荐:
60 |
61 | ``` bash
62 | --max-requests-inflight=1500
63 | --max-mutating-requests-inflight=500
64 | ```
65 |
66 | 节点数量大于 3000 时,推荐:
67 |
68 | ``` bash
69 | --max-requests-inflight=3000
70 | --max-mutating-requests-inflight=1000
71 | ```
72 |
73 | ## kube-scheduler 与 kube-controller-manager 优化
74 |
75 | ### 高可用
76 |
77 | kube-controller-manager 和 kube-scheduler 是通过 leader election 实现高可用,启用时需要添加以下参数:
78 |
79 | ``` bash
80 | --leader-elect=true
81 | --leader-elect-lease-duration=15s
82 | --leader-elect-renew-deadline=10s
83 | --leader-elect-resource-lock=endpoints
84 | --leader-elect-retry-period=2s
85 | ```
86 |
87 | ### 控制 QPS
88 |
89 | 与 kube-apiserver 通信的 qps 限制,推荐为:
90 |
91 | ``` bash
92 | --kube-api-qps=100
93 | ```
94 |
95 | ## Kubelet 优化
96 |
97 | * 设置 `--image-pull-progress-deadline=30m`
98 | * 设置 `--serialize-image-pulls=false`(需要 Docker 使用 overlay2 )
99 | * Kubelet 单节点允许运行的最大 Pod 数:`--max-pods=110`(默认是 110,可以根据实际需要设置)
100 |
101 | ## 集群 DNS 高可用
102 |
103 | 设置反亲和,让集群 DNS (kube-dns 或 coredns) 分散在不同节点,避免单点故障:
104 |
105 | ``` yaml
106 | affinity:
107 | podAntiAffinity:
108 | requiredDuringSchedulingIgnoredDuringExecution:
109 | - weight: 100
110 | labelSelector:
111 | matchExpressions:
112 | - key: k8s-app
113 | operator: In
114 | values:
115 | - kube-dns
116 | topologyKey: kubernetes.io/hostname
117 | ```
118 |
119 | ## ETCD 优化
120 |
121 | 参考 [ETCD 优化](etcd-optimization.md)
122 |
123 | ## 参考资料
124 |
125 | * [Considerations for large clusters](https://kubernetes.io/docs/setup/best-practices/cluster-large/)
--------------------------------------------------------------------------------
/content/best-practices/ops/securely-maintain-or-offline-node.md:
--------------------------------------------------------------------------------
1 | # 安全维护或下线节点
2 |
3 | 有时候我们需要对节点进行维护或进行版本升级等操作,操作之前需要对节点执行驱逐 (kubectl drain),驱逐时会将节点上的 Pod 进行删除,以便它们漂移到其它节点上,当驱逐完毕之后,节点上的 Pod 都漂移到其它节点了,这时我们就可以放心的对节点进行操作了。
4 |
5 | ## 驱逐存在的问题
6 |
7 | 有一个问题就是,驱逐节点是一种有损操作,驱逐的原理:
8 |
9 | 1. 封锁节点 (设为不可调度,避免新的 Pod 调度上来)。
10 | 2. 将该节点上的 Pod 删除。
11 | 3. ReplicaSet 控制器检测到 Pod 减少,会重新创建一个 Pod,调度到新的节点上。
12 |
13 | 这个过程是先删除,再创建,并非是滚动更新,因此更新过程中,如果一个服务的所有副本都在被驱逐的节点上,则可能导致该服务不可用。
14 |
15 | 我们再来下什么情况下驱逐会导致服务不可用:
16 |
17 | 1. 服务存在单点故障,所有副本都在同一个节点,驱逐该节点时,就可能造成服务不可用。
18 | 2. 服务没有单点故障,但刚好这个服务涉及的 Pod 全部都部署在这一批被驱逐的节点上,所以这个服务的所有 Pod 同时被删,也会造成服务不可用。
19 | 3. 服务没有单点故障,也没有全部部署到这一批被驱逐的节点上,但驱逐时造成这个服务的一部分 Pod 被删,短时间内服务的处理能力下降导致服务过载,部分请求无法处理,也就降低了服务可用性。
20 |
21 | ## 解决方案
22 |
23 | 针对第一点,我们可以使用前面讲的 [Pod 打散调度](../ha/pod-split-up-scheduling.md) 避免单点故障。
24 |
25 | 针对第二和第三点,我们可以通过配置 PDB (PodDisruptionBudget) 来避免所有副本同时被删除,驱逐时 K8S 会 "观察" 服务的当前可用与期望的副本数,根据定义的 PDB 来控制 Pod 删除速率,达到阀值时会等待 Pod 在其它节点上启动并就绪后再继续删除,以避免同时删除太多的 Pod 导致服务不可用或可用性降低,下面给出两个示例。
26 |
27 | 示例一 (保证驱逐时 zookeeper 至少有 90% 的副本可用):
28 |
29 | ``` yaml
30 | apiVersion: policy/v1beta1
31 | kind: PodDisruptionBudget
32 | metadata:
33 | name: zk-pdb
34 | spec:
35 | minAvailable: 90%
36 | selector:
37 | matchLabels:
38 | app: zookeeper
39 | ```
40 |
41 | 示例二 (保证驱逐时 zookeeper 最多有一个副本不可用,相当于逐个删除并等待在其它节点完成重建):
42 |
43 | ``` yaml
44 | apiVersion: policy/v1beta1
45 | kind: PodDisruptionBudget
46 | metadata:
47 | name: zk-pdb
48 | spec:
49 | maxUnavailable: 1
50 | selector:
51 | matchLabels:
52 | app: zookeeper
53 | ```
54 |
--------------------------------------------------------------------------------
/content/best-practices/ops/securely-modify-container-root-dir.md:
--------------------------------------------------------------------------------
1 | # 安全变更容器数据盘路径
2 |
3 | 本文介绍如何安全的对容器的数据盘路径进行变更。
4 |
5 | ## Docker 运行时
6 |
7 | ### 注意事项
8 |
9 | 如果节点上容器运行时是 Docker,想要变更 Docker Root Dir,需要谨慎一点。如果操作不慎,可能造成采集不到容器监控数据,因为容器监控数据由 kubelet 的 cadvisor 模块提供,而由于 docker 没有实现 CRI 接口,cadvisor 会对 Docker 有一些特殊处理: 在刚启动时,通过 `docker info` 获取 `Docker Root Dir` 路径,后续逻辑会依赖这个路径。
10 |
11 | 如果在 kubelet 运行过程中,改了 `Docker Root Dir`,cadvisor 并不会更新路径,仍然认为路径是之前的,就会造成 kubelet 不能正常返回监控指标并且报类似如下的错:
12 |
13 | ```txt
14 | Mar 21 02:59:26 VM-67-101-centos kubelet[714]: E0321 02:59:26.320938 714 manager.go:1086] Failed to create existing container: /kubepods/burstable/podb267f18b-a641-4004-a660-4c6a43b6e520/03164d8f0d1f55a285b50b2117d6fdb2c33d2fa87f46dba0f43b806017607d03: failed to identify the read-write layer ID for container "03164d8f0d1f55a285b50b2117d6fdb2c33d2fa87f46dba0f43b806017607d03". - open /var/lib/docker/image/overlay2/layerdb/mounts/03164d8f0d1f55a285b50b2117d6fdb2c33d2fa87f46dba0f43b806017607d03/mount-id: no such file or directory
15 | ```
16 |
17 | > 参考 [排障案例: cAdvisor 无数据](https://imroc.cc/kubernetes-troubleshooting/methods/node/cadvisor-no-data)。
18 |
19 | ### 变更步骤
20 |
21 | 1. 驱逐节点(`kubectl drain NODE`),让存量 Pod 漂移到其它节点上,参考 [安全维护或下线节点](securely-maintain-or-offline-node)。
22 | 2. 修改 dockerd 配置文件 `/etc/docker/daemon.json`:
23 | ```json
24 | {
25 | "graph": "/data/docker"
26 | }
27 | ```
28 | 3. 重启 dockerd:
29 | ```bash
30 | systemctl restart docker
31 | # systemctl restart dockerd
32 | ```
33 | 4. 重启 kubelet
34 | ```bash
35 | systemctl restart kubelet
36 | ```
37 | 5. 节点恢复为可调度状态: `kubectl uncordon NODE`。
38 |
39 | ## 其它运行时
40 |
41 | 其它运行时都实现了 CRI 接口,变更容器 Root Dir 就不需要那么严谨,不过安全起见,还是建议先安全的将节点上存量 Pod 驱逐走(参考 [安全维护或下线节点](securely-maintain-or-offline-node)),然后再修改运行时配置并重启容器运行时。
42 |
43 | 配置修改方式参考对应运行时的官方文档,这里以常用的 `containerd` 为例:
44 |
45 | 1. 修改 `/etc/containerd/config.toml`:
46 | ```toml
47 | root = "/data/containerd"
48 | ```
49 | 2. 重启 containerd:
50 | ```bash
51 | systemctl restart containerd
52 | ```
53 | 3. 节点恢复为可调度状态: `kubectl uncordon NODE`。
54 |
--------------------------------------------------------------------------------
/content/best-practices/performance-optimization/cpu.md:
--------------------------------------------------------------------------------
1 | # CPU 绑核
2 |
3 | ## 背景
4 |
5 | 对于一些计算密集型,或对 CPU 比较敏感的业务,可以开启 CPU 亲和性,即绑核,避免跟其它 Pod 争抢 CPU 降低性能。
6 |
7 | ## 操作步骤
8 |
9 | 1. 驱逐节点:
10 | ```bash
11 | kubectl drain
12 | ```
13 | 2. 停止 kubelet:
14 | ```bash
15 | systemctl stop kubelet
16 | ```
17 | 3. 修改 kubelet 参数:
18 | ```txt
19 | --cpu-manager-policy="static"
20 | ```
21 | 4. 删除旧的 CPU 管理器状态文件:
22 | ```bash
23 | rm var/lib/kubelet/cpu_manager_state
24 | ```
25 | 5. 启动 kubelet
26 | ```bash
27 | systemctl start kubelet
28 | ```
29 |
30 | ## 绑定 NUMA 亲和性
31 |
32 | CPU 规格较大的节点,可能会跨 NUMA,如果 Pod 中业务进程运行的时候,在不同 NUMA 的 CPU 之间切换,会有一定的性能损耗,这种情况可以进一步开启 NUMA 的亲和性,让 Pod 中进程都跑在同一 NUMA 的 CPU 上,减少性能损耗。
33 |
34 | ### 前提条件
35 |
36 | * 内核启用 NUMA: 确保 `/etc/default/grub` 中没有 `numa=off`,若有就改为 `numa=on`。
37 | * k8s 1.18 版本以上 (依赖特性 TopologyManager 在 1.18 进入 beta 默认开启)。
38 |
39 | ### 启用方法
40 |
41 | 增加 kubelet 参数:
42 |
43 | * `--cpu-manager-policy=static`
44 | * `--topology-manager-policy=single-numa-node`
45 |
46 | ### 验证 NUMA 亲和性
47 |
48 | 1. 确认节点CPU 分布情况:
49 |
50 | ```txt
51 | NUMA node0 CPU(s): 0-23,48-71
52 | NUMA node1 CPU(s): 24-47,72-95
53 | ```
54 |
55 | 
56 |
57 | 2. 先后创建三个static类型(request和limit严格一致)的Pod:
58 |
59 | ```txt
60 | debug1: CPU request==limit==40C
61 | debug2: CPU request==limit==40C
62 | debug3: CPU request==limit==10C
63 | ```
64 |
65 | 实验预期:
66 | * debug1与debug2分布在不同的numa上,各自占用40C CPU资源,numa1与numa2各自剩余8C。
67 | * debug3预期需要10C并且都在一个numa上,在debug1和debug2各自占用40C的情况下,总共剩余16C CPU,但每个numa剩余8C{'<'}10C,debug3必定调度失败。
68 |
69 | 3. 验证
70 | debug1上创建40个100%使用CPU的进程,查看进程分布情况:debug1全部分布在numa0上:
71 |
72 | 
73 |
74 | 
75 |
76 | 
77 |
78 | 
79 |
80 | 同样,debug2全部分布在numa1上。
81 |
82 | debug3由于没有numa满足>=10C,调度失败。
83 |
84 | 
85 |
86 | 
87 |
88 | ### 确保Pod内的进程在本numa分配内存
89 |
90 | 本质上是通过系统调用(set_mempolicy)设置进程属性,在内核给进程分配内存时,内核只在进程所属numa分配内存。如果业务进程本身没有明显调用set_mempolicy设置内存分配策略,可以通过numactl --localalloc cmd 启动的进程,内核分配内存时会严格保证内存分布在本numa
91 |
92 | ## 参考资料
93 |
94 | * [https://docs.qq.com/doc/DSkNYQWt4bHhva0F6](https://docs.qq.com/doc/DSkNYQWt4bHhva0F6)
95 | * [https://blog.csdn.net/nicekwell/article/details/9368307](https://blog.csdn.net/nicekwell/article/details/9368307)
96 | * [为什么 NUMA 会影响程序的延迟](https://draveness.me/whys-the-design-numa-performance/)
97 | * [控制节点上的 CPU 管理策略](https://kubernetes.io/zh-cn/docs/tasks/administer-cluster/cpu-management-policies/)
98 |
--------------------------------------------------------------------------------
/content/cases/devcontainer/deploy.md:
--------------------------------------------------------------------------------
1 | # 部署富容器
2 |
3 | ## 部署 YAML
4 |
5 | 编写 Kubernetes 的 YAML 来部署 `devcontainer`,示例:
6 |
7 |
8 | ```yaml title="devcontainer.yaml"
9 | apiVersion: apps/v1
10 | kind: DaemonSet
11 | metadata:
12 | labels:
13 | app: devcontainer
14 | name: devcontainer
15 | namespace: devcontainer
16 | spec:
17 | selector:
18 | matchLabels:
19 | app: devcontainer
20 | template:
21 | metadata:
22 | labels:
23 | app: devcontainer
24 | spec:
25 | containers:
26 | - image: your.registry.com/private/devcontainer:latest
27 | imagePullPolicy: IfNotPresent
28 | name: devcontainer
29 | tty: true
30 | stdin: true
31 | securityContext:
32 | privileged: true
33 | runAsUser: 0
34 | runAsGroup: 0
35 | volumeMounts:
36 | - mountPath: /host
37 | name: host
38 | mountPropagation: Bidirectional
39 | - mountPath: /sys/fs/cgroup
40 | name: cgroup
41 | readOnly: true
42 | - mountPath: /root
43 | name: root
44 | - mountPath: /data # host 与容器内保持一致的路径,可用于在容器内运行某些项目的容器构建脚本,路径一致方便host上的dockerd对项目路径mount进容器
45 | name: data
46 | mountPropagation: Bidirectional
47 | dnsPolicy: Default
48 | hostNetwork: true
49 | hostPID: false
50 | restartPolicy: Always
51 | terminationGracePeriodSeconds: 1
52 | volumes:
53 | - name: root
54 | hostPath:
55 | path: /data/root
56 | type: DirectoryOrCreate
57 | - name: cgroup
58 | hostPath:
59 | path: /sys/fs/cgroup
60 | type: Directory
61 | - name: host
62 | hostPath:
63 | path: /
64 | type: Directory
65 | - name: data
66 | hostPath:
67 | path: /data
68 | type: DirectoryOrCreate
69 | updateStrategy:
70 | rollingUpdate:
71 | maxSurge: 0
72 | maxUnavailable: 1
73 | type: RollingUpdate
74 | ```
75 |
76 | ## 要点解析
77 |
78 | 编写 YAMl 时注意以下关键点:
79 | * 镜像填写你构建 `devcotnainer` 时指定的镜像名称。
80 | * `privileged` 置为 true,使用特权容器,避免因权限问题各种报错。
81 | * `dnsPolicy` 置为 Default,表示容器内直接使用宿主机所使用的 DNS 解析,保持容器内外的 DNS 解析行为一致。
82 | * `hostNetwork` 置为 true,直接使用宿主机的网络,不使用容器网络(没必要)。
83 | * 将宿主机根目录挂载到容器内的 `/host` 下,这样就可以在容器内操作宿主机内任意文件,无需登录宿主机的 SSH。
84 | * 将宿主机的 cgroup 目录(`/sys/fs/cgroup`)挂载到容器内同路径位置,因为 systemd 依赖这个才能正常运行。
85 | * 将宿主机的 `/data/root` 挂载到容器的用户目录(`/root`),因为很多软件都会写入文件到用户目录下,如果不持久化,容器重启后就会丢失。
86 | * 将宿主机的 `/data` 挂载到容器内的相同路径,日常工作用到的源码都存放到 `/data` 的子目录,这样在容器内外路径都是一致的,避免构建镜像时因 client 和 server 识别到的路径不一致造成异常。
87 |
--------------------------------------------------------------------------------
/content/cases/devcontainer/dind.md:
--------------------------------------------------------------------------------
1 | # 在容器内构建镜像
2 |
3 | ## 概述
4 | 在富容器中的日常开发中,还可能涉及构建容器,还有就是富容器自身也需要实现自举,即在富容器内编译自己的新版本镜像。本文将介绍如何实现在容器内编译容器。
5 |
6 | ## 使用 nerdctl 构建镜像
7 |
8 | 如果是在容器内编译 devcontainer 自身的镜像,可以用 [nerdctl](https://github.com/containerd/nerdctl) 替代 docker 命令来编译,编译时指定 buildkit 的 unix 套接字地址:
9 |
10 | ```bash
11 | nerdctl build --buildkit-host=unix:///host/run/buildkit/buildkitd.sock -t your.registry.com/private/devcontainer:latest .
12 | ```
13 |
14 | > buildkit 的 unix 套接字地址默认是 `/run/buildkit/buildkitd.sock`,但 buildkitd 是运行在宿主机上的,容器内并没有这个文件。而容器内可以将宿主机的根路径挂载到容器内的 `/host`,所以这里指定 buildkitd 的 unix 套接字地址为 `unix:///host/run/buildkit/buildkitd.sock`。
15 |
16 | ## nerdctl 配置文件
17 |
18 | 平时使用 nerdctl 查看容器和镜像时,我们往往希望是看到的是 k3s 里用到的镜像和容器列表,我们可以给 nerdctl 配置默认的 namespace 和运行时 unix 套接字地址来实现。
19 |
20 | nerdctl 的配置文件路径是 `/etc/nerdctl/nerdctl.toml`,配置格式参考 [官网文档](https://github.com/containerd/nerdctl/blob/main/docs/config.md)。
21 |
22 | 配置方法:
23 |
24 | ```toml title="nerdctl.toml"
25 | address = "unix:///host/run/k3s/containerd/containerd.sock"
26 | namespace = "k8s.io"
27 | ```
28 |
29 | ## 使用 docker 构建镜像
30 | 有时候我们也需要用 docker 来构建镜像(很多开源项目中依赖这个),我们可以将容器内安装的 docker 命令放到 `PATH` 之外的目录,如 `/bins/docker`,然后再写个名为 `docker` 的脚本文件放到 `/usr/local/bin/docker`:
31 |
32 | ```bash title="docker"
33 | #!/bin/bash
34 |
35 | /bins/docker -H unix:///host/var/run/docker.sock $@
36 | ```
37 |
38 | 这样就可以利用 docker 脚本调用真正的 docker 命令,自动加上 dockerd 的 unix 的套接字地址,该地址指向宿主机上的 `docker.sock` 文件。
39 |
--------------------------------------------------------------------------------
/content/cases/devcontainer/host.md:
--------------------------------------------------------------------------------
1 | # 宿主机安装容器环境
2 |
3 | ## 概述
4 |
5 | 宿主机上只需安装容器所需的环境,一是运行容器所需的 k3s,二是编译容器所需的 `buildkit`。
6 |
7 | 有的同学可能会问:为什么不直接用 docker 构建镜像?
8 |
9 | 因为 `devcontainer` 可能经常需要随着自身的需求不断迭代,每次修改后构建镜像,然后让 k3s 重启容器来更新 `devcontainer`,而 docker 构建出的镜像无法直接与 k3s 共享,如果用 docker 来构建 `devcontainer`,需要将容器导出然后再导入到 k3s 的 containerd 才能用,而这种几十G的富容器构建本身就很耗时,如果每次还需要再导入导出一次,就更加麻烦也更耗时,还占用更多空间,所以不如直接使用 buildkit 复用 k3s 的 containerd 作为 worker 来构建镜像,这样等镜像构建完,`devcontainer` 重启后就可以自动更新了。
10 |
11 | 下面将介绍 k3s 和 buildkit 的安装与配置方法。
12 |
13 | ## 安装 k3s
14 |
15 | 在宿主机上执行以下命令安装 k3s,用于声明式的方式运行容器:
16 |
17 | ```bash
18 | curl -sfL https://get.k3s.io | sh -s - server \
19 | --disable-network-policy \
20 | --disable-cloud-controller \
21 | --disable-helm-controller \
22 | --data-dir=/data/k3s \
23 | --disable=traefik,local-storage,metrics-server
24 | ```
25 |
26 | ## 安装 buildkit
27 |
28 | 通过以下脚本安装最新稳定版的 buildkit 相关二进制到 `/usr/local/bin`:
29 |
30 | ```bash
31 | getLatestRelease() {
32 | release=$(curl -s "https://api.github.com/repos/moby/buildkit/releases/latest" | grep -Po '"tag_name": "v\K[^"]*')
33 | echo "${release}"
34 | }
35 |
36 | # https://github.com/moby/buildkit/releases
37 | BUIDKIT_VERSION=v$(getLatestRelease "moby/buildkit")
38 |
39 | mkdir -p /tmp/buildkit
40 | cd /tmp/buildkit
41 | wget -O buildkit.tar.gz https://github.com/moby/buildkit/releases/download/${BUIDKIT_VERSION}/buildkit-${BUIDKIT_VERSION}.linux-$(dpkg --print-architecture).tar.gz
42 | tar -zxvf buildkit.tar.gz
43 | mv ./bin/* /usr/local/bin/
44 | ```
45 |
46 | ## 配置 buildkit
47 |
48 | 准备 `buildkit.toml` 配置文件:
49 |
50 | ```toml
51 | root = "/data/buildkit"
52 |
53 | [registry."docker.io"]
54 | mirrors = ["your.mirrors.com"]
55 | ```
56 |
57 | * `root` 指定 buildkit 构建镜像用的数据目录,通常指定到数据盘下的路径,不占用系统盘空间。
58 | * `mirrors` 指定镜像仓库的 mirror,如果需要,可以在这里配置(这里只是构建镜像时用的 mirror,运行容器时的 mirror 是在容器运行时的配置里配)。
59 |
60 | 准备 `buildkit.service` 配置文件,用于 `systemd` 拉起 `buildkitd` 进程:
61 |
62 | ```service
63 | [Unit]
64 | Description=BuildKit
65 | Documentation=https://github.com/moby/buildkit
66 |
67 | [Service]
68 | ExecStart=/usr/local/bin/buildkitd --oci-worker=false --containerd-worker=true --containerd-worker-addr=/run/k3s/containerd/containerd.sock --containerd-worker-net=host
69 |
70 | [Install]
71 | WantedBy=multi-user.target
72 | ```
73 |
74 | * `ExecStart` 指定 `buaildkitd` 的启动命令,指定使用 k3s 的 containerd 作为 worker 来构建镜像,并且指定 containerd 的 sock 地址。
75 |
76 | 最后使用以下脚本拷贝配置并启动 `buildkitd`:
77 |
78 | ```bash
79 | mkdir -p /etc/buildkit
80 | cp ./buildkit.toml /etc/buildkit/
81 | cp ./buildkit.service /etc/systemd/system/
82 | systemctl daemon-reload
83 | systemctl enable buildkit
84 | systemctl start buildkit
85 | ```
86 |
--------------------------------------------------------------------------------
/content/cases/devcontainer/overview.md:
--------------------------------------------------------------------------------
1 | # 概述
2 |
3 | ## 什么是富容器开发环境?
4 |
5 | 众所周知,容器具有环境一致性和可移植性的优势,我们可以利用容器技术,打造自己专属的开发容器,将平时的开发环境依赖都写到 `Dockerfile` 中,最终编译出专属的开发容器镜像。通常开发环境依赖很多,我本人的开发容器镜像编译出来有 30G 左右,这种用于开发,依赖众多的富容器我们就叫它富容器开发环境。
6 |
7 | ## 容器部署方式
8 |
9 | 在远程开发机上通过 k3s 部署精简版的 k8s,然后通过 Daemonset 方式声明式部署 devcontainer,网络使用 HostNetwork,无需容器网络,容器内 SSH 监听 22 以外的端口,避免与开发机自身的 SSH 端口冲突。
10 |
11 | ## 富容器的日常开发方式
12 |
13 | 在富容器中会启动 SSH,我们的电脑、平板、手机等设备可通过 SSH 登录富容器来进行日常开发:
14 |
15 | 
16 |
17 | 容器内包含日常开发所用到的工具,我本人以前用过很多 IDE 和编辑器,现在使用 Neovim 编辑器作为主力工具,而 Neovim 无需 GUI 界面,通过终端就可以用,所以平时工作大部分操作都在富容器中进行,算力和存储都 offload 到远程的富容器了,不怎么占用本机资源,可以实现多设备轻办公,即使使用低配的平板电脑也能轻松进行超大工程的开发,可随时随地办公。
18 |
19 |
20 | ## Zellij + Neovim 工作流
21 |
22 | 既然是富容器远程开发环境,我们就需要让终端持久化来“保存现场”,形成我们专属的工作空间,每次登录进去都可以继续上次的工作,不需要每次都要重新打开很多终端。以前我是通过 [tmux](https://github.com/tmux/tmux) 来实现的,后来切换到了 [zellij](https://github.com/zellij-org/zellij),后者是 Rust 写的后起之秀,比 tmux 更强大,更现代化。
23 |
24 | 日常开发使用 Neovim 编辑器,基于 [LazyVim](https://github.com/LazyVim/LazyVim) 高度 DIY 定制自己的配置,甚至自己写插件实现日常所需要的需求,几乎完全替代了以前使用的 JetBrains 全家桶 IDE 和 VSCode。
25 |
26 | 对于本地的终端软件,这个就无所谓了,只需要用到最基础的功能,macOS 可以用 [iTerm2](https://iterm2.com/),如果是用于轻办公的移动设备,可以用 [Termius](https://termius.com/)。
27 |
28 | 下面的 GIF 展示了一些基础的使用效果:
29 |
30 | 
31 |
32 | > `sdd` 是我SSH登录开发富容器的 alias。
33 |
--------------------------------------------------------------------------------
/content/cases/devcontainer/ssh.md:
--------------------------------------------------------------------------------
1 | # SSH 配置
2 | ## 配置文件目录结构
3 |
4 | SSH 相关的配置都在 `/etc/ssh` 这个目录下,建议是先用容器安装一下 ssh,然后将 `/etc/ssh` 目录下的配置都拷贝出来。
5 |
6 | `sshd_config` 是主要配置文件,其余是自动生成的公钥和密钥文件:
7 |
8 | ```txt
9 | config
10 | └── after
11 | └── etc
12 | └── ssh
13 | ├── sshd_config
14 | ├── ssh_host_ecdsa_key
15 | ├── ssh_host_ecdsa_key.pub
16 | ├── ssh_host_ed25519_key
17 | ├── ssh_host_ed25519_key.pub
18 | ├── ssh_host_rsa_key
19 | └── ssh_host_rsa_key.pub
20 | ```
21 |
22 | 为什么要将自动生成的公钥和密钥也放进来?因为不希望每次编译镜像都生成新的公钥和密钥,这样每次编译后 SSH 的公钥和密钥有变化,重新 SSH 登录时,会报错,需要清理本机 `~/.ssh/known_hosts` 对应的记录才能正常登录。 当然你也可以修改本地 SSH 配置,加上 `StrictHostKeyChecking no` 的选项也能登录上去,每次还是会有警告提示,有强迫症的用户受不了。
23 |
24 | ## 修改 sshd_config
25 |
26 | `sshd_config` 中最关键的配置是修改打开端口号,默认是 22,修改成其它端口避免与宿主机 SSH 端口冲突:
27 |
28 | ```txt title="config/after/etc/ssh/sshd_config"
29 | Port 36001
30 | ```
31 |
--------------------------------------------------------------------------------
/content/cases/home-network/alist.md:
--------------------------------------------------------------------------------
1 | # 网盘挂载工具:AList
2 |
3 | ## 为什么需要 AList ?
4 |
5 | 网上有海量的视频资源都通过网盘共享,我们可以转存到自己网盘,然后再通过 alist 挂载到路由器,直接在线观看网盘里的视频,如果网盘限速或宽带不够,也可以结合 aria2 将网盘里的文件离线下载到路由器本地。
6 |
7 | ## 开源项目
8 |
9 | AList 的项目地址:https://github.com/alist-org/alist
10 |
11 | ## 目录结构
12 |
13 | ```txt
14 | alist
15 | ├── daemonset.yaml
16 | └── kustomization.yaml
17 | ```
18 |
19 | ## 准备 daemonset.yaml
20 |
21 |
22 |
23 | ## 准备 kustomization.yaml
24 |
25 | ```yaml title="kustomization.yaml"
26 | apiVersion: kustomize.config.k8s.io/v1beta1
27 | kind: Kustomization
28 |
29 | resources:
30 | - daemonset.yaml
31 |
32 | namespace: default
33 | ```
34 |
35 | ## 访问 Alist
36 |
37 | 访问入口:http://`路由器内网 IP`:5244/
38 |
39 | ## 配置网盘
40 |
41 | 进入【AList 管理】页面,添加存储:
42 |
43 | 
44 |
45 | 选择对应的驱动,AList 支持很多网盘和对象存储,具体配置方法可在 [AList 使用指南](https://alist.nn.ci/zh/guide/) 中找到对应存储驱动的配置步骤。
46 |
47 | ## 与 Aria2 联动
48 |
49 | AList 挂载的网盘中的文件可直接发送给 Aria2 打包下载,下面介绍配置方法。
50 |
51 | 进入主页,右下角点击本地设置:
52 |
53 | 
54 |
55 | 输入 Aria2 RPC 的地址和密钥:
56 |
57 | 
58 |
59 | 进入挂载的网盘目录,选中要下载的文件,点击【发送到 Aria2】:
60 |
61 | 
62 |
63 | 然后你就进入 Aria2 的 Web 页面就可以观察到你的 Aria2 正在努力帮你离线下载文件啦。
64 |
--------------------------------------------------------------------------------
/content/cases/home-network/aria2.md:
--------------------------------------------------------------------------------
1 | # 离线下载工具:Aria2
2 |
3 | ## 概述
4 |
5 | 当我们刷到好看的剧或电影,往往希望给路由器下发下载任务进行离线下载,而 Aria2 正是离线下载的好工具,本文介绍如何使用云原生的方式部署 Aria2 到路由器。
6 |
7 | ## 开源项目
8 |
9 | 本文部署的 Aria2 使用这个开源项目构建的容器镜像:https://github.com/P3TERX/Aria2-Pro-Docker
10 |
11 | ## 目录结构
12 |
13 | ```txt
14 | aria2
15 | ├── daemonset.yaml
16 | └── kustomization.yaml
17 | ```
18 |
19 | ## 准备 daemonset.yaml
20 |
21 |
22 |
23 | * 注意修改 yaml 中指定的密码(111111)。
24 |
25 | ## 准备 kustomization.yaml
26 |
27 | ```yaml title="kustomization.yaml"
28 | apiVersion: kustomize.config.k8s.io/v1beta1
29 | kind: Kustomization
30 |
31 | resources:
32 | - daemonset.yaml
33 |
34 | namespace: default
35 | ```
36 |
37 | ## 访问 Aria2
38 |
39 | 访问入口:http://`路由器内网 IP`:6880/
40 |
41 | 用浏览器打开 Aria2 的 Web 界面后,在 `AriaNg 设置` 新建 RPC 设置,输入 `Aria2` 的 RPC 地址:`http://<路由器 IP>:6800/jsonrpc`,密码为 yaml 中设置的密码:
42 |
43 | 
44 |
45 | 然后就可以在【正在下载】中去新建下载任务了:
46 |
47 | 
48 |
49 | > 按照本文中 yaml 的配置,下载完成的文件会落盘到宿主机的 `/data/media/downloads/completed` 文件夹下。
50 |
--------------------------------------------------------------------------------
/content/cases/home-network/containerized-nftables.md:
--------------------------------------------------------------------------------
1 | # 容器化声明式管理 nftables
2 |
3 | ## 概述
4 |
5 | 在前面一些章节中我们用到了 nftables,如使用 nftables 配置防火墙规则、配置主路由出公网的 IP MASQUERADE,这些 nftables 的配置我们并没有容器化,而是直接写到宿主机的 `/etc/nftables.conf` 中,如果需要修改或新增规则,都需要手动去修改该文件并执行 `nft -f /etc/nftables.conf` 来生效。
6 |
7 | 实际上 nftables 也可以进行容器化,这样会带来一些好处:
8 | 1. 减少非容器化管理的内容,降低维护的工作量。
9 | 2. 后续可跟其它容器化应用一样,接入 GitOps 来进一步提升配置管理的便利性。
10 |
11 | ## docker-nftables 开源项目
12 |
13 | nftables 是内核 netfilter 的功能模块,与 iptables 类似,也需要一个 client 来操作规则,容器化 nftables 除了要保证宿主机内核支持 nftables 外,还需要保证容器内有 nftables 的 `nft` 命令工具,而 [docker-nftables](https://github.com/imroc/docker-nftables) 项目提供了该容器镜像,本文也将使用该项目自动构建出的容器镜像来容器化 nftables 配置。
14 |
15 | 开源项目地址:https://github.com/imroc/docker-nftables
16 |
17 | ## 目录结构
18 |
19 | ```txt
20 | nftables
21 | ├── config
22 | │ ├── firewall.conf
23 | │ └── ppp.conf
24 | ├── daemonset.yaml
25 | ├── entrypoint.sh
26 | └── kustomization.yaml
27 | ```
28 |
29 | > 可以将 `nftables` 按 `table` 维度拆分成单独的文件进行维护,放到 `config` 目录下。
30 |
31 | ## 配置 entrypoint.sh
32 |
33 | 创建 `entrypoint.sh`,该脚本用作 Pod 启动入口:
34 |
35 | ```bash showLineNumbers title="entrypoint.sh"
36 | #!/bin/bash
37 |
38 | set -ex
39 |
40 | nft -c -f /etc/nftables.conf
41 | nft -f /etc/nftables.conf
42 |
43 | sleep infinity
44 | ```
45 |
46 | 要点解析:
47 |
48 | * `set -ex` 是为了将执行的命令展示到 Pod 标准输出,并让在 nftables 规则有误的情况下,提前退出脚本。
49 | * 在真正设置 nftables 规则之前,先用 `nft -c -f /etc/nftables.conf` 检查语法是否正确,如果有误就先退出,避免 “部分成功”。
50 | * 结尾处使用 `sleep infinity` 让 Pod 保持不退出的状态,避免 Pod 被重新拉起(DaemonSet/Deployment 下容器的 `restartPolicy` 只能为 `Always`)
51 |
52 | ## 配置 daemonset.yaml
53 |
54 |
55 |
56 | 要点解析:
57 |
58 | * 挂载 `entrypoint.sh` 脚本并指定启动入口为该脚本。
59 | * 将 nftables 配置挂载到 `/etc/nftables` 目录下,该目录下的 `*.conf` 和 `*.nft` 文件会被自动 include 合并到 nftables 配置中。
60 |
61 | ## 配置 kustomization.yaml
62 |
63 | ```yaml showLineNumbers title="kustomization.yaml"
64 | apiVersion: kustomize.config.k8s.io/v1beta1
65 | kind: Kustomization
66 |
67 | resources:
68 | - daemonset.yaml
69 |
70 | configMapGenerator:
71 | - name: nftables-config
72 | files:
73 | - config/firewall.conf
74 | - config/ppp.conf
75 | - name: nftables-script
76 | files:
77 | - entrypoint.sh
78 |
79 | namespace: default
80 | ```
81 |
--------------------------------------------------------------------------------
/content/cases/home-network/ddns.md:
--------------------------------------------------------------------------------
1 | # DDNS
2 |
3 | ## 为什么需要 DDNS 服务
4 |
5 | 如果希望从外面通过 ssh 远程登录家里的路由器,或者通过 VPN 连上家里的内网,就需要知道家里的公网 IP 地址,而公网 IP 地址每次拨号都会变(比如断点或重启路由器就会重新拨号),所以需要一个 DDNS 服务来自动修改 DNS 解析,指向当前家里的公网 IP 地址。
6 |
7 | ## 告知运营商开通固定 IP
8 |
9 | 目前我知道的只有电信的宽带支持独占 IP(固定 IP), 即拨号后分配的公网 IP 只有你一家在用,不是多家共享。只有开通了这个固定 IP 功能,你才能从外面通过公网地址连上家里,需要打电话给运营商(电信是 10000),通过人工服务,让客户给开通,理由就说家里有许多智能家居设备(比如摄像头),有从外网连上家里网络的需求。
10 |
11 | ## DDNS 开源项目
12 |
13 | 本文部署的 DDNS 服务使用这个开源项目所构建出的容器镜像:https://github.com/NewFuture/DDNS
14 |
15 | ## 目录结构
16 |
17 | ```txt
18 | ddns
19 | ├── config
20 | │ └── config.json
21 | ├── daemonset.yaml
22 | └── kustomization.yaml
23 | ```
24 |
25 | ## 配置 DDNS 服务:config.json
26 |
27 | ```json showLineNumbers title="config/config.json"
28 | {
29 | "$schema": "https://ddns.newfuture.cc/schema/v2.8.json",
30 | "debug": false,
31 | "dns": "dnspod",
32 | "id": "******",
33 | "token": "********************************",
34 | "index4": "shell:ip -4 addr show ppp0 scope global | awk '/inet/{print $2}' | awk -F '/' '{print $1}'",
35 | "ipv4": [
36 | "home.imroc.cc"
37 | ],
38 | "proxy": null,
39 | "ttl": null
40 | }
41 | ```
42 |
43 | * 我的域名在 DNSPod 管理,所以配置的是 DNSPod 的 id 和 token。
44 | * `index4` 是指定获取本机公网 IPv4 的方法,我用的主路由方案,所以获取公网 IP 的方法直接读 `ppp0` 网卡上的公网 IP 地址就行,不需要调外部接口获取。
45 | * 如果主路由也有 IPv6,可以加上 IPv6 的获取方法: `"index6": "shell:ip -6 addr show ppp0 | grep 'scope global dynamic' | awk '/inet6/{print $2}' | awk -F '/' '{print $1}'",`
46 |
47 | ## 配置 daemonset.yaml
48 |
49 |
50 |
51 | ## 配置 kustomization.yaml
52 |
53 | ```yaml title="kustomization.yaml"
54 | apiVersion: kustomize.config.k8s.io/v1beta1
55 | kind: Kustomization
56 |
57 | resources:
58 | - daemonset.yaml
59 |
60 | configMapGenerator:
61 | - files:
62 | - config/config.json
63 | name: ddns-config
64 |
65 | namespace: default
66 | ```
67 |
--------------------------------------------------------------------------------
/content/cases/home-network/dnsmasq.md:
--------------------------------------------------------------------------------
1 | # DHCP 与 DNS 服务: dnsmasq
2 |
3 | ## dnsmasq 介绍
4 |
5 | `DHCP` 与 `DNS` 服务需在主路由上开启,如果用的主路由方案,可用云原生的方式部署一个 DHCP 和 DNS 服务,[dnsmasq](https://thekelleys.org.uk/dnsmasq/doc.html) 是一个同时支持这两种功能的开源软件,我们可以用下面的方法进行部署。
6 |
7 | ## docker-dnsmasq 开源项目
8 |
9 | 本文部署的 dnsmasq 服务使用这个开源项目所自动编译出的容器镜像:https://github.com/imroc/docker-dnsmasq
10 |
11 | ## 目录结构
12 |
13 | ```txt
14 | dnsmasq
15 | ├── config
16 | │ └── dnsmasq.conf
17 | ├── daemonset.yaml
18 | └── kustomization.yaml
19 | ```
20 |
21 | ## 配置 dnsmasq.conf
22 |
23 |
24 |
25 | 要点解析:
26 | * `server` 指向上游的 DNS 地址,主路由在 PPPoE 拨号后会自动获取上游 dns 地址并写到 `/etc/resolv.conf`,可以复制过来。
27 | * `dhcp-range` 指定内网设备自动获取的 IP 地址范围以及子网掩码。
28 | * `dhcp-option=option:router` 指定内网设备的默认网关,即当前主路由的内网静态 IP 地址。
29 | * `dhcp-option=option:dns-server` 指定内网设备自动获取的 DNS 地址,通常写 dnsmasq 自身的地址,即主路由的内网静态 IP 地址,不过由于我用了透明代理,希望内网设备直接用 PPPoE 拨号获得的运营商的 DNS 地址(好处是如果透明代理故障,停掉流量拦截规则后,内网设备也能正常从运营商 DNS 解析域名)。
30 |
31 | ## 配置 daemonset.yaml
32 |
33 |
34 |
35 | ## 配置 kustomization.yaml
36 |
37 | ```yaml title="kustomization.yaml"
38 | apiVersion: kustomize.config.k8s.io/v1beta1
39 | kind: Kustomization
40 |
41 | resources:
42 | - daemonset.yaml
43 |
44 | configMapGenerator:
45 | - name: dnsmasq-config
46 | files:
47 | - config/dnsmasq.conf
48 |
49 | namespace: default
50 | ```
51 |
--------------------------------------------------------------------------------
/content/cases/home-network/filebrowser.md:
--------------------------------------------------------------------------------
1 | # 文件管理器:filebrowser
2 |
3 | ## 为什么需要文件管理器?
4 |
5 | 有时候不希望通过 ssh 登录路由器来操作文件,比如用的是手机,又希望在 aria2 将视频文件离线下载完成后,将文件移动到指定文件夹下,方便家庭影院相关应用能自动识别和搜刮。
6 |
7 | ## 开源项目
8 |
9 | filebrowser 的项目地址:https://github.com/filebrowser/filebrowser
10 |
11 | ## 准备密码
12 |
13 | filebrowser 启动的时候可以指定登录的用户名和密码,密码需要经过 hash,而 filebrowser 自带 hash 子命令,可以先将得到想要设置的密码的 hash 值:
14 |
15 | ```bash
16 | $ docker run --rm -it --entrypoint="" filebrowser/filebrowser:v2.27.0 sh
17 | $ /filebrowser hash 111111
18 | $2a$10$q/0NjHYLYvP/rcB1VdRBxeVg/AnaPILgMJYyrEnOpw6mhimhsgjeG
19 | ```
20 |
21 | > 这里以 `111111` 为密码,得到的 hash 值为 `$2a$10$q/0NjHYLYvP/rcB1VdRBxeVg/AnaPILgMJYyrEnOpw6mhimhsgjeG`。
22 |
23 | ## 目录结构
24 |
25 | ```txt
26 | filebrowser
27 | ├── daemonset.yaml
28 | └── kustomization.yaml
29 | ```
30 |
31 | ## 配置 daemonset.yaml
32 |
33 |
34 |
35 | * 注意替换用户名以及密码的 hash 值。
36 |
37 | ## 配置 kustomization.yaml
38 |
39 | ```yaml title="kustomization.yaml"
40 | apiVersion: kustomize.config.k8s.io/v1beta1
41 | kind: Kustomization
42 |
43 | resources:
44 | - daemonset.yaml
45 |
46 | namespace: default
47 | ```
48 |
49 | ## 访问文件管理器
50 |
51 | 访问入口:http://`路由器内网 IP`:8567/
52 |
53 | 输入用户名密码后,就可以在网页里直接操作路由器上的文件了。
54 |
55 | 
56 |
--------------------------------------------------------------------------------
/content/cases/home-network/home-assistant.md:
--------------------------------------------------------------------------------
1 | # 智能家居助手:HomeAssistant
2 |
3 | ## HomeAssistant 介绍
4 |
5 | `HomeAssistant` 是一个开源的智能家居管理系统,通常是直接以操作系统形式安装到设备里,但也提供了容器化部署的方式,本文介绍将 `HomeAssistant` 部署到 Kubernetes 的方法。
6 |
7 | ## 开源项目
8 |
9 | * 项目地址: https://github.com/home-assistant/core
10 | * 官网: https://www.home-assistant.io/
11 |
12 | ## 目录结构
13 |
14 | ```txt
15 | home-assistant
16 | ├── daemonset.yaml
17 | └── kustomization.yaml
18 | ```
19 |
20 | ## 配置 daemonset.yaml
21 |
22 |
23 |
24 | ## 配置 kustomization.yaml
25 |
26 | ```yaml title="kustomization.yaml"
27 | apiVersion: kustomize.config.k8s.io/v1beta1
28 | kind: Kustomization
29 |
30 | resources:
31 | - daemonset.yaml
32 |
33 | namespace: default
34 | ```
35 |
36 | ## 访问 HomeAssistant
37 |
38 | 访问入口:http://`路由器内网 IP`:8123/
39 |
40 | 
41 |
--------------------------------------------------------------------------------
/content/cases/home-network/homepage.md:
--------------------------------------------------------------------------------
1 | # 家庭导航页:homepage
2 |
3 | ## 概述
4 |
5 | 随着路由器上部署的服务越来越多,记不住每个服务的页面地址,可以部署一个 homepage 并暴露 80 端口,进去后就是家里的服务导航页,可以快速跳转家里的各个服务的页面。
6 |
7 | ## 开源项目
8 |
9 | * 项目地址:https://github.com/gethomepage/homepage
10 | * 官网:https://gethomepage.dev/latest/
11 |
12 | ## 目录结构
13 |
14 | ```txt
15 | homepage
16 | ├── config
17 | │ ├── bookmarks.yaml
18 | │ ├── services.yaml
19 | │ ├── settings.yaml
20 | │ └── widgets.yaml
21 | ├── daemonset.yaml
22 | └── kustomization.yaml
23 | ```
24 |
25 | ## 准备 homepage 配置文件
26 |
27 | ```yaml title="config/bookmarks.yaml"
28 | # https://gethomepage.dev/latest/configs/bookmarks/
29 | ```
30 |
31 | ```yaml title="config/settings.yaml"
32 | # https://gethomepage.dev/latest/configs/settings/
33 |
34 | providers:
35 | openweathermap: openweathermapapikey
36 | weatherapi: weatherapiapikey
37 | ```
38 |
39 | ```yaml title="config/widgets.yaml"
40 | # https://gethomepage.dev/latest/widgets/
41 |
42 | - resources:
43 | cpu: true
44 | memory: true
45 | disk: /
46 | ```
47 |
48 | ```yaml title="config/services.yaml"
49 | # https://gethomepage.dev/latest/configs/services/
50 |
51 | - 家庭主页:
52 | - Jellyfin:
53 | href: http://10.10.10.2:8096/
54 | description: 家庭影院
55 | - Grafana:
56 | href: http://10.10.10.2:3000/
57 | description: 监控面板
58 | - VictoriaMetrics:
59 | href: http://10.10.10.2:8428/
60 | description: 监控工具
61 | - AriaNg:
62 | href: http://10.10.10.2:6880/
63 | description: 离线下载
64 | - AList:
65 | href: http://10.10.10.2:5244/
66 | description: 云盘
67 | - HomeAssistant:
68 | href: http://10.10.10.2:8123/
69 | description: Home Assistant
70 | - Filebrowser:
71 | href: http://10.10.10.2:8567/
72 | description: 文件管理
73 | ```
74 |
75 | * `services.yaml` 是定义导航页列表的配置文件,可以根据自己家里的情况进行 DIY。
76 |
77 | ## 准备 daemonset.yaml
78 |
79 |
80 |
81 | ## 准备 kustomization.yaml
82 |
83 | ```yaml title="kustomization.yaml"
84 | apiVersion: kustomize.config.k8s.io/v1beta1
85 | kind: Kustomization
86 |
87 | resources:
88 | - daemonset.yaml
89 |
90 | namespace: default
91 |
92 | configMapGenerator:
93 | - name: homepage-config
94 | files:
95 | - config/services.yaml
96 | - config/settings.yaml
97 | - config/bookmarks.yaml
98 | - config/widgets.yaml
99 | ```
100 |
101 | ## 访问 Homepage
102 |
103 | 访问入口:http://`路由器内网 IP`
104 |
105 | 
106 |
--------------------------------------------------------------------------------
/content/cases/home-network/intro.md:
--------------------------------------------------------------------------------
1 | # 方案介绍
2 |
3 | ## 什么是云原生家庭网络?
4 |
5 | 使用声明式 YAML 部署家庭网络所需应用,如路由器、家庭影院、监控系统、离线下载工具等。
6 |
7 | ## 为什么家庭网络需要云原生?
8 |
9 | 所有应用都声明式部署,可以轻松叠加所需新功能和应用,换设备或者重新装机后,能够一键部署所有应用,不需要那么多繁杂的步骤。对于熟悉 k8s 的小伙伴们,这是得天独厚的优势,也是一个不错的落地实践的机会。
10 |
11 | ## 软路由与系统选型
12 |
13 | 折腾云原生家庭网络的本质就是在一台软路由机器上,使用云原生的方式部署家庭网络所需的各种应用。
14 |
15 | 对于软路由安装的操作系统,网上很多软路由方案使用 OpenWRT、LEDE、iKuai、RouterOS 等软路由专用系统。而我们这里使用云原生方式部署,那就需要用主流的 Linux 发行版,一方面对 k8s 适配好,另一方面,熟悉云原生的小伙伴基本都对主流 Linux 发行版比较了解,不需要学习软路由专用的那些操作系统,降低学习成本。对于硬件,一般可以选购软路由专用的工控机盒子,也可以用树莓派来 DIY。
16 |
17 | 我选择使用 Ubuntu 作为家庭网络的操作系统,因为该 Linux 发新版用户量很大,内核和软件包都可以用比较新的,对容器的支持也很好,最重要的是本人对 Ubuntu 比较熟悉。
18 |
19 | ## 网络方案选型
20 |
21 | 主要分为主路由和旁路由两大类方案。
22 |
23 | ### 主路由方案
24 |
25 | 主路由方案是软路由机器安装 Ubuntu,上联光猫,光猫开启桥接(关闭路由功能),由 Ubuntu 来做 PPPoE 拨号上网和路由,其它的家庭所需应用都使用云原生的方式部署在 Ubuntu 里,包括 DNS 缓存、DHCP 服务、防火墙以及其他应用。
26 |
27 | 
28 |
29 | ### 旁路由方案
30 |
31 | 旁路由方案相比主路由方案,对于 Ubuntu 来说,只是没有了 PPPoE 拨号、DNS 缓存、DHCP 服务、防火墙等功能,其余的也都还是用云原生的方式部署在 Ubuntu。只是还需要有一个主路由,可以用单独的路由器机器,也可以启用光猫自带的路由功能(对于有大带宽的需求不推荐,因为通常光猫的路由转发能力较弱):
32 |
33 | 
34 |
35 | 还可以直接在软路由机器里使用 EXSI 这种虚拟化操作系统,在里面虚拟两个操作系统,一个 RouterOS 或 OpenWRT 作为主路由,另一个 Ubuntu 作为旁路由,形成“双软路由”(本人一开始也是这种方案,后来不用了,因为需要引入太多的系统和配置步骤,重新装机时很麻烦,违背了云原生一键部署的初衷)。
36 |
37 | 
38 |
--------------------------------------------------------------------------------
/content/cases/home-network/jellyfin.md:
--------------------------------------------------------------------------------
1 | # 家庭影院服务:Jellyfin
2 |
3 | ## 概述
4 |
5 | 如果需要在家中搭建一个家庭影院,Jellyfin 是一个不错的选择。它是一个免费的软件,可以让你在家中的任何设备上观看你的媒体文件。只要将视频文件放入指定目录下,它可以自动搜刮相关的海报、简介等信息,可以在各种设备上播放,包括电视、手机、平板电脑等,所有平台都可以同步播放记录和进度,自动接着之前没看完的继续看。
6 |
7 | ## 开源项目
8 |
9 | Jellyfin 的项目地址是:https://github.com/jellyfin/jellyfin
10 |
11 | ## 目录结构
12 |
13 | ```txt
14 | jellyfin
15 | ├── daemonset.yaml
16 | └── kustomization.yaml
17 | ```
18 |
19 | ## 配置 daemonset.yaml
20 |
21 |
22 |
23 | ## 配置 kustomization.yaml
24 |
25 | ```yaml title="kustomization.yaml"
26 | apiVersion: kustomize.config.k8s.io/v1beta1
27 | kind: Kustomization
28 |
29 | resources:
30 | - daemonset.yaml
31 |
32 | namespace: default
33 | ```
34 |
35 | ## 访问 Jellyfin
36 |
37 | 访问入口:http://`路由器内网 IP`:8096/
38 |
39 | 
40 |
41 | ## 安装豆瓣刮削器
42 |
43 | 刮削器在国内还是用豆瓣的数据更好点,可以在登录 Jellyfin 后安装这个插件: https://github.com/cxfksword/jellyfin-plugin-metashark
44 |
--------------------------------------------------------------------------------
/content/cases/home-network/nfs.md:
--------------------------------------------------------------------------------
1 | # 家庭 NAS 服务:NFS
2 |
3 | ## 为什么需要 NFS ?
4 |
5 | 家里有些设备,比如电视机、投影仪,支持通过 NFS 远程读取文件来看路由器磁盘中的视频文件,前提是路由器安装了 NFS 服务(传说中的 NAS 中的一种协议)。
6 |
7 | ## 开源项目
8 |
9 | 本文部署的 NFS 服务使用这个开源项目构建的容器镜像:https://github.com/ehough/docker-nfs-server
10 |
11 | ## 目录结构
12 |
13 | ```txt
14 | nfs
15 | ├── config
16 | │ └── exports
17 | ├── daemonset.yaml
18 | └── kustomization.yaml
19 | ```
20 |
21 | ## 配置 exports 文件
22 |
23 | 将要共享的目录写在 `exports` 文件中,每行一个目录,格式为:`目录路径 权限设置`:
24 |
25 | ```txt
26 | /data/media *(rw,no_root_squash,sync)
27 | /data/media/movies *(rw,no_root_squash,sync)
28 | ```
29 |
30 | ## 配置 daemonset.yaml
31 |
32 |
33 |
34 | ## 配置 kustomization.yaml
35 |
36 | ```yaml title="kustomization.yaml"
37 | apiVersion: kustomize.config.k8s.io/v1beta1
38 | kind: Kustomization
39 |
40 | resources:
41 | - daemonset.yaml
42 |
43 | namespace: default
44 |
45 | configMapGenerator:
46 | - name: nfs-exports
47 | files:
48 | - config/exports
49 | ```
50 |
51 | ## 注意事项
52 |
53 | 要求节点有 `nfs` 和 `nfsd` 两个内核模块:
54 |
55 | ```bash
56 | lsmod | grep nfs
57 | modprobe nfs
58 | modprobe nfsd
59 | ```
60 |
61 | 如没有,ubuntu 可尝试安装:
62 |
63 | ```bash
64 | sudo apt-get install nfs-kernel-server
65 | ```
66 |
67 | 需禁用节点自身启动的 nfs-server 和 rpc-statd 服务,避免冲突:
68 |
69 | ```bash
70 | systemctl disable nfs-server
71 | systemctl stop nfs-server
72 | systemctl stop rpc-statd
73 | ```
74 |
--------------------------------------------------------------------------------
/content/cases/home-network/qbittorrent.md:
--------------------------------------------------------------------------------
1 | # 离线下载工具:qBittorrent
2 |
3 | ## 概述
4 |
5 | 当我们刷到好看的剧或电影,找资源时可能经常找的是种子或磁力链接,而 qBittorrent 正是专门这类资源的工具,还提供了其它丰富的功能,比如 RSS 订阅自动下载最新集到指定目录,结合 Jellyfin,实现自动追剧(无需视频会员,愉快的追剧)。
6 |
7 | ## 目录结构
8 |
9 | ```txt
10 | aria2
11 | ├── daemonset.yaml
12 | └── kustomization.yaml
13 | ```
14 |
15 | ## 准备 daemonset.yaml
16 |
17 |
18 |
19 | * `PUID` 和 `PGID` 都设为 0 (root),避免下载到挂载的 `/downloads` 目录因权限问题而导致下载失败。
20 | * `WEBUI_PORT` 是 web 版界面的端口,可自行改一个不冲突的端口。
21 | * `TORRENTING_PORT` 为 torrent 监听端口,默认 16881,由于我的 Aria2 的 torrent 也用的这个端口,故改成了其它不冲突的端口。
22 |
23 | ## 准备 kustomization.yaml
24 |
25 | ```yaml title="kustomization.yaml"
26 | apiVersion: kustomize.config.k8s.io/v1beta1
27 | kind: Kustomization
28 |
29 | resources:
30 | - daemonset.yaml
31 |
32 | namespace: default
33 | ```
34 |
35 | ## 访问 qBittorrent
36 |
37 | 访问入口:http://`路由器内网 IP`:9367/
38 |
39 | 
40 |
41 | ## 自动追剧
42 |
43 | qBittorrent 支持 RSS 订阅来实现剧集更新后自动下载最新集。
44 |
45 | 这里以 [domp4](https://mp4us.com) 上的片源为例,要 RSS 订阅自动下载 domp4 上的资源,首先需要将片源信息转换成 RSS,而 [RSSHub](https://docs.rsshub.app/zh/) 正是将各个热门网站的信息转换成 RSS 的开源工具,当然也包括 domp4 的片源,参考[官网文档](https://docs.rsshub.app/zh/routes/multimedia#domp4-%E5%BD%B1%E8%A7%86)。
46 |
47 | 不过有些站点的 RSS 不推荐用 RSSHub 官网的公共实例,而用国内开发者维护的公共实例,因为可能由于官方公共实例网络环境或被限频等因素导致拿不到 RSS,关于公共实例参考 [RSSHub 官网文档:公共实例](https://docs.rsshub.app/zh/guide/instances) 。
48 |
49 | 我个人用的 `yangzhi.app` 这个公共实例,下面分享如何利用这个公共实例订阅 domp4 上的剧集。
50 |
51 | 点右上角的 【rss】,然后点击【新 RSS 订阅】:
52 |
53 | 
54 |
55 | 会弹出一个输入框,输入 RSS 地址:
56 |
57 | 
58 |
59 | 输入:`https://yangzhi.app/domp4/detail/xxx`。
60 |
61 | 将 `xxx` 替换为剧集在 domp4 上的 id,该 id 的获取方法是在剧集下载列表页面 URL 后缀,比如 `凡人修仙传` 的地址是 https://www.mp4us.com/html/9DvTT8bbbbb8.html,所以它的 id 是 `9DvTT8bbbbb8`,对应的 RSS 地址就是 `https://yangzhi.app/domp4/detail/9DvTT8bbbbb8`。
62 |
63 | 添加了 RSS 订阅后还需定义下载器规则,点击【RSS 下载器】:
64 |
65 | 
66 |
67 | 然后配置下载规则:
68 |
69 | 
70 |
71 | * 如需过滤掉不需要的内容,可勾选【使用正则表达式】,我这里不需要凡人修仙传的重制版,所以用正则过滤了下。
72 | * 勾选【保存到其它目录】,给剧集单独指定一个目录,该目录可与 Jellyfin 共享挂载(通过 hostPath),qBittorrent 自动下载最新集后,Jellyfin 也会自动搜刮剧集信息并在 Jellyfin 上展示更新的剧集信息。
73 | * 【对以下订阅源应用规则】勾选前面创建的 RSS 订阅名称,表示当该 RSS 订阅规则检查到资源更新时,qBittorrent 将自动下载资源到指定目录。
74 |
75 | 自动下载规则配置好后,不一定生效,还需确保两个全局开关打开,点击设置图标:
76 |
77 | 
78 |
79 | 确保这两个勾选上:
80 |
81 | 
82 |
83 | 大功告成!可以愉快的追剧了。
84 |
--------------------------------------------------------------------------------
/content/cases/home-network/radvd.md:
--------------------------------------------------------------------------------
1 | # IPv6 路由通告服务:radvd
2 |
3 | ## 为什么需要 radvd ?
4 |
5 | 如果你使用主路由方案,宽带也支持 IPv6,且希望家里的设备也都使用 IPv6,那就需要在主路由上部署 radvd 作为路由通告服务,类似 IPv4 的 DHCP 服务,为内网设备分配 IPv6 地址。
6 |
7 | ## 编译 radvd 镜像
8 |
9 | Dockerfile 示例:
10 |
11 | ```dockerfile showLineNumbers title="Dockerfile"
12 | FROM ubuntu:22.04
13 | RUN apt update -y
14 | RUN apt install -y radvd
15 | ENTRYPOINT ["/usr/sbin/radvd", "--config", "/etc/radvd.d/radvd.conf", "--logmethod", "stderr_clean", "--nodaemon"]
16 | ```
17 |
18 | ## 目录结构
19 |
20 | ```txt
21 | radvd
22 | ├── Dockerfile
23 | ├── config
24 | │ └── radvd.conf
25 | ├── daemonset.yaml
26 | └── kustomization.yaml
27 | ```
28 |
29 | ## 配置 radvd.conf
30 |
31 |
32 |
33 | * 我的 `enp2s0` 网口连的交换机,与其它内网设备在同一个二层网络,在此网口配置路由通告。
34 | * `route` 和 `prefix` 都写这个网口的静态 IPv6 地址。
35 |
36 | ## 配置 daemonset.yaml
37 |
38 |
39 |
40 | > 使用 `initContainer` 自动修改内核参数以启用 IPv6 转发和接收路由通告(拨号的网卡通过路由通告接收来自运营商分配的 IPv6 地址)。
41 |
42 | ## 配置 kustomization.yaml
43 |
44 | ```yaml showLineNumbers title="kustomization.yaml"
45 | apiVersion: kustomize.config.k8s.io/v1beta1
46 | kind: Kustomization
47 |
48 | resources:
49 | - daemonset.yaml
50 |
51 | namespace: default
52 |
53 | configMapGenerator:
54 | - name: radvd-config
55 | files:
56 | - config/radvd.conf
57 | ```
58 |
--------------------------------------------------------------------------------
/content/cases/home-network/samba.md:
--------------------------------------------------------------------------------
1 | # 家庭 NAS 服务:Samba
2 |
3 | ## 为什么需要 Samba 服务?
4 |
5 | 家里有些设备,比如电视机、投影仪,支持通过 Samba 远程读取文件来看路由器磁盘中的视频文件,前提是路由器安装了 Samba 服务(传说中的 NAS 中的一种协议)。
6 |
7 | ## 开源项目
8 |
9 | 本文部署的 Samba 服务使用这个开源项目构建的容器镜像:https://github.com/dperson/samba
10 |
11 | ## 目录结构
12 |
13 | ```txt
14 | samba
15 | ├── daemonset.yaml
16 | └── kustomization.yaml
17 | ```
18 |
19 | ## 配置 daemonset.yaml
20 |
21 |
22 |
23 | ## 配置 kustomization.yaml
24 |
25 | ```yaml title="kustomization.yaml"
26 | apiVersion: kustomize.config.k8s.io/v1beta1
27 | kind: Kustomization
28 |
29 | resources:
30 | - daemonset.yaml
31 |
32 | namespace: default
33 | ```
34 |
--------------------------------------------------------------------------------
/content/cases/nextcloud.md:
--------------------------------------------------------------------------------
1 | # 在 Kubernetes 部署 Nextcloud 自建网盘
2 |
3 | ```yaml
4 |
5 | ```
6 |
--------------------------------------------------------------------------------
/content/certs/sign-certs-with-cfssl.md:
--------------------------------------------------------------------------------
1 | # 使用 cfssl 生成证书
2 |
3 | 搭建各种云原生环境的过程中,经常需要生成证书,比如最常见的 etcd,本文记录使用 cfssl 快速生成证书的方法。
4 |
5 | ## 安装 cfssl
6 |
7 | **方法1**: 去 [release](https://github.com/cloudflare/cfssl/releases) 页面下载,然后解压安装。
8 |
9 | **方法2**: 使用 go install 安装:
10 |
11 | ```bash
12 | go install github.com/cloudflare/cfssl/cmd/cfssl@latest
13 | go install github.com/cloudflare/cfssl/cmd/cfssljson@latest
14 | ```
15 |
16 | ## 创建 CA 证书
17 |
18 | 由于各个组件都需要配置证书,并且依赖 CA 证书来签发证书,所以我们首先要生成好 CA 证书以及后续的签发配置文件:
19 |
20 | ``` bash
21 | cat > ca-csr.json < ca-config.json < 由于这里是 CA 证书,是签发其它证书的根证书,这个证书密钥不会分发出去作为 client 证书,所有组件使用的 client 证书都是由 CA 证书签发而来,所以 CA 证书的 CN 和 O 的名称并不重要,后续其它签发出来的证书的 CN 和 O 的名称才是有用的。
76 |
77 | ## 为 ETCD 签发证书
78 |
79 | 这里证书可以只创建一次,所有 etcd 实例都共用这里创建的证书:
80 |
81 | ``` bash
82 | cat > etcd-csr.json < hosts 需要包含 etcd 被访问时用到的地址,可以用 IP ,域名或泛域名。
116 |
117 | 会生成下面两个重要的文件:
118 |
119 | * `etcd-key.pem`: etcd 密钥。
120 | * `etcd.pem`: etcd 证书。
121 |
--------------------------------------------------------------------------------
/content/deploy/aws/aws-load-balancer-controller.md:
--------------------------------------------------------------------------------
1 | # 安装 aws-load-balancer-controller
2 |
3 | ## 概述
4 |
5 | EKS 集群创建好后,默认只有一个 CLB (Classic Load Balancer) 类型的 Service 实现,托管的, 用户不可见,不感知。
6 |
7 | 通常要安装开源的 [AWS Load Balancer Controller](https://github.com/kubernetes-sigs/aws-load-balancer-controller) 作为 Service/Ingress 在 AWS 环境里的 LB 实现:
8 | * Service 使用 NLB (Network Load Balancer) 实现,默认通过 MutatingWebhook 自动为 Service 加上 LoadBalancerClass 指定为 `service.k8s.aws/nlb` 来替代默认的 CLB 实现。
9 | * Ingress 使用 ALB (Application Load Balancer) 实现。
10 |
11 |
--------------------------------------------------------------------------------
/content/deploy/aws/eks.md:
--------------------------------------------------------------------------------
1 | # 创建 EKS 集群
2 |
3 | ## 添加 EKS 集群
4 |
5 | 在 AWS 控制台搜 eks 进入 EKS 控制台:
6 |
7 | 
8 |
9 | `添加集群` - `创建`:
10 |
11 | 
12 |
13 | 按照步骤配置集群:
14 |
15 | 
16 |
17 | 等待集群创建完成,此时集群是空的,无法调度 pod,也无法通过 kubectl 进行操作。
18 |
19 | ## 配置 kubectl
20 |
21 | 配置 kubectl 前先确保 aws 命令行工具已安装,并参考官方文档进行配置:https://docs.aws.amazon.com/zh_cn/cli/latest/userguide/cli-chap-configure.html
22 |
23 | 我这里图省事,用最简单的方式配置,拿到 Access Key 和 Secret Key 后,执行 `aws configure` 配置即可:
24 |
25 | 
26 |
27 | 然后执行类似下面的命令,会自动将 eks 集群的 kubeconfig 合并到 `~/.kube/config`:
28 |
29 | ```bash
30 | aws eks update-kubeconfig --region us-east-1 --name test-cluster
31 | ```
32 |
33 | > 参考官方文档:https://docs.aws.amazon.com/eks/latest/userguide/create-kubeconfig.html
34 |
35 | 执行一下 kubectl 试试:
36 |
37 | ```bash
38 | $ kubectl get pod -A
39 | NAMESPACE NAME READY STATUS RESTARTS AGE
40 | kube-system coredns-54d6f577c6-4qqm7 0/1 Pending 0 42m
41 | kube-system coredns-54d6f577c6-tsh7n 0/1 Pending 0 42m
42 |
43 | $ kubectl get node
44 | No resources found
45 | ```
46 |
47 | 可以发现自带的 coredns 没有节点可以调度,接下来需要为集群添加计算资源。
48 |
49 | ## 添加计算资源
50 |
51 | 在集群信息页切到`计算`选项卡:
52 |
53 | 
54 |
55 | 点`添加节点组`来添加 EC2 实例作为计算资源,可以设置数量和自动伸缩的最大最小数量。
56 |
57 | 尽量不要选`使用启动模版`,因为需要自行创建 EC2 的启动模版,你得知道所选系统、机型和规格等配置能否兼容 EKS,配错了会导致节点组创建失败,或者节点组创建成功,但最后 EC2 机器加入集群失败。
58 |
59 | 
60 |
--------------------------------------------------------------------------------
/content/deploy/k3s/install.md:
--------------------------------------------------------------------------------
1 | # 快速安装
2 |
3 | ## 概述
4 |
5 | 本文主要给出一些具体的安装实践案例供大家参考。
6 |
7 | ## 安装精简版 k3s
8 |
9 | 有时候个人开发者只想用 k3s 来替代容器来部署一些应用,不需要 k8s 很多复杂的功能,此时在安装的时候可以禁用很多不需要的组件,节约服务器资源:
10 |
11 | ```bash
12 | $ curl -sfL https://get.k3s.io | sh -s - server \
13 | --disable-kube-proxy \
14 | --disable-cloud-controller \
15 | --disable-network-policy \
16 | --disable-helm-controller \
17 | --disable=traefik,local-storage,metrics-server,servicelb,coredns
18 | ```
19 |
20 | > 可根据自身需求,删除部分 `disable` 参数来启用相关功能。
21 |
22 | ## 国内环境安装 k3s
23 |
24 | 国内环境需替换安装脚本地址和依赖的默认 registry:
25 |
26 | ```bash showLineNumbers
27 | # highlight-next-line
28 | INSTALL_K3S_MIRROR=cn curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | sh -s - server \
29 | --disable-cloud-controller \
30 | --disable-network-policy \
31 | --disable-helm-controller \
32 | --disable=traefik,local-storage,metrics-server \
33 | # highlight-next-line
34 | --system-default-registry=registry.cn-hangzhou.aliyuncs.com
35 | ```
36 |
37 | * 国内环境使用 k3s 默认安装脚本网络不通,使用 mirror 脚本替代。
38 | * 通过 `--system-default-registry` 修改依赖的默认 registry,比如 pause、coredns、svclb 等依赖镜像,会从 rancher 的 dockerhub 镜像仓库拉取,国内环境可能拉取不到 dockerhub 镜像,指定为阿里云的 mirror 镜像仓库。
39 |
40 |
41 | ## 路由器上安装极简 k3s
42 |
43 | 将 k3s 安装在自家路由器上,统一用声明式的 yaml 管理路由器的应用和功能,方便刷机后也能重新一键安装回来:
44 |
45 | ```bash showLineNumbers
46 | INSTALL_K3S_MIRROR=cn curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | sh -s - server \
47 | # highlight-start
48 | --node-ip=10.10.10.2 \
49 | --tls-san=10.10.10.2 \
50 | --tls-san-security=false \
51 | # highlight-end
52 | --disable-cloud-controller \
53 | --disable-network-policy \
54 | --disable-helm-controller \
55 | --disable=traefik,local-storage,metrics-server \
56 | --system-default-registry=registry.cn-hangzhou.aliyuncs.com
57 | ```
58 |
59 | > 如果是主路由,公网 ip 每次拨号会变,而 k3s 启动时会获取到外网 ip 作为 hostname,用导出的 kubeconfig 去访问 apiserver 时,会报证书问题(签发时不包含重新拨号之后的外网 ip),可以用 `--node-ip`、`--tls-san` 强制指定一下路由器使用的静态内网 IP。
60 |
61 | ## 延长证书有效期
62 |
63 | k3s 签发的证书默认有效期是 1 年,到期前 90 天之后重启 k3s 会自动续期,但在某些场景无需严格考虑证书安全,比如家用路由器,可自定义下生成的证书的有效期。
64 |
65 | 配置方法是创建 `/etc/default/k3s` 文件,添加如下内容:
66 |
67 | ```env
68 | CATTLE_NEW_SIGNED_CERT_EXPIRATION_DAYS=3650
69 | ```
70 |
71 | > 3650 天 = 10 年
72 |
73 | 然后手动触发证书轮转并重启:
74 |
75 | ```bash
76 | k3s certificate rotate
77 | systemctl restart k3s
78 | ```
79 |
80 | ## 禁止驱逐
81 |
82 | k8s 会自动检测 node 内存和存储空间,超过阈值就会触发驱逐,如果 k3s 只是单机使用,驱逐毫无意义且影响使用,这时可以禁用驱逐。
83 |
84 | 在 `/etc/rancher/k3s/config.yaml` 中添加如下配置:
85 |
86 | ```yaml
87 | kubelet-arg:
88 | - "eviction-hard=memory.available<1Mi,nodefs.available<1Mi" # 禁用驱逐
89 | ```
90 |
91 | ## 禁用镜像清理
92 |
93 | 当存储空间不够时 k3s 会尝试自动清理镜像来释放空间,可能导致不希望被清理的镜像被清理掉,这时可以禁用镜像清理。
94 |
95 | 在 `/etc/rancher/k3s/config.yaml` 中添加如下配置:
96 |
97 | ```yaml
98 | kubelet-arg:
99 | - "image-gc-high-threshold=100" # 禁用 image gc
100 | ```
101 |
102 | ## 允许指定 sysctl
103 |
104 | 如果有些 Pod 的 `securityContext` 指定了一些不安全的内核参数,Pod 就会启动失败(比如 svclb 的 Pod 指定了 `net.ipv4.ip_forward` 这个内核参数)。
105 |
106 | 要启用这些用到的不安全内核参数,在 `/etc/rancher/k3s/config.yaml` 中添加类似如下的配置:
107 |
108 | ```yaml
109 | kubelet-arg:
110 | - "allowed-unsafe-sysctls=net.ipv6.*,net.ipv4.*"
111 | ```
112 |
--------------------------------------------------------------------------------
/content/deploy/k3s/offline.md:
--------------------------------------------------------------------------------
1 | # 离线安装
2 |
3 | ## 步骤
4 |
5 | ### 下载离线文件
6 |
7 | 进入 [k3s release](https://github.com/k3s-io/k3s/releases) 页面,下载 k3s 二进制和依赖镜像的压缩包:
8 |
9 | * `k3s`: 二进制。
10 | * `k3s-airgap-images-amd64.tar`: 镜像压缩包。
11 |
12 | 下载安装脚本:
13 |
14 | ```bash
15 | curl -o install.sh https://get.k3s.io
16 | ```
17 |
18 | 下载完将所有文件放入需要安装 k3s 的机器上。
19 |
20 | ### 安装依赖镜像
21 |
22 | ```bash
23 | sudo mkdir -p /var/lib/rancher/k3s/agent/images/
24 | sudo cp ./k3s-airgap-images-amd64.tar /var/lib/rancher/k3s/agent/images/
25 | ```
26 |
27 | ### 安装 k3s 二进制
28 |
29 | ```bash
30 | chmod +x k3s
31 | cp k3s /usr/local/bin/
32 | ```
33 |
34 | ### 执行安装脚本
35 |
36 | ```bash
37 | chmod +x install.sh
38 | INSTALL_K3S_SKIP_DOWNLOAD=true ./install.sh
39 | ```
40 |
41 | ### 验证
42 |
43 | 查看 k3s 运行状态:
44 |
45 | ```bash
46 | systemctl status k3s
47 | ```
48 |
49 | 查看 k3s 日志:
50 |
51 | ```bash
52 | journalctl -u k3s -f
53 | ```
54 |
55 | 查看 k3s 集群状态:
56 |
57 | ```bash
58 | $ k3s kubectl get node
59 | NAME STATUS ROLES AGE VERSION
60 | vm-55-160-centos Ready control-plane,master 3m22s v1.25.2+k3s1
61 | $ k3s kubectl get pod -A
62 | NAMESPACE NAME READY STATUS RESTARTS AGE
63 | kube-system local-path-provisioner-5b5579c644-6h99x 1/1 Running 0 3m22s
64 | kube-system coredns-75fc8f8fff-sjjzs 1/1 Running 0 3m22s
65 | kube-system helm-install-traefik-crd-mgffn 0/1 Completed 0 3m22s
66 | kube-system metrics-server-74474969b-6bj6r 1/1 Running 0 3m22s
67 | kube-system svclb-traefik-0ab06643-6vj96 2/2 Running 0 3m1s
68 | kube-system helm-install-traefik-m7wdm 0/1 Completed 2 3m22s
69 | kube-system traefik-7d647b7597-dw6b4 1/1 Running 0 3m1s
70 | ```
71 |
72 | ### 获取 kubeconfig
73 |
74 | 若希望在本机之外用 kubectl 操作集群,可以将 kubeconfig 导出来:
75 |
76 | ```bash
77 | k3s kubectl config view --raw > k3s
78 | ```
79 |
80 | 修改其中 server 地址的 IP 为本机 IP,将 kubeconfig 文件放到 kubectl 所在机器上,然后用 [kubecm](https://github.com/sunny0826/kubecm) 合并到本地 kubeconfig:
81 |
82 | ```bash
83 | kubecm add --context-name=k3s -cf k3s
84 | ```
85 |
86 | 使用 [kubectx](https://github.com/ahmetb/kubectx) 切换 context:
87 |
88 | ```bash
89 | $ kubectl ctx k3s
90 | Switched to context "k3s".
91 | ```
92 |
93 | 使用 kubectl 操作 k3s 集群:
94 |
95 | ```bash
96 | $ kubectl get node
97 | NAME STATUS ROLES AGE VERSION
98 | vm-55-160-centos Ready control-plane,master 14m v1.25.2+k3s1
99 | ```
100 |
101 | ## 参考资料
102 |
103 | * [k3s 离线安装官方文档](https://docs.k3s.io/zh/installation/airgap)
104 |
105 |
--------------------------------------------------------------------------------
/content/deploy/terraform.md:
--------------------------------------------------------------------------------
1 | # 使用 Terraform 创建集群
2 |
3 | 利用 Terrafrom 可以创建各种云上产品化的 Kubernetes 集群。
4 |
5 | ## 准备配置文件
6 |
7 | 创建 `main.tf`, 可参考[附录](../appendix/terraform/) 中的示例,根据自己需求按照注释提示替换内容
8 |
9 | ## 创建集群
10 |
11 | 在 `main.tf` 所在目录执行 `terraform init`,然后再执行 `terraform apply`,输入 `yes` 确认执行。
12 |
13 | 等待大约1分多钟,会自动打印创建出来的集群 id:
14 |
15 | ```txt
16 | tencentcloud_eks_cluster.roc-test: Still creating... [1m10s elapsed]
17 | tencentcloud_eks_cluster.roc-test: Still creating... [1m20s elapsed]
18 | tencentcloud_eks_cluster.roc-test: Creation complete after 1m21s [id=cls-4d2qxcs5]
19 |
20 | Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
21 | ```
22 |
23 | ## 获取 kubeconfig
24 |
25 | 集群刚创建好的时候,APIServer 外网访问的 CLB 还没创建好,不知道外网 IP 地址,terraform 本地记录的状态里,kubeconfig 的 server 地址就为空。所以我们先 refresh 一下,将创建好的 server 地址同步到本地:
26 |
27 | ```bash
28 | terraform refresh
29 | ```
30 |
31 | 然后导出 kubeconfig 文件:
32 |
33 | ```bash
34 | terraform show -json | jq -r '.values.root_module.resources[] | select(.address | test("tencentcloud_eks_cluster.roc-test")) | .values.kube_config' > eks
35 | ```
36 |
37 | > 注意替换 `roc-test` 为自己在 `main.tf` 文件中定义的名字。
38 |
39 | 使用 [kubecm](../kubectl/merge-kubeconfig-with-kubecm) 可以一键导入合并 kubeconfig:
40 |
41 | ```bash
42 | kubecm add -f eks
43 | ```
44 |
45 | 使用 [kubectx](../kubectl/quick-switch-with-kubectx) 可以切换 context:
46 |
47 | ```bash
48 | kubectl ctx eks
49 | ```
50 |
51 | 然后就可以使用 kubectl 操作集群了。
52 |
53 | ## 销毁集群
54 |
55 | 在 `main.tf` 所在目录执行:
56 |
57 | ```bash
58 | terraform destroy
59 | ```
60 |
61 | ## 参考资料
62 |
63 | * [Terrafrom TencentCloud Provider Documentation](https://registry.terraform.io/providers/tencentcloudstack/tencentcloud/latest/docs)
64 |
65 |
--------------------------------------------------------------------------------
/content/dev/kubebuilder/init-before-start.md:
--------------------------------------------------------------------------------
1 | # 在 Controller 启动前对依赖进行初始化
2 |
3 | ## 场景
4 |
5 | 某些 Controller 在启动前需要进行一些初始化操作,比如根据存储在 CR 中的信息来初始化内部的缓存,作为 Controller 调谐时的依赖项。
6 |
7 | :::tip[备注]
8 |
9 | 比如针对负载均衡器的端口分配器,内部缓存需记录各个负载均衡器及其已分配的端口信息,Controller 在调谐时检查到还未为其分配端口,就从缓存中拿到负载均衡器及其端口信息,计算出下一个可用端口并执行分配操作,最后 Controller 将已分配信息记录到 status 中。
10 |
11 | :::
12 |
13 | ## 实现方法
14 |
15 | 核心是在调用 Controller 的 SetupWithManager 之前调用 Manager 的 Add 方法,传入 Runable 对象,会保证其在 cache 准备就行之后,Controller 启动之前执行。
16 |
17 | ```go
18 | if err := mgr.Add(&initCache{mgr.GetClient()}); err != nil {
19 | setupLog.Error(err, "problem add init cache")
20 | os.Exit(1)
21 | }
22 | ```
23 |
24 | 该对象实现 Runnable 接口(Start 函数),如果是多副本选主的 Controller,也实现下 LeaderElectionRunnable 接口(NeedLeaderElection 函数),确保成为 leader 后才执行:
25 |
26 | ```go
27 | type initCache struct {
28 | client.Client
29 | }
30 |
31 | func (i *initCache) NeedLeaderElection() bool {
32 | return true
33 | }
34 |
35 | func (i *initCache) Start(ctx context.Context) error {
36 | setupLog.Info("starting init cache")
37 | defer setupLog.Info("end init cache")
38 |
39 | // 初始化端口池
40 | ppl := &networkingv1alpha1.CLBPortPoolList{}
41 | if err := i.List(ctx, ppl); err != nil {
42 | return err
43 | }
44 | for _, pp := range ppl.Items {
45 | if err := portpool.Allocator.AddPool(pp.Name, pp.Spec.StartPort, pp.Spec.EndPort, pp.Spec.SegmentLength); err != nil {
46 | return err
47 | }
48 | }
49 |
50 | // 初始化已分配的端口信息
51 | pbl := &networkingv1alpha1.CLBPodBindingList{}
52 | if err := i.List(ctx, pbl); err != nil {
53 | return err
54 | }
55 | for _, pb := range pbl.Items {
56 | for _, podBinding := range pb.Status.PortBindings {
57 | if err := portpool.Allocator.MarkAllocated(podBinding.Pool, podBinding.Port, podBinding.Protocol); err != nil {
58 | return err
59 | }
60 | }
61 | }
62 | return nil
63 | }
64 | ```
65 |
--------------------------------------------------------------------------------
/content/dev/kubebuilder/multi-version.md:
--------------------------------------------------------------------------------
1 | # 多版本 API 开发
2 |
3 | ## 背景
4 |
5 | 随着 controller 的不断迭代,可能需要新增或变更某些功能,此时一般需要新增 API 版本进行支持,我们就需要管理多个 API 版本,本文介绍用 kubebuilder 开发 controller 时如何高效且优雅的管理多个 API 版本。
6 |
7 |
8 | ## 新增 API 版本
9 |
10 | 如果要对某个 API 新增版本,先用 kubebuilder 操作新建一下,如:
11 |
12 | ```bash
13 | kubebuilder create api --group webapp --version v1beta2 --kind Guestbook
14 | ```
15 |
16 | 然后将上一个版本的 API 的 `xx_types.go` (如 `guestbook_types.go`) 文件拷贝覆盖到新版本的 `xx_types.go` 文件中,并修改新版本 `xx_types.go` 文件中的包名为新版本的包名。
17 |
18 | 在新版本的 API 结构体中上面加上下面的注释标记并移出其它版本 API 结构体的这个标记(如果有的话),表示后续新增的对象使用该版本 API 进行存储(存量的还是用旧版本存储):
19 |
20 | ```go
21 | // +kubebuilder:storageversion
22 | ```
23 |
24 | ## API 自动转换
25 |
26 | 客户端(如kubectl、client-go)在请求 API 时,会指定 API 版本,当存储的 API 版本与请求的版本不一致时,APIServer 可以调用 controller 提供的 webhook 进行自动转换,下面是实现自动转换 API 版本的 webhook 的方法。
27 |
28 | 首先使用 `kubebuilder` 创建 webhook(假设是希望将 v1alpha1 的 Guestbook 转换成 v1beta1):
29 |
30 | ```bash
31 | kubebuilder create webhook --group webapp --version v1beta1 --kind Guestbook --conversion --spoke v1alpha1
32 | ```
33 |
34 | - `--version` 指定 Hub 版本(存储的 API 版本),通常是最新版本。
35 | - `--spoke` 指定请求的要被自动转换的其它 API 版本,通常是旧版本。
36 |
37 | :::tip[注意]
38 | - 以上只是实现 API 自动转换需要的 create webhook 参数,如果还有其他 webhook 需求(如 ValidatingWebhook `--programmatic-validation`,自动设置默认值 `--defaulting`)也需要带上,相同 API 版本的 webhook 创建后不能重新执行 create webhook 命令来追加功能,需一次性到位。
39 | :::
40 |
41 | 执行后可以看到:
42 | 1. v1beta1 下新增了 `xx_conversion.go` 文件,为 API 结构体新增了 `Hub` 空函数,实现 [Hub](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion#Hub) 接口,用于标记该 API 版本是 Hub 版本,其它版本的 API 将会自动转换成这个版本。
43 | 2. v1alpha1 下也新增了 `xx_conversion.go` 文件,为 API 结构体新增了 `ConvertTo` 和 `ConvertFrom` 函数,实现 [Convertible](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion#Convertible) 接口,将会被 webhook 自动调用用于转换 API 版本。
44 |
45 | 然后将旧版本 API 下的 `xx_webhook.go`(包含 `SetupWebhookWithManager`) 和 `xx_webhook_test.go` 移动到新版本 API 下,并修改包名为新版本的包名,调用 `SetupWebhookWithManager` 的地方也跟着改一下引用到新版本。Webhook 中的其它相关逻辑如果需要改动也要改一下(比如 `ValidateXXX`, `Default` 等),围绕新版本 API 来改动相关逻辑。
46 |
47 | ## 参考资料
48 |
49 | - [Kubebuilder Tutorial: Multi-Version API](https://book.kubebuilder.io/multiversion-tutorial/tutorial)
50 |
--------------------------------------------------------------------------------
/content/dev/kubebuilder/quickstart.md:
--------------------------------------------------------------------------------
1 | # 快速上手
2 |
3 | ## 安装
4 |
5 | 如果你有 Homebrew,可以通过 `brew install kubebuilder` 一键安装。
6 |
7 | 没有的话可以通过下面的脚本安装 `kubebuilder` 二进制到 `PATH` 下:
8 |
9 | ```txt
10 | # download kubebuilder and install locally.
11 | curl -L -o kubebuilder "https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)"
12 | chmod +x kubebuilder && sudo mv kubebuilder /usr/local/bin/
13 | ```
14 |
15 | ## 创建项目
16 |
17 | ```bash
18 | mkdir -p ~/projects/guestbook
19 | cd ~/projects/guestbook
20 | kubebuilder init --domain my.domain --repo my.domain/guestbook
21 | ```
22 |
23 | - `domain` 是指 API Group 的域名后缀,比如要创建的 API 的 Group 是 `test.my.domain`,那么 `domain` 就是 `my.domain`。
24 | - `repo` 是 go 项目的 go module 名称,相当于 `go mod init` 的参数。
25 |
26 | ## 创建 API
27 |
28 | 假设要创建的 API 的 `apiVersion` 是 `webapp.my.domain/v1alpha1`,`kind` 是 `Guestbook`,那么命令如下:
29 |
30 | ```bash
31 | kubebuilder create api --group webapp --version v1alpha1 --kind Guestbook
32 | ```
33 |
34 | - `group` 是 API Group 的前缀,完整 API Group 是这个 `group` 参数加上前面 `kubebuilder init` 时指定的 `domain`,前面指定的是 `my.domain`,所以完整的 API Group 是 `webapp.my.domain`。
35 | - `version` 是 API 的版本,比如 `v1alpha1`。
36 | - `kind` 是 API 的 Kind,比如 `Guestbook`。
37 |
38 |
39 | ## 生成代码和 YAML
40 |
41 | 创建或修改 API 后:
42 | 1. 执行 `make manifests` 可生成相关 YAML,在 `config` 目录下。
43 | 2. 执行 `make generate` 可生成相关代码,在 `api` 目录下,主要是更新 `zz_generated.deepcopy.go`。
44 |
45 | ## 参考资料
46 |
47 | - [Kubebuilder Quick Start](https://book.kubebuilder.io/quick-start)
48 |
--------------------------------------------------------------------------------
/content/dev/kubebuilder/remove-api.md:
--------------------------------------------------------------------------------
1 | # 移除 API
2 |
3 | ## 场景
4 |
5 | 创建了新的试验性 API 并围绕它开发相关逻辑,但发现它不合适,需要将其从 API 中移除。
6 |
7 | ## 编辑 PROJECT 文件
8 |
9 | 将 API 从 PROJECT 文件中移除(resources 数组下)。
10 |
11 | ## 移除 types
12 |
13 | 移除 `api` 目录下相关的结构体定义。
14 |
15 | ## 移除 CRD 定义
16 |
17 | 移除自动生成的 CRD 定义,在 `config/crd/bases` 目录下。
18 |
19 | ## 移除 RBAC 定义
20 |
21 | 删除如下文件:
22 |
23 | - `config/rbac/xxx_admin_role.yaml`
24 | - `config/rbac/xxx_editor_role.yaml`
25 | - `config/rbac/xxx_viewer_role.yaml`
26 |
27 | 删除下面文件中关于该 API 的部分:
28 |
29 | - `config/rbac/role.yaml`
30 | - `config/rbac/kustomization.yaml`
31 |
32 | ## 删除 samples
33 |
34 | 删除 `config/samples` 目录下相关的样例资源以及 `config/samples/kustomization.yaml` 中相关的文件引用。
35 |
36 | ## 删除 webhook
37 |
38 | 如果该 API 包含 webhook:
39 | 1. 删除 `config/webhook/manifests.yaml` 中相关的内容。
40 | 2. 删除 webhook 相关代码文件,`internal/webhook/xxx/xxx_webhook.go` 和 `internal/webhook/xxx/xxx_webhook_test.go`
41 |
42 | ## 删除 controller
43 |
44 | 如果为该 API 实现了 controller,需删除相关代码文件:
45 | - `internal/controller/xxx_controller.go`
46 | - `internal/controller/xxx_controller_test.go`
47 |
48 | ## 删除 Manager 相关引用
49 |
50 | Manager 的初始化逻辑中:
51 | 1. controller 调用 SetupWithManager 的部分删掉。
52 | 2. webhook 调用 `SetupXXXWebhookWithManager` 的部分删掉。
53 |
--------------------------------------------------------------------------------
/content/dev/kubebuilder/webhook.md:
--------------------------------------------------------------------------------
1 | # Webhook 开发
2 |
3 | ## 概述
4 |
5 | K8S 有 MutatingWebhook 和 ValidatingWebhook 两种类型的 Webhook,分别用于修改 Pod 的 Spec 和验证 Pod 的 Spec,本文教你如何用 kubebuilder 快速开发 Webhook。
6 |
7 | ## 快速创建 MutatingWebhook 和 ValidatingWebhook 脚手架代码
8 |
9 | 使用 kubebuilder 创建 Webhook 脚手架代码用 `kubebuilder create webhook`,`--defaulting` 和 `--programmatic-validation` 参数分别表示 MutatingWebhook 和 ValidatingWebhook。
10 |
11 | 假如同时创建两种 Webhook,示例命令如下:
12 |
13 | ```bash
14 | kubebuilder create webhook --group networking --version v1alpha1 --kind CLBPortPool --defaulting --programmatic-validation
15 | ```
16 |
17 | ## 创建 K8S 内置资源的 Webhook
18 |
19 | 有时候我们希望对 K8S 内置的资源进行一些校验和修改,比如为 Pod 支持一些自定义的注解,如果有设置相应注解就自动校验并创建本项目的 CRD 资源与 Pod 进行关联,再由本项目中的 controller 进行调谐。
20 |
21 | 如何实现呢?以给 Pod 添加 ValidatingWebhook 和 MutatingWebhook 为例,添加 Pod API 到项目但不创建 CRD 和控制器:
22 |
23 | ```bash
24 | kubebuilder create api --group core --kind Pod --version v1 --controller=false --resource=false
25 | ```
26 |
27 | 然后再创建 Webhook:
28 |
29 | ```bash
30 | kubebuilder create webhook --group core --version v1 --kind Pod --defaulting --programmatic-validation
31 | ```
32 |
33 | 最后再修改下初始化 manager 的代码,让它调用 `SetupPodWebhookWithManager`。
34 |
35 | ## 限制 Webhook 作用范围
36 |
37 | 假如为 Pod 创建了 Webhook,但希望只针对带有特定注解或 label 的 Pod 生效,此时可以配置下匹配条件,避免对所有 Pod 生效。
38 |
39 | :::info[注意]
40 |
41 | 如果 Pod Webhook 没配置匹配条件, 当 Manager Pod 异常(比如节点压力大被驱逐),可能导致所有 Pod 无法创建和修改,连 Manager Pod 自身也无法创建出来,导致整个集群陷入瘫痪。
42 |
43 | :::
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | 将 Webhook 的自定义写成用于 patch 的 yaml,如 `patch-match-conditions.yaml`,放在 `config/webhook` 目录下,然后修改该目录下的 `kustomization.yaml`:
55 |
56 | ```yaml
57 | resources:
58 | - manifests.yaml
59 | - service.yaml
60 |
61 | namePrefix: example-app-
62 |
63 | configurations:
64 | - kustomizeconfig.yaml
65 |
66 | # highlight-add-start
67 | patches:
68 | - path: patch-match-conditions.yaml
69 | target:
70 | group: admissionregistration.k8s.io
71 | version: v1
72 | kind: ValidatingWebhookConfiguration
73 | name: validating-webhook-configuration
74 | # highlight-add-end
75 | ```
76 |
--------------------------------------------------------------------------------
/content/gitops/argocd/cluster-and-repo.md:
--------------------------------------------------------------------------------
1 | # 集群与 Git 仓库管理
2 |
3 | ## 管理方法
4 |
5 | 推荐每个集群使用一个 Git 仓库来存储该集群所要部署的所有应用的 YAML 与配置。
6 |
7 | 如果多个集群要部署相同或相似的应用,可抽取成单独的 Git 仓库,作为 submodule 引用进来。
8 |
9 | 这样做的好处是既可以减少冗余配置,又可以控制爆炸半径。submodule 可能被多个 Git 仓库共享(即多个集群部署相同应用),但如果不执行 `git submodule update --remote` 的话,引用的 commit id 是不会变的,所以也不会因为上游应用更新而使所有使用了该应用的集群一下子全部都更新。
10 |
11 | ## 添加集群
12 |
13 | 查看当前有哪些集群:
14 |
15 | ```bash
16 | argocd cluster list
17 | ```
18 |
19 | > 默认会有一个 `in-cluster` 集群,代表 `argocd` 所在集群。
20 |
21 | 添加新集群:
22 |
23 | ```bash
24 | argocd cluster add mycluster
25 | ```
26 |
27 | > `mycluster` 是 kubeconfig 中配置的 context 名称,会将此 context 对应的集群添加到 argocd 中,如果希望添加时修改集群名称,可以加 `--name` 来覆盖名称。
28 |
29 | ## Git 仓库管理
30 |
31 | 查看当前有哪些 Git 仓库:
32 |
33 | ```bash
34 | argocd repo list
35 | ```
36 |
37 | 添加 Git 仓库:
38 |
39 | ```bash
40 | argocd repo add --ssh-private-key-path $HOME/.ssh/id_rsa --insecure-skip-server-verification git@yourgit.com:your-org/your-repo.git
41 | ```
42 |
43 | > 通常 GitOps 使用的仓库是私有仓库,所以添加仓库时一般用 `--ssh-private-key-path` 指定下 SSH 密钥,以便让 argocd 能够正常拉取到 Git 仓库。
44 |
--------------------------------------------------------------------------------
/content/gitops/argocd/project.md:
--------------------------------------------------------------------------------
1 | # Git 项目组织方法
2 |
3 | ## 在根目录创建 ApplicationSet
4 |
5 | 在 Git 仓库根目录下创建 `argo-apps.yaml` 的文件,定义 `ArgoCD` 的 `ApplicationSet`:
6 |
7 | ```yaml title="argo-apps.yaml" showLineNumbers
8 | apiVersion: argoproj.io/v1alpha1
9 | kind: ApplicationSet
10 | metadata:
11 | name: apps-mycluster # ApplicationSet 名称,建议带集群名称后缀
12 | namespace: argocd
13 | spec:
14 | goTemplate: true
15 | goTemplateOptions: ["missingkey=error"]
16 | generators:
17 | - git: # 通过当前 Git 仓库的 apps 目录下的子目录自动生成 Application
18 | repoURL: git@yourgit.com:your-org/your-repo.git
19 | revision: HEAD
20 | directories:
21 | - path: apps/*
22 | template:
23 | metadata:
24 | name: "{{.path.basename}}-mycluster" # 自动创建的 Application 的名称格式为: 目录名-集群名
25 | namespace: argocd
26 | spec:
27 | project: default
28 | source:
29 | repoURL: git@yourgit.com:your-org/your-repo.git
30 | targetRevision: HEAD
31 | path: "apps/{{.path.basename}}" # 自动生成的 Application 使用的 YAML 内容在对应子目录下
32 | destination:
33 | name: mycluster # Application 被部署的目的集群
34 | syncPolicy:
35 | automated:
36 | prune: true
37 | selfHeal: true
38 | ```
39 |
40 | > 要点解析请看注释内容。
41 |
42 | ## apps 子目录管理方法
43 |
44 | apps 下面的每个子目录中的 YAML,都将作为一个 `Application` 所需的 K8S 资源,可以直接是 K8S YAML,也可以是 `kustomize` 格式的结构。
45 |
46 | 建议统一采用 `kustomize` 的格式来组织,示例:
47 |
48 | ```txt
49 | apps
50 | ├── jellyfin
51 | │ ├── daemonset.yaml
52 | │ └── kustomization.yaml
53 | └── monitoring
54 | ├── kustomization.yaml
55 | ├── namespace.yaml
56 | ├── values.yaml
57 | └── vm-hostpath-pv.yaml
58 | ```
59 |
60 | 这样,如果你要添加新的应用,只需要在 apps 下直接新增一个目录就行,不需要再去定义 `Application` 了,会由 `ApplicationSet` 自动生成。
61 |
62 | ## submodules 管理
63 |
64 | 多个集群可能会安装相同的应用,而我们采用一个集群的配置对应一个 Git 仓库的管理方法,相同的依赖应用可以提取到单独的 Git 仓库,通过 git 的 submodule 方式引用。
65 |
66 | 比如多个集群都会安装 EnvoyGateway,将 EnvoyGateway 用单独的 Git 仓库管理,文件结构如下:
67 |
68 | ```txt
69 | install
70 | ├── Makefile
71 | ├── install.yaml
72 | └── kustomization.yaml
73 | ```
74 |
75 | 假设对于的 Git 仓库是 `git@yourgit.com:your-org/envoygateway.git`,现将其作为依赖引入到当前 Git 仓库,首先添加 git submodule:
76 |
77 | ```bash
78 | git submodule add --depth=1 git@yourgit.com:your-org/envoygateway.git submodules/envoygateway
79 | ```
80 |
81 | 然后在 apps 目录下创建 envoygateway 的目录,并创建 `kustomization.yaml`:
82 |
83 | ```txt
84 | apps
85 | └── envoygateway
86 | └── kustomization.yaml
87 | ```
88 |
89 | `kustomization.yaml` 的内容如下:
90 |
91 | ```yaml
92 | apiVersion: kustomize.config.k8s.io/v1beta1
93 | kind: Kustomization
94 | resources:
95 | - ../../submodules/envoygateway/install
96 | ```
97 |
98 | 其它集群的 Git 仓库也一样的操作,这样就实现了多个集群共享同一个应用的 YAML,如果有细微自定义差别,可直接修改 `kustomization.yaml` 进行自定义。如果这个共同依赖的应用需要更新版本,就更新这个 submodules 对应的仓库,然后再更新集群对应仓库的 submodule:
99 |
100 | ```bash
101 | git submodule update --init --remote
102 | ```
103 |
104 | > 每个集群对应仓库的 submodule 分开更新,可实现按集群灰度,避免更新出现问题一下子影响所有集群。
105 |
--------------------------------------------------------------------------------
/content/images/podman.md:
--------------------------------------------------------------------------------
1 | # 使用 Podman 构建镜像
2 |
3 | ## 概述
4 |
5 | [Podman](https://podman.io/) 是一个类似 docker 的工具,可以运行容器,也可以构建镜像,甚至可以像 docker 一样支持构建多平台镜像。如今 Docker Desktop 已经宣布收费,可以考虑使用 Podman 来替代。
6 |
7 | ## 安装
8 |
9 | 参考 [官方安装文档](https://podman.io/getting-started/installation),我使用的是 Mac,安装很简单:
10 |
11 | ```bash
12 | brew install podman
13 | ```
14 |
15 | 由于 podman 是基于 Linux 的,安装在 Mac 需要先启动它的虚拟机:
16 |
17 | ```bash
18 | podman machine init
19 | podman machine start
20 | ```
21 |
22 | 最后检查下是否 ok:
23 |
24 | ```bash
25 | podman info
26 | ```
27 |
28 | ## Podman 构建镜像的背后
29 |
30 | Podman 构建镜像在背后实际是利用了 [Buildah](https://buildah.io/) 这个工具去构建,只是封装了一层,更容易使用了。
31 |
32 | ## Podman 构建镜像的方法
33 |
34 | `podman build` 基本兼容 `docker build`,所以你可以像使用 docker 一样去使用 podman 构建镜像。
35 |
36 | ## FAQ
37 |
38 | ### 未启动虚拟机导致报错
39 |
40 | 执行 podman 命令是,遇到 `connect: no such file or directory` 的报错:
41 |
42 | ```bash
43 | $ podman build --platform=linux/amd64 . -t imroc/crontab:centos -f centos.Dockerfile
44 | Cannot connect to Podman. Please verify your connection to the Linux system using `podman system connection list`, or try `podman machine init` and `podman machine start` to manage a new Linux VM
45 | Error: unable to connect to Podman socket: Get "http://d/v4.0.2/libpod/_ping": dial unix ///var/folders/91/dsfxsd7j28z2mxl7vm91mjg40000gn/T/podman-run--1/podman/podman.sock: connect: no such file or directory
46 | ```
47 |
48 | 通常是因为在非 Linux 的系统上,没有启动 podman linux 虚拟机导致的,启动下就可以了。
49 |
50 | ### 代理导致拉取镜像失败
51 |
52 | 使用 podman 构建镜像或直接拉取镜像的过程中,遇到这种报错:
53 |
54 | ```txt
55 | Error: error creating build container: initializing source docker://centos:8: pinging container registry registry-1.docker.io: Get "https://registry-1.docker.io/v2/": proxyconnect tcp: dial tcp 127.0.0.1:12639: connect: connection refused
56 | ```
57 |
58 | 通常是因为启动 podman 虚拟机时,终端上有 HTTP 代理的环境变量,可以销毁虚拟机,重新启动,启动前确保当前终端没有 HTTP 代理的环境变量。
59 |
60 | ## 参考资料
61 |
62 | * [Migrating from Docker to Podman](https://marcusnoble.co.uk/2021-09-01-migrating-from-docker-to-podman/)
--------------------------------------------------------------------------------
/content/images/sync-images-with-skopeo.md:
--------------------------------------------------------------------------------
1 | # 使用 skopeo 批量同步 helm chart 依赖镜像
2 |
3 | ## skopeo 是什么?
4 |
5 | [skepeo](https://github.com/containers/skopeo) 是一个开源的容器镜像搬运工具,比较通用,各种镜像仓库都支持。
6 |
7 | ## 安装 skopeo
8 |
9 | 参考官方的 [安装指引](https://github.com/containers/skopeo/blob/main/install.md)。
10 |
11 | ## 导出当前 helm 配置依赖哪些镜像
12 |
13 | ```bash
14 | $ helm template -n monitoring -f kube-prometheus-stack.yaml ./kube-prometheus-stack | grep "image:" | awk -F 'image:' '{print $2}' | awk '{$1=$1;print}' | sed -e 's/^"//' -e 's/"$//' > images.txt
15 | $ cat images.txt
16 | quay.io/prometheus/node-exporter:v1.3.1
17 | quay.io/kiwigrid/k8s-sidecar:1.19.2
18 | quay.io/kiwigrid/k8s-sidecar:1.19.2
19 | grafana/grafana:9.0.2
20 | registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.5.0
21 | quay.io/prometheus-operator/prometheus-operator:v0.57.0
22 | quay.io/prometheus/alertmanager:v0.24.0
23 | quay.io/prometheus/prometheus:v2.36.1
24 | bats/bats:v1.4.1
25 | k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1
26 | k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1
27 | ```
28 |
29 | * 使用 helm template 渲染 yaml,利用脚本导出所有依赖的容器镜像并记录到 `images.txt`。
30 | * 可以检查下 `images.txt` 中哪些不需要同步,删除掉。
31 |
32 | ## 准备同步脚本
33 |
34 | 准备同步脚本(`sync.sh`):
35 |
36 | ```bash
37 | #! /bin/bash
38 |
39 | DST_IMAGE_REPO="registry.imroc.cc/prometheus"
40 |
41 | cat images.txt | while read line
42 | do
43 | while :
44 | do
45 | skopeo sync --src=docker --dest=docker $line $DST_IMAGE_REPO
46 | if [ "$?" == "0" ]; then
47 | break
48 | fi
49 | done
50 | done
51 | ```
52 |
53 | * 修改 `DST_IMAGE_REPO` 为你要同步的目标仓库地址与路径,`images.txt` 中的镜像都会被同步到这个仓库路径下面。
54 |
55 | 赋予脚本执行权限:
56 |
57 | ```bash
58 | chmod +x sync.sh
59 | ```
60 |
61 | ## 登录仓库
62 |
63 | 同步镜像时,不管是源和目的,涉及到私有镜像,都需要先登录,不然同步会报错。
64 |
65 | 登录方法很简单,跟 `docker login` 一样,指定要登录的镜像仓库地址:
66 |
67 | ```bash
68 | skopeo login registry.imroc.cc
69 | ```
70 |
71 | 然后输入用户名密码即可。
72 |
73 | ## 执行同步
74 |
75 | 最后执行 `./sync.sh` 即可将所有镜像一键同步到目标仓库中,中途如果失败会一直重试直到成功。
76 |
77 | ## FAQ
78 |
79 | ### 为什么不用 skopeo 配置文件方式批量同步?
80 |
81 | 因为配置相对复杂和麻烦,不如直接用一个列表文本,每行代表一个镜像,通过脚本读取每一行分别进行同步,这样更简单。
--------------------------------------------------------------------------------
/content/kubectl/build.md:
--------------------------------------------------------------------------------
1 | # 自行编译 kubectl
2 |
3 | 如果对 kubectl 有些特殊需求,进行了二次开发,可通过下面的方式编译:
4 |
5 | ```bash
6 | make kubectl KUBE_BUILD_PLATFORMS=darwin/arm64
7 | ```
8 |
9 | - `KUBE_BUILD_PLATFORMS` 指定要编译的目标平台。
10 |
--------------------------------------------------------------------------------
/content/kubectl/kubectl-aliases.md:
--------------------------------------------------------------------------------
1 | # 使用 kubectl-aliases 缩短命令
2 |
3 | 日常使用 kubectl 进行各种操作,每次输入完整命令会比较浪费时间,推荐使用 [kubectl-aliases](https://github.com/ahmetb/kubectl-aliases) 来提升 kubectl 日常操作效率,敲更少的字符完成更多的事。
4 |
5 | ## 安装 kubectl-aliases
6 |
7 | 参考 [官方安装文档](https://github.com/ahmetb/kubectl-aliases#installation)
8 |
9 | ## 查看完整列表
10 |
11 | ```bash
12 | cat ~/.kubectl_aliases
13 | ```
14 |
15 | ## 高频使用的别名
16 |
17 | ```bash
18 | ka // kubectl apply --recursive -f
19 | kg // kubectl get
20 | kgpo // kubectl get pods
21 | ksys // kubectl -n kube-system
22 | ksysgpo // kubectl -n kube-system get pods
23 | kd // kubectl describe
24 | kdpo // kubectl describe pod
25 | ```
26 |
27 | ## 自定义
28 |
29 | 建议针对自己常用的操作设置下别名,比如经常操作 istio 的话,可以用 `ki` 来代替 `kubectl -n istio-system`。
30 |
31 | 编辑 `~/.kubectl_aliases`:
32 |
33 | ```bash
34 | alias ki='kubectl -n istio-system'
35 | ```
36 |
--------------------------------------------------------------------------------
/content/kubectl/merge-kubeconfig-with-kubecm.md:
--------------------------------------------------------------------------------
1 | # 使用 kubecm 合并 kubeconfig
2 |
3 | Kubernetes 提供了 kubectl 命令行工具来操作集群,使用 kubeconfig 作为配置文件,默认路径是 `~/.kube/config`,如果想使用 kubectl 对多个集群进行管理和操作,就在 kubeconfig 中配置多个集群的信息即可,通常可以通过编辑 kubeconfig 文件或执行一堆 `kubectl config` 的命令来实现。
4 |
5 | 一般情况下,Kubernetes 集群在安装或创建好之后,都会生成 kubeconfig 文件,如何简单高效的将这些 kubeconfig 合并以便让我们通过一个 kubeconfig 就能方便的管理多集群呢?我们可以借助 [kubecm](https://github.com/sunny0826/kubecm) 这个工具,本文将介绍如何利用 `kubecm` 来实现多集群的 kubeconfig 高效管理。
6 |
7 | ## 安装 kubecm
8 |
9 | 首先需要在管理多集群的机器上安装 `kubecm`,安装方法参考 [官方文档](https://kubecm.cloud/#/zh-cn/install) 。
10 |
11 | ## 使用 kubecm 添加访问凭证到 kubeconfig
12 |
13 | 首先拿到你集群的 kubeconfig 文件,将其重命名为你想指定的 context 名称,然后通过下面的命令将 kubeconfig 信息合并到 `~/.kube/config`:
14 |
15 | ``` bash
16 | kubecm add --context-name=dev -cf config.yaml
17 | ```
18 |
19 | * `dev` 替换为希望导入后的 context 名称。
20 | * `config.yaml` 替换为 kubeconfig 文件名。
21 |
22 | ## 查看集群列表
23 |
24 | 通过 `kubecm` 添加了要管理和操作的集群后,通过 `kubecm ls` 可查看 kubeconfig 中的集群列表 (星号标识的是当前操作的集群):
25 |
26 |
27 | 
28 |
29 | ## 切换集群
30 |
31 | 当想要切换到其它集群操作时,可使用 `kubecm switch` 进行交互式切换:
32 |
33 | 
34 |
35 |
36 | 不过还是推荐使用 kubectx 进行切换。
37 |
38 | ## 移除集群
39 |
40 | 如果想要移除某个集群,可以用 `kubecm delete `:
41 |
42 | 
43 |
--------------------------------------------------------------------------------
/content/kubectl/quick-switch-with-kubectx.md:
--------------------------------------------------------------------------------
1 | # 使用 kubectx 和 kubens 快速切换
2 |
3 | 推荐使用 `kubectx` 和 `kubens` 来在多个集群和命名空间之间快速切换。
4 |
5 | ## 项目地址
6 |
7 | 这两个工具都在同一个项目中: [https://github.com/ahmetb/kubectx](https://github.com/ahmetb/kubectx)
8 |
9 | ## 安装
10 |
11 | 参考 [官方安装文档](https://github.com/ahmetb/kubectx#installation)。
12 |
13 | 推荐使用 kubectl 插件的方式安装:
14 |
15 | ```bash
16 | kubectl krew install ctx
17 | kubectl krew install ns
18 | ```
19 |
20 | > 如果没安装 [krew](https://krew.sigs.k8s.io/),需提前安装下,参考 [krew 安装文档](https://krew.sigs.k8s.io/docs/user-guide/setup/install/)。
21 |
22 | ## 使用
23 |
24 | 插件方式安装后,使用如下命令切换集群:
25 |
26 | ```bash
27 | kubectl ctx [CLUSTER]
28 | ```
29 |
30 | 切换命名空间:
31 |
32 | ```bash
33 | kubectl ns [NAMESPACE]
34 | ```
35 |
36 | 推荐结合 [使用 kubectl 别名快速执行命令](./kubectl-aliases.md) 来缩短命令:
37 |
38 | ```bash
39 | k ctx [CLUSTER]
40 | k ns [NAMESPACE]
41 | ```
--------------------------------------------------------------------------------
/content/monitoring/prometheus/annotation-discovery.md:
--------------------------------------------------------------------------------
1 | # 基于 Pod 和 Service 注解的服务发现
2 |
3 | ## 背景
4 |
5 | 很多应用会为 Pod 或 Service 打上一些注解用于 Prometheus 的服务发现,如 `prometheus.io/scrape: "true"`,这种注解并不是 Prometheus 官方支持的,而是社区的习惯性用法,要使这种注解生效,还需结合 Prometheus 的采集配置,本文介绍具体的配置方法。
6 |
7 | :::warning
8 |
9 | 如果你使用 `kube-prometheus-stack` 部署的监控系统,默认就会对自身的一些组件创建采集规则,比如会给 `kube-state-metrics` 创建 `ServiceMonitor`,并且 `kube-state-metrics` 的 `Service` 上也有 `prometheus.io/scrape: "true"` 的注解,如果配置了基于 Service 注解的服务发现,就会导致重复采集。
10 |
11 | :::
12 |
13 | ## 真实案例
14 |
15 | ### istio 指标采集
16 |
17 | [istio](https://istio.io/) 使用了这种 Pod 注解,当 Pod 被自动注入 sidecar 的同时也会被自动注入以下注解:
18 |
19 | ```yaml
20 | prometheus.io/path: /stats/prometheus
21 | prometheus.io/port: "15020"
22 | prometheus.io/scrape: "true"
23 | ```
24 |
25 | 表示声明让 Prometheus 采集 Envoy Sidecar 暴露的 metrics,端口是 15020,路径是 `/stats/prometheus`。
26 |
27 | 除此之外,控制面组件 istiod 的 Pod 也会有类似注解:
28 |
29 | ```yaml
30 | prometheus.io/port: "15014"
31 | prometheus.io/scrape: "true"
32 | ```
33 |
34 | ### Kubernetes Addon 指标采集
35 |
36 | Kubenretes 源码仓库中的一些 addon 组件也使用了这种注解,有的是 Pod 注解,有的是 Service 注解。
37 | * [coredns](https://github.com/kubernetes/kubernetes/blob/release-1.30/cluster/addons/dns/coredns/coredns.yaml.base#L196-L197) 使用 Service 注解:
38 | ```yaml showLineNumbers
39 | apiVersion: v1
40 | kind: Service
41 | metadata:
42 | name: kube-dns
43 | namespace: kube-system
44 | annotations:
45 | # highlight-start
46 | prometheus.io/port: "9153"
47 | prometheus.io/scrape: "true"
48 | # highlight-end
49 | ```
50 | * [nodelocaldns](https://github.com/kubernetes/kubernetes/blob/release-1.30/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml#L125-L126) 使用 Pod 注解:
51 | ```yaml showLineNumbers
52 | apiVersion: apps/v1
53 | kind: DaemonSet
54 | metadata:
55 | name: node-local-dns
56 | namespace: kube-system
57 | labels:
58 | k8s-app: node-local-dns
59 | kubernetes.io/cluster-service: "true"
60 | addonmanager.kubernetes.io/mode: Reconcile
61 | spec:
62 | updateStrategy:
63 | rollingUpdate:
64 | maxUnavailable: 10%
65 | selector:
66 | matchLabels:
67 | k8s-app: node-local-dns
68 | template:
69 | metadata:
70 | labels:
71 | k8s-app: node-local-dns
72 | annotations:
73 | # highlight-start
74 | prometheus.io/port: "9253"
75 | prometheus.io/scrape: "true"
76 | # highlight-end
77 | ```
78 |
79 | ## Prometheus 采集配置
80 |
81 | ### 根据 Pod 注解动态采集
82 |
83 |
84 |
85 | ### 根据 Service 注解动态采集
86 |
87 |
88 |
89 | ## kube-prometheus-stack 采集配置方法
90 |
91 | 如果你使用 [kube-prometheus-stack](https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack) 来安装 Prometheus,需要在 `additionalScrapeConfigs` 里加上采集配置,示例:
92 |
93 | ```yaml
94 | prometheus:
95 | prometheusSpec:
96 | additionalScrapeConfigs:
97 | - job_name: "kubernetes-service-endpoints"
98 | ...
99 | - job_name: "kubernetes-pods"
100 | ...
101 | ```
102 |
--------------------------------------------------------------------------------
/content/rbac/create-user-using-csr-api.md:
--------------------------------------------------------------------------------
1 | # 使用 CSR API 创建用户
2 |
3 | k8s 支持 CSR API,通过创建 `CertificateSigningRequest` 资源就可以发起 CSR 请求,管理员审批通过之后 `kube-controller-manager` 就会为我们签发证书,确保 `kube-controller-manager` 配了根证书密钥对:
4 |
5 | ``` bash
6 | --cluster-signing-cert-file=/var/lib/kubernetes/ca.pem
7 | --cluster-signing-key-file=/var/lib/kubernetes/ca-key.pem
8 | ```
9 |
10 | ## 安装 cfssl
11 |
12 | 我们用 cfssl 来创建 key 和 csr 文件,所以需要先安装 cfssl:
13 |
14 | ``` bash
15 | curl -L https://pkg.cfssl.org/R1.2/cfssl_linux-amd64 -o cfssl
16 | curl -L https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64 -o cfssljson
17 | curl -L https://pkg.cfssl.org/R1.2/cfssl-certinfo_linux-amd64 -o cfssl-certinfo
18 |
19 | chmod +x cfssl cfssljson cfssl-certinfo
20 | sudo mv cfssl cfssljson cfssl-certinfo /usr/local/bin/
21 | ```
22 |
23 | > 更多 cfssl 详情参考: [使用 cfssl 生成证书](../certs/sign-certs-with-cfssl.md)。
24 |
25 | ## 创建步骤
26 |
27 | 指定要创建的用户名:
28 |
29 | ``` bash
30 | USERNAME="roc"
31 | ```
32 |
33 | 再创建 key 和 csr 文件:
34 |
35 | ``` bash
36 | cat < ${USERNAME}.pem
85 | ```
86 |
87 | 得到证书文件:
88 |
89 | ```
90 | roc.pem
91 | ```
92 |
93 | 至此,我们已经创建好了用户,用户的证书密钥对文件:
94 |
95 | ```
96 | roc.pem
97 | roc-key.pem
98 | ```
99 |
100 | ## 配置 kubeconfig
101 |
102 | ``` bash
103 | # 增加 user
104 | kubectl config set-credentials ${USERNAME} --embed-certs=true --client-certificate=${USERNAME}.pem --client-key=${USERNAME}-key.pem
105 |
106 | # 如果还没配 cluster,可以通过下面命令配一下
107 | kubectl config set-cluster --server= --certificate-authority=
108 |
109 | # 增加 context,绑定 cluster 和 user
110 | kubectl config set-context --cluster= --user=${USERNAME}
111 |
112 | # 使用刚增加的 context
113 | kubectl config use-context
114 | ```
--------------------------------------------------------------------------------
/coscli.log:
--------------------------------------------------------------------------------
1 | [36mINFO[0m[2024-05-28 16:56:45] Do you want to delete 2024/05/28/20240528164435.png? (y/n)
2 | [36mINFO[0m[2024-05-28 16:56:47] Delete cos://image-host-1251893006/2024/05/28/20240528164435.png successfully!
3 |
--------------------------------------------------------------------------------
/giscus.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultCommentOrder": "newest"
3 | }
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "kubernetes-guide",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "docusaurus": "docusaurus",
7 | "start": "docusaurus start",
8 | "build": "docusaurus build",
9 | "swizzle": "docusaurus swizzle",
10 | "deploy": "docusaurus deploy",
11 | "clear": "docusaurus clear",
12 | "serve": "docusaurus serve",
13 | "write-translations": "docusaurus write-translations",
14 | "write-heading-ids": "docusaurus write-heading-ids",
15 | "typecheck": "tsc"
16 | },
17 | "dependencies": {
18 | "@docusaurus/core": "^3.7.0",
19 | "@docusaurus/plugin-ideal-image": "^3.7.0",
20 | "@docusaurus/plugin-pwa": "^3.7.0",
21 | "@docusaurus/preset-classic": "^3.7.0",
22 | "@giscus/react": "^3.1.0",
23 | "@mdx-js/react": "^3.1.0",
24 | "clsx": "^2.1.1",
25 | "docusaurus-plugin-sass": "^0.2.6",
26 | "path-browserify": "^1.0.1",
27 | "plugin-image-zoom": "github:flexanalytics/plugin-image-zoom",
28 | "prism-react-renderer": "^2.4.0",
29 | "raw-loader": "^4.0.2",
30 | "react": "^18.3.1",
31 | "react-dom": "^18.3.1",
32 | "sass": "^1.83.4"
33 | },
34 | "devDependencies": {
35 | "@docusaurus/module-type-aliases": "^3.7.0",
36 | "@docusaurus/tsconfig": "^3.7.0",
37 | "@docusaurus/types": "^3.7.0",
38 | "typescript": "^5.7.3"
39 | },
40 | "browserslist": {
41 | "production": [
42 | ">0.5%",
43 | "not dead",
44 | "not op_mini all"
45 | ],
46 | "development": [
47 | "last 3 chrome version",
48 | "last 3 firefox version",
49 | "last 5 safari version"
50 | ]
51 | },
52 | "engines": {
53 | "node": ">=18.0"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/Comment.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useThemeConfig, useColorMode } from '@docusaurus/theme-common'
3 | import Giscus, { GiscusProps } from '@giscus/react'
4 | import { useLocation } from '@docusaurus/router';
5 |
6 | const defaultConfig: Partial = {
7 | id: 'comments',
8 | mapping: 'specific',
9 | reactionsEnabled: '1',
10 | emitMetadata: '0',
11 | inputPosition: 'top',
12 | loading: 'lazy',
13 | strict: '1',
14 | lang: 'zh-CN',
15 | }
16 |
17 | export default function Comment(): JSX.Element {
18 | const themeConfig = useThemeConfig()
19 |
20 | // merge default config
21 | const giscus = { ...defaultConfig, ...themeConfig.giscus }
22 |
23 | if (!giscus.repo || !giscus.repoId || !giscus.categoryId) {
24 | throw new Error(
25 | 'You must provide `repo`, `repoId`, and `categoryId` to `themeConfig.giscus`.',
26 | )
27 | }
28 |
29 | const path = useLocation().pathname.replace(/^\/|\/$/g, '');
30 | const firstSlashIndex = path.indexOf('/');
31 | var subPath: string = ""
32 | if (firstSlashIndex !== -1) {
33 | subPath = path.substring(firstSlashIndex + 1)
34 | } else {
35 | subPath = "index"
36 | }
37 |
38 | giscus.term = subPath
39 | giscus.theme =
40 | useColorMode().colorMode === 'dark' ? 'transparent_dark' : 'light'
41 |
42 | return (
43 |
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/FileBlock.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CodeBlock from '@theme/CodeBlock';
3 | import { useLocation } from '@docusaurus/router';
4 | import * as path from 'path-browserify';
5 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
6 |
7 | let extToLang = new Map([
8 | ["sh", "bash"],
9 | ["yml", "yaml"]
10 | ]);
11 |
12 | export default function FileBlock({ file, showFileName, ...prop }: { file: string, showFileName?: boolean }) {
13 | // get url path without "/" prefix and suffix
14 | var urlPath = useLocation().pathname.replace(/^\/|\/$/g, '');
15 |
16 | // remove locale prefix in urlPath
17 | const { i18n } = useDocusaurusContext()
18 | if (i18n.currentLocale != i18n.defaultLocale) {
19 | urlPath = urlPath.replace(/^[^\/]*\/?/g, '')
20 | }
21 |
22 | // find file content according to topPath and file path param
23 | var filepath = ""
24 | if (file.startsWith("@site/")) {
25 | filepath = file.replace(/^@site\//g, '')
26 | } else {
27 | filepath = "codeblock/" + file
28 | }
29 |
30 | // load file raw content according to filepath
31 | var content = require('!!raw-loader!@site/' + filepath)?.default
32 | content = content.replace(/\t/g, " "); // replace tab to 2 spaces
33 |
34 | // infer language of code block based on filename extension if language is not set
35 | const filename = path.basename(file);
36 | if (!prop.language) {
37 | var language = path.extname(filename).replace(/^\./, '')
38 | const langMappingName = extToLang.get(language)
39 | if (langMappingName) {
40 | language = langMappingName
41 | }
42 | prop.language = language
43 | }
44 |
45 | // set title to filename if showFileName is set and title is not set
46 | if (!prop.title && showFileName) {
47 | prop.title = filename
48 | }
49 |
50 | return (
51 |
52 | {content}
53 |
54 | );
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/src/css/custom.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Any CSS included here will be global. The classic template
3 | * bundles Infima by default. Infima is a CSS framework designed to
4 | * work well for content-centric websites.
5 | */
6 |
7 | /* You can override the default Infima variables here. */
8 | :root {
9 | --ifm-color-primary: #2e8555;
10 | --ifm-color-primary-dark: #29784c;
11 | --ifm-color-primary-darker: #277148;
12 | --ifm-color-primary-darkest: #205d3b;
13 | --ifm-color-primary-light: #33925d;
14 | --ifm-color-primary-lighter: #359962;
15 | --ifm-color-primary-lightest: #3cad6e;
16 | --ifm-code-font-size: 95%;
17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
18 |
19 | // “喜爱” 图标的颜色
20 | --site-color-svg-icon-favorite: #e9669e;
21 | }
22 |
23 | /* For readability concerns, you should choose a lighter palette in dark mode. */
24 | [data-theme='dark'] {
25 | --ifm-color-primary: #25c2a0;
26 | --ifm-color-primary-dark: #21af90;
27 | --ifm-color-primary-darker: #1fa588;
28 | --ifm-color-primary-darkest: #1a8870;
29 | --ifm-color-primary-light: #29d5b0;
30 | --ifm-color-primary-lighter: #32d8b4;
31 | --ifm-color-primary-lightest: #4fddbf;
32 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
33 | }
34 |
35 | /* 代码高亮行的样式 */
36 | .code-block-highlighted-line {
37 | background-color: rgb(72, 77, 91);
38 | span[class*='codeLineNumber'] {
39 | background-color: rgb(72, 77, 91);
40 | }
41 | }
42 | .code-block-add-line {
43 | background-color: #213227;
44 | span[class*='codeLineNumber'] {
45 | background-color: #213227;
46 | }
47 | }
48 | .code-block-update-line {
49 | background-color: #362d1e;
50 | span[class*='codeLineNumber'] {
51 | background-color: #362d1e;
52 | }
53 | }
54 | .code-block-error-line {
55 | background-color: #ff000020;
56 | span[class*='codeLineNumber'] {
57 | background-color: #ff000020;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/theme/DocCategoryGeneratedIndexPage/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import DocCategoryGeneratedIndexPage from '@theme-original/DocCategoryGeneratedIndexPage';
3 | import type DocCategoryGeneratedIndexPageType from '@theme/DocCategoryGeneratedIndexPage';
4 | import type { WrapperProps } from '@docusaurus/types';
5 | // highlight-add-line
6 | import Comment from '@site/src/components/Comment';
7 |
8 | type Props = WrapperProps;
9 |
10 | export default function DocCategoryGeneratedIndexPageWrapper(props: Props): JSX.Element {
11 | return (
12 | <>
13 |
14 | {/* highlight-add-line */}
15 |
16 | >
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/theme/DocItem/Layout/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Layout from '@theme-original/DocItem/Layout';
3 | import type LayoutType from '@theme/DocItem/Layout';
4 | import type { WrapperProps } from '@docusaurus/types';
5 | // highlight-add-line
6 | import Comment from '@site/src/components/Comment';
7 |
8 | type Props = WrapperProps;
9 |
10 | export default function LayoutWrapper(props: Props): JSX.Element {
11 | return (
12 | <>
13 |
14 | {/* highlight-add-line */}
15 |
16 | >
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/theme/MDXComponents.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // Import the original mapper
3 | import MDXComponents from '@theme-original/MDXComponents';
4 |
5 | import FileBlock from '@site/src/components/FileBlock';
6 | import CodeBlock from '@theme-original/CodeBlock';
7 | import Tabs from '@theme-original/Tabs';
8 | import TabItem from '@theme-original/TabItem';
9 |
10 | export default {
11 | // Re-use the default mapping
12 | ...MDXComponents,
13 | // Add more components to be imported by default
14 | FileBlock,
15 | CodeBlock,
16 | Tabs,
17 | TabItem,
18 | };
19 |
--------------------------------------------------------------------------------
/src/utils/prismDark.ts:
--------------------------------------------------------------------------------
1 | import { themes, type PrismTheme } from 'prism-react-renderer';
2 |
3 | const baseTheme = themes.vsDark;
4 |
5 | export default {
6 | plain: {
7 | color: '#D4D4D4',
8 | backgroundColor: '#212121',
9 | },
10 | styles: [
11 | ...baseTheme.styles,
12 | {
13 | types: ['title'],
14 | style: {
15 | color: '#569CD6',
16 | fontWeight: 'bold',
17 | },
18 | },
19 | {
20 | types: ['property', 'parameter'],
21 | style: {
22 | color: '#9CDCFE',
23 | },
24 | },
25 | {
26 | types: ['script'],
27 | style: {
28 | color: '#D4D4D4',
29 | },
30 | },
31 | {
32 | types: ['boolean', 'arrow', 'atrule', 'tag'],
33 | style: {
34 | color: '#569CD6',
35 | },
36 | },
37 | {
38 | types: ['number', 'color', 'unit'],
39 | style: {
40 | color: '#B5CEA8',
41 | },
42 | },
43 | {
44 | types: ['font-matter'],
45 | style: {
46 | color: '#CE9178',
47 | },
48 | },
49 | {
50 | types: ['keyword', 'rule'],
51 | style: {
52 | color: '#C586C0',
53 | },
54 | },
55 | {
56 | types: ['regex'],
57 | style: {
58 | color: '#D16969',
59 | },
60 | },
61 | {
62 | types: ['maybe-class-name'],
63 | style: {
64 | color: '#4EC9B0',
65 | },
66 | },
67 | {
68 | types: ['constant'],
69 | style: {
70 | color: '#4FC1FF',
71 | },
72 | },
73 | ],
74 | } satisfies PrismTheme;
75 |
--------------------------------------------------------------------------------
/static/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Kubernetes 实践指南",
3 | "short_name": "Kubernetes 实践指南",
4 | "theme_color": "#12affa",
5 | "background_color": "#424242",
6 | "display": "standalone",
7 | "scope": "https://imroc.cc/kubernetes/",
8 | "start_url": "https://imroc.cc/kubernetes/",
9 | "related_applications": [
10 | {
11 | "platform": "webapp",
12 | "url": "https://imroc.cc/kubernetes/manifest.json"
13 | }
14 | ],
15 | "icons": [
16 | {
17 | "src": "img/logo.svg",
18 | "sizes": "any"
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // This file is not used in compilation. It is here just for a nice editor experience.
3 | "extends": "@docusaurus/tsconfig",
4 | "compilerOptions": {
5 | "baseUrl": "."
6 | }
7 | }
8 |
--------------------------------------------------------------------------------