1. 程式人生 > >kubeadm原始碼分析(內含kubernetes離線包,三步安裝)_Kubernetes中文社群

kubeadm原始碼分析(內含kubernetes離線包,三步安裝)_Kubernetes中文社群

k8s離線安裝包 三步安裝,簡單到難以置信

kubeadm原始碼分析

說句實在話,kubeadm的程式碼寫的真心一般,質量不是很高。

幾個關鍵點來先說一下kubeadm乾的幾個核心的事:

  • kubeadm 生成證書在/etc/kubernetes/pki目錄下
  • kubeadm 生成static pod yaml配置,全部在/etc/kubernetes/manifasts下
  • kubeadm 生成kubelet配置,kubectl配置等 在/etc/kubernetes下
  • kubeadm 通過client go去啟動dns

kubeadm init

程式碼入口 cmd/kubeadm/app/cmd/init.go 建議大家去看看cobra

找到Run函式來分析下主要流程:

  1. 如果證書不存在,就建立證書,所以如果我們有自己的證書可以把它放在/etc/kubernetes/pki下即可, 下文細看如果生成證書
  1. if res, _ := certsphase.UsingExternalCA(i.cfg); !res {
  2. if err := certsphase.CreatePKIAssets(i.cfg); err != nil {
  3. return err
  4. }
  1. 建立kubeconfig檔案
  1. if err := kubeconfigphase.CreateInitKubeConfigFiles(kubeConfigDir, i.cfg); err != nil {
  2. return err
  3. }
  1. 建立manifest檔案,etcd apiserver manager scheduler都在這裡建立, 可以看到如果你的配置檔案裡已經寫了etcd的地址了,就不建立了,這我們就可以自己裝etcd叢集,而不用預設單點的etcd,很有用
  1. controlplanephase.CreateInitStaticPodManifestFiles(manifestDir, i.cfg);
  2. if len(i.cfg.Etcd.Endpoints) == 0 {
  3. if err := etcdphase.CreateLocalEtcdStaticPodManifestFile(manifestDir, i.cfg); err != nil {
  4. return fmt.Errorf(“error creating local etcd static pod manifest file: %v”, err)
  5. }
  6. }
  1. 等待APIserver和kubelet啟動成功,這裡就會遇到我們經常遇到的映象拉不下來的錯誤,其實有時kubelet因為別的原因也會報這個錯,讓人誤以為是映象弄不下來
  1. if err := waitForAPIAndKubelet(waiter); err != nil {
  2. ctx := map[string]string{
  3. “Error”: fmt.Sprintf(“%v”, err),
  4. “APIServerImage”: images.GetCoreImage(kubeadmconstants.KubeAPIServer, i.cfg.GetControlPlaneImageRepository(), i.cfg.KubernetesVersion, i.cfg.UnifiedControlPlaneImage),
  5. “ControllerManagerImage”: images.GetCoreImage(kubeadmconstants.KubeControllerManager, i.cfg.GetControlPlaneImageRepository(), i.cfg.KubernetesVersion, i.cfg.UnifiedControlPlaneImage),
  6. “SchedulerImage”: images.GetCoreImage(kubeadmconstants.KubeScheduler, i.cfg.GetControlPlaneImageRepository(), i.cfg.KubernetesVersion, i.cfg.UnifiedControlPlaneImage),
  7. }
  8. kubeletFailTempl.Execute(out, ctx)
  9. return fmt.Errorf(“couldn’t initialize a Kubernetes cluster”)
  10. }
  1. 給master加標籤,加汙點, 所以想要pod排程到master上可以把汙點清除了
  1. if err := markmasterphase.MarkMaster(client, i.cfg.NodeName); err != nil {
  2. return fmt.Errorf(“error marking master: %v”, err)
  3. }
  1. 生成tocken
  1. if err := nodebootstraptokenphase.UpdateOrCreateToken(client, i.cfg.Token, false, i.cfg.TokenTTL.Duration, kubeadmconstants.DefaultTokenUsages, []string{kubeadmconstants.NodeBootstrapTokenAuthGroup}, tokenDescription); err != nil {
  2. return fmt.Errorf(“error updating or creating token: %v”, err)
  3. }
  1. 呼叫clientgo建立dns和kube-proxy
  1. if err := dnsaddonphase.EnsureDNSAddon(i.cfg, client); err != nil {
  2. return fmt.Errorf(“error ensuring dns addon: %v”, err)
  3. }
  4. if err := proxyaddonphase.EnsureProxyAddon(i.cfg, client); err != nil {
  5. return fmt.Errorf(“error ensuring proxy addon: %v”, err)
  6. }

筆者批判程式碼無腦式的一個流程到底,要是筆者操刀定抽象成介面 RenderConf Save Run Clean等,DNS kube-porxy以及其它元件去實現,然後問題就是沒把dns和kubeproxy的配置渲染出來,可能是它們不是static pod的原因, 然後就是join時的bug下文提到

證書生成

迴圈的呼叫了這一坨函式,我們只需要看其中一兩個即可,其它的都差不多

  1. certActions := []func(cfg *kubeadmapi.MasterConfiguration) error{
  2. CreateCACertAndKeyfiles,
  3. CreateAPIServerCertAndKeyFiles,
  4. CreateAPIServerKubeletClientCertAndKeyFiles,
  5. CreateServiceAccountKeyAndPublicKeyFiles,
  6. CreateFrontProxyCACertAndKeyFiles,
  7. CreateFrontProxyClientCertAndKeyFiles,
  8. }

根證書生成:

  1. //返回了根證書的公鑰和私鑰
  2. func NewCACertAndKey() (*x509.Certificate, *rsa.PrivateKey, error) {
  3. caCert, caKey, err := pkiutil.NewCertificateAuthority()
  4. if err != nil {
  5. return nil, nil, fmt.Errorf(“failure while generating CA certificate and key: %v”, err)
  6. }
  7. return caCert, caKey, nil
  8. }

k8s.io/client-go/util/cert 這個庫裡面有兩個函式,一個生成key的一個生成cert的:

  1. key, err := certutil.NewPrivateKey()
  2. config := certutil.Config{
  3. CommonName: “kubernetes”,
  4. }
  5. cert, err := certutil.NewSelfSignedCACert(config, key)

config裡面我們也可以填充一些別的證書資訊:

  1. type Config struct {
  2. CommonName string
  3. Organization []string
  4. AltNames AltNames
  5. Usages []x509.ExtKeyUsage
  6. }

私鑰就是封裝了rsa庫裡面的函式:

  1. “crypto/rsa”
  2. “crypto/x509”
  3. func NewPrivateKey() (*rsa.PrivateKey, error) {
  4. return rsa.GenerateKey(cryptorand.Reader, rsaKeySize)
  5. }

自簽證書,所以根證書裡只有CommonName資訊,Organization相當於沒設定:

  1. func NewSelfSignedCACert(cfg Config, key *rsa.PrivateKey) (*x509.Certificate, error) {
  2. now := time.Now()
  3. tmpl := x509.Certificate{
  4. SerialNumber: new(big.Int).SetInt64(0),
  5. Subject: pkix.Name{
  6. CommonName: cfg.CommonName,
  7. Organization: cfg.Organization,
  8. },
  9. NotBefore: now.UTC(),
  10. NotAfter: now.Add(duration365d * 10).UTC(),
  11. KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
  12. BasicConstraintsValid: true,
  13. IsCA: true,
  14. }
  15. certDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &tmpl, &tmpl, key.Public(), key)
  16. if err != nil {
  17. return nil, err
  18. }
  19. return x509.ParseCertificate(certDERBytes)
  20. }

生成好之後把之寫入檔案:

  1. pkiutil.WriteCertAndKey(pkiDir, baseName, cert, key);
  2. certutil.WriteCert(certificatePath, certutil.EncodeCertPEM(cert))

這裡呼叫了pem庫進行了編碼

  1. encoding/pem
  2. func EncodeCertPEM(cert *x509.Certificate) []byte {
  3. block := pem.Block{
  4. Type: CertificateBlockType,
  5. Bytes: cert.Raw,
  6. }
  7. return pem.EncodeToMemory(&block)
  8. }

然後我們看apiserver的證書生成:

  1. caCert, caKey, err := loadCertificateAuthorithy(cfg.CertificatesDir, kubeadmconstants.CACertAndKeyBaseName)
  2. //從根證書生成apiserver證書
  3. apiCert, apiKey, err := NewAPIServerCertAndKey(cfg, caCert, caKey)

這時需要關注AltNames了比較重要,所有需要訪問master的地址域名都得加進去,對應配置檔案中apiServerCertSANs欄位,其它東西與根證書無差別

  1. config := certutil.Config{
  2. CommonName: kubeadmconstants.APIServerCertCommonName,
  3. AltNames: *altNames,
  4. Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
  5. }

建立k8s配置檔案

可以看到建立了這些檔案

  1. return createKubeConfigFiles(
  2. outDir,
  3. cfg,
  4. kubeadmconstants.AdminKubeConfigFileName,
  5. kubeadmconstants.KubeletKubeConfigFileName,
  6. kubeadmconstants.ControllerManagerKubeConfigFileName,
  7. kubeadmconstants.SchedulerKubeConfigFileName,
  8. )

k8s封裝了兩個渲染配置的函式: 區別是你的kubeconfig檔案裡會不會產生token,比如你進入dashboard需要一個token,或者你呼叫api需要一個token那麼請生成帶token的配置 生成的conf檔案基本一直只是比如ClientName這些東西不同,所以加密後的證書也不同,ClientName會被加密到證書裡,然後k8s取出來當用戶使用

所以重點來了,我們做多租戶時也要這樣去生成。然後給該租戶繫結角色。

  1. return kubeconfigutil.CreateWithToken(
  2. spec.APIServer,
  3. “kubernetes”,
  4. spec.ClientName,
  5. certutil.EncodeCertPEM(spec.CACert),
  6. spec.TokenAuth.Token,
  7. ), nil
  8. return kubeconfigutil.CreateWithCerts(
  9. spec.APIServer,
  10. “kubernetes”,
  11. spec.ClientName,
  12. certutil.EncodeCertPEM(spec.CACert),
  13. certutil.EncodePrivateKeyPEM(clientKey),
  14. certutil.EncodeCertPEM(clientCert),
  15. ), nil

然後就是填充Config結構體嘍, 最後寫到檔案裡,略

  1. “k8s.io/client-go/tools/clientcmd/api
  2. return &clientcmdapi.Config{
  3. Clusters: map[string]*clientcmdapi.Cluster{
  4. clusterName: {
  5. Server: serverURL,
  6. CertificateAuthorityData: caCert,
  7. },
  8. },
  9. Contexts: map[string]*clientcmdapi.Context{
  10. contextName: {
  11. Cluster: clusterName,
  12. AuthInfo: userName,
  13. },
  14. },
  15. AuthInfos: map[string]*clientcmdapi.AuthInfo{},
  16. CurrentContext: contextName,
  17. }

建立static pod yaml檔案

這裡返回了apiserver manager scheduler的pod結構體,

  1. specs := GetStaticPodSpecs(cfg, k8sVersion)
  2. staticPodSpecs := map[string]v1.Pod{
  3. kubeadmconstants.KubeAPIServer: staticpodutil.ComponentPod(v1.Container{
  4. Name: kubeadmconstants.KubeAPIServer,
  5. Image: images.GetCoreImage(kubeadmconstants.KubeAPIServer, cfg.GetControlPlaneImageRepository(), cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage),
  6. Command: getAPIServerCommand(cfg, k8sVersion),
  7. VolumeMounts: staticpodutil.VolumeMountMapToSlice(mounts.GetVolumeMounts(kubeadmconstants.KubeAPIServer)),
  8. LivenessProbe: staticpodutil.ComponentProbe(cfg, kubeadmconstants.KubeAPIServer, int(cfg.API.BindPort), “/healthz”, v1.URISchemeHTTPS),
  9. Resources: staticpodutil.ComponentResources(“250m”),
  10. Env: getProxyEnvVars(),
  11. }, mounts.GetVolumes(kubeadmconstants.KubeAPIServer)),
  12. kubeadmconstants.KubeControllerManager: staticpodutil.ComponentPod(v1.Container{
  13. Name: kubeadmconstants.KubeControllerManager,
  14. Image: images.GetCoreImage(kubeadmconstants.KubeControllerManager, cfg.GetControlPlaneImageRepository(), cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage),
  15. Command: getControllerManagerCommand(cfg, k8sVersion),
  16. VolumeMounts: staticpodutil.VolumeMountMapToSlice(mounts.GetVolumeMounts(kubeadmconstants.KubeControllerManager)),
  17. LivenessProbe: staticpodutil.ComponentProbe(cfg, kubeadmconstants.KubeControllerManager, 10252, “/healthz”, v1.URISchemeHTTP),
  18. Resources: staticpodutil.ComponentResources(“200m”),
  19. Env: getProxyEnvVars(),
  20. }, mounts.GetVolumes(kubeadmconstants.KubeControllerManager)),
  21. kubeadmconstants.KubeScheduler: staticpodutil.ComponentPod(v1.Container{
  22. Name: kubeadmconstants.KubeScheduler,
  23. Image: images.GetCoreImage(kubeadmconstants.KubeScheduler, cfg.GetControlPlaneImageRepository(), cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage),
  24. Command: getSchedulerCommand(cfg),
  25. VolumeMounts: staticpodutil.VolumeMountMapToSlice(mounts.GetVolumeMounts(kubeadmconstants.KubeScheduler)),
  26. LivenessProbe: staticpodutil.ComponentProbe(cfg, kubeadmconstants.KubeScheduler, 10251, “/healthz”, v1.URISchemeHTTP),
  27. Resources: staticpodutil.ComponentResources(“100m”),
  28. Env: getProxyEnvVars(),
  29. }, mounts.GetVolumes(kubeadmconstants.KubeScheduler)),
  30. }
  31. //獲取特定版本的映象
  32. func GetCoreImage(image, repoPrefix, k8sVersion, overrideImage string) string {
  33. if overrideImage != “” {
  34. return overrideImage
  35. }
  36. kubernetesImageTag := kubeadmutil.KubernetesVersionToImageTag(k8sVersion)
  37. etcdImageTag := constants.DefaultEtcdVersion
  38. etcdImageVersion, err := constants.EtcdSupportedVersion(k8sVersion)
  39. if err == nil {
  40. etcdImageTag = etcdImageVersion.String()
  41. }
  42. return map[string]string{
  43. constants.Etcd: fmt.Sprintf(“%s/%s-%s:%s”, repoPrefix, “etcd”, runtime.GOARCH, etcdImageTag),
  44. constants.KubeAPIServer: fmt.Sprintf(“%s/%s-%s:%s”, repoPrefix, “kube-apiserver”, runtime.GOARCH, kubernetesImageTag),
  45. constants.KubeControllerManager: fmt.Sprintf(“%s/%s-%s:%s”, repoPrefix, “kube-controller-manager”, runtime.GOARCH, kubernetesImageTag),
  46. constants.KubeScheduler: fmt.Sprintf(“%s/%s-%s:%s”, repoPrefix, “kube-scheduler”, runtime.GOARCH, kubernetesImageTag),
  47. }[image]
  48. }
  49. //然後就把這個pod寫到檔案裡了,比較簡單
  50. staticpodutil.WriteStaticPodToDisk(componentName, manifestDir, spec);

建立etcd的一樣,不多廢話

等待kubelet啟動成功

這個錯誤非常容易遇到,看到這個基本就是kubelet沒起來,我們需要檢查:selinux swap 和Cgroup driver是不是一致 setenforce 0 && swapoff -a && systemctl restart kubelet如果不行請保證 kubelet的Cgroup driver與docker一致,docker info|grep Cg

  1. go func(errC chan error, waiter apiclient.Waiter) {
  2. // This goroutine can only make kubeadm init fail. If this check succeeds, it won’t do anything special
  3. if err := waiter.WaitForHealthyKubelet(40*time.Second, “http://localhost:10255/healthz”); err != nil {
  4. errC <- err
  5. }
  6. }(errorChan, waiter)
  7. go func(errC chan error, waiter apiclient.Waiter) {
  8. // This goroutine can only make kubeadm init fail. If this check succeeds, it won’t do anything special
  9. if err := waiter.WaitForHealthyKubelet(60*time.Second, “http://localhost:10255/healthz/syncloop”); err != nil {
  10. errC <- err
  11. }
  12. }(errorChan, waiter)

建立DNS和kubeproxy

我就是在此發現coreDNS的

  1. if features.Enabled(cfg.FeatureGates, features.CoreDNS) {
  2. return coreDNSAddon(cfg, client, k8sVersion)
  3. }
  4. return kubeDNSAddon(cfg, client, k8sVersion)

然後coreDNS的yaml配置模板直接是寫在程式碼裡的: /app/phases/addons/dns/manifests.go

  1. CoreDNSDeployment = `
  2. apiVersion: apps/v1beta2
  3. kind: Deployment
  4. metadata:
  5. name: coredns
  6. namespace: kube-system
  7. labels:
  8. k8s-app: kube-dns
  9. spec:
  10. replicas: 1
  11. selector:
  12. matchLabels:
  13. k8s-app: kube-dns
  14. template:
  15. metadata:
  16. labels:
  17. k8s-app: kube-dns
  18. spec:
  19. serviceAccountName: coredns
  20. tolerations:
  21. – key: CriticalAddonsOnly
  22. operator: Exists
  23. – key: {{ .MasterTaintKey }}

然後渲染模板,最後呼叫k8sapi建立,這種建立方式可以學習一下,雖然有點拙劣,這地方寫的遠不如kubectl好

  1. coreDNSConfigMap := &v1.ConfigMap{}
  2. if err := kuberuntime.DecodeInto(legacyscheme.Codecs.UniversalDecoder(), configBytes, coreDNSConfigMap); err != nil {
  3. return fmt.Errorf(“unable to decode CoreDNS configmap %v”, err)
  4. }
  5. // Create the ConfigMap for CoreDNS or update it in case it already exists
  6. if err := apiclient.CreateOrUpdateConfigMap(client, coreDNSConfigMap); err != nil {
  7. return err
  8. }
  9. coreDNSClusterRoles := &rbac.ClusterRole{}
  10. if err := kuberuntime.DecodeInto(legacyscheme.Codecs.UniversalDecoder(), []byte(CoreDNSClusterRole), coreDNSClusterRoles); err != nil {
  11. return fmt.Errorf(“unable to decode CoreDNS clusterroles %v”, err)
  12. }

這裡值得一提的是kubeproxy的configmap真應該把apiserver地址傳入進來,允許自定義,因為做高可用時需要指定虛擬ip,得修改,很麻煩 kubeproxy大差不差,不說了,想改的話改: app/phases/addons/proxy/manifests.go

kubeadm join

kubeadm join比較簡單,一句話就可以說清楚,獲取cluster info, 建立kubeconfig,怎麼建立的kubeinit裡面已經說了。帶上token讓kubeadm有許可權 可以拉取

  1. return https.RetrieveValidatedClusterInfo(cfg.DiscoveryFile)
  2. cluster info內容
  3. type Cluster struct {
  4. // LocationOfOrigin indicates where this object came from. It is used for round tripping config post-merge, but never serialized.
  5. LocationOfOrigin string
  6. // Server is the address of the kubernetes cluster (https://hostname:port).
  7. Server string `json:”server”`
  8. // InsecureSkipTLSVerify skips the validity check for the server’s certificate. This will make your HTTPS connections insecure.
  9. // +optional
  10. InsecureSkipTLSVerify bool `json:”insecure-skip-tls-verify,omitempty”`
  11. // CertificateAuthority is the path to a cert file for the certificate authority.
  12. // +optional
  13. CertificateAuthority string `json:”certificate-authority,omitempty”`
  14. // CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority
  15. // +optional
  16. CertificateAuthorityData []byte `json:”certificate-authority-data,omitempty”`
  17. // Extensions holds additional information. This is useful for extenders so that reads and writes don’t clobber unknown fields
  18. // +optional
  19. Extensions map[string]runtime.Object `json:”extensions,omitempty”`
  20. }
  21. return kubeconfigutil.CreateWithToken(
  22. clusterinfo.Server,
  23. “kubernetes”,
  24. TokenUser,
  25. clusterinfo.CertificateAuthorityData,
  26. cfg.TLSBootstrapToken,
  27. ), nil

CreateWithToken上文提到了不再贅述,這樣就能去生成kubelet配置檔案了,然後把kubelet啟動起來即可

kubeadm join的問題就是渲染配置時沒有使用命令列傳入的apiserver地址,而用clusterinfo裡的地址,這不利於我們做高可用,可能我們傳入一個虛擬ip,但是配置裡還是apiser的地址 +++ author = “fanux” date = “2014-07-11T10:54:24+02:00” draft = false title = “kubeadm原始碼分析” tags = [“event”,”dotScale”,”sketchnote”] image = “images/2014/Jul/titledotscale.png” comments = true # set false to hide Disqus comments share = true # set false to share buttons menu = “” # set “main” to add this content to the main menu +++

k8s離線安裝包 三步安裝,簡單到難以置信

kubeadm原始碼分析

說句實在話,kubeadm的程式碼寫的真心一般,質量不是很高。

幾個關鍵點來先說一下kubeadm乾的幾個核心的事:

  • kubeadm 生成證書在/etc/kubernetes/pki目錄下
  • kubeadm 生成static pod yaml配置,全部在/etc/kubernetes/manifasts下
  • kubeadm 生成kubelet配置,kubectl配置等 在/etc/kubernetes下
  • kubeadm 通過client go去啟動dns

kubeadm init

程式碼入口 cmd/kubeadm/app/cmd/init.go 建議大家去看看cobra

找到Run函式來分析下主要流程:

  1. 如果證書不存在,就建立證書,所以如果我們有自己的證書可以把它放在/etc/kubernetes/pki下即可, 下文細看如果生成證書
  1. if res, _ := certsphase.UsingExternalCA(i.cfg); !res {
  2. if err := certsphase.CreatePKIAssets(i.cfg); err != nil {
  3. return err
  4. }
  1. 建立kubeconfig檔案
  1. if err := kubeconfigphase.CreateInitKubeConfigFiles(kubeConfigDir, i.cfg); err != nil {
  2. return err
  3. }
  1. 建立manifest檔案,etcd apiserver manager scheduler都在這裡建立, 可以看到如果你的配置檔案裡已經寫了etcd的地址了,就不建立了,這我們就可以自己裝etcd叢集,而不用預設單點的etcd,很有用
  1. controlplanephase.CreateInitStaticPodManifestFiles(manifestDir, i.cfg);
  2. if len(i.cfg.Etcd.Endpoints) == 0 {
  3. if err := etcdphase.CreateLocalEtcdStaticPodManifestFile(manifestDir, i.cfg); err != nil {
  4. return fmt.Errorf(“error creating local etcd static pod manifest file: %v”, err)
  5. }
  6. }
  1. 等待APIserver和kubelet啟動成功,這裡就會遇到我們經常遇到的映象拉不下來的錯誤,其實有時kubelet因為別的原因也會報這個錯,讓人誤以為是映象弄不下來
  1. if err := waitForAPIAndKubelet(waiter); err != nil {
  2. ctx := map[string]string{
  3. “Error”: fmt.Sprintf(“%v”, err),
  4. “APIServerImage”: images.GetCoreImage(kubeadmconstants.KubeAPIServer, i.cfg.GetControlPlaneImageRepository(), i.cfg.KubernetesVersion, i.cfg.UnifiedControlPlaneImage),
  5. “ControllerManagerImage”: images.GetCoreImage(kubeadmconstants.KubeControllerManager, i.cfg.GetControlPlaneImageRepository(), i.cfg.KubernetesVersion, i.cfg.UnifiedControlPlaneImage),
  6. “SchedulerImage”: images.GetCoreImage(kubeadmconstants.KubeScheduler, i.cfg.GetControlPlaneImageRepository(), i.cfg.KubernetesVersion, i.cfg.UnifiedControlPlaneImage),
  7. }
  8. kubeletFailTempl.Execute(out, ctx)
  9. return fmt.Errorf(“couldn’t initialize a Kubernetes cluster”)
  10. }
  1. 給master加標籤,加汙點, 所以想要pod排程到master上可以把汙點清除了
  1. if err := markmasterphase.MarkMaster(client, i.cfg.NodeName); err != nil {
  2. return fmt.Errorf(“error marking master: %v”, err)
  3. }
  1. 生成tocken
  1. if err := nodebootstraptokenphase.UpdateOrCreateToken(client, i.cfg.Token, false, i.cfg.TokenTTL.Duration, kubeadmconstants.DefaultTokenUsages, []string{kubeadmconstants.NodeBootstrapTokenAuthGroup}, tokenDescription); err != nil {
  2. return fmt.Errorf(“error updating or creating token: %v”, err)
  3. }
  1. 呼叫clientgo建立dns和kube-proxy
  1. if err := dnsaddonphase.EnsureDNSAddon(i.cfg, client); err != nil {
  2. return fmt.Errorf(“error ensuring dns addon: %v”, err)
  3. }
  4. if err := proxyaddonphase.EnsureProxyAddon(i.cfg, client); err != nil {
  5. return fmt.Errorf(“error ensuring proxy addon: %v”, err)
  6. }

筆者批判程式碼無腦式的一個流程到底,要是筆者操刀定抽象成介面 RenderConf Save Run Clean等,DNS kube-porxy以及其它元件去實現,然後問題就是沒把dns和kubeproxy的配置渲染出來,可能是它們不是static pod的原因, 然後就是join時的bug下文提到

證書生成

迴圈的呼叫了這一坨函式,我們只需要看其中一兩個即可,其它的都差不多

  1. certActions := []func(cfg *kubeadmapi.MasterConfiguration) error{
  2. CreateCACertAndKeyfiles,
  3. CreateAPIServerCertAndKeyFiles,
  4. CreateAPIServerKubeletClientCertAndKeyFiles,
  5. CreateServiceAccountKeyAndPublicKeyFiles,
  6. CreateFrontProxyCACertAndKeyFiles,
  7. CreateFrontProxyClientCertAndKeyFiles,
  8. }

根證書生成:

  1. //返回了根證書的公鑰和私鑰
  2. func NewCACertAndKey() (*x509.Certificate, *rsa.PrivateKey, error) {
  3. caCert, caKey, err := pkiutil.NewCertificateAuthority()
  4. if err != nil {
  5. return nil, nil, fmt.Errorf(“failure while generating CA certificate and key: %v”, err)
  6. }
  7. return caCert, caKey, nil
  8. }

k8s.io/client-go/util/cert 這個庫裡面有兩個函式,一個生成key的一個生成cert的:

  1. key, err := certutil.NewPrivateKey()
  2. config := certutil.Config{
  3. CommonName: “kubernetes”,
  4. }
  5. cert, err := certutil.NewSelfSignedCACert(config, key)

config裡面我們也可以填充一些別的證書資訊:

  1. type Config struct {
  2. CommonName string
  3. Organization []string
  4. AltNames AltNames
  5. Usages []x509.ExtKeyUsage
  6. }

私鑰就是封裝了rsa庫裡面的函式:

  1. “crypto/rsa”
  2. “crypto/x509”
  3. func NewPrivateKey() (*rsa.PrivateKey, error) {
  4. return rsa.GenerateKey(cryptorand.Reader, rsaKeySize)
  5. }

自簽證書,所以根證書裡只有CommonName資訊,Organization相當於沒設定:

  1. func NewSelfSignedCACert(cfg Config, key *rsa.PrivateKey) (*x509.Certificate, error) {
  2. now := time.Now()
  3. tmpl := x509.Certificate{
  4. SerialNumber: new(big.Int).SetInt64(0),
  5. Subject: pkix.Name{
  6. CommonName: cfg.CommonName,
  7. Organization: cfg.Organization,
  8. },
  9. NotBefore: now.UTC(),
  10. NotAfter: now.Add(duration365d * 10).UTC(),
  11. KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
  12. BasicConstraintsValid: true,
  13. IsCA: true,
  14. }
  15. certDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &tmpl, &tmpl, key.Public(), key)
  16. if err != nil {
  17. return nil, err
  18. }
  19. return x509.ParseCertificate(certDERBytes)
  20. }

生成好之後把之寫入檔案:

  1. pkiutil.WriteCertAndKey(pkiDir, baseName, cert, key);
  2. certutil.WriteCert(certificatePath, certutil.EncodeCertPEM(cert))

這裡呼叫了pem庫進行了編碼

  1. encoding/pem
  2. func EncodeCertPEM(cert *x509.Certificate) []byte {
  3. block := pem.Block{
  4. Type: CertificateBlockType,
  5. Bytes: cert.Raw,
  6. }
  7. return pem.EncodeToMemory(&block)
  8. }

然後我們看apiserver的證書生成:

  1. caCert, caKey, err := loadCertificateAuthorithy(cfg.CertificatesDir, kubeadmconstants.CACertAndKeyBaseName)
  2. //從根證書生成apiserver證書
  3. apiCert, apiKey, err := NewAPIServerCertAndKey(cfg, caCert, caKey)

這時需要關注AltNames了比較重要,所有需要訪問master的地址域名都得加進去,對應配置檔案中apiServerCertSANs欄位,其它東西與根證書無差別

  1. config := certutil.Config{
  2. CommonName: kubeadmconstants.APIServerCertCommonName,
  3. AltNames: *altNames,
  4. Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
  5. }

建立k8s配置檔案

可以看到建立了這些檔案

  1. return createKubeConfigFiles(
  2. outDir,
  3. cfg,
  4. kubeadmconstants.AdminKubeConfigFileName,
  5. kubeadmconstants.KubeletKubeConfigFileName,
  6. kubeadmconstants.ControllerManagerKubeConfigFileName,
  7. kubeadmconstants.SchedulerKubeConfigFileName,
  8. )

k8s封裝了兩個渲染配置的函式: 區別是你的kubeconfig檔案裡會不會產生token,比如你進入dashboard需要一個token,或者你呼叫api需要一個token那麼請生成帶token的配置 生成的conf檔案基本一直只是比如ClientName這些東西不同,所以加密後的證書也不同,ClientName會被加密到證書裡,然後k8s取出來當用戶使用

所以重點來了,我們做多租戶時也要這樣去生成。然後給該租戶繫結角色。

  1. return kubeconfigutil.CreateWithToken(
  2. spec.APIServer,
  3. “kubernetes”,
  4. spec.ClientName,
  5. certutil.EncodeCertPEM(spec.CACert),
  6. spec.TokenAuth.Token,
  7. ), nil
  8. return kubeconfigutil.CreateWithCerts(
  9. spec.APIServer,
  10. “kubernetes”,
  11. spec.ClientName,
  12. certutil.EncodeCertPEM(spec.CACert),
  13. certutil.EncodePrivateKeyPEM(clientKey),
  14. certutil.EncodeCertPEM(clientCert),
  15. ), nil

然後就是填充Config結構體嘍, 最後寫到檔案裡,略

  1. “k8s.io/client-go/tools/clientcmd/api
  2. return &clientcmdapi.Config{
  3. Clusters: map[string]*clientcmdapi.Cluster{
  4. clusterName: {
  5. Server: serverURL,
  6. CertificateAuthorityData: caCert,
  7. },
  8. },
  9. Contexts: map[string]*clientcmdapi.Context{
  10. contextName: {
  11. Cluster: clusterName,
  12. AuthInfo: userName,
  13. },
  14. },
  15. AuthInfos: map[string]*clientcmdapi.AuthInfo{},
  16. CurrentContext: contextName,
  17. }

建立static pod yaml檔案

這裡返回了apiserver manager scheduler的pod結構體,

  1. specs := GetStaticPodSpecs(cfg, k8sVersion)
  2. staticPodSpecs := map[string]v1.Pod{
  3. kubeadmconstants.KubeAPIServer: staticpodutil.ComponentPod(v1.Container{
  4. Name: kubeadmconstants.KubeAPIServer,
  5. Image: images.GetCoreImage(kubeadmconstants.KubeAPIServer, cfg.GetControlPlaneImageRepository(), cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage),
  6. Command: getAPIServerCommand(cfg, k8sVersion),
  7. VolumeMounts: staticpodutil.VolumeMountMapToSlice(mounts.GetVolumeMounts(kubeadmconstants.KubeAPIServer)),
  8. LivenessProbe: staticpodutil.ComponentProbe(cfg, kubeadmconstants.KubeAPIServer, int(cfg.API.BindPort), “/healthz”, v1.URISchemeHTTPS),
  9. Resources: staticpodutil.ComponentResources(“250m”),
  10. Env: getProxyEnvVars(),
  11. }, mounts.GetVolumes(kubeadmconstants.KubeAPIServer)),
  12. kubeadmconstants.KubeControllerManager: staticpodutil.ComponentPod(v1.Container{
  13. Name: kubeadmconstants.KubeControllerManager,
  14. Image: images.GetCoreImage(kubeadmconstants.KubeControllerManager, cfg.GetControlPlaneImageRepository(), cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage),
  15. Command: getControllerManagerCommand(cfg, k8sVersion),
  16. VolumeMounts: staticpodutil.VolumeMountMapToSlice(mounts.GetVolumeMounts(kubeadmconstants.KubeControllerManager)),
  17. LivenessProbe: staticpodutil.ComponentProbe(cfg, kubeadmconstants.KubeControllerManager, 10252, “/healthz”, v1.URISchemeHTTP),
  18. Resources: staticpodutil.ComponentResources(“200m”),
  19. Env: getProxyEnvVars(),
  20. }, mounts.GetVolumes(kubeadmconstants.KubeControllerManager)),
  21. kubeadmconstants.KubeScheduler: staticpodutil.ComponentPod(v1.Container{
  22. Name: kubeadmconstants.KubeScheduler,
  23. Image: images.GetCoreImage(kubeadmconstants.KubeScheduler, cfg.GetControlPlaneImageRepository(), cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage),
  24. Command: getSchedulerCommand(cfg),
  25. VolumeMounts: staticpodutil.VolumeMountMapToSlice(mounts.GetVolumeMounts(kubeadmconstants.KubeScheduler)),
  26. LivenessProbe: staticpodutil.ComponentProbe(cfg, kubeadmconstants.KubeScheduler, 10251, “/healthz”, v1.URISchemeHTTP),
  27. Resources: staticpodutil.ComponentResources(“100m”),
  28. Env: getProxyEnvVars(),
  29. }, mounts.GetVolumes(kubeadmconstants.KubeScheduler)),
  30. }
  31. //獲取特定版本的映象
  32. func GetCoreImage(image, repoPrefix, k8sVersion, overrideImage string) string {
  33. if overrideImage != “” {
  34. return overrideImage
  35. }
  36. kubernetesImageTag := kubeadmutil.KubernetesVersionToImageTag(k8sVersion)
  37. etcdImageTag := constants.DefaultEtcdVersion
  38. etcdImageVersion, err := constants.EtcdSupportedVersion(k8sVersion)
  39. if err == nil {
  40. etcdImageTag = etcdImageVersion.String()
  41. }
  42. return map[string]string{
  43. constants.Etcd: fmt.Sprintf(“%s/%s-%s:%s”, repoPrefix, “etcd”, runtime.GOARCH, etcdImageTag),
  44. constants.KubeAPIServer: fmt.Sprintf(“%s/%s-%s:%s”, repoPrefix, “kube-apiserver”, runtime.GOARCH, kubernetesImageTag),
  45. constants.KubeControllerManager: fmt.Sprintf(“%s/%s-%s:%s”, repoPrefix, “kube-controller-manager”, runtime.GOARCH, kubernetesImageTag),
  46. constants.KubeScheduler: fmt.Sprintf(“%s/%s-%s:%s”, repoPrefix, “kube-scheduler”, runtime.GOARCH, kubernetesImageTag),
  47. }[image]
  48. }
  49. //然後就把這個pod寫到檔案裡了,比較簡單
  50. staticpodutil.WriteStaticPodToDisk(componentName, manifestDir, spec);

建立etcd的一樣,不多廢話

等待kubelet啟動成功

這個錯誤非常容易遇到,看到這個基本就是kubelet沒起來,我們需要檢查:selinux swap 和Cgroup driver是不是一致 setenforce 0 && swapoff -a && systemctl restart kubelet如果不行請保證 kubelet的Cgroup driver與docker一致,docker info|grep Cg

  1. go func(errC chan error, waiter apiclient.Waiter) {
  2. // This goroutine can only make kubeadm init fail. If this check succeeds, it won’t do anything special
  3. if err := waiter.WaitForHealthyKubelet(40*time.Second, “http://localhost:10255/healthz”); err != nil {
  4. errC <- err
  5. }
  6. }(errorChan, waiter)
  7. go func(errC chan error, waiter apiclient.Waiter) {
  8. // This goroutine can only make kubeadm init fail. If this check succeeds, it won’t do anything special
  9. if err := waiter.WaitForHealthyKubelet(60*time.Second, “http://localhost:10255/healthz/syncloop”); err != nil {
  10. errC <- err
  11. }
  12. }(errorChan, waiter)

建立DNS和kubeproxy

我就是在此發現coreDNS的

  1. if features.Enabled(cfg.FeatureGates, features.CoreDNS) {
  2. return coreDNSAddon(cfg, client, k8sVersion)
  3. }
  4. return kubeDNSAddon(cfg, client, k8sVersion)

然後coreDNS的yaml配置模板直接是寫在程式碼裡的: /app/phases/addons/dns/manifests.go

  1. CoreDNSDeployment = `
  2. apiVersion: apps/v1beta2
  3. kind: Deployment
  4. metadata:
  5. name: coredns
  6. namespace: kube-system
  7. labels:
  8. k8s-app: kube-dns
  9. spec:
  10. replicas: 1
  11. selector:
  12. matchLabels:
  13. k8s-app: kube-dns
  14. template:
  15. metadata:
  16. labels:
  17. k8s-app: kube-dns
  18. spec:
  19. serviceAccountName: coredns
  20. tolerations:
  21. – key: CriticalAddonsOnly
  22. operator: Exists
  23. – key: {{ .MasterTaintKey }}

然後渲染模板,最後呼叫k8sapi建立,這種建立方式可以學習一下,雖然有點拙劣,這地方寫的遠不如kubectl好

  1. coreDNSConfigMap := &v1.ConfigMap{}
  2. if err := kuberuntime.DecodeInto(legacyscheme.Codecs.UniversalDecoder(), configBytes, coreDNSConfigMap); err != nil {
  3. return fmt.Errorf(“unable to decode CoreDNS configmap %v”, err)
  4. }
  5. // Create the ConfigMap for CoreDNS or update it in case it already exists
  6. if err := apiclient.CreateOrUpdateConfigMap(client, coreDNSConfigMap); err != nil {
  7. return err
  8. }
  9. coreDNSClusterRoles := &rbac.ClusterRole{}
  10. if err := kuberuntime.DecodeInto(legacyscheme.Codecs.UniversalDecoder(), []byte(CoreDNSClusterRole), coreDNSClusterRoles); err != nil {
  11. return fmt.Errorf(“unable to decode CoreDNS clusterroles %v”, err)
  12. }

這裡值得一提的是kubeproxy的configmap真應該把apiserver地址傳入進來,允許自定義,因為做高可用時需要指定虛擬ip,得修改,很麻煩 kubeproxy大差不差,不說了,想改的話改: app/phases/addons/proxy/manifests.go

kubeadm join

kubeadm join比較簡單,一句話就可以說清楚,獲取cluster info, 建立kubeconfig,怎麼建立的kubeinit裡面已經說了。帶上token讓kubeadm有許可權 可以拉取

  1. return https.RetrieveValidatedClusterInfo(cfg.DiscoveryFile)
  2. cluster info內容
  3. type Cluster struct {
  4. // LocationOfOrigin indicates where this object came from. It is used for round tripping config post-merge, but never serialized.
  5. LocationOfOrigin string
  6. // Server is the address of the kubernetes cluster (https://hostname:port).
  7. Server string `json:”server”`
  8. // InsecureSkipTLSVerify skips the validity check for the server’s certificate. This will make your HTTPS connections insecure.
  9. // +optional
  10. InsecureSkipTLSVerify bool `json:”insecure-skip-tls-verify,omitempty”`
  11. // CertificateAuthority is the path to a cert file for the certificate authority.
  12. // +optional
  13. CertificateAuthority string `json:”certificate-authority,omitempty”`
  14. // CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority
  15. // +optional
  16. CertificateAuthorityData []byte `json:”certificate-authority-data,omitempty”`
  17. // Extensions holds additional information. This is useful for extenders so that reads and writes don’t clobber unknown fields
  18. // +optional
  19. Extensions map[string]runtime.Object `json:”extensions,omitempty”`
  20. }
  21. return kubeconfigutil.CreateWithToken(
  22. clusterinfo.Server,
  23. “kubernetes”,
  24. TokenUser,
  25. clusterinfo.CertificateAuthorityData,
  26. cfg.TLSBootstrapToken,
  27. ), nil

CreateWithToken上文提到了不再贅述,這樣就能去生成kubelet配置檔案了,然後把kubelet啟動起來即可

kubeadm join的問題就是渲染配置時沒有使用命令列傳入的apiserver地址,而用clusterinfo裡的地址,這不利於我們做高可用,可能我們傳入一個虛擬ip,但是配置裡還是apiser的地址