yet.org

Kubernetes step by step

Tectonic from CoreOS is an enterprise-grade Kubernetes solution which simplifies management operation of a k8s environment by leveraging CoreOS, fleet, Rkt and Flannel. In this article we’ll manually build a cluster of three CoreOS nodes on top of VMware Fusion to see how all of this fits together.

Introduction

Kubernetes is already available, ready to consume as a Service from Google, Platform9 where you’ll quickly be up and running. They are also some automation tooling, like Ansible Playbooks, SaltStack or bootKube, specifically built to rapidly deploy a cluster with all the required components.

Before automating the deployment, I’m curious to do it step by step, to get a really good understanding of the internals. In this guide, heavily inspired by the CoreOS one, we’ll be using components of Kubernetes itself, the kubelet, to spin-out the rest of the infrastructure, more containers. I find it pretty elegant that Kubernetes can host itself, by the way Kubernetes community is working on improving this experience even further.

So lets jumstart our Kubernetes cluster by preparing 3 VMs running CoreOS and configure them as a 3-node etcd cluster.

Beware, 5-node is recommended for production environment, or loosing one to maintenance will put you at risk if anything else goes wrong.

CoreOS deployment on VMware Fusion

Start by downloading the latest stable CoreOS VMware image:

curl -LO https://stable.release.core-os.net/amd64-usr/current/coreos_production_vmware_ova.ova

As we’ve seen in our About Kubernetes article, etcd is a crucial component of k8s. So lets first create a etcd cluster, to do so we need to generate a new cluster token which will be used for node discovery. Three nodes should be enough for our demo environment.

Generate a new cluster token using the URL below, update the size if you want more nodes. The size specified here will be used to determine if all members have joined the cluster. After bootstrapping the cluster can grow or shrink.

curl -w "\n" 'https://discovery.etcd.io/new?size=3'

To easily configure each node, we’ll be using a cloud-config file, inspired by the cloud-init project. Lets create this file

# vi user_data

The content should be similar to the following, update below your ssh-key, token, IP addresses …

#cloud-config

hostname: kube-01
ssh_authorized_keys:
  - "ssh-rsa AAAAB3NzaC1yc........"

coreos:
  etcd2:
    discovery: https://discovery.etcd.io/<token>
    listen-client-urls: http://12.0.0.11:2379,http://127.0.0.1:2379
    initial-advertise-peer-urls: http://12.0.0.11:2380
    listen-peer-urls: http://12.0.0.11:2380

 units:
   - name: 00-enostar.network
     runtime: true
     content: |
       [Match]
       Name=en*

       [Network]
       Address=12.0.0.11/24
       Gateway=12.0.0.2
       DNS=192.168.1.221

   - name: etcd2.service
     command: start

   - name: fleet.service
     command: start

Above we configure CoreOS to start etcd and fleet which will be used to build a HA cluster.

Copy and Paste it to CoreOS online validator to check for any syntax error

https://coreos.com/validate/

Now Create a config-drive disk to automatically configure your CoreOS node at Boot time, on macOS

mkdir -p ~/new-drive/openstack/latest
cp user_data ~/new-drive/openstack/latest/user_data
hdiutil makehybrid -iso -joliet -default-volume-name config-2 -o configdrive-1.iso ~/new-drive
rm -r ~/new-drive

For Linux just replace hdiutil command line by

mkisofs -R -V config-2 -o configdrive.iso /tmp/new-drive

Everything is now ready to open the OVA file downloaded earlier. From VMware Fusion select

File > Import...

You can then select the previously downloaded OVA file, click Retry if you’ve been asked, you should then see

Click on Customize settings and connect the previously created Config Drive ISO

Note: Each CoreOS node will require its own Config Drive ISO, its used upon each reboot, do not disconnect it afterwards.

Depending on your available resource, you can also scale-up CPU and Memory.

Power-on your CoreOS VM and ssh in

ssh core@12.0.0.11

Note: If you cannot login, you can autologin from GRUB. Reboot your node and Press e at GRUB prompt to add coreos.autologin as boot parameter and Press F10.

You can then look at cloudinit logs

journalctl _EXE=/usr/bin/coreos-cloudinit

Verify etcd status

systemctl status etcd2

If there is anything wrong above, check etcd logs

journalctl -f -t etcd2

If etcd isn’t running you can verify its corresponding systemd runtime unit

cat /run/systemd/system/etcd2.service.d/20-cloudinit.conf

And finally you can restart it

sudo systemctl restart etcd2

Note: Internet Access and DNS resolution is required for the etcd cluster bootstrapping, it access the discovery URL at discovery.etcd.io. Refer to the docs for offline bootstrapping. Be sure to change the token if you need to start the bootstrapping process from scratch.

Repeat a similar process for two more nodes and run etcdctl to check the cluster-wide health information. It will contact all the members of the cluster and collect the health information for you.

etcdctl cluster-health
member 1b0edb1b9b4ea4c5 is healthy: got healthy result from http://localhost:2379
member a10ff4050999b675 is healthy: got healthy result from http://localhost:2379
member b48e6630d8c3f3cd is healthy: got healthy result from http://localhost:2379

Check the member list

etcdctl member list
1b0edb1b9b4ea4c5: name=12f3c00...ab46874fe peerURLs=http://12.0.0.13:2380 clientURLs=http://localhost:2379,http://localhost:4001 isLeader=false
a10ff4050999b675: name=65eaf4f...9a4a74f3a9 peerURLs=http://12.0.0.12:2380 clientURLs=http://localhost:2379,http://localhost:4001 isLeader=false
b48e6630d8c3f3cd: name=b55f820...7bc36a9 peerURLs=http://12.0.0.11:2380 clientURLs=http://localhost:2379,http://localhost:4001 isLeader=true

Congrat, you have a fully operational 3-node etcd cluster and a leader elected.

TLS Assests

You enter the boring part, which by itself justify next time to automate all of this, but it’s a good learning experience. So lets continue.

Kubernetes will validate client using certificate authentication, so we need to put in place a Certificate Authority and generate the proper credentials.

Cluster Root CA

On kube–01 node, create a new Certificate authority, used to sign our certificates

openssl genrsa -out ca-key.pem 2048
openssl req -x509 -new -nodes -key ca-key.pem -days 10000 -out ca.pem -subj "/CN=kube-ca"

OpenSSL Config k8s

Prepare the following configuration file

vi openssl.cnf

[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = kubernetes
DNS.2 = kubernetes.default
DNS.3 = kubernetes.default.svc
DNS.4 = kubernetes.default.svc.cluster.local
IP.1 = 13.0.0.1
IP.2 = 12.0.0.11

13.0.0.1 correspond to the first IP of the CIDR network to use for service cluster VIP. [K8S_SERVICE_IP]
12.0.0.11 is the IP of our first CoreOS node, if we had multiple k8s master node we could put here a Load Balancer Virtual IP [MASTER_HOST]

API Server Keypair

Now create a Keypair for your API Server

openssl genrsa -out apiserver-key.pem 2048
openssl req -new -key apiserver-key.pem -out apiserver.csr -subj "/CN=kube-apiserver" -config openssl.cnf
openssl x509 -req -in apiserver.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out apiserver.pem -days 365 -extensions v3_req -extfile openssl.cnf

Workers Keypairs

For security concerns, each worker node will be using its own TLS Certificate. Create the following configuration file for OpenSSL

vi worker-openssl.cnf

[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
IP.1 = $ENV::WORKER_IP

Now generates kube–02 Worker Keypair

openssl genrsa -out kube-02-worker-key.pem 2048
WORKER_IP=12.0.0.12 openssl req -new -key kube-02-worker-key.pem -out kube-02-worker.csr -subj "/CN=kube-02" -config worker-openssl.cnf
WORKER_IP=12.0.0.12 openssl x509 -req -in kube-02-worker.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out kube-02-worker.pem -days 365 -extensions v3_req -extfile worker-openssl.cnf

Repeat the process above for kube–03, but replace 12.0.0.12 by 12.0.0.13 and kube–02 by kube–03.

Cluster Admin Keypair

Generate the last keypair like this

openssl genrsa -out admin-key.pem 2048
openssl req -new -key admin-key.pem -out admin.csr -subj "/CN=kube-admin"
openssl x509 -req -in admin.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out admin.pem -days 365

Kubernetes Master Node

TLS Assets

We are now ready to transform kube–01 node to a single Kubernetes Master Node. First put in place the TLS keys at their expected location.

On kube–01 do the following

mkdir -p /etc/kubernetes/ssl
cp /home/core/ca.pem /etc/kubernetes/ssl
cp /home/core/apiserver.pem /etc/kubernetes/ssl
cp /home/core/apiserver-key.pem /etc/kubernetes/ssl

Set proper permissions

sudo chmod 600 /etc/kubernetes/ssl/*-key.pem
sudo chown root:root /etc/kubernetes/ssl/*-key.pem

Flannel

Flannel provides a software defined overlay network for routing traffic to/from pods. Lets configure it

mkdir /etc/flannel
vi /etc/flannel/options.env

FLANNELD_IFACE=12.0.0.11
FLANNELD_ETCD_ENDPOINTS=http://12.0.0.11:2379,http://12.0.0.12:2379,http://12.0.0.13:2379

Above we’ve put first the Master node IP address and then all etcd node IP addresses.

Now create the following systemd drop-in, its a method for overriding parameters of a systemd unit. In this case it will allow to use the above configuration when Flannel starts.

mkdir /etc/systemd/system/flanneld.service.d/
vi /etc/systemd/system/flanneld.service.d/40-ExecStartPre-symlink.conf

[Service]
ExecStartPre=/usr/bin/ln -sf /etc/flannel/options.env /run/flannel/options.env

Docker

We’ll now use the same drop-in mechanism to make sure Docker is configured to use Flannel, which is masterless. To achieve that goal its only necessary to make sure flanneld is already running when Docker starts.

mkdir /etc/systemd/system/docker.service.d/
vi /etc/systemd/system/docker.service.d/40-flannel.conf

[Unit]
Requires=flanneld.service
After=flanneld.service

Kubelet

Kubelet is an agent running on each node that start and stops pods and do other machine related tasks.

Create the following Kubelet Unit

vi /etc/systemd/system/kubelet.service

[Service]
ExecStartPre=/usr/bin/mkdir -p /etc/kubernetes/manifests

Environment=KUBELET_VERSION=v1.2.4_coreos.cni.1
ExecStart=/usr/lib/coreos/kubelet-wrapper \
    --api-servers=http://127.0.0.1:8080 \
    --network-plugin-dir=/etc/kubernetes/cni/net.d \
    --network-plugin=cni \
    --register-schedulable=false \
    --allow-privileged=true \
    --config=/etc/kubernetes/manifests \
    --hostname-override=12.0.0.11 \
    --cluster-dns=13.0.0.10 \
    --cluster-domain=cluster.local
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target

v1.2.4_coreos.cni.1 is the latest Hyperkube release which includes Container Networking Interface (CNI), required for Calico. You can check on quay.io if you want a newer version.
--register-schedulable=false to make sure no Pods will be running on our master node
--config=/etc/kubernetes/manifests watch directory for pods to run
12.0.0.11 this node public routable IP
13.0.0.10 is the DNS_SERVICE_IP, it should be in the SERVICE_IP_RANGE which is 13.0.0.0/24 but cannot be the first IP. The same IP must be configured on all worker nodes to enable DNS Service Discovery.

API Server Pod

Now that we have our nice kubelet ready, we can leverage it to deploy our stateless k8s API Server. All we need to do is place our Pod manifest in the configured watch directory. The kubelet will make sure it stays running.

Create the following Pod manifest

mkdir -p /etc/kubernetes/manifests
vi /etc/kubernetes/manifests/kube-apiserver.yaml

apiVersion: v2
kind: Pod
metadata:
  name: kube-apiserver
  namespace: kube-system
spec:
  hostNetwork: true
  containers:
  - name: kube-apiserver
    image: quay.io/coreos/hyperkube:v1.2.4_coreos.1
    command:
    - /hyperkube
   - apiserver
   - --bind-address=0.0.0.0
   - --etcd-servers=http://12.0.0.11:2379,http://12.0.0.12:2379,http://12.0.0.13:2379
   - --allow-privileged=true
   - --service-cluster-ip-range=13.0.0.0/24
   - --secure-port=443
   - --advertise-address=12.0.0.11
   - --admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,ResourceQuota
   - --tls-cert-file=/etc/kubernetes/ssl/apiserver.pem
   - --tls-private-key-file=/etc/kubernetes/ssl/apiserver-key.pem
   - --client-ca-file=/etc/kubernetes/ssl/ca.pem
   - --service-account-key-file=/etc/kubernetes/ssl/apiserver-key.pem
   - --runtime-config=extensions/v1beta1=true,extensions/v1beta1/thirdpartyresources=true
   ports:
   - containerPort: 443
     hostPort: 443
     name: https
   - containerPort: 8080
     hostPort: 8080
     name: local
   volumeMounts:
   - mountPath: /etc/kubernetes/ssl
     name: ssl-certs-kubernetes
     readOnly: true
   - mountPath: /etc/ssl/certs
     name: ssl-certs-host
     readOnly: true
 volumes:
 - hostPath:
     path: /etc/kubernetes/ssl
   name: ssl-certs-kubernetes
 - hostPath:
     path: /usr/share/ca-certificates
   name: ssl-certs-host

http://12.0.0.11:2379,http://12.0.0.12:2379,http://12.0.0.13:2379 are ETCD_ENDPOINTS, where etcd is running.
13.0.0.0/24 is the SERVICE_IP_RANGE, this range isn’t accessible from the network, it’s only locally significant to the host. Traffic from a container destinated to such a service address will be NATed when traversing the kube-proxy. The proxy will then load balance to one of the matching container for the destinated service, so the destination IP on the wire will be one within the Flannel Range IP, here on the 14.0.0./24 network.
12.0.0.11 is this node routable IP Address

kube-proxy Pod

The kube-proxy is responsible for directing traffic destined for specific services and pods to the correct location

Lets use the same mechanism for our kube-proxy Pod by creating the following Manifest

vi /etc/kubernetes/manifests/kube-proxy.yaml

apiVersion: v1
kind: Pod
metadata:
  name: kube-proxy
  namespace: kube-system
spec:
  hostNetwork: true
  containers:
  - name: kube-proxy
    image: quay.io/coreos/hyperkube:v1.2.4_coreos.1
    command:
    - /hyperkube
    - proxy
    - --master=http://127.0.0.1:8080
    - --proxy-mode=iptables
    securityContext:
      privileged: true
    volumeMounts:
    - mountPath: /etc/ssl/certs
      name: ssl-certs-host
      readOnly: true
  volumes:
  - hostPath:
      path: /usr/share/ca-certificates
    name: ssl-certs-host

kube-controller-manager Pod

And now create a manifest for our kube-controller which is responsible for reconciling any required actions based on changes to Replication Controllers.

vi /etc/kubernetes/manifests/kube-controller-manager.yaml

apiVersion: v1
kind: Pod
metadata:
  name: kube-controller-manager
  namespace: kube-system
spec:
  hostNetwork: true
  containers:
  - name: kube-controller-manager
    image: quay.io/coreos/hyperkube:v1.2.4_coreos.1
    command:
    - /hyperkube
    - controller-manager
    - --master=http://127.0.0.1:8080
    - --leader-elect=true
    - --service-account-private-key-file=/etc/kubernetes/ssl/apiserver-key.pem
    - --root-ca-file=/etc/kubernetes/ssl/ca.pem
    livenessProbe:
      httpGet:
        host: 127.0.0.1
        path: /healthz
        port: 10252
      initialDelaySeconds: 15
      timeoutSeconds: 1
    volumeMounts:
    - mountPath: /etc/kubernetes/ssl
      name: ssl-certs-kubernetes
      readOnly: true
    - mountPath: /etc/ssl/certs
      name: ssl-certs-host
      readOnly: true
  volumes:
  - hostPath:
      path: /etc/kubernetes/ssl
    name: ssl-certs-kubernetes
  - hostPath:
      path: /usr/share/ca-certificates
    name: ssl-certs-host

kube-scheduler Pod

Same for the scheduler

vi /etc/kubernetes/manifests/kube-scheduler.yaml

apiVersion: v1
kind: Pod
metadata:
  name: kube-scheduler
  namespace: kube-system
spec:
  hostNetwork: true
  containers:
  - name: kube-scheduler
    image: quay.io/coreos/hyperkube:v1.2.4_coreos.1
    command:
    - /hyperkube
    - scheduler
    - --master=http://127.0.0.1:8080
    - --leader-elect=true
    livenessProbe:
      httpGet:
        host: 127.0.0.1
        path: /healthz
        port: 10251
      initialDelaySeconds: 15
      timeoutSeconds: 1

Calico Node Container

Calico provides network policy to our cluster. It will run on all hosts to connect containers to the flannel overlay network and enforce network policy created using k8s policy API. It restrict Pod to only talk to authorized resources only.

Create the following Unit

vi /etc/systemd/system/calico-node.service

[Unit]
Description=Calico per-host agent
Requires=network-online.target
After=network-online.target

[Service]
Slice=machine.slice
Environment=CALICO_DISABLE_FILE_LOGGING=true
Environment=HOSTNAME=12.0.0.11
Environment=IP=12.0.0.11
Environment=FELIX_FELIXHOSTNAME=12.0.0.11
Environment=CALICO_NETWORKING=false
Environment=NO_DEFAULT_POOLS=true
Environment=ETCD_ENDPOINTS=http://12.0.0.11:2379,http://12.0.0.12:2379,http://12.0.0.13:2379
ExecStart=/usr/bin/rkt run --inherit-env --stage1-from-dir=stage1-fly.aci \
--volume=modules,kind=host,source=/lib/modules,readOnly=false \
--mount=volume=modules,target=/lib/modules \
--trust-keys-from-https quay.io/calico/node:v0.19.0

KillMode=mixed
Restart=always
TimeoutStartSec=0

[Install]
WantedBy=multi-user.target

12.0.0.11 this node routable IP Address
http://12.0.0.11:2379,http://12.0.0.12:2379,http://12.0.0.13:2379 are ETCD_ENDPOITNS

policy-agent Pod

We are almost done on the master node. This agent monitors the API for changes related to network policies and configures Calico to implement them.

vi /etc/kubernetes/manifests/policy-agent.yaml

apiVersion: v1
kind: Pod
metadata:
  name: calico-policy-agent
  namespace: calico-system
spec:
  hostNetwork: true
  containers:
    # The Calico policy agent.
    - name: k8s-policy-agent
      image: calico/k8s-policy-agent:v0.1.4
      env:
        - name: ETCD_ENDPOINTS
          value: "http://12.0.0.11:2379,http://12.0.0.12:2379,http://12.0.0.13:2379"
        - name: K8S_API
          value: "http://127.0.0.1:8080"
        - name: LEADER_ELECTION
          value: "true"
    # Leader election container used by the policy agent.
    - name: leader-elector
      image: quay.io/calico/leader-elector:v0.1.0
      imagePullPolicy: IfNotPresent
      args:
        - "--election=calico-policy-election"
        - "--election-namespace=calico-system"
        - "--http=127.0.0.1:4040"

http://12.0.0.11:2379,http://12.0.0.12:2379,http://12.0.0.13:2379 are ETCD_ENDPOITNS

CNI Config

Lets create a kubelet CNI configuration file to instruct it to call the flannel plugin but then to delegate control to the Calico plugin.

mkdir -p /etc/kubernetes/cni/net.d/
vi /etc/kubernetes/cni/net.d/10-calico.conf

{
    "name": "calico",
    "type": "flannel",
    "delegate": {
        "type": "calico",
        "etcd_endpoints": "http://12.0.0.11:2379,http://12.0.0.12:2379,http://12.0.0.13:2379",
        "log_level": "none",
        "log_level_stderr": "info",
        "hostname": "${12.0.0.10}",
        "policy": {
            "type": "k8s",
            "k8s_api_root": "http://127.0.0.1:8080/api/v1/"
        }
    }
}

http://12.0.0.11:2379,http://12.0.0.12:2379,http://12.0.0.13:2379 are ETCD_ENDPOINTS
12.0.0.10 this node routable IP Address

Start Services

We are now ready to start all of the master components. Tell systemd that you’ve changed units on disk for it to rescan everything

sudo systemctl daemon-reload

Configure flannel Pod network IP range, it uses etcd and will encapsulate traffic using VXLAN.

curl -X PUT -d "value={\"Network\":\"14.0.0.0/16\",\"Backend\":{\"Type\":\"vxlan\"}}" "http://12.0.0.11:2379/v2/keys/coreos.com/network/config"

14.0.0.0/16 The CIDR network to use for pod IPs. Each pod launched in the cluster will be assigned an IP out of this range. This network must be routable between all hosts in the cluster. In a default installation, the flannel overlay network will provide routing to this network.
12.0.0.11 IP address of one of our etcd node

Start Kubelet, which will also start the Pod manifest for the API server, the controller manager, proxy and scheduler

sudo systemctl start kubelet

Ensure it will be started after a reboot

sudo systemctl enable kubelet

Start Calico

sudo systemctl start calico-node

Ensure it will be start after a reboot

sudo systemctl enable calico-node

Check API is up

You can observe the downloading process

sudo systemctl status kubelet.service

Before going to the next step wait until the k8s API respond, try from the k8s master node

curl http://127.0.0.1:8080/version

You should get

{
"major": "1",
"minor": "2",
"gitVersion": "v1.2.4+coreos.1",
"gitCommit": "7f80f816ee1a23c26647aee8aecd32f0b21df754",
"gitTreeState": "clean"
}

Kubernetes namespaces

For other hosts in the cluster to discover Kubernetes Control Plane pods, lets create a corresponding namespace

 curl -H "Content-Type: application/json" -XPOST -d'{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"kube-system"}}' "http://127.0.0.1:8080/api/v1/namespaces"

Create also a namespace for Calico policy-agent

curl -H "Content-Type: application/json" -XPOST -d'{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"calico-system"}}' "http://127.0.0.1:8080/api/v1/namespaces"

Enable network policy API

To enable Network policy in Kubernetes which is currently implemented as a 3rd party resource, run

curl -H "Content-Type: application/json" -XPOST http://127.0.0.1:8080/apis/extensions/v1beta1/namespaces/default/thirdpartyresources --   data-binary @- <<BODY
{
  "kind": "ThirdPartyResource",
  "apiVersion": "extensions/v1beta1",
  "metadata": {
    "name": "network-policy.net.alpha.kubernetes.io"
  },
  "description": "Specification for a network isolation policy",
  "versions": [
    {
      "name": "v1alpha1"
    }
  ]
}
BODY

Kubernetes Worker Nodes

Almost there, we can now switch to our worker nodes.

TLS Assets

Place the TLS keypairs generated previously in the following directory. For each node.

scp ca.pem core12.0.0.12:/tmp
scp kube-02*.pem core12.0.0.12:/tmp
ssh core@12.0.0.12
mkdir -p /etc/kubernetes/ssl/
mv /tmp/kube-02-worker* /tmp/ca.pem /etc/kubernetes/ssl
sudo chmod 600 /etc/kubernetes/ssl/*-key.pem
sudo chown root:root /etc/kubernetes/ssl/*-key.pem
cd /etc/kubernetes/ssl/
sudo ln -s kube-02-worker.pem worker.pem
sudo ln -s kube-02-worker-key.pem worker-key.pem

Networking

Same as for the master node, do the following for each of your worker nodes.

mkdir /etc/flannel
vi /etc/flannel/options.env

FLANNELD_IFACE=12.0.0.12
FLANNELD_ETCD_ENDPOINTS=http://12.0.0.11:2379,http://12.0.0.12:2379,http://12.0.0.13:2379

http://12.0.0.11:2379,http://12.0.0.12:2379,http://12.0.0.13:2379 ETCD_ENDPOINTS
12.0.0.10 this node routable IP Address

Now create the systemd drop-in which will use the above configuration when flannel starts

mkdir /etc/systemd/system/flanneld.service.d/
vi /etc/systemd/system/flanneld.service.d/40-ExecStartPre-symlink.conf

[Service]
ExecStartPre=/usr/bin/ln -sf /etc/flannel/options.env /run/flannel/options.env

Flannel isn’t yet started but you can come back here later to check its running configuration with

cat /run/flannel/subnet.env

Docker

We need flanneld to be running prior to Docker, do the following on each worker node

mkdir  /etc/systemd/system/docker.service.d
vi /etc/systemd/system/docker.service.d/40-flannel.conf

[Unit]
Requires=flanneld.service
After=flanneld.service

kubelet unit

Create the following Unit in each worker node

vi /etc/systemd/system/kubelet.service

[Service]
ExecStartPre=/usr/bin/mkdir -p /etc/kubernetes/manifests

Environment=KUBELET_VERSION=v1.2.4_coreos.cni.1
ExecStart=/usr/lib/coreos/kubelet-wrapper \
  --api-servers=https://12.0.0.11 \
  --network-plugin-dir=/etc/kubernetes/cni/net.d \
  --network-plugin=calico \
  --register-node=true \
  --allow-privileged=true \
  --config=/etc/kubernetes/manifests \
  --hostname-override=12.0.0.12 \
  --cluster-dns=13.0.0.10 \
  --cluster-domain=cluster.local \
  --kubeconfig=/etc/kubernetes/worker-kubeconfig.yaml \
  --tls-cert-file=/etc/kubernetes/ssl/worker.pem \
  --tls-private-key-file=/etc/kubernetes/ssl/worker-key.pem
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target

v1.2.4_coreos.cni.1 hyperkube version
12.0.0.11 Master Host IP address
12.0.0.12 Node routable IP address
13.0.0.10 DNS_SERVICE_IP

CNI Config

For each worker node

mkdir -p /etc/kubernetes/cni/net.d/
vi /etc/kubernetes/cni/net.d/10-calico.conf

{
    "name": "calico",
    "type": "flannel",
    "delegate": {
        "type": "calico",
        "etcd_endpoints": "http://12.0.0.11:2379,http://12.0.0.12:2379,http://12.0.0.13:2379",
        "log_level": "none",
        "log_level_stderr": "info",
        "hostname": "12.0.0.12",
        "policy": {
            "type": "k8s",
            "k8s_api_root": "https://12.0.0.11:443/api/v1/",
            "k8s_client_key": "/etc/kubernetes/ssl/worker-key.pem",
            "k8s_client_certificate": "/etc/kubernetes/ssl/worker.pem"
        }
    }
}

http://12.0.0.11:2379,http://12.0.0.12:2379,http://12.0.0.13:2379 ETCD_ENDPOINTS
12.0.0.12 this node routable IP address
12.0.0.11 k8s master node IP address

kube-proxy Pod

On each worker node

mkdir /etc/kubernetes/manifests/
vi /etc/kubernetes/manifests/kube-proxy.yaml

apiVersion: v1
kind: Pod
metadata:
  name: kube-proxy
  namespace: kube-system
spec:
  hostNetwork: true
  containers:
  - name: kube-proxy
    image: quay.io/coreos/hyperkube:v1.2.4_coreos.1
    command:
    - /hyperkube
    - proxy
    - --master=https://12.0.0.11
    - --kubeconfig=/etc/kubernetes/worker-kubeconfig.yaml
    - --proxy-mode=iptables
    securityContext:
      privileged: true
    volumeMounts:
      - mountPath: /etc/ssl/certs
        name: "ssl-certs"
      - mountPath: /etc/kubernetes/worker-kubeconfig.yaml
        name: "kubeconfig"
        readOnly: true
      - mountPath: /etc/kubernetes/ssl
        name: "etc-kube-ssl"
        readOnly: true
  volumes:
    - name: "ssl-certs"
      hostPath:
        path: "/usr/share/ca-certificates"
    - name: "kubeconfig"
      hostPath:
        path: "/etc/kubernetes/worker-kubeconfig.yaml"
    - name: "etc-kube-ssl"
      hostPath:
        path: "/etc/kubernetes/ssl"

12.0.0.11 k8s master node

kubeconfig

Create the following YAML on each worker node to facilitate secure communication between k8s components

vi /etc/kubernetes/worker-kubeconfig.yaml

apiVersion: v1
kind: Config
clusters:
- name: local
  cluster:
    certificate-authority: /etc/kubernetes/ssl/ca.pem
users:
- name: kubelet
  user:
    client-certificate: /etc/kubernetes/ssl/worker.pem
    client-key: /etc/kubernetes/ssl/worker-key.pem
contexts:
- context:
    cluster: local
    user: kubelet
  name: kubelet-context
current-context: kubelet-context

Calico container

On each worker node

vi /etc/systemd/system/calico-node.service

[Unit]
Description=Calico node for network policy
Requires=network-online.target
After=network-online.target

[Service]
Slice=machine.slice
Environment=CALICO_DISABLE_FILE_LOGGING=true
Environment=HOSTNAME=12.0.0.12
Environment=IP=12.0.0.12
Environment=FELIX_FELIXHOSTNAME=12.0.0.12
Environment=CALICO_NETWORKING=false
Environment=NO_DEFAULT_POOLS=true
Environment=ETCD_ENDPOINTS=http://12.0.0.11:2379,http://12.0.0.12:2379,http://12.0.0.13:2379
ExecStart=/usr/bin/rkt run --inherit-env --stage1-from-dir=stage1-fly.aci \
--volume=modules,kind=host,source=/lib/modules,readOnly=false \
--mount=volume=modules,target=/lib/modules \
--trust-keys-from-https quay.io/calico/node:v0.19.0
KillMode=mixed
Restart=always
TimeoutStartSec=0

[Install]
WantedBy=multi-user.target

http://12.0.0.11:2379,http://12.0.0.12:2379,http://12.0.0.13:2379 ETCD_ENDPOINTS
12.0.0.12 this node routable IP address

Start Services

On each worker node start the following services

sudo systemctl daemon-reload
sudo systemctl start flanneld
sudo systemctl start kubelet
sudo systemctl start calico-node

Ensure they are all started on each boot

sudo systemctl enable flanneld
sudo systemctl enable kubelet
sudo systemctl enable calico-node

Check the health of the kubelet and calico systemd unit

systemctl status kubelet.service
systemctl status calico-node.service

kubectl

kubectl is the Kubernetes command line tool giving you full control of your cluster from your workstation.

installation

download the binary, for Linux

curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl

for macOS

curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/darwin/amd64/kubectl

Then do

chmod +x kubectl
mv kubectl /usr/local/bin/kubectl

or if you use brew

brew install kubectl

for Windows

curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/windows/amd64/kubectl.exe

autocompletion

On linux

echo "source <(kubectl completion bash)" >> ~/.bashrc

On MacOS, works only if you used brew not a direct download.

brew install bash-completion
echo "source $(brew --prefix)/etc/bash_completion" >> ~/.bash_profile
echo "source <(kubectl completion bash)" >> ~/.bash_profile

Configuration

You need to share with kubectl your environment details, update below the path to your TLS certificates

kubectl config set-cluster default-cluster --server=https://12.0.0.11 --certificate-authority=/<absolute_path_to>/ca.pem
kubectl config set-credentials default-admin --certificate-authority=/<absolute_path_to>/ca.pem --client-key=/<absolute_path_to>/admin-key.pem --client-certificate=/<absolute_path_to>/admin.pem
kubectl config set-context default-system --cluster=default-cluster --user=default-admin
kubectl config use-context default-system

12.0.0.1 master node IP address

Check everything looks good

# kubectl versions
# kubectl get nodes
NAME        STATUS                     AGE
12.0.0.11   Ready,SchedulingDisabled   2h
12.0.0.12   Ready                      22m
12.0.0.13   Ready                      22m

SchedulingDisabled we configured our master node not to be part of our scheduling not to have end user containers running on our master.

Congrat, you deserve some applause !!!! Tedious isn’t it, it’s a good promotion for automation tooling like Ansible ;)

DNS Addon

This step is documented here. In our case, the DNS_SERVICE_IP is 13.0.0.10.

Each Pod will then be able to resolve containers names in the cluster.local domain. Kubelet will inject 13.0.0.10 as the resolver into each provisioned container.

But I wasn’t able to make it work, kube2sky container exit while trying to connect to master API with its service IP while the certificate were generated with another one. It’s a bit tricky to fix without having to rely on setting insecure-skip-tls-verify.

Tectonic console

We are now done for our k8s deployment, lets go on with Tectonic now.

Start by registering for a Tectonic Starter, it’s free but will give you a feature limited version. You won’t have any authentication layer or container registry. If you prefer to see the full version, you’ll have to sign up for a 30 day eval instead. Once registered for it, you’ll be able to download a Kubernetes formated pull secret from your Account Assets page. This secret specifies credentials that k8s will use to pull the image down from a container registry.

you can install the pull secret

kubectl create -f coreos-pull-secret.yml

Now download the Tectonic Manifest which defines a replication controller that pulls, runs and maintains the console container throughout its lifecycle on the k8s cluster.

Upload this manifest

kubectl create -f tectonic-console.yaml

Monitor Tectonic container status

kubectl get pods --watch -l tectonic-app=console
NAME                            READY     STATUS              RESTARTS   AGE
tectonic-console-v0.1.6-u0yzk   0/1       ContainerCreating   0          1m

After a bit you should see

NAME                            READY     STATUS    RESTARTS   AGE
tectonic-console-v0.1.6-rdtqo   1/1       Running   0          26s

Installed in 26s, nice insn’t it !!!

Since it’s a starter edition, there isn’t any authentication in place for the console, you need to use port forwarding to access it

kubectl get pods -l tectonic-app=console -o template --template="{{range.items}}{{.metadata.name}}{{end}}" | xargs -I{} kubectl port-forward {} 9000

The first part of the command above is getting the name of the tectonic console Pod, second part is forwarding local port 9000 to it.

You should now be able to access the console at http://localhost:9000

To simplify things a bit, you can also expose the console by creating a Service

vi tectonic-console-service.yml

apiVersion: v1
kind: Service
metadata:
  name: tectonic-console-public
spec:
  type: NodePort
  ports:
    - port: 9000
      nodePort: 32000
      protocol: TCP
      name: tectonic-console-expose
  selector:
    tectonic-app: console
    tectonic-component: ui

Create the service using kubectl

kubectl create -f tectonic-console-service.yml

Now you can access the console using any node IP adress on port 32000, for example http://12.0.0.12:32000

In case you want to remove it from your cluster just do

kubectl delete replicationcontrollers tectonic-console-v0.1.6
kubectl delete secrets coreos-pull-secret
kubectl delete svc tectonic-console-public

Kubernetes Dashboard

Apart from tectonic, Kubernetes offers its own native UI that you install by running the command below.

kubectl create -f https://github.com/kubernetes/dashboard/blob/master/src/deploy/kubernetes-dashboard.yaml

I’ll talk about this UI in a future article.

Simple demo

Kubernetes can be easy to use when we abstract away the YAML or the API by using instead the high level commands. Lets try the following example.

You deserve to have a little bit of fun, so lets try to deploy Nginx within our cluster.

kubectl run nginx --image=nginx:1.10

The command creates a deployment, check if the corresponding pod is running

kubectl get po
NAME                            READY     STATUS    RESTARTS   AGE
nginx-1173773998-pfw67          1/1       Running   0          10s

To get access to it easily, you can expose your deployment

kubectl expose deployment nginx --port 80 --type=NodePort

This creates a Service as you can see below

kubectl get svc
NAME                      CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
nginx                     13.0.0.212   nodes         80/TCP     12s

Now look into your newly created service

kubectl describe svc nginx
Name:     nginx
Namespace:    default
Labels:     run=nginx
Selector:   run=nginx
Type:     NodePort
IP:     13.0.0.212
Port:     <unset> 80/TCP
NodePort:   <unset> 32613/TCP
Endpoints:    14.0.99.2:80
Session Affinity: None
No events.

You should now be able to access nginx on any worker node IP on port 80.

But you can also exec commands directly within a selected the container

kubectl exec -ti nginx-1173773998-pfw67 -- bash

To clean your environment do

kubectl delete deployment nginx
kubectl delete svc nginx

Troubleshooting

You can’t install anything on a CoreOS machine but you can start a toolbox which is a priviledged container running a Fedora distribution like this

/usr/bin/toolbox

Wait until it downloads the image and launch the corresponding container, you can then install troubleshooting tools like this

yum install iputils
yum install net-tools
yum install tcpdump

To troubleshoot within a busybox container you can run

kubectl run -i --tty busybox --image=busybox --restart=Never -- sh 

Conclusion

Wow, it was a long, long process, but it’s a good learning experience to better understand all the moving parts involved in Kubernetes. If you see another version of k8s and want to upgrade, look at the following documentation.

If you want to play with your cluster, you can now try to deploy the Guestbook example application.

Good Luck !!!