AWS - Install Kubernetes using Ansible

Quick note: the original post dates from 25-11-2019 but got updated at 01-04-2020 with latest Kubernetes version.

Introduction

This blog post is a follow up of a post we wrote couple of days ago where we created 3 EC2 instances on AWS. That blog post can be found here. If you want to follow along with this guide, then use that post to create 3 instances.

Executing the code from that post, will result in three EC2 instances on AWS:

AWS

The output of the Terraform script gives me back the following IP addresses to be used in our Ansible hosts file.

Outputs:

instance_ip_addr = [
  "172.31.13.140",
  "172.31.5.24",
  "172.31.14.2",
]
instance_ips = [
  "18.203.68.253",
  "3.249.4.142",
  "3.249.141.242",
]

Note: ensure to select instance types with 2CPU’s or more (t2.medium as a minimim in AWS), otherwise you will get an error message later: > The number of available CPUs 1 is less than the required 2

Using Ansible to install Kubernetes

In this post, we will focus on using Ansible to install Kubernetes. We will base ourselves on an excellent post written by the DigitalOcean community but finetune it for our AWS endeavour. Also, what we do here with Ansible are essentially the same steps as I did in this post where I configured Kubernetes manually.

First of all, let’s create a hosts file for our Ansible scripts. We will define 1 master and 2 workers. The IP addresses are the same as the ones in the DigitalOcean screenshot (obviously).

#hosts
[masters]
master ansible_host=18.203.68.253 ansible_user=ubuntu

[workers]
worker1 ansible_host=3.249.4.142 ansible_user=ubuntu
worker2 ansible_host=3.249.141.242 ansible_user=ubuntu

[all:vars]
ansible_python_interpreter=/usr/bin/python3
ansible_ssh_private_key_file=~/.ssh/AWS-Cisco.pem

Next, we will define create a file to install all the updates, to create the ubuntu user and ensure the ubuntu user has sudo rights.

- hosts: all
  become: yes
  tasks:
    - name: create the 'ubuntu' user
      user: name=ubuntu append=yes state=present createhome=yes shell=/bin/bash

    - name: allow 'ubuntu' to have passwordless sudo
      lineinfile:
        dest: /etc/sudoers
        line: 'ubuntu ALL=(ALL) NOPASSWD: ALL'
        validate: 'visudo -cf %s'

    - name: set up authorized keys for the ubuntu user
      authorized_key: user=ubuntu key="{{item}}"
      with_file:
        - ~/.ssh/id_rsa.pub

Next up, we will create a different file that installs the Kubernetes specific dependencies on the 3 nodes. First of all, we start with the installation of Docker, then we add Kubernetes as well as the kubelet and kubeadm toolset.

#kube-dependencies.yml
- hosts: all
  become: yes
  tasks:
   - name: install Docker
     apt:
       name: docker.io
       state: present
       update_cache: true

   - name: install APT Transport HTTPS
     apt:
       name: apt-transport-https
       state: present

   - name: add Kubernetes apt-key
     apt_key:
       url: https://packages.cloud.google.com/apt/doc/apt-key.gpg
       state: present

   - name: add Kubernetes' APT repository
     apt_repository:
      repo: deb http://apt.kubernetes.io/ kubernetes-xenial main
      state: present
      filename: 'kubernetes'

   - name: install kubelet
     apt:
       name: kubelet
       state: present
       update_cache: true

   - name: install kubeadm
     apt:
       name: kubeadm
       state: present

- hosts: master
  become: yes
  tasks:
   - name: install kubectl
     apt:
       name: kubectl
       state: present

When the above is finished, we will create a specific file for the master node. This file will take care of the initialization of the Kubernetes cluster, will create the .kube directory and will copy the admin.conf file to the user’s kube.config file and also install the Calico network. Just similar as what we did in the manual process.

#master.yml
- hosts: master
  become: yes
  tasks:
    - name: initialize the cluster
      shell: kubeadm init --pod-network-cidr=10.244.0.0/16 >> cluster_initialized.txt
      args:
        chdir: $HOME
        creates: cluster_initialized.txt

    - name: create .kube directory
      become: yes
      become_user: ubuntu
      file:
        path: $HOME/.kube
        state: directory
        mode: 0755

    - name: copy admin.conf to user's kube config
      copy:
        src: /etc/kubernetes/admin.conf
        dest: /home/ubuntu/.kube/config
        remote_src: yes
        owner: ubuntu

    - name: install Pod network
      become: yes
      become_user: ubuntu
      shell: kubectl apply -f https://docs.projectcalico.org/v3.9/manifests/calico.yaml >> pod_network_setup.txt
      args:
        chdir: $HOME
        creates: pod_network_setup.txt

Note: if you prefer to use Flannel CNI plugin instead of Calico, it suffices to replace that part with:

   - name: install Pod network
      become: yes
      become_user: ubuntu
      shell: kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml >> pod_network_setup.txt
      args:
        chdir: $HOME
        creates: pod_network_setup.txt

Last file we create is the workers.yml file. In that file, we will first retrieve the join command from the master node and then we will join worker nodes to the cluster.

- hosts: master
  become: yes
  gather_facts: false
  tasks:
    - name: get join command
      shell: kubeadm token create --print-join-command
      register: join_command_raw

    - name: set join command
      set_fact:
        join_command: "{{ join_command_raw.stdout_lines[0] }}"


- hosts: workers
  become: yes
  tasks:
    - name: join cluster
      shell: "{{ hostvars['master'].join_command }} >> node_joined.txt"
      args:
        chdir: $HOME
        creates: node_joined.txt

Executing the Ansible playbooks

First, we will perform the initial configuration as described above.

WAUTERW-M-65P7:Ansible wauterw$ ansible-playbook -i hosts initial.yml

PLAY [all] ************************************************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************************************
ok: [worker2]
ok: [master]
ok: [worker1]

TASK [create the 'ubuntu' user] ***************************************************************************************************************
ok: [master]
ok: [worker2]
ok: [worker1]

TASK [allow 'ubuntu' to have passwordless sudo] ***********************************************************************************************
changed: [worker1]
changed: [worker2]
changed: [master]

TASK [set up authorized keys for the ubuntu user] *********************************************************************************************
changed: [worker2] => (item=ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC6ULleEqtsN/OoaAmQbZCuHjNgDrRr3Gur33n6WfpuHCDNAiqTo5kr0jTKlhWPiBTtuokbONvyJGGL9qkpSmM5isr/fK88WpUOqEc28wYENHCQXsdRMZUYAdxPrtdQgAXSMJEHye9ea8e7E/U275c9D1kKNcHV/Xf06RNSqbHTFvbX79WqK3F6I6DG8rcogw+WGw0pH7lqg/8k1aWjH+apNXjl8NO3ABzLNyc3I6ytUvFOHIkRZeEPNuH4mpb122VAPTYiSpU+F6E8PQUomnGPSSMwfU+T0Sq33H4TwXkgRnuuhD/ZutJjb31mARluxU/nf2FlKuLKHuImc39opoMF wauterw@WAUTERW-M-65P7)
changed: [worker1] => (item=ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC6ULleEqtsN/OoaAmQbZCuHjNgDrRr3Gur33n6WfpuHCDNAiqTo5kr0jTKlhWPiBTtuokbONvyJGGL9qkpSmM5isr/fK88WpUOqEc28wYENHCQXsdRMZUYAdxPrtdQgAXSMJEHye9ea8e7E/U275c9D1kKNcHV/Xf06RNSqbHTFvbX79WqK3F6I6DG8rcogw+WGw0pH7lqg/8k1aWjH+apNXjl8NO3ABzLNyc3I6ytUvFOHIkRZeEPNuH4mpb122VAPTYiSpU+F6E8PQUomnGPSSMwfU+T0Sq33H4TwXkgRnuuhD/ZutJjb31mARluxU/nf2FlKuLKHuImc39opoMF wauterw@WAUTERW-M-65P7)
changed: [master] => (item=ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC6ULleEqtsN/OoaAmQbZCuHjNgDrRr3Gur33n6WfpuHCDNAiqTo5kr0jTKlhWPiBTtuokbONvyJGGL9qkpSmM5isr/fK88WpUOqEc28wYENHCQXsdRMZUYAdxPrtdQgAXSMJEHye9ea8e7E/U275c9D1kKNcHV/Xf06RNSqbHTFvbX79WqK3F6I6DG8rcogw+WGw0pH7lqg/8k1aWjH+apNXjl8NO3ABzLNyc3I6ytUvFOHIkRZeEPNuH4mpb122VAPTYiSpU+F6E8PQUomnGPSSMwfU+T0Sq33H4TwXkgRnuuhD/ZutJjb31mARluxU/nf2FlKuLKHuImc39opoMF wauterw@WAUTERW-M-65P7)

PLAY RECAP ************************************************************************************************************************************
master                     : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
worker1                    : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
worker2                    : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
 

Next, we will install the Kubernetes dependencies:

WAUTERW-M-65P7:Ansible wauterw$ ansible-playbook -i hosts kube-dependencies.yml 

PLAY [all] ************************************************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************************************
ok: [worker1]
ok: [worker2]
ok: [master]

TASK [install Docker] *************************************************************************************************************************
 [WARNING]: Could not find aptitude. Using apt-get instead

changed: [master]
changed: [worker2]
changed: [worker1]

TASK [install APT Transport HTTPS] ************************************************************************************************************
changed: [master]
changed: [worker2]
changed: [worker1]

TASK [add Kubernetes apt-key] *****************************************************************************************************************
changed: [worker2]
changed: [master]
changed: [worker1]

TASK [add Kubernetes' APT repository] *********************************************************************************************************
changed: [worker2]
changed: [worker1]
changed: [master]

TASK [install kubelet] ************************************************************************************************************************
changed: [worker2]
changed: [master]
changed: [worker1]

TASK [install kubeadm] ************************************************************************************************************************
changed: [worker1]
changed: [worker2]
changed: [master]

PLAY [master] *********************************************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************************************
ok: [master]

TASK [install kubectl] ************************************************************************************************************************
ok: [master]

PLAY RECAP ************************************************************************************************************************************
master                     : ok=9    changed=6    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
worker1                    : ok=7    changed=6    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
worker2                    : ok=7    changed=6    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  

When the dependencies are installed, it’s time to configure the master.

WAUTERW-M-65P7:Ansible wauterw$ ansible-playbook -i hosts master.yml 

PLAY [master] *********************************************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************************************
ok: [master]

TASK [initialize the cluster] *****************************************************************************************************************
changed: [master]

TASK [create .kube directory] *****************************************************************************************************************
changed: [master]

TASK [copy admin.conf to user's kube config] **************************************************************************************************
changed: [master]

TASK [install Pod network] ********************************************************************************************************************
changed: [master]

PLAY RECAP ************************************************************************************************************************************
master                     : ok=5    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Next up, we are going to configure the workers.

WAUTERW-M-65P7:Ansible wauterw$ ansible-playbook -i hosts workers.yml 

PLAY [master] *********************************************************************************************************************************

TASK [get join command] ***********************************************************************************************************************
changed: [master]

TASK [set join command] ***********************************************************************************************************************
ok: [master]

PLAY [workers] ********************************************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************************************
ok: [worker2]
ok: [worker1]

TASK [join cluster] ***************************************************************************************************************************
changed: [worker2]
changed: [worker1]

PLAY RECAP ************************************************************************************************************************************
master                     : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
worker1                    : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
worker2                    : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Validate the K8S cluster

First, we need to SSH into the K8S master. In our case, this is available on ‘IP address’.

ssh ubuntu@18.203.68.253

This works, since we have configured the EC2 instances with our public key that is stored in our ~/.ssh directory (id_rsa,pub). Since we are also using our security key from AWS, we can also login as follows.

ssh -i AWS.pem ubuntu@18.203.68.253

Next, let’s see if our nodes are up:

ubuntu@ip-172-31-13-140:~$ kubectl get nodes -o  wide
NAME               STATUS   ROLES    AGE     VERSION   INTERNAL-IP     EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION    CONTAINER-RUNTIME
ip-172-31-13-140   Ready    master   11m     v1.18.0   172.31.13.140   <none>        Ubuntu 18.04.1 LTS   4.15.0-1021-aws   docker://19.3.6
ip-172-31-14-2     Ready    <none>   4m29s   v1.18.0   172.31.14.2     <none>        Ubuntu 18.04.1 LTS   4.15.0-1021-aws   docker://19.3.6
ip-172-31-5-24     Ready    <none>   4m29s   v1.18.0   172.31.5.24     <none>        Ubuntu 18.04.1 LTS   4.15.0-1021-aws   docker://19.3.6

And our namespaces:

ubuntu@ip-172-31-13-140:~$ kubectl get namespaces
NAME              STATUS   AGE
default           Active   12m
kube-node-lease   Active   12m
kube-public       Active   12m
kube-system       Active   12m

We are now ready to deploy some applications onto our cluster. We’ll blog about that at some later point in time. First wanted to get my head around the installation of the K8S clusters onto several plartforms.

See you next time!