By using Kubernetes, we basically delegate the responsibilities to achieve high availability and scalability to Kubernetes, Thus having a HA Kubernetes cluster running is the first step to begin with and the foundation of the whole backend.

Since our workload is running on AWS and AWS EKS is still quite primitive and not production tested enough, we’ll go with deploying and managing a K8S cluster on our own using popular open source tools. The advantages of doing this is:

  • The installation process is fully automated.
  • The community is big and active.
  • We have full control over the cluster we created.
  • Since it’s open source, we can figure out what exactly is going on and have a better understanding of the Cluster.
  • Since it’s open source, we can customise our deployment based on our own requirements.

Compared to commercial managed Kubernetes cluster, the shortage is also quite obvious:

  • The maintenance effort will be higher.

Installing K8S cluster using kops

To cut the story short, we’re using kops, a popular installation tool open sourced on github. It has official step-to-step guides on it’s github page, but it’s too primitive. The purpose of this article is to show how we used kops with customizations to deploy our K8S cluster.

To check the version matrix and install your desired kops and kubectl for your OS, please follow the official documentation. You will also need to install terraform, the versions we used is:

software version
kops 1.9.0
kubectl 1.9.7
k8s 1.9.3
terraform 0.11.7

I assume you have required tools installed already on your machine by now.

IAM Permissions

Since kops deploys K8S cluster on AWS, you need to prepare an AWS IAM account with the following access right:

1
2
3
4
5
AmazonEC2FullAccess
AmazonRoute53FullAccess
AmazonS3FullAccess
IAMFullAccess
AmazonVPCFullAccess

In order to be able to give any developers the ability to do it, I’ll create a group called kops that has the above access rights. In case we want to give other developers to ability to play with K8S cluster, just add new IAM users to the group.

I assume you will use Terraform to manage AWS resources, the following is the detail .tf configurations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
resource "aws_iam_group" "kops" {
name = "kops"
}

resource "aws_iam_group_policy_attachment" "kops-ec2" {
group = "${aws_iam_group.kops.name}"
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2FullAccess"
}

resource "aws_iam_group_policy_attachment" "kops-route53" {
group = "${aws_iam_group.kops.name}"
policy_arn = "arn:aws:iam::aws:policy/AmazonRoute53FullAccess"
}

resource "aws_iam_group_policy_attachment" "kops-s3" {
group = "${aws_iam_group.kops.name}"
policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
}

resource "aws_iam_group_policy_attachment" "kops-iam" {
group = "${aws_iam_group.kops.name}"
policy_arn = "arn:aws:iam::aws:policy/IAMFullAccess"
}

resource "aws_iam_group_policy_attachment" "kops-vpc" {
group = "${aws_iam_group.kops.name}"
policy_arn = "arn:aws:iam::aws:policy/AmazonVPCFullAccess"
}

DNS

We’ll create a gossip-based cluster, so we don’t have to configure any DNS, the only requirement to trigger this is to have the cluster name end with .k8s.local. In our case, we name our K8S cluster as mx-cluster.k8s.local.

State Store

The state store required is a S3 bucket, it’s used to store all the states and representations of the K8S cluster you created. The bucket we created is mx-k8s-state in the us-east-1 region, in which our K8S is located.

The same as the IAM group, we use Terraform to do this and you can find the code in the same file:

1
2
3
4
5
6
7
8
9
10
11
resource "aws_s3_bucket" "mx-k8s-state" {
bucket = "mx-k8s-state"
acl = "private"
versioning {
enabled = true
}
tags {
Name = "mx-k8s-state"
KubernetesCluster = "mx-cluster.k8s.local"
}
}

VPC, Subnets, Route tables and NAT gateway

It’s not compulsory to create VPC, subnets and NAT gateway on our own, we only choose to do this before hand because if we leverage kops to do it, it always creates 1 NAT gateway per AZ, which is a waste of resources and money. We only need 1 NAT gateway and let all private subnets share it. And the key to do this correctly is to create the required resources and tag them the way kops will tag it properly, then when we create the cluster using kops, we can specify.

The .tf configuration is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
resource "aws_vpc" "mx-cluster-k8s-local" {
cidr_block = "10.1.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true

tags = {
KubernetesCluster = "mx-cluster.k8s.local"
Name = "mx-cluster.k8s.local"
"kubernetes.io/cluster/mx-cluster.k8s.local" = "owned"
}
}

resource "aws_vpc_dhcp_options" "mx-cluster-k8s-local" {
domain_name = "ec2.internal"
domain_name_servers = ["AmazonProvidedDNS"]

tags = {
KubernetesCluster = "mx-cluster.k8s.local"
Name = "mx-cluster.k8s.local"
"kubernetes.io/cluster/mx-cluster.k8s.local" = "owned"
}
}

resource "aws_vpc_dhcp_options_association" "mx-cluster-k8s-local" {
vpc_id = "${aws_vpc.mx-cluster-k8s-local.id}"
dhcp_options_id = "${aws_vpc_dhcp_options.mx-cluster-k8s-local.id}"
}

resource "aws_subnet" "us-east-1a-mx-cluster-k8s-local" {
vpc_id = "${aws_vpc.mx-cluster-k8s-local.id}"
cidr_block = "10.1.32.0/19"
availability_zone = "us-east-1a"

tags = {
KubernetesCluster = "mx-cluster.k8s.local"
Name = "us-east-1a.mx-cluster.k8s.local"
SubnetType = "Private"
"kubernetes.io/cluster/mx-cluster.k8s.local" = "shared"
"kubernetes.io/role/internal-elb" = "1"
}
}

resource "aws_subnet" "us-east-1b-mx-cluster-k8s-local" {
vpc_id = "${aws_vpc.mx-cluster-k8s-local.id}"
cidr_block = "10.1.64.0/19"
availability_zone = "us-east-1b"

tags = {
KubernetesCluster = "mx-cluster.k8s.local"
Name = "us-east-1b.mx-cluster.k8s.local"
SubnetType = "Private"
"kubernetes.io/cluster/mx-cluster.k8s.local" = "shared"
"kubernetes.io/role/internal-elb" = "1"
}
}

resource "aws_subnet" "us-east-1c-mx-cluster-k8s-local" {
vpc_id = "${aws_vpc.mx-cluster-k8s-local.id}"
cidr_block = "10.1.96.0/19"
availability_zone = "us-east-1c"

tags = {
KubernetesCluster = "mx-cluster.k8s.local"
Name = "us-east-1c.mx-cluster.k8s.local"
SubnetType = "Private"
"kubernetes.io/cluster/mx-cluster.k8s.local" = "shared"
"kubernetes.io/role/internal-elb" = "1"
}
}

resource "aws_subnet" "utility-us-east-1a-mx-cluster-k8s-local" {
vpc_id = "${aws_vpc.mx-cluster-k8s-local.id}"
cidr_block = "10.1.0.0/22"
availability_zone = "us-east-1a"

tags = {
KubernetesCluster = "mx-cluster.k8s.local"
Name = "utility-us-east-1a.mx-cluster.k8s.local"
SubnetType = "Utility"
"kubernetes.io/cluster/mx-cluster.k8s.local" = "shared"
"kubernetes.io/role/elb" = "1"
}
}

resource "aws_subnet" "utility-us-east-1b-mx-cluster-k8s-local" {
vpc_id = "${aws_vpc.mx-cluster-k8s-local.id}"
cidr_block = "10.1.4.0/22"
availability_zone = "us-east-1b"

tags = {
KubernetesCluster = "mx-cluster.k8s.local"
Name = "utility-us-east-1b.mx-cluster.k8s.local"
SubnetType = "Utility"
"kubernetes.io/cluster/mx-cluster.k8s.local" = "shared"
"kubernetes.io/role/elb" = "1"
}
}

resource "aws_subnet" "utility-us-east-1c-mx-cluster-k8s-local" {
vpc_id = "${aws_vpc.mx-cluster-k8s-local.id}"
cidr_block = "10.1.8.0/22"
availability_zone = "us-east-1c"

tags = {
KubernetesCluster = "mx-cluster.k8s.local"
Name = "utility-us-east-1c.mx-cluster.k8s.local"
SubnetType = "Utility"
"kubernetes.io/cluster/mx-cluster.k8s.local" = "shared"
"kubernetes.io/role/elb" = "1"
}
}

resource "aws_eip" "us-east-1a-mx-cluster-k8s-local" {
vpc = true
tags {
KubernetesCluster = "mx-cluster.k8s.local"
Name = "us-east-1a.mx-cluster.k8s.local"
"kubernetes.io/cluster/mx-cluster.k8s.local" = "shared"
}
}

resource "aws_nat_gateway" "us-east-1a-mx-cluster-k8s-local" {
subnet_id = "${aws_subnet.utility-us-east-1a-mx-cluster-k8s-local.id}"
allocation_id = "${aws_eip.us-east-1a-mx-cluster-k8s-local.id}"

tags {
KubernetesCluster = "mx-cluster.k8s.local"
Name = "us-east-1a.mx-cluster.k8s.local"
"kubernetes.io/cluster/mx-cluster.k8s.local" = "shared"
}
}

resource "aws_internet_gateway" "mx-cluster-k8s-local" {
vpc_id = "${aws_vpc.mx-cluster-k8s-local.id}"

tags = {
KubernetesCluster = "mx-cluster.k8s.local"
Name = "mx-cluster.k8s.local"
"kubernetes.io/cluster/mx-cluster.k8s.local" = "owned"
}
}

resource "aws_route" "mx-cluster-k8s-local" {
route_table_id = "${aws_route_table.mx-cluster-k8s-local.id}"
destination_cidr_block = "0.0.0.0/0"
gateway_id = "${aws_internet_gateway.mx-cluster-k8s-local.id}"
}

resource "aws_route" "private-us-east-1a-mx-cluster-k8s-local" {
route_table_id = "${aws_route_table.private-us-east-1a-mx-cluster-k8s-local.id}"
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = "${aws_nat_gateway.us-east-1a-mx-cluster-k8s-local.id}"
}

resource "aws_route" "private-us-east-1b-mx-cluster-k8s-local" {
route_table_id = "${aws_route_table.private-us-east-1b-mx-cluster-k8s-local.id}"
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = "${aws_nat_gateway.us-east-1a-mx-cluster-k8s-local.id}"
}

resource "aws_route" "private-us-east-1c-mx-cluster-k8s-local" {
route_table_id = "${aws_route_table.private-us-east-1c-mx-cluster-k8s-local.id}"
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = "${aws_nat_gateway.us-east-1a-mx-cluster-k8s-local.id}"
}

resource "aws_route_table" "private-us-east-1a-mx-cluster-k8s-local" {
vpc_id = "${aws_vpc.mx-cluster-k8s-local.id}"

route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = "${aws_nat_gateway.us-east-1a-mx-cluster-k8s-local.id}"
}

tags = {
KubernetesCluster = "mx-cluster.k8s.local"
Name = "private-us-east-1a.mx-cluster.k8s.local"
AssociatedNatgateway = "${aws_nat_gateway.us-east-1a-mx-cluster-k8s-local.id}"
"kubernetes.io/cluster/mx-cluster.k8s.local" = "owned"
"kubernetes.io/kops/role" = "private-us-east-1a"
}
}

resource "aws_route_table" "private-us-east-1b-mx-cluster-k8s-local" {
vpc_id = "${aws_vpc.mx-cluster-k8s-local.id}"

route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = "${aws_nat_gateway.us-east-1a-mx-cluster-k8s-local.id}"
}

tags = {
KubernetesCluster = "mx-cluster.k8s.local"
Name = "private-us-east-1b.mx-cluster.k8s.local"
AssociatedNatgateway = "${aws_nat_gateway.us-east-1a-mx-cluster-k8s-local.id}"
"kubernetes.io/cluster/mx-cluster.k8s.local" = "owned"
"kubernetes.io/kops/role" = "private-us-east-1b"
}
}

resource "aws_route_table" "private-us-east-1c-mx-cluster-k8s-local" {
vpc_id = "${aws_vpc.mx-cluster-k8s-local.id}"

route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = "${aws_nat_gateway.us-east-1a-mx-cluster-k8s-local.id}"
}

tags = {
KubernetesCluster = "mx-cluster.k8s.local"
Name = "private-us-east-1c.mx-cluster.k8s.local"
AssociatedNatgateway = "${aws_nat_gateway.us-east-1a-mx-cluster-k8s-local.id}"
"kubernetes.io/cluster/mx-cluster.k8s.local" = "owned"
"kubernetes.io/kops/role" = "private-us-east-1c"
}
}

resource "aws_route_table" "mx-cluster-k8s-local" {
vpc_id = "${aws_vpc.mx-cluster-k8s-local.id}"

route {
cidr_block = "0.0.0.0/0"
gateway_id = "${aws_internet_gateway.mx-cluster-k8s-local.id}"
}

tags = {
KubernetesCluster = "mx-cluster.k8s.local"
Name = "mx-cluster.k8s.local"
"kubernetes.io/cluster/mx-cluster.k8s.local" = "owned"
"kubernetes.io/kops/role" = "public"
}
}

resource "aws_route_table_association" "private-us-east-1a-mx-cluster-k8s-local" {
subnet_id = "${aws_subnet.us-east-1a-mx-cluster-k8s-local.id}"
route_table_id = "${aws_route_table.private-us-east-1a-mx-cluster-k8s-local.id}"
}

resource "aws_route_table_association" "private-us-east-1b-mx-cluster-k8s-local" {
subnet_id = "${aws_subnet.us-east-1b-mx-cluster-k8s-local.id}"
route_table_id = "${aws_route_table.private-us-east-1b-mx-cluster-k8s-local.id}"
}

resource "aws_route_table_association" "private-us-east-1c-mx-cluster-k8s-local" {
subnet_id = "${aws_subnet.us-east-1c-mx-cluster-k8s-local.id}"
route_table_id = "${aws_route_table.private-us-east-1c-mx-cluster-k8s-local.id}"
}

resource "aws_route_table_association" "utility-us-east-1a-mx-cluster-k8s-local" {
subnet_id = "${aws_subnet.utility-us-east-1a-mx-cluster-k8s-local.id}"
route_table_id = "${aws_route_table.mx-cluster-k8s-local.id}"
}

resource "aws_route_table_association" "utility-us-east-1b-mx-cluster-k8s-local" {
subnet_id = "${aws_subnet.utility-us-east-1b-mx-cluster-k8s-local.id}"
route_table_id = "${aws_route_table.mx-cluster-k8s-local.id}"
}

resource "aws_route_table_association" "utility-us-east-1c-mx-cluster-k8s-local" {
subnet_id = "${aws_subnet.utility-us-east-1c-mx-cluster-k8s-local.id}"
route_table_id = "${aws_route_table.mx-cluster-k8s-local.id}"
}

Create our cluster configuration

With the above AWS resources created, we can now carry on to create the K8S cluster. Before we create it for real, we can do a dry-run and output the configuration to yaml for review. Now let’s do it.

First of all, we are able to get the private subnets and utility subnets by output your terraform state.

1
2
3
4
terraform output
private_subnet_ids = subnet-438b456d,subnet-4ee27204,subnet-9a38f5c6
utility_subnet_ids = subnet-4f894761,subnet-91e070db,subnet-7749842b
vpc_id = vpc-d92424a2

Then proceed with the kops creation command, with the vpc and subnets specified.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cd ../
kops create cluster --name=mx-cluster.k8s.local \
--state=s3://mx-k8s-state \
--vpc="vpc-d92424a2" \
--subnets="subnet-438b456d,subnet-4ee27204,subnet-9a38f5c6" \
--utility-subnets="subnet-4f894761,subnet-91e070db,subnet-7749842b" \
--zones="us-east-1a,us-east-1b,us-east-1c" \
--node-count=1 \
--node-size=m5.large \
--associate-public-ip=false \
--master-zones="us-east-1a,us-east-1b,us-east-1c" \
--master-size=m5.large \
--topology=private \
--networking=flannel-vxlan \
--bastion=false \
--network-cidr="10.1.0.0/16" \
--image="kope.io/k8s-1.9-debian-stretch-amd64-hvm-ebs-2018-03-11" \
--admin-access="your.ip.address.topen/32" \
--dry-run -oyaml

Let me explain the other parameters I passed into the command:

parameter purpose
name we specify our cluster name
state we specify our s3 bucket as the state store
zones we make sure the work nodes are distributed in 3 AZs to achieve HA
node-count we only specify 1 work node as bootstrap
node-size we use m5.large instance (2 cores, 8GB RAM)
associate-public-ip we don’t allow the nodes to reachable from outside world
master-zones we let the master nodes spread across 3 AZs to achieve HA
master-size we use m5.large instance (2 cores, 8GB RAM)
topology the cluster will stay in private subnets, and the API will be exposed via ELB
networking we’ll use flannel-vxlan CNI
bastion we’ll not create the bastion instance here
network-cidr a new VPC will be created using this cidr
image we use debian stretch for compatibility of k8s 1.9.x
admin-access for security group to allow access from our office

You should see the output like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
apiVersion: kops/v1alpha2
kind: Cluster
metadata:
creationTimestamp: null
name: mx-cluster.k8s.local
spec:
api:
loadBalancer:
type: Public
authorization:
rbac: {}
channel: stable
cloudProvider: aws
configBase: s3://mx-k8s-state/mx-cluster.k8s.local
etcdClusters:
- etcdMembers:
- instanceGroup: master-us-east-1a
name: a
- instanceGroup: master-us-east-1b
name: b
- instanceGroup: master-us-east-1c
name: c
name: main
- etcdMembers:
- instanceGroup: master-us-east-1a
name: a
- instanceGroup: master-us-east-1b
name: b
- instanceGroup: master-us-east-1c
name: c
name: events
iam:
allowContainerRegistry: true
legacy: false
kubernetesApiAccess:
- your.ip.address.topen/32
kubernetesVersion: 1.9.3
masterPublicName: api.mx-cluster.k8s.local
networkCIDR: 10.1.0.0/16
networkID: vpc-d92424a2
networking:
flannel:
backend: vxlan
nonMasqueradeCIDR: 100.64.0.0/10
sshAccess:
- your.ip.address.topen/32
subnets:
- cidr: 10.1.32.0/19
id: subnet-438b456d
name: us-east-1a
type: Private
zone: us-east-1a
- cidr: 10.1.64.0/19
id: subnet-4ee27204
name: us-east-1b
type: Private
zone: us-east-1b
- cidr: 10.1.96.0/19
id: subnet-9a38f5c6
name: us-east-1c
type: Private
zone: us-east-1c
- cidr: 10.1.0.0/22
id: subnet-4f894761
name: utility-us-east-1a
type: Utility
zone: us-east-1a
- cidr: 10.1.4.0/22
id: subnet-91e070db
name: utility-us-east-1b
type: Utility
zone: us-east-1b
- cidr: 10.1.8.0/22
id: subnet-7749842b
name: utility-us-east-1c
type: Utility
zone: us-east-1c
topology:
dns:
type: Public
masters: private
nodes: private

---

apiVersion: kops/v1alpha2
kind: InstanceGroup
metadata:
creationTimestamp: null
labels:
kops.k8s.io/cluster: mx-cluster.k8s.local
name: master-us-east-1a
spec:
associatePublicIp: false
image: kope.io/k8s-1.9-debian-stretch-amd64-hvm-ebs-2018-03-11
machineType: m5.large
maxSize: 1
minSize: 1
nodeLabels:
kops.k8s.io/instancegroup: master-us-east-1a
role: Master
subnets:
- us-east-1a

---

apiVersion: kops/v1alpha2
kind: InstanceGroup
metadata:
creationTimestamp: null
labels:
kops.k8s.io/cluster: mx-cluster.k8s.local
name: master-us-east-1b
spec:
associatePublicIp: false
image: kope.io/k8s-1.9-debian-stretch-amd64-hvm-ebs-2018-03-11
machineType: m5.large
maxSize: 1
minSize: 1
nodeLabels:
kops.k8s.io/instancegroup: master-us-east-1b
role: Master
subnets:
- us-east-1b

---

apiVersion: kops/v1alpha2
kind: InstanceGroup
metadata:
creationTimestamp: null
labels:
kops.k8s.io/cluster: mx-cluster.k8s.local
name: master-us-east-1c
spec:
associatePublicIp: false
image: kope.io/k8s-1.9-debian-stretch-amd64-hvm-ebs-2018-03-11
machineType: m5.large
maxSize: 1
minSize: 1
nodeLabels:
kops.k8s.io/instancegroup: master-us-east-1c
role: Master
subnets:
- us-east-1c

---

apiVersion: kops/v1alpha2
kind: InstanceGroup
metadata:
creationTimestamp: null
labels:
kops.k8s.io/cluster: mx-cluster.k8s.local
name: nodes
spec:
associatePublicIp: false
image: kope.io/k8s-1.9-debian-stretch-amd64-hvm-ebs-2018-03-11
machineType: m5.large
maxSize: 1
minSize: 1
nodeLabels:
kops.k8s.io/instancegroup: nodes
role: Node
subnets:
- us-east-1a
- us-east-1b
- us-east-1c

Read the configuration and cross-reference your 3 private subnets and public utility subnets around the 3 AZs, make sure things are correct. Check the instance groups as well (they are actually AutoScaling Groups).

Once we’re sure that things are good, we can carry out the cluster provisioning for real. We still leverage Terraform to do it. kops can output Terraform configuration files so it makes our life easy.

Now let’s create the cluster for real! As we’ve been using Terraform to manage our AWS resources, luckily here we can use Terraform too as kops is able to generate Terraform configuration files. Run the following command:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kops create cluster --name=mx-cluster.k8s.local \
--state=s3://mx-k8s-state \
--zones "us-east-1a,us-east-1b,us-east-1c" \
--node-count=1 \
--node-size=m5.large \
--associate-public-ip=false \
--master-zones "us-east-1a,us-east-1b,us-east-1c" \
--master-size=m5.large \
--topology=private \
--networking=flannel-vxlan \
--bastion=false \
--network-cidr="10.1.0.0/16" \
--image="kope.io/k8s-1.9-debian-stretch-amd64-hvm-ebs-2018-03-11" \
--admin-access="your.ip.address.topen/32" \
--out=. \
--target=terraform

You should see the kubernetes.tf file and a data folder generated in the current directory.

Here before we apply the configuration, we need to make a customization, if you notice the above yaml, the sshAccess defines whom to open the 22 for sshAccess. Because our nodes are in private subnets and we’ll use a VPN connection, we need to adjust this value to the following:

1
2
3
4
5
6
kops edit cluster --name=mx-cluster.k8s.local --state=s3://mx-k8s-state

// update this value
// sshAccess:
// from your.ip.address.topen/32 to 10.1.0.0/16
// This means allow any 22 traffic from the instances with in the VPC.

After making this change, we re-generate the terraform configurations by running:

1
2
3
4
kops update cluster --name=mx-cluster.k8s.local \
--state=s3://mx-k8s-state \
--out=. \
--target=terraform

Apply the configuration to create the cluster:

1
2
3
terraform init
terraform plan -out plan
terraform apply "plan"

Once this command is done, all the required AWS resources will be already created, and K8S clusters will be bootstrapping on the master and worker nodes. You can check whether your cluster is up and running by:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kops validate cluster mx-cluster.k8s.local --state=s3://mx-k8s-state
Validating cluster mx-cluster.k8s.local

INSTANCE GROUPS
NAME ROLE MACHINETYPE MIN MAX SUBNETS
master-us-east-1a Master m5.large 1 1 us-east-1a
master-us-east-1b Master m5.large 1 1 us-east-1b
master-us-east-1c Master m5.large 1 1 us-east-1c
nodes Node m5.large 1 1 us-east-1a,us-east-1b,us-east-1c

NODE STATUS
NAME ROLE READY
ip-10-1-121-175.ec2.internal master True
ip-10-1-39-127.ec2.internal master True
ip-10-1-47-176.ec2.internal node True
ip-10-1-86-18.ec2.internal master True

Your cluster mx-cluster.k8s.local is ready

To make yourself life easier, you can set environment variables for name and state so that you don’t have to repeat yourself every time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export NAME=mx-cluster.k8s.local
export KOPS_STATE_STORE=s3://mx-k8s-state

kops validate cluster  ✔  10008  13:05:34
Using cluster from kubectl context: mx-cluster.k8s.local

Validating cluster mx-cluster.k8s.local

INSTANCE GROUPS
NAME ROLE MACHINETYPE MIN MAX SUBNETS
master-us-east-1a Master m5.large 1 1 us-east-1a
master-us-east-1b Master m5.large 1 1 us-east-1b
master-us-east-1c Master m5.large 1 1 us-east-1c
nodes Node m5.large 1 1 us-east-1a,us-east-1b,us-east-1c

NODE STATUS
NAME ROLE READY
ip-10-1-121-175.ec2.internal master True
ip-10-1-39-127.ec2.internal master True
ip-10-1-47-176.ec2.internal node True
ip-10-1-86-18.ec2.internal master True

Your cluster mx-cluster.k8s.local is ready

Up to this point, the k8s cluster is up and running, in order to make it usable for us, we need to do some additional work: add some add-ons to it. Proceed to next section for instructions.

Deploy required add-ons for Kubernetes Cluster on AWS

After the bare-metal k8s cluster is up and running, We still need to deploy some add-ons onto the cluster to make it usable.

Dashboard

We might want to have a nice web-based UI to check the status of our cluster, here comes in the dashboard.

1
kubectl create -f https://raw.githubusercontent.com/kubernetes/kops/master/addons/kubernetes-dashboard/v1.8.3.yaml

Now visit the dashboard at:

https://your-api-elb/ui

You will be prompt to input login credentials, the user name will be admin, the password can be get by running:

1
kubectl config view --minify

The dashboard is integrated with RBAC of k8s, so in order to view all resources, you need to grant the default user as a cluster admin.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vi kube-system-rbac-role-binding.yml

# Input the following configurations.

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: system:default-sa
subjects:
- kind: ServiceAccount
name: default
namespace: kube-system
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io

kubectl create -f kube-system-rbac-role-binding.yml

Get the login token by running:

1
kubectl -n kube-system describe $(kubectl -n kube-system get secret -o name | grep 'default-token') | awk '/token:/ {print $2}'

Heapster

We also want to have some basic monitoring regarding the usage of CPU and RAM of the nodes in the cluster. We can deploy heapster to do it:

1
kubectl create -f https://github.com/kubernetes/kops/blob/master/addons/monitoring-standalone/v1.7.0.yaml

After heapster is up and running, we will start to get graphs about CPU and RAM usage in the dashboard.

Ingress Controller

Exposing all services running inside the cluster using LoadBalancer is not a real good idea, because for each service that you specify a LoadBalancer, the k8s cluster will create an AWS ELB for you, that means if you have 5 services, you will created 5 ELBs, which is obviously not ideal. To solve this problem, we can deploy a ingress controller add-on. The one we use will be the nginx ingress controller, which is able to do TLS termination, HTTP to HTTPs redirection and domain based routing.

In order to make this to work, we need create a few more AWS resources to support it:

  • An Ingress AWS ELB
  • An Security Group to be used by this AWS ELB which allows traffic from 80 and 443
  • An Security Group Rule for the nodes to allow traffic from the Ingress ELB.
  • Attach the nodes to the Ingress AWS ELB

We will continue to use Terraform to create these resources, modify the kubernetes.tf file generated previously and add in the following configurations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
resource "aws_security_group" "ingress-mx-cluster-k8s-local" {
name = "ingress.mx-cluster.k8s.local"
vpc_id = "vpc-d92424a2"
description = "Security group for nginx ingress ELB"

ingress {
from_port = 80
to_port = 80
protocol = "TCP"
cidr_blocks = ["0.0.0.0/0"]
}

ingress {
from_port = 443
to_port = 443
protocol = "TCP"
cidr_blocks = ["0.0.0.0/0"]
}

ingress {
from_port = 3
to_port = 4
protocol = "ICMP"
cidr_blocks = ["0.0.0.0/0"]
}

egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}

tags = {
KubernetesCluster = "mx-cluster.k8s.local"
Name = "ingress.mx-cluster.k8s.local"
"kubernetes.io/cluster/mx-cluster.k8s.local" = "owned"
}
}

resource "aws_security_group_rule" "all-node-to-ingress" {
type = "ingress"
security_group_id = "${aws_security_group.nodes-mx-cluster-k8s-local.id}"
source_security_group_id = "${aws_security_group.ingress-mx-cluster-k8s-local.id}"
from_port = 0
to_port = 0
protocol = "-1"
}

resource "aws_elb" "ingress-mx-cluster-k8s-local" {
name = "ingress-mx-cluster-k8s-local"

listener = {
instance_port = 30275
instance_protocol = "TCP"
lb_port = 443
lb_protocol = "TCP"
}

listener = {
instance_port = 31982
instance_protocol = "TCP"
lb_port = 80
lb_protocol = "TCP"
}

cross_zone_load_balancing = true

security_groups = ["${aws_security_group.ingress-mx-cluster-k8s-local.id}"]
subnets = ["subnet-4f894761", "subnet-7749842b", "subnet-91e070db"]

health_check = {
target = "TCP:31982"
healthy_threshold = 2
unhealthy_threshold = 6
interval = 10
timeout = 5
}

idle_timeout = 60

tags = {
KubernetesCluster = "mx-cluster.k8s.local"
"kubernetes.io/service-name" = "kube-ingress/ingress-nginx"
"kubernetes.io/cluster/mx-cluster.k8s.local" = "owned"
}
}

resource "aws_autoscaling_attachment" "nodes-mx-cluster-k8s-local" {
elb = "${aws_elb.ingress-mx-cluster-k8s-local.id}"
autoscaling_group_name = "${aws_autoscaling_group.nodes-mx-cluster-k8s-local.id}"
}

resource "aws_load_balancer_policy" "proxy-protocol" {
load_balancer_name = "${aws_elb.ingress-mx-cluster-k8s-local.name}"
policy_name = "k8s-proxyprotocol-enabled"
policy_type_name = "ProxyProtocolPolicyType"

policy_attribute = {
name = "ProxyProtocol"
value = "true"
}
}

resource "aws_load_balancer_backend_server_policy" "proxy-protocol-80" {
load_balancer_name = "${aws_elb.ingress-mx-cluster-k8s-local.name}"
instance_port = 31982

policy_names = [
"${aws_load_balancer_policy.proxy-protocol.policy_name}",
]
}

resource "aws_load_balancer_backend_server_policy" "proxy-protocol-443" {
load_balancer_name = "${aws_elb.ingress-mx-cluster-k8s-local.name}"
instance_port = 30275

policy_names = [
"${aws_load_balancer_policy.proxy-protocol.policy_name}",
]
}

Plan and review the changes and apply when you’re confirmed:

1
2
terraform plan -out plan
terraform apply "plan"

We now have the AWS infrastructure ready for the ingress controller, let’s deploy the ingress controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
vi nginx-ingress-controller.yml

# Add the following configurations

apiVersion: v1
kind: Namespace
metadata:
name: kube-ingress
labels:
k8s-addon: ingress-nginx.addons.k8s.io

---

apiVersion: v1
kind: ServiceAccount
metadata:
name: nginx-ingress-controller
namespace: kube-ingress
labels:
k8s-addon: ingress-nginx.addons.k8s.io

---

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
labels:
k8s-addon: ingress-nginx.addons.k8s.io
name: nginx-ingress-controller
namespace: kube-ingress
rules:
- apiGroups:
- ""
resources:
- configmaps
- endpoints
- nodes
- pods
- secrets
verbs:
- list
- watch
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- apiGroups:
- ""
resources:
- services
verbs:
- get
- list
- watch
- apiGroups:
- "extensions"
resources:
- ingresses
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- "extensions"
resources:
- ingresses/status
verbs:
- update

---

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
labels:
k8s-addon: ingress-nginx.addons.k8s.io
name: nginx-ingress-controller
namespace: kube-ingress
rules:
- apiGroups:
- ""
resources:
- configmaps
- pods
- secrets
verbs:
- get
- apiGroups:
- ""
resources:
- configmaps
resourceNames:
# Defaults to "<election-id>-<ingress-class>"
# Here: "<ingress-controller-leader>-<nginx>"
# This has to be adapted if you change either parameter
# when launching the nginx-ingress-controller.
- "ingress-controller-leader-nginx"
verbs:
- get
- update
- apiGroups:
- ""
resources:
- configmaps
verbs:
- create
- apiGroups:
- ""
resources:
- endpoints
verbs:
- get
- create
- update

---

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
labels:
k8s-addon: ingress-nginx.addons.k8s.io
name: nginx-ingress-controller
namespace: kube-ingress
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: nginx-ingress-controller
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: system:serviceaccount:kube-ingress:nginx-ingress-controller

---

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
labels:
k8s-addon: ingress-nginx.addons.k8s.io
name: nginx-ingress-controller
namespace: kube-ingress
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: nginx-ingress-controller
subjects:
- kind: ServiceAccount
name: nginx-ingress-controller
namespace: kube-ingress

---

kind: Service
apiVersion: v1
metadata:
name: nginx-default-backend
namespace: kube-ingress
labels:
k8s-app: default-http-backend
k8s-addon: ingress-nginx.addons.k8s.io
spec:
ports:
- port: 80
targetPort: http
selector:
app: nginx-default-backend

---

kind: Deployment
apiVersion: extensions/v1beta1
metadata:
name: nginx-default-backend
namespace: kube-ingress
labels:
k8s-app: default-http-backend
k8s-addon: ingress-nginx.addons.k8s.io
spec:
replicas: 1
revisionHistoryLimit: 10
template:
metadata:
labels:
k8s-app: default-http-backend
k8s-addon: ingress-nginx.addons.k8s.io
app: nginx-default-backend
spec:
terminationGracePeriodSeconds: 60
containers:
- name: default-http-backend
image: k8s.gcr.io/defaultbackend:1.3
livenessProbe:
httpGet:
path: /healthz
port: 8080
scheme: HTTP
initialDelaySeconds: 30
timeoutSeconds: 5
resources:
limits:
cpu: 10m
memory: 20Mi
requests:
cpu: 10m
memory: 20Mi
ports:
- name: http
containerPort: 8080
protocol: TCP

---

kind: ConfigMap
apiVersion: v1
metadata:
name: ingress-nginx
namespace: kube-ingress
labels:
k8s-addon: ingress-nginx.addons.k8s.io
data:
use-proxy-protocol: "true"

---

kind: Service
apiVersion: v1
metadata:
name: ingress-nginx
namespace: kube-ingress
labels:
k8s-addon: ingress-nginx.addons.k8s.io
spec:
type: NodePort
selector:
app: ingress-nginx
ports:
- name: http
nodePort: 31982
port: 80
protocol: TCP
targetPort: http
- name: https
nodePort: 30275
port: 443
protocol: TCP
targetPort: https

---

kind: Deployment
apiVersion: extensions/v1beta1
metadata:
name: ingress-nginx
namespace: kube-ingress
labels:
k8s-app: nginx-ingress-controller
k8s-addon: ingress-nginx.addons.k8s.io
spec:
replicas: 3
template:
metadata:
labels:
app: ingress-nginx
k8s-app: nginx-ingress-controller
k8s-addon: ingress-nginx.addons.k8s.io
annotations:
prometheus.io/port: '10254'
prometheus.io/scrape: 'true'
spec:
terminationGracePeriodSeconds: 60
serviceAccountName: nginx-ingress-controller
containers:
- image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.12.0
name: nginx-ingress-controller
imagePullPolicy: Always
ports:
- name: http
containerPort: 80
protocol: TCP
- name: https
containerPort: 443
protocol: TCP
readinessProbe:
httpGet:
path: /healthz
port: 10254
scheme: HTTP
livenessProbe:
httpGet:
path: /healthz
port: 10254
scheme: HTTP
initialDelaySeconds: 30
timeoutSeconds: 5
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
args:
- /nginx-ingress-controller
- --default-backend-service=$(POD_NAMESPACE)/nginx-default-backend
- --configmap=$(POD_NAMESPACE)/ingress-nginx
- --publish-service=$(POD_NAMESPACE)/ingress-nginx
- --annotations-prefix=ingress.kubernetes.io


kubectl create -f nginx-ingress-controller.yml

Now go to the ingress ELB and wait for a while, you should see the active instance becomes 1, this means the ingress has been successfully setup.

Up to this point, your cluster is ready for you to deploy your workload!

Nodes