Skip to main content

Create a secure Kubernetes HA cluster in AWS using kube-aws

Jun 18, 2017

Create a secure Kubernetes HA cluster in AWS using kube-aws

There are several tools that allow automatic deployment of Kubernetes clusters in AWS, like kube-aws, kops, kismatic and others. Some of the tools can also provision AWS infrastructure according to your use case.

kube-aws allows you to customize a yaml file and generate a Cloudformation stack that automates the creation of VPCs, subnets, Nat Gateways, security groups, etc. Even if it’s easier to deploy everything using kube-aws, I find it safer to manage your network infrastructure separately, especially when using Cloudformation, which sometimes can roll back and mess your entire stack.

The fallowing setup use a base Cloudformation stack to configure Public and Private Subnets, IGW, NatGW, Route Tables and KMS. After the stack is created we’ll deploy Kubernetes on top of it using kube-aws.

Features:

  • all the nodes are deployed in private subnets
  • 3 distinct availability zones
  • 3 masters in HA, one per availability zone
  • workers configured using node pools, similar to [GKE node pools](https://cloud.google.com/container-engine/docs/node-pools)
  • HA ETCD with encrypted partitions for data, automatic backups to S3 and automatic recovery from failover
  • role based authentication using the RBAC plugin
  • users authentication using OpenID Connect Identity (OIDC)
  • AWS IAM roles directly assigned to pods using [kube2iam](https://github.com/jtblin/kube2iam)
  • cluster autoscaling

Note: all the configurations are available in GitHub

Deploy the Kubernetes cluster

1. Clone my demo repository https://github.com/camilb/kube-aws-secure locally

2. Create the base Cloudformation stack by customising and running ./vpc/deploy.vpc.sh script

3. Install kube-aws; in this example I’m using kube-aws v0.9-7-rc.3

4. Get the output values from the Cloudformation base stack created and update the ./kube-aws/cluster.yaml

aws cloudformation describe-stacks --stack-name kube-aws-vpc | jq -r '.Stacks[].Outputs'

5. Render the stack and credentials. Go to ./kube-aws directory and execute the fallowing command:

kube-aws render credentials --generate-ca 
kube-aws render stack

Please read the kube-aws documentation if you plan to use your own CA.

6. To launch the cluster, first you’ll need a S3 bucket. Use an existing bucket or create a new one.

kube-aws up --s3-uri s3://your-bucket-name

7. Access your Kubernetes cluster. Since we are creating all the resources in private networks, in order to access it, you’ll need a VPN placed in one of the public subnets. I’m using pritunl, which is very easy to configure. In approximately 5 minutes you can get it up and running on a t2.nano

After the VPN is set, make a quick check to list the nodes and pods in kube-system. You should get something similar to this:

$ kubectl --kubeconfig=./kubeconfig get nodes -o wide
NAME                         STATUS    AGE  EXTERNAL-IP  KERNEL-VERSION
ip-10-0-1-176.ec2.internal   Ready     1h     <none>       4.9.24-coreos
ip-10-0-1-219.ec2.internal   Ready     1h     <none>       4.9.24-coreos
ip-10-0-2-151.ec2.internal   Ready     1h     <none>       4.9.24-coreos
ip-10-0-2-245.ec2.internal   Ready     1h     <none>       4.9.24-coreos
ip-10-0-3-124.ec2.internal   Ready     1h     <none>       4.9.24-coreos
ip-10-0-3-96.ec2.internal    Ready     1h     <none>       4.9.24-coreos
$ kubectl --kubeconfig=./kubeconfig get pods -n kube-system
        NAME                                                 READY     STATUS    RESTARTS   AGE       
        calico-node-0ws56                                    1/1       Running   0          1h 
        calico-node-12177                                    1/1       Running   0          1h 
        calico-node-8kxsf                                    1/1       Running   0          1h 
        calico-node-9sq7p                                    1/1       Running   0          1h 
        calico-node-hh3l9                                    1/1       Running   0          1h 
        calico-node-vtzf2                                    1/1       Running   0          1h 
        calico-policy-controller-2265106533-gkl2q            1/1       Running   0          1h 
        dex-3003102726-2zjp0                                 1/1       Running   0          1h 
        heapster-v1.3.0-264939892-ggg1c                      2/2       Running   0          1h 
        kube-apiserver-ip-10-0-1-219.ec2.internal            1/1       Running   0          1h 
        kube-apiserver-ip-10-0-2-245.ec2.internal            1/1       Running   0          1h 
        kube-apiserver-ip-10-0-3-124.ec2.internal            1/1       Running   0          1h 
        kube-controller-manager-ip-10-0-1-219.ec2.internal   1/1       Running   0          1h 
        kube-controller-manager-ip-10-0-2-245.ec2.internal   1/1       Running   0          1h 
        kube-controller-manager-ip-10-0-3-124.ec2.internal   1/1       Running   0          1h 
        kube-dns-1759312207-2cd6s                            3/3       Running   0          1h 
        kube-dns-1759312207-khgqs                            3/3       Running   0          1h 
        kube-dns-autoscaler-2188776582-0rzp8                 1/1       Running   0          1h 
        kube-proxy-ip-10-0-1-176.ec2.internal                1/1       Running   0          1h 
        kube-proxy-ip-10-0-1-219.ec2.internal                1/1       Running   0          1h 
        kube-proxy-ip-10-0-2-151.ec2.internal                1/1       Running   0          1h 
        kube-proxy-ip-10-0-2-245.ec2.internal                1/1       Running   0          1h 
        kube-proxy-ip-10-0-3-124.ec2.internal                1/1       Running   0          1h 
        kube-proxy-ip-10-0-3-96.ec2.internal                 1/1       Running   0          1h 
        kube-rescheduler-3155147949-99k34                    1/1       Running   0          1h 
        kube-scheduler-ip-10-0-1-219.ec2.internal            1/1       Running   0          1h 
        kube-scheduler-ip-10-0-2-245.ec2.internal            1/1       Running   0          1h 
        kube-scheduler-ip-10-0-3-124.ec2.internal            1/1       Running   0          1h 
        kubernetes-dashboard-3963616910-fk65g                1/1       Running   0          1h 

Optionally you can configure your ~/.kube/config according to kubeconfig file to avoid passing the --kubeconfig flag on your commands.

Important

In order to expose public services using ELB or Ingress, the public subnets have to be tagged with the cluster name.

Ex. KubeernetesCluser=cluster_name

Add-ons

kube2iam

First we’ll configure the kube2iam to allow some of our applications to assume AWS IAM Roles.

When RBAC is enabled kube2iam needs permissions to list pods and namespaces. We have to grant these permissions.

$ kubectl create -f ./addons/kube2iam/rbac.yaml

deploy the kube2iam DaemonSet

change the account ID to yours in ./addons/kube2iam/k2i.ds.yaml, then create the DaemonSet

$ kubectl create -f ./addons/kube2iam/k2i.ds.yaml

Now your pods can assume all the roles that have a trust relationship configured.

Route53

This add-on is based on ExternalDNS project which allows you to control Route53 DNS records dynamically via Kubernetes resources.

Create a role named k8s-route53 using this policy. You also have to establish a trust relationship in order to allow the role to be assumed. An example is provided here.

Now change the values in external-dns.yaml and deploy it.

$ kubectl create -f ./addons/route53/external-dns.yaml

Nginx Ingress Controller

I’m choosing nginx over traefik because of the Proxy Protocol support. This allows to use the nginx ingress controller in AWS behind an ELB configured with Proxy Protocol. Here are some benefits from it:

  • ingress address is associated with your ELB and doesn’t change when you replace the workers
  • workers can be placed in private networks without public IPs
  • better DDOS protection from ELB
  • unlimited number of services that can be exposed using only one ELB. It’s like a cheap version of AWS ALB

Deploy

$ kubectl create -f ./addons/ingress/nginx/rbac.yaml
$ kubectl create -f ./addons/ingress/nginx/nginx.yaml

kube-lego

Kube-Lego automatically requests certificates for Kubernetes Ingress resources from Let’s Encrypt.

$ kubectl create -f ./addons/kube-lego/rbac.yaml
$ kubectl create -f ./addons/kube-lego/kube-lego.yaml

Dex

Dex runs natively on top of any Kubernetes cluster using Third Party Resources and can drive API server authentication through the OpenID Connect plugin.

By default you have administrator rights using the TLS certificates. If you plan to grant restricted permissions to other users, Dex can facilitate users access using OpenID Connect Tokens.

In this example we use the Github provider to identify the users.

Please configure the ./addons/dex/elb/internal-elb.yaml file then expose the service.

The DNS is configured automatically by ExternalDNS add-on and should be available in approximately 1 minute.

You can now use a client like dex’s example-app to obtain a authentication token.

If you prefer, you can use this app as a always running service by configuring and deploying ./addons/kid/kid.yaml

  $ kubectl create secret \
    generic kid \
    --from-literal=CLIENT_ID=your-client-id \
    --from-literal=CLIENT_SECRET=your-client-secret \
    -n kube-system

  $ kubectl create -f ./addons/kid/kid.yaml

Please check the dex [documentation](https://github.com/coreos/dex/tree/master/Documentation) if you need more informations.

Make a quick test by granting a user permissions to list the pods in kube-system namespace.

$ kubectl create -f ./examples/rbac/pod-reader.yaml
$ kubectl create rolebinding pod-reader --role=pod-reader --user=user@example.com --namespace=kube-system

Example of ~/.kube/config for a user

   apiVersion: v1
    clusters:
    - cluster:
        certificate-authority-data: ca.pem_base64_encoded
        server: https://kubeapi.example.com
      name: your_cluster_name
    contexts:
    - context:
        cluster: your_cluster_name
        user: user@example.com
      name: your_cluster_name
    current-context: your_cluster_name
    kind: Config
    preferences: {}
    users:
    - name: user@example.com
      user:
        auth-provider:
          config:
            access-token: id_token
            client-id: client_id 
            client-secret: client_secret
            extra-scopes: groups
            id-token: id_token
            idp-issuer-url: https://dex.example.com
            refresh-token: refresh_token
          name: oidc

If you already have the ~/.kube/config set, you can use this example to configure the user authentication

    $ kubectl config set-credentials user@example.com \
      --auth-provider=oidc \
      --auth-provider-arg=idp-issuer-url=https://dex.example.com \
      --auth-provider-arg=client-id=your_client_id \
      --auth-provider-arg=client-secret=your_client_secret \
      --auth-provider-arg=refresh-token=your_refresh_token \
      --auth-provider-arg=id-token=your_id_token \
      --auth-provider-arg=extra-scopes=groups

Once your id_token expires, kubectl will attempt to refresh your id_token using your refresh_token and client_secret storing the new values for the refresh_token and id_token in your ~/.kube/config

At this point you have a pretty secure, highly available, Kubernetes cluster in AWS.

For even better security please also consider using the Pod Security Policy, Calico Network Policy and Istio]

Monitoring

A easy to setup, in-cluster, monitoring solution using Prometheus is available here https://github.com/camilb/prometheus-kubernetes