├── .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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925114751.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925114800.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925114808.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F04%2F07%2F20240407153149.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F04%2F08%2F20240408083135.png) 28 | 29 | ## 哪些场景适合使用 KEDA ? 30 | 31 | 下面罗列下适合使用 KEDA 的场景。 32 | 33 | ### 微服务多级调用 34 | 35 | 在微服务中,基本都存在多级调用的业务场景,压力是逐级传递的,下面展示了一个常见的情况: 36 | 37 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F04%2F08%2F20240408084514.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F04%2F09%2F20240409172007.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F04%2F08%2F20240408084514.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925111323.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925111350.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925111402.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925111437.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925111448.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925111502.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925111001.png) 8 | 9 | 这样当滚动更新时,LB 绑定的 NodePort 一般无需变动,也就不需要担心 LB 解绑导致对业务有损。 10 | 11 | ## LB 直通 Pod 场景 12 | 13 | 现在很多云厂商也都支持了 LB 直通 Pod,即 LB 直接将流量转发给 Pod,不需要再经过集群内做一次转发: 14 | 15 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925111009.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925110939.png) 6 | 7 | 销毁 Pod 时需要的优雅终止的时间通常比较长 (preStop + 业务进程停止超过 30s),有的极端情况甚至可能长达数小时,这时候可以根据实际情况自定义 `terminationGracePeriodSeconds`,避免过早的被 `SIGKILL` 杀死,示例: 8 | 9 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925110946.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925110746.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925112252.png) 10 | 11 | 当 server 端发生滚动更新时: 12 | 13 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925112258.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925110349.png) 12 | 13 | ### rr 策略负载不均 14 | 15 | 假如长连接服务的不同连接的保持时长差异很大,而 ipvs 转发时默认是 rr 策略转发,如果某些后端 Pod "运气较差",它们上面的连接保持时间比较较长,而由于是 rr 转发,它们身上累计的连接数就可能较多,节点上通过 `ipvsadm -Ln -t CLUSTER-IP:PORT` 查看某个 service 的转发情况: 16 | 17 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925110404.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925110418.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925111834.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925111846.png) 73 | 74 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925111855.png) 75 | 76 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925111907.png) 77 | 78 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925111918.png) 79 | 80 | 同样,debug2全部分布在numa1上。 81 | 82 | debug3由于没有numa满足>=10C,调度失败。 83 | 84 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925111926.png) 85 | 86 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925111934.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F05%2F28%2F20240528105702.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/gif/devcontainer.gif) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F05%2F22%2F20240522095000.png) 44 | 45 | 选择对应的驱动,AList 支持很多网盘和对象存储,具体配置方法可在 [AList 使用指南](https://alist.nn.ci/zh/guide/) 中找到对应存储驱动的配置步骤。 46 | 47 | ## 与 Aria2 联动 48 | 49 | AList 挂载的网盘中的文件可直接发送给 Aria2 打包下载,下面介绍配置方法。 50 | 51 | 进入主页,右下角点击本地设置: 52 | 53 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F05%2F22%2F20240522094849.png) 54 | 55 | 输入 Aria2 RPC 的地址和密钥: 56 | 57 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F05%2F22%2F20240522095547.png) 58 | 59 | 进入挂载的网盘目录,选中要下载的文件,点击【发送到 Aria2】: 60 | 61 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F05%2F22%2F20240522095724.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F05%2F22%2F20240522093337.png) 44 | 45 | 然后就可以在【正在下载】中去新建下载任务了: 46 | 47 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F05%2F22%2F20240522093648.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F05%2F23%2F20240523112259.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F05%2F26%2F20240526083501.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F05%2F27%2F20240527172426.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F03%2F04%2F20240304154229.png) 28 | 29 | ### 旁路由方案 30 | 31 | 旁路由方案相比主路由方案,对于 Ubuntu 来说,只是没有了 PPPoE 拨号、DNS 缓存、DHCP 服务、防火墙等功能,其余的也都还是用云原生的方式部署在 Ubuntu。只是还需要有一个主路由,可以用单独的路由器机器,也可以启用光猫自带的路由功能(对于有大带宽的需求不推荐,因为通常光猫的路由转发能力较弱): 32 | 33 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F03%2F04%2F20240304154322.png) 34 | 35 | 还可以直接在软路由机器里使用 EXSI 这种虚拟化操作系统,在里面虚拟两个操作系统,一个 RouterOS 或 OpenWRT 作为主路由,另一个 Ubuntu 作为旁路由,形成“双软路由”(本人一开始也是这种方案,后来不用了,因为需要引入太多的系统和配置步骤,重新装机时很麻烦,违背了云原生一键部署的初衷)。 36 | 37 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F03%2F04%2F20240304154359.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F05%2F25%2F20240525112804.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F10%2F27%2F20241027164741.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F10%2F27%2F20241027170127.png) 54 | 55 | 会弹出一个输入框,输入 RSS 地址: 56 | 57 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F10%2F27%2F20241027170318.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F10%2F27%2F20241027170754.png) 66 | 67 | 然后配置下载规则: 68 | 69 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F10%2F27%2F20241027170932.png) 70 | 71 | * 如需过滤掉不需要的内容,可勾选【使用正则表达式】,我这里不需要凡人修仙传的重制版,所以用正则过滤了下。 72 | * 勾选【保存到其它目录】,给剧集单独指定一个目录,该目录可与 Jellyfin 共享挂载(通过 hostPath),qBittorrent 自动下载最新集后,Jellyfin 也会自动搜刮剧集信息并在 Jellyfin 上展示更新的剧集信息。 73 | * 【对以下订阅源应用规则】勾选前面创建的 RSS 订阅名称,表示当该 RSS 订阅规则检查到资源更新时,qBittorrent 将自动下载资源到指定目录。 74 | 75 | 自动下载规则配置好后,不一定生效,还需确保两个全局开关打开,点击设置图标: 76 | 77 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F10%2F27%2F20241027171520.png) 78 | 79 | 确保这两个勾选上: 80 | 81 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F10%2F27%2F20241027171632.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F03%2F12%2F20240312141131.png) 8 | 9 | `添加集群` - `创建`: 10 | 11 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F03%2F12%2F20240312141248.png) 12 | 13 | 按照步骤配置集群: 14 | 15 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F03%2F12%2F20240312141416.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F03%2F12%2F20240312142604.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F03%2F12%2F20240312150934.png) 54 | 55 | 点`添加节点组`来添加 EC2 实例作为计算资源,可以设置数量和自动伸缩的最大最小数量。 56 | 57 | 尽量不要选`使用启动模版`,因为需要自行创建 EC2 的启动模版,你得知道所选系统、机型和规格等配置能否兼容 EKS,配错了会导致节点组创建失败,或者节点组创建成功,但最后 EC2 机器加入集群失败。 58 | 59 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2024%2F03%2F12%2F20240312153042.png) 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 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925135859.png) 28 | 29 | ## 切换集群 30 | 31 | 当想要切换到其它集群操作时,可使用 `kubecm switch` 进行交互式切换: 32 | 33 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925135910.png) 34 | 35 | 36 | 不过还是推荐使用 kubectx 进行切换。 37 | 38 | ## 移除集群 39 | 40 | 如果想要移除某个集群,可以用 `kubecm delete `: 41 | 42 | ![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925135920.png) 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 | INFO[2024-05-28 16:56:45] Do you want to delete 2024/05/28/20240528164435.png? (y/n) 2 | INFO[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 | --------------------------------------------------------------------------------