用Ansible 自動化搭建本地Kubernetes叢集

IT寫輪眼發表於2020-04-05

目標要求

  • 通過ansible指令碼, 降低安裝部署一套k8s叢集的工作難度

技能要求

  • 熟悉linux的基本操作命令
  • 熟悉Ansible的基本操作
  • 熟悉Docker的基本操作
  • 基本閱讀完成k8s的官方文件,對k8s有一個基本的認識,瞭解其術語和基本概念.能夠根據關鍵字,迅速在官網上找到對應的文件進行查閱.
  • 指令碼使用 kubeadm 來建立k8s叢集, 請讀者熟讀文件
  • 瞭解負載均衡的技術概念, HAProxy 的基本工作原理
  • 瞭解高可用的技術概念, Keepalived的基本工作原理

叢集搭建的幾大步驟

  1. 準備階段
  2. 網路規劃
  3. 獲取安裝指令碼
  4. 根據目標主機和網路規劃修改配置檔案
  5. 分階段執行指令碼

準備階段

目標主機

  • 作業系統: ubuntu 18.04
  • 配置主機能夠使用 root 使用者進行 ssh證照登入
  • 目標主機硬體配置符合安裝k8s的最低要求, 參考
  • 若干臺主機
    • 單 master 模式的叢集, 需要 1 臺master主機, 和至少1臺的worker主機(用於跑pod負載)
    • 3 master 模式的 負載均衡 + 高可用 模式的叢集, 需要 3 臺 master 主機, 和至少1臺的worker主機(用於跑pod負載)
  • 所有的目標主機配置好靜態IP, 同一個叢集的主機, 儘量在同一個子網內

控制端主機

  • 我們通過控制端主機進行操作, 完成整個k8s叢集的建立過程
  • 控制端主機上要求安裝 Python3 環境
  • 安裝了 Ansible, 我們通過 Ansible 進行整個叢集的建立操作

網路要求

  • 目標主機配置了靜態IP

  • 目標主機之間能夠網路互通

  • 控制端主機能夠與目標主機網路互通, 控制端主機能夠通過SSH證照方式,對目標主機進行ssh操作 參考:ssh-copy-id

  • 目標主機能夠訪問公網, 以便目標主機能夠下載依賴軟體包和docker映象 (離線方式的解決方案另外開篇敘述)

  • k8s網路外掛,採用 Calico

    問: 為什麼採用 Calico ?

    答: 參考這篇文章的描述和評測,認為 Calico 是一個比較成熟的方案.

網路規劃

在進行建立叢集的工作之前,我們需要先叢集的ip地址進行統一規劃:

  • 明確每一臺主機的靜態ip地址
  • 確定每一臺主機在叢集裡擔任的角色: master, worker
  • 根據主機數量(資源,成本)選擇採用:
    • 單 master 叢集模式
    • 多 master 叢集模式 (多個 master 組成 負載均衡 + 高可用)
  • 確定 控制平面域名, IP地址,
    • 控制平面的IP地址, 在高可用的模式下, 為高可用服務的 虛IP. 在單master節點的模式下, 為 master 的IP地址
    • 多 master 叢集模式下, 如果控制平面使用的埠與 kube-apiserverer 使用的埠相同(預設:6443), 則負載均衡服務不能夠與master執行在同一臺主機上, 否則會出現埠衝突. 在此情況下, 可以選擇另外的worker主機執行負載均衡服務. 或者更改控制平面的埠為其他值,例如:7443. 但是, 這種方式下, 需要在開始建立叢集之前, 先在3臺master上搭建好 負載均衡+高可用 服務.
    • 在所有的目標主機上 /etc/hosts 檔案裡新增一條 控制平面域名 到IP的解析記錄
  • 確定k8s叢集中, pod 使用的網段, 一般來說,只要和目標主機不在同一個網段即可.
  • 確定k8s叢集中, service 使用的網段, 不能和目標主機以及pod使用的網路在同一網段

獲取安裝指令碼

  • 安裝指令碼原始碼託管在github上, 原始碼地址

  • 獲取程式碼到控制端主機

    git clone https://github.com/LoveInShenZhen/k8s-ubuntu-ansible.git
    複製程式碼

根據目標主機和網路規劃修改配置檔案

Ansible 的 hosts 檔案

  • hosts 檔案描述了我們的 ansible 指令碼要操作的目標主機

  • 檔案sample 如下, 檔案中的配置以下文的 案例描述 為例:

    # 主機清單檔案參考: http://ansible.com.cn/docs/intro_inventory.html
    [nodes:children]
    master
    worker
    
    [master:children]
    first_master
    other_master
    
    # host_name 只能包含 英文字母,數字,中槓線 這3種字元, 我們使用 host_name 作為k8s 的node_name, 要保證唯一性 (注: 不允許有下劃線)
    # 建立叢集時的第一個 Master
    [first_master]
    192.168.3.151 host_name=master-1
    
    # 需要加入到現有叢集的其他 Master
    [other_master]
    192.168.3.152 host_name=master-2
    192.168.3.153 host_name=master-3
    
    [worker]
    192.168.3.154 host_name=work-1
    192.168.3.155 host_name=work-2
    
    # vip_interface 為虛IP所繫結的網路卡裝置名稱
    [lb_and_ha]
    192.168.3.151 vip_interface=eth1
    192.168.3.152 vip_interface=eth1
    192.168.3.153 vip_interface=eth1
    
    [all:vars]
    ansible_ssh_user=root
    ansible_ssh_private_key_file=<請替換成你的root使用者證照>
    ansible_python_interpreter=/usr/bin/python3
    複製程式碼
  • 目標主機,按照用途和分工不同, 分成不同的組, 說明如下:

    組名 說明
    nodes k8s叢集內所有的 master 主機和所有的 worker 主機
    master k8s叢集管理節點, 包含2個 子組, 分別是 first_masterother_master
    first_master 建立叢集時的第一個 Master
    other_master 要加入到現有叢集的其他 Master. 如果是 單master 模式下, 該組成員為空
    worker k8s叢集工作節點, 用於執行負載pod
    lb_and_ha 用於執行 k8s_kube-apiserverer負載均衡服務 + 高可用 的節點
  • 請根據網路規劃, 修改分組中的主機的 ip, host_name

  • host_name 應該是全域性唯一, 只能包含 英文字母,數字,中槓線 這3種字元

    為什麼?

    • 在 kubeadm init 初始化叢集 和 kubeadm join 新增節點到叢集的時候, 都是用了 --node-name 引數來指定節點的名稱, 我們的指令碼是使用主機名作為此引數的值, 因此需要為每個主機單獨設定一個不重複的主機名.
  • lb_and_ha 組中, 每個主機需要單獨設定 vip_interface 引數.

    為什麼?

    • vip_interface 被用來指定虛IP所繫結的網路卡裝置名稱
    • 主機上可能有不止一塊網路卡, 所以需要進行明確指定
    • 主機上的多塊網路卡可能設定成bond模式, 通過此引數來指定虛IP繫結到指定的 bond網路卡上
  • 請設定 ansible_ssh_private_key_file 為root使用者ssh登入目標主機的證照

全域性配置檔案

  • 檔案路徑: roles/common/defaults/main.yml

  • 檔案sample 如下, 檔案中的配置以下文的 案例描述 為例:

    ---
    # defaults file for common
    
    k8s:
        # 控制平面的 主機域名和埠號
        # ref: https://kubernetes.io/zh/docs/setup/production-environment/tools/kubeadm/high-availability/#%E4%BD%BF%E7%94%A8%E5%A0%86%E6%8E%A7%E5%88%B6%E5%B9%B3%E9%9D%A2%E5%92%8C-etcd-%E8%8A%82%E7%82%B9
        # kubeadm init --control-plane-endpoint "control_plane_dns:control_plane_port" ...(略)
        control_plane_dns: k8s.cluster.local
        control_plane_port: 6443
        # apiserver_advertise_address: 0.0.0.0
        apiserver_bind_port: 6443
        # ref: https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/#pod-network
        pod_network_cidr: "192.168.0.0/16"
        service_cidr: "10.96.0.0/12"
        service_dns_domain: "cluster.local"
        # 可選值: registry.cn-hangzhou.aliyuncs.com/google_containers  [官方文件](https://github.com/AliyunContainerService/sync-repo)
        # gcr.azk8s.cn/google_containers  [官方文件](http://mirror.azure.cn/help/gcr-proxy-cache.html)
        image_repository: "registry.cn-hangzhou.aliyuncs.com/google_containers"
    
    apt:
        docker:
            apt_key_url: https://download.docker.com/linux/ubuntu/gpg
            apt_repository: https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu
        k8s:
            apt_key_url: https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg
            apt_repository: https://mirrors.aliyun.com/kubernetes/apt/
    
    # master 節點個數
    master_count: "{{ groups['master'] | length }}"
    # 是否是單master模式
    single_master: "{{ (groups['master'] | length) == 1 }}"
    # 
    first_master: "{{ groups['first_master'] | first }}"
    
    ntpdate_server: cn.ntp.org.cn
    
    docker:
      # daemon.json 配置, 參考: https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file
      daemon:
        # Docker Hub映象伺服器
        registry-mirrors: 
          - https://dockerhub.azk8s.cn
          - https://docker.mirrors.ustc.edu.cn
          - https://reg-mirror.qiniu.com
        # 參考: https://kubernetes.io/zh/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#%E5%9C%A8%E6%8E%A7%E5%88%B6%E5%B9%B3%E9%9D%A2%E8%8A%82%E7%82%B9%E4%B8%8A%E9%85%8D%E7%BD%AE-kubelet-%E4%BD%BF%E7%94%A8%E7%9A%84-cgroup-%E9%A9%B1%E5%8A%A8%E7%A8%8B%E5%BA%8F
        exec-opts:
          - "native.cgroupdriver=systemd"
    
    複製程式碼
  • 配置引數說明

    • k8s.control_plane_dns
      • 控制平面的域名
    • k8s.control_plane_port
      • 控制平面的埠.
      • 如果我們希望在 master 的主機上可以執行控制平面的負載均衡服務, 則需要將此埠設定成一個與k8s.apiserver_bind_port不同的值
      • 如果是 單master 模式下的叢集, 實際上就不需要控制平面的負載均衡服務, 可以設定的與k8s.apiserver_bind_port 一致.
      • 在下文中的案例描述裡, 我們演示的過程是從單master叢集變化成3master叢集, 控制平面服務的高可用和負載均衡, 我們不放在master的機器上部署,而是選擇2臺worker主機來做一主一備方式的HA+LB的方式, 所以控制平面的埠就一開始按照單master的方式, 設定成與k8s.apiserver_bind_port, 預設 6443
    • k8s.apiserver_bind_port
      • kube-apiserverer 服務的埠,一般不做修改,預設 6443, 請參考 kubeadm init 的 --apiserver-bind-port 引數
    • k8s.pod_network_cidr
      • 請參考 kubeadm init 命令的 --pod-network-cidr 引數
    • k8s.service_cidr
      • 請參考 kubeadm init 命令的 --service-cidr 引數
    • k8s.service_dns_domain
      • 請參考 kubeadm init 命令的 --service-dns-domain 引數
    • k8s.image_repository
    • apt.docker.apt_key_url
      • Docker’s official GPG key.
    • apt.docker.apt_repository
    • apt.k8s.apt_key_url
      • Kubernetes 映象源 GPG key
    • apt.k8s.apt_repository
    • ntpdate_server
      • 時間同步伺服器域名

分階段執行指令碼

進入到指令碼原始碼中的 build_k8s 目錄 (即hosts 檔案所在的目錄)

第一步: 為所有的目標主機執行初始化設定

ansible-playbook prepare_all_host.yml
複製程式碼

第二步: 先建立一個單 master 的叢集

  1. 檢查全域性配置檔案 中的 k8s.control_plane_port, 我們使用預設值: 6443

  2. 更新所有節點的 /etc/hosts, 將控制平面的域名解析到 第一個master節點 的IP地址上

    為什麼?

    • 在建立叢集, 向叢集新增節點的過程, 我們需要保證通過 控制平面域名+控制平面埠 的方式, 可以訪問到master的kube-apiserverer服務. 所以在叢集的建立的過程中, 先暫時將控制平面域名解析到第一個Master 節點
    • 等待其餘的2個master都加入到叢集后, 再將3個master配置成高可用, 控制平面的虛IP生效後, 再更新所有節點的 /etc/hosts, 將控制平面的域名解析到虛IP.
  3. 執行以下命令進行更新控制平面域名解析操作, 注意下面示例中的傳參方式

    • 通過 -e "key1=value1 key2=value2 ..." 方式傳參
    • 需要指定 domain_namedomain_ip 2個引數
    • domain_name 為控制平面域名, 請根據制定的網路規劃進行賦值
    • domain_ip 為控制域名解析的目標IP, 這裡我們指定為第一個master的IP
    ansible-playbook set_hosts.yml -e "domain_name=k8s.cluster.local domain_ip=192.168.3.151"
    複製程式碼
  4. 執行指令碼, 開始建立第一個master節點

    ansible-playbook create_first_master_for_cluster.yml
    複製程式碼
  5. 指令碼執行完成後, 第一個master應該順利啟動了, ssh到 master-1 上執行以下命令, 檢視叢集節點資訊:

    kubectl get nodes
    複製程式碼

    應該出現如下資訊, 表明叢集已經順利建立了,儘管現在只有一個master-1節點

    NAME       STATUS     ROLES    AGE   VERSION
    master-1   NotReady   master   29s   v1.18.0
    複製程式碼

    在master-1上執行以下命令檢視叢集資訊

    kubectl cluster-info
    複製程式碼

    輸出如下資訊, 可以看到叢集已經是 running 狀態

    Kubernetes master is running at https://k8s.cluster.local:7443
    KubeDNS is running at https://k8s.cluster.local:7443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    
    To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
    複製程式碼

第三步: 新增其他的節點到叢集

執行指令碼, 新增 其他的 master 節點worker節點到叢集

注: 新增 --forks 1 以便一臺一臺的加. 因為測試中發現, 並行新增的時候, 有一定概率出現因ETCD發生重新選舉而導致新增Master失敗, 更多數量(超過4臺)的主機的同時加入叢集的情況, 因為硬體資源有限, 沒有測試過.

ansible-playbook --forks 1 add_other_node_to_cluster.yml
複製程式碼

檢查叢集節點資訊, 在一臺 master 節點上執行命令:

root@master-2:~# kubectl get nodes
NAME       STATUS     ROLES    AGE     VERSION
master-1   Ready      master   9m13s   v1.18.0
master-2   NotReady   master   4m5s    v1.18.0
master-3   NotReady   master   2m21s   v1.18.0
work-1     NotReady   <none>   110s    v1.18.0
work-2     NotReady   <none>   107s    v1.18.0
複製程式碼

第四步: 建立2臺負載均衡, 代理後面3臺master的kube-apiserverer服務

注: 單master節點模式不需要執行此步驟

完成了 第三步 之後, 叢集已經執行起來了. 只不過, 由於 控制平面的域名 是解析到 第一個 master 的IP 上的, 所以現在雖然有3臺master在叢集, 但是隻有第一個master才能夠通過控制平面的域名提供k8s叢集的 kube-apiserverer 服務.

下面, 我們選擇2臺除master節點之外的主機(可以是 worker 主機, 也可以是額外的2臺主機), 建立一套一主一備形式的 負載均衡 + 高可用

  1. 檢查主機清單檔案: hosts 中的 [lb_and_ha] 的2臺主機的 IP地址需要繫結虛IP的網路卡裝置名稱 是否設定正確

    # vip_interface 為虛IP所繫結的網路卡裝置名稱
    [lb_and_ha]
    192.168.3.154 vip_interface=eth0
    192.168.3.155 vip_interface=eth0
    複製程式碼
  2. 檢查 create_haproxy.yml 配置, 檔案sample 如下, 檔案中的配置以下文的 案例描述 為例:

    ---
    - name: Create a load blance using HAproxy
      hosts: lb_and_ha
      vars:
        # 負載均衡對外提供服務的埠
        service_bind_port: "{{ k8s.control_plane_port }}"
        # 後端伺服器使用的埠, 是給下面轉換的過濾器使用的, haproxy.cfg.j2 模板沒有使用該變數
        backend_server_port: "{{ k8s.apiserver_bind_port }}"
        # 轉換成形如: ['192.168.3.154:6443', '192.168.3.155:6443'] 的列表
        backend_servers: "{{ ansible_play_hosts_all | map('regex_replace', '^(.*)$',  '\\1:' + backend_server_port) | list }}"
        # 或者採用如下的方式, 手動設定, 這樣 backend_servers 可以由叢集外的主機來擔任
        # backend_servers:
        #   # - "<ip>:<port>"
        #   - "192.168.3.154:6443"
        #   - "192.168.3.155:6443"
    
        # 是否開啟 haproxy stats 頁面
        ha_stats_enable: True
        # haproxy stats 頁面的服務埠
        ha_stats_port: 1936
        # haproxy stats 頁面的 url
        ha_stats_url: /haproxy_stats
        # haproxy stats 頁面的訪問的使用者名稱
        ha_stats_user: admin
        # haproxy stats 頁面的訪問的密碼
        ha_stats_pwd: showmethemoney
        container_name: k8s_kube-apiserverers_haproxy
      tasks:
        - name: check parameters
          fail:
            msg: "Please setup backend_servers parameter"
          when: backend_servers == None or (backend_servers|count) == 0 or backend_servers[0] == '' or  backend_servers[0] == '<ip>:<port>'
    
          # 進行主機的基礎設定
        - import_role:
            name: basic_setup
    
        - name: pip install docker (python package)
          pip:
            executable: /usr/bin/pip3
            name: docker
            state: present
        
        - name: mkdir -p /opt/haproxy
          file:
            path: /opt/haproxy
            state: directory
    
        - name: get container info
          docker_container_info:
            name: "{{ container_name }}"
          register: ha_container
    
        - name: setup HAproxy configuration
          template:
            backup: True
            src: haproxy.cfg.j2
            dest: /opt/haproxy/haproxy.cfg
            mode: u=rw,g=r,o=r
          notify: restart HAproxy container
    
        - name: create HAproxy container
          docker_container:
            detach: yes
            image: haproxy:alpine
            name: "{{ container_name }}"
            volumes:
              - "/opt/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg"
            network_mode: bridge
            ports:
              - "{{ service_bind_port }}:{{ service_bind_port }}"
              - "{{ ha_stats_port }}:{{ ha_stats_port }}"
            restart_policy: always
            state: started
    
      handlers:
        - name: restart HAproxy container
          docker_container:
            name: "{{ container_name }}"
            state: started
            restart: yes
          when: ha_container.exists
    複製程式碼
  3. 在分配的2臺worker上建立負載均衡服務

ansible-playbook create_haproxy.yml
複製程式碼
  1. 指令碼執行完畢後, 我們可以在其中一臺機器上, 檢視 haproxy 的監控頁面, 例如: 192.168.3.155 http://192.168.3.155:1936/haproxy_stats , 訪問密碼為 create_haproxy.yml 中 ha_stats_pwd 的值

    haproxy

第五步: 將2臺負載均衡配置成一主一備的高可用方式, 虛IP生效

現在我們有2臺提供相同服務的負載均衡, 接下來我們將這2臺負載均衡配置成一主一備的方式, 並讓 虛IP 生效

  1. 檢查 create_keepalived.yml 配置, 檔案sample 如下, 檔案中的配置以下文的 案例描述 為例:

    ---
    - name: setup keepalived on target host
      hosts: lb_and_ha
      vars:
        # 虛IP
        virtual_ipaddress: 192.168.3.150/24
        keepalived_router_id: 99
        keepalived_password: FE3C5A94ACDC
        container_name: k8s_kube-apiserverers_keepalived
      tasks:
        # 進行主機的基礎設定
        - import_role:
            name: basic_setup
        
        - import_role:
            name: install_docker
    
        - name: pip install docker (python package)
          pip:
            executable: /usr/bin/pip3
            name: docker
            state: present
    
        - name: mkdir -p /opt/keepalived
          file:
            path: /opt/keepalived
            state: directory
    
        - name: get container info
          docker_container_info:
            name: "{{ container_name }}"
          register: the_container
    
        - name: copy Dockerfile to target host
          copy:
            src: keepalived.dockerfile
            dest: /opt/keepalived/Dockerfile
    
        - name: build keepalived image
          docker_image:
            name: keepalived:latest
            source: build
            build:
              path: /opt/keepalived
              pull: yes
    
        - name: setup keepalived configuration
          template:
            src: keepalived.conf.j2
            dest: /opt/keepalived/keepalived.conf
            mode: u=rw,g=r,o=r
          notify: restart keepalived container
    
        - name: create keepalived container
          docker_container:
            capabilities:
              - NET_ADMIN
              - NET_BROADCAST
              - NET_RAW
            network_mode: host
            detach: yes
            image: keepalived:latest
            name: "{{ container_name }}"
            volumes:
              - "/opt/keepalived/keepalived.conf:/etc/keepalived/keepalived.conf"
            restart_policy: always
            state: started
    
      handlers:
        - name: restart keepalived container
          docker_container:
            name: "{{ container_name }}"
            state: started
            restart: yes
          when: the_container.exists
    
    複製程式碼
  2. 確定 虛IP 配置項 virtual_ipaddress 與規劃的一致

    注意: 這裡的虛ip配置, ip地址需要加上子網掩碼位數的標識, 例如: /24

  3. 執行指令碼, 在 3 個master上配置高可用(keepalived方式)

    ansible-playbook create_keepalived.yml
    複製程式碼
  4. 執行完畢後, 檢查是否能夠 ping 通虛IP. 能ping通, 說明主備模式下的虛IP已經生效

第六步: 將控制平面域名解析至虛IP

目前為止, 控制平面的域名 還是指向 master-1. 接著我們需要將 控制平面的域名 解析到 虛IP上, 這樣就可以通過 控制平面的域名 來訪問到上一步建立的 負載均衡 服務了.

我們需要更新所有節點的 /etc/hosts, 將控制平面的域名解析到虛IP上

執行指令碼命令:

在此例中, 虛IP地址為: 192.168.3.150

ansible-playbook set_hosts.yml -e "domain_name=k8s.cluster.local domain_ip=192.168.3.150"
複製程式碼

在節點主機對控制平面域名進行 ping 測試, 驗證域名已正確解析到虛IP上.

在master節點主機上, 執行命令, 檢查是否能正常檢視節點資訊 (kubectl 命令會通過控制平面域名和埠來訪問控制平面的 kube-apiserverer):

kubectl get nodes
複製程式碼

相關文章