From cab2f468eb2050979a4494e8c1e69b8bf5f9f65b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 13 Oct 2023 12:25:19 +0000 Subject: [PATCH] deploy: d1f2d0d54f9e36e38dad14f0773f3c813b61b8da --- 404.html | 4 ++-- assets/js/154ae48b.2a8fd028.js | 1 - assets/js/154ae48b.96ce7fc1.js | 1 + .../{runtime~main.7bd67f81.js => runtime~main.80c1bcfa.js} | 2 +- blog/2022-recap/index.html | 4 ++-- blog/archive/index.html | 4 ++-- blog/index.html | 4 ++-- blog/tags/index.html | 4 ++-- blog/tags/recap/index.html | 4 ++-- docs/admin/image_build/index.html | 4 ++-- docs/architecture/iaas/index.html | 4 ++-- docs/architecture/network/index.html | 4 ++-- "docs/category/admin-\345\260\210\345\215\200/index.html" | 4 ++-- docs/category/container/index.html | 4 ++-- docs/category/kubernetes/index.html | 4 ++-- docs/category/network/index.html | 4 ++-- .../index.html" | 4 ++-- .../index.html" | 4 ++-- docs/faq/index.html | 4 ++-- docs/intro/index.html | 4 ++-- docs/self-paced-labs/frr_bgp/index.html | 4 ++-- docs/self-paced-labs/index.html | 4 ++-- docs/self-paced-labs/kaniko/index.html | 4 ++-- docs/self-paced-labs/kops/index.html | 6 +++--- docs/self-paced-labs/kwok-in-docker/index.html | 4 ++-- docs/self-paced-labs/quarkus-with-helm-charts/index.html | 4 ++-- docs/self-paced-labs/tool_compare_lab/index.html | 4 ++-- docs/self-paced-labs/vault/index.html | 4 ++-- docs/tutorial-basics/congratulations/index.html | 4 ++-- docs/tutorial-basics/create-security-group/index.html | 4 ++-- docs/tutorial-basics/create-vm/index.html | 4 ++-- docs/tutorial-basics/upload-ssh-key/index.html | 4 ++-- docs/tutorial-extra/create-private-network/index.html | 4 ++-- docs/tutorial-extra/create-volume/index.html | 4 ++-- docs/tutorial-extra/floating-ip/index.html | 4 ++-- icon-license/index.html | 4 ++-- index.html | 4 ++-- intro/index.html | 4 ++-- usecase/index.html | 4 ++-- 39 files changed, 75 insertions(+), 75 deletions(-) delete mode 100644 assets/js/154ae48b.2a8fd028.js create mode 100644 assets/js/154ae48b.96ce7fc1.js rename assets/js/{runtime~main.7bd67f81.js => runtime~main.80c1bcfa.js} (66%) diff --git a/404.html b/404.html index 9bb5a85..dc09363 100644 --- a/404.html +++ b/404.html @@ -10,13 +10,13 @@ - +
跳至主要内容

找不到頁面

我們沒有您要找的頁面。

請聯絡原始連結來源網站的所有者,並通知他們連結已毀損。

- + \ No newline at end of file diff --git a/assets/js/154ae48b.2a8fd028.js b/assets/js/154ae48b.2a8fd028.js deleted file mode 100644 index 5028ee9..0000000 --- a/assets/js/154ae48b.2a8fd028.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunknew_infra_labs_docs=self.webpackChunknew_infra_labs_docs||[]).push([[9068],{3905:(e,t,a)=>{a.d(t,{Zo:()=>c,kt:()=>m});var n=a(7294);function l(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function r(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,n)}return a}function s(e){for(var t=1;t=0||(l[a]=e[a]);return l}(e,t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(l[a]=e[a])}return l}var p=n.createContext({}),u=function(e){var t=n.useContext(p),a=t;return e&&(a="function"==typeof e?e(t):s(s({},t),e)),a},c=function(e){var t=u(e.components);return n.createElement(p.Provider,{value:t},e.children)},i="mdxType",k={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},d=n.forwardRef((function(e,t){var a=e.components,l=e.mdxType,r=e.originalType,p=e.parentName,c=o(e,["components","mdxType","originalType","parentName"]),i=u(a),d=l,m=i["".concat(p,".").concat(d)]||i[d]||k[d]||r;return a?n.createElement(m,s(s({ref:t},c),{},{components:a})):n.createElement(m,s({ref:t},c))}));function m(e,t){var a=arguments,l=t&&t.mdxType;if("string"==typeof e||l){var r=a.length,s=new Array(r);s[0]=d;var o={};for(var p in t)hasOwnProperty.call(t,p)&&(o[p]=t[p]);o.originalType=e,o[i]="string"==typeof e?e:l,s[1]=o;for(var u=2;u{a.r(t),a.d(t,{assets:()=>p,contentTitle:()=>s,default:()=>i,frontMatter:()=>r,metadata:()=>o,toc:()=>u});var n=a(7462),l=(a(7294),a(3905));const r={description:""},s="OpenStack \u4e0a\u5229\u7528 kops \u642d\u5efa K8S Cluster",o={unversionedId:"self-paced-labs/kops/index",id:"self-paced-labs/kops/index",title:"OpenStack \u4e0a\u5229\u7528 kops \u642d\u5efa K8S Cluster",description:"",source:"@site/docs/self-paced-labs/kops/index.md",sourceDirName:"self-paced-labs/kops",slug:"/self-paced-labs/kops/",permalink:"/docs/self-paced-labs/kops/",draft:!1,editUrl:"https://github.com/cloud-native-taiwan/Infra-Labs-Docs/tree/main/docs/self-paced-labs/kops/index.md",tags:[],version:"current",frontMatter:{description:""},sidebar:"self_paced_labs",previous:{title:"Kubernetes",permalink:"/docs/category/kubernetes"},next:{title:"KWOK in Docker - \u9ad4\u9a57\u4e0a\u5343\u7bc0\u9ede\u7684 K8s \u5de5\u5177",permalink:"/docs/self-paced-labs/kwok-in-docker/"}},p={},u=[{value:"kops \u662f\u4ec0\u9ebc\uff1f",id:"kops-\u662f\u4ec0\u9ebc",level:2},{value:"kops \u540d\u8a5e",id:"kops-\u540d\u8a5e",level:2},{value:"Storage",id:"storage",level:3},{value:"STATE_STORE",id:"state_store",level:4},{value:"API",id:"api",level:3},{value:"Cluster spec",id:"cluster-spec",level:4},{value:"Instace Group",id:"instace-group",level:4},{value:"Cloud",id:"cloud",level:3},{value:"The Cloud",id:"the-cloud",level:4},{value:"Tasks",id:"tasks",level:4},{value:"Model",id:"model",level:4},{value:"\u5b89\u88dd kops",id:"\u5b89\u88dd-kops",level:2},{value:"\u524d\u7f6e\u6e96\u5099",id:"\u524d\u7f6e\u6e96\u5099",level:3},{value:"\u900f\u904e homebrew \u5b89\u88dd",id:"\u900f\u904e-homebrew-\u5b89\u88dd",level:3},{value:"\u900f\u904e\u5b98\u65b9\u7de8\u8b6f\u597d\u7684 release \u5b89\u88dd",id:"\u900f\u904e\u5b98\u65b9\u7de8\u8b6f\u597d\u7684-release-\u5b89\u88dd",level:3},{value:"Linux",id:"linux",level:4},{value:"Mac",id:"mac",level:4},{value:"\u81ea\u884c\u7de8\u8b6f",id:"\u81ea\u884c\u7de8\u8b6f",level:4},{value:"Lab\uff1a\u5efa\u7acb Kubernetes Cluster",id:"lab\u5efa\u7acb-kubernetes-cluster",level:2},{value:"\u4e0b\u8f09 OpenStack Credential",id:"\u4e0b\u8f09-openstack-credential",level:3},{value:"\u8a2d\u5b9a kops STATE_STORE",id:"\u8a2d\u5b9a-kops-state_store",level:3},{value:"\u5efa\u7acb Kubernetes Cluster Template",id:"\u5efa\u7acb-kubernetes-cluster-template",level:3},{value:"\u9a57\u8b49 Kubernetes Cluster",id:"\u9a57\u8b49-kubernetes-cluster",level:3},{value:"\u522a\u9664 Kubernetes Cluster",id:"\u522a\u9664-kubernetes-cluster",level:3},{value:"\u5c0f\u7d50",id:"\u5c0f\u7d50",level:2}],c={toc:u};function i(e){let{components:t,...r}=e;return(0,l.kt)("wrapper",(0,n.Z)({},c,r,{components:t,mdxType:"MDXLayout"}),(0,l.kt)("h1",{id:"openstack-\u4e0a\u5229\u7528-kops-\u642d\u5efa-k8s-cluster"},"OpenStack \u4e0a\u5229\u7528 kops \u642d\u5efa K8S Cluster"),(0,l.kt)("admonition",{title:"\u672c\u7bc7 Lab \u76f8\u95dc\u9644\u4ef6",type:"note"},(0,l.kt)("p",{parentName:"admonition"},"\u672c\u7bc7\u76f8\u95dc\u9644\u4ef6\u5728",(0,l.kt)("a",{parentName:"p",href:"https://github.com/cloud-native-taiwan/Infra-Labs-Docs/tree/main/attachments/kops"},"\u9019\u88e1"))),(0,l.kt)("p",null,"\u672c\u6b21 lab \u5c07\u6703\u5e36\u9818\u5927\u5bb6\u5229\u7528 kops \u5728 OpenStack \u4e0a\u5feb\u901f\u642d\u5efa\u4e00\u500b Kubernetes cluster\u3002"),(0,l.kt)("h2",{id:"kops-\u662f\u4ec0\u9ebc"},"kops \u662f\u4ec0\u9ebc\uff1f"),(0,l.kt)("p",null,"Kops \u662f Kubernetes Operation \u7684\u7e2e\u5beb\uff0c\u9867\u540d\u601d\u7fa9\u5c31\u662f\u4e00\u500b\u62ff\u4f86\u505a K8S \u7dad\u904b\u76f8\u95dc\u64cd\u4f5c\u7684\u4e00\u500b\u5de5\u5177\uff0c\u800c kops \u76ee\u524d\u6709\u5e7e\u9805\u7279\u9ede"),(0,l.kt)("ul",null,(0,l.kt)("li",{parentName:"ul"},"\u652f\u63f4\u5efa\u7acb\u3001\u7dad\u8b77\u3001\u5347\u7d1a\u3001\u6467\u6bc0 K8S \u7fa4\u96c6"),(0,l.kt)("li",{parentName:"ul"},"\u652f\u63f4 AWS (production)\u3001OpenStack/DigitalOcean (Beta)\u3001Azure/GCP (Alpha)"),(0,l.kt)("li",{parentName:"ul"},"\u80fd\u5920\u900f\u904e kops \u751f\u6210\u7fa4\u96c6\u7684 Terraform template"),(0,l.kt)("li",{parentName:"ul"},"\u7ba1\u7406 K8S cluster add-ons"),(0,l.kt)("li",{parentName:"ul"},"\u81ea\u52d5\u90e8\u7f72 K8S \u7fa4\u96c6")),(0,l.kt)("h2",{id:"kops-\u540d\u8a5e"},"kops \u540d\u8a5e"),(0,l.kt)("h3",{id:"storage"},"Storage"),(0,l.kt)("h4",{id:"state_store"},"STATE_STORE"),(0,l.kt)("p",null,"STATE STORE \u5b9a\u7fa9\u4e86 kops \u9700\u8981\u5132\u5b58\u5176\u8cc7\u6599\u7684\u5730\u65b9\uff0ckops \u6703\u5132\u5b58\u7684\u8cc7\u6599\u5305\u542b\u4e86 cluster spec\u3001instance group \u8ddf ssh key \u7b49\u3002"),(0,l.kt)("h3",{id:"api"},"API"),(0,l.kt)("h4",{id:"cluster-spec"},"Cluster spec"),(0,l.kt)("p",null,"Cluster spec \u5b9a\u7fa9\u4e86\u7fa4\u96c6\u672c\u8eab\u6240\u9700\u7684\u4e00\u4e9b specification\uff0c\u4f8b\u5982\u7fa4\u96c6\u4e2d\u7684 DNS\u3001Load Balancer\u3001etcd cluster \u7b49\u8cc7\u8a0a\u3002"),(0,l.kt)("h4",{id:"instace-group"},"Instace Group"),(0,l.kt)("p",null,"Instance Group \u5b9a\u7fa9\u4e86\u5be6\u969b worker node\u3001master node\u3001etcd node \u7684\u4e00\u4e9b\u76f8\u95dc\u8cc7\u6599\uff0c\u4ee5 OpenStack \u70ba\u4f8b\u5c31\u662f\u6703\u5efa\u7acb\u7684\u865b\u64ec\u6a5f\u6240\u4f7f\u7528\u7684 flavor\u3001image \u7b49\u3002"),(0,l.kt)("h3",{id:"cloud"},"Cloud"),(0,l.kt)("h4",{id:"the-cloud"},"The Cloud"),(0,l.kt)("p",null,"The Cloud \u70ba\u4e0d\u540c\u96f2\u7aef\u5e73\u53f0\u5b9a\u7fa9\u4e86\u7d71\u4e00\u7684 golang interface\uff0c\u7531\u65bc kops \u652f\u63f4\u5f88\u591a\u4e0d\u540c\u96f2\u7aef\u74b0\u5883\uff0c\u6240\u4ee5\u5229\u7528\u9019\u500b interface \u505a abstraction"),(0,l.kt)("h4",{id:"tasks"},"Tasks"),(0,l.kt)("p",null,"\u4e00\u500b task \u5c31\u662f\u5c0d\u96f2\u7aef\u74b0\u5883\u64cd\u4f5c\u7684\u4e00\u500b API call"),(0,l.kt)("h4",{id:"model"},"Model"),(0,l.kt)("p",null,"Model \u662f\u5c07\u4e4b\u524d\u5b9a\u7fa9\u7684 cluster spec \u88e1\u9762\u7684\u5167\u5bb9\u5c0d\u61c9\u5230\u5be6\u969b\u7684 task\n\u4f8b\u5982\u8aaa\u6211\u5011\u5728 cluster spec \u4e0a\u5b9a\u7fa9\u4e86\u4e00\u500b load balancer \uff0c\u90a3 loadbalancer model \u5c31\u6703\u5c07\u5176\u5c0d\u61c9\u5230\u5be6\u969b\u4e0a\u5efa\u7acb\u4e00\u500b loadbalancer \u6240\u9700\u8981\u7684\u5404\u7a2e API call"),(0,l.kt)("h2",{id:"\u5b89\u88dd-kops"},"\u5b89\u88dd kops"),(0,l.kt)("h3",{id:"\u524d\u7f6e\u6e96\u5099"},"\u524d\u7f6e\u6e96\u5099"),(0,l.kt)("p",null,"\u5b89\u88dd kops \u524d\u9700\u8981\u5b89\u88dd kubectl\uff0c\u5b89\u88dd\u65b9\u5f0f\u8acb\u898b\u6b64",(0,l.kt)("a",{parentName:"p",href:"https://kubernetes.io/docs/tasks/tools/"},"\u5b98\u65b9\u6587\u4ef6")),(0,l.kt)("h3",{id:"\u900f\u904e-homebrew-\u5b89\u88dd"},"\u900f\u904e homebrew \u5b89\u88dd"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"brew update && brew install kops\n")),(0,l.kt)("h3",{id:"\u900f\u904e\u5b98\u65b9\u7de8\u8b6f\u597d\u7684-release-\u5b89\u88dd"},"\u900f\u904e\u5b98\u65b9\u7de8\u8b6f\u597d\u7684 release \u5b89\u88dd"),(0,l.kt)("h4",{id:"linux"},"Linux"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"curl -Lo kops https://github.com/kubernetes/kops/releases/download/$(curl -s https://api.github.com/repos/kubernetes/kops/releases/latest | grep tag_name | cut -d '\"' -f 4)/kops-linux-amd64\nchmod +x kops\nsudo mv kops /usr/local/bin/kops\n")),(0,l.kt)("h4",{id:"mac"},"Mac"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"curl -Lo kops https://github.com/kubernetes/kops/releases/download/$(curl -s https://api.github.com/repos/kubernetes/kops/releases/latest | grep tag_name | cut -d '\"' -f 4)/kops-darwin-amd64\nchmod +x kops\nsudo mv kops /usr/local/bin/kops\n")),(0,l.kt)("h4",{id:"\u81ea\u884c\u7de8\u8b6f"},"\u81ea\u884c\u7de8\u8b6f"),(0,l.kt)("p",null,"\u81ea\u884c\u7de8\u8b6f\u7684\u8a71\u8acb\u5148\u78ba\u8a8d\u74b0\u5883\u5167\u6709\u5b89\u88dd golang\uff0c\u4e26\u4e14\u8a2d\u5b9a\u597d GOPATH \u8207 GOBIN"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"git clone git@github.com:kubernetes/kops.git\ncd kops\nmake\n")),(0,l.kt)("h2",{id:"lab\u5efa\u7acb-kubernetes-cluster"},"Lab\uff1a\u5efa\u7acb Kubernetes Cluster"),(0,l.kt)("h3",{id:"\u4e0b\u8f09-openstack-credential"},"\u4e0b\u8f09 OpenStack Credential"),(0,l.kt)("blockquote",null,(0,l.kt)("p",{parentName:"blockquote"},(0,l.kt)("strong",{parentName:"p"},"Note")),(0,l.kt)("p",{parentName:"blockquote"},"\u8acb\u806f\u7d61 admin \u958b\u555f load balancer \u4f7f\u7528\u6b0a\u9650")),(0,l.kt)("p",null,"\u9032\u5165 OpenStack \u9762\u677f -> Identity / Application Credentials\uff0c\u5728\u53f3\u4e0a\u65b9\u9078\u64c7\u5efa\u7acb\u65b0\u7684 Application Credential\u3002"),(0,l.kt)("blockquote",null,(0,l.kt)("p",{parentName:"blockquote"},"\u4ee5 CNTUG Infra Lab \u70ba\u4f8b\uff0c\u8a72\u9023\u7d50\u5728 ",(0,l.kt)("a",{parentName:"p",href:"https://openstack.cloudnative.tw/identity/application_credentials/"},"https://openstack.cloudnative.tw/identity/application_credentials/"))),(0,l.kt)("p",null,"\u5efa\u7acb\u5b8c\u6210\u5f8c\uff0c\u9ede\u64ca Download openrc file\uff0c\u5c07\u6703\u4e0b\u8f09\u4e00\u500b Shell Script \u6a94\u6848\u3002"),(0,l.kt)("p",null,"\u4e0b\u8f09\u5b8c\u6210\u5f8c\uff0c\u6703\u9700\u8981\u5c07 RC file \u7684\u8cc7\u8a0a\u653e\u9032 shell environment variable"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"source openrc.sh\nexport OS_DOMAIN_NAME=Default\n")),(0,l.kt)("h3",{id:"\u8a2d\u5b9a-kops-state_store"},"\u8a2d\u5b9a kops STATE_STORE"),(0,l.kt)("p",null,"kops \u6703\u9700\u8981\u8a2d\u5b9a\u5132\u5b58\u8cc7\u6599\u7684\u554f\u984c\u4e5f\u5c31\u662f\u4e0a\u9762\u63d0\u5230\u7684 STATE_STORE"),(0,l.kt)("p",null,"\u7531\u65bc Infra Labs \u6709\u63d0\u4f9b Swift \u670d\u52d9\uff0c\u6211\u5011\u53ef\u4ee5\u76f4\u63a5\u5229\u7528"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"export KOPS_STATE_STORE=swift://kops\n")),(0,l.kt)("h3",{id:"\u5efa\u7acb-kubernetes-cluster-template"},"\u5efa\u7acb Kubernetes Cluster Template"),(0,l.kt)("p",null,"\u63a5\u4e0b\u4f86\u8981\u5229\u7528 kops \u5efa\u7acb cluster \u7684\u8cc7\u6599\uff0c\u5f88\u7c21\u55ae\u53ea\u9700\u8981\u4e00\u884c\u6307\u4ee4\uff1a"),(0,l.kt)("blockquote",null,(0,l.kt)("p",{parentName:"blockquote"}," ",(0,l.kt)("strong",{parentName:"p"},"Note")),(0,l.kt)("p",{parentName:"blockquote"},"\u8acb\u66ff\u63db\u6307\u4ee4\u4e2d \u8ddf ")),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"kops create cluster \\\n --cloud openstack \\\n --name \\\n --state ${KOPS_STATE_STORE} \\\n --zones nova \\\n --network-cidr 10.0.0.0/24 \\\n --image Ubuntu-22.04 \\\n --master-count=3 \\\n --node-count=1 \\\n --node-size m1.small \\\n --master-size m1.small \\\n --etcd-storage-type NVMe \\\n --api-loadbalancer-type public\\\n --topology private \\\n --bastion \\\n --ssh-public-key \\\n --networking calico \\\n --os-ext-net public \\\n --os-dns-servers=8.8.8.8,8.8.4.4 \\\n --os-octavia=true \\\n --os-octavia-provider=ovn\n")),(0,l.kt)("p",null,"\u672c\u6b21 lab \u4e2d\uff0c\u6211\u5011\u6703\u5efa\u7acb\u4e00\u500b\u5171 4 node \u7684 cluster\uff0c\u5305\u542b 3 \u500b master/etcd node \u8ddf 1 \u500b work node\u3002"),(0,l.kt)("p",null,"\u67b6\u69cb\u5716\u5982\u4e0b\uff1a"),(0,l.kt)("p",null,(0,l.kt)("img",{alt:"architecture",src:a(9767).Z,width:"2355",height:"1605"})),(0,l.kt)("p",null,(0,l.kt)("inlineCode",{parentName:"p"},"kops create cluster")," \u6703\u5efa\u7acb cluster spec, instance group \u7b49\u7b49\u8cc7\u6599\uff0c\u63a5\u4e0b\u4f86\u9700\u8981\u7528 ",(0,l.kt)("inlineCode",{parentName:"p"},"kops update cluster")," \u4f86\u5be6\u969b\u5efa\u7acb Kubernetes cluster"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"kops update cluster --name --yes --admin\n")),(0,l.kt)("p",null,"\u9019\u6642\u5019 kops \u5c31\u6703\u958b\u59cb\u547c\u53eb OpenStack API\uff0c\u5efa\u7acb Kubernetes cluster \u6240\u9700\u8981\u7684 VM\uff0c\u7db2\u8def\uff0c\u786c\u789f\uff0cload balancer \u7b49\u8cc7\u6e90\u3002\u7d50\u675f\u5f8c\u8981\u7b49\u5f85\u5927\u7d04 5-10 \u5206\u9418\u8b93\u6574\u500b cluster \u555f\u52d5\u3002"),(0,l.kt)("p",null,"\u6b64\u6642\u9019\u500b\u65b0\u5efa\u7acb\u7684 Kubernetes cluster \u7684 config \u6703\u88ab\u5beb\u5165\u5728 ",(0,l.kt)("inlineCode",{parentName:"p"},"~/.kube/config")," \u4e0b"),(0,l.kt)("blockquote",null,(0,l.kt)("p",{parentName:"blockquote"},(0,l.kt)("strong",{parentName:"p"},"Note")),(0,l.kt)("p",{parentName:"blockquote"},"\u82e5\u662f\u5728 update \u4e2d\u51fa\u73fe 409 error\uff0c\u4e26\u4e14 OpenStack Dashboard \u4e0a load balancer \u5728 PENDING_UPDATE \u72c0\u614b\uff0c\u8acb\u806f\u7d61\u7ba1\u7406\u54e1")),(0,l.kt)("h3",{id:"\u9a57\u8b49-kubernetes-cluster"},"\u9a57\u8b49 Kubernetes Cluster"),(0,l.kt)("p",null,"\u6700\u5f8c\u6211\u5011\u53ef\u4ee5\u9a57\u8b49 Kubernetes Cluster \u5b89\u88dd\u6210\u529f\u3002"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"kops validate cluster --wait 5m\n")),(0,l.kt)("p",null,"\u6703\u5f97\u5230\u4ee5\u4e0b output"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-Using",metastring:"cluster from kubectl context: igene.k8s.local",cluster:!0,from:!0,kubectl:!0,"context:":!0,"igene.k8s.local":!0},"\nValidating cluster igene.k8s.local\n\nINSTANCE GROUPS\nNAME ROLE MACHINETYPE MIN MAX SUBNETS\nbastions Bastion m1.tiny 1 1 nova\nmaster-nova-1 Master m1.small 1 1 nova\nmaster-nova-2 Master m1.small 1 1 nova\nmaster-nova-3 Master m1.small 1 1 nova\nnodes-nova Node m1.small 1 1 nova\n\nNODE STATUS\nNAME ROLE READY\nmaster-nova-1-nvhtbc master True\nmaster-nova-2-jgnhbo master True\nmaster-nova-3-vllcwf master True\nnodes-nova-yxorvl node True\n\nYour cluster igene.k8s.local is ready\n")),(0,l.kt)("p",null,"\u6b64\u6642\u5c31\u53ef\u4ee5\u5229\u7528 ",(0,l.kt)("inlineCode",{parentName:"p"},"kubectl")," \u5c0d cluster \u9032\u884c\u64cd\u4f5c\u4e86\u3002"),(0,l.kt)("p",null,"\u4f7f\u7528 kops \u5b89\u88dd\u7684 cluster \u9810\u8a2d\u5df2\u7d93\u5b89\u88dd ",(0,l.kt)("inlineCode",{parentName:"p"},"cloud-provider-openstack")," \u4e26\u4e14\u6574\u5408\u4e86 Cinder CSI \u548c Octavia Load Balancer \u7b49\u529f\u80fd\u3002"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"kubectl get csidrivers.storage.k8s.io\nNAME ATTACHREQUIRED PODINFOONMOUNT STORAGECAPACITY TOKENREQUESTS REQUIRESREPUBLISH MODES AGE\ncinder.csi.openstack.org true true false false Persistent,Ephemeral 13h\n")),(0,l.kt)("h3",{id:"\u522a\u9664-kubernetes-cluster"},"\u522a\u9664 Kubernetes Cluster"),(0,l.kt)("p",null,"kops \u7d00\u9304\u4e86\u5176\u5efa\u7acb\u5728 OpenStack \u4e0a\u7684\u6240\u6709\u8cc7\u6e90\uff0c\u56e0\u6b64\u8981\u6467\u6bc0 Kubernetes cluster \u4e26\u4e14\u91cb\u51fa\u8cc7\u6e90\u975e\u5e38\u7c21\u55ae\uff0c\u53ea\u9700\u4e00\u884c\u6307\u4ee4\uff1a"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"kops delete cluster --yes\n")),(0,l.kt)("p",null,"kops \u5c07\u6703\u522a\u9664\u6240\u6709\u5728 OpenStack \u4e0a\u5efa\u7acb\u7684\u8cc7\u6e90\u3002"),(0,l.kt)("h2",{id:"\u5c0f\u7d50"},"\u5c0f\u7d50"),(0,l.kt)("p",null,"\u5728\u6b64\u6b21 lab \u4e2d\uff0c\u6211\u5011\u6559\u5b78\u4e86\u5982\u4f55\u5229\u7528 kops \u5728 OpenStack \u4e0a\u5efa\u7acb\u4e00\u500b\u5b8c\u6574\u7684 Kubernetes cluster\u3002kops \u642d\u5efa\u7684 cluster \u540c\u6642\u4e5f\u6574\u5408\u4e86 ",(0,l.kt)("inlineCode",{parentName:"p"},"cloud-provider-openstack")," \u7684\u76f8\u95dc\u529f\u80fd\uff0c\u8b93\u4f7f\u7528\u8005\u53ef\u4ee5\u5728 cluster \u5167\u76f4\u63a5\u4f7f\u7528 ",(0,l.kt)("inlineCode",{parentName:"p"},"persistant volume")," \u548c ",(0,l.kt)("inlineCode",{parentName:"p"},"load balancer")," \u7b49\u529f\u80fd\u3002"),(0,l.kt)("p",null,"\u5f8c\u7e8c\u8207 Kubernetes \u76f8\u95dc\u7684 lab \u4e5f\u6703\u5efa\u8b70\u5229\u7528\u6b64\u65b9\u6cd5\u67b6\u8a2d Kubernetes cluster\u3002"))}i.isMDXComponent=!0},9767:(e,t,a)=>{a.d(t,{Z:()=>n});const n=a.p+"assets/images/architecture-e8dc7921fe63c1980785b669f2f8ed21.png"}}]); \ No newline at end of file diff --git a/assets/js/154ae48b.96ce7fc1.js b/assets/js/154ae48b.96ce7fc1.js new file mode 100644 index 0000000..7603468 --- /dev/null +++ b/assets/js/154ae48b.96ce7fc1.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunknew_infra_labs_docs=self.webpackChunknew_infra_labs_docs||[]).push([[9068],{3905:(e,t,a)=>{a.d(t,{Zo:()=>u,kt:()=>m});var n=a(7294);function l(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function r(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,n)}return a}function s(e){for(var t=1;t=0||(l[a]=e[a]);return l}(e,t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(l[a]=e[a])}return l}var p=n.createContext({}),c=function(e){var t=n.useContext(p),a=t;return e&&(a="function"==typeof e?e(t):s(s({},t),e)),a},u=function(e){var t=c(e.components);return n.createElement(p.Provider,{value:t},e.children)},i="mdxType",k={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},d=n.forwardRef((function(e,t){var a=e.components,l=e.mdxType,r=e.originalType,p=e.parentName,u=o(e,["components","mdxType","originalType","parentName"]),i=c(a),d=l,m=i["".concat(p,".").concat(d)]||i[d]||k[d]||r;return a?n.createElement(m,s(s({ref:t},u),{},{components:a})):n.createElement(m,s({ref:t},u))}));function m(e,t){var a=arguments,l=t&&t.mdxType;if("string"==typeof e||l){var r=a.length,s=new Array(r);s[0]=d;var o={};for(var p in t)hasOwnProperty.call(t,p)&&(o[p]=t[p]);o.originalType=e,o[i]="string"==typeof e?e:l,s[1]=o;for(var c=2;c{a.r(t),a.d(t,{assets:()=>p,contentTitle:()=>s,default:()=>i,frontMatter:()=>r,metadata:()=>o,toc:()=>c});var n=a(7462),l=(a(7294),a(3905));const r={description:""},s="OpenStack \u4e0a\u5229\u7528 kops \u642d\u5efa K8S Cluster",o={unversionedId:"self-paced-labs/kops/index",id:"self-paced-labs/kops/index",title:"OpenStack \u4e0a\u5229\u7528 kops \u642d\u5efa K8S Cluster",description:"",source:"@site/docs/self-paced-labs/kops/index.md",sourceDirName:"self-paced-labs/kops",slug:"/self-paced-labs/kops/",permalink:"/docs/self-paced-labs/kops/",draft:!1,editUrl:"https://github.com/cloud-native-taiwan/Infra-Labs-Docs/tree/main/docs/self-paced-labs/kops/index.md",tags:[],version:"current",frontMatter:{description:""},sidebar:"self_paced_labs",previous:{title:"Kubernetes",permalink:"/docs/category/kubernetes"},next:{title:"KWOK in Docker - \u9ad4\u9a57\u4e0a\u5343\u7bc0\u9ede\u7684 K8s \u5de5\u5177",permalink:"/docs/self-paced-labs/kwok-in-docker/"}},p={},c=[{value:"kops \u662f\u4ec0\u9ebc\uff1f",id:"kops-\u662f\u4ec0\u9ebc",level:2},{value:"kops \u540d\u8a5e",id:"kops-\u540d\u8a5e",level:2},{value:"Storage",id:"storage",level:3},{value:"STATE_STORE",id:"state_store",level:4},{value:"API",id:"api",level:3},{value:"Cluster spec",id:"cluster-spec",level:4},{value:"Instace Group",id:"instace-group",level:4},{value:"Cloud",id:"cloud",level:3},{value:"The Cloud",id:"the-cloud",level:4},{value:"Tasks",id:"tasks",level:4},{value:"Model",id:"model",level:4},{value:"\u5b89\u88dd kops",id:"\u5b89\u88dd-kops",level:2},{value:"\u524d\u7f6e\u6e96\u5099",id:"\u524d\u7f6e\u6e96\u5099",level:3},{value:"\u900f\u904e homebrew \u5b89\u88dd",id:"\u900f\u904e-homebrew-\u5b89\u88dd",level:3},{value:"\u900f\u904e\u5b98\u65b9\u7de8\u8b6f\u597d\u7684 release \u5b89\u88dd",id:"\u900f\u904e\u5b98\u65b9\u7de8\u8b6f\u597d\u7684-release-\u5b89\u88dd",level:3},{value:"Linux",id:"linux",level:4},{value:"Mac",id:"mac",level:4},{value:"\u81ea\u884c\u7de8\u8b6f",id:"\u81ea\u884c\u7de8\u8b6f",level:4},{value:"Lab\uff1a\u5efa\u7acb Kubernetes Cluster",id:"lab\u5efa\u7acb-kubernetes-cluster",level:2},{value:"\u4e0b\u8f09 OpenStack Credential",id:"\u4e0b\u8f09-openstack-credential",level:3},{value:"\u8a2d\u5b9a kops STATE_STORE",id:"\u8a2d\u5b9a-kops-state_store",level:3},{value:"\u5efa\u7acb Kubernetes Cluster Template",id:"\u5efa\u7acb-kubernetes-cluster-template",level:3},{value:"\u9a57\u8b49 Kubernetes Cluster",id:"\u9a57\u8b49-kubernetes-cluster",level:3},{value:"\u522a\u9664 Kubernetes Cluster",id:"\u522a\u9664-kubernetes-cluster",level:3},{value:"\u5c0f\u7d50",id:"\u5c0f\u7d50",level:2},{value:"\u9644\u9304",id:"\u9644\u9304",level:2},{value:"\u52a0\u5165\u4e0d\u540c Flavor \u7684 Instance",id:"\u52a0\u5165\u4e0d\u540c-flavor-\u7684-instance",level:3}],u={toc:c};function i(e){let{components:t,...r}=e;return(0,l.kt)("wrapper",(0,n.Z)({},u,r,{components:t,mdxType:"MDXLayout"}),(0,l.kt)("h1",{id:"openstack-\u4e0a\u5229\u7528-kops-\u642d\u5efa-k8s-cluster"},"OpenStack \u4e0a\u5229\u7528 kops \u642d\u5efa K8S Cluster"),(0,l.kt)("admonition",{title:"\u672c\u7bc7 Lab \u76f8\u95dc\u9644\u4ef6",type:"note"},(0,l.kt)("p",{parentName:"admonition"},"\u672c\u7bc7\u76f8\u95dc\u9644\u4ef6\u5728",(0,l.kt)("a",{parentName:"p",href:"https://github.com/cloud-native-taiwan/Infra-Labs-Docs/tree/main/attachments/kops"},"\u9019\u88e1"))),(0,l.kt)("p",null,"\u672c\u6b21 lab \u5c07\u6703\u5e36\u9818\u5927\u5bb6\u5229\u7528 kops \u5728 OpenStack \u4e0a\u5feb\u901f\u642d\u5efa\u4e00\u500b Kubernetes cluster\u3002"),(0,l.kt)("h2",{id:"kops-\u662f\u4ec0\u9ebc"},"kops \u662f\u4ec0\u9ebc\uff1f"),(0,l.kt)("p",null,"Kops \u662f Kubernetes Operation \u7684\u7e2e\u5beb\uff0c\u9867\u540d\u601d\u7fa9\u5c31\u662f\u4e00\u500b\u62ff\u4f86\u505a K8S \u7dad\u904b\u76f8\u95dc\u64cd\u4f5c\u7684\u4e00\u500b\u5de5\u5177\uff0c\u800c kops \u76ee\u524d\u6709\u5e7e\u9805\u7279\u9ede"),(0,l.kt)("ul",null,(0,l.kt)("li",{parentName:"ul"},"\u652f\u63f4\u5efa\u7acb\u3001\u7dad\u8b77\u3001\u5347\u7d1a\u3001\u6467\u6bc0 K8S \u7fa4\u96c6"),(0,l.kt)("li",{parentName:"ul"},"\u652f\u63f4 AWS (production)\u3001OpenStack/DigitalOcean (Beta)\u3001Azure/GCP (Alpha)"),(0,l.kt)("li",{parentName:"ul"},"\u80fd\u5920\u900f\u904e kops \u751f\u6210\u7fa4\u96c6\u7684 Terraform template"),(0,l.kt)("li",{parentName:"ul"},"\u7ba1\u7406 K8S cluster add-ons"),(0,l.kt)("li",{parentName:"ul"},"\u81ea\u52d5\u90e8\u7f72 K8S \u7fa4\u96c6")),(0,l.kt)("h2",{id:"kops-\u540d\u8a5e"},"kops \u540d\u8a5e"),(0,l.kt)("h3",{id:"storage"},"Storage"),(0,l.kt)("h4",{id:"state_store"},"STATE_STORE"),(0,l.kt)("p",null,"STATE STORE \u5b9a\u7fa9\u4e86 kops \u9700\u8981\u5132\u5b58\u5176\u8cc7\u6599\u7684\u5730\u65b9\uff0ckops \u6703\u5132\u5b58\u7684\u8cc7\u6599\u5305\u542b\u4e86 cluster spec\u3001instance group \u8ddf ssh key \u7b49\u3002"),(0,l.kt)("h3",{id:"api"},"API"),(0,l.kt)("h4",{id:"cluster-spec"},"Cluster spec"),(0,l.kt)("p",null,"Cluster spec \u5b9a\u7fa9\u4e86\u7fa4\u96c6\u672c\u8eab\u6240\u9700\u7684\u4e00\u4e9b specification\uff0c\u4f8b\u5982\u7fa4\u96c6\u4e2d\u7684 DNS\u3001Load Balancer\u3001etcd cluster \u7b49\u8cc7\u8a0a\u3002"),(0,l.kt)("h4",{id:"instace-group"},"Instace Group"),(0,l.kt)("p",null,"Instance Group \u5b9a\u7fa9\u4e86\u5be6\u969b worker node\u3001master node\u3001etcd node \u7684\u4e00\u4e9b\u76f8\u95dc\u8cc7\u6599\uff0c\u4ee5 OpenStack \u70ba\u4f8b\u5c31\u662f\u6703\u5efa\u7acb\u7684\u865b\u64ec\u6a5f\u6240\u4f7f\u7528\u7684 flavor\u3001image \u7b49\u3002"),(0,l.kt)("h3",{id:"cloud"},"Cloud"),(0,l.kt)("h4",{id:"the-cloud"},"The Cloud"),(0,l.kt)("p",null,"The Cloud \u70ba\u4e0d\u540c\u96f2\u7aef\u5e73\u53f0\u5b9a\u7fa9\u4e86\u7d71\u4e00\u7684 golang interface\uff0c\u7531\u65bc kops \u652f\u63f4\u5f88\u591a\u4e0d\u540c\u96f2\u7aef\u74b0\u5883\uff0c\u6240\u4ee5\u5229\u7528\u9019\u500b interface \u505a abstraction"),(0,l.kt)("h4",{id:"tasks"},"Tasks"),(0,l.kt)("p",null,"\u4e00\u500b task \u5c31\u662f\u5c0d\u96f2\u7aef\u74b0\u5883\u64cd\u4f5c\u7684\u4e00\u500b API call"),(0,l.kt)("h4",{id:"model"},"Model"),(0,l.kt)("p",null,"Model \u662f\u5c07\u4e4b\u524d\u5b9a\u7fa9\u7684 cluster spec \u88e1\u9762\u7684\u5167\u5bb9\u5c0d\u61c9\u5230\u5be6\u969b\u7684 task\n\u4f8b\u5982\u8aaa\u6211\u5011\u5728 cluster spec \u4e0a\u5b9a\u7fa9\u4e86\u4e00\u500b load balancer \uff0c\u90a3 loadbalancer model \u5c31\u6703\u5c07\u5176\u5c0d\u61c9\u5230\u5be6\u969b\u4e0a\u5efa\u7acb\u4e00\u500b loadbalancer \u6240\u9700\u8981\u7684\u5404\u7a2e API call"),(0,l.kt)("h2",{id:"\u5b89\u88dd-kops"},"\u5b89\u88dd kops"),(0,l.kt)("h3",{id:"\u524d\u7f6e\u6e96\u5099"},"\u524d\u7f6e\u6e96\u5099"),(0,l.kt)("p",null,"\u5b89\u88dd kops \u524d\u9700\u8981\u5b89\u88dd kubectl\uff0c\u5b89\u88dd\u65b9\u5f0f\u8acb\u898b\u6b64",(0,l.kt)("a",{parentName:"p",href:"https://kubernetes.io/docs/tasks/tools/"},"\u5b98\u65b9\u6587\u4ef6")),(0,l.kt)("h3",{id:"\u900f\u904e-homebrew-\u5b89\u88dd"},"\u900f\u904e homebrew \u5b89\u88dd"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"brew update && brew install kops\n")),(0,l.kt)("h3",{id:"\u900f\u904e\u5b98\u65b9\u7de8\u8b6f\u597d\u7684-release-\u5b89\u88dd"},"\u900f\u904e\u5b98\u65b9\u7de8\u8b6f\u597d\u7684 release \u5b89\u88dd"),(0,l.kt)("h4",{id:"linux"},"Linux"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"curl -Lo kops https://github.com/kubernetes/kops/releases/download/$(curl -s https://api.github.com/repos/kubernetes/kops/releases/latest | grep tag_name | cut -d '\"' -f 4)/kops-linux-amd64\nchmod +x kops\nsudo mv kops /usr/local/bin/kops\n")),(0,l.kt)("h4",{id:"mac"},"Mac"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"curl -Lo kops https://github.com/kubernetes/kops/releases/download/$(curl -s https://api.github.com/repos/kubernetes/kops/releases/latest | grep tag_name | cut -d '\"' -f 4)/kops-darwin-amd64\nchmod +x kops\nsudo mv kops /usr/local/bin/kops\n")),(0,l.kt)("h4",{id:"\u81ea\u884c\u7de8\u8b6f"},"\u81ea\u884c\u7de8\u8b6f"),(0,l.kt)("p",null,"\u81ea\u884c\u7de8\u8b6f\u7684\u8a71\u8acb\u5148\u78ba\u8a8d\u74b0\u5883\u5167\u6709\u5b89\u88dd golang\uff0c\u4e26\u4e14\u8a2d\u5b9a\u597d GOPATH \u8207 GOBIN"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"git clone git@github.com:kubernetes/kops.git\ncd kops\nmake\n")),(0,l.kt)("h2",{id:"lab\u5efa\u7acb-kubernetes-cluster"},"Lab\uff1a\u5efa\u7acb Kubernetes Cluster"),(0,l.kt)("h3",{id:"\u4e0b\u8f09-openstack-credential"},"\u4e0b\u8f09 OpenStack Credential"),(0,l.kt)("blockquote",null,(0,l.kt)("p",{parentName:"blockquote"},(0,l.kt)("strong",{parentName:"p"},"Note")),(0,l.kt)("p",{parentName:"blockquote"},"\u8acb\u806f\u7d61 admin \u958b\u555f load balancer \u4f7f\u7528\u6b0a\u9650")),(0,l.kt)("p",null,"\u9032\u5165 OpenStack \u9762\u677f -> Identity / Application Credentials\uff0c\u5728\u53f3\u4e0a\u65b9\u9078\u64c7\u5efa\u7acb\u65b0\u7684 Application Credential\u3002"),(0,l.kt)("blockquote",null,(0,l.kt)("p",{parentName:"blockquote"},"\u4ee5 CNTUG Infra Lab \u70ba\u4f8b\uff0c\u8a72\u9023\u7d50\u5728 ",(0,l.kt)("a",{parentName:"p",href:"https://openstack.cloudnative.tw/identity/application_credentials/"},"https://openstack.cloudnative.tw/identity/application_credentials/"))),(0,l.kt)("p",null,"\u5efa\u7acb\u5b8c\u6210\u5f8c\uff0c\u9ede\u64ca Download openrc file\uff0c\u5c07\u6703\u4e0b\u8f09\u4e00\u500b Shell Script \u6a94\u6848\u3002"),(0,l.kt)("p",null,"\u4e0b\u8f09\u5b8c\u6210\u5f8c\uff0c\u6703\u9700\u8981\u5c07 RC file \u7684\u8cc7\u8a0a\u653e\u9032 shell environment variable"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"source openrc.sh\nexport OS_DOMAIN_NAME=Default\n")),(0,l.kt)("h3",{id:"\u8a2d\u5b9a-kops-state_store"},"\u8a2d\u5b9a kops STATE_STORE"),(0,l.kt)("p",null,"kops \u6703\u9700\u8981\u8a2d\u5b9a\u5132\u5b58\u8cc7\u6599\u7684\u554f\u984c\u4e5f\u5c31\u662f\u4e0a\u9762\u63d0\u5230\u7684 STATE_STORE"),(0,l.kt)("p",null,"\u7531\u65bc Infra Labs \u6709\u63d0\u4f9b Swift \u670d\u52d9\uff0c\u6211\u5011\u53ef\u4ee5\u76f4\u63a5\u5229\u7528"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"export KOPS_STATE_STORE=swift://kops\n")),(0,l.kt)("h3",{id:"\u5efa\u7acb-kubernetes-cluster-template"},"\u5efa\u7acb Kubernetes Cluster Template"),(0,l.kt)("p",null,"\u63a5\u4e0b\u4f86\u8981\u5229\u7528 kops \u5efa\u7acb cluster \u7684\u8cc7\u6599\uff0c\u5f88\u7c21\u55ae\u53ea\u9700\u8981\u4e00\u884c\u6307\u4ee4\uff1a"),(0,l.kt)("blockquote",null,(0,l.kt)("p",{parentName:"blockquote"}," ",(0,l.kt)("strong",{parentName:"p"},"Note")),(0,l.kt)("p",{parentName:"blockquote"},"\u8acb\u66ff\u63db\u6307\u4ee4\u4e2d \u8ddf ")),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"kops create cluster \\\n --cloud openstack \\\n --name .k8s.local \\\n --state ${KOPS_STATE_STORE} \\\n --zones nova \\\n --network-cidr 10.0.0.0/24 \\\n --image Ubuntu-22.04 \\\n --master-count=3 \\\n --node-count=1 \\\n --node-size m1.small \\\n --master-size m1.small \\\n --etcd-storage-type NVMe \\\n --api-loadbalancer-type public\\\n --topology private \\\n --bastion \\\n --ssh-public-key \\\n --networking calico \\\n --os-ext-net public \\\n --os-dns-servers=8.8.8.8,8.8.4.4 \\\n --os-octavia=true \\\n --os-octavia-provider=ovn\n")),(0,l.kt)("p",null,"\u672c\u6b21 lab \u4e2d\uff0c\u6211\u5011\u6703\u5efa\u7acb\u4e00\u500b\u5171 4 node \u7684 cluster\uff0c\u5305\u542b 3 \u500b master/etcd node \u8ddf 1 \u500b work node\u3002"),(0,l.kt)("p",null,"\u67b6\u69cb\u5716\u5982\u4e0b\uff1a"),(0,l.kt)("p",null,(0,l.kt)("img",{alt:"architecture",src:a(9767).Z,width:"2355",height:"1605"})),(0,l.kt)("p",null,(0,l.kt)("inlineCode",{parentName:"p"},"kops create cluster")," \u6703\u5efa\u7acb cluster spec, instance group \u7b49\u7b49\u8cc7\u6599\uff0c\u63a5\u4e0b\u4f86\u9700\u8981\u7528 ",(0,l.kt)("inlineCode",{parentName:"p"},"kops update cluster")," \u4f86\u5be6\u969b\u5efa\u7acb Kubernetes cluster"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"kops update cluster --name --yes --admin\n")),(0,l.kt)("p",null,"\u9019\u6642\u5019 kops \u5c31\u6703\u958b\u59cb\u547c\u53eb OpenStack API\uff0c\u5efa\u7acb Kubernetes cluster \u6240\u9700\u8981\u7684 VM\uff0c\u7db2\u8def\uff0c\u786c\u789f\uff0cload balancer \u7b49\u8cc7\u6e90\u3002\u7d50\u675f\u5f8c\u8981\u7b49\u5f85\u5927\u7d04 5-10 \u5206\u9418\u8b93\u6574\u500b cluster \u555f\u52d5\u3002"),(0,l.kt)("p",null,"\u6b64\u6642\u9019\u500b\u65b0\u5efa\u7acb\u7684 Kubernetes cluster \u7684 config \u6703\u88ab\u5beb\u5165\u5728 ",(0,l.kt)("inlineCode",{parentName:"p"},"~/.kube/config")," \u4e0b"),(0,l.kt)("blockquote",null,(0,l.kt)("p",{parentName:"blockquote"},(0,l.kt)("strong",{parentName:"p"},"Note")),(0,l.kt)("p",{parentName:"blockquote"},"\u82e5\u662f\u5728 update \u4e2d\u51fa\u73fe 409 error\uff0c\u4e26\u4e14 OpenStack Dashboard \u4e0a load balancer \u5728 PENDING_UPDATE \u72c0\u614b\uff0c\u8acb\u806f\u7d61\u7ba1\u7406\u54e1")),(0,l.kt)("h3",{id:"\u9a57\u8b49-kubernetes-cluster"},"\u9a57\u8b49 Kubernetes Cluster"),(0,l.kt)("p",null,"\u6700\u5f8c\u6211\u5011\u53ef\u4ee5\u9a57\u8b49 Kubernetes Cluster \u5b89\u88dd\u6210\u529f\u3002"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"kops validate cluster --wait 5m\n")),(0,l.kt)("p",null,"\u6703\u5f97\u5230\u4ee5\u4e0b output"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-Using",metastring:"cluster from kubectl context: igene.k8s.local",cluster:!0,from:!0,kubectl:!0,"context:":!0,"igene.k8s.local":!0},"\nValidating cluster igene.k8s.local\n\nINSTANCE GROUPS\nNAME ROLE MACHINETYPE MIN MAX SUBNETS\nbastions Bastion m1.tiny 1 1 nova\nmaster-nova-1 Master m1.small 1 1 nova\nmaster-nova-2 Master m1.small 1 1 nova\nmaster-nova-3 Master m1.small 1 1 nova\nnodes-nova Node m1.small 1 1 nova\n\nNODE STATUS\nNAME ROLE READY\nmaster-nova-1-nvhtbc master True\nmaster-nova-2-jgnhbo master True\nmaster-nova-3-vllcwf master True\nnodes-nova-yxorvl node True\n\nYour cluster igene.k8s.local is ready\n")),(0,l.kt)("p",null,"\u6b64\u6642\u5c31\u53ef\u4ee5\u5229\u7528 ",(0,l.kt)("inlineCode",{parentName:"p"},"kubectl")," \u5c0d cluster \u9032\u884c\u64cd\u4f5c\u4e86\u3002"),(0,l.kt)("p",null,"\u4f7f\u7528 kops \u5b89\u88dd\u7684 cluster \u9810\u8a2d\u5df2\u7d93\u5b89\u88dd ",(0,l.kt)("inlineCode",{parentName:"p"},"cloud-provider-openstack")," \u4e26\u4e14\u6574\u5408\u4e86 Cinder CSI \u548c Octavia Load Balancer \u7b49\u529f\u80fd\u3002"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"kubectl get csidrivers.storage.k8s.io\nNAME ATTACHREQUIRED PODINFOONMOUNT STORAGECAPACITY TOKENREQUESTS REQUIRESREPUBLISH MODES AGE\ncinder.csi.openstack.org true true false false Persistent,Ephemeral 13h\n")),(0,l.kt)("h3",{id:"\u522a\u9664-kubernetes-cluster"},"\u522a\u9664 Kubernetes Cluster"),(0,l.kt)("p",null,"kops \u7d00\u9304\u4e86\u5176\u5efa\u7acb\u5728 OpenStack \u4e0a\u7684\u6240\u6709\u8cc7\u6e90\uff0c\u56e0\u6b64\u8981\u6467\u6bc0 Kubernetes cluster \u4e26\u4e14\u91cb\u51fa\u8cc7\u6e90\u975e\u5e38\u7c21\u55ae\uff0c\u53ea\u9700\u4e00\u884c\u6307\u4ee4\uff1a"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"kops delete cluster --yes\n")),(0,l.kt)("p",null,"kops \u5c07\u6703\u522a\u9664\u6240\u6709\u5728 OpenStack \u4e0a\u5efa\u7acb\u7684\u8cc7\u6e90\u3002"),(0,l.kt)("h2",{id:"\u5c0f\u7d50"},"\u5c0f\u7d50"),(0,l.kt)("p",null,"\u5728\u6b64\u6b21 lab \u4e2d\uff0c\u6211\u5011\u6559\u5b78\u4e86\u5982\u4f55\u5229\u7528 kops \u5728 OpenStack \u4e0a\u5efa\u7acb\u4e00\u500b\u5b8c\u6574\u7684 Kubernetes cluster\u3002kops \u642d\u5efa\u7684 cluster \u540c\u6642\u4e5f\u6574\u5408\u4e86 ",(0,l.kt)("inlineCode",{parentName:"p"},"cloud-provider-openstack")," \u7684\u76f8\u95dc\u529f\u80fd\uff0c\u8b93\u4f7f\u7528\u8005\u53ef\u4ee5\u5728 cluster \u5167\u76f4\u63a5\u4f7f\u7528 ",(0,l.kt)("inlineCode",{parentName:"p"},"persistant volume")," \u548c ",(0,l.kt)("inlineCode",{parentName:"p"},"load balancer")," \u7b49\u529f\u80fd\u3002"),(0,l.kt)("p",null,"\u5f8c\u7e8c\u8207 Kubernetes \u76f8\u95dc\u7684 lab \u4e5f\u6703\u5efa\u8b70\u5229\u7528\u6b64\u65b9\u6cd5\u67b6\u8a2d Kubernetes cluster\u3002"),(0,l.kt)("h2",{id:"\u9644\u9304"},"\u9644\u9304"),(0,l.kt)("h3",{id:"\u52a0\u5165\u4e0d\u540c-flavor-\u7684-instance"},"\u52a0\u5165\u4e0d\u540c Flavor \u7684 Instance"),(0,l.kt)("p",null,"kops \u53ef\u4ee5\u900f\u904e instance groups \u52a0\u5165\u4e0d\u540c flavor \u8ddf\u6578\u91cf\u7684 instance"),(0,l.kt)("p",null,"Instance group \u7684",(0,l.kt)("a",{parentName:"p",href:"https://github.com/cloud-native-taiwan/Infra-Labs-Docs/tree/main/attachments/kops/ig.yaml"},"\u7bc4\u4f8b\u5982\u4e0b"),"\uff1a"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-yaml"},"apiVersion: kops.k8s.io/v1alpha2\nkind: InstanceGroup\nmetadata:\n labels:\n kops.k8s.io/cluster: .k8s.local\n name: nodes-nova-arm\nspec:\n image: Ubuntu-22.04-aarch64\n machineType: a1.large\n maxSize: 1\n minSize: 1\n role: Node\n subnets:\n - nova\n")),(0,l.kt)("p",null,"\u900f\u904e\u4ee5\u4e0b\u6307\u4ee4\u5efa\u7acb instance group \u4e26\u4e14\u66f4\u65b0 cluster \u4ee5\u5efa\u7acb\u65b0\u7684 instance"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-bash"},"cat ig.yaml | kops create -f -\n\nkops update cluster --name .k8s.local --yes --admin\n")),(0,l.kt)("p",null,"\u65b0\u7684 Instance \u5c07\u6703\u88ab\u5efa\u7acb\u7136\u5f8c\u52a0\u5165\u81f3 cluster \u4e2d\u3002"))}i.isMDXComponent=!0},9767:(e,t,a)=>{a.d(t,{Z:()=>n});const n=a.p+"assets/images/architecture-e8dc7921fe63c1980785b669f2f8ed21.png"}}]); \ No newline at end of file diff --git a/assets/js/runtime~main.7bd67f81.js b/assets/js/runtime~main.80c1bcfa.js similarity index 66% rename from assets/js/runtime~main.7bd67f81.js rename to assets/js/runtime~main.80c1bcfa.js index 04b5a7e..da50fc5 100644 --- a/assets/js/runtime~main.7bd67f81.js +++ b/assets/js/runtime~main.80c1bcfa.js @@ -1 +1 @@ -(()=>{"use strict";var e,a,f,t,r,c={},d={};function b(e){var a=d[e];if(void 0!==a)return a.exports;var f=d[e]={id:e,loaded:!1,exports:{}};return c[e].call(f.exports,f,f.exports,b),f.loaded=!0,f.exports}b.m=c,b.c=d,e=[],b.O=(a,f,t,r)=>{if(!f){var c=1/0;for(i=0;i=r)&&Object.keys(b.O).every((e=>b.O[e](f[o])))?f.splice(o--,1):(d=!1,r0&&e[i-1][2]>r;i--)e[i]=e[i-1];e[i]=[f,t,r]},b.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return b.d(a,{a:a}),a},f=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,b.t=function(e,t){if(1&t&&(e=this(e)),8&t)return e;if("object"==typeof e&&e){if(4&t&&e.__esModule)return e;if(16&t&&"function"==typeof e.then)return e}var r=Object.create(null);b.r(r);var c={};a=a||[null,f({}),f([]),f(f)];for(var d=2&t&&e;"object"==typeof d&&!~a.indexOf(d);d=f(d))Object.getOwnPropertyNames(d).forEach((a=>c[a]=()=>e[a]));return c.default=()=>e,b.d(r,c),r},b.d=(e,a)=>{for(var f in a)b.o(a,f)&&!b.o(e,f)&&Object.defineProperty(e,f,{enumerable:!0,get:a[f]})},b.f={},b.e=e=>Promise.all(Object.keys(b.f).reduce(((a,f)=>(b.f[f](e,a),a)),[])),b.u=e=>"assets/js/"+({15:"9d79d9d9",45:"bde37766",53:"935f2afb",316:"7d599cfc",533:"b2b675dd",624:"4afb2786",836:"0480b142",1089:"dbbbf6a5",1477:"b2f554cd",1676:"cc86e853",1713:"a7023ddc",1762:"0aeab5ff",2389:"3f4505ec",2535:"814f3328",2755:"73e77ed9",3003:"aa88ca16",3085:"1f391b9e",3089:"a6aa9e1f",3237:"1df93b7f",3608:"9e4087bc",3757:"6a480695",3780:"477e2275",4013:"01a85c17",5004:"3a7ed67e",5384:"50632808",6103:"ccc49370",6307:"ae2e5a65",6504:"822bd8ab",6601:"55740cef",6631:"2c397297",7130:"73ba6f8f",7296:"8ce03581",7452:"fda3117f",7555:"d41873c0",7918:"17896441",8322:"80a5c357",8328:"7a474ba4",8566:"7656137d",8569:"54b5121e",8610:"6875c492",8762:"0e68862a",9068:"154ae48b",9101:"45c90035",9111:"d9314a8f",9220:"116e62cf",9283:"0334c168",9514:"1be78505",9671:"0e384e19",9784:"b8678a3f",9817:"14eb3368",9905:"a372f565"}[e]||e)+"."+{15:"0752c17f",45:"bf1931d7",53:"93c1e55d",210:"116848b5",316:"7d057b82",533:"fb5f20ba",624:"ab6a6482",836:"1d4932a7",1089:"115141f8",1477:"0979074c",1676:"55f00a3a",1713:"134f550d",1762:"4eb11cf1",2389:"ea107734",2529:"2c8d163a",2535:"88e6b13f",2755:"22aa3b31",3003:"177adbbb",3085:"8611a0e5",3089:"60ac1fdd",3237:"c64bb853",3608:"0a080816",3757:"5ee6b31b",3780:"443c742d",4013:"6b8d33ad",4972:"0d7f8cac",5004:"07d33042",5384:"e60daf90",6103:"5af14526",6307:"c28564e8",6504:"bd113526",6601:"2e4300c1",6631:"9ac0fb14",7130:"18de283e",7296:"931dc61c",7452:"38d0a1bf",7555:"c66e808a",7918:"1f0c4aee",8322:"80917bbc",8328:"d0d21dbd",8566:"e5a0e439",8569:"81a47040",8610:"f97fd454",8762:"041a9826",9068:"2a8fd028",9101:"975a6e50",9111:"d809ae0b",9220:"c1f2dee4",9283:"c00e90f9",9514:"faf56411",9671:"f5cfce13",9784:"851afc48",9817:"366181f3",9905:"584ee838"}[e]+".js",b.miniCssF=e=>{},b.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),b.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),t={},r="new-infra-labs-docs:",b.l=(e,a,f,c)=>{if(t[e])t[e].push(a);else{var d,o;if(void 0!==f)for(var n=document.getElementsByTagName("script"),i=0;i{d.onerror=d.onload=null,clearTimeout(s);var r=t[e];if(delete t[e],d.parentNode&&d.parentNode.removeChild(d),r&&r.forEach((e=>e(f))),a)return a(f)},s=setTimeout(u.bind(null,void 0,{type:"timeout",target:d}),12e4);d.onerror=u.bind(null,d.onerror),d.onload=u.bind(null,d.onload),o&&document.head.appendChild(d)}},b.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},b.p="/",b.gca=function(e){return e={17896441:"7918",50632808:"5384","9d79d9d9":"15",bde37766:"45","935f2afb":"53","7d599cfc":"316",b2b675dd:"533","4afb2786":"624","0480b142":"836",dbbbf6a5:"1089",b2f554cd:"1477",cc86e853:"1676",a7023ddc:"1713","0aeab5ff":"1762","3f4505ec":"2389","814f3328":"2535","73e77ed9":"2755",aa88ca16:"3003","1f391b9e":"3085",a6aa9e1f:"3089","1df93b7f":"3237","9e4087bc":"3608","6a480695":"3757","477e2275":"3780","01a85c17":"4013","3a7ed67e":"5004",ccc49370:"6103",ae2e5a65:"6307","822bd8ab":"6504","55740cef":"6601","2c397297":"6631","73ba6f8f":"7130","8ce03581":"7296",fda3117f:"7452",d41873c0:"7555","80a5c357":"8322","7a474ba4":"8328","7656137d":"8566","54b5121e":"8569","6875c492":"8610","0e68862a":"8762","154ae48b":"9068","45c90035":"9101",d9314a8f:"9111","116e62cf":"9220","0334c168":"9283","1be78505":"9514","0e384e19":"9671",b8678a3f:"9784","14eb3368":"9817",a372f565:"9905"}[e]||e,b.p+b.u(e)},(()=>{var e={1303:0,532:0};b.f.j=(a,f)=>{var t=b.o(e,a)?e[a]:void 0;if(0!==t)if(t)f.push(t[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var r=new Promise(((f,r)=>t=e[a]=[f,r]));f.push(t[2]=r);var c=b.p+b.u(a),d=new Error;b.l(c,(f=>{if(b.o(e,a)&&(0!==(t=e[a])&&(e[a]=void 0),t)){var r=f&&("load"===f.type?"missing":f.type),c=f&&f.target&&f.target.src;d.message="Loading chunk "+a+" failed.\n("+r+": "+c+")",d.name="ChunkLoadError",d.type=r,d.request=c,t[1](d)}}),"chunk-"+a,a)}},b.O.j=a=>0===e[a];var a=(a,f)=>{var t,r,c=f[0],d=f[1],o=f[2],n=0;if(c.some((a=>0!==e[a]))){for(t in d)b.o(d,t)&&(b.m[t]=d[t]);if(o)var i=o(b)}for(a&&a(f);n{"use strict";var e,a,f,t,r,d={},c={};function b(e){var a=c[e];if(void 0!==a)return a.exports;var f=c[e]={id:e,loaded:!1,exports:{}};return d[e].call(f.exports,f,f.exports,b),f.loaded=!0,f.exports}b.m=d,b.c=c,e=[],b.O=(a,f,t,r)=>{if(!f){var d=1/0;for(i=0;i=r)&&Object.keys(b.O).every((e=>b.O[e](f[o])))?f.splice(o--,1):(c=!1,r0&&e[i-1][2]>r;i--)e[i]=e[i-1];e[i]=[f,t,r]},b.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return b.d(a,{a:a}),a},f=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,b.t=function(e,t){if(1&t&&(e=this(e)),8&t)return e;if("object"==typeof e&&e){if(4&t&&e.__esModule)return e;if(16&t&&"function"==typeof e.then)return e}var r=Object.create(null);b.r(r);var d={};a=a||[null,f({}),f([]),f(f)];for(var c=2&t&&e;"object"==typeof c&&!~a.indexOf(c);c=f(c))Object.getOwnPropertyNames(c).forEach((a=>d[a]=()=>e[a]));return d.default=()=>e,b.d(r,d),r},b.d=(e,a)=>{for(var f in a)b.o(a,f)&&!b.o(e,f)&&Object.defineProperty(e,f,{enumerable:!0,get:a[f]})},b.f={},b.e=e=>Promise.all(Object.keys(b.f).reduce(((a,f)=>(b.f[f](e,a),a)),[])),b.u=e=>"assets/js/"+({15:"9d79d9d9",45:"bde37766",53:"935f2afb",316:"7d599cfc",533:"b2b675dd",624:"4afb2786",836:"0480b142",1089:"dbbbf6a5",1477:"b2f554cd",1676:"cc86e853",1713:"a7023ddc",1762:"0aeab5ff",2389:"3f4505ec",2535:"814f3328",2755:"73e77ed9",3003:"aa88ca16",3085:"1f391b9e",3089:"a6aa9e1f",3237:"1df93b7f",3608:"9e4087bc",3757:"6a480695",3780:"477e2275",4013:"01a85c17",5004:"3a7ed67e",5384:"50632808",6103:"ccc49370",6307:"ae2e5a65",6504:"822bd8ab",6601:"55740cef",6631:"2c397297",7130:"73ba6f8f",7296:"8ce03581",7452:"fda3117f",7555:"d41873c0",7918:"17896441",8322:"80a5c357",8328:"7a474ba4",8566:"7656137d",8569:"54b5121e",8610:"6875c492",8762:"0e68862a",9068:"154ae48b",9101:"45c90035",9111:"d9314a8f",9220:"116e62cf",9283:"0334c168",9514:"1be78505",9671:"0e384e19",9784:"b8678a3f",9817:"14eb3368",9905:"a372f565"}[e]||e)+"."+{15:"0752c17f",45:"bf1931d7",53:"93c1e55d",210:"116848b5",316:"7d057b82",533:"fb5f20ba",624:"ab6a6482",836:"1d4932a7",1089:"115141f8",1477:"0979074c",1676:"55f00a3a",1713:"134f550d",1762:"4eb11cf1",2389:"ea107734",2529:"2c8d163a",2535:"88e6b13f",2755:"22aa3b31",3003:"177adbbb",3085:"8611a0e5",3089:"60ac1fdd",3237:"c64bb853",3608:"0a080816",3757:"5ee6b31b",3780:"443c742d",4013:"6b8d33ad",4972:"0d7f8cac",5004:"07d33042",5384:"e60daf90",6103:"5af14526",6307:"c28564e8",6504:"bd113526",6601:"2e4300c1",6631:"9ac0fb14",7130:"18de283e",7296:"931dc61c",7452:"38d0a1bf",7555:"c66e808a",7918:"1f0c4aee",8322:"80917bbc",8328:"d0d21dbd",8566:"e5a0e439",8569:"81a47040",8610:"f97fd454",8762:"041a9826",9068:"96ce7fc1",9101:"975a6e50",9111:"d809ae0b",9220:"c1f2dee4",9283:"c00e90f9",9514:"faf56411",9671:"f5cfce13",9784:"851afc48",9817:"366181f3",9905:"584ee838"}[e]+".js",b.miniCssF=e=>{},b.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),b.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),t={},r="new-infra-labs-docs:",b.l=(e,a,f,d)=>{if(t[e])t[e].push(a);else{var c,o;if(void 0!==f)for(var n=document.getElementsByTagName("script"),i=0;i{c.onerror=c.onload=null,clearTimeout(s);var r=t[e];if(delete t[e],c.parentNode&&c.parentNode.removeChild(c),r&&r.forEach((e=>e(f))),a)return a(f)},s=setTimeout(u.bind(null,void 0,{type:"timeout",target:c}),12e4);c.onerror=u.bind(null,c.onerror),c.onload=u.bind(null,c.onload),o&&document.head.appendChild(c)}},b.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},b.p="/",b.gca=function(e){return e={17896441:"7918",50632808:"5384","9d79d9d9":"15",bde37766:"45","935f2afb":"53","7d599cfc":"316",b2b675dd:"533","4afb2786":"624","0480b142":"836",dbbbf6a5:"1089",b2f554cd:"1477",cc86e853:"1676",a7023ddc:"1713","0aeab5ff":"1762","3f4505ec":"2389","814f3328":"2535","73e77ed9":"2755",aa88ca16:"3003","1f391b9e":"3085",a6aa9e1f:"3089","1df93b7f":"3237","9e4087bc":"3608","6a480695":"3757","477e2275":"3780","01a85c17":"4013","3a7ed67e":"5004",ccc49370:"6103",ae2e5a65:"6307","822bd8ab":"6504","55740cef":"6601","2c397297":"6631","73ba6f8f":"7130","8ce03581":"7296",fda3117f:"7452",d41873c0:"7555","80a5c357":"8322","7a474ba4":"8328","7656137d":"8566","54b5121e":"8569","6875c492":"8610","0e68862a":"8762","154ae48b":"9068","45c90035":"9101",d9314a8f:"9111","116e62cf":"9220","0334c168":"9283","1be78505":"9514","0e384e19":"9671",b8678a3f:"9784","14eb3368":"9817",a372f565:"9905"}[e]||e,b.p+b.u(e)},(()=>{var e={1303:0,532:0};b.f.j=(a,f)=>{var t=b.o(e,a)?e[a]:void 0;if(0!==t)if(t)f.push(t[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var r=new Promise(((f,r)=>t=e[a]=[f,r]));f.push(t[2]=r);var d=b.p+b.u(a),c=new Error;b.l(d,(f=>{if(b.o(e,a)&&(0!==(t=e[a])&&(e[a]=void 0),t)){var r=f&&("load"===f.type?"missing":f.type),d=f&&f.target&&f.target.src;c.message="Loading chunk "+a+" failed.\n("+r+": "+d+")",c.name="ChunkLoadError",c.type=r,c.request=d,t[1](c)}}),"chunk-"+a,a)}},b.O.j=a=>0===e[a];var a=(a,f)=>{var t,r,d=f[0],c=f[1],o=f[2],n=0;if(d.some((a=>0!==e[a]))){for(t in c)b.o(c,t)&&(b.m[t]=c[t]);if(o)var i=o(b)}for(a&&a(f);n - +

CNTUG Infra Labs 2022 Recap

· 閱讀時間約 13 分鐘
Gene Kuo

CNTUG Infra Labs 自從 2022/1/1 開放申請至今已經快一年了,其中經過了幾次軟硬體升級、架構更改以及 outage。這篇文章大概來記錄一下 Infra Labs 這一年的整個建立以及營運過程。

Rack View

專案動機

自己其實在很久以前一直就有在購買伺服器及網路設備建立一個小小的 OpenStack/Ceph 測試集群,但集群使用率不管是運算資源、儲存空間還是網路資源其實一直都不高。幾年前獲得 public IPv4 地址的時候就有想法要免費提供給社群和學生使用,希望能夠促進社群的成長。但由於資金問題遲遲無法實施這項計畫。

來日本工作後,手邊能夠運用的資金變得稍微充足一點,再加上日本不管是機櫃空間還是網路服務相較台灣都便宜的許多,於是這個計畫就死灰復燃了。

專案資金來源

企業贊助

一開始資金來源是由一些社群成員的個人贊助,剩下不足的從自己的荷包中出。後續在社群成員的努力之下有拉到了 Red Hat 以及聚碩一年的贊助,大大減輕了資金的負擔。

個人贊助

在這裡也先特別感謝社群成員 Hazel, Samina 等人的贊助,以及 OCF 協助管理贊助款項。

由於租借機櫃以及網路的每月費用仍是一筆不小的數字,如果各位讀者有餘力的話也歡迎贊助 CNTUG 社群,有部分款項將會用來維持 Infra Labs 的營運。

設備清單列表

設備的部分主要有幾個來源

  • 以前 homelab 使用過的設備
  • 日本 Yahoo 拍賣購買
  • 中國淘寶/閒魚購買
  • 台灣二手社團購買
  • eBay 購買

其中前兩項佔了大多數。

設備的購買基本上全部都是二手的,甚至在日本 Yahoo 拍賣上買到日本俗稱的 Junk 品(為測試不知道好壞)CPU,大約 30 顆一個一個拿來測試挑出能使用的來。這也是為什麼 Infra Labs 可以使用相對新的 CPU (Xeon Scalabe 2nd Generation) 的原因。

若想詳細了解目前使用的設備可以參考 Architecture

專案建置流程

硬體資源建置流程

在與上游廠商談好了網路以及機櫃的租用合約後就來了實際建構環境的過程了。整個 Infra Labs 的建置基本上是從一個空空的機櫃開始、從上架、接線、設備設定、OS 安裝、IaaS 部署一層一層的蓋上去。

Rack After Cabling

在這邊也額外感謝 Shouko 以及 Samina 抽空來幫忙上架以及接線和 steveyiyop6i 協助網路設定和排除等。

軟體資源建置流程

軟體的建置過程在 COSCUP x KCD Taiwan 2022 議程 如何在幾小時內快速部署一個私有雲 — 以 CNTUG Infra Labs 為例 有介紹,如果想瞭解的話可以觀看錄影影片。目前也有部分設定公開在 Infra Labs 的 GitHub 上,後續將會陸續公開其他設定。

營運過程

Outage

營運過程其實發生不少 outage,主要都是在網路或是 OpenStack 上,Ceph 目前還沒有遇到太多問題。總體來講,系統可用率應該是有在 95% 以上。

Outage 的部分不乏幾個蠻有趣的:

  • 在機櫃的時候不小心扯到上游網路設備的電源線,好死不死那台設備只有接單電源加上忘記 commit 設定,造成了幾個小時的斷線。
  • Core router 更新 firmware 後某個本來壞掉的功能修好了,結果那個功能造成網路不定時掉包。

由於是免費提供的服務並沒有提供 SLA,但是還是盡力能讓系統可用率越高越好。明年若是時間允許的話,每次 outage 也都希望能夠提供比較詳細的 outage report 給使用者。

提供的服務

目前提供的皆為普遍 Public Cloud 常見的服務

  • 虛擬機器 (Virtual Machine)
  • 區塊儲存 (Block Storage)
  • 物件儲存 (Object Storage)
  • 網路
  • 附載平衡 (Load Balancer)
  • DNS

最近新增了提供給虛擬機器 GPU 使用,如果有需求歡迎申請使用。

專案成果

今年 (2022) 本專案提供對象,主要包含技術社群與電資學生使用,其中亦對 Infra Labs 進行測試和系統架設,以下為 2022 年使用案例的成果發表,並達成本專案回饋社會與培育英才的目標。

目前專案利用狀況

目前專案中總共運行了以下資源:

  • VM: 64 台
  • vCPU: 352 個
  • RAM: 433 GiB
  • Public IPv4: 66 個
  • NVMe Storage: 約 2.4 TiB
  • SSD Storage: 約 1.5 TiB

Resource Usage

目前困難

資金

今年有 Red Hat 以及聚碩兩間公司贊助了不小的一筆錢才能安然度過,但目前已經確定明年無法再繼續贊助。雖然以目前的薪資是足以撐起每月的費用,但是也是月月成為月光族的狀態。雖然整個集群是免費提供,但是使用者有餘力的話還是可以透過 OCF 進行贊助以維持計畫持續的營運。

設備

目前記憶體的使用率已經達到了一定的程度,由於整個系統是只有 3 台主機組成的 Hyper Converged Infrastructure,在目前的記憶體使用率下,在一台主機維護的時候將不夠資源 live migrate 那台主機的所有 VM instance 至其他兩台。後續在資金允許的狀況下應該會先將主機記憶體進行升級。

網路的部分是目前最大的 SPOF (Single point of failure)。目前在網路架構中使用了 100G 交換機作為主要的交換器,由於在疫情期間那台交換器已經漲價了超過 6 倍,沒辦法購入第二台做 MLAG 等機制備援。另外主機上的 100G NIC 也是單 port,所以目前只要相關網路設備有需要進行升級或維護整個系統將會下線。

人力

目前整個系統的維護是由我一人進行完成,基本上所有心力放在維護和運營上就已經很忙了,要做架構的調整或是其他的改進時間上就顯得不太足夠。

由於 OpenStack 跟 Ceph 上手難度較高,自己也還沒有一個比較系統性的流程可以教學沒有相關經驗的人上手,因此要尋找新的人手難度非常高。

未來規劃

未來規劃其實很多項目已經寫很久了,但是由於時間問題一直無法實行。

改進監控

監控系統其實是目前最沒有規劃的一個部分,雖然 OpenStack 部署工具在部署時有一併部署一些相關的工具,但其實沒有到很符合需求。目前已經有一些改進項目測試到一半,例如 smoke-test 等,未來希望能夠提供使用者一個查看目前系統狀況的網頁。

新增 ARM 伺服器

幾個月前有收到了幾台捐助給社群的 ARM64 架構的伺服器,但由於那個系統實在太挑記憶體了,一直無法找到足夠的記憶體讓 3 台主機上線。未來會持續收集記憶體,希望能提供使用者一些不同的硬體架構進行測試和學習。

新增 RISC-V 伺服器

這一項規劃其實比較偏向許願性質,RISC-V 本身在虛擬化的規格上有一部分剛制定完畢,自己還蠻有興趣測試是否能在 RISC-V 上利用 OpenStack 系統跑 Virtual Machine。當初 ARM64 剛興起進行 OpenStack porting 也是個蠻有趣的經歷。但目前由於支援虛擬化的 RISC-V 測試板不易取得,所以這項計畫還在擱置當中。

改善網路架構

上面有提到目前網路架構其實是整個 Infra Labs 最脆弱的部分,但由於牽扯到設備、資金等問題,改善難度比較大。

更多貢獻

Infra Labs 維運過程中其實有時都會遭遇一些 bug,筆者已經貢獻了幾個 PR 至 kops 以及 OpenStack 的 bug report,期望在未來能夠利用 Infra Labs 對開源社群進行更多的貢獻。

總結

Infra Labs 算是我生涯中進行過最複雜的一個計畫了,其中其實學到了蠻多東西也燒了蠻多的錢。在這一年內其實整個系統的利用率比我一開始預期的好,也希望明年 Infra Labs 可以為台灣甚至全世界的開源社群帶來更多的貢獻。

最後如果對 Infra Labs 不管是想贊助,想深入了解或是有興趣加入的都歡迎寄信到 infra@cloudnative.tw 或是透過各種管道找我聊天。

- + \ No newline at end of file diff --git a/blog/archive/index.html b/blog/archive/index.html index 2f5bcaa..5c588ef 100644 --- a/blog/archive/index.html +++ b/blog/archive/index.html @@ -10,13 +10,13 @@ - + - + \ No newline at end of file diff --git a/blog/index.html b/blog/index.html index 4f8fe28..0050839 100644 --- a/blog/index.html +++ b/blog/index.html @@ -10,13 +10,13 @@ - +

· 閱讀時間約 13 分鐘
Gene Kuo

CNTUG Infra Labs 自從 2022/1/1 開放申請至今已經快一年了,其中經過了幾次軟硬體升級、架構更改以及 outage。這篇文章大概來記錄一下 Infra Labs 這一年的整個建立以及營運過程。

- + \ No newline at end of file diff --git a/blog/tags/index.html b/blog/tags/index.html index 84dc687..e9bf998 100644 --- a/blog/tags/index.html +++ b/blog/tags/index.html @@ -10,13 +10,13 @@ - + - + \ No newline at end of file diff --git a/blog/tags/recap/index.html b/blog/tags/recap/index.html index 090507c..a67ca98 100644 --- a/blog/tags/recap/index.html +++ b/blog/tags/recap/index.html @@ -10,13 +10,13 @@ - +

1 篇文章 含有標籤「recap」

檢視所有標籤

· 閱讀時間約 13 分鐘
Gene Kuo

CNTUG Infra Labs 自從 2022/1/1 開放申請至今已經快一年了,其中經過了幾次軟硬體升級、架構更改以及 outage。這篇文章大概來記錄一下 Infra Labs 這一年的整個建立以及營運過程。

- + \ No newline at end of file diff --git a/docs/admin/image_build/index.html b/docs/admin/image_build/index.html index bf13d58..74392d9 100644 --- a/docs/admin/image_build/index.html +++ b/docs/admin/image_build/index.html @@ -10,13 +10,13 @@ - +

Image Build and Tagging

前言

本篇將會介紹 Infra Labs OpenStack Container Image building 跟 tagging 的方式。

Infra Labs 使用 OpenStack Kolla 進行 Image Building,並且由於需要同時支援 ARM64 和 AMD64 架構的部署,必須進行 Container Registry Manifest 的調整。

Building

Infra Labs 目前使用的 Kolla Image 並未進行任何修改,可以按照官方文件方式進行 Kolla 的安裝。

目前使用 Ubuntu 作為 base image 進行編譯。

編譯的指令如下:

kolla-build -b ubuntu --registry 192.168.0.1:80 --push --tag 2023.1-amd64
kolla-build -b ubuntu --registry 192.168.0.1:80 --push --tag 2023.1-arm64

根據不同的架構上不同的 Image Tag

Manifest Tagging

為了讓 ARM64/AMD64 對應架構能夠透過統一的 tag pull image,我們需要針對 Manifest 做一些調整。

將附錄的 Image 列表存成 kolla_list 檔案

接下來登入使用的 Harbor Registry,已經登入過後可以跳過此步驟:

docker login 192.168.0.1:80

將舊的 manifest 刪除

for i in $(cat kolla_list); do sudo docker manifest rm 192.168.0.1:80/kolla/$i:2023.1; done

建立新的 manifest 然後 push 至 Harbor

for i in $(cat kolla_list); do sudo docker manifest create --insecure 192.168.0.10:5000/kolla/$i:2023.1 --amend 192.168.0.10:5000/kolla/$i:2023.1-arm64 --amend 192.168.0.10:5000/kolla/$i:2023.1-amd64; done
for i in $(cat kolla_list); do sudo docker manifest push --insecure 192.168.0.10:5000/kolla/$i:2023.1; done`

成功後,docker pull 將會根據 host 的 CPU 架構 pull 對應架構的 image。

附錄

kolla_list 內容如下:

aodh-api
aodh-base
aodh-evaluator
aodh-expirer
aodh-listener
aodh-notifier
barbican-api
barbican-base
barbican-keystone-listener
barbican-worker
base
bifrost-base
bifrost-deploy
blazar-api
blazar-base
blazar-manager
ceilometer-base
ceilometer-central
ceilometer-compute
ceilometer-ipmi
ceilometer-notification
cinder-api
cinder-backup
cinder-base
cinder-scheduler
cinder-volume
cloudkitty-api
cloudkitty-base
cloudkitty-processor
collectd
cron
cyborg-agent
cyborg-api
cyborg-base
cyborg-conductor
designate-api
designate-backend-bind9
designate-base
designate-central
designate-mdns
designate-producer
designate-sink
designate-worker
dnsmasq
etcd
fluentd
freezer-api
freezer-base
freezer-scheduler
glance-api
glance-base
gnocchi-api
gnocchi-base
gnocchi-metricd
gnocchi-statsd
grafana
hacluster-base
hacluster-corosync
hacluster-pacemaker
hacluster-pacemaker-remote
hacluster-pcs
haproxy
haproxy-ssh
heat-api
heat-api-cfn
heat-base
heat-engine
horizon
influxdb
ironic-api
ironic-base
ironic-conductor
ironic-inspector
ironic-neutron-agent
ironic-prometheus-exporter
ironic-pxe
iscsid
keepalived
keystone
keystone-base
keystone-fernet
keystone-ssh
kolla-toolbox
kuryr-base
kuryr-libnetwork
letsencrypt
magnum-api
magnum-base
magnum-conductor
manila-api
manila-base
manila-data
manila-scheduler
manila-share
mariadb-base
mariadb-clustercheck
mariadb-server
masakari-api
masakari-base
masakari-engine
masakari-monitors
memcached
mistral-api
mistral-base
mistral-engine
mistral-event-engine
mistral-executor
multipathd
murano-api
murano-base
murano-engine
neutron-base
neutron-bgp-dragent
neutron-dhcp-agent
neutron-infoblox-ipam-agent
neutron-l3-agent
neutron-linuxbridge-agent
neutron-metadata-agent
neutron-metering-agent
neutron-mlnx-agent
neutron-openvswitch-agent
neutron-ovn-agent
neutron-server
neutron-sriov-agent
nova-api
nova-base
nova-compute
nova-compute-ironic
nova-conductor
nova-libvirt
nova-novncproxy
nova-scheduler
nova-serialproxy
nova-spicehtml5proxy
nova-ssh
octavia-api
octavia-base
octavia-driver-agent
octavia-health-manager
octavia-housekeeping
octavia-worker
opensearch
opensearch-dashboards
openstack-base
openvswitch-base
openvswitch-db-server
openvswitch-vswitchd
ovn-base
ovn-controller
ovn-nb-db-server
ovn-northd
ovn-sb-db-server
ovsdpdk
ovsdpdk-db
ovsdpdk-vswitchd
placement-api
placement-base
prometheus-alertmanager
prometheus-base
prometheus-blackbox-exporter
prometheus-cadvisor
prometheus-elasticsearch-exporter
prometheus-haproxy-exporter
prometheus-libvirt-exporter
prometheus-memcached-exporter
prometheus-msteams
prometheus-mtail
prometheus-mysqld-exporter
prometheus-node-exporter
prometheus-openstack-exporter
prometheus-ovn-exporter
prometheus-v2-server
proxysql
rabbitmq
redis
redis-base
redis-sentinel
sahara-api
sahara-base
sahara-engine
senlin-api
senlin-base
senlin-conductor
senlin-engine
senlin-health-manager
skyline-apiserver
skyline-base
skyline-console
solum-api
solum-base
solum-conductor
solum-deployer
solum-worker
swift-account
swift-base
swift-container
swift-object
swift-object-expirer
swift-proxy-server
swift-rsyncd
tacker-base
tacker-conductor
tacker-server
telegraf
tgtd
trove-api
trove-base
trove-conductor
trove-guestagent
trove-taskmanager
venus-api
venus-base
venus-manager
vitrage-api
vitrage-base
vitrage-graph
vitrage-ml
vitrage-notifier
vitrage-persistor
watcher-api
watcher-applier
watcher-base
watcher-engine
zun-api
zun-base
zun-cni-daemon
zun-compute
zun-wsprox
- + \ No newline at end of file diff --git a/docs/architecture/iaas/index.html b/docs/architecture/iaas/index.html index 014f840..7f4e959 100644 --- a/docs/architecture/iaas/index.html +++ b/docs/architecture/iaas/index.html @@ -10,13 +10,13 @@ - +

Infra Labs IaaS 架構

TODO:

  • 加上一些比較特殊設定的介紹

前言

目前 Infra Labs 透過 OpenStack 和 Ceph 來提供 Infrastructure as a Service (IaaS) 服務。本篇會介紹使用的 OpenStack 專案以及 Ceph 的架構。

設定檔以及一些 Ansible Script 將公開於 Infra-Labs-Config repository

硬體

Infra Labs 使用了 9 台伺服器用於提供 IaaS 服務,規格如下:

Hostname: openstack01-03

  • CPU: Intel Xeon Gold 6230R * 2
  • RAM: 32GB DDR4 2933Mhz ECC RDIMM * 12
  • NIC:
    • on board quad port 1G
    • Mellanox ConnectX-4 100GbE
  • Disk
    • Boot Disk: Intel 730 240GB * 2 or Sandisk CloudSpeed Eco Gen II 480GB * 2
    • Ceph SSD: Samsung NGSFF PM983 3.84TB

Hostname: openstack04-05

  • CPU: AMD Epyc 7413
  • RAM: 32GB DDR4 3200Mhz ECC RDIMM * 8
  • NIC:
    • on board quad port 1G
    • Mellanox ConnectX-4 Lx Dual Port 25GbE
  • Disk
    • Boot Disk: Seagate Enterprise Performance 15K 900GB * 2
    • Ceph SSD: Samsung 980 1TB * 4
    • Ceph HDD: Seagate X18 16TB

Hostname: openstack06

  • CPU: Xeon Silver 4110
  • RAM: 32GB DDR4 2666Mhz ECC RDIMM * 6
  • NIC:
    • on board dual port 1GbE
    • Mellanox ConnectX-4 Lx Dual Port 25GbE
  • Disk
    • Boot Disk: Intel S3500 120GB * 2
    • Ceph HDD: Seagate X18 16TB

Hostname: arm01-03

  • CPU: Ampere eMAG 8180
  • RAM: 32GB DDR4 2400Mhz ECC RDIMM * 2(arm03 * 4)
  • NIC:
    • on board dual port 1GbE
    • Mellanox ConnectX-4 Lx Dual Port 25GbE
  • Disk
    • Boot Disk: Intel S3500 120GB
    • Ceph HDD: Seagate X16 16TB

軟體

主機任務分配

  • OpenStack Controller: openstack01-03
  • OpenStack Compute: openstack01-05
  • Ceph Controller: openstack01-03
  • Ceph OSDs: All nodes
  • Monitoring: openstack06

Ansible

Ansible 被用來做一些 OS 安裝後的設定,如安裝一些必要軟體、網卡的設定等等。

OpenStack

Infra Labs 所使用的 OpenStack 服務有:

用於部屬主機:

用於提供服務:

其他 OpenStack 所需元件:

詳細設定檔皆公開至此 Repo

Ceph

Ceph 使用 Cephadm 部屬,後端網路使用 100G/25G 網卡。

提供的服務有:

  • RBD
    • 提供給 Nova, Cinder, Glance 作為儲存後端。
  • RGW
    • 提供給 Swift 作為儲存後端,並且透過 Keystone 認證提供 S3 Compatible API。

目前 Crush Rule 分為兩種:

  • replicated_nvme
    • 使用 NVMe SSD 作為儲存媒介
  • replicated_sata_ssd
    • 使用 SATA SSD 作為儲存媒介
  • erasure profile main
    • 4+2 Erasure Coding 作為 S3 儲存池
- + \ No newline at end of file diff --git a/docs/architecture/network/index.html b/docs/architecture/network/index.html index 257f9f2..447dbe9 100644 --- a/docs/architecture/network/index.html +++ b/docs/architecture/network/index.html @@ -10,14 +10,14 @@ - +

Infra Labs 網路架構

前言

此篇文件將介紹目前 Infra Labs 所使用的網路架構,由於預算問題很多部分設計都可以再改進,如果有相關建議歡迎與管理員討論。

架構圖

High Level Network

設備

目前 Infra Labs 採用多廠牌網路設備,由於網路設備使用開源軟體極少以及價格通常偏高。 因預算問題,目前網路設備大多數使用閉源軟體,並且有單點故障的風險 (Single Point of Failure)。

廠商若有意願贊助網路設備,歡迎聯絡管理員

路由器

路由器目前使用一台 Juniper NFX250,主要功能是與上游進行 BGP 宣告和 Infra Labs 使用 public IP 的 gateway。除此之外,路由器也被用來過濾掉一些容易被攻擊的連接埠。

另一台 NFX250 主要用於:

  • In band management 網路 NAT gateway
  • Dashboard, DNS server 的 NAT
  • Out of band Mangement 網路 VPN access
  • DNS server 流量限制

交換機

Arista DCS-7060CX-32S

Arista DCS-7060CX-32S 100G 交換機主要用於 VM 內/外部網路、Libvirt migration 網路和 Ceph 的 public/private 網路。

Celestica DX010

Celestica DX010 100G 作為 Rack2 的核心交換機。

Juniper EX2200-24T

Juniper EX2200-24T 主要用於 Out of band management 網路。下接至各個主機 IPMI、網路設備 management port,上接至 Fortigate 200D。

LTE 設備

LTE 設備主要提供備援網路,若 NFX250 設定出問題導致主要網路斷線無法從外部連入,可以透過 LTE 設備連入 management 網路進行維修。

網段

目前網段主要分為以下:

  • vlan3000 (untagged access):
    • 192.168.0.0/24
    • PXE/IB
  • vlan 100
    • 10.0.0.0/24
    • VM internal
  • vlan 101
    • 10.0.1.0/24
    • libvirt internal (for migration)
  • vlan 1088
    • rack2_mgmt: 192.168.88.0/24
  • vlan 1113
    • 192.168.113.0/24
    • API network
  • vlan 1114
    • 192.168.114.0/24
    • Ceph Public
  • vlan 1115
    • 192.168.115.0/24
    • Ceph Private
  • vlan 2116
    • 103.122.116.0/23 public
  • vlan 1007
    • mgmt: 192.168.7.0/24
- + \ No newline at end of file diff --git "a/docs/category/admin-\345\260\210\345\215\200/index.html" "b/docs/category/admin-\345\260\210\345\215\200/index.html" index 060cf44..7cc6791 100644 --- "a/docs/category/admin-\345\260\210\345\215\200/index.html" +++ "b/docs/category/admin-\345\260\210\345\215\200/index.html" @@ -10,13 +10,13 @@ - + - + \ No newline at end of file diff --git a/docs/category/container/index.html b/docs/category/container/index.html index 9e96c81..a667205 100644 --- a/docs/category/container/index.html +++ b/docs/category/container/index.html @@ -10,13 +10,13 @@ - + - + \ No newline at end of file diff --git a/docs/category/kubernetes/index.html b/docs/category/kubernetes/index.html index aedef18..41f3423 100644 --- a/docs/category/kubernetes/index.html +++ b/docs/category/kubernetes/index.html @@ -10,13 +10,13 @@ - + - + \ No newline at end of file diff --git a/docs/category/network/index.html b/docs/category/network/index.html index 1efd6bd..7322249 100644 --- a/docs/category/network/index.html +++ b/docs/category/network/index.html @@ -10,13 +10,13 @@ - + - + \ No newline at end of file diff --git "a/docs/category/\345\237\272\347\244\216\346\225\231\345\255\270/index.html" "b/docs/category/\345\237\272\347\244\216\346\225\231\345\255\270/index.html" index a1ec978..c0dd67f 100644 --- "a/docs/category/\345\237\272\347\244\216\346\225\231\345\255\270/index.html" +++ "b/docs/category/\345\237\272\347\244\216\346\225\231\345\255\270/index.html" @@ -10,13 +10,13 @@ - + - + \ No newline at end of file diff --git "a/docs/category/\351\200\262\351\232\216\346\225\231\345\255\270/index.html" "b/docs/category/\351\200\262\351\232\216\346\225\231\345\255\270/index.html" index 39f2457..c3e411a 100644 --- "a/docs/category/\351\200\262\351\232\216\346\225\231\345\255\270/index.html" +++ "b/docs/category/\351\200\262\351\232\216\346\225\231\345\255\270/index.html" @@ -10,13 +10,13 @@ - + - + \ No newline at end of file diff --git a/docs/faq/index.html b/docs/faq/index.html index fa9d34b..3d549e5 100644 --- a/docs/faq/index.html +++ b/docs/faq/index.html @@ -10,7 +10,7 @@ - + @@ -20,7 +20,7 @@ 若因特殊原因需要開啟,請 聯絡管理員 並提供詳細的理由。

Q: 我的 Public IPv4 不夠用了,該怎麼辦?

在預設情況下,我們提供 2 組 Public IPv4 地址。 若您的 IP 地址已用完,我們建議您創建自己的私有網路,並搭配 SNAT 的方式來使用。

若您因實驗需求或服務等其他因素需要更多 Public IPv4 地址。 請 聯絡管理員 並提出使用計畫進行申請。

Q: 請問 VM 有提供 IPv6 地址嗎?

目前 public 網段預設提供 IPv6 地址,歡迎使用。

- + \ No newline at end of file diff --git a/docs/intro/index.html b/docs/intro/index.html index c4c8250..8e951f1 100644 --- a/docs/intro/index.html +++ b/docs/intro/index.html @@ -10,13 +10,13 @@ - +

開始使用 Openstack

Openstack 連結:https://openstack.cloudnative.tw

Jerry 在部落格上撰寫了一些使用的相關心得,也可以參考其撰寫的內容 Jerry Yang's Blog

登入系統並更換密碼

取得帳號及密碼後,登入 OpenStack

點擊右上方的 Settings

接著選擇左邊的 Change Password,即可修改密碼。

接下來請照著教學建立 VM。

- + \ No newline at end of file diff --git a/docs/self-paced-labs/frr_bgp/index.html b/docs/self-paced-labs/frr_bgp/index.html index 9b45793..2ba4796 100644 --- a/docs/self-paced-labs/frr_bgp/index.html +++ b/docs/self-paced-labs/frr_bgp/index.html @@ -10,7 +10,7 @@ - + @@ -25,7 +25,7 @@ show ip bgp
instance-2# show ip bgp
BGP table version is 2, local router ID is 192.168.2.20, vrf id 0
Default local pref 100, local AS 65532
Status codes: s suppressed, d damped, h history, * valid, > best, = multipath,
i internal, r RIB-failure, S Stale, R Removed
Nexthop codes: @NNN nexthop's vrf id, < announce-nh-self
Origin codes: i - IGP, e - EGP, ? - incomplete
RPKI validation codes: V valid, I invalid, N Not found

Network Next Hop Metric LocPrf Weight Path
*> 192.168.1.0/24 192.168.2.10 0 65533 65531 i
*> 192.168.2.0/24 0.0.0.0 0 32768 i

Displayed 2 routes and 2 total paths
  • 查看 BGP 路由 show ip bgp 192.168.1.0/24
instance-2# show ip bgp 192.168.1.0/24
BGP routing table entry for 192.168.1.0/24, version 1
Paths: (1 available, best #1, table default)
Advertised to non peer-group peers:
192.168.2.10
65533 65531
192.168.2.10 from 192.168.2.10 (192.168.2.10)
Origin IGP, valid, external, best (First path received)
Community: 65533:100
Last update: Sat May 14 20:58:07 2022

補充資訊

RIR

RIR 全名 Regional Internet Registries,負責管理全球的網路資源 (ASN, IPv4, IPv6)。

目前全球一共五個 RIR,分別是北美的 ARIN、歐洲 RIPE、亞洲 APNIC、非洲 AFRINIC 及拉丁美洲的 LACNIC。

而某些區域的 RIR 也會允許 NIR (National Internet registry) 負責管理該國家的網路資源。(Eg: TWNIC 管理台灣地區的 ASN, IPv4 及 IPv6 資源)

AS-SET

AS-SET 是一種 Objetc,可以將自己的下游寫入,並提供給上游作為 route filter 使用。通常會從 members 的值來進行巡迴,並產生要過濾的 prefixes。

steveyiyo@SteveYis-MacBook-Pro-2 ~ % whois -r AS-STEVEYI
as-set: AS-STEVEYI
descr: SteveYi Network Service
members: AS7480
members: AS13586
members: AS60614
members: AS141173
members: AS209557
remarks: --- Customer ---
members: AS-YI
remarks: --- DownStream ---
members: AS-STEVEYI-C
remarks: ----------
tech-c: YT1698-RIPE
admin-c: YT1698-RIPE
mnt-by: STEVEYI-MNT
created: 2020-09-10T18:57:46Z
last-modified: 2022-04-26T18:15:21Z
source: RIPE

類實際案例

今天小易 (AS7480) 跟 Gene (AS147035) 建立了 BGP Session,Gene 為了確認小易是不是有權限使用 AS7480,而透過 WHOIS 查詢到了 Email 並進行確認。

steveyiyo@SteveYis-MacBook-Pro-2 ~ % whois -r as7480 | grep mail 
abuse-mailbox: abuse@steveyi.net

而建立 BGP Session 後,Gene 想要根據小易的 ASN / AS-SET 來過濾路由,於是使用了開源的 bgpq4 工具來進行。

steveyiyo@dev:~$ bgpq4 -4l AS7480_v4 AS7480 -J
policy-options {
replace:
prefix-list AS7480_v4 {
44.31.73.0/24;
103.156.184.0/23;
103.156.185.0/24;
103.172.124.0/24;
}
}

Terraform 教學

由於本教學是基於 Terraform 的,所以會使用 Terraform 的環境來進行教學。

安裝 Terraform

至 Terraform 官網的 Downloads 下載最新版本。
不同的版本有不同的下載方式 / 指令。

How to run Terraform script

一旦安裝好 Terraform 後,我們就可以來進行部署了。

首先執行以下指令:

# 先初始化環境
`terraform init`

接著,我們要設定 OpenStack 的環境資訊。

進入 OpenStack 面板 -> Identity / Application Credentials,在右上方選擇建立新的 Application Credential。

以 CNTUG Infra Lab 為例,該連結在 https://openstack.cloudnative.tw/identity/application_credentials/

建立完成後,點擊 Download openrc file,將會下載一個 Shell Script 檔案。

最後則是部署環境,先執行剛剛的 Shell Script 再執行以下指令:

# 部署環境
`terraform apply`

# 會跳出確認視窗,請輸入 `yes` 來確認。

# ref: https://www.terraform.io/cli/commands/apply

How to destroy the environment

當實驗結束後,我們可以使用以下指令來銷毀環境:

# 銷毀環境
terraform destroy

# 會跳出確認視窗,請輸入 `yes` 來確認。

# ref: https://www.terraform.io/cli/commands/destroy
- + \ No newline at end of file diff --git a/docs/self-paced-labs/index.html b/docs/self-paced-labs/index.html index 292d4c7..97a1cd9 100644 --- a/docs/self-paced-labs/index.html +++ b/docs/self-paced-labs/index.html @@ -10,13 +10,13 @@ - + - + \ No newline at end of file diff --git a/docs/self-paced-labs/kaniko/index.html b/docs/self-paced-labs/kaniko/index.html index 9561175..051d2bf 100644 --- a/docs/self-paced-labs/kaniko/index.html +++ b/docs/self-paced-labs/kaniko/index.html @@ -10,7 +10,7 @@ - + @@ -20,7 +20,7 @@ 但在掛載 /var/run/docker.sock 後,相當於環境沒有被完全隔離,可能與其他容器衝突。

Use other tool without privileged mode

使用如本篇介紹的 Kaniko 或是 Buildah,主要是為了解決 Dind 的問題:

  • Docker-in-Docker 需要特權模式才能運行
  • Docker-in-Docker 通常會導致性能損失並且速度可能非常慢

運作原理

  • 以下操作皆在 user-space 使用 executor image 執行,避免使用privileged mode,因此也不會需要 docker daemon。

    • Kaniko 會從 Base image 提取 file system
    • 當逐行執行 Dockerfile 時,在 user-space 對 filesystem 做快照,運用 cache 去和 base image 比對
    • 將有更改的 files append 到 base image,並更新 image 的 metadata
    • 當最後執行完整個 Dockerfile 時, Kaniko executor 將 build 完成的 image push 到指定的 registry 中。
  • 流程如下圖:

  • 根據顏色分為不同層級

    • 紫色的是最外層的邏輯
    • 藍色的是每個 stage
    • 綠色的是執行 Dockerfile 裡面的 instruction

kaniko_workflow

  • 實際執行時,實際上會由 builder executable 去負責圖中所有的工作,這個執行檔會存在由 Kaniko 建立的 /workspace 底下,並先 extract base image 的 filesystem 到 root,在執行後續操作

不需要 privileged mode 的原因

  • Kaniko 在所有操作中沒有引入任何 Docker daemon 或者 CLI 操作。
    • 解壓文件系統(filesystem)
    • 執行命令
    • 在執行器鏡像(executor image)的用戶空間中對文件系統做快照(snapshot)

優點

  • 不需相依於 Docker daemon
  • 都在 Linux userspace 執行
  • 採用非特權權限的容器

缺點

  • 在 Kaniko container 裡面 build time 要用 root 執行
  • 較傳統用 docker build 的方式複雜
  • 沒有支援 local 的 layer caching

使用情境

  • 官方文件中,推薦了四種運行 Kaniko 的方法:
    • Kubernetes cluster
    • In gVisor
    • In Google Cloud Build
    • In Docker
  • 適合在無法獲取 docker daemon 的環境下 build image:如 Kubernetes cluster、Google Kubernetes Engine
  • 適合用在 Gitlab CI 過程中去 build image

事前準備

  1. Dockerfile
  2. Build Contexts:包含 Dockerfile 的 Source Code 目錄
    • GCS Bucket
    • S3 Bucket
    • Azure Blob Storage
    • Local Directory
    • Local Tarball
    • Standard Input
    • Git Repository
  3. Registry:用來存放 image 的 Registry,支援 OCI 標準的 Registry 皆可
    • Docker Hub
    • Harbor
    • Google GCR
    • Amazon ECR
    • Azure Container Registry
    • JFrog Container Registry

Lab

  1. 使用 minukube 在本機上運行
  2. 搭配 Gitlab CI

Lab 1.

  1. 準備執行所需環境:Kubernetes Cluster 以及 Docker Hub 帳號
  2. 準備使用 kaniko 所需的 config files
  3. 準備要用來掛載的本地目錄
  4. 建立 secret 用來儲存 registry 所需的 authorization token
  5. 在 Kubernetes cluster 中建立相關資源並完成 build and push image
  6. pull image 下來測試是否能運行

準備執行所需環境

  • 安裝 minikube 或是使用 kops 建立 cluster
    • 根據自身作業系統選擇相對應的版本安裝
  • 建立 cluster
    minikube start
  • 建立 Docker Hub 帳號

準備使用 kaniko 所需的 config files

這邊需要準備三個檔案

  • pod.yaml:建立執行 kaniko 的 pod
    • --context: 可指定 Git Repository 作為 build context,若未特別指定則預設使用 local directory,用法如下:
    • --context=git://[repository url][#reference][#commit-id]
    • --destination: 指定 Registry 以及 Repo 名稱,可在此處上 image tag,這邊我們使用 docker hub 作為目標 Registry,因此需更改為使用者 docker hub的 username
  • volume.yaml:建立存放 build context 的 persistent volume
  • volume-claim.yaml:建立 persistent volume claim,後續會用來掛載 kaniko container

準備要用來掛載的本地目錄

需要在 cluster 內建立作為 build context 掛載的目錄,並建立 dockerfile

這邊以運行 minikube 為例,可透過 minikube ssh 進到 cluster 內

$ mkdir kaniko && cd kaniko
$ echo 'FROM ubuntu' >> dockerfile
$ echo 'ENTRYPOINT ["/bin/bash", "-c", "echo hello"]' >> dockerfile
$ cat dockerfile
FROM ubuntu
ENTRYPOINT ["/bin/bash", "-c", "echo hello"]
$ pwd
/home/<user-name>/kaniko # 將此路徑填入 volume.yaml file

建立 secret 用來儲存 Registry 所需的 authorization token

在 build 完 image 後,通常需要將 image 上傳到 image Registry,這邊使用 secret 存放相關帳號認證資訊

kubectl create secret docker-registry regcred --docker-server=<your-registry-server> --docker-username=<your-name> --docker-password=<your-pword> --docker-email=<your-email>
  • your-registry-server: 填入 Registry 的 FQDN. (https://index.docker.io/v1/ for DockerHub)
  • your-name: Dockerhub username.
  • your-pword: Dockerhub password.
  • your-email: Dockerhub email.

在 pod.yaml 中會使用到此 secret

在 Kubernetes cluster 中建立相關資源並完成 build and push image

  • 首先建立 persistent volume
kubectl create -f volume.yaml
  • 建立 persistent volume
kubectl create -f volume-claim.yaml
  • 檢查 volume 是否有 mount 成功
kubectl get pv dockerfile
  • 建立 pod
kubectl create -f pod.yaml
kubectl get pods
  • 檢查 build log
kubectl logs kaniko
INFO[0005] Retrieving image manifest ubuntu
INFO[0005] Retrieving image ubuntu from registry index.docker.io
INFO[0008] Built cross stage deps: map[]
INFO[0008] Retrieving image manifest ubuntu
INFO[0008] Returning cached image manifest
INFO[0008] Executing 0 build triggers
INFO[0008] Skipping unpacking as no commands require it.
INFO[0008] ENTRYPOINT ["/bin/bash", "-c", "echo hello"]
INFO[0008] Pushing image to kn71026/kaniko
INFO[0013] Pushed index.docker.io/kn71026/kaniko@sha256:dfc60edba2296b8fa40264952467798e5e62320f280078383033067d060e3376

pull image 下來測試是否能運行

docker run -it kn71026/kaniko:latest
Unable to find image 'kn71026/kaniko:latest' locally
latest: Pulling from kn71026/kaniko
125a6e411906: Pull complete
Digest: sha256:dfc60edba2296b8fa40264952467798e5e62320f280078383033067d060e3376
Status: Downloaded newer image for kn71026/kaniko:latest
hello

小結

這樣就完成了在 Kubernetes 中用 kaniko build image 的流程,官方也提供了許多參數,如 cache 設定build 完成後先不 push 到 registry 等參數供開發人員選擇。

Lab 2:Using Kaniko on Gitlab CI

在 Lab2 中,將使用 Gitlab CI 整合 Kaniko,完成在 CICD 過程中的 image build 操作,步驟如下:

  1. 建立 Gitlab runner,executor 必須是 Docker/Kubernetes/Docker Machine
  2. 在 Gitlab 專案中加上 .gitlab-ci.yml 以導入 Kaniko

建立 Gitlab runner

這邊的 GitLab runner executor 我們選擇使用 Kubernetes,方便起見,一樣使用 minikube 架設 cluster,建立步驟如下:

  1. 建立 gitlab-runner namespace
  • 建立 gitlab-runner-namespace.yaml 檔案,並切換到 gitlab-runner namespace
$ kubectl create -f gitlab-runner-namespace.yaml
$ kubectl get namespace
NAME STATUS AGE
gitlab-runner Active 5s
$ kubectl config set-context --current --namespace=gitlab-runner
Context "minikube" modified.
  1. 在 gitlab-runner namespace 下建立 role,允許 create/delete/exec/delete 以及 讀取 pods 的 log,在執行 pipeline 時,gitlab runner 會在 cluster 中建立新的 pod 執行 gitlab-ci.yaml 中定義的 job,因此需要讓 gitlab runner 有存取 Kubernetes API 的權限,預設在 gitlab-runner namespace 中的 default service account 是沒有權限的,因此我們需要先定義 gitlab-runner-gitlab-runner-role.yaml
  • 接著建立 role
$ kubectl create -f gitlab-runner-gitlab-runner-role.yaml
role.rbac.authorization.k8s.io/gitlab-runner created
$ kubectl get --namespace=gitlab-runner role
NAME CREATED AT
gitlab-runner 2022-05-31T07:55:43Z
$ kubectl describe role --namespace gitlab-runner gitlab-runner
Name: gitlab-runner
Labels: <none>
Annotations: <none>
PolicyRule:
Resources Non-Resource URLs Resource Names Verbs
--------- ----------------- -------------- -----
pods/exec [] [] [create]
pods/log [] [] [get]
configmaps [] [] [list get create delete update]
pods/attach [] [] [list get create delete update]
secrets [] [] [list get create delete update]
pods [] [] [list get watch create delete]
  1. Assign/bind role 到 service account system:serviceaccount:gitlabrunner:default

在建立好 role 後,我們需要將 role bind 到 service account

$  kubectl create rolebinding --namespace=gitlab-runner gitlab-runner-binding --role=gitlab-runner --serviceaccount=gitlab-runner:default
rolebinding.rbac.authorization.k8s.io/gitlab-runner-binding created
$ kubectl get --namespace gitlab-runner rolebinding
NAME ROLE AGE
gitlab-runner-binding Role/gitlab-runner 14s
  1. 使用 Helm 安裝 gitlab runner

在完成上述的環境建置步驟後,終於可以安裝 gitlab runner 了,但這邊會使用到 Helm,若未安裝過需先到官網安裝

安裝 Helm 後,需要先加入 Gitlab Helm repository

$ helm repo add gitlab https://charts.gitlab.io

裝好 Helm 後,在用 Helm 建立 Chart 前,需要先準備 GitLab url 以及 runner registration token,可以到想導入 Kaniko 的專案左側 sidebar 最下方 Settings 內的 CI/CD 頁面內,找到 Runner 並點開,就可以看到這兩項資訊: Gitlab_runner_info

若使用 gitlab ee 版本,URL 就會是 https://gitlab.com/, 但若是自架的 Gitlab,URL 就會有所不同。

再來創建等下要使用的 values.yaml,這邊提供較簡單的版本,若需要詳細設定可以參考 Gitlab 官方的版本

  • 完成後可執行
# For Helm 3
$ helm install --namespace gitlab-runner gitlab-runner -f values.yaml gitlab/gitlab-runner
  • 若建立成功,可看到成功 deploy 的訊息,也可透過 kubectl get all 確認
NAME: gitlab-runner
LAST DEPLOYED: Tue May 31 20:57:51 2022
NAMESPACE: gitlab-runner
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Your GitLab Runner should now be registered against the GitLab instance reachable at: "https://gitlab.com/"

Runner namespace "gitlab-runner" was found in runners.config template.
$ kubectl get all
NAME READY STATUS RESTARTS AGE
pod/gitlab-runner-785f7fd48c-bvjqk 1/1 Running 0 59s

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/gitlab-runner 1/1 1 1 59s

NAME DESIRED CURRENT READY AGE
replicaset.apps/gitlab-runner-785f7fd48c 1 1 1 59s
  • 在 Gitlab 專案 Settings 內的 CI/CD,點擊 Expand 展開 Runners 區塊,也能看到已註冊好的 Gitlab runner。

Gitlab_runner_available

在 Gitlab 專案中導入 Kaniko

接著需要在專案根目錄中加入 .gitlab-ci.yml,若是之前已有 .gitlab-ci.yml 的專案,可直接更改檔案內容,另外專案本身也需有 Dockerfile,將在 .gitlab-ci.yml 內設定相關路徑,由於 GitLab ee 版本有提供 Container Registry,因此這邊不特別做設定推送 image 到 dockerhub上,這邊以官方提供的範例設定檔來示範。

commit 後即可看到 pipeline 開始執行,若 .gitlab-ci.yml 填寫正確即可看到 Kaniko 成功運行並 push image 到專案的 Container Registry 中。 Gitlab_job

若想推送到 Dockerhub 中,可先在本機登入 Dockerhub,再將 auth 資訊存入 GitLab CI/CD Variables 中,在 CI 過程中將變數傳入,即可推送到 Dockerhub,方法如下:

  • 登入 Docker hub
docker login index.docker.io
  • 檢查 auth 資訊是否有存入
cat ~/.docker/config.json
  • 有成功登入應該會看到檔案內容如下:
{
"auths": {
"https://index.docker.io/v1/": {
"auth": "xxxxxxxxxxxxxxx"
}
}
}
  • 將上面的參數存入 GitLab 專案左側 sidebar Settings 內的 CI/CD Variables 中,並在創立完 /kaniko/.docker 目錄後,將此變數存進 /kaniko/.docker/config.json 中,或是也能用 secret 的方式,掛載相關認證資訊到 pod 上。

小結

在 Gitlab CI 過程中使用 Kaniko 還蠻方便的,但比較適用在使用 k8s executors 的情況,若搭配 Gitlab runner 以及 Kaniko 的 cache,build 速度則會更快。

Reference

- + \ No newline at end of file diff --git a/docs/self-paced-labs/kops/index.html b/docs/self-paced-labs/kops/index.html index e55722d..ad35c8b 100644 --- a/docs/self-paced-labs/kops/index.html +++ b/docs/self-paced-labs/kops/index.html @@ -10,14 +10,14 @@ - +

OpenStack 上利用 kops 搭建 K8S Cluster

本篇 Lab 相關附件

本篇相關附件在這裡

本次 lab 將會帶領大家利用 kops 在 OpenStack 上快速搭建一個 Kubernetes cluster。

kops 是什麼?

Kops 是 Kubernetes Operation 的縮寫,顧名思義就是一個拿來做 K8S 維運相關操作的一個工具,而 kops 目前有幾項特點

  • 支援建立、維護、升級、摧毀 K8S 群集
  • 支援 AWS (production)、OpenStack/DigitalOcean (Beta)、Azure/GCP (Alpha)
  • 能夠透過 kops 生成群集的 Terraform template
  • 管理 K8S cluster add-ons
  • 自動部署 K8S 群集

kops 名詞

Storage

STATE_STORE

STATE STORE 定義了 kops 需要儲存其資料的地方,kops 會儲存的資料包含了 cluster spec、instance group 跟 ssh key 等。

API

Cluster spec

Cluster spec 定義了群集本身所需的一些 specification,例如群集中的 DNS、Load Balancer、etcd cluster 等資訊。

Instace Group

Instance Group 定義了實際 worker node、master node、etcd node 的一些相關資料,以 OpenStack 為例就是會建立的虛擬機所使用的 flavor、image 等。

Cloud

The Cloud

The Cloud 為不同雲端平台定義了統一的 golang interface,由於 kops 支援很多不同雲端環境,所以利用這個 interface 做 abstraction

Tasks

一個 task 就是對雲端環境操作的一個 API call

Model

Model 是將之前定義的 cluster spec 裡面的內容對應到實際的 task -例如說我們在 cluster spec 上定義了一個 load balancer ,那 loadbalancer model 就會將其對應到實際上建立一個 loadbalancer 所需要的各種 API call

安裝 kops

前置準備

安裝 kops 前需要安裝 kubectl,安裝方式請見此官方文件

透過 homebrew 安裝

brew update && brew install kops

透過官方編譯好的 release 安裝

Linux

curl -Lo kops https://github.com/kubernetes/kops/releases/download/$(curl -s https://api.github.com/repos/kubernetes/kops/releases/latest | grep tag_name | cut -d '"' -f 4)/kops-linux-amd64
chmod +x kops
sudo mv kops /usr/local/bin/kops

Mac

curl -Lo kops https://github.com/kubernetes/kops/releases/download/$(curl -s https://api.github.com/repos/kubernetes/kops/releases/latest | grep tag_name | cut -d '"' -f 4)/kops-darwin-amd64
chmod +x kops
sudo mv kops /usr/local/bin/kops

自行編譯

自行編譯的話請先確認環境內有安裝 golang,並且設定好 GOPATH 與 GOBIN

git clone git@github.com:kubernetes/kops.git
cd kops
make

Lab:建立 Kubernetes Cluster

下載 OpenStack Credential

Note

請聯絡 admin 開啟 load balancer 使用權限

進入 OpenStack 面板 -> Identity / Application Credentials,在右上方選擇建立新的 Application Credential。

以 CNTUG Infra Lab 為例,該連結在 https://openstack.cloudnative.tw/identity/application_credentials/

建立完成後,點擊 Download openrc file,將會下載一個 Shell Script 檔案。

下載完成後,會需要將 RC file 的資訊放進 shell environment variable

source openrc.sh
export OS_DOMAIN_NAME=Default

設定 kops STATE_STORE

kops 會需要設定儲存資料的問題也就是上面提到的 STATE_STORE

由於 Infra Labs 有提供 Swift 服務,我們可以直接利用

export KOPS_STATE_STORE=swift://kops

建立 Kubernetes Cluster Template

接下來要利用 kops 建立 cluster 的資料,很簡單只需要一行指令:

Note

請替換指令中 <cluster_name> 跟 <ssh_key_path>

kops create cluster \
--cloud openstack \
--name <cluster_name> \
--state ${KOPS_STATE_STORE} \
--zones nova \
--network-cidr 10.0.0.0/24 \
--image Ubuntu-22.04 \
--master-count=3 \
--node-count=1 \
--node-size m1.small \
--master-size m1.small \
--etcd-storage-type NVMe \
--api-loadbalancer-type public\
--topology private \
--bastion \
--ssh-public-key <ssh_key_path> \
--networking calico \
--os-ext-net public \
--os-dns-servers=8.8.8.8,8.8.4.4 \
--os-octavia=true \
--os-octavia-provider=ovn

本次 lab 中,我們會建立一個共 4 node 的 cluster,包含 3 個 master/etcd node 跟 1 個 work node。

架構圖如下:

architecture

kops create cluster 會建立 cluster spec, instance group 等等資料,接下來需要用 kops update cluster 來實際建立 Kubernetes cluster

kops update cluster --name <cluster_name> --yes --admin

這時候 kops 就會開始呼叫 OpenStack API,建立 Kubernetes cluster 所需要的 VM,網路,硬碟,load balancer 等資源。結束後要等待大約 5-10 分鐘讓整個 cluster 啟動。

此時這個新建立的 Kubernetes cluster 的 config 會被寫入在 ~/.kube/config

Note

若是在 update 中出現 409 error,並且 OpenStack Dashboard 上 load balancer 在 PENDING_UPDATE 狀態,請聯絡管理員

驗證 Kubernetes Cluster

最後我們可以驗證 Kubernetes Cluster 安裝成功。

kops validate cluster --wait 5m

會得到以下 output


Validating cluster igene.k8s.local

INSTANCE GROUPS
NAME ROLE MACHINETYPE MIN MAX SUBNETS
bastions Bastion m1.tiny 1 1 nova
master-nova-1 Master m1.small 1 1 nova
master-nova-2 Master m1.small 1 1 nova
master-nova-3 Master m1.small 1 1 nova
nodes-nova Node m1.small 1 1 nova

NODE STATUS
NAME ROLE READY
master-nova-1-nvhtbc master True
master-nova-2-jgnhbo master True
master-nova-3-vllcwf master True
nodes-nova-yxorvl node True

Your cluster igene.k8s.local is ready

此時就可以利用 kubectl 對 cluster 進行操作了。

使用 kops 安裝的 cluster 預設已經安裝 cloud-provider-openstack 並且整合了 Cinder CSI 和 Octavia Load Balancer 等功能。

kubectl get csidrivers.storage.k8s.io
NAME ATTACHREQUIRED PODINFOONMOUNT STORAGECAPACITY TOKENREQUESTS REQUIRESREPUBLISH MODES AGE
cinder.csi.openstack.org true true false <unset> false Persistent,Ephemeral 13h

刪除 Kubernetes Cluster

kops 紀錄了其建立在 OpenStack 上的所有資源,因此要摧毀 Kubernetes cluster 並且釋出資源非常簡單,只需一行指令:

kops delete cluster <cluster_name> --yes

kops 將會刪除所有在 OpenStack 上建立的資源。

小結

在此次 lab 中,我們教學了如何利用 kops 在 OpenStack 上建立一個完整的 Kubernetes cluster。kops 搭建的 cluster 同時也整合了 cloud-provider-openstack 的相關功能,讓使用者可以在 cluster 內直接使用 persistant volumeload balancer 等功能。

後續與 Kubernetes 相關的 lab 也會建議利用此方法架設 Kubernetes cluster。

- +例如說我們在 cluster spec 上定義了一個 load balancer ,那 loadbalancer model 就會將其對應到實際上建立一個 loadbalancer 所需要的各種 API call

安裝 kops

前置準備

安裝 kops 前需要安裝 kubectl,安裝方式請見此官方文件

透過 homebrew 安裝

brew update && brew install kops

透過官方編譯好的 release 安裝

Linux

curl -Lo kops https://github.com/kubernetes/kops/releases/download/$(curl -s https://api.github.com/repos/kubernetes/kops/releases/latest | grep tag_name | cut -d '"' -f 4)/kops-linux-amd64
chmod +x kops
sudo mv kops /usr/local/bin/kops

Mac

curl -Lo kops https://github.com/kubernetes/kops/releases/download/$(curl -s https://api.github.com/repos/kubernetes/kops/releases/latest | grep tag_name | cut -d '"' -f 4)/kops-darwin-amd64
chmod +x kops
sudo mv kops /usr/local/bin/kops

自行編譯

自行編譯的話請先確認環境內有安裝 golang,並且設定好 GOPATH 與 GOBIN

git clone git@github.com:kubernetes/kops.git
cd kops
make

Lab:建立 Kubernetes Cluster

下載 OpenStack Credential

Note

請聯絡 admin 開啟 load balancer 使用權限

進入 OpenStack 面板 -> Identity / Application Credentials,在右上方選擇建立新的 Application Credential。

以 CNTUG Infra Lab 為例,該連結在 https://openstack.cloudnative.tw/identity/application_credentials/

建立完成後,點擊 Download openrc file,將會下載一個 Shell Script 檔案。

下載完成後,會需要將 RC file 的資訊放進 shell environment variable

source openrc.sh
export OS_DOMAIN_NAME=Default

設定 kops STATE_STORE

kops 會需要設定儲存資料的問題也就是上面提到的 STATE_STORE

由於 Infra Labs 有提供 Swift 服務,我們可以直接利用

export KOPS_STATE_STORE=swift://kops

建立 Kubernetes Cluster Template

接下來要利用 kops 建立 cluster 的資料,很簡單只需要一行指令:

Note

請替換指令中 <cluster_name> 跟 <ssh_key_path>

kops create cluster \
--cloud openstack \
--name <cluster_name>.k8s.local \
--state ${KOPS_STATE_STORE} \
--zones nova \
--network-cidr 10.0.0.0/24 \
--image Ubuntu-22.04 \
--master-count=3 \
--node-count=1 \
--node-size m1.small \
--master-size m1.small \
--etcd-storage-type NVMe \
--api-loadbalancer-type public\
--topology private \
--bastion \
--ssh-public-key <ssh_key_path> \
--networking calico \
--os-ext-net public \
--os-dns-servers=8.8.8.8,8.8.4.4 \
--os-octavia=true \
--os-octavia-provider=ovn

本次 lab 中,我們會建立一個共 4 node 的 cluster,包含 3 個 master/etcd node 跟 1 個 work node。

架構圖如下:

architecture

kops create cluster 會建立 cluster spec, instance group 等等資料,接下來需要用 kops update cluster 來實際建立 Kubernetes cluster

kops update cluster --name <cluster_name> --yes --admin

這時候 kops 就會開始呼叫 OpenStack API,建立 Kubernetes cluster 所需要的 VM,網路,硬碟,load balancer 等資源。結束後要等待大約 5-10 分鐘讓整個 cluster 啟動。

此時這個新建立的 Kubernetes cluster 的 config 會被寫入在 ~/.kube/config

Note

若是在 update 中出現 409 error,並且 OpenStack Dashboard 上 load balancer 在 PENDING_UPDATE 狀態,請聯絡管理員

驗證 Kubernetes Cluster

最後我們可以驗證 Kubernetes Cluster 安裝成功。

kops validate cluster --wait 5m

會得到以下 output


Validating cluster igene.k8s.local

INSTANCE GROUPS
NAME ROLE MACHINETYPE MIN MAX SUBNETS
bastions Bastion m1.tiny 1 1 nova
master-nova-1 Master m1.small 1 1 nova
master-nova-2 Master m1.small 1 1 nova
master-nova-3 Master m1.small 1 1 nova
nodes-nova Node m1.small 1 1 nova

NODE STATUS
NAME ROLE READY
master-nova-1-nvhtbc master True
master-nova-2-jgnhbo master True
master-nova-3-vllcwf master True
nodes-nova-yxorvl node True

Your cluster igene.k8s.local is ready

此時就可以利用 kubectl 對 cluster 進行操作了。

使用 kops 安裝的 cluster 預設已經安裝 cloud-provider-openstack 並且整合了 Cinder CSI 和 Octavia Load Balancer 等功能。

kubectl get csidrivers.storage.k8s.io
NAME ATTACHREQUIRED PODINFOONMOUNT STORAGECAPACITY TOKENREQUESTS REQUIRESREPUBLISH MODES AGE
cinder.csi.openstack.org true true false <unset> false Persistent,Ephemeral 13h

刪除 Kubernetes Cluster

kops 紀錄了其建立在 OpenStack 上的所有資源,因此要摧毀 Kubernetes cluster 並且釋出資源非常簡單,只需一行指令:

kops delete cluster <cluster_name> --yes

kops 將會刪除所有在 OpenStack 上建立的資源。

小結

在此次 lab 中,我們教學了如何利用 kops 在 OpenStack 上建立一個完整的 Kubernetes cluster。kops 搭建的 cluster 同時也整合了 cloud-provider-openstack 的相關功能,讓使用者可以在 cluster 內直接使用 persistant volumeload balancer 等功能。

後續與 Kubernetes 相關的 lab 也會建議利用此方法架設 Kubernetes cluster。

附錄

加入不同 Flavor 的 Instance

kops 可以透過 instance groups 加入不同 flavor 跟數量的 instance

Instance group 的範例如下

apiVersion: kops.k8s.io/v1alpha2
kind: InstanceGroup
metadata:
labels:
kops.k8s.io/cluster: <cluster_name>.k8s.local
name: nodes-nova-arm
spec:
image: Ubuntu-22.04-aarch64
machineType: a1.large
maxSize: 1
minSize: 1
role: Node
subnets:
- nova

透過以下指令建立 instance group 並且更新 cluster 以建立新的 instance

cat ig.yaml | kops create -f -

kops update cluster --name <cluster_name>.k8s.local --yes --admin

新的 Instance 將會被建立然後加入至 cluster 中。

+ \ No newline at end of file diff --git a/docs/self-paced-labs/kwok-in-docker/index.html b/docs/self-paced-labs/kwok-in-docker/index.html index e3fc9bb..433c713 100644 --- a/docs/self-paced-labs/kwok-in-docker/index.html +++ b/docs/self-paced-labs/kwok-in-docker/index.html @@ -10,13 +10,13 @@ - +

KWOK in Docker - 體驗上千節點的 K8s 工具

Kubernetes SIGs(Special Interest Group 特別興趣小組)前陣子發布了很有趣的 K8s 模擬器 —— KWOK:Kubernetes WithOut Kubelet。

KWOK 的應用情境,開發者想要在幾秒鐘內建立一個由數千個節點組成的 Kubernetes,且模擬具有低資源佔用的真實節點,並且在不花費太多基礎設施的情況下大規模測試的 Kubernetes 控制器。

讓我們現在來體驗 KWOK 工具的安裝 & 模擬吧!

Demo 環境

本次實驗使用 CNTUG Infra Labs 的主機,有興趣的朋友可以直接到 CNTUG Infra Labs 說明文件 首頁點擊「申請 Infra Labs」。

  • OS: Ubuntu 22.04
  • Docker Engine: 23.0.1
  • KWOK: Docker - All in one image - cluster:v1.26.0
  • K8s simulate version: v1.26.0
  • kubectl version: v1.26.0

前置作業

安裝 Docker

根據 Docker 官方安裝教學:

curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

安裝完後就可以直接使用了,但如果不想前面一直打 sudo,就加入以下命令。

sudo usermod -aG docker $USER

最後再 logout 重新登入就可以了。

安裝 kubectl

kubectl 是一個命令列工具,用於操作 Kubernetes 集群,使用下列步驟安裝 kubectl

curl -LO https://dl.k8s.io/release/v1.26.0/bin/linux/amd64/kubectl
curl -LO https://dl.k8s.io/v1.26.0/bin/linux/amd64/kubectl.sha256
echo "$(cat kubectl.sha256) kubectl" | sha256sum --check
# If check success, install it.
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
# Remove install files.
rm kubectl*

使用 kubectl 指令測試版本,確認 kubectl 安裝成功。

kubectl version

啟動 KWOK

docker run --rm -it -d -p 8080:8080 registry.k8s.io/kwok/cluster:v1.26.0

docker ps -a 確認服務是不是有啟動成功

確認沒問題後就可以依照官方教學開始實驗模擬。

實驗模擬

建立完成後,就可以用 kubectl 查看資源。

kubectl get node -o wide
kubectl get pod -A
kubectl get svc -A

但目前裡面是沒有任何內容,為了要模擬 K8s 狀況,要先加入新節點。

KWOK 新增節點

輸入以下命令就可以新增節點。(註:這裡是根據 KWOK 官網加入節點的教學,實際上 K8s 不會這樣新增節點,特此注意。

kubectl apply -f - <<EOF
apiVersion: v1
kind: Node
metadata:
annotations:
node.alpha.kubernetes.io/ttl: "0"
kwok.x-k8s.io/node: fake
labels:
beta.kubernetes.io/arch: amd64
beta.kubernetes.io/os: linux
kubernetes.io/arch: amd64
kubernetes.io/hostname: kwok-node-0
kubernetes.io/os: linux
kubernetes.io/role: agent
node-role.kubernetes.io/agent: ""
type: kwok
name: kwok-node-0
spec:
taints: # Avoid scheduling actual running pods to fake Node
- effect: NoSchedule
key: kwok.x-k8s.io/node
value: fake
status:
allocatable:
cpu: 32
memory: 256Gi
pods: 110
capacity:
cpu: 32
memory: 256Gi
pods: 110
nodeInfo:
architecture: amd64
bootID: ""
containerRuntimeVersion: ""
kernelVersion: ""
kubeProxyVersion: fake
kubeletVersion: fake
machineID: ""
operatingSystem: linux
osImage: ""
systemUUID: ""
phase: Running
EOF

新增後會顯示 node/kwok-node-0 created

確認 Node 有進入 Ready。

kubectl get node -o wide

新增 Deployment

這裡我們來嘗試加入一些 Pod 到 KWOK 裡面。

kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: fake-pod
namespace: default
spec:
replicas: 10
selector:
matchLabels:
app: fake-pod
template:
metadata:
labels:
app: fake-pod
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: type
operator: In
values:
- kwok
# A taints was added to an automatically created Node.
# You can remove taints of Node or add this tolerations.
tolerations:
- key: "kwok.x-k8s.io/node"
operator: "Exists"
effect: "NoSchedule"
containers:
- name: fake-container
image: fake-image
EOF

Deployment 建立成功後,會顯示下列輸出: deployment.apps/fake-pod created

建立完成後,列出 Deployment、ReplicaSet 和所有 Pod。

kubectl get deployment
kubectl get replicaset
kubectl get pod

看起來沒問題,連 ReplicaSet 都可以模擬呢!

也可以試著 describe deployment。

kubectl describe deploy fake-pod

describe 出來的內容很實際呢,下一段來編輯看看 Deployment。

編輯 Deployment

模擬工具可以模仿多個 Pod 做 scale up 情況,那我們就來嘗試看看!

kubectl edit deploy fake-pod

就把 .spec.replicas 調到 120 個看看。

編輯完成後退出,成功就會顯示 deployment.apps/fake-pod edited,來看看有沒有產生成功。

kubectl get deployment
kubectl get replicaset

這裡顯示為 110 個 READY,就代表是沒有問題的,因為單節點 Pod 數量建議上限為 110 個。

如果回去看前面建立節點的步驟,.status.allocatable.pods 也是寫上 110。

也可以搭配 grep 看一下 Pending 的 Pod。

kubectl get pod | grep Pending

刪除 Deployment

kubectl delete deployment fake-pod
kubectl get pod -A

停止 KWOK

最後就把 KWOK 的 container 停止就好。

docker ps -a
docker stop <CONTAINER ID>

總結

目前這樣初步玩下來覺得 KWOK 是還蠻不錯的模擬器,這樣測下來結果該有的都有,這工具個人覺得蠻適合剛入門 K8s 的人,或者想測試一些多節點的特殊玩法,不用怕測失敗把整個環境搞爛花時間重建,如果想要模擬上千節點情況也不需要昂貴的硬體設備。

真要說唯一缺點的話,CrashLoopBackOff 是不會出現的,沒辦法讓新手體驗地獄,真的有點可惜,但畢竟是 K8s WithOut Kubelet,背後是不會跑真正的 container,沒有辦法模擬出也是很正常的。

- + \ No newline at end of file diff --git a/docs/self-paced-labs/quarkus-with-helm-charts/index.html b/docs/self-paced-labs/quarkus-with-helm-charts/index.html index 6fdc2d1..fb87923 100644 --- a/docs/self-paced-labs/quarkus-with-helm-charts/index.html +++ b/docs/self-paced-labs/quarkus-with-helm-charts/index.html @@ -10,13 +10,13 @@ - +

Helm Chart 整合以 Quarkus 為例

Helm 是一個管理 Kubernetes 資源的打包工具,其產出單元可以稱為 charts,類似於容器映像檔或是 Ubuntu 中的 apt 管理工具,因此它是可以被建立或是刪除。其同時也是 CNCF Application Definition & Image Build 中的一員。常見類似的有 Kustomize 等。

一般規劃一個服務要部署在 Kubernetes 上時也許會定義如下資源。但對於要退回或是修改都會維護這些大量資源,其對於發布的應用程式沒有版本控制的概念。

  • Deployment
  • Service
  • Ingress
  • Secret

Helm 可以幫助我們完成這些任務,以方便管理應用程式資源和發布的生命週期,並統一管理、配置和更新這些 Kubernetes 的資源檔案

  • 發布和套用一套模板
  • 將資源作為一個 charts 來管理
  • 倉庫儲存這些 charts

透過本文章會學習到以下:

  • 使用 charts 管理部署至 Kubernetes 資源
  • charts 整合 Kubeconform 驗證 Kubernetes 部署資源
  • charts 整合 kube-score 實現 yaml 檔案靜態掃描
  • charts 單元測試
  • charts 整合 helm-docs 產生文件
  • charts 推送以 Docker Hub 中 Registry 為例
  • helm 指令使用

此文章範例來源程式碼可以至此連結

實驗環境

  • OS: Windows 11 WSL (Ubuntu 22.04)
  • Docker Engine: 23.0.5
  • k3d version: v5.4.9
  • kubectl version: v1.27.1
  • Helm version: v3.11.3
  • kube-score version: v1.17.0
  • kubeconform version: v0.6.3
  • helm-docs version: v1.11.0

建立 Helm Chart

使用 helm create <name> 指令建立一個 charts,如下:

$ helm create web-app
Creating web-app

此時會建立一些基礎的資源,更詳細的 charts 結構內容可參閱其官方文件

.
└── web-app
├── Chart.yaml # chart 相關資訊(版本號等)
├── charts # 依賴其它 chart 的目錄
├── templates # 用來產生 Kubernetes 資源,會與定義的值資源整合
│ ├── NOTES.txt
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ ├── hpa.yaml
│ ├── ingress.yaml
│ ├── service.yaml
│ ├── serviceaccount.yaml
│ └── tests
│ └── test-connection.yaml
└── values.yaml

因為範例的應用程式需要 clusterrole 等其它資源,因此有多定義一些資源。charts 基礎資源只是給了部署至 Kubernetes 資源一些基礎結構,應當依照需求自行設計。

_helpers.tpl 這個檔案可以定義共用的模板,以此範例來說定義了一個共用註解區塊的方法,如下。該 web-app.annotation 是方法名稱,其用 define 關鍵字進行定義。

{{/* # 註解區塊
common annotation
*/}}
{{- define "web-app.annotation" -}} #
org.cch.com/owner: cch
org.cch.com/organize: CCH Tech
{{- end }}

web-app.annotation 將其使用於 deployment.yaml 中的 metadata.annotations,結果如下。

apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "web-app.fullname" . }}
labels:
{{- include "web-app.labels" . | nindent 4 }}
annotations:
{{- include "web-app.annotation" . | nindent 4 }}
{{- with .Values.deploymentAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "web-app.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
{{- if .Values.app.properties }}
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- end }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "web-app.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "web-app.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
env:
- name: KUBE_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: KUBE_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
{{- with .Values.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.extraEnvFrom }}
envFrom:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.livenessProbe }}
livenessProbe:
{{- tpl (toYaml .) $ | nindent 12 }}
{{- end }}
{{- with .Values.readinessProbe }}
readinessProbe:
{{- tpl (toYaml .) $ | nindent 12 }}
{{- end }}
{{- with .Values.lifecycle }}
lifecycle:
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

這邊使用 include 函式將其 web-app.annotation 模板引入,並使用 . 將其作為一個值,最後將結果往 indent 函示傳入。Helm 提供多種的模板函數,有興趣者可以至此官方文件進行查看。

當模板定義好之後,必須將其填充資料,否則無法正常被 Kubernetes 執行。在預設上會使用 values.yaml 進行填充。

以上面的 deployment.yaml 模板為例,執行以下,可以看見值被 values.yaml 填充。

# -s 指定某個要被轉譯的模板
# -f 指定對模板進行填充的 values yaml
$ helm template ./ -s templates/deployment.yaml -f values.yaml
---
# Source: web-app/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: release-name-web-app
labels:
helm.sh/chart: web-app-0.1.0
app.kubernetes.io/name: web-app
app.kubernetes.io/instance: release-name
app.kubernetes.io/version: "1.16.0"
app.kubernetes.io/managed-by: Helm
annotations:
org.cch.com/owner: cch
org.cch.com/organize: CCH Tech
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: web-app
app.kubernetes.io/instance: release-name
template:
metadata:
annotations:
checksum/config: 9370cee06587dc819f556fef23ae74570f025b5f5570ab3188bd3007ff0828f0
labels:
app.kubernetes.io/name: web-app
app.kubernetes.io/instance: release-name
spec:
serviceAccountName: web
securityContext:
fsGroup: 185
runAsNonRoot: true
containers:
- name: web-app
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
runAsGroup: 185
runAsUser: 185
image: "egistry.hub.docker.com/cch0124/helm-demo:latest"
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
protocol: TCP
env:
- name: KUBE_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: KUBE_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
livenessProbe:
httpGet:
path: /q/health/live
port: http
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 30
successThreshold: 1
timeoutSeconds: 10
readinessProbe:
httpGet:
path: /q/health/ready
port: http
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 30
successThreshold: 1
timeoutSeconds: 10
lifecycle:
preStop:
exec:
command:
- sh
- -c
- sleep 10
resources:
limits:
cpu: 100m
memory: 256Mi
requests:
cpu: 10m
memory: 256Mi

Helm 透過這種設計做到了 Kubernetes 原生無法管理 yaml 的問題。到這邊可以了解模板如何轉譯,但在轉譯前可以對 charts 進行格式上驗證,驗證模板中的格式是否正確,但此階段無法確保轉譯出來的 Kubernetes 資源可被使用。

$ helm lint ./ -f values.yaml 
==> Linting ./
[INFO] Chart.yaml: icon is recommended

1 chart(s) linted, 0 chart(s) failed

Helm charts 整合 Kubeconform

當一個 charts 被定義好之後,應當驗證其模板轉譯出來 yaml 的完整性。Kubeconform 是一個驗證工具 Kubernetes yaml 資源的工具,可以將其導入 CI 流程。

本實驗是用 ubuntu 環境直接安裝,其它方式可參閱此連結

$ wget https://github.com/yannh/kubeconform/releases/download/v0.6.3/kubeconform-linux-amd64.tar.gz
$ tar xf kubeconform-linux-amd64.tar.gz
$ sudo mv kubeconform /usr/local/bin

驗證方式很簡單的使用,可以透過 helm template 產生出期望的部署的 yaml 檔案,再透過 Kubeconform 驗證。

驗證過程會建議,指定 Kubernetes 版本來進行驗證,Kubernetes 每個版本的 API 會有所不同,這樣可以明確知道 charts 可以在哪個環境運行。避免 Kubernetes 升級後帶來的災難。

$ helm template --skip-tests --kube-version v1.23.0 ./ -f ci/values-analysis.yaml | kubeconform --summary -kubernetes-version 1.23.0
Summary: 8 resources found parsing stdin - Valid: 8, Invalid: 0, Errors: 0, Skipped: 0

如果此時,想使用 1.26.0 版本的 Kubernetes 環境驗證會發現錯誤。這是因為 1.23.0 版本支援 autoscaling/v2beta1,但 1.26.0 官方認為他已經可以穩定運行所以變更為 autoscaling/v2

$ helm template --skip-tests --kube-version v1.26.0 ./ -f ci/values-analysis.yaml | kubeconform --summary -kubernetes-version 1.26.0
stdin - HorizontalPodAutoscaler release-name-web-app failed validation: could not find schema for HorizontalPodAutoscaler
Summary: 8 resources found parsing stdin - Valid: 7, Invalid: 0, Errors: 1, Skipped: 0

這驗證結果要有更詳細的內容可以加入 -output 參數來指定格式,其支援的有 json、junit 與 tap,而 text 為預設

$ helm template --skip-tests ./ -f ci/values-analysis.yaml | kubeconform -kubernetes-version 1.23.0  -output junit
<testsuites name="kubeconform" time="1.137183422" tests="8" failures="0" disabled="0" errors="0">
<testsuite name="stdin" id="1" tests="8" failures="0" errors="0" disabled="0" skipped="0">
<testcase name="release-name-web-app" classname="ConfigMap@v1" time="0"></testcase>
<testcase name="default/release-name-web-app" classname="ClusterRole@rbac.authorization.k8s.io/v1" time="0"></testcase>
<testcase name="release-name-web-app" classname="ClusterRoleBinding@rbac.authorization.k8s.io/v1" time="0"></testcase>
<testcase name="web" classname="ServiceAccount@v1" time="0"></testcase>
<testcase name="release-name-web-app" classname="Service@v1" time="0"></testcase>
<testcase name="release-name-web-app" classname="Ingress@networking.k8s.io/v1" time="0"></testcase>
<testcase name="release-name-web-app" classname="HorizontalPodAutoscaler@autoscaling/v2beta1" time="0"></testcase>
<testcase name="release-name-web-app" classname="Deployment@apps/v1" time="0"></testcase>
</testsuite>
</testsuites>

這邊簡易了使用其 kubeconform 操作,預設上環境常常可能會有非原始 Kubernetes 資源的 CRD,如果要做驗證的話可以 -ignore-missing-schemas 掉或是用 -schema-location 方式去做處理這部分可以參閱連結

Helm charts 整合 kube-score

對於驗證 yaml 完整性來說可能還不夠,如果想要進階的對物件資源進行驗證可以使用 kube-score 來實現,其輸出內容會是推薦可實踐的內容,可能相關於安全部分或是可靠性部分。同樣的可以將其導入 CI 流程來強化 charts 所轉譯物件是否夠強壯。

本實驗是用 ubuntu 環境直接安裝,如下:

$ wget https://github.com/zegl/kube-score/releases/download/v1.17.0/kube-score_1.17.0_linux_amd64.tar.gz
$ tar xf kube-score_1.17.0_linux_amd64.tar.gz
$ sudo mv kube-score /usr/local/bin

有興趣者可以藉由官方網站位置進行體驗。

這邊同樣的用 helm template 方式產生 yaml 資源,並透過 kube-score 進行掃描

$ helm template --skip-tests --kube-version v1.23.0 ./ -f ci/values-analysis.yaml | kube-score score --kubernetes-version "v1.23" -o ci -
[OK] release-name-web-app apps/v1/Deployment
[OK] release-name-web-app apps/v1/Deployment
[OK] release-name-web-app apps/v1/Deployment
[SKIPPED] release-name-web-app apps/v1/Deployment: Skipped because container-ephemeral-storage-request-equals-limit is ignored
[OK] release-name-web-app apps/v1/Deployment
[OK] release-name-web-app apps/v1/Deployment: Pod Topology Spread Constraints
[SKIPPED] release-name-web-app apps/v1/Deployment: Skipped because container-memory-requests-equal-limits is ignored
[CRITICAL] release-name-web-app apps/v1/Deployment: (web-app) Ephemeral Storage limit is not set
[SKIPPED] release-name-web-app apps/v1/Deployment: Skipped because container-ports-check is ignored
[CRITICAL] release-name-web-app apps/v1/Deployment: (web-app) The container is running with a low user ID
[CRITICAL] release-name-web-app apps/v1/Deployment: (web-app) The container running with a low group ID
[CRITICAL] release-name-web-app apps/v1/Deployment: (web-app) The pod has a container with a writable root filesystem
[SKIPPED] release-name-web-app apps/v1/Deployment: Skipped because container-seccomp-profile is ignored
[CRITICAL] release-name-web-app apps/v1/Deployment: (web-app) ImagePullPolicy is not set to Always
[OK] release-name-web-app apps/v1/Deployment
[CRITICAL] release-name-web-app apps/v1/Deployment: The pod does not have a matching NetworkPolicy
[OK] release-name-web-app apps/v1/Deployment
[SKIPPED] release-name-web-app apps/v1/Deployment: Skipped because container-resource-requests-equal-limits is ignored
[SKIPPED] release-name-web-app apps/v1/Deployment: Skipped because container-cpu-requests-equal-limits is ignored
[CRITICAL] release-name-web-app apps/v1/Deployment: (web-app) Image with latest tag
[OK] release-name-web-app apps/v1/Deployment
[OK] release-name-web-app apps/v1/Deployment
[CRITICAL] release-name-web-app apps/v1/Deployment: No matching PodDisruptionBudget was found
[WARNING] release-name-web-app apps/v1/Deployment: Deployment does not have a host podAntiAffinity set
[OK] release-name-web-app autoscaling/v2beta1/HorizontalPodAutoscaler
[OK] release-name-web-app autoscaling/v2beta1/HorizontalPodAutoscaler
[OK] release-name-web-app autoscaling/v2beta1/HorizontalPodAutoscaler
[OK] release-name-web-app networking.k8s.io/v1/Ingress
[OK] release-name-web-app networking.k8s.io/v1/Ingress
[OK] release-name-web-app networking.k8s.io/v1/Ingress
[OK] release-name-web-app v1/Service
[OK] release-name-web-app v1/Service
[OK] release-name-web-app v1/Service
[OK] release-name-web-app v1/Service

這邊發現一些 CRITICAL 訊息,這表示 kube-score 建議你要做像是不使用 latest 映像檔標籤或是希望有 PodDisruptionBudget 的物件等。在 CI 中出現了 CRITICAL 會是以不通過收場。如果假設在環境上沒有打算實現 PodDisruptionBudget 或是 NetworkPolicy 資源,我們可以透過 --disable-ignore-checks-annotations 或是 Kubernetes 中 annotations 欄位使用 kube-score/ignore 來實現必驗證的動作,官方提供的清單

這邊在 ci/values-analysis.yaml 多新增了以下的定義來決策 kube-score 要啟用哪些檢查或是關閉檢查,已關閉來說避開了 NetworkPolicysecurityContext 的 user 和 group ID 檢查。

deploymentAnnotations:
kube-score/ignore: pod-networkpolicy,container-security-context-user-group-id,container-security-context-readonlyrootfilesystem,container-ephemeral-storage-request-and-limit,deployment-has-poddisruptionbudget
kube-score/enable: container-ports-check

最後,在驗證一次沒有出現 CRITICAL 警告,就可以通過檢查。要關閉什麼檢查,這取決於團隊設計的架構,也不應當略過一些基本的檢查像是 securityContext 設定。

$ helm template --skip-tests --kube-version v1.23.0 ./ -f ci/values-analysis.yaml | kube-score score --kubernetes-version "v1.23" -o ci -
[OK] release-name-web-app apps/v1/Deployment
[OK] release-name-web-app apps/v1/Deployment
[OK] release-name-web-app apps/v1/Deployment
[SKIPPED] release-name-web-app apps/v1/Deployment: Skipped because container-resource-requests-equal-limits is ignored
[OK] release-name-web-app apps/v1/Deployment
[OK] release-name-web-app apps/v1/Deployment
[SKIPPED] release-name-web-app apps/v1/Deployment: Skipped because pod-networkpolicy is ignored
[SKIPPED] release-name-web-app apps/v1/Deployment: Skipped because container-security-context-user-group-id is ignored
[OK] release-name-web-app apps/v1/Deployment
[SKIPPED] release-name-web-app apps/v1/Deployment: Skipped because container-security-context-readonlyrootfilesystem is ignored
[SKIPPED] release-name-web-app apps/v1/Deployment: Skipped because container-seccomp-profile is ignored
[OK] release-name-web-app apps/v1/Deployment
[SKIPPED] release-name-web-app apps/v1/Deployment: Skipped because container-memory-requests-equal-limits is ignored
[SKIPPED] release-name-web-app apps/v1/Deployment: Skipped because container-ephemeral-storage-request-equals-limit is ignored
[OK] release-name-web-app apps/v1/Deployment
[OK] release-name-web-app apps/v1/Deployment: Pod Topology Spread Constraints
[SKIPPED] release-name-web-app apps/v1/Deployment: Skipped because container-cpu-requests-equal-limits is ignored
[OK] release-name-web-app apps/v1/Deployment
[SKIPPED] release-name-web-app apps/v1/Deployment: Skipped because container-ephemeral-storage-request-and-limit is ignored
[SKIPPED] release-name-web-app apps/v1/Deployment: Skipped because deployment-has-poddisruptionbudget is ignored
[WARNING] release-name-web-app apps/v1/Deployment: Deployment does not have a host podAntiAffinity set
[OK] release-name-web-app apps/v1/Deployment
[OK] release-name-web-app apps/v1/Deployment
[OK] release-name-web-app autoscaling/v2beta1/HorizontalPodAutoscaler
[OK] release-name-web-app autoscaling/v2beta1/HorizontalPodAutoscaler
[OK] release-name-web-app autoscaling/v2beta1/HorizontalPodAutoscaler
[OK] release-name-web-app networking.k8s.io/v1/Ingress
[OK] release-name-web-app networking.k8s.io/v1/Ingress
[OK] release-name-web-app networking.k8s.io/v1/Ingress
[OK] release-name-web-app v1/Service
[OK] release-name-web-app v1/Service
[OK] release-name-web-app v1/Service
[OK] release-name-web-app v1/Service

charts 單元測試

上面驗證了 chartsyaml 資源,如果想更要在貼近一點現實可以透過單元測試方式驗證轉譯的物件內容是不是你期望在 Kubernets 上運行的內容。這邊使用 helm-unittest 套件來進行單元測試。

他屬於 Helm 的套件,因此可以使用 helm plugin 方式安裝

$ helm plugin install https://github.com/helm-unittest/helm-unittest.git

一開始在 charts 的目錄下建立 tests 目錄,並把該目錄新增自 .helmignore 中。要觸發測試可以使用 helm unittest CHART_NAME下面是一個測試完成的結果,預設格式是 XUnit,這可以使用 -t 參數進行替換,可以是 JUnit、NUnit 或 XUnit。

$ helm unittest web-app/

### Chart [ web-app ] web-app/

PASS test cluster role web-app/tests/clusterrole_test.yaml
PASS test clusterrolebinding web-app/tests/clusterrolebinding_test.yaml
PASS test configMap web-app/tests/configmap_test.yaml
PASS test deployment web-app/tests/deployment_test.yaml
PASS test service web-app/tests/service_test.yaml
PASS test serviceAccount web-app/tests/serviceaccount_test.yaml

Charts: 1 passed, 1 total # 表示一個 charts 被測試
Test Suites: 6 passed, 6 total # 測試的場景有 6 個,通過 6 個
Tests: 6 passed, 6 total
Snapshot: 0 passed, 0 total
Time: 12.111921ms

如果要將結果輸出至 CI 的產物可以使用 -o FILE_NAME 方式,輸出的檔案會取決於在哪個目錄執行 helm unittest

下面是一個測試 deployment.yaml 的內容。在 asserts 中用了 equalisNotEmptycontains 等關鍵字,但重點在於 path 其就是用來對應真實部署資源的欄位。

  • path: 會是參照 Kubernetes 物件資源上內容,通常會精準指向某個物件中欄位。可以想像是透過 kubectl get 某物件然後使用 jsonpath 來自定義輸出。
  • value: 期望 path 上的值,但這通常用於 Kubernetes 上的欄位指對應一個值使用。
  • content: 類似於 path,但應用於多輸出的內容。
suite: test deployment
values: # 測試時要使用的 values yaml
- ../ci/values-test.yaml
templates: # 要被測試的模板
- templates/deployment.yaml
- templates/configmap.yaml
release: # 模擬 charts 部署時的內容
name: test-release
namespace: test
chart: # 模擬 charts 的版本和 app 版本
version: 0.1.0+test
appVersion: 1.16.0
tests:
- it: should pass all kinds of assertion
template: templates/deployment.yaml
documentIndex: 0
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/managed-by"]
value: Helm
- equal:
path: metadata.labels["app.kubernetes.io/name"]
value: web-app
- equal:
path: metadata.annotations["org.cch.com/owner"]
value: cch
- equal:
path: metadata.annotations["org.cch.com/organize"]
value: CCH Tech
- isNotEmpty:
path: spec.template.metadata.annotations["checksum/config"]
- equal:
path: spec.template.spec.containers[?(@.name == "web-app")].image
value: registry.hub.docker.com/cch0124/helm-demo:v2.2.9
- equal:
path: spec.template.spec.serviceAccountName
value: web
- matchRegex:
path: metadata.name
pattern: ^.*-web-app$
- contains:
path: spec.template.spec.containers[?(@.name == "web-app")].ports
content:
containerPort: 8080
protocol: TCP
name: http
- notContains:
path: spec.template.spec.containers[?(@.name == "web-app")].ports
content:
containerPort: 80
- isNotEmpty:
path: spec.template.spec.containers[?(@.name == "web-app")].livenessProbe
- isNotEmpty:
path: spec.template.spec.containers[?(@.name == "web-app")].readinessProbe
- isNotEmpty:
path: spec.template.spec.containers[?(@.name == "web-app")].resources
- isSubset:
path: spec.template.spec.containers[?(@.name == "web-app")].securityContext
content:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
allowPrivilegeEscalation: false
runAsUser: 185
runAsGroup: 185
- isNotEmpty:
path: spec.template.spec.containers[?(@.name == "web-app")].lifecycle
- isSubset:
path: spec.template.spec.securityContext
content:
fsGroup: 185
runAsNonRoot: true
- isKind:
of: Deployment
- isAPIVersion:
of: apps/v1

更多的細節可以參照 helm-unittest 官方文件

charts 整合 helm-docs 產生文件

是一個將 charts 中的資訊轉譯成 markdown 檔案的工具。生成的內容包含了 Chart.yaml 中的一些資訊或是 values.yaml 中定義的值內容,該內容會使用註解來進行解析。

轉譯 markdown 這流程是從 gotemplate 實作,相似於 charts 模板。預設上 helm-docs 有基本的樣式來做轉譯,當然也可以透過 README.md.gotmpl 檔案進行轉譯內容的定義。

# {{ template "chart.name" . }}

![Version: {{ .Version }}](https://img.shields.io/badge/Version-{{ .Version | replace "-" "--" }}-informational?style=for-the-badge)
{{ if .Type }}![Type: {{ .Type }}](https://img.shields.io/badge/Type-{{ .Type }}-informational?style=for-the-badge) {{ end }}
{{ if .AppVersion }}![AppVersion: {{ .AppVersion }}](https://img.shields.io/badge/AppVersion-{{ .AppVersion | replace "-" "--" }}-informational?style=for-the-badge) {{ end }}

## {{ template "chart.name" . }} Chart Information


[Homepage]({{ template "chart.homepage" . }})

{{ template "chart.sourcesSection" . }}

{{ template "chart.maintainersSection" . }}


## Description

{{ template "chart.description" . }}


{{ template "chart.requirementsSection" . }}


{{ template "chart.valuesSection" . }}

helm-docs 的指令允許用 -t 指定模板檔案或是 -f 指定到 charts 中預設的 values.yaml 等,下方為指令所提供的參數。

$ helm-docs --help
helm-docs automatically generates markdown documentation for helm charts from requirements and values files

Usage:
helm-docs [flags]

Flags:
-b, --badge-style string badge style to use for charts (default "flat-square")
-c, --chart-search-root string directory to search recursively within for charts (default ".")
-g, --chart-to-generate strings List of charts that will have documentation generated. Comma separated, no space. Empty list - generate for all charts in chart-search-root
-u, --document-dependency-values For charts with dependencies, include the dependency values in the chart values documentation
-d, --dry-run don't actually render any markdown files just print to stdout passed
-h, --help help for helm-docs
-i, --ignore-file string The filename to use as an ignore file to exclude chart directories (default ".helmdocsignore")
--ignore-non-descriptions ignore values without a comment, this values will not be included in the README
-l, --log-level string Level of logs that should printed, one of (panic, fatal, error, warning, info, debug, trace) (default "info")
-o, --output-file string markdown file path relative to each chart directory to which rendered documentation will be written (default "README.md")
-s, --sort-values-order string order in which to sort the values table ("alphanum" or "file") (default "alphanum")
-t, --template-files strings gotemplate file paths relative to each chart directory from which documentation will be generated (default [README.md.gotmpl])
-f, --values-file string Path to values file (default "values.yaml")
--version version for helm-docs

helm-docs 中可定義的轉譯模板可參考此官方連結,對於轉譯來說比較重要的參數是 --chart-search-root--template-files 如果其提供的預設值不符合場景設計可以進行靈活上的調整。

下面是定義後的 READEM.md.gotmpl,使用上比較麻煩的部分就是 gotemplate 的語法,其它基本上看官方的文檔內容就可以組合出自己想要的格式內容。

# {{ template "chart.name" . }}

![Version: {{ .Version }}](https://img.shields.io/badge/Version-{{ .Version | replace "-" "--" }}-informational?style=for-the-badge)
{{ if .Type }}![Type: {{ .Type }}](https://img.shields.io/badge/Type-{{ .Type }}-informational?style=for-the-badge) {{ end }}
{{ if .AppVersion }}![AppVersion: {{ .AppVersion }}](https://img.shields.io/badge/AppVersion-{{ .AppVersion | replace "-" "--" }}-informational?style=for-the-badge) {{ end }}

## {{ template "chart.name" . }} Chart Information


[Homepage]({{ template "chart.homepage" . }})

{{ template "chart.sourcesSection" . }}

{{ template "chart.maintainersSection" . }}


## Description

{{ template "chart.description" . }}


{{ template "chart.requirementsSection" . }}


{{ template "chart.valuesSection" . }}

上述的 READEM.md.gotmpl 比較重要的內容是 values.yaml 中所定義的值,因為這是給使用或是管理 charts 的人來做一個依據的參考。雖然透過 chart.valuesSection 模板來解析 values.yaml 內容,但如過沒針對 values.yaml 進行處理,則該模板也是沒意義的。那要如何定義有價值的內容呢,下面就來進行說明吧。

image 結構來說,在 values.yaml 中定義了以下內容

image:
# -- Image registry
repository: registry.hub.docker.com/cch0124/helm-demo
# -- Image pull policy
pullPolicy: IfNotPresent
# -- Overrides the image tag whose default is the chart appVersion.
tag: "latest"

該內容上如果要讓欄位有描述,則可以在該欄位上以 # -- 開頭並向後填寫要描述的註解,最後轉譯結果會如下

KeyTypeDefaultDescription
image.pullPolicystring"IfNotPresent"Image pull policy
image.repositorystring"registry.hub.docker.com/cch0124/helm-demo"Image registry
image.tagstring"latest"Overrides the image tag whose default is the chart appVersion.

如果在 values.yaml 是沒有撰寫註解格式,則最後葉節點會被轉譯出來,但是描述部分就有些許空虛了。

rbac:
create: true

轉譯結果:

KeyTypeDefaultDescription
rbac.createbooltrue

如果在 values.yaml 的根節點欄位撰寫註解,則葉節點不會被轉譯,會以根節點物件呈現。

# -- Resource requests and limits for the application. Ref [kubernetes doc manage-resources-containers](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)
resources:
limits:
cpu: 100m
memory: 256Mi
requests:
cpu: 10m
memory: 256Mi

轉譯結果:

KeyTypeDefaultDescription
resourcesobject{"limits":{"cpu":"100m","memory":"256Mi"},"requests":{"cpu":"10m","memory":"256Mi"}}Resource requests and limits for the application. Ref kubernetes doc manage-resources-containers

如果在 values.yaml 的根節點和葉節點定義註解如下,則葉節點有被註解會被轉譯,否則不會。

# -- Configure the healthcheck for the application
livenessProbe:
httpGet:
# -- This is the liveness check endpoint. Can not overrid.
path: /q/health/live
port: http
scheme: HTTP
# @default -- Number of seconds after the container has started before startup, liveness or readiness probes are initiated.
initialDelaySeconds: 60
# @default -- How often (in seconds) to perform the probe.
periodSeconds: 30
# @default -- Minimum consecutive successes for the probe to be considered successful after having failed.
successThreshold: 1
# @default -- Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1.
timeoutSeconds: 10

轉譯結果:

KeyTypeDefaultDescription
livenessProbeobject{"httpGet":{"path":"/q/health/live","port":"http","scheme":"HTTP"},"initialDelaySeconds":60,"periodSeconds":30,"successThreshold":1,"timeoutSeconds":10}Configure the healthcheck for the application
livenessProbe.httpGet.pathstring"/q/health/live"This is the liveness check endpoint. Can not overrid.

其還提供 @default、和 @ignored 等選項

  • @default 如果不想包含在 values.yaml 中的預設值,或是一些計算的值可以使用
  • @ignored 忽略某些值

整體來說你可以使用 helm-docs 產生一個可讀性的文件檔案,在某種程度上可以協助部署或是交接。但具體要如何呈現想要的內容則取決於團隊的共識。本文章範例的結果可以至此連結

打包 charts 至 DockerHub

打包 charts

切換目錄至 charts 並使用 helm packagecharts 進行打包,執行後當前目錄會有一個 CHART_NAME-VERSION.tgz 檔案。

$ helm package --version 0.0.1 ./

登入 Docker Hub 並推送

登入部分使用 Docker Hub 的 Personal Access Token (PAT) 進行,也推薦使用此方式。

$ export PAT=PERSONAL_ACCESS_TOKEN
$ export USERNAME=LOGIN_USERNAME
$ echo $PAT | docker login -u $USERNAME --password-stdin

推送 charts 至 Docker Hub,使用 helm push 並指定要推送的 charts 和推送目標。

$ helm push web-app-0.0.1.tgz oci://registry-1.docker.io/cch0124
Pushed: registry-1.docker.io/cch0124/web-app:0.0.1
Digest: sha256:10b324b17dc620974f715fd44e6743dbd068a6e0487d496a76d2781c81184199

部署與操作 Helm charts

部署

上個章節已經將 charts 推送至 Docker Hub 接著將使用它來拉取 charts 並部署至模擬的 Kubernetes 環境。

部署之前,可以藉由 helm show 來進行一些資訊查看。

# 查看 charts 所有訊息
$ helm show all oci://registry-1.docker.io/cch0124/web-app --version 0.0.1

直接進行部署,透過 helm install,預設為 defaultnamespace,下面資訊為部署後資料。

$ helm install web-app oci://registry-1.docker.io/cch0124/web-app --version 0.0.1
Pulled: registry-1.docker.io/cch0124/web-app:0.0.1
Digest: sha256:10b324b17dc620974f715fd44e6743dbd068a6e0487d496a76d2781c81184199
NAME: web-app
LAST DEPLOYED: Mon Sep 11 20:57:10 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
1. Get the application URL by running these commands:
export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=web-app,app.kubernetes.io/instance=web-app" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT

驗證部署結果與應用程式測試 API。

  1. 驗證部署

對於 Helm 來說它是有 namespace 概念,因此在操作時須帶 namespace,從 STATUS 欄位可以看到是成功部署的。

$ helm list 
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
web-app default 1 2023-09-11 20:57:10.735141112 +0800 CST deployed web-app-0.0.1 1.16.0

使用 kubectl get 資源驗證是否有正常運行。

$ kubectl get pods 
NAME READY STATUS RESTARTS AGE
web-app-69d997c6ff-wvbwd 1/1 Running 0 3m18s
  1. 測試 API
$ kubectl run mycurlpod --image=curlimages/curl -i --tty -- sh
$ curl -XGET http://web-app.default.svc.cluster.local:8080/hello
Hello from RESTEasy Reactive

更新 Helm chart

上個步驟在部署時,沒有部署到 ingress,這時使用 helm upgrade 進行。

下面為一個錯誤的更新,導致最後 STATUSfailed

$ helm upgrade web-app oci://registry-1.docker.io/cch0124/web-app --version 0.0.1 --set ingress.enabled=true --set ingress.hosts[0].host=helm-demo.cch.com --set ingress.hosts[0].paths[0].pathType=Prefix
Pulled: registry-1.docker.io/cch0124/web-app:0.0.1
Digest: sha256:10b324b17dc620974f715fd44e6743dbd068a6e0487d496a76d2781c81184199
Error: UPGRADE FAILED: failed to create resource: Ingress.extensions "web-app" is invalid: spec.rules[0].http.paths[0].path: Invalid value: "": must be an absolute path
$ helm list
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
web-app default 2 2023-09-11 21:18:27.328839062 +0800 CST failed web-app-0.0.1 1.16.0

修正後,已經成功更新一個版本,REVISION 變為 3,ingress 資源也被更新部署。在做更新時也推薦使用 --atomic 參數,這可幫助 charts 更新失敗後可以進行退回上一個版本。這邊使用的是 --set 方式同時也是可以使用 -f 來指定要轉譯模板的值,以優先順序來說 --set 為最高。

$ helm upgrade web-app oci://registry-1.docker.io/cch0124/web-app --version 0.0.1 --set ingress.enabled=true --set ingress.hosts[0].host=helm-demo.cch.com --set ingress.hosts[0].paths[0].pathType=Prefix --set ingress.hosts[0].paths[0].path=/ --atomic
Pulled: registry-1.docker.io/cch0124/web-app:0.0.1
Digest: sha256:10b324b17dc620974f715fd44e6743dbd068a6e0487d496a76d2781c81184199
Release "web-app" has been upgraded. Happy Helming!
NAME: web-app
LAST DEPLOYED: Mon Sep 11 21:21:14 2023
NAMESPACE: default
STATUS: deployed
REVISION: 3
TEST SUITE: None
NOTES:
1. Get the application URL by running these commands:
http://helm-demo.cch.com/

更新後所建立的 ingress。同時在外部可以透過 curl 存取服務。

$ kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
web-app traefik helm-demo.cch.com 172.20.0.2,172.20.0.3 80 84s
$ curl http://helm-demo.cch.com:8080/cluster/namespace
["default","kube-system","kube-public","kube-node-lease"]

回滾 charts

上個步驟暴露了 API 至 Kubernetes 集群外,如果想要讓其回到第一次無 ingress 狀態,可以使用 helm rollback

$ helm rollback web-app 
1 (App: 1.16.0, Chart: web-app-0.0.1) 2 (App: 1.16.0, Chart: web-app-0.0.1) 3 (App: 1.16.0, Chart: web-app-0.0.1)

此時會多一個版本,詳細的資訊使用 helm history,且 ingress 也回到第一版本狀態。

$ helm history web-app
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 Mon Sep 11 20:57:10 2023 superseded web-app-0.0.1 1.16.0 Install complete
2 Mon Sep 11 21:18:27 2023 failed web-app-0.0.1 1.16.0 Upgrade "web-app" failed: failed to create resource: Ingress.extensions "web-app" is invalid: spec.rules[0].http.paths[0].path: Invalid value: "": must be an absolute path
3 Mon Sep 11 21:21:14 2023 superseded web-app-0.0.1 1.16.0 Upgrade complete
4 Mon Sep 11 21:31:32 2023 deployed web-app-0.0.1 1.16.0 Rollback to 1

驗證結果,如下:

$ kubectl get ingress
No resources found in default namespace.
$ kubectl exec mycurlpod -it /bin/sh
~ $ curl http://web-app.default.svc.cluster.local:8080/pod/list/name?namespace=default
["web-app-69d997c6ff-wvbwd","mycurlpod"]
~ $ curl http://web-app.default.svc.cluster.local:8080/pod/list/name?namespace=kube-system
["helm-install-traefik-crd-jgf8d","helm-install-traefik-hpt7l","svclb-traefik-c047adea-n9grs","traefik-6468fd4675-v56h5","svclb-traefik-c047adea-8phnq","local-path-provisioner-5f8bbd68f9-wcptd","coredns-5cfbb9f57c-ljsrl","metrics-server-d8ccb79c9-fcvc9"]

最後補充 .helmignore,將 charts 打包時請考慮把敏感資料忽略,避免機密外洩。

到這邊認識了 Helm 的概念:

  • Chart 一個可以部署在 K8s Cluster 的檔案
  • Template 組成 Kubernetes 資源
  • Values Kubernetes 資源參數化
  • Release Kubernetes Cluster 中部署的包
  • Push 將 Chart 檔丟置倉庫
  • Install 安裝一個 charts
  • Upgrade 更改 Kubernetes 集群中的現有版本
  • Rollback 回到以前部署的某一版本

總結

Helm 是一個可以解決 Kubernetes 部署資源的管理工具,但對於 charts 的模板在撰寫時學習曲線會比較高一些,畢竟要了解 gotemplate。而 charts 也應該被驗證或是掃描來提高可用性和安全性,來驗證你做了什麼和保護 charts。而 charts 另一個重要的是文件,有好的文件可以使得管理上更加輕鬆。此文章所提到的工具也不是唯一,每個團隊的使用都有自己選擇,但最後的目的都是要保護 charts

另一方面,charts 可以被版控,也可以針對每個環境使用不同的 values.yaml 來做轉譯,減少了管理多環境的資源,同時對於現今的 GitOps 概念來說可以做一個很好的整合。

- + \ No newline at end of file diff --git a/docs/self-paced-labs/tool_compare_lab/index.html b/docs/self-paced-labs/tool_compare_lab/index.html index e63e2a9..a82fd11 100644 --- a/docs/self-paced-labs/tool_compare_lab/index.html +++ b/docs/self-paced-labs/tool_compare_lab/index.html @@ -10,7 +10,7 @@ - + @@ -20,7 +20,7 @@ docker build -t <image-name>:<version> . --no-cache

DinD

首先運行一個 dind container,並將 Dockerfile 等內容 mount 到 container 內,並開 shell 進到 dind container 中,再 build image,同樣比較有無 layer caching,此處使用 --cache-from 前面 build 好的 image 以加速 build 流程。

$ docker run -v $PWD:/workspace --privileged -d --name dind-test docker:dind
$ docker exec -it dind-test /bin/sh
$ cd workspace
$ docker build -t <image-name>:<version> .

# build image with cache
$ docker build -t <pre-existing-image-name>:<version> . --cache-from <image-name>:<version>

Buildkit

啟用 Buildkit 後,再下跟 docker build 一樣的指令即可,如果有下載 Docker Desktop,則預設就會啟用

$ DOCKER_BUILDKIT=1 docker build .

Buildah

先單獨安裝 Buildah,再下 buildah bud 即可開始 build image

# 安裝 Buildah
$ sudo apt install buildah
# build image
$ buildah bud -t <image-name>:<version> .
# build image with layers cache
$ buildah bud -t <image-name>:<version> --layers .
# 列出 build 好的 image
$ buildah images

Kaniko

使用 Docker 運行 kaniko v1.8.1 的 container,並將 build 好的 image 推到 Dockerhub 上

$ docker run -v $PWD:/workspace \
-v ~/.docker/config.json:/kaniko/config.json \
--env DOCKER_CONFIG=/kaniko gcr.io/kaniko-project/executor:v1.8.1 \
-d <username>/<image-name>:<version>

測試結果

CPU Usage

CPU 資源使用量的部分,如上面 lab 區塊提到,看的檔案是 cpu usagecpu usage 為 lab 使用的環境:vCPU*4 的資源使用總和,且 build 過程中皆無使用 cache,比較結果如下:

  • 平均/最大/最小使用資源,單位為毫秒 (millisecond) CPU Usage

從上圖可以看到 Kaniko、Buildah 使用的資源低於 Docker,且略勝 Buildkit,以平均使用量來排序: Docker>Dind>Buildkit>Kaniko>Buildah

Memory

比較所使用的 memory,並分為 build 過程是否有使用 cache,至於 kaniko 的部分因目前尚未實作 local layer caching,因此此處僅計算沒有 cache 的 memory 使用部分。

  • 平均/最大/最小使用資源,單位為 Mib

  • 有 cache

memory Usage

  • 沒有 cache

memory Usage

從上面兩張圖可以看到在有 cache 的情況下,Dind 的記憶體使用率明顯高於其他 build tool,排除 Dind,使用率平均最低的則是 Buildah 接著是 Kaniko。但在沒有 cache 的情況下,Buildah 卻遠高於其他的 build tool。

Build Time

Build time 的部分同樣比較有無使用 cache 的情況。

在有使用到 build cache 的情況下,Docker/ BuildKit/ Kaniko 都是在一秒內 build 完,Buildah 則是平均 6 秒,因此此處並無製作圖表,僅製作沒有 cache 的情況下花費的時間做比較。

  • 單位為秒

build time

Docker cache 機制

Dockerfile 中的每條指令可視為一個 layer,在 build 的過程中會比較 Layer 與之前是否相同,若相同在使用 docker build 時會顯示 Using cache 以及使用了哪個 cache,cahce 則會存在 /var/lib/docker/image/overlay2/imagedb/content/sha256 底下,若是使用 BuildKit,則會顯示 CACHED,並拿之前的 build cache 來用,可以大幅減少 build image 的時間,若是 Dockerfile 內容有更改或是使用到的檔案內容有更改就不用使用到 cache,而是會重新 build layer,且後續的 layer 也會需要重新 build,因此在撰寫 Dockerfile 時可以將比較常更改的內容放在 Dockerfile 尾端。

Image Size

在 image size 的部分則可以發現差別並不大

  • 單位為 MB

image size

各項工具比較

DinDKanikoBuildahBuildKit
Run without privileged modeXOOO
Local layer cachingXXOO
Remote layer cachingOOOO
Need dockerfileOOXO

小結

在此次 lab 中,比較了不同 build tool 的資源使用量以及 build 出來的 image size 以及花費的時間,可以發現每個 build tool 各有優缺,也有一些 tool 的資源使用量與想像的有所不同,但無可否認的是這些工具發展十分迅速,在找資料時也發現有些原本被開發人員詬病的特性已經被修掉了,容器安全性的疑慮也有了其他解法,因此,最佳解仍然是根據開發及維運人員的環境及需求去選擇適合的工具。

Reference

- + \ No newline at end of file diff --git a/docs/self-paced-labs/vault/index.html b/docs/self-paced-labs/vault/index.html index 13fe95c..c5de63f 100644 --- a/docs/self-paced-labs/vault/index.html +++ b/docs/self-paced-labs/vault/index.html @@ -10,13 +10,13 @@ - +

Quarkus 整合 Vault KV 引擎

備註

本篇文章內容與實驗會同步 CCH0124 - vault-demo

HashiCorp Vault 屬於 CNCF 的 Key Management 類別的一員。

現今服務開發對於應用程式存取機密性資料相對都是難免,無論是在原始碼、配置檔或是其它位置。如果沒有一個機制來控管授權,想必是相當的可怕隨時都有洩漏危機。透過 Vault 可以集中管理這些機密資源,應用程式或是使用者要存取機密資源都需經過身分驗證來獲取相對應訪問資源,這樣可減少不必要的洩漏。

本文章會學習到以下

  • 安裝 Vault
  • 使用 Vault Client 交互
  • 使用 KV 引擎
  • 設置 Vault Auth (Token/Kubernetes)
  • 設置 Vault Policy
  • Quarkus 框架整合 Vault KV 引擎

實驗環境

  • OS: Windows 11 WSL
  • Docker Engine: 23.0.5
  • k3d version: v5.4.9
  • kubectl version: v1.27.1
  • Helm version: v3.11.3
  • Vault chart version: 0.25.0

環境安裝與配置

  1. install docker
  2. install kubectl
  3. install k3d

安裝完 k3d 後,使用下面 k3d 配置檔案建立一個 Kubernetes 環境 :

## vault-conf.yaml
apiVersion: k3d.io/v1alpha4 # this will change in the future as we make everything more stable
kind: Simple # internally, we also have a Cluster config, which is not yet available externally
metadata:
name: vault-cluster # name that you want to give to your cluster (will still be prefixed with `k3d-`)
servers: 1 # same as `--servers 1`
agents: 2 # same as `--agents 2`
kubeAPI: # same as `--api-port myhost.my.domain:6445` (where the name would resolve to 127.0.0.1)
host: "vault.cch.com" # important for the `server` setting in the kubeconfig
hostIP: "127.0.0.1" # where the Kubernetes API will be listening on
hostPort: "6450"
image: rancher/k3s:v1.23.14-k3s1
network: argo-net
ports:
- port: 8050:80
nodeFilters:
- loadbalancer
- port: 8453:443
nodeFilters:
- loadbalancer
options:
k3s:
extraArgs:
- arg: --disable=traefik
nodeFilters:
- server:*

使用 k3d 命令工具安裝模擬的 Kubernetes 環境,配置檔指向上述定義的檔案

$ k3d cluster create --config=vault-conf.yaml

當 Kubernetes 環境被建立之後,透過 kubectl config 指令可以看到被建立的環境如下

$ kubectl config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
* k3d-vault-cluster k3d-vault-cluster admin@k3d-vault-cluster
  1. Install Helm
  2. Install Nginx ingress controller:

k3d 上安裝 Kubernetes 時關閉了 traefik 選項,所以這邊用 ingress-nginx 取代,並使用 Helm chart 安裝,安裝步驟如下:

$ helm repo add ingress-nginx  https://kubernetes.github.io/ingress-nginx
$ helm search repo ingress-nginx
$ helm install ingress-nginx ingress-nginx/ingress-nginx --version 4.6.0 --namespace ingress-nginx --create-namespace
$ kubectl get all -n ingress-nginx
NAME READY STATUS RESTARTS AGE
pod/ingress-nginx-controller-7d5fb757db-gx8hp 1/1 Running 0 2m36s

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/ingress-nginx-controller-admission ClusterIP 10.43.138.141 <none> 443/TCP 2m36s
service/ingress-nginx-controller LoadBalancer 10.43.236.109 172.19.0.2,172.19.0.3,172.19.0.4 80:32369/TCP,443:31027/TCP 2m36s

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/ingress-nginx-controller 1/1 1 1 2m36s

NAME DESIRED CURRENT READY AGE
replicaset.apps/ingress-nginx-controller-7d5fb757db 1 1 1 2m36s
  1. Install Vault:

使用 Helm chart 安裝

helm repo add hashicorp https://helm.releases.hashicorp.com
helm search repo hashicorp/vault
quarkus-demo/vault-demo$ helm install vault hashicorp/vault --version 0.25.0 --namespace vault --create-namespace -f values/vault/values.yaml

關於 Vault 的 values.yaml 安裝配置可參考我的鏈結

安裝完之後必須作初始化和解封的動作,流程如下:

# 初始化
$ kubectl --namespace vault exec -it vault-0 -- vault operator init
Unseal Key 1: rie1qIyzUi7hBPNkHPgI5FAni67Dwd8or8F1pK5BHGHP
Unseal Key 2: V1JTIz+PZOJRpWen8eGBu3a/f70rurWh57gF7prZTy1+
Unseal Key 3: sbYXRQ74xQJmEC8JjoL6i5W2SzydEK81sI7fyUTX1QFW
Unseal Key 4: 4cAAitfZP5wOy6Dv/9poBuPa8r6HjJ11OYNgDcVUiFMz
Unseal Key 5: kkEnDx8VPY3IzHMIY2zF+DntuH8W+ZhelDlK7Z6e7VIq

Initial Root Token: hvs.3nqeHIjaG8aIMZmmBuOPSlM4

Vault initialized with 5 key shares and a key threshold of 3. Please securely
distribute the key shares printed above. When the Vault is re-sealed,
restarted, or stopped, you must supply at least 3 of these keys to unseal it
before it can start servicing requests.

Vault does not store the generated root key. Without at least 3 keys to
reconstruct the root key, Vault will remain permanently sealed!

It is possible to generate new unseal keys, provided you have a quorum of
existing unseal keys shares. See "vault operator rekey" for more information.
# 解封,預設上至少選三個 Unseal Key
$ kubectl --namespace vault exec -ti vault-0 -- vault operator unseal rie1qIyzUi7hBPNkHPgI5FAni67Dwd8or8F1pK5BHGHP
$ kubectl --namespace vault exec -ti vault-0 -- vault operator unseal sbYXRQ74xQJmEC8JjoL6i5W2SzydEK81sI7fyUTX1QFW
$ kubectl --namespace vault exec -ti vault-0 -- vault operator unseal kkEnDx8VPY3IzHMIY2zF+DntuH8W+ZhelDlK7Z6e7VIq
  1. Install Vault client

要安裝的原因是不想使用 kubectl exec 方式進行一些配置,因此交互會從 windows WSL 環境為請求端,再透過 ingress 的入口進行存取。

在 WSL 環境中,需定義兩個環境變數 VAULT_ADDRVAULT_SKIP_VERIFY

export VAULT_ADDR=https://vault-demo.cch.com:8453
export VAULT_SKIP_VERIFY=true
  • VAULT_ADDR 這邊指向 Ingress 入口位置
  • VAULT_SKIP_VERIFY 因為沒有完整的 SSL,所以這邊跳過驗證,但實務上不建議

接著透過 vault login 登入,這邊登入的 token 是使用上個步驟的初始 Vault 時產生的 Root Token。

$ vault login -tls-skip-verify hvs.3nqeHIjaG8aIMZmmBuOPSlM4 

這邊必須要能夠登入,才能往下進行。

KV secrets engine - version 2

在啟用版本 2 的 KV 引擎時,必須透過 secret 引擎啟用。secret 引擎是一個儲存、生成或是加密數據的組件。版本 2 的 KV 引擎是一個可存取 key/value 對的功能,並且附帶過去每次異動版本歷程,因此舊的配置是可以被檢索的,下圖為官方提供的圖。

對於應用程式存取而言,必須要先啟用 KV 功能。

$ vault secrets enable -version=2 kv
Success! Enabled the kv secrets engine at: kv/

透過 list 指令可以列出我們創建的 KV 路徑引擎

$ vault secrets list
Path Type Accessor Description
---- ---- -------- -----------
cubbyhole/ cubbyhole cubbyhole_293822d8 per-token private secret storage
identity/ identity identity_5bba97e7 identity store
kv/ kv kv_cd1f8bda n/a # 這是被啟用的 KV 引擎,路徑是 kv
sys/ system system_6e9e02ef system endpoints used for control, policy and debugging

建立給應用程式的 KV 對

Quarkus 範例會定義 mqtt.brokermqtt.usernamemqtt.password 三個環境變數,最後目標是可以透過 API 介面來獲取從 Vault 儲存的值。

下面使用 kv put 將值定義至 Vault 中。在 Vault 的 KV 引擎中,它類似以資料夾結構為概念去建立路徑。在此範例中,會在 kv 路徑下建立一個 quarkus/vault-demo 路徑,並塞入我們要給應用程式的值。

$ vault kv put kv/quarkus/vault-demo mqtt.broker="tcp://localhost:8883" mqtt.username="demo" mqtt.password="demo1234"
======= Secret Path =======
kv/data/quarkus/vault-demo

======= Metadata =======
Key Value
--- -----
created_time 2023-08-12T14:33:34.390315381Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1

建立後,可以透過 kv get 來獲取某路徑下已經定義的鍵值對。version 為 1,是因為我們推送了一次值。

$ vault kv get -mount=kv quarkus/vault-demo
======= Secret Path =======
kv/data/quarkus/vault-demo

======= Metadata =======
Key Value
--- -----
created_time 2023-08-12T14:33:34.390315381Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1 # 上步驟推資料所以異動一次
# 下面為推送的鍵值對
======== Data ========
Key Value
--- -----
mqtt.broker tcp://localhost:8883
mqtt.password demo1234
mqtt.username demo

如果資料定義錯誤想要做修正可以使用 kv patch 指令,假設 mqtt.password 設定錯誤要換成 demo12345678。這時又對資料進行了異動因此 version

$ vault kv patch -mount=kv quarkus/vault-demo mqtt.password=demo12345678
======= Secret Path =======
kv/data/quarkus/vault-demo

======= Metadata =======
Key Value
--- -----
created_time 2023-08-12T14:41:39.647900006Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 2 # 此時更新密碼因此版本在被遞增

更新後,再使用 kv get 時該 mqtt.password 應當是被置換過值。

檢索特定版本

如果想查看過去異動,或是更新前的查看都可以使用 version 來進行。在前面的過程中版本已經遞增至 2。如果想回頭看 1 的版本,可以如下

$ vault kv get -version=1 -mount=kv quarkus/vault-demo
======= Secret Path =======
kv/data/quarkus/vault-demo

======= Metadata =======
Key Value
--- -----
created_time 2023-08-12T14:33:34.390315381Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1

======== Data ========
Key Value
--- -----
mqtt.broker tcp://localhost:8883
mqtt.password demo1234
mqtt.username demo

預設上版本 2 的 KV 引擎 Maximum number of versions 是未設定,如下使用 read 指令查詢可獲取其配置。但可以依照需求去做調整。

$ vault read kv/config
Key Value
--- -----
cas_required false
delete_version_after 0s
max_versions 0

刪除與復原版本

對於 Vault 中 delete 動作是軟刪除,該版本會被標記是刪除且定義一個 deletion_time 時戳,這可用於 undelete 操作基本上可以認知成回滾。當 KV 版本超過 max-versions 設置的數量時,或者使用 destroy 操作時,版本的數據才會被永久刪除。

刪除版本 1

$ vault kv delete -versions=1 -mount=kv quarkus/vault-demo
Success! Data deleted (if it existed) at: kv/data/quarkus/vault-demo

刪除後,使用 metadata 操作獲取詳細版本資訊。Version 1 刪除後被定義了 deletion_time,表示不會被立即移除。此時注意 Version 2 因為沒做 delete 動作因此 deletion_time 沒被定義。

$ vault kv metadata get  -mount=kv quarkus/vault-demo
======== Metadata Path ========
kv/metadata/quarkus/vault-demo

========== Metadata ==========
Key Value
--- -----
cas_required false
created_time 2023-08-12T14:33:34.390315381Z
current_version 2
custom_metadata <nil>
delete_version_after 0s
max_versions 0
oldest_version 0
updated_time 2023-08-12T14:41:39.647900006Z

====== Version 1 ======
Key Value
--- -----
created_time 2023-08-12T14:33:34.390315381Z
deletion_time 2023-08-12T17:12:50.563260118Z # deletion_time 被定義,表示不會被立即移除
destroyed false

====== Version 2 ======
Key Value
--- -----
created_time 2023-08-12T14:41:39.647900006Z
deletion_time n/a
destroyed false

嘗試復原版本 1,注意到 deletion_time 欄位時戳被設置為 n/a

$ vault kv undelete -versions=1 -mount=kv quarkus/vault-demo
Success! Data written to: kv/undelete/quarkus/vault-demo
$ vault kv metadata get -mount=kv quarkus/vault-demo
======== Metadata Path ========
kv/metadata/quarkus/vault-demo
...
====== Version 1 ======
Key Value
--- -----
created_time 2023-08-12T14:33:34.390315381Z
deletion_time n/a # 要被刪除時間已經設回原始
destroyed false

...

至於 destroy 操作,不接續展示基本上是會直接將指定版本資源刪除,不做任何標記。

定義訪問 quarkus/vault-demo 的角色

以此範例來說應用程式只想要讀取 quarkus/vault-demo 這個路徑下的資源,因此要給予適當權限,同時減少攻擊面。下面定義了一個政策的檔案,針對這 kv/data/quarkus/vault-demo 路徑只給予讀(read)權限。

# policy.hcl
path "kv/data/quarkus/vault-demo" {
capabilities = ["read"]
}

如果將 path 定義 kv/data/quarkus/+ 其實也是可以的,就看應用方面到哪裡。+ 表示當下同層路徑都可存取

  • kv/data/quarkus/a
  • kv/data/quarkus/b

但是下面是無法被存取

  • kv/data/quarkus/a/a

將上述 policy.hcl 寫入 Vault 中,透過 policy write 指令。建立之後,透過 policy list 可以查看是否被建立。

$ vault policy write vault policy.hcl
Success! Uploaded policy: vault
$ vault policy list
default
vault # 建立的 policy 名稱是 vault
root

當然這樣定義了權限還不夠,因為它沒被綁定到某個主題可能是一個 token 服務或是 user/pass 服務。截至當前都是使用 roottoken 做操作,應用程式的存取不應該使用 root 權限進行操作,因此需要再定義一個屬於應用程式存取的 token

透過 token create 建立一個 token 如下,並賦予該有的權限,時效可用 ttl 設定。token lookup 可以查看該 token 內容,像是過期時間(expire_time)、可存取的政策(policies)等等。-policy 綁定了上步驟被定義為 vault 的政策,應用程式範例將使用此 token 進行存取。

$ vault token create -policy=vault --ttl=768h
Key Value
--- -----
token hvs.CAESIIDXLAV0-_yh1VJjVQkDxojDEP54lTfT0Y-UsVEBJKBhGh4KHGh2cy5lRFRMeFpBSmpGMGo2ZXRNR0RsTDh3UmI
token_accessor KqbfPMrF6pmmFLb4n7E1niob
token_duration 768h
token_renewable true
token_policies ["default" "vault"]
identity_policies []
policies ["default" "vault"]

$ vault token lookup -accessor KqbfPMrF6pmmFLb4n7E1niob
Key Value
--- -----
accessor KqbfPMrF6pmmFLb4n7E1niob
creation_time 1692449442
creation_ttl 768h
display_name token
entity_id n/a
expire_time 2023-09-20T12:50:42.985961541Z
explicit_max_ttl 0s
id n/a
issue_time 2023-08-19T12:50:42.985964311Z
meta <nil>
num_uses 0
orphan false
path auth/token/create
policies [default vault]
renewable true
ttl 767h59m36s
type service

對於 token 預設系統 TTL 值是 768h 如下。

$ vault read sys/auth/token/tune
Key Value
--- -----
default_lease_ttl 768h
description token based credentials
force_no_cache false
max_lease_ttl 768h
token_type default-service

要針對 TTL 進行調整可以使用以下方式,這邊要調整的話當下登入者必須有足夠權限。

$ vault write sys/auth/token/tune default_lease_ttl=8h max_lease_ttl=720h

Quarkus 整合 Vault

上面章節透過 Vault 的 KV 給服務建立一個可存放機密物件的資源並將重要的資源存放至該路徑上。同時間,也建立了一個 policy 資源並將其和 token 資源綁在一起給應用程式做存取。

此章節會透過 Quarkus 框架與 Vault 進行一個整合範例。會分成兩部分來說明

  1. 地端開發
  2. 佈署至 Kubernetes 環境

Quarkus 使用 io.quarkiverse.vault:quarkus-vault 第三方套件整合 Vault,接續將會實作整合部分。範例中簡單的使用外部環境變數,如下

mqtt.broker=
mqtt.username=
mqtt.password=

這些值在上面章節已經完成放置 Vault 服務。

地端開發

這邊再進行 Vault 存取的配置來讓 Vault 進行環境變數的注入。因為 SSL 非正式所以這邊使用了 quarkus.tls.trust-all=true 來進行信任。重點是下面配置

%dev.quarkus.vault.authentication.client-token=hvs.CAESIIDXLAV0-_yh1VJjVQkDxojDEP54lTfT0Y-UsVEBJKBhGh4KHGh2cy5lRFRMeFpBSmpGMGo2ZXRNR0RsTDh3UmI
quarkus.vault.url=https://vault-demo.cch.com:8453
quarkus.vault.kv-secret-engine-mount-path=kv
quarkus.vault.secret-config-kv-path=quarkus/vault-demo
quarkus.vault.kv-secret-engine-version=2
  1. quarkus.vault.authentication.client-token 表示使用 token 方式對 Vault 進行存取
  2. quarkus.vault.url 表示存取 Vault 的位置
  3. quarkus.vault.kv-secret-engine-mount-path 啟用 secret 中的 kv 引擎掛載的路徑
  4. quarkus.vault.secret-config-kv-path 掛載路徑下要存取的子路徑
  5. quarkus.vault.kv-secret-engine-version 使用的版本,預設是 2

這邊的 %dev 是指在 dev 環境使用該值。

驗證方面,寫了一個 API 接口,透過該接口可以驗證是否從 Vault 的 KV 獲取資源

    @GET
@Path("/mqtt/config")
@Produces(MediaType.APPLICATION_JSON)
public Response config() {
return Response.ok(Map.of("url", mqttConnect.broker())).build();
}

最後運行應用程式,嘗試存取 /hello/mqtt/config 接口如下,可以看出他確實拿到了資源。

$ curl http://localhost:8080/hello/mqtt/config
{"url":"tcp://localhost:8883"}

對於地端來說雖然 quarkus.vault.authentication.client-token 的值已經有限制權限和時效,但還是會習慣把 quarkus.vault.authentication.client-token 放置 .env 檔案中,這樣基本上原始碼不會出現關於存取 Vault 服務可存取 API 的 toekn 資源。

程式碼可參考我的鏈結

Kubernetes 環境

對於佈署至 Kubernetes 環境,Vault 提供了用 serviceAccounttoken 存取 Vault 服務的功能。相較於使用上面 token 方式,會比較靈活很多,我會認為不必特別管理 token 是否會過期,或是管理多個 token

要使用 Kubernetes 認證必須執行以下,這邊會使用 root 權限進行操作

  1. 啟用該功能
$ vault auth enable kubernetes
Success! Enabled kubernetes auth method at: kubernetes/
  1. /config 介面配置 Vault 與 Kubernetes 的通訊,使用 Pod 的 serviceAccounttoken。 Vault 將定期重新讀取該檔案以支援短期令牌(short-lived tokens)。這樣 Vault 將嘗試從預設掛載位置 /var/run/secrets/kubernetes.io/serviceaccount/ 內的 tokenca.crt 加載它們。
$ vault write auth/kubernetes/config \
kubernetes_host=https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT
Success! Data written to: auth/kubernetes/config

$KUBERNETES_SERVICE_HOST$KUBERNETES_SERVICE_PORT 是環境變數,需替代真實的值,本人測試時環境變數似乎無法被系統替代。

  1. 設定允許被存取的 namespaceserviceAccount
$ vault write auth/kubernetes/role/quarkus-vault \
> bound_service_account_names=vault \
> bound_service_account_namespaces=prod \
> policies=vault \
> ttl=1h
Success! Data written to: auth/kubernetes/role/quarkus-vault
  • quarkus-vault 是角色名稱,應用程式存取用此角色
  • bound_service_account_names 可使用該角色的 serviceAccout
  • bound_service_account_namespaces 可使用該角色的 namespace
  • policies 該角色綁定的政策,這邊會對應上面所設定的政策

Vault 中一個角色能被 kubernetes 中的 namespace 下的多個 serviceAccount 資源共同所使用,政策同樣也可以綁定多個。

當設定好之後,在 Quarkus 專案中的 application.properties 檔案定義以下,表示在 prod 環境中使用以下定義的值。

%prod.quarkus.vault.url=http://vault.vault.svc.cluster.local:8200
%prod.quarkus.vault.authentication.kubernetes.role=quarkus-vault

接著嘗試佈署我們的應用程式至 k3d 模擬的 Kubernetes 環境,下面是部分 deployment.yaml 檔案內容詳細可參考此鏈結

# deployment.yaml
...
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/managed-by: quarkus
app.kubernetes.io/name: vault
app.kubernetes.io/version: 1.0.0-SNAPSHOT
app.kubernetes.io/part-of: vault
name: vault
namespace: prod
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: vault
app.kubernetes.io/version: 1.0.0-SNAPSHOT
app.kubernetes.io/part-of: vault
template:
metadata:
labels:
app.kubernetes.io/managed-by: quarkus
app.kubernetes.io/name: vault
app.kubernetes.io/version: 1.0.0-SNAPSHOT
app.kubernetes.io/part-of: vault
namespace: prod
spec:
containers:
- env:
- name: KUBERNETES_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: registry.hub.docker.com/cch0124/vault:788ed52
imagePullPolicy: Always
...
securityContext:
fsGroup: 185
runAsGroup: 185
runAsNonRoot: true
runAsUser: 185
serviceAccountName: vault
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
labels:
app.kubernetes.io/name: vault
app.kubernetes.io/part-of: vault
app.kubernetes.io/version: 1.0.0-SNAPSHOT
app.kubernetes.io/managed-by: quarkus
name: vault
namespace: prod
spec:
ingressClassName: nginx
rules:
- host: vault-demo.cch.com
http:
paths:
- backend:
service:
name: vault
port:
name: http
path: /api/v1(/|$)(.*)
pathType: Prefix

接著透過佈署 deployment.yaml 測試,應用程式佈署資源如下

vault-demo/example/vault/kubernetes$ kubectl apply -f deployment.yaml
$ kubectl get all -n prod
NAME READY STATUS RESTARTS AGE
pod/vault-76cfcb466f-s6z2m 1/1 Running 0 6m15s

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/vault ClusterIP 10.43.236.207 <none> 8080/TCP 34m

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/vault 1/1 1 1 34m

NAME DESIRED CURRENT READY AGE
replicaset.apps/vault-76cfcb466f 1 1 1 34m
$ kubectl get ingress -n prod
NAME CLASS HOSTS ADDRESS PORTS AGE
vault nginx vault-demo.cch.com 172.18.0.2,172.18.0.3,172.18.0.4 80 35m

測試,確實那到了 KV 上定義的值

$ curl http://vault-demo.cch.com:8050/api/v1/hello/mqtt/config
{"url":"tcp://localhost:8883"}

這邊,應用程式透過了 Vault 中 kubernete 認證方式對 Vault 進行存取,而非 token 認證。整體的流程會如下圖所示

總結

Vault 在應用上可以集中式管理機密資源,加上需要透過認證才能去存取資源基本上攻擊面或是洩漏資訊可能性少了很多。此篇文章透過了框架對 Vault 資源進行整合,其使用對開發者來說也是輕鬆。但對於應用程式存取 Vault 除了透過框架提供的方式,還可以使用 Vault 的 sidecar 方式或是 CSI (Container Storage Interface) 整合,如果是對於遺留較久的專案可以考慮這種方式。

但如果服務多的話,對於 Vault 上的 path 設計其實也是很重要的一環,不應當所有服務都對一個路徑下的 KV 進行存取。

- + \ No newline at end of file diff --git a/docs/tutorial-basics/congratulations/index.html b/docs/tutorial-basics/congratulations/index.html index de3addc..7a93c9e 100644 --- a/docs/tutorial-basics/congratulations/index.html +++ b/docs/tutorial-basics/congratulations/index.html @@ -10,13 +10,13 @@ - + - + \ No newline at end of file diff --git a/docs/tutorial-basics/create-security-group/index.html b/docs/tutorial-basics/create-security-group/index.html index d4c76b3..1205fb7 100644 --- a/docs/tutorial-basics/create-security-group/index.html +++ b/docs/tutorial-basics/create-security-group/index.html @@ -10,13 +10,13 @@ - +

建立安全性群組

OpenStack VM 可以透過安全性群組對 VM 做簡單的防火牆管理。在預設狀態下,每個專案會有一個 default 安全性群組,並且會阻擋所有流入流量。

首先,進入網路 -> 安全性群組,點擊右方新增安全性群組,填入名稱和描述。

建立後可以點選右方管理規則,更改規則。

加入規則時,需填入以下資訊:

  • 規則
    • TCP/UDP/ICMP 等,或是已經登入好的如 SSH, HTTP, HTTPS
    • 方向
      • 預設由 VM 流出的網路是全開的,所以通常方向都選擇入口即可
    • 開放埠口 (port)
      • 可以選擇開放單一埠口,一個範圍的埠口,或是全開 (1-65536)
    • CIDR
      • 此規則適用的 CIDR
        • 如果要所有 IPv4 都能使用此規則,可選擇 0.0.0.0/0
        • 或是可以根據需求更改,如只允許 140.113.0.0/16 的 IP 連入 SSH 埠口等

填寫完畢後點選加入,即成功將此規則加入安全性群組。

- + \ No newline at end of file diff --git a/docs/tutorial-basics/create-vm/index.html b/docs/tutorial-basics/create-vm/index.html index 8c6d38c..f6457fc 100644 --- a/docs/tutorial-basics/create-vm/index.html +++ b/docs/tutorial-basics/create-vm/index.html @@ -10,14 +10,14 @@ - +

建立 VM (Launch Instances)

首先,進入運算 -> 雲實例,點擊右方 發動雲實例 相繼輸入以下內容

  • 詳細資訊
    • 雲實例名稱(虛擬機名稱)
    • 計數(虛擬機數量)
  • 來源
    • 選擇下方的可用鏡像直接開機(若您需要其他鏡像,請聯繫管理員)
  • 類型
    • 虛擬機的規格
  • 網路
    • 選擇自己建立的私有網路
    • 或選擇 publicv4 直接使用 Public IPv4 地址
  • 安全性群組
    • 選擇自己設定好的安全群組。 在預設情況下,default 安全群組禁止所有外部流量連入
  • Key Pair
    • 選擇需要 Access VM 的 SSH Key。

建立 VM 後,會自動建立 <VM 名稱>.<專案名稱>.infra.cloudnative.tw 的 DNS record 指至此 VM。

預設登入帳號:

  • Ubuntu: ubuntu
  • Debian: debian
- + \ No newline at end of file diff --git a/docs/tutorial-basics/upload-ssh-key/index.html b/docs/tutorial-basics/upload-ssh-key/index.html index f1e8658..8c3bda4 100644 --- a/docs/tutorial-basics/upload-ssh-key/index.html +++ b/docs/tutorial-basics/upload-ssh-key/index.html @@ -10,14 +10,14 @@ - +

上傳 SSH 金鑰

OpenStack 上所有 VM 預設都是使用 SSH Public Key 進行 SSH 認證,因此必須建立或上傳自己的 SSH Public Key 以便在 VM 建立的過程中注入。

首先,點選左邊選單欄 運算 -> 密鑰對。

右上角將有

  • 新增密鑰對
  • 輸入公鑰 等選項

如果自己在本地已經有 SSH Public Key 要上傳,點選輸入公鑰並填入

  • 密鑰對名稱
  • 金鑰類型
  • 金鑰或上傳檔案

點擊輸入公鑰後及成功上傳

如果要建立新的 SSH Public Key 點選新增密鑰對輸入名稱並選擇類型,按下新增密鑰對後及會下載新增的密鑰對。

- + \ No newline at end of file diff --git a/docs/tutorial-extra/create-private-network/index.html b/docs/tutorial-extra/create-private-network/index.html index 311ffab..6736c4e 100644 --- a/docs/tutorial-extra/create-private-network/index.html +++ b/docs/tutorial-extra/create-private-network/index.html @@ -10,14 +10,14 @@ - +

建立私有網路

OpenStack 允許各個用戶在自己專案建立自己的私有網路,私有網路中的 VM 可以透過路由器 SNAT 連線至外部網路。 我們會建議使用者建立一個自己的私有網路並且透過一個 bastion host 作為一個跳板連入。

首先,進入網路 -> 網路,點擊建立網路。

相繼輸入以下資訊

  • 網路
    • 網路名稱
    • 勾選建立子網路
  • 子網路
    • 子網路名稱
    • 網路地址
      • 請選擇慣用的私有網路地址
      • Eg. 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
    • IP Version (選擇 IPv4 或是 IPv6)
    • 閘道 IP(非必要,用於路由器 IP 地址)
  • 子網路詳細資訊
    • DHCP 地址池
    • DNS 位置

建立路由器

路由器是用來做外部網路和私有網路中的 SNAT/DNAT,要透過路由器才能在 VM 上聯結浮動 IP

首先,進入網路 -> 路由器,點擊新增路由器,填寫:

  • 路由器名稱
  • 對外網路選擇 publicv4publicv6

點選新增路由器。

當路由器建立完畢後,點選路由器名稱進入路由器資訊,並且選擇網路卡選單,點擊右方加入網路卡,填寫:

  • 子網域
    • 選擇你建立的私有網路
  • IP 位址
    • 填寫當初子網域所填寫的閘道 IP

點選提交後將會建立一個新的網路卡連接路由器和私有網路,提供給私有網路 SNAT 和上面 VM 浮動 IP DNAT

- + \ No newline at end of file diff --git a/docs/tutorial-extra/create-volume/index.html b/docs/tutorial-extra/create-volume/index.html index ea61fba..9a71047 100644 --- a/docs/tutorial-extra/create-volume/index.html +++ b/docs/tutorial-extra/create-volume/index.html @@ -10,13 +10,13 @@ - +

建立 Volume

首先,進入雲硬碟 -> 雲硬碟,點擊新增雲硬碟,填入:

  • 名稱
  • 雲硬碟來源
    • 如果要使用此雲硬碟開機,可以在此選擇映像檔,便會將此映像檔內容複製到此雲硬碟中
  • 類型
    • 預設是使用 HDD 儲存池,如果需要高效能的狀況下可以選擇 NVMe
  • 容量

點選新增雲硬碟後即會開始建立。

建立完成後在最右方 Actions 點選選單欄,可以將雲硬碟掛載至 VM 上。

- + \ No newline at end of file diff --git a/docs/tutorial-extra/floating-ip/index.html b/docs/tutorial-extra/floating-ip/index.html index 31ce4fb..0090e3a 100644 --- a/docs/tutorial-extra/floating-ip/index.html +++ b/docs/tutorial-extra/floating-ip/index.html @@ -10,13 +10,13 @@ - +

綁定浮動 IP

如果使用私有網路,可以將 VM 綁上一個浮動的 Public IP,透過路由器 DNAT 進行連線。

在 VM 頁面右邊 Actions 下拉選單中點選聯結浮動 IP,選擇或建立新的浮動 IP 地址和欲聯結的接口,點選聯結後即可聯結浮動 IP。

- + \ No newline at end of file diff --git a/icon-license/index.html b/icon-license/index.html index 0815ae3..6308346 100644 --- a/icon-license/index.html +++ b/icon-license/index.html @@ -10,13 +10,13 @@ - + - + \ No newline at end of file diff --git a/index.html b/index.html index 4fc060d..5396d08 100644 --- a/index.html +++ b/index.html @@ -10,13 +10,13 @@ - +

實戰學習

提供雲端平台讓學生與社群成員親身測試與部署軟體基礎設施相關服務。

全方位支援

涵蓋多種服務,如虛擬機器、儲存、網路、負載平衡等,滿足不同需求。

開源共享

資源提供給開源社群,支持相關服務建設與工作坊活動。

- + \ No newline at end of file diff --git a/intro/index.html b/intro/index.html index 5127e37..c5d5f93 100644 --- a/intro/index.html +++ b/intro/index.html @@ -10,14 +10,14 @@ - +

CNTUG Logo

CNTUG Infra Labs 成立的宗旨是為了培育台灣軟體基礎設施領域的未來學生和工程師。目前已在 Equinix 東京機房成功建立完善的環境,得到了 CNTUG 社群多位成員的共同出資支持。此建置過程中運用了大量開源專案,例如 OpenStack、Ceph 和 Ansible 等。

由於基礎設施相關軟體具有較高的上手難度,並需要足夠的運算、儲存和網路資源,因此 CNTUG Infra Labs 計劃提供一個雲端平台供學生和社群成員測試及架設相關服務。我們期望這將吸引更多學生參與,並在軟體基礎設施領域不斷精進。同時,剩餘資源將提供給開源社群,供他們架設所需服務(如網站、Mattermost 和 Jitsi Meet 等),或用於工作坊活動。

目前提供的服務包括:

  • 虛擬機器 (Virtual Machine)
  • 區塊儲存 (Block Storage)
  • 物件儲存 (Object Storage)
  • 網路
  • 負載平衡 (Load Balancer)
  • DNS
  • GPU 等。

已經有使用者在這個平台上積極進行開源貢獻,詳情可以查閱使用案例分頁。

若有疑問或願意贊助本專案,敬請聯繫 infra@cloudnative.tw

- + \ No newline at end of file diff --git a/usecase/index.html b/usecase/index.html index 04b21fa..1d5dd7a 100644 --- a/usecase/index.html +++ b/usecase/index.html @@ -10,13 +10,13 @@ - + - + \ No newline at end of file