mirror of
https://github.com/jpetazzo/container.training.git
synced 2026-02-28 00:13:51 +00:00
Compare commits
103 Commits
2020-07-ar
...
2020-11-nr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3797fc6f9f | ||
|
|
242a3d9ddf | ||
|
|
a14f5e81ce | ||
|
|
29edb1aefe | ||
|
|
bd3c91f342 | ||
|
|
fa709f0cb4 | ||
|
|
543b44fb29 | ||
|
|
109f6503e4 | ||
|
|
337be57182 | ||
|
|
63d88236b2 | ||
|
|
799aa21302 | ||
|
|
95247d6d39 | ||
|
|
536a9cc44b | ||
|
|
2ff3d88bab | ||
|
|
295ee9b6b4 | ||
|
|
17c5f6de01 | ||
|
|
556dbb965c | ||
|
|
32250f8053 | ||
|
|
bdede6de07 | ||
|
|
eefdc21488 | ||
|
|
e145428910 | ||
|
|
76789b6113 | ||
|
|
f9660ba9dc | ||
|
|
c2497508f8 | ||
|
|
b5d3b213b1 | ||
|
|
b4c76ad11d | ||
|
|
b251ff3812 | ||
|
|
ede4ea0dd5 | ||
|
|
2ab06c6dfd | ||
|
|
3a01deb039 | ||
|
|
b88f63e1f7 | ||
|
|
918311ac51 | ||
|
|
73e8110f09 | ||
|
|
ecb5106d59 | ||
|
|
e4d8cd4952 | ||
|
|
c4aedbd327 | ||
|
|
2fb3584b1b | ||
|
|
cb90cc9a1e | ||
|
|
bf28dff816 | ||
|
|
b5cb871c69 | ||
|
|
aa8f538574 | ||
|
|
ebf2e23785 | ||
|
|
0553a1ba8b | ||
|
|
9d47177028 | ||
|
|
9d4a035497 | ||
|
|
6fe74cb35c | ||
|
|
43aa41ed51 | ||
|
|
f6e810f648 | ||
|
|
4c710d6826 | ||
|
|
410c98399e | ||
|
|
19c9843a81 | ||
|
|
69d084e04a | ||
|
|
1300d76890 | ||
|
|
0040313371 | ||
|
|
c9e04b906d | ||
|
|
41f66f4144 | ||
|
|
aced587fd0 | ||
|
|
749b3d1648 | ||
|
|
c40cc71bbc | ||
|
|
69b775ef27 | ||
|
|
3bfc14c5f7 | ||
|
|
97984af8a2 | ||
|
|
9b31c45899 | ||
|
|
c0db28d439 | ||
|
|
0e49bfa837 | ||
|
|
fc9c0a6285 | ||
|
|
d4914fa168 | ||
|
|
e4edd9445c | ||
|
|
ba7deefce5 | ||
|
|
be104f1b44 | ||
|
|
5c329b0b79 | ||
|
|
78ffd22499 | ||
|
|
33174a1682 | ||
|
|
d402a2ea93 | ||
|
|
1fc3abcffd | ||
|
|
c1020f24b1 | ||
|
|
4fc81209d4 | ||
|
|
ed841711c5 | ||
|
|
07457af6f7 | ||
|
|
2d4961fbd3 | ||
|
|
14679999be | ||
|
|
29c6d2876a | ||
|
|
a02e7429ad | ||
|
|
fee0be7f09 | ||
|
|
d98fcbce87 | ||
|
|
35320837e5 | ||
|
|
d73e597198 | ||
|
|
b4c0378114 | ||
|
|
efdc4fcfa9 | ||
|
|
c32fcc81bb | ||
|
|
f6930042bd | ||
|
|
2e2767b090 | ||
|
|
115cc5e0c0 | ||
|
|
d252fe254b | ||
|
|
7d96562042 | ||
|
|
4ded8c699d | ||
|
|
620a3df798 | ||
|
|
d28723f07a | ||
|
|
f2334d2d1b | ||
|
|
ddf79eebc7 | ||
|
|
6467264ff5 | ||
|
|
55fcff9333 | ||
|
|
8fb7ea3908 |
@@ -9,21 +9,21 @@ services:
|
||||
|
||||
etcd:
|
||||
network_mode: "service:pause"
|
||||
image: k8s.gcr.io/etcd:3.4.3
|
||||
image: k8s.gcr.io/etcd:3.4.9
|
||||
command: etcd
|
||||
|
||||
kube-apiserver:
|
||||
network_mode: "service:pause"
|
||||
image: k8s.gcr.io/hyperkube:v1.17.2
|
||||
image: k8s.gcr.io/hyperkube:v1.18.8
|
||||
command: kube-apiserver --etcd-servers http://127.0.0.1:2379 --address 0.0.0.0 --disable-admission-plugins=ServiceAccount --allow-privileged
|
||||
|
||||
kube-controller-manager:
|
||||
network_mode: "service:pause"
|
||||
image: k8s.gcr.io/hyperkube:v1.17.2
|
||||
image: k8s.gcr.io/hyperkube:v1.18.8
|
||||
command: kube-controller-manager --master http://localhost:8080 --allocate-node-cidrs --cluster-cidr=10.CLUSTER.0.0/16
|
||||
"Edit the CLUSTER placeholder first. Then, remove this line.":
|
||||
|
||||
kube-scheduler:
|
||||
network_mode: "service:pause"
|
||||
image: k8s.gcr.io/hyperkube:v1.17.2
|
||||
image: k8s.gcr.io/hyperkube:v1.18.8
|
||||
command: kube-scheduler --master http://localhost:8080
|
||||
|
||||
@@ -9,20 +9,20 @@ services:
|
||||
|
||||
etcd:
|
||||
network_mode: "service:pause"
|
||||
image: k8s.gcr.io/etcd:3.4.3
|
||||
image: k8s.gcr.io/etcd:3.4.9
|
||||
command: etcd
|
||||
|
||||
kube-apiserver:
|
||||
network_mode: "service:pause"
|
||||
image: k8s.gcr.io/hyperkube:v1.17.2
|
||||
image: k8s.gcr.io/hyperkube:v1.18.8
|
||||
command: kube-apiserver --etcd-servers http://127.0.0.1:2379 --address 0.0.0.0 --disable-admission-plugins=ServiceAccount
|
||||
|
||||
kube-controller-manager:
|
||||
network_mode: "service:pause"
|
||||
image: k8s.gcr.io/hyperkube:v1.17.2
|
||||
image: k8s.gcr.io/hyperkube:v1.18.8
|
||||
command: kube-controller-manager --master http://localhost:8080
|
||||
|
||||
kube-scheduler:
|
||||
network_mode: "service:pause"
|
||||
image: k8s.gcr.io/hyperkube:v1.17.2
|
||||
image: k8s.gcr.io/hyperkube:v1.18.8
|
||||
command: kube-scheduler --master http://localhost:8080
|
||||
|
||||
33
k8s/certbot.yaml
Normal file
33
k8s/certbot.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: certbot
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: certbot
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- path: /.well-known/acme-challenge/
|
||||
backend:
|
||||
serviceName: certbot
|
||||
servicePort: 80
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: certbot
|
||||
subsets:
|
||||
- addresses:
|
||||
- ip: A.B.C.D
|
||||
ports:
|
||||
- port: 8000
|
||||
protocol: TCP
|
||||
|
||||
11
k8s/cm-certificate.yaml
Normal file
11
k8s/cm-certificate.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: xyz.A.B.C.D.nip.io
|
||||
spec:
|
||||
secretName: xyz.A.B.C.D.nip.io
|
||||
dnsNames:
|
||||
- xyz.A.B.C.D.nip.io
|
||||
issuerRef:
|
||||
name: letsencrypt-staging
|
||||
kind: ClusterIssuer
|
||||
18
k8s/cm-clusterissuer.yaml
Normal file
18
k8s/cm-clusterissuer.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-staging
|
||||
spec:
|
||||
acme:
|
||||
# Remember to update this if you use this manifest to obtain real certificates :)
|
||||
email: hello@example.com
|
||||
server: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
# To use the production environment, use the following line instead:
|
||||
#server: https://acme-v02.api.letsencrypt.org/directory
|
||||
privateKeySecretRef:
|
||||
name: issuer-letsencrypt-staging
|
||||
solvers:
|
||||
- http01:
|
||||
ingress:
|
||||
class: traefik
|
||||
|
||||
77
k8s/consul-1.yaml
Normal file
77
k8s/consul-1.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
# Basic Consul cluster using Cloud Auto-Join.
|
||||
# Caveats:
|
||||
# - no actual persistence
|
||||
# - scaling down to 1 will break the cluster
|
||||
# - pods may be colocated
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: consul
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources:
|
||||
- pods
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: consul
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: consul
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: consul
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: consul
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: consul
|
||||
spec:
|
||||
ports:
|
||||
- port: 8500
|
||||
name: http
|
||||
selector:
|
||||
app: consul
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: consul
|
||||
spec:
|
||||
serviceName: consul
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: consul
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: consul
|
||||
spec:
|
||||
serviceAccountName: consul
|
||||
containers:
|
||||
- name: consul
|
||||
image: "consul:1.8"
|
||||
env:
|
||||
- name: NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
args:
|
||||
- "agent"
|
||||
- "-bootstrap-expect=3"
|
||||
- "-retry-join=provider=k8s label_selector=\"app=consul\" namespace=\"$(NAMESPACE)\""
|
||||
- "-client=0.0.0.0"
|
||||
- "-data-dir=/consul/data"
|
||||
- "-server"
|
||||
- "-ui"
|
||||
@@ -1,5 +1,9 @@
|
||||
# Better Consul cluster.
|
||||
# There is still no actual persistence, but:
|
||||
# - podAntiaffinity prevents pod colocation
|
||||
# - clusters works when scaling down to 1 (thanks to lifecycle hook)
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
kind: Role
|
||||
metadata:
|
||||
name: consul
|
||||
rules:
|
||||
@@ -11,17 +15,16 @@ rules:
|
||||
- list
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: consul
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
kind: Role
|
||||
name: consul
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: consul
|
||||
namespace: default
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
@@ -68,11 +71,16 @@ spec:
|
||||
terminationGracePeriodSeconds: 10
|
||||
containers:
|
||||
- name: consul
|
||||
image: "consul:1.6"
|
||||
image: "consul:1.8"
|
||||
env:
|
||||
- name: NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
args:
|
||||
- "agent"
|
||||
- "-bootstrap-expect=3"
|
||||
- "-retry-join=provider=k8s label_selector=\"app=consul\""
|
||||
- "-retry-join=provider=k8s label_selector=\"app=consul\" namespace=\"$(NAMESPACE)\""
|
||||
- "-client=0.0.0.0"
|
||||
- "-data-dir=/consul/data"
|
||||
- "-server"
|
||||
104
k8s/consul-3.yaml
Normal file
104
k8s/consul-3.yaml
Normal file
@@ -0,0 +1,104 @@
|
||||
# Even better Consul cluster.
|
||||
# That one uses a volumeClaimTemplate to achieve true persistence.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: consul
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources:
|
||||
- pods
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: consul
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: consul
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: consul
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: consul
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: consul
|
||||
spec:
|
||||
ports:
|
||||
- port: 8500
|
||||
name: http
|
||||
selector:
|
||||
app: consul
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: consul
|
||||
spec:
|
||||
serviceName: consul
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: consul
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: data
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: consul
|
||||
spec:
|
||||
serviceAccountName: consul
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
- labelSelector:
|
||||
matchExpressions:
|
||||
- key: app
|
||||
operator: In
|
||||
values:
|
||||
- persistentconsul
|
||||
topologyKey: kubernetes.io/hostname
|
||||
terminationGracePeriodSeconds: 10
|
||||
containers:
|
||||
- name: consul
|
||||
image: "consul:1.8"
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /consul/data
|
||||
env:
|
||||
- name: NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
args:
|
||||
- "agent"
|
||||
- "-bootstrap-expect=3"
|
||||
- "-retry-join=provider=k8s label_selector=\"app=consul\" namespace=\"$(NAMESPACE)\""
|
||||
- "-client=0.0.0.0"
|
||||
- "-data-dir=/consul/data"
|
||||
- "-server"
|
||||
- "-ui"
|
||||
lifecycle:
|
||||
preStop:
|
||||
exec:
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- consul leave
|
||||
336
k8s/dashboard-with-token.yaml
Normal file
336
k8s/dashboard-with-token.yaml
Normal file
@@ -0,0 +1,336 @@
|
||||
# This file is based on the following manifest:
|
||||
# https://github.com/kubernetes/dashboard/blob/master/aio/deploy/recommended.yaml
|
||||
# It adds a ServiceAccount that has cluster-admin privileges on the cluster,
|
||||
# and exposes the dashboard on a NodePort. It makes it easier to do quick demos
|
||||
# of the Kubernetes dashboard, without compromising the security too much.
|
||||
|
||||
# Copyright 2017 The Kubernetes Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: kubernetes-dashboard
|
||||
|
||||
---
|
||||
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
name: kubernetes-dashboard
|
||||
namespace: kubernetes-dashboard
|
||||
|
||||
---
|
||||
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
name: kubernetes-dashboard
|
||||
namespace: kubernetes-dashboard
|
||||
spec:
|
||||
type: NodePort
|
||||
ports:
|
||||
- port: 443
|
||||
targetPort: 8443
|
||||
selector:
|
||||
k8s-app: kubernetes-dashboard
|
||||
|
||||
---
|
||||
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
name: kubernetes-dashboard-certs
|
||||
namespace: kubernetes-dashboard
|
||||
type: Opaque
|
||||
|
||||
---
|
||||
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
name: kubernetes-dashboard-csrf
|
||||
namespace: kubernetes-dashboard
|
||||
type: Opaque
|
||||
data:
|
||||
csrf: ""
|
||||
|
||||
---
|
||||
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
name: kubernetes-dashboard-key-holder
|
||||
namespace: kubernetes-dashboard
|
||||
type: Opaque
|
||||
|
||||
---
|
||||
|
||||
kind: ConfigMap
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
name: kubernetes-dashboard-settings
|
||||
namespace: kubernetes-dashboard
|
||||
|
||||
---
|
||||
|
||||
kind: Role
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
name: kubernetes-dashboard
|
||||
namespace: kubernetes-dashboard
|
||||
rules:
|
||||
# Allow Dashboard to get, update and delete Dashboard exclusive secrets.
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
resourceNames: ["kubernetes-dashboard-key-holder", "kubernetes-dashboard-certs", "kubernetes-dashboard-csrf"]
|
||||
verbs: ["get", "update", "delete"]
|
||||
# Allow Dashboard to get and update 'kubernetes-dashboard-settings' config map.
|
||||
- apiGroups: [""]
|
||||
resources: ["configmaps"]
|
||||
resourceNames: ["kubernetes-dashboard-settings"]
|
||||
verbs: ["get", "update"]
|
||||
# Allow Dashboard to get metrics.
|
||||
- apiGroups: [""]
|
||||
resources: ["services"]
|
||||
resourceNames: ["heapster", "dashboard-metrics-scraper"]
|
||||
verbs: ["proxy"]
|
||||
- apiGroups: [""]
|
||||
resources: ["services/proxy"]
|
||||
resourceNames: ["heapster", "http:heapster:", "https:heapster:", "dashboard-metrics-scraper", "http:dashboard-metrics-scraper"]
|
||||
verbs: ["get"]
|
||||
|
||||
---
|
||||
|
||||
kind: ClusterRole
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
name: kubernetes-dashboard
|
||||
rules:
|
||||
# Allow Metrics Scraper to get metrics from the Metrics server
|
||||
- apiGroups: ["metrics.k8s.io"]
|
||||
resources: ["pods", "nodes"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
---
|
||||
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
name: kubernetes-dashboard
|
||||
namespace: kubernetes-dashboard
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: kubernetes-dashboard
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: kubernetes-dashboard
|
||||
namespace: kubernetes-dashboard
|
||||
|
||||
---
|
||||
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: kubernetes-dashboard
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: kubernetes-dashboard
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: kubernetes-dashboard
|
||||
namespace: kubernetes-dashboard
|
||||
|
||||
---
|
||||
|
||||
kind: Deployment
|
||||
apiVersion: apps/v1
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
name: kubernetes-dashboard
|
||||
namespace: kubernetes-dashboard
|
||||
spec:
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 10
|
||||
selector:
|
||||
matchLabels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
spec:
|
||||
containers:
|
||||
- name: kubernetes-dashboard
|
||||
image: kubernetesui/dashboard:v2.0.0
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8443
|
||||
protocol: TCP
|
||||
args:
|
||||
- --auto-generate-certificates
|
||||
- --namespace=kubernetes-dashboard
|
||||
# Uncomment the following line to manually specify Kubernetes API server Host
|
||||
# If not specified, Dashboard will attempt to auto discover the API server and connect
|
||||
# to it. Uncomment only if the default does not work.
|
||||
# - --apiserver-host=http://my-address:port
|
||||
volumeMounts:
|
||||
- name: kubernetes-dashboard-certs
|
||||
mountPath: /certs
|
||||
# Create on-disk volume to store exec logs
|
||||
- mountPath: /tmp
|
||||
name: tmp-volume
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
scheme: HTTPS
|
||||
path: /
|
||||
port: 8443
|
||||
initialDelaySeconds: 30
|
||||
timeoutSeconds: 30
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsUser: 1001
|
||||
runAsGroup: 2001
|
||||
volumes:
|
||||
- name: kubernetes-dashboard-certs
|
||||
secret:
|
||||
secretName: kubernetes-dashboard-certs
|
||||
- name: tmp-volume
|
||||
emptyDir: {}
|
||||
serviceAccountName: kubernetes-dashboard
|
||||
nodeSelector:
|
||||
"kubernetes.io/os": linux
|
||||
# Comment the following tolerations if Dashboard must not be deployed on master
|
||||
tolerations:
|
||||
- key: node-role.kubernetes.io/master
|
||||
effect: NoSchedule
|
||||
|
||||
---
|
||||
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: dashboard-metrics-scraper
|
||||
name: dashboard-metrics-scraper
|
||||
namespace: kubernetes-dashboard
|
||||
spec:
|
||||
ports:
|
||||
- port: 8000
|
||||
targetPort: 8000
|
||||
selector:
|
||||
k8s-app: dashboard-metrics-scraper
|
||||
|
||||
---
|
||||
|
||||
kind: Deployment
|
||||
apiVersion: apps/v1
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: dashboard-metrics-scraper
|
||||
name: dashboard-metrics-scraper
|
||||
namespace: kubernetes-dashboard
|
||||
spec:
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 10
|
||||
selector:
|
||||
matchLabels:
|
||||
k8s-app: dashboard-metrics-scraper
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: dashboard-metrics-scraper
|
||||
annotations:
|
||||
seccomp.security.alpha.kubernetes.io/pod: 'runtime/default'
|
||||
spec:
|
||||
containers:
|
||||
- name: dashboard-metrics-scraper
|
||||
image: kubernetesui/metrics-scraper:v1.0.4
|
||||
ports:
|
||||
- containerPort: 8000
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
scheme: HTTP
|
||||
path: /
|
||||
port: 8000
|
||||
initialDelaySeconds: 30
|
||||
timeoutSeconds: 30
|
||||
volumeMounts:
|
||||
- mountPath: /tmp
|
||||
name: tmp-volume
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsUser: 1001
|
||||
runAsGroup: 2001
|
||||
serviceAccountName: kubernetes-dashboard
|
||||
nodeSelector:
|
||||
"kubernetes.io/os": linux
|
||||
# Comment the following tolerations if Dashboard must not be deployed on master
|
||||
tolerations:
|
||||
- key: node-role.kubernetes.io/master
|
||||
effect: NoSchedule
|
||||
volumes:
|
||||
- name: tmp-volume
|
||||
emptyDir: {}
|
||||
|
||||
---
|
||||
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
name: cluster-admin
|
||||
namespace: kubernetes-dashboard
|
||||
|
||||
---
|
||||
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
name: kubernetes-dashboard-cluster-admin
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: cluster-admin
|
||||
namespace: kubernetes-dashboard
|
||||
30
k8s/event-node.yaml
Normal file
30
k8s/event-node.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
kind: Event
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
generateName: hello-
|
||||
labels:
|
||||
container.training/test: ""
|
||||
|
||||
#eventTime: "2020-07-04T00:00:00.000000Z"
|
||||
#firstTimestamp: "2020-01-01T00:00:00.000000Z"
|
||||
#lastTimestamp: "2020-12-31T00:00:00.000000Z"
|
||||
#count: 42
|
||||
|
||||
involvedObject:
|
||||
kind: Node
|
||||
apiVersion: v1
|
||||
name: kind-control-plane
|
||||
# Note: the uid should be the Node name (not the uid of the Node).
|
||||
# This might be specific to global objects.
|
||||
uid: kind-control-plane
|
||||
|
||||
type: Warning
|
||||
reason: NodeOverheat
|
||||
message: "Node temperature exceeds critical threshold"
|
||||
action: Hello
|
||||
source:
|
||||
component: thermal-probe
|
||||
#host: node1
|
||||
#reportingComponent: ""
|
||||
#reportingInstance: ""
|
||||
|
||||
36
k8s/event-pod.yaml
Normal file
36
k8s/event-pod.yaml
Normal file
@@ -0,0 +1,36 @@
|
||||
kind: Event
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
# One convention is to use <objectname>.<timestamp>,
|
||||
# where the timestamp is taken with a nanosecond
|
||||
# precision and expressed in hexadecimal.
|
||||
# Example: web-5dcb957ccc-fjvzc.164689730a36ec3d
|
||||
name: hello.1234567890
|
||||
# The label doesn't serve any purpose, except making
|
||||
# it easier to identify or delete that specific event.
|
||||
labels:
|
||||
container.training/test: ""
|
||||
|
||||
#eventTime: "2020-07-04T00:00:00.000000Z"
|
||||
#firstTimestamp: "2020-01-01T00:00:00.000000Z"
|
||||
#lastTimestamp: "2020-12-31T00:00:00.000000Z"
|
||||
#count: 42
|
||||
|
||||
involvedObject:
|
||||
### These 5 lines should be updated to refer to an object.
|
||||
### Make sure to put the correct "uid", because it is what
|
||||
### "kubectl describe" is using to gather relevant events.
|
||||
#apiVersion: v1
|
||||
#kind: Pod
|
||||
#name: magic-bean
|
||||
#namespace: blue
|
||||
#uid: 7f28fda8-6ef4-4580-8d87-b55721fcfc30
|
||||
|
||||
type: Normal
|
||||
reason: BackupSuccessful
|
||||
message: "Object successfully dumped to gitops repository"
|
||||
source:
|
||||
component: gitops-sync
|
||||
#reportingComponent: ""
|
||||
#reportingInstance: ""
|
||||
|
||||
@@ -27,7 +27,7 @@ spec:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- "apk update && apk add curl && curl https://github.com/jpetazzo.keys > /root/.ssh/authorized_keys"
|
||||
- "mkdir -p /root/.ssh && apk update && apk add curl && curl https://github.com/jpetazzo.keys > /root/.ssh/authorized_keys"
|
||||
containers:
|
||||
- name: web
|
||||
image: nginx
|
||||
|
||||
29
k8s/hpa-v2-pa-httplat.yaml
Normal file
29
k8s/hpa-v2-pa-httplat.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
kind: HorizontalPodAutoscaler
|
||||
apiVersion: autoscaling/v2beta2
|
||||
metadata:
|
||||
name: rng
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: rng
|
||||
minReplicas: 1
|
||||
maxReplicas: 20
|
||||
behavior:
|
||||
scaleUp:
|
||||
stabilizationWindowSeconds: 60
|
||||
scaleDown:
|
||||
stabilizationWindowSeconds: 180
|
||||
metrics:
|
||||
- type: Object
|
||||
object:
|
||||
describedObject:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
name: httplat
|
||||
metric:
|
||||
name: httplat_latency_seconds
|
||||
target:
|
||||
type: Value
|
||||
value: 0.1
|
||||
|
||||
@@ -3,6 +3,10 @@ kind: Ingress
|
||||
metadata:
|
||||
name: whatever
|
||||
spec:
|
||||
#tls:
|
||||
#- secretName: whatever.A.B.C.D.nip.io
|
||||
# hosts:
|
||||
# - whatever.A.B.C.D.nip.io
|
||||
rules:
|
||||
- host: whatever.A.B.C.D.nip.io
|
||||
http:
|
||||
|
||||
63
k8s/kyverno-namespace-setup.yaml
Normal file
63
k8s/kyverno-namespace-setup.yaml
Normal file
@@ -0,0 +1,63 @@
|
||||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: setup-namespace
|
||||
spec:
|
||||
rules:
|
||||
- name: setup-limitrange
|
||||
match:
|
||||
resources:
|
||||
kinds:
|
||||
- Namespace
|
||||
generate:
|
||||
kind: LimitRange
|
||||
name: default-limitrange
|
||||
namespace: "{{request.object.metadata.name}}"
|
||||
data:
|
||||
spec:
|
||||
limits:
|
||||
- type: Container
|
||||
min:
|
||||
cpu: 0.1
|
||||
memory: 0.1
|
||||
max:
|
||||
cpu: 2
|
||||
memory: 2Gi
|
||||
default:
|
||||
cpu: 0.25
|
||||
memory: 500Mi
|
||||
defaultRequest:
|
||||
cpu: 0.25
|
||||
memory: 250Mi
|
||||
- name: setup-resourcequota
|
||||
match:
|
||||
resources:
|
||||
kinds:
|
||||
- Namespace
|
||||
generate:
|
||||
kind: ResourceQuota
|
||||
name: default-resourcequota
|
||||
namespace: "{{request.object.metadata.name}}"
|
||||
data:
|
||||
spec:
|
||||
hard:
|
||||
requests.cpu: "10"
|
||||
requests.memory: 10Gi
|
||||
limits.cpu: "20"
|
||||
limits.memory: 20Gi
|
||||
- name: setup-networkpolicy
|
||||
match:
|
||||
resources:
|
||||
kinds:
|
||||
- Namespace
|
||||
generate:
|
||||
kind: NetworkPolicy
|
||||
name: default-networkpolicy
|
||||
namespace: "{{request.object.metadata.name}}"
|
||||
data:
|
||||
spec:
|
||||
podSelector: {}
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector: {}
|
||||
|
||||
22
k8s/kyverno-pod-color-1.yaml
Normal file
22
k8s/kyverno-pod-color-1.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: pod-color-policy-1
|
||||
spec:
|
||||
validationFailureAction: enforce
|
||||
rules:
|
||||
- name: ensure-pod-color-is-valid
|
||||
match:
|
||||
resources:
|
||||
kinds:
|
||||
- Pod
|
||||
selector:
|
||||
matchExpressions:
|
||||
- key: color
|
||||
operator: Exists
|
||||
- key: color
|
||||
operator: NotIn
|
||||
values: [ red, green, blue ]
|
||||
validate:
|
||||
message: "If it exists, the label color must be red, green, or blue."
|
||||
deny: {}
|
||||
21
k8s/kyverno-pod-color-2.yaml
Normal file
21
k8s/kyverno-pod-color-2.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: pod-color-policy-2
|
||||
spec:
|
||||
validationFailureAction: enforce
|
||||
background: false
|
||||
rules:
|
||||
- name: prevent-color-change
|
||||
match:
|
||||
resources:
|
||||
kinds:
|
||||
- Pod
|
||||
validate:
|
||||
message: "Once label color has been added, it cannot be changed."
|
||||
deny:
|
||||
conditions:
|
||||
- key: "{{ request.oldObject.metadata.labels.color }}"
|
||||
operator: NotEqual
|
||||
value: "{{ request.object.metadata.labels.color }}"
|
||||
|
||||
25
k8s/kyverno-pod-color-3.yaml
Normal file
25
k8s/kyverno-pod-color-3.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: pod-color-policy-3
|
||||
spec:
|
||||
validationFailureAction: enforce
|
||||
background: false
|
||||
rules:
|
||||
- name: prevent-color-removal
|
||||
match:
|
||||
resources:
|
||||
kinds:
|
||||
- Pod
|
||||
selector:
|
||||
matchExpressions:
|
||||
- key: color
|
||||
operator: DoesNotExist
|
||||
validate:
|
||||
message: "Once label color has been added, it cannot be removed."
|
||||
deny:
|
||||
conditions:
|
||||
- key: "{{ request.oldObject.metadata.labels.color }}"
|
||||
operator: NotIn
|
||||
value: []
|
||||
|
||||
@@ -5,8 +5,8 @@ metadata:
|
||||
annotations:
|
||||
apparmor.security.beta.kubernetes.io/allowedProfileNames: runtime/default
|
||||
apparmor.security.beta.kubernetes.io/defaultProfileName: runtime/default
|
||||
seccomp.security.alpha.kubernetes.io/allowedProfileNames: docker/default
|
||||
seccomp.security.alpha.kubernetes.io/defaultProfileName: docker/default
|
||||
seccomp.security.alpha.kubernetes.io/allowedProfileNames: runtime/default
|
||||
seccomp.security.alpha.kubernetes.io/defaultProfileName: runtime/default
|
||||
name: restricted
|
||||
spec:
|
||||
allowPrivilegeEscalation: false
|
||||
|
||||
17
k8s/test.yaml
Normal file
17
k8s/test.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: whatever
|
||||
spec:
|
||||
#tls:
|
||||
#- secretName: whatever.A.B.C.D.nip.io
|
||||
# hosts:
|
||||
# - whatever.A.B.C.D.nip.io
|
||||
rules:
|
||||
- host: whatever.nip.io
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
backend:
|
||||
serviceName: whatever
|
||||
servicePort: 1234
|
||||
@@ -50,8 +50,10 @@ spec:
|
||||
- --api.insecure
|
||||
- --log.level=INFO
|
||||
- --metrics.prometheus
|
||||
- --providers.kubernetescrd
|
||||
- --providers.kubernetesingress
|
||||
- --entrypoints.http.Address=:80
|
||||
- --entrypoints.https.Address=:443
|
||||
- --entrypoints.https.http.tls.certResolver=default
|
||||
---
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
@@ -96,6 +98,15 @@ rules:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingresses
|
||||
- ingressclasses
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
---
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
|
||||
@@ -1 +1 @@
|
||||
traefik-v1.yaml
|
||||
traefik-v2.yaml
|
||||
24
prepare-vms/infra/example.openstack-cli
Normal file
24
prepare-vms/infra/example.openstack-cli
Normal file
@@ -0,0 +1,24 @@
|
||||
INFRACLASS=openstack-cli
|
||||
|
||||
# Copy that file to e.g. openstack or ovh, then customize it.
|
||||
# Some Openstack providers (like OVHcloud) will let you download
|
||||
# a file containing credentials. That's what you need to use.
|
||||
# The file below contains some example values.
|
||||
export OS_AUTH_URL=https://auth.cloud.ovh.net/v3/
|
||||
export OS_IDENTITY_API_VERSION=3
|
||||
export OS_USER_DOMAIN_NAME=${OS_USER_DOMAIN_NAME:-"Default"}
|
||||
export OS_PROJECT_DOMAIN_NAME=${OS_PROJECT_DOMAIN_NAME:-"Default"}
|
||||
export OS_TENANT_ID=abcd1234
|
||||
export OS_TENANT_NAME="0123456"
|
||||
export OS_USERNAME="user-xyz123"
|
||||
export OS_PASSWORD=AbCd1234
|
||||
export OS_REGION_NAME="GRA7"
|
||||
|
||||
# And then some values to indicate server type, image, etc.
|
||||
# You can see available flavors with `openstack flavor list`
|
||||
export OS_FLAVOR=s1-4
|
||||
# You can see available images with `openstack image list`
|
||||
export OS_IMAGE=896c5f54-51dc-44f0-8c22-ce99ba7164df
|
||||
# You can create a key with `openstack keypair create --public-key ~/.ssh/id_rsa.pub containertraining`
|
||||
export OS_KEY=containertraining
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
INFRACLASS=openstack
|
||||
INFRACLASS=openstack-tf
|
||||
|
||||
# If you are using OpenStack, copy this file (e.g. to "openstack" or "enix")
|
||||
# and customize the variables below.
|
||||
export TF_VAR_user="jpetazzo"
|
||||
@@ -6,4 +7,4 @@ export TF_VAR_tenant="training"
|
||||
export TF_VAR_domain="Default"
|
||||
export TF_VAR_password="..."
|
||||
export TF_VAR_auth_url="https://api.r1.nxs.enix.io/v3"
|
||||
export TF_VAR_flavor="GP1.S"
|
||||
export TF_VAR_flavor="GP1.S"
|
||||
5
prepare-vms/infra/hetzner
Normal file
5
prepare-vms/infra/hetzner
Normal file
@@ -0,0 +1,5 @@
|
||||
INFRACLASS=hetzner
|
||||
if ! [ -f ~/.config/hcloud/cli.toml ]; then
|
||||
warn "~/.config/hcloud/cli.toml not found."
|
||||
warn "Make sure that the Hetzner CLI (hcloud) is installed and configured."
|
||||
fi
|
||||
1
prepare-vms/infra/scaleway
Normal file
1
prepare-vms/infra/scaleway
Normal file
@@ -0,0 +1 @@
|
||||
INFRACLASS=scaleway
|
||||
@@ -43,6 +43,16 @@ _cmd_cards() {
|
||||
info "$0 www"
|
||||
}
|
||||
|
||||
_cmd clean "Remove information about stopped clusters"
|
||||
_cmd_clean() {
|
||||
for TAG in tags/*; do
|
||||
if grep -q ^stopped$ "$TAG/status"; then
|
||||
info "Removing $TAG..."
|
||||
rm -rf "$TAG"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
_cmd deploy "Install Docker on a bunch of running VMs"
|
||||
_cmd_deploy() {
|
||||
TAG=$1
|
||||
@@ -65,6 +75,27 @@ _cmd_deploy() {
|
||||
sleep 1
|
||||
done"
|
||||
|
||||
# Special case for scaleway since it doesn't come with sudo
|
||||
if [ "$INFRACLASS" = "scaleway" ]; then
|
||||
pssh -l root "
|
||||
grep DEBIAN_FRONTEND /etc/environment || echo DEBIAN_FRONTEND=noninteractive >> /etc/environment
|
||||
grep cloud-init /etc/sudoers && rm /etc/sudoers
|
||||
apt-get update && apt-get install sudo -y"
|
||||
fi
|
||||
|
||||
# FIXME
|
||||
# Special case for hetzner since it doesn't have an ubuntu user
|
||||
#if [ "$INFRACLASS" = "hetzner" ]; then
|
||||
# pssh -l root "
|
||||
#[ -d /home/ubuntu ] ||
|
||||
# useradd ubuntu -m -s /bin/bash
|
||||
#echo 'ubuntu ALL=(ALL:ALL) NOPASSWD:ALL' > /etc/sudoers.d/ubuntu
|
||||
#[ -d /home/ubuntu/.ssh ] ||
|
||||
# install --owner=ubuntu --mode=700 --directory /home/ubuntu/.ssh
|
||||
#[ -f /home/ubuntu/.ssh/authorized_keys ] ||
|
||||
# install --owner=ubuntu --mode=600 /root/.ssh/authorized_keys --target-directory /home/ubuntu/.ssh"
|
||||
#fi
|
||||
|
||||
# Copy settings and install Python YAML parser
|
||||
pssh -I tee /tmp/settings.yaml <tags/$TAG/settings.yaml
|
||||
pssh "
|
||||
@@ -131,19 +162,19 @@ _cmd_kubebins() {
|
||||
cd /usr/local/bin
|
||||
if ! [ -x etcd ]; then
|
||||
##VERSION##
|
||||
curl -L https://github.com/etcd-io/etcd/releases/download/v3.4.3/etcd-v3.4.3-linux-amd64.tar.gz \
|
||||
curl -L https://github.com/etcd-io/etcd/releases/download/v3.4.9/etcd-v3.4.9-linux-amd64.tar.gz \
|
||||
| sudo tar --strip-components=1 --wildcards -zx '*/etcd' '*/etcdctl'
|
||||
fi
|
||||
if ! [ -x hyperkube ]; then
|
||||
##VERSION##
|
||||
curl -L https://dl.k8s.io/v1.17.2/kubernetes-server-linux-amd64.tar.gz \
|
||||
curl -L https://dl.k8s.io/v1.18.10/kubernetes-server-linux-amd64.tar.gz \
|
||||
| sudo tar --strip-components=3 -zx \
|
||||
kubernetes/server/bin/kube{ctl,let,-proxy,-apiserver,-scheduler,-controller-manager}
|
||||
fi
|
||||
sudo mkdir -p /opt/cni/bin
|
||||
cd /opt/cni/bin
|
||||
if ! [ -x bridge ]; then
|
||||
curl -L https://github.com/containernetworking/plugins/releases/download/v0.7.6/cni-plugins-amd64-v0.7.6.tgz \
|
||||
curl -L https://github.com/containernetworking/plugins/releases/download/v0.8.6/cni-plugins-linux-amd64-v0.8.6.tgz \
|
||||
| sudo tar -zx
|
||||
fi
|
||||
"
|
||||
@@ -173,13 +204,15 @@ _cmd_kube() {
|
||||
pssh --timeout 200 "
|
||||
sudo apt-get update -q &&
|
||||
sudo apt-get install -qy kubelet$EXTRA_APTGET kubeadm$EXTRA_APTGET kubectl$EXTRA_APTGET &&
|
||||
kubectl completion bash | sudo tee /etc/bash_completion.d/kubectl"
|
||||
kubectl completion bash | sudo tee /etc/bash_completion.d/kubectl &&
|
||||
echo 'alias k=kubectl' | sudo tee /etc/bash_completion.d/k &&
|
||||
echo 'complete -F __start_kubectl k' | sudo tee -a /etc/bash_completion.d/k"
|
||||
|
||||
# Initialize kube master
|
||||
pssh --timeout 200 "
|
||||
if i_am_first_node && [ ! -f /etc/kubernetes/admin.conf ]; then
|
||||
kubeadm token generate > /tmp/token &&
|
||||
sudo kubeadm init $EXTRA_KUBEADM --token \$(cat /tmp/token) --apiserver-cert-extra-sans \$(cat /tmp/ipv4)
|
||||
sudo kubeadm init $EXTRA_KUBEADM --token \$(cat /tmp/token) --apiserver-cert-extra-sans \$(cat /tmp/ipv4) --ignore-preflight-errors=NumCPU
|
||||
fi"
|
||||
|
||||
# Put kubeconfig in ubuntu's and docker's accounts
|
||||
@@ -212,17 +245,23 @@ _cmd_kube() {
|
||||
if i_am_first_node; then
|
||||
kubectl apply -f https://raw.githubusercontent.com/jpetazzo/container.training/master/k8s/metrics-server.yaml
|
||||
fi"
|
||||
}
|
||||
|
||||
_cmd kubetools "Install a bunch of CLI tools for Kubernetes"
|
||||
_cmd_kubetools() {
|
||||
TAG=$1
|
||||
need_tag
|
||||
|
||||
# Install kubectx and kubens
|
||||
pssh "
|
||||
[ -d kubectx ] || git clone https://github.com/ahmetb/kubectx &&
|
||||
sudo ln -sf /home/ubuntu/kubectx/kubectx /usr/local/bin/kctx &&
|
||||
sudo ln -sf /home/ubuntu/kubectx/kubens /usr/local/bin/kns &&
|
||||
sudo cp /home/ubuntu/kubectx/completion/*.bash /etc/bash_completion.d &&
|
||||
sudo ln -sf \$HOME/kubectx/kubectx /usr/local/bin/kctx &&
|
||||
sudo ln -sf \$HOME/kubectx/kubens /usr/local/bin/kns &&
|
||||
sudo cp \$HOME/kubectx/completion/*.bash /etc/bash_completion.d &&
|
||||
[ -d kube-ps1 ] || git clone https://github.com/jonmosco/kube-ps1 &&
|
||||
sudo -u docker sed -i s/docker-prompt/kube_ps1/ /home/docker/.bashrc &&
|
||||
sudo -u docker tee -a /home/docker/.bashrc <<EOF
|
||||
. /home/ubuntu/kube-ps1/kube-ps1.sh
|
||||
. \$HOME/kube-ps1/kube-ps1.sh
|
||||
KUBE_PS1_PREFIX=""
|
||||
KUBE_PS1_SUFFIX=""
|
||||
KUBE_PS1_SYMBOL_ENABLE="false"
|
||||
@@ -273,7 +312,54 @@ EOF"
|
||||
sudo chmod +x /usr/local/bin/aws-iam-authenticator
|
||||
fi"
|
||||
|
||||
sep "Done"
|
||||
# Install the krew package manager
|
||||
pssh "
|
||||
if [ ! -d /home/docker/.krew ]; then
|
||||
cd /tmp &&
|
||||
curl -fsSL https://github.com/kubernetes-sigs/krew/releases/latest/download/krew.tar.gz |
|
||||
tar -zxf- &&
|
||||
sudo -u docker -H ./krew-linux_amd64 install krew &&
|
||||
echo export PATH=/home/docker/.krew/bin:\\\$PATH | sudo -u docker tee -a /home/docker/.bashrc
|
||||
fi"
|
||||
|
||||
# Install k9s and popeye
|
||||
pssh "
|
||||
if [ ! -x /usr/local/bin/k9s ]; then
|
||||
FILENAME=k9s_\$(uname -s)_\$(uname -m).tar.gz &&
|
||||
curl -sSL https://github.com/derailed/k9s/releases/latest/download/\$FILENAME |
|
||||
sudo tar -zxvf- -C /usr/local/bin k9s
|
||||
fi
|
||||
if [ ! -x /usr/local/bin/popeye ]; then
|
||||
FILENAME=popeye_\$(uname -s)_\$(uname -m).tar.gz &&
|
||||
curl -sSL https://github.com/derailed/popeye/releases/latest/download/\$FILENAME |
|
||||
sudo tar -zxvf- -C /usr/local/bin popeye
|
||||
fi"
|
||||
|
||||
# Install Tilt
|
||||
pssh "
|
||||
if [ ! -x /usr/local/bin/tilt ]; then
|
||||
curl -fsSL https://raw.githubusercontent.com/tilt-dev/tilt/master/scripts/install.sh | bash
|
||||
fi"
|
||||
|
||||
# Install Skaffold
|
||||
pssh "
|
||||
if [ ! -x /usr/local/bin/skaffold ]; then
|
||||
curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-amd64 &&
|
||||
sudo install skaffold /usr/local/bin/
|
||||
fi"
|
||||
|
||||
# Install Kompose
|
||||
pssh "
|
||||
if [ ! -x /usr/local/bin/kompose ]; then
|
||||
curl -Lo kompose https://github.com/kubernetes/kompose/releases/latest/download/kompose-linux-amd64 &&
|
||||
sudo install kompose /usr/local/bin
|
||||
fi"
|
||||
|
||||
pssh "
|
||||
if [ ! -x /usr/local/bin/kubeseal ]; then
|
||||
curl -Lo kubeseal https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.13.1/kubeseal-linux-amd64 &&
|
||||
sudo install kubeseal /usr/local/bin
|
||||
fi"
|
||||
}
|
||||
|
||||
_cmd kubereset "Wipe out Kubernetes configuration on all nodes"
|
||||
@@ -295,29 +381,44 @@ _cmd_kubetest() {
|
||||
set -e
|
||||
if i_am_first_node; then
|
||||
which kubectl
|
||||
for NODE in \$(awk /[0-9]\$/\ {print\ \\\$2} /etc/hosts); do
|
||||
for NODE in \$(grep [0-9]\$ /etc/hosts | grep -v ^127 | awk {print\ \\\$2}); do
|
||||
echo \$NODE ; kubectl get nodes | grep -w \$NODE | grep -w Ready
|
||||
done
|
||||
fi"
|
||||
}
|
||||
|
||||
_cmd ids "(FIXME) List the instance IDs belonging to a given tag or token"
|
||||
_cmd_ids() {
|
||||
_cmd ips "Show the IP addresses for a given tag"
|
||||
_cmd_ips() {
|
||||
TAG=$1
|
||||
need_tag $TAG
|
||||
|
||||
info "Looking up by tag:"
|
||||
aws_get_instance_ids_by_tag $TAG
|
||||
|
||||
# Just in case we managed to create instances but weren't able to tag them
|
||||
info "Looking up by token:"
|
||||
aws_get_instance_ids_by_client_token $TAG
|
||||
SETTINGS=tags/$TAG/settings.yaml
|
||||
CLUSTERSIZE=$(awk '/^clustersize:/ {print $2}' $SETTINGS)
|
||||
while true; do
|
||||
for I in $(seq $CLUSTERSIZE); do
|
||||
read ip || return 0
|
||||
printf "%s\t" "$ip"
|
||||
done
|
||||
printf "\n"
|
||||
done < tags/$TAG/ips.txt
|
||||
}
|
||||
|
||||
_cmd list "List available groups for a given infrastructure"
|
||||
_cmd list "List all VMs on a given infrastructure (or all infras if no arg given)"
|
||||
_cmd_list() {
|
||||
need_infra $1
|
||||
infra_list
|
||||
case "$1" in
|
||||
"")
|
||||
for INFRA in infra/*; do
|
||||
$0 list $INFRA
|
||||
done
|
||||
;;
|
||||
*/example.*)
|
||||
;;
|
||||
*)
|
||||
need_infra $1
|
||||
sep "Listing instances for $1"
|
||||
infra_list
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_cmd listall "List VMs running on all configured infrastructures"
|
||||
@@ -411,16 +512,6 @@ _cmd_opensg() {
|
||||
infra_opensg
|
||||
}
|
||||
|
||||
_cmd portworx "Prepare the nodes for Portworx deployment"
|
||||
_cmd_portworx() {
|
||||
TAG=$1
|
||||
need_tag
|
||||
|
||||
pssh "
|
||||
sudo truncate --size 10G /portworx.blk &&
|
||||
sudo losetup /dev/loop4 /portworx.blk"
|
||||
}
|
||||
|
||||
_cmd disableaddrchecks "Disable source/destination IP address checks"
|
||||
_cmd_disableaddrchecks() {
|
||||
TAG=$1
|
||||
@@ -457,6 +548,17 @@ _cmd_remap_nodeports() {
|
||||
if i_am_first_node && ! grep -q '$ADD_LINE' $MANIFEST_FILE; then
|
||||
sudo sed -i 's/\($FIND_LINE\)\$/\1\n$ADD_LINE/' $MANIFEST_FILE
|
||||
fi"
|
||||
|
||||
info "If you have manifests hard-coding nodePort values,"
|
||||
info "you might want to patch them with a command like:"
|
||||
info "
|
||||
|
||||
if i_am_first_node; then
|
||||
kubectl -n kube-system patch svc prometheus-server \\
|
||||
-p 'spec: { ports: [ {port: 80, nodePort: 10101} ]}'
|
||||
fi
|
||||
|
||||
"
|
||||
}
|
||||
|
||||
_cmd quotas "Check our infrastructure quotas (max instances)"
|
||||
@@ -465,18 +567,6 @@ _cmd_quotas() {
|
||||
infra_quotas
|
||||
}
|
||||
|
||||
_cmd retag "(FIXME) Apply a new tag to a group of VMs"
|
||||
_cmd_retag() {
|
||||
OLDTAG=$1
|
||||
NEWTAG=$2
|
||||
TAG=$OLDTAG
|
||||
need_tag
|
||||
if [[ -z "$NEWTAG" ]]; then
|
||||
die "You must specify a new tag to apply."
|
||||
fi
|
||||
aws_tag_instances $OLDTAG $NEWTAG
|
||||
}
|
||||
|
||||
_cmd ssh "Open an SSH session to the first node of a tag"
|
||||
_cmd_ssh() {
|
||||
TAG=$1
|
||||
@@ -566,6 +656,8 @@ _cmd_start() {
|
||||
done
|
||||
sep
|
||||
info "Deployment successful."
|
||||
info "To log into the first machine of that batch, you can run:"
|
||||
info "$0 ssh $TAG"
|
||||
info "To terminate these instances, you can run:"
|
||||
info "$0 stop $TAG"
|
||||
}
|
||||
@@ -629,8 +721,8 @@ _cmd_helmprom() {
|
||||
need_tag
|
||||
pssh "
|
||||
if i_am_first_node; then
|
||||
sudo -u docker -H helm repo add stable https://kubernetes-charts.storage.googleapis.com/
|
||||
sudo -u docker -H helm install prometheus stable/prometheus \
|
||||
sudo -u docker -H helm helm repo add prometheus-community https://prometheus-community.github.io/helm-charts/
|
||||
sudo -u docker -H helm install prometheus prometheus-community/prometheus \
|
||||
--namespace kube-system \
|
||||
--set server.service.type=NodePort \
|
||||
--set server.service.nodePort=30090 \
|
||||
@@ -663,11 +755,12 @@ _cmd_webssh() {
|
||||
sudo apt-get update &&
|
||||
sudo apt-get install python-tornado python-paramiko -y"
|
||||
pssh "
|
||||
[ -d webssh ] || git clone https://github.com/jpetazzo/webssh"
|
||||
cd /opt
|
||||
[ -d webssh ] || sudo git clone https://github.com/jpetazzo/webssh"
|
||||
pssh "
|
||||
for KEYFILE in /etc/ssh/*.pub; do
|
||||
read a b c < \$KEYFILE; echo localhost \$a \$b
|
||||
done > webssh/known_hosts"
|
||||
done | sudo tee /opt/webssh/known_hosts"
|
||||
pssh "cat >webssh.service <<EOF
|
||||
[Unit]
|
||||
Description=webssh
|
||||
@@ -676,7 +769,7 @@ Description=webssh
|
||||
WantedBy=multi-user.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/home/ubuntu/webssh
|
||||
WorkingDirectory=/opt/webssh
|
||||
ExecStart=/usr/bin/env python run.py --fbidhttp=false --port=1080 --policy=reject
|
||||
User=nobody
|
||||
Group=nogroup
|
||||
@@ -699,11 +792,6 @@ _cmd_www() {
|
||||
python3 -m http.server
|
||||
}
|
||||
|
||||
greet() {
|
||||
IAMUSER=$(aws iam get-user --query 'User.UserName')
|
||||
info "Hello! You seem to be UNIX user $USER, and IAM user $IAMUSER."
|
||||
}
|
||||
|
||||
pull_tag() {
|
||||
# Pre-pull a bunch of images
|
||||
pssh --timeout 900 'for I in \
|
||||
@@ -793,27 +881,3 @@ make_key_name() {
|
||||
SHORT_FINGERPRINT=$(ssh-add -l | grep RSA | head -n1 | cut -d " " -f 2 | tr -d : | cut -c 1-8)
|
||||
echo "${SHORT_FINGERPRINT}-${USER}"
|
||||
}
|
||||
|
||||
sync_keys() {
|
||||
# make sure ssh-add -l contains "RSA"
|
||||
ssh-add -l | grep -q RSA \
|
||||
|| die "The output of \`ssh-add -l\` doesn't contain 'RSA'. Start the agent, add your keys?"
|
||||
|
||||
AWS_KEY_NAME=$(make_key_name)
|
||||
info "Syncing keys... "
|
||||
if ! aws ec2 describe-key-pairs --key-name "$AWS_KEY_NAME" &>/dev/null; then
|
||||
aws ec2 import-key-pair --key-name $AWS_KEY_NAME \
|
||||
--public-key-material "$(ssh-add -L \
|
||||
| grep -i RSA \
|
||||
| head -n1 \
|
||||
| cut -d " " -f 1-2)" &>/dev/null
|
||||
|
||||
if ! aws ec2 describe-key-pairs --key-name "$AWS_KEY_NAME" &>/dev/null; then
|
||||
die "Somehow, importing the key didn't work. Make sure that 'ssh-add -l | grep RSA | head -n1' returns an RSA key?"
|
||||
else
|
||||
info "Imported new key $AWS_KEY_NAME."
|
||||
fi
|
||||
else
|
||||
info "Using existing key $AWS_KEY_NAME."
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
if ! command -v aws >/dev/null; then
|
||||
warn "AWS CLI (aws) not found."
|
||||
fi
|
||||
|
||||
infra_list() {
|
||||
aws_display_tags
|
||||
aws ec2 describe-instances --output json |
|
||||
jq -r '.Reservations[].Instances[] | [.InstanceId, .ClientToken, .State.Name, .InstanceType ] | @tsv'
|
||||
}
|
||||
|
||||
infra_quotas() {
|
||||
greet
|
||||
aws_greet
|
||||
|
||||
max_instances=$(aws ec2 describe-account-attributes \
|
||||
--attribute-names max-instances \
|
||||
@@ -21,10 +26,10 @@ infra_start() {
|
||||
COUNT=$1
|
||||
|
||||
# Print our AWS username, to ease the pain of credential-juggling
|
||||
greet
|
||||
aws_greet
|
||||
|
||||
# Upload our SSH keys to AWS if needed, to be added to each VM's authorized_keys
|
||||
key_name=$(sync_keys)
|
||||
key_name=$(aws_sync_keys)
|
||||
|
||||
AMI=$(aws_get_ami) # Retrieve the AWS image ID
|
||||
if [ -z "$AMI" ]; then
|
||||
@@ -61,7 +66,7 @@ infra_start() {
|
||||
aws_tag_instances $TAG $TAG
|
||||
|
||||
# Wait until EC2 API tells us that the instances are running
|
||||
wait_until_tag_is_running $TAG $COUNT
|
||||
aws_wait_until_tag_is_running $TAG $COUNT
|
||||
|
||||
aws_get_instance_ips_by_tag $TAG > tags/$TAG/ips.txt
|
||||
}
|
||||
@@ -98,7 +103,7 @@ infra_disableaddrchecks() {
|
||||
done
|
||||
}
|
||||
|
||||
wait_until_tag_is_running() {
|
||||
aws_wait_until_tag_is_running() {
|
||||
max_retry=100
|
||||
i=0
|
||||
done_count=0
|
||||
@@ -214,3 +219,32 @@ aws_get_ami() {
|
||||
##VERSION##
|
||||
find_ubuntu_ami -r $AWS_DEFAULT_REGION -a amd64 -v 18.04 -t hvm:ebs -N -q
|
||||
}
|
||||
|
||||
aws_greet() {
|
||||
IAMUSER=$(aws iam get-user --query 'User.UserName')
|
||||
info "Hello! You seem to be UNIX user $USER, and IAM user $IAMUSER."
|
||||
}
|
||||
|
||||
aws_sync_keys() {
|
||||
# make sure ssh-add -l contains "RSA"
|
||||
ssh-add -l | grep -q RSA \
|
||||
|| die "The output of \`ssh-add -l\` doesn't contain 'RSA'. Start the agent, add your keys?"
|
||||
|
||||
AWS_KEY_NAME=$(make_key_name)
|
||||
info "Syncing keys... "
|
||||
if ! aws ec2 describe-key-pairs --key-name "$AWS_KEY_NAME" &>/dev/null; then
|
||||
aws ec2 import-key-pair --key-name $AWS_KEY_NAME \
|
||||
--public-key-material "$(ssh-add -L \
|
||||
| grep -i RSA \
|
||||
| head -n1 \
|
||||
| cut -d " " -f 1-2)" &>/dev/null
|
||||
|
||||
if ! aws ec2 describe-key-pairs --key-name "$AWS_KEY_NAME" &>/dev/null; then
|
||||
die "Somehow, importing the key didn't work. Make sure that 'ssh-add -l | grep RSA | head -n1' returns an RSA key?"
|
||||
else
|
||||
info "Imported new key $AWS_KEY_NAME."
|
||||
fi
|
||||
else
|
||||
info "Using existing key $AWS_KEY_NAME."
|
||||
fi
|
||||
}
|
||||
|
||||
57
prepare-vms/lib/infra/hetzner.sh
Normal file
57
prepare-vms/lib/infra/hetzner.sh
Normal file
@@ -0,0 +1,57 @@
|
||||
if ! command -v hcloud >/dev/null; then
|
||||
warn "Hetzner CLI (hcloud) not found."
|
||||
fi
|
||||
if ! [ -f ~/.config/hcloud/cli.toml ]; then
|
||||
warn "~/.config/hcloud/cli.toml not found."
|
||||
fi
|
||||
|
||||
infra_list() {
|
||||
[ "$(hcloud server list -o json)" = "null" ] && return
|
||||
|
||||
hcloud server list -o json |
|
||||
jq -r '.[] | [.id, .name , .status, .server_type.name] | @tsv'
|
||||
}
|
||||
|
||||
infra_start() {
|
||||
COUNT=$1
|
||||
|
||||
HETZNER_INSTANCE_TYPE=${HETZNER_INSTANCE_TYPE-cx21}
|
||||
HETZNER_DATACENTER=${HETZNER_DATACENTER-nbg1-dc3}
|
||||
HETZNER_IMAGE=${HETZNER_IMAGE-168855}
|
||||
|
||||
for I in $(seq 1 $COUNT); do
|
||||
NAME=$(printf "%s-%03d" $TAG $I)
|
||||
sep "Starting instance $I/$COUNT"
|
||||
info " Datacenter: $HETZNER_DATACENTER"
|
||||
info " Name: $NAME"
|
||||
info " Instance type: $HETZNER_INSTANCE_TYPE"
|
||||
hcloud server create \
|
||||
--type=${HETZNER_INSTANCE_TYPE} \
|
||||
--datacenter=${HETZNER_DATACENTER} \
|
||||
--image=${HETZNER_IMAGE} \
|
||||
--name=$NAME \
|
||||
--label=tag=$TAG \
|
||||
--ssh-key ~/.ssh/id_rsa.pub
|
||||
done
|
||||
|
||||
hetzner_get_ips_by_tag $TAG > tags/$TAG/ips.txt
|
||||
}
|
||||
|
||||
infra_stop() {
|
||||
for ID in $(hetzner_get_ids_by_tag $TAG); do
|
||||
info "Scheduling deletion of instance $ID..."
|
||||
hcloud server delete $ID &
|
||||
done
|
||||
info "Waiting for deletion to complete..."
|
||||
wait
|
||||
}
|
||||
|
||||
hetzner_get_ids_by_tag() {
|
||||
TAG=$1
|
||||
hcloud server list --selector=tag=$TAG -o json | jq -r .[].name
|
||||
}
|
||||
|
||||
hetzner_get_ips_by_tag() {
|
||||
TAG=$1
|
||||
hcloud server list --selector=tag=$TAG -o json | jq -r .[].public_net.ipv4.ip
|
||||
}
|
||||
53
prepare-vms/lib/infra/openstack-cli.sh
Normal file
53
prepare-vms/lib/infra/openstack-cli.sh
Normal file
@@ -0,0 +1,53 @@
|
||||
infra_list() {
|
||||
openstack server list -f json |
|
||||
jq -r '.[] | [.ID, .Name , .Status, .Flavor] | @tsv'
|
||||
}
|
||||
|
||||
infra_start() {
|
||||
COUNT=$1
|
||||
|
||||
sep "Starting $COUNT instances"
|
||||
info " Region: $OS_REGION_NAME"
|
||||
info " User: $OS_USERNAME"
|
||||
info " Flavor: $OS_FLAVOR"
|
||||
info " Image: $OS_IMAGE"
|
||||
openstack server create \
|
||||
--flavor $OS_FLAVOR \
|
||||
--image $OS_IMAGE \
|
||||
--key-name $OS_KEY \
|
||||
--min $COUNT --max $COUNT \
|
||||
--property workshopctl=$TAG \
|
||||
$TAG
|
||||
|
||||
sep "Waiting for IP addresses to be available"
|
||||
GOT=0
|
||||
while [ "$GOT" != "$COUNT" ]; do
|
||||
echo "Got $GOT/$COUNT IP addresses."
|
||||
oscli_get_ips_by_tag $TAG > tags/$TAG/ips.txt
|
||||
GOT="$(wc -l < tags/$TAG/ips.txt)"
|
||||
done
|
||||
|
||||
}
|
||||
|
||||
infra_stop() {
|
||||
info "Counting instances..."
|
||||
oscli_get_instances_json $TAG |
|
||||
jq -r .[].Name |
|
||||
wc -l
|
||||
info "Deleting instances..."
|
||||
oscli_get_instances_json $TAG |
|
||||
jq -r .[].Name |
|
||||
xargs -P10 -n1 openstack server delete
|
||||
info "Done."
|
||||
}
|
||||
|
||||
oscli_get_instances_json() {
|
||||
TAG=$1
|
||||
openstack server list -f json --name "${TAG}-[0-9]*"
|
||||
}
|
||||
|
||||
oscli_get_ips_by_tag() {
|
||||
TAG=$1
|
||||
oscli_get_instances_json $TAG |
|
||||
jq -r .[].Networks | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' || true
|
||||
}
|
||||
51
prepare-vms/lib/infra/scaleway.sh
Normal file
51
prepare-vms/lib/infra/scaleway.sh
Normal file
@@ -0,0 +1,51 @@
|
||||
if ! command -v scw >/dev/null; then
|
||||
warn "Scaleway CLI (scw) not found."
|
||||
fi
|
||||
if ! [ -f ~/.config/scw/config.yaml ]; then
|
||||
warn "~/.config/scw/config.yaml not found."
|
||||
fi
|
||||
|
||||
infra_list() {
|
||||
scw instance server list -o json |
|
||||
jq -r '.[] | [.id, .name, .state, .commercial_type] | @tsv'
|
||||
}
|
||||
|
||||
infra_start() {
|
||||
COUNT=$1
|
||||
|
||||
SCW_INSTANCE_TYPE=${SCW_INSTANCE_TYPE-DEV1-M}
|
||||
SCW_ZONE=${SCW_ZONE-fr-par-1}
|
||||
|
||||
for I in $(seq 1 $COUNT); do
|
||||
NAME=$(printf "%s-%03d" $TAG $I)
|
||||
sep "Starting instance $I/$COUNT"
|
||||
info " Zone: $SCW_ZONE"
|
||||
info " Name: $NAME"
|
||||
info " Instance type: $SCW_INSTANCE_TYPE"
|
||||
scw instance server create \
|
||||
type=${SCW_INSTANCE_TYPE} zone=${SCW_ZONE} \
|
||||
image=ubuntu_bionic name=${NAME}
|
||||
done
|
||||
sep
|
||||
|
||||
scw_get_ips_by_tag $TAG > tags/$TAG/ips.txt
|
||||
}
|
||||
|
||||
infra_stop() {
|
||||
info "Counting instances..."
|
||||
scw_get_ids_by_tag $TAG | wc -l
|
||||
info "Deleting instances..."
|
||||
scw_get_ids_by_tag $TAG |
|
||||
xargs -n1 -P10 -I@@ \
|
||||
scw instance server delete force-shutdown=true server-id=@@
|
||||
}
|
||||
|
||||
scw_get_ids_by_tag() {
|
||||
TAG=$1
|
||||
scw instance server list name=$TAG -o json | jq -r .[].id
|
||||
}
|
||||
|
||||
scw_get_ips_by_tag() {
|
||||
TAG=$1
|
||||
scw instance server list name=$TAG -o json | jq -r .[].public_ip.address
|
||||
}
|
||||
23
prepare-vms/lib/infra/unimplemented.sh
Normal file
23
prepare-vms/lib/infra/unimplemented.sh
Normal file
@@ -0,0 +1,23 @@
|
||||
infra_disableaddrchecks() {
|
||||
die "unimplemented"
|
||||
}
|
||||
|
||||
infra_list() {
|
||||
die "unimplemented"
|
||||
}
|
||||
|
||||
infra_opensg() {
|
||||
die "unimplemented"
|
||||
}
|
||||
|
||||
infra_quotas() {
|
||||
die "unimplemented"
|
||||
}
|
||||
|
||||
infra_start() {
|
||||
die "unimplemented"
|
||||
}
|
||||
|
||||
infra_stop() {
|
||||
die "unimplemented"
|
||||
}
|
||||
@@ -37,7 +37,7 @@ def system(cmd):
|
||||
td = str(t2-t1)[:5]
|
||||
f.write(bold("[{}] in {}s\n".format(retcode, td)))
|
||||
STEP += 1
|
||||
with open("/home/ubuntu/.bash_history", "a") as f:
|
||||
with open(os.environ["HOME"] + "/.bash_history", "a") as f:
|
||||
f.write("{}\n".format(cmd))
|
||||
if retcode != 0:
|
||||
msg = "The following command failed with exit code {}:\n".format(retcode)
|
||||
@@ -114,7 +114,7 @@ system("sudo sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' /e
|
||||
|
||||
system("sudo service ssh restart")
|
||||
system("sudo apt-get -q update")
|
||||
system("sudo apt-get -qy install git jq")
|
||||
system("sudo apt-get -qy install git jid jq")
|
||||
system("sudo apt-get -qy install emacs-nox joe")
|
||||
|
||||
#######################
|
||||
|
||||
@@ -18,7 +18,13 @@ pssh() {
|
||||
echo "[parallel-ssh] $@"
|
||||
export PSSH=$(which pssh || which parallel-ssh)
|
||||
|
||||
$PSSH -h $HOSTFILE -l ubuntu \
|
||||
if [ "$INFRACLASS" = hetzner ]; then
|
||||
LOGIN=root
|
||||
else
|
||||
LOGIN=ubuntu
|
||||
fi
|
||||
|
||||
$PSSH -h $HOSTFILE -l $LOGIN \
|
||||
--par 100 \
|
||||
-O LogLevel=ERROR \
|
||||
-O UserKnownHostsFile=/dev/null \
|
||||
|
||||
@@ -1,21 +1,45 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
There are two ways to use this script:
|
||||
|
||||
1. Pass a tag name as a single argument.
|
||||
It will then take the clusters corresponding to that tag, and assign one
|
||||
domain name per cluster. Currently it gets the domains from a hard-coded
|
||||
path. There should be more domains than clusters.
|
||||
Example: ./map-dns.py 2020-08-15-jp
|
||||
|
||||
2. Pass a domain as the 1st argument, and IP addresses then.
|
||||
It will configure the domain with the listed IP addresses.
|
||||
Example: ./map-dns.py open-duck.site 1.2.3.4 2.3.4.5 3.4.5.6
|
||||
|
||||
In both cases, the domains should be configured to use GANDI LiveDNS.
|
||||
"""
|
||||
import os
|
||||
import requests
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
# configurable stuff
|
||||
domains_file = "../../plentydomains/domains.txt"
|
||||
config_file = os.path.join(
|
||||
os.environ["HOME"], ".config/gandi/config.yaml")
|
||||
tag = "test"
|
||||
tag = None
|
||||
apiurl = "https://dns.api.gandi.net/api/v5/domains"
|
||||
|
||||
if len(sys.argv) == 2:
|
||||
tag = sys.argv[1]
|
||||
domains = open(domains_file).read().split()
|
||||
domains = [ d for d in domains if not d.startswith('#') ]
|
||||
ips = open(f"tags/{tag}/ips.txt").read().split()
|
||||
settings_file = f"tags/{tag}/settings.yaml"
|
||||
clustersize = yaml.safe_load(open(settings_file))["clustersize"]
|
||||
else:
|
||||
domains = [sys.argv[1]]
|
||||
ips = sys.argv[2:]
|
||||
clustersize = len(ips)
|
||||
|
||||
# inferred stuff
|
||||
domains = open(domains_file).read().split()
|
||||
apikey = yaml.safe_load(open(config_file))["apirest"]["key"]
|
||||
ips = open(f"tags/{tag}/ips.txt").read().split()
|
||||
settings_file = f"tags/{tag}/settings.yaml"
|
||||
clustersize = yaml.safe_load(open(settings_file))["clustersize"]
|
||||
|
||||
# now do the fucking work
|
||||
while domains and ips:
|
||||
|
||||
@@ -6,8 +6,8 @@ clustersize: 1
|
||||
# The hostname of each node will be clusterprefix + a number
|
||||
clusterprefix: node
|
||||
|
||||
# Jinja2 template to use to generate ready-to-cut
|
||||
cards_template: clusters.csv
|
||||
# Jinja2 template to use to generate ready-to-cut cards
|
||||
cards_template: cards.html
|
||||
|
||||
# Use "Letter" in the US, and "A4" everywhere else
|
||||
paper_size: Letter
|
||||
|
||||
@@ -25,5 +25,6 @@ steps:
|
||||
- webssh
|
||||
- tailhist
|
||||
- kube
|
||||
- kubetools
|
||||
- cards
|
||||
- kubetest
|
||||
|
||||
@@ -35,6 +35,8 @@ TAG=$PREFIX-$SETTINGS
|
||||
retry 5 ./workshopctl deploy $TAG
|
||||
retry 5 ./workshopctl disabledocker $TAG
|
||||
retry 5 ./workshopctl kubebins $TAG
|
||||
retry 5 ./workshopctl webssh $TAG
|
||||
retry 5 ./workshopctl tailhist $TAG
|
||||
./workshopctl cards $TAG
|
||||
|
||||
SETTINGS=admin-kubenet
|
||||
@@ -48,6 +50,8 @@ TAG=$PREFIX-$SETTINGS
|
||||
retry 5 ./workshopctl disableaddrchecks $TAG
|
||||
retry 5 ./workshopctl deploy $TAG
|
||||
retry 5 ./workshopctl kubebins $TAG
|
||||
retry 5 ./workshopctl webssh $TAG
|
||||
retry 5 ./workshopctl tailhist $TAG
|
||||
./workshopctl cards $TAG
|
||||
|
||||
SETTINGS=admin-kuberouter
|
||||
@@ -61,6 +65,8 @@ TAG=$PREFIX-$SETTINGS
|
||||
retry 5 ./workshopctl disableaddrchecks $TAG
|
||||
retry 5 ./workshopctl deploy $TAG
|
||||
retry 5 ./workshopctl kubebins $TAG
|
||||
retry 5 ./workshopctl webssh $TAG
|
||||
retry 5 ./workshopctl tailhist $TAG
|
||||
./workshopctl cards $TAG
|
||||
|
||||
#INFRA=infra/aws-us-west-1
|
||||
@@ -76,5 +82,7 @@ TAG=$PREFIX-$SETTINGS
|
||||
--count $((3*$STUDENTS))
|
||||
|
||||
retry 5 ./workshopctl deploy $TAG
|
||||
retry 5 ./workshopctl kube $TAG 1.15.9
|
||||
retry 5 ./workshopctl kube $TAG 1.17.13
|
||||
retry 5 ./workshopctl webssh $TAG
|
||||
retry 5 ./workshopctl tailhist $TAG
|
||||
./workshopctl cards $TAG
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
resource "openstack_compute_instance_v2" "machine" {
|
||||
count = "${var.count}"
|
||||
name = "${format("%s-%04d", "${var.prefix}", count.index+1)}"
|
||||
image_name = "Ubuntu 16.04.5 (Xenial Xerus)"
|
||||
image_name = "Ubuntu 18.04.4 20200324"
|
||||
flavor_name = "${var.flavor}"
|
||||
security_groups = ["${openstack_networking_secgroup_v2.full_access.name}"]
|
||||
key_pair = "${openstack_compute_keypair_v2.ssh_deploy_key.name}"
|
||||
|
||||
@@ -15,7 +15,6 @@ for lib in lib/*.sh; do
|
||||
done
|
||||
|
||||
DEPENDENCIES="
|
||||
aws
|
||||
ssh
|
||||
curl
|
||||
jq
|
||||
|
||||
@@ -2,22 +2,23 @@
|
||||
#/ /kube-halfday.yml.html 200!
|
||||
#/ /kube-fullday.yml.html 200!
|
||||
#/ /kube-twodays.yml.html 200!
|
||||
/ /kube-adv.yml.html 200!
|
||||
|
||||
# And this allows to do "git clone https://container.training".
|
||||
/info/refs service=git-upload-pack https://github.com/jpetazzo/container.training/info/refs?service=git-upload-pack
|
||||
|
||||
#/dockermastery https://www.udemy.com/course/docker-mastery/?referralCode=1410924A733D33635CCB
|
||||
#/kubernetesmastery https://www.udemy.com/course/kubernetesmastery/?referralCode=7E09090AF9B79E6C283F
|
||||
/dockermastery https://www.udemy.com/course/docker-mastery/?couponCode=SWEETFEBSALEC1
|
||||
/kubernetesmastery https://www.udemy.com/course/kubernetesmastery/?couponCode=SWEETFEBSALEC4
|
||||
/dockermastery https://www.udemy.com/course/docker-mastery/?referralCode=1410924A733D33635CCB
|
||||
/kubernetesmastery https://www.udemy.com/course/kubernetesmastery/?referralCode=7E09090AF9B79E6C283F
|
||||
#/dockermastery https://www.udemy.com/course/docker-mastery/?couponCode=DOCKERALLDAY
|
||||
#/kubernetesmastery https://www.udemy.com/course/kubernetesmastery/?couponCode=DOCKERALLDAY
|
||||
|
||||
# Shortlink for the QRCode
|
||||
/q /qrcode.html 200
|
||||
|
||||
# Shortlinks for next training in English and French
|
||||
/next https://www.eventbrite.com/e/livestream-intensive-kubernetes-bootcamp-tickets-103262336428
|
||||
#/next https://www.eventbrite.com/e/livestream-intensive-kubernetes-bootcamp-tickets-103262336428
|
||||
/next https://skillsmatter.com/courses/700-advanced-kubernetes-concepts-workshop-jerome-petazzoni
|
||||
/hi5 https://enix.io/fr/services/formation/online/
|
||||
|
||||
/ /intro.yml.html 200!
|
||||
/vms https://docs.google.com/spreadsheets/d/1u91MzRvUiZiI55x_sto1kk9LP4QoxqOBjvjhZCeyjU4/edit
|
||||
/chat https://gitter.im/jpetazzo/training-20200707-online
|
||||
# Survey form
|
||||
/please https://docs.google.com/forms/d/e/1FAIpQLSfIYSgrV7tpfBNm1hOaprjnBHgWKn5n-k5vtNXYJkOX1sRxng/viewform
|
||||
|
||||
@@ -233,7 +233,7 @@ def setup_tmux_and_ssh():
|
||||
ipaddr = "$IPADDR"
|
||||
uid = os.getuid()
|
||||
|
||||
raise Exception("""
|
||||
raise Exception(r"""
|
||||
1. If you're running this directly from a node:
|
||||
|
||||
tmux
|
||||
@@ -247,6 +247,16 @@ rm -f /tmp/tmux-{uid}/default && ssh -t -L /tmp/tmux-{uid}/default:/tmp/tmux-100
|
||||
3. If you cannot control a remote tmux:
|
||||
|
||||
tmux new-session ssh docker@{ipaddr}
|
||||
|
||||
4. If you are running this locally with a remote cluster, make sure your prompt has the expected format:
|
||||
|
||||
tmux
|
||||
IPADDR=$(
|
||||
kubectl get nodes -o json |
|
||||
jq -r '.items[0].status.addresses[] | select(.type=="ExternalIP") | .address'
|
||||
)
|
||||
export PS1="\n[{ipaddr}] \u@\h:\w\n\$ "
|
||||
|
||||
""".format(uid=uid, ipaddr=ipaddr))
|
||||
else:
|
||||
logging.info("Found tmux session. Trying to acquire shell prompt.")
|
||||
|
||||
@@ -307,6 +307,8 @@ Let's remove the `redis` container:
|
||||
$ docker rm -f redis
|
||||
```
|
||||
|
||||
* `-f`: Force the removal of a running container (uses SIGKILL)
|
||||
|
||||
And create one that doesn't block the `redis` name:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Logging (extra material)
|
||||
# Logging
|
||||
|
||||
In this chapter, we will explain the different ways to send logs from containers.
|
||||
|
||||
|
||||
@@ -95,6 +95,24 @@ $ ssh <login>@<ip-address>
|
||||
|
||||
---
|
||||
|
||||
class: in-person
|
||||
|
||||
## `tailhist`
|
||||
|
||||
The shell history of the instructor is available online in real time.
|
||||
|
||||
Note the IP address of the instructor's virtual machine (A.B.C.D).
|
||||
|
||||
Open http://A.B.C.D:1088 in your browser and you should see the history.
|
||||
|
||||
The history is updated in real time (using a WebSocket connection).
|
||||
|
||||
It should be green when the WebSocket is connected.
|
||||
|
||||
If it turns red, reloading the page should fix it.
|
||||
|
||||
---
|
||||
|
||||
## Checking your Virtual Machine
|
||||
|
||||
Once logged in, make sure that you can run a basic Docker command:
|
||||
|
||||
@@ -119,7 +119,7 @@ Nano and LinuxKit VMs in Hyper-V!)
|
||||
|
||||
- golang, mongo, python, redis, hello-world ... and more being added
|
||||
|
||||
- you should still use `--plaform` with multi-os images to be certain
|
||||
- you should still use `--platform` with multi-os images to be certain
|
||||
|
||||
- Windows Containers now support `localhost` accessible containers (July 2018)
|
||||
|
||||
|
||||
BIN
slides/images/hpa-v2-pa-latency.png
Normal file
BIN
slides/images/hpa-v2-pa-latency.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
BIN
slides/images/hpa-v2-pa-pods.png
Normal file
BIN
slides/images/hpa-v2-pa-pods.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
@@ -1,3 +1,74 @@
|
||||
- date: [2020-10-05, 2020-10-06]
|
||||
country: www
|
||||
city: streaming
|
||||
event: ENIX SAS
|
||||
speaker: jpetazzo
|
||||
title: Docker intensif (en français)
|
||||
lang: fr
|
||||
attend: https://enix.io/fr/services/formation/online/
|
||||
|
||||
- date: [2020-10-07, 2020-10-09]
|
||||
country: www
|
||||
city: streaming
|
||||
event: ENIX SAS
|
||||
speaker: jpetazzo
|
||||
title: Fondamentaux Kubernetes (en français)
|
||||
lang: fr
|
||||
attend: https://enix.io/fr/services/formation/online/
|
||||
|
||||
- date: 2020-10-12
|
||||
country: www
|
||||
city: streaming
|
||||
event: ENIX SAS
|
||||
speaker: jpetazzo
|
||||
title: Packaging pour Kubernetes (en français)
|
||||
lang: fr
|
||||
attend: https://enix.io/fr/services/formation/online/
|
||||
|
||||
- date: [2020-10-13, 2020-10-14]
|
||||
country: www
|
||||
city: streaming
|
||||
event: ENIX SAS
|
||||
speaker: jpetazzo
|
||||
title: Kubernetes avancé (en français)
|
||||
lang: fr
|
||||
attend: https://enix.io/fr/services/formation/online/
|
||||
|
||||
- date: [2020-10-19, 2020-10-20]
|
||||
country: www
|
||||
city: streaming
|
||||
event: ENIX SAS
|
||||
speaker: jpetazzo
|
||||
title: Opérer Kubernetes (en français)
|
||||
lang: fr
|
||||
attend: https://enix.io/fr/services/formation/online/
|
||||
|
||||
- date: [2020-09-28, 2020-10-01]
|
||||
country: www
|
||||
city: streaming
|
||||
event: Skills Matter
|
||||
speaker: jpetazzo
|
||||
title: Advanced Kubernetes Concepts
|
||||
attend: https://skillsmatter.com/courses/700-advanced-kubernetes-concepts-workshop-jerome-petazzoni
|
||||
|
||||
- date: [2020-08-29, 2020-08-30]
|
||||
country: www
|
||||
city: streaming
|
||||
event: fwdays
|
||||
speaker: jpetazzo
|
||||
title: Intensive Docker Online Workshop
|
||||
attend: https://fwdays.com/en/event/intensive-docker-workshop
|
||||
slides: https://2020-08-fwdays.container.training/
|
||||
|
||||
- date: [2020-09-12, 2020-09-13]
|
||||
country: www
|
||||
city: streaming
|
||||
event: fwdays
|
||||
speaker: jpetazzo
|
||||
title: Kubernetes Intensive Online Workshop
|
||||
attend: https://fwdays.com/en/event/kubernetes-intensive-workshop
|
||||
slides: https://2020-09-fwdays.container.training/
|
||||
|
||||
- date: [2020-07-07, 2020-07-09]
|
||||
country: www
|
||||
city: streaming
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
title: |
|
||||
Introduction
|
||||
to Containers
|
||||
|
||||
chat: "[Slack](https://dockercommunity.slack.com/messages/C7GKACWDV)"
|
||||
#chat: "[Gitter](https://gitter.im/jpetazzo/workshop-yyyymmdd-city)"
|
||||
|
||||
gitrepo: github.com/jpetazzo/container.training
|
||||
|
||||
slides: http://container.training/
|
||||
|
||||
#slidenumberprefix: "#SomeHashTag — "
|
||||
|
||||
exclude:
|
||||
- self-paced
|
||||
|
||||
content:
|
||||
- shared/title.md
|
||||
- logistics.md
|
||||
- containers/intro.md
|
||||
- shared/about-slides.md
|
||||
- shared/chat-room-im.md
|
||||
#- shared/chat-room-zoom-meeting.md
|
||||
#- shared/chat-room-zoom-webinar.md
|
||||
- shared/toc.md
|
||||
-
|
||||
#- containers/Docker_Overview.md
|
||||
#- containers/Docker_History.md
|
||||
- containers/Training_Environment.md
|
||||
#- containers/Installing_Docker.md
|
||||
- containers/First_Containers.md
|
||||
- containers/Background_Containers.md
|
||||
#- containers/Start_And_Attach.md
|
||||
- containers/Naming_And_Inspecting.md
|
||||
#- containers/Labels.md
|
||||
- containers/Getting_Inside.md
|
||||
- containers/Initial_Images.md
|
||||
-
|
||||
- containers/Building_Images_Interactively.md
|
||||
- containers/Building_Images_With_Dockerfiles.md
|
||||
- containers/Cmd_And_Entrypoint.md
|
||||
- containers/Copying_Files_During_Build.md
|
||||
- containers/Exercise_Dockerfile_Basic.md
|
||||
-
|
||||
- containers/Container_Networking_Basics.md
|
||||
#- containers/Network_Drivers.md
|
||||
#- containers/Container_Network_Model.md
|
||||
- containers/Local_Development_Workflow.md
|
||||
- containers/Compose_For_Dev_Stacks.md
|
||||
- containers/Exercise_Composefile.md
|
||||
-
|
||||
- containers/Multi_Stage_Builds.md
|
||||
#- containers/Publishing_To_Docker_Hub.md
|
||||
- containers/Dockerfile_Tips.md
|
||||
- containers/Exercise_Dockerfile_Advanced.md
|
||||
#- containers/Docker_Machine.md
|
||||
#- containers/Advanced_Dockerfiles.md
|
||||
#- containers/Init_Systems.md
|
||||
#- containers/Application_Configuration.md
|
||||
#- containers/Logging.md
|
||||
#- containers/Namespaces_Cgroups.md
|
||||
#- containers/Copy_On_Write.md
|
||||
#- containers/Containers_From_Scratch.md
|
||||
#- containers/Container_Engines.md
|
||||
#- containers/Pods_Anatomy.md
|
||||
#- containers/Ecosystem.md
|
||||
#- containers/Orchestration_Overview.md
|
||||
-
|
||||
- shared/thankyou.md
|
||||
- containers/links.md
|
||||
@@ -1,70 +0,0 @@
|
||||
title: |
|
||||
Introduction
|
||||
to Containers
|
||||
|
||||
chat: "[Slack](https://dockercommunity.slack.com/messages/C7GKACWDV)"
|
||||
#chat: "[Gitter](https://gitter.im/jpetazzo/workshop-yyyymmdd-city)"
|
||||
|
||||
gitrepo: github.com/jpetazzo/container.training
|
||||
|
||||
slides: http://container.training/
|
||||
|
||||
#slidenumberprefix: "#SomeHashTag — "
|
||||
|
||||
exclude:
|
||||
- in-person
|
||||
|
||||
content:
|
||||
- shared/title.md
|
||||
# - shared/logistics.md
|
||||
- containers/intro.md
|
||||
- shared/about-slides.md
|
||||
#- shared/chat-room-im.md
|
||||
#- shared/chat-room-zoom-meeting.md
|
||||
#- shared/chat-room-zoom-webinar.md
|
||||
- shared/toc.md
|
||||
- - containers/Docker_Overview.md
|
||||
- containers/Docker_History.md
|
||||
- containers/Training_Environment.md
|
||||
- containers/Installing_Docker.md
|
||||
- containers/First_Containers.md
|
||||
- containers/Background_Containers.md
|
||||
- containers/Start_And_Attach.md
|
||||
- - containers/Initial_Images.md
|
||||
- containers/Building_Images_Interactively.md
|
||||
- containers/Building_Images_With_Dockerfiles.md
|
||||
- containers/Cmd_And_Entrypoint.md
|
||||
- containers/Copying_Files_During_Build.md
|
||||
- containers/Exercise_Dockerfile_Basic.md
|
||||
- - containers/Multi_Stage_Builds.md
|
||||
- containers/Publishing_To_Docker_Hub.md
|
||||
- containers/Dockerfile_Tips.md
|
||||
- containers/Exercise_Dockerfile_Advanced.md
|
||||
- - containers/Naming_And_Inspecting.md
|
||||
- containers/Labels.md
|
||||
- containers/Getting_Inside.md
|
||||
- - containers/Container_Networking_Basics.md
|
||||
- containers/Network_Drivers.md
|
||||
- containers/Container_Network_Model.md
|
||||
#- containers/Connecting_Containers_With_Links.md
|
||||
- containers/Ambassadors.md
|
||||
- - containers/Local_Development_Workflow.md
|
||||
- containers/Windows_Containers.md
|
||||
- containers/Working_With_Volumes.md
|
||||
- containers/Compose_For_Dev_Stacks.md
|
||||
- containers/Exercise_Composefile.md
|
||||
- containers/Docker_Machine.md
|
||||
- - containers/Advanced_Dockerfiles.md
|
||||
- containers/Init_Systems.md
|
||||
- containers/Application_Configuration.md
|
||||
- containers/Logging.md
|
||||
- containers/Resource_Limits.md
|
||||
- - containers/Namespaces_Cgroups.md
|
||||
- containers/Copy_On_Write.md
|
||||
#- containers/Containers_From_Scratch.md
|
||||
- - containers/Container_Engines.md
|
||||
- containers/Pods_Anatomy.md
|
||||
- containers/Ecosystem.md
|
||||
- containers/Orchestration_Overview.md
|
||||
- shared/thankyou.md
|
||||
- containers/links.md
|
||||
@@ -1,71 +0,0 @@
|
||||
title: |
|
||||
Intensive
|
||||
Docker
|
||||
Bootcamp
|
||||
|
||||
#chat: "[Slack](https://dockercommunity.slack.com/messages/C7GKACWDV)"
|
||||
chat: "[Gitter](https://gitter.im/jpetazzo/training-20200707-online)"
|
||||
|
||||
gitrepo: github.com/jpetazzo/container.training
|
||||
|
||||
slides: https://2020-07-ardan.container.training/
|
||||
|
||||
#slidenumberprefix: "#SomeHashTag — "
|
||||
|
||||
exclude:
|
||||
- self-paced
|
||||
|
||||
content:
|
||||
- shared/title.md
|
||||
- logistics.md
|
||||
- containers/intro.md
|
||||
- shared/about-slides.md
|
||||
- shared/chat-room-im.md
|
||||
#- shared/chat-room-zoom-meeting.md
|
||||
#- shared/chat-room-zoom-webinar.md
|
||||
- shared/toc.md
|
||||
- # DAY 1
|
||||
#- containers/Docker_Overview.md
|
||||
#- containers/Docker_History.md
|
||||
- containers/Training_Environment.md
|
||||
- containers/First_Containers.md
|
||||
- containers/Background_Containers.md
|
||||
- containers/Initial_Images.md
|
||||
-
|
||||
- containers/Building_Images_Interactively.md
|
||||
- containers/Building_Images_With_Dockerfiles.md
|
||||
- containers/Cmd_And_Entrypoint.md
|
||||
- containers/Copying_Files_During_Build.md
|
||||
- containers/Exercise_Dockerfile_Basic.md
|
||||
- # DAY 2
|
||||
- containers/Publishing_To_Docker_Hub.md
|
||||
- containers/Naming_And_Inspecting.md
|
||||
- containers/Labels.md
|
||||
- containers/Start_And_Attach.md
|
||||
- containers/Getting_Inside.md
|
||||
- containers/Resource_Limits.md
|
||||
-
|
||||
- containers/Dockerfile_Tips.md
|
||||
- containers/Multi_Stage_Builds.md
|
||||
- containers/Advanced_Dockerfiles.md
|
||||
- containers/Exercise_Dockerfile_Advanced.md
|
||||
- # DAY 3
|
||||
- containers/Container_Networking_Basics.md
|
||||
- containers/Network_Drivers.md
|
||||
- containers/Container_Network_Model.md
|
||||
-
|
||||
- containers/Local_Development_Workflow.md
|
||||
- containers/Compose_For_Dev_Stacks.md
|
||||
- containers/Exercise_Composefile.md
|
||||
- containers/Logging.md
|
||||
#- containers/Working_With_Volumes.md
|
||||
#- containers/Application_Configuration.md
|
||||
- shared/thankyou.md
|
||||
#-
|
||||
#- containers/Docker_Machine.md
|
||||
#- containers/Ambassadors.md
|
||||
#- containers/Namespaces_Cgroups.md
|
||||
#- containers/Copy_On_Write.md
|
||||
#- containers/Containers_From_Scratch.md
|
||||
#- containers/Pods_Anatomy.md
|
||||
#- containers/Ecosystem.md
|
||||
549
slides/k8s/admission.md
Normal file
549
slides/k8s/admission.md
Normal file
@@ -0,0 +1,549 @@
|
||||
# Dynamic Admission Control
|
||||
|
||||
- This is one of the many ways to extend the Kubernetes API
|
||||
|
||||
- High level summary: dynamic admission control relies on webhooks that are ...
|
||||
|
||||
- dynamic (can be added/removed on the fly)
|
||||
|
||||
- running inside our outside the cluster
|
||||
|
||||
- *validating* (yay/nay) or *mutating* (can change objects that are created/updated)
|
||||
|
||||
- selective (can be configured to apply only to some kinds, some selectors...)
|
||||
|
||||
- mandatory or optional (should it block operations when webhook is down?)
|
||||
|
||||
- Used for themselves (e.g. policy enforcement) or as part of operators
|
||||
|
||||
---
|
||||
|
||||
## Use cases
|
||||
|
||||
Some examples ...
|
||||
|
||||
- Stand-alone admission controllers
|
||||
|
||||
*validating:* policy enforcement (e.g. quotas, naming conventions ...)
|
||||
|
||||
*mutating:* inject or provide default values (e.g. pod presets)
|
||||
|
||||
- Admission controllers part of a greater system
|
||||
|
||||
*validating:* advanced typing for operators
|
||||
|
||||
*mutating:* inject sidecars for service meshes
|
||||
|
||||
---
|
||||
|
||||
## You said *dynamic?*
|
||||
|
||||
- Some admission controllers are built in the API server
|
||||
|
||||
- They are enabled/disabled through Kubernetes API server configuration
|
||||
|
||||
(e.g. `--enable-admission-plugins`/`--disable-admission-plugins` flags)
|
||||
|
||||
- Here, we're talking about *dynamic* admission controllers
|
||||
|
||||
- They can be added/remove while the API server is running
|
||||
|
||||
(without touching the configuration files or even having access to them)
|
||||
|
||||
- This is done through two kinds of cluster-scope resources:
|
||||
|
||||
ValidatingWebhookConfiguration and MutatingWebhookConfiguration
|
||||
|
||||
---
|
||||
|
||||
## You said *webhooks?*
|
||||
|
||||
- A ValidatingWebhookConfiguration or MutatingWebhookConfiguration contains:
|
||||
|
||||
- a resource filter
|
||||
<br/>
|
||||
(e.g. "all pods", "deployments in namespace xyz", "everything"...)
|
||||
|
||||
- an operations filter
|
||||
<br/>
|
||||
(e.g. CREATE, UPDATE, DELETE)
|
||||
|
||||
- the address of the webhook server
|
||||
|
||||
- Each time an operation matches the filters, it is sent to the webhook server
|
||||
|
||||
---
|
||||
|
||||
## What gets sent exactly?
|
||||
|
||||
- The API server will `POST` a JSON object to the webhook
|
||||
|
||||
- That object will be a Kubernetes API message with `kind` `AdmissionReview`
|
||||
|
||||
- It will contain a `request` field, with, notably:
|
||||
|
||||
- `request.uid` (to be used when replying)
|
||||
|
||||
- `request.object` (the object created/deleted/changed)
|
||||
|
||||
- `request.oldObject` (when an object is modified)
|
||||
|
||||
- `request.userInfo` (who was making the request to the API in the first place)
|
||||
|
||||
(See [the documentation](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#request) for a detailed example showing more fields.)
|
||||
|
||||
---
|
||||
|
||||
## How should the webhook respond?
|
||||
|
||||
- By replying with another `AdmissionReview` in JSON
|
||||
|
||||
- It should have a `response` field, with, notably:
|
||||
|
||||
- `response.uid` (matching the `request.uid`)
|
||||
|
||||
- `response.allowed` (`true`/`false`)
|
||||
|
||||
- `response.status.message` (optional string; useful when denying requests)
|
||||
|
||||
- `response.patchType` (when a mutating webhook changes the object; e.g. `json`)
|
||||
|
||||
- `response.patch` (the patch, encoded in base64)
|
||||
|
||||
---
|
||||
|
||||
## What if the webhook *does not* respond?
|
||||
|
||||
- If "something bad" happens, the API server follows the `failurePolicy` option
|
||||
|
||||
- this is a per-webhook option (specified in the webhook configuration)
|
||||
|
||||
- it can be `Fail` (the default) or `Ignore` ("allow all, unmodified")
|
||||
|
||||
- What's "something bad"?
|
||||
|
||||
- webhook responds with something invalid
|
||||
|
||||
- webhook takes more than 10 seconds to respond
|
||||
<br/>
|
||||
(this can be changed with `timeoutSeconds` field in the webhook config)
|
||||
|
||||
- webhook is down or has invalid certificates
|
||||
<br/>
|
||||
(TLS! It's not just a good idea; for admission control, it's the law!)
|
||||
|
||||
---
|
||||
|
||||
## What did you say about TLS?
|
||||
|
||||
- The webhook configuration can indicate:
|
||||
|
||||
- either `url` of the webhook server (has to begin with `https://`)
|
||||
|
||||
- or `service.name` and `service.namespace` of a Service on the cluster
|
||||
|
||||
- In the latter case, the Service has to accept TLS connections on port 443
|
||||
|
||||
- It has to use a certificate with CN `<name>.<namespace>.svc`
|
||||
|
||||
(**and** a `subjectAltName` extension with `DNS:<name>.<namespace>.svc`)
|
||||
|
||||
- The certificate needs to be valid (signed by a CA trusted by the API server)
|
||||
|
||||
... alternatively, we can pass a `caBundle` in the webhook configuration
|
||||
|
||||
---
|
||||
|
||||
## Webhook server inside or outside
|
||||
|
||||
- "Outside" webhook server is defined with `url` option
|
||||
|
||||
- convenient for external webooks (e.g. tamper-resistent audit trail)
|
||||
|
||||
- also great for initial development (e.g. with ngrok)
|
||||
|
||||
- requires outbound connectivity (duh) and can become a SPOF
|
||||
|
||||
- "Inside" webhook server is defined with `service` option
|
||||
|
||||
- convenient when the webhook needs to be deployed and managed on the cluster
|
||||
|
||||
- also great for air gapped clusters
|
||||
|
||||
- development can be harder (but tools like [Tilt](https://tilt.dev) can help)
|
||||
|
||||
---
|
||||
|
||||
## Developing a simple admission webhook
|
||||
|
||||
- We're going to register a custom webhook!
|
||||
|
||||
- First, we'll just dump the `AdmissionRequest` object
|
||||
|
||||
(using a little Node app)
|
||||
|
||||
- Then, we'll implement a strict policy on a specific label
|
||||
|
||||
(using a little Flask app)
|
||||
|
||||
- Development will happen in local containers, plumbed with ngrok
|
||||
|
||||
- The we will deploy to the cluster 🔥
|
||||
|
||||
---
|
||||
|
||||
## Running the webhook locally
|
||||
|
||||
- We prepared a Docker Compose file to start the whole stack
|
||||
|
||||
(the Node "echo" app, the Flask app, and one ngrok tunnel for each of them)
|
||||
|
||||
.exercise[
|
||||
|
||||
- Go to the webhook directory:
|
||||
```bash
|
||||
cd ~/container.training/webhooks/admission
|
||||
```
|
||||
|
||||
- Start the webhook in Docker containers:
|
||||
```bash
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
*Note the URL in `ngrok-echo_1` looking like `url=https://xxxx.ngrok.io`.*
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## What's ngrok?
|
||||
|
||||
- Ngrok provides secure tunnels to access local services
|
||||
|
||||
- Example: run `ngrok http 1234`
|
||||
|
||||
- `ngrok` will display a publicly-available URL (e.g. https://xxxxyyyyzzzz.ngrok.io)
|
||||
|
||||
- Connections to https://xxxxyyyyzzzz.ngrok.io will terminate at `localhost:1234`
|
||||
|
||||
- Basic product is free; extra features (vanity domains, end-to-end TLS...) for $$$
|
||||
|
||||
- Perfect to develop our webhook!
|
||||
|
||||
- Probably not for production, though
|
||||
|
||||
(webhook requests and responses now pass through the ngrok platform)
|
||||
|
||||
---
|
||||
|
||||
## Update the webhook configuration
|
||||
|
||||
- We have a webhook configuration in `k8s/webhook-configuration.yaml`
|
||||
|
||||
- We need to update the configuration with the correct `url`
|
||||
|
||||
.exercise[
|
||||
|
||||
- Edit the webhook configuration manifest:
|
||||
```bash
|
||||
vim k8s/webhook-configuration.yaml
|
||||
```
|
||||
|
||||
- **Uncomment** the `url:` line
|
||||
|
||||
- **Update** the `.ngrok.io` URL with the URL shown by Compose
|
||||
|
||||
- Save and quit
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Register the webhook configuration
|
||||
|
||||
- Just after we register the webhook, it will be called for each matching request
|
||||
|
||||
(CREATE and UPDATE on Pods in all namespaces)
|
||||
|
||||
- The `failurePolicy` is `Ignore`
|
||||
|
||||
(so if the webhook server is down, we can still create pods)
|
||||
|
||||
.exercise[
|
||||
|
||||
- Register the webhook:
|
||||
```bash
|
||||
kubectl apply -f k8s/webhook-configuration.yaml
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
It is strongly recommended to tail the logs of the API server while doing that.
|
||||
|
||||
---
|
||||
|
||||
## Create a pod
|
||||
|
||||
- Let's create a pod and try to set a `color` label
|
||||
|
||||
.exercise[
|
||||
|
||||
- Create a pod named `chroma`:
|
||||
```bash
|
||||
kubectl run --restart=Never chroma --image=nginx
|
||||
```
|
||||
|
||||
- Add a label `color` set to `pink`:
|
||||
```bash
|
||||
kubectl label pod chroma color=pink
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
We should see the `AdmissionReview` objects in the Compose logs.
|
||||
|
||||
Note: the webhook doesn't do anything (other than printing the request payload).
|
||||
|
||||
---
|
||||
|
||||
## Use the "real" admission webhook
|
||||
|
||||
- We have a small Flask app implementing a particular policy on pod labels:
|
||||
|
||||
- if a pod sets a label `color`, it must be `blue`, `green`, `red`
|
||||
|
||||
- once that `color` label is set, it cannot be removed or changed
|
||||
|
||||
- That Flask app was started when we did `docker-compose up` earlier
|
||||
|
||||
- It is exposed through its own ngrok tunnel
|
||||
|
||||
- We are going to use that webhook instead of the other one
|
||||
|
||||
(by changing only the `url` field in the ValidatingWebhookConfiguration)
|
||||
|
||||
---
|
||||
|
||||
## Update the webhook configuration
|
||||
|
||||
.exercise[
|
||||
|
||||
- First, check the ngrok URL of the tunnel for the Flask app:
|
||||
```bash
|
||||
docker-compose logs ngrok-flask
|
||||
```
|
||||
|
||||
- Then, edit the webhook configuration:
|
||||
```bash
|
||||
kubectl edit validatingwebhookconfiguration admission.container.training
|
||||
```
|
||||
- Find the `url:` field with the `.ngrok.io` URL and update it
|
||||
|
||||
- Save and quit; the new configuration is applied immediately
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Verify the behavior of the webhook
|
||||
|
||||
- Try to create a few pods and/or change labels on existing pods
|
||||
|
||||
- What happens if we try to make changes to the earlier pod?
|
||||
|
||||
(the one that has `label=pink`)
|
||||
|
||||
---
|
||||
|
||||
## Deploying the webhook on the cluster
|
||||
|
||||
- Let's see what's needed to self-host the webhook server!
|
||||
|
||||
- The webhook needs to be reachable through a Service on our cluster
|
||||
|
||||
- The Service needs to accept TLS connections on port 443
|
||||
|
||||
- We need a proper TLS certificate:
|
||||
|
||||
- with the right `CN` and `subjectAltName` (`<servicename>.<namespace>.svc`)
|
||||
|
||||
- signed by a trusted CA
|
||||
|
||||
- We can either use a "real" CA, or use the `caBundle` option to specify the CA cert
|
||||
|
||||
(the latter makes it easy to use self-signed certs)
|
||||
|
||||
---
|
||||
|
||||
## In practice
|
||||
|
||||
- We're going to generate a key pair and a self-signed certificate
|
||||
|
||||
- We will store them in a Secret
|
||||
|
||||
- We will run the webhook in a Deployment, exposed with a Service
|
||||
|
||||
- We will update the webhook configuration to use that Service
|
||||
|
||||
- The Service will be named `admission`, in Namespace `webhooks`
|
||||
|
||||
(keep in mind that the ValidatingWebhookConfiguration itself is at cluster scope)
|
||||
|
||||
---
|
||||
|
||||
## Let's get to work!
|
||||
|
||||
.exercise[
|
||||
|
||||
- Make sure we're in the right directory:
|
||||
```bash
|
||||
cd ~/container.training/webhooks/admission
|
||||
```
|
||||
|
||||
- Create the namespace:
|
||||
```bash
|
||||
kubectl create namespace webhooks
|
||||
```
|
||||
|
||||
- Switch to the namespace:
|
||||
```bash
|
||||
kubectl config set-context --current --namespace=webhooks
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Deploying the webhook
|
||||
|
||||
- *Normally,* we would author an image for this
|
||||
|
||||
- Since our webhook is just *one* Python source file ...
|
||||
|
||||
... we'll store it in a ConfigMap, and install dependencies on the fly
|
||||
|
||||
.exercise[
|
||||
|
||||
- Load the webhook source in a ConfigMap:
|
||||
```bash
|
||||
kubectl create configmap admission --from-file=flask/webhook.py
|
||||
```
|
||||
|
||||
- Create the Deployment and Service:
|
||||
```bash
|
||||
kubectl apply -f k8s/webhook-server.yaml
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Generating the key pair and certificate
|
||||
|
||||
- Let's call OpenSSL to the rescue!
|
||||
|
||||
(of course, there are plenty others options; e.g. `cfssl`)
|
||||
|
||||
.exercise[
|
||||
|
||||
- Generate a self-signed certificate:
|
||||
```bash
|
||||
NAMESPACE=webhooks
|
||||
SERVICE=admission
|
||||
CN=$SERVICE.$NAMESPACE.svc
|
||||
openssl req -x509 -newkey rsa:4096 -nodes -keyout key.pem -out cert.pem \
|
||||
-days 30 -subj /CN=$CN -addext subjectAltName=DNS:$CN
|
||||
```
|
||||
|
||||
- Load up the key and cert in a Secret:
|
||||
```bash
|
||||
kubectl create secret tls admission --cert=cert.pem --key=key.pem
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Update the webhook configuration
|
||||
|
||||
- Let's reconfigure the webhook to use our Service instead of ngrok
|
||||
|
||||
.exercise[
|
||||
|
||||
- Edit the webhook configuration manifest:
|
||||
```bash
|
||||
vim k8s/webhook-configuration.yaml
|
||||
```
|
||||
|
||||
- Comment out the `url:` line
|
||||
|
||||
- Uncomment the `service:` section
|
||||
|
||||
- Save, quit
|
||||
|
||||
- Update the webhook configuration:
|
||||
```bash
|
||||
kubectl apply -f k8s/webhook-configuration.yaml
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Add our self-signed cert to the `caBundle`
|
||||
|
||||
- The API server won't accept our self-signed certificate
|
||||
|
||||
- We need to add it to the `caBundle` field in the webhook configuration
|
||||
|
||||
- The `caBundle` will be our `cert.pem` file, encoded in base64
|
||||
|
||||
---
|
||||
|
||||
Shell to the rescue!
|
||||
|
||||
.exercise[
|
||||
|
||||
- Load up our cert and encode it in base64:
|
||||
```bash
|
||||
CA=$(base64 -w0 < cert.pem)
|
||||
```
|
||||
|
||||
- Define a patch operation to update the `caBundle`:
|
||||
```bash
|
||||
PATCH='[{
|
||||
"op": "replace",
|
||||
"path": "/webhooks/0/clientConfig/caBundle",
|
||||
"value":"'$CA'"
|
||||
}]'
|
||||
```
|
||||
|
||||
- Patch the webhook configuration:
|
||||
```bash
|
||||
kubectl patch validatingwebhookconfiguration \
|
||||
admission.webhook.container.training \
|
||||
--type='json' -p="$PATCH"
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Try it out!
|
||||
|
||||
- Keep an eye on the API server logs
|
||||
|
||||
- Tail the logs of the pod running the webhook server
|
||||
|
||||
- Create a few pods; we should see requests in the webhook server logs
|
||||
|
||||
- Check that the label `color` is enforced correctly
|
||||
|
||||
(it should only allow values of `red`, `green`, `blue`)
|
||||
|
||||
???
|
||||
|
||||
:EN:- Dynamic admission control with webhooks
|
||||
:FR:- Contrôle d'admission dynamique (webhooks)
|
||||
386
slides/k8s/aggregation-layer.md
Normal file
386
slides/k8s/aggregation-layer.md
Normal file
@@ -0,0 +1,386 @@
|
||||
# The Aggregation Layer
|
||||
|
||||
- The aggregation layer is a way to extend the Kubernetes API
|
||||
|
||||
- It is similar to CRDs
|
||||
|
||||
- it lets us define new resource types
|
||||
|
||||
- these resources can then be used with `kubectl` and other clients
|
||||
|
||||
- The implementation is very different
|
||||
|
||||
- CRDs are handled within the API server
|
||||
|
||||
- the aggregation layer offloads requests to another process
|
||||
|
||||
- They are designed for very different use-cases
|
||||
|
||||
---
|
||||
|
||||
## CRDs vs aggregation layer
|
||||
|
||||
- The Kubernetes API is a REST-ish API with a hierarchical structure
|
||||
|
||||
- It can be extended with Custom Resource Definifions (CRDs)
|
||||
|
||||
- Custom resources are managed by the Kubernetes API server
|
||||
|
||||
- we don't need to write code
|
||||
|
||||
- the API server does all the heavy lifting
|
||||
|
||||
- these resources are persisted in Kubernetes' "standard" database
|
||||
<br/>
|
||||
(for most installations, that's `etcd`)
|
||||
|
||||
- We can also define resources that are *not* managed by the API server
|
||||
|
||||
(the API server merely proxies the requests to another server)
|
||||
|
||||
---
|
||||
|
||||
## Which one is best?
|
||||
|
||||
- For things that "map" well to objects stored in a traditional database:
|
||||
|
||||
*probably CRDs*
|
||||
|
||||
- For things that "exist" only in Kubernetes and don't represent external resources:
|
||||
|
||||
*probably CRDs*
|
||||
|
||||
- For things that are read-only, at least from Kubernetes' perspective:
|
||||
|
||||
*probably aggregation layer*
|
||||
|
||||
- For things that can't be stored in etcd because of size or access patterns:
|
||||
|
||||
*probably aggregation layer*
|
||||
|
||||
|
||||
---
|
||||
|
||||
## How are resources organized?
|
||||
|
||||
- Let's have a look at the Kubernetes API hierarchical structure
|
||||
|
||||
- Useful: `.metadata.selfLink` contains the URI of a resource
|
||||
|
||||
.exercise[
|
||||
|
||||
- Check the `apiVersion` and URI of a "core" resource, e.g. a Node:
|
||||
```bash
|
||||
kubectl get nodes -o json | jq .items[0].apiVersion
|
||||
kubectl get nodes -o json | jq .items[0].metadata.selfLink
|
||||
```
|
||||
|
||||
- Get the `apiVersion` and URI for a "non-core" resource, e.g. a ClusterRole:
|
||||
```bash
|
||||
kubectl get clusterrole view -o json | jq .apiVersion
|
||||
kubectl get clusterrole view -o json | jq .metadata.selfLink
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Core vs non-core
|
||||
|
||||
- This is the structure of the URIs that we just checked:
|
||||
|
||||
```
|
||||
/api/v1/nodes/node1
|
||||
↑ ↑ ↑
|
||||
`version` `kind` `name`
|
||||
|
||||
/apis/rbac.authorization.k8s.io/v1/clusterroles/view
|
||||
↑ ↑ ↑ ↑
|
||||
`group` `version` `kind` `name`
|
||||
```
|
||||
|
||||
- There is no group for "core" resources
|
||||
|
||||
- Or, we could say that the group, `core`, is implied
|
||||
|
||||
---
|
||||
|
||||
## Group-Version-Kind
|
||||
|
||||
- In the API server, the Group-Version-Kind triple maps to a Go type
|
||||
|
||||
(look for all the "GVK" occurrences in the source code!)
|
||||
|
||||
- In the API server URI router, the GVK is parsed "relatively early"
|
||||
|
||||
(so that the server can know which resource we're talking about)
|
||||
|
||||
- "Well, actually ..." Things are a bit more complicated, see next slides!
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Namespaced resources
|
||||
|
||||
- Here are what namespaced resources URIs look like:
|
||||
|
||||
```
|
||||
/api/v1/namespaces/default/services/kubernetes
|
||||
↑ ↑ ↑ ↑
|
||||
`version` `namespace` `kind` `name`
|
||||
|
||||
/apis/apps/v1/namespaces/kube-system/daemonsets/kube-proxy
|
||||
↑ ↑ ↑ ↑ ↑
|
||||
`group` `version` `namespace` `kind` `name`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Subresources
|
||||
|
||||
- Many resources have *subresources*, for instance:
|
||||
|
||||
- `/status` (decouples status updates from other updates)
|
||||
|
||||
- `/scale` (exposes a consistent interface for autoscalers)
|
||||
|
||||
- `/proxy` (allows access to HTTP resources)
|
||||
|
||||
- `/portforward` (used by `kubectl port-forward`)
|
||||
|
||||
- `/logs` (access pod logs)
|
||||
|
||||
- These are added at the end of the URI
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Accessing a subresource
|
||||
|
||||
.exercise[
|
||||
|
||||
- List `kube-proxy` pods:
|
||||
```bash
|
||||
kubectl get pods --namespace=kube-system --selector=k8s-app=kube-proxy
|
||||
PODNAME=$(
|
||||
kubectl get pods --namespace=kube-system --selector=k8s-app=kube-proxy \
|
||||
-o json | jq .items[0].metadata.name)
|
||||
```
|
||||
|
||||
- Execute a command in a pod, showing the API requests:
|
||||
```bash
|
||||
kubectl -v6 exec --namespace=kube-system $PODNAME -- echo hello world
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
--
|
||||
|
||||
The full request looks like:
|
||||
```
|
||||
POST https://.../api/v1/namespaces/kube-system/pods/kube-proxy-c7rlw/exec?
|
||||
command=echo&command=hello&command=world&container=kube-proxy&stderr=true&stdout=true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Listing what's supported on the server
|
||||
|
||||
- There are at least three useful commands to introspect the API server
|
||||
|
||||
.exercise[
|
||||
|
||||
- List resources types, their group, kind, short names, and scope:
|
||||
```bash
|
||||
kubectl api-resources
|
||||
```
|
||||
|
||||
- List API groups + versions:
|
||||
```bash
|
||||
kubectl api-versions
|
||||
```
|
||||
|
||||
- List APIServices:
|
||||
```bash
|
||||
kubectl get apiservices
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
--
|
||||
|
||||
🤔 What's the difference between the last two?
|
||||
|
||||
---
|
||||
|
||||
## API registration
|
||||
|
||||
- `kubectl api-versions` shows all API groups, including `apiregistration.k8s.io`
|
||||
|
||||
- `kubectl get apiservices` shows the "routing table" for API requests
|
||||
|
||||
- The latter doesn't show `apiregistration.k8s.io`
|
||||
|
||||
(APIServices belong to `apiregistration.k8s.io`)
|
||||
|
||||
- Most API groups are `Local` (handled internally by the API server)
|
||||
|
||||
- If we're running the `metrics-server`, it should handle `metrics.k8s.io`
|
||||
|
||||
- This is an API group handled *outside* of the API server
|
||||
|
||||
- This is the *aggregation layer!*
|
||||
|
||||
---
|
||||
|
||||
## Finding resources
|
||||
|
||||
The following assumes that `metrics-server` is deployed on your cluster.
|
||||
|
||||
.exercise[
|
||||
|
||||
- Check that the metrics.k8s.io is registered with `metrics-server`:
|
||||
```bash
|
||||
kubectl get apiservices | grep metrics.k8s.io
|
||||
```
|
||||
|
||||
- Check the resource kinds registered in the metrics.k8s.io group:
|
||||
```bash
|
||||
kubectl api-resources --api-group=metrics.k8s.io
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
(If the output of either command is empty, install `metrics-server` first.)
|
||||
|
||||
---
|
||||
|
||||
## `nodes` vs `nodes`
|
||||
|
||||
- We can have multiple resources with the same name
|
||||
|
||||
.exercise[
|
||||
|
||||
- Look for resources named `node`:
|
||||
```bash
|
||||
kubectl api-resources | grep -w nodes
|
||||
```
|
||||
|
||||
- Compare the output of both commands:
|
||||
```bash
|
||||
kubectl get nodes
|
||||
kubectl get nodes.metrics.k8s.io
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
--
|
||||
|
||||
🤔 What are the second kind of nodes? How can we see what's really in them?
|
||||
|
||||
---
|
||||
|
||||
## Node vs NodeMetrics
|
||||
|
||||
- `nodes.metrics.k8s.io` (aka NodeMetrics) don't have fancy *printer columns*
|
||||
|
||||
- But we can look at the raw data (with `-o json` or `-o yaml`)
|
||||
|
||||
.exercise[
|
||||
|
||||
- Look at NodeMetrics objects with one of these commands:
|
||||
```bash
|
||||
kubectl get -o yaml nodes.metrics.k8s.io
|
||||
kubectl get -o yaml NodeMetrics
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
--
|
||||
|
||||
💡 Alright, these are the live metrics (CPU, RAM) for our nodes.
|
||||
|
||||
---
|
||||
|
||||
## An easier way to consume metrics
|
||||
|
||||
- We might have seen these metrics before ... With an easier command!
|
||||
|
||||
--
|
||||
|
||||
.exercise[
|
||||
|
||||
- Display node metrics:
|
||||
```bash
|
||||
kubectl top nodes
|
||||
```
|
||||
|
||||
- Check which API requests happen behind the scenes:
|
||||
```bash
|
||||
kubectl top nodes -v6
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Aggregation layer in practice
|
||||
|
||||
- We can write an API server to handle a subset of the Kubernetes API
|
||||
|
||||
- Then we can register that server by creating an APIService resource
|
||||
|
||||
.exercise[
|
||||
|
||||
- Check the definition used for the `metrics-server`:
|
||||
```bash
|
||||
kubectl describe apiservices v1beta1.metrics.k8s.io
|
||||
```
|
||||
]
|
||||
|
||||
- Group priority is used when multiple API groups provide similar kinds
|
||||
|
||||
(e.g. `nodes` and `nodes.metrics.k8s.io` as seen earlier)
|
||||
|
||||
---
|
||||
|
||||
## Authentication flow
|
||||
|
||||
- We have two Kubernetes API servers:
|
||||
|
||||
- "aggregator" (the main one; clients connect to it)
|
||||
|
||||
- "aggregated" (the one providing the extra API; aggregator connects to it)
|
||||
|
||||
- Aggregator deals with client authentication
|
||||
|
||||
- Aggregator authenticates with aggregated using mutual TLS
|
||||
|
||||
- Aggregator passes (/forwards/proxies/...) requests to aggregated
|
||||
|
||||
- Aggregated performs authorization by calling back aggregator
|
||||
|
||||
("can subject X perform action Y on resource Z?")
|
||||
|
||||
[This doc page](https://kubernetes.io/docs/tasks/extend-kubernetes/configure-aggregation-layer/#authentication-flow) has very nice swim lanes showing that flow.
|
||||
|
||||
---
|
||||
|
||||
## Discussion
|
||||
|
||||
- Aggregation layer is great for metrics
|
||||
|
||||
(fast-changing, ephemeral data, that would be outrageously bad for etcd)
|
||||
|
||||
- It *could* be a good fit to expose other REST APIs as a pass-thru
|
||||
|
||||
(but it's more common to see CRDs instead)
|
||||
|
||||
???
|
||||
|
||||
:EN:- The aggregation layer
|
||||
:FR:- Étendre l'API avec le *aggregation layer*
|
||||
179
slides/k8s/apiserver-deepdive.md
Normal file
179
slides/k8s/apiserver-deepdive.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# API server internals
|
||||
|
||||
- Understanding the internals of the API server is useful.red[¹]:
|
||||
|
||||
- when extending the Kubernetes API server (CRDs, webhooks...)
|
||||
|
||||
- when running Kubernetes at scale
|
||||
|
||||
- Let's dive into a bit of code!
|
||||
|
||||
.footnote[.red[¹]And by *useful*, we mean *strongly recommended or else...*]
|
||||
|
||||
---
|
||||
|
||||
## The main handler
|
||||
|
||||
- The API server parses its configuration, and builds a `GenericAPIServer`
|
||||
|
||||
- ... which contains an `APIServerHandler` ([src](https://github.com/kubernetes/apiserver/blob/release-1.19/pkg/server/handler.go#L37
|
||||
))
|
||||
|
||||
- ... which contains a couple of `http.Handler` fields
|
||||
|
||||
- Requests go through:
|
||||
|
||||
- `FullhandlerChain` (a series of HTTP filters, see next slide)
|
||||
|
||||
- `Director` (switches the request to `GoRestfulContainer` or `NonGoRestfulMux`)
|
||||
|
||||
- `GoRestfulContainer` is for "normal" APIs; integrates nicely with OpenAPI
|
||||
|
||||
- `NonGoRestfulMux` is for everything else (e.g. proxy, delegation)
|
||||
|
||||
---
|
||||
|
||||
## The chain of handlers
|
||||
|
||||
- API requests go through a complex chain of filters ([src](https://github.com/kubernetes/apiserver/blob/release-1.19/pkg/server/config.go#L671))
|
||||
|
||||
(note when reading that code: requests start at the bottom and go up)
|
||||
|
||||
- This is where authentication, authorization, and admission happen
|
||||
|
||||
(as well as a few other things!)
|
||||
|
||||
- Let's review an arbitrary selection of some of these handlers!
|
||||
|
||||
*In the following slides, the handlers are in chronological order.*
|
||||
|
||||
*Note: handlers are nested; so they can act at the beginning and end of a request.*
|
||||
|
||||
---
|
||||
|
||||
## `WithPanicRecovery`
|
||||
|
||||
- Reminder about Go: there is no exception handling in Go; instead:
|
||||
|
||||
- functions typically return a composite `(SomeType, error)` type
|
||||
|
||||
- when things go really bad, the code can call `panic()`
|
||||
|
||||
- `panic()` can be caught with `recover()`
|
||||
<br/>
|
||||
(but this is almost never used like an exception handler!)
|
||||
|
||||
- The API server code is not supposed to `panic()`
|
||||
|
||||
- But just in case, we have that handler to prevent (some) crashes
|
||||
|
||||
---
|
||||
|
||||
## `WithRequestInfo` ([src](https://github.com/kubernetes/apiserver/blob/release-1.19/pkg/endpoints/request/requestinfo.go#L163))
|
||||
|
||||
|
||||
- Parse out essential information:
|
||||
|
||||
API group, version, namespace, resource, subresource, verb ...
|
||||
|
||||
- WithRequestInfo: parse out API group+version, Namespace, resource, subresource ...
|
||||
|
||||
- Maps HTTP verbs (GET, PUT, ...) to Kubernetes verbs (list, get, watch, ...)
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## HTTP verb mapping
|
||||
|
||||
- POST → create
|
||||
|
||||
- PUT → update
|
||||
|
||||
- PATCH → patch
|
||||
|
||||
- DELETE
|
||||
<br/> → delete (if a resource name is specified)
|
||||
<br/> → deletecollection (otherwise)
|
||||
|
||||
- GET, HEAD
|
||||
<br/> → get (if a resource name is specified)
|
||||
<br/> → list (otherwise)
|
||||
<br/> → watch (if the `?watch=true` option is specified)
|
||||
|
||||
---
|
||||
|
||||
## `WithWaitGroup`,
|
||||
|
||||
- When we shutdown, tells clients (with in-flight requests) to retry
|
||||
|
||||
- only for "short" requests
|
||||
|
||||
- for long running requests, the client needs to do more
|
||||
|
||||
- Long running requests include `watch` verb, `proxy` sub-resource
|
||||
|
||||
(See also `WithTimeoutForNonLongRunningRequests`)
|
||||
|
||||
---
|
||||
|
||||
## AuthN and AuthZ
|
||||
|
||||
- `WithAuthentication`:
|
||||
the request goes through a *chain* of authenticators
|
||||
([src](https://github.com/kubernetes/apiserver/blob/release-1.19/pkg/endpoints/filters/authentication.go#L38))
|
||||
|
||||
- WithAudit
|
||||
|
||||
- WithImpersonation: used for e.g. `kubectl ... --as another.user`
|
||||
|
||||
- WithPriorityAndFairness or WithMaxInFlightLimit
|
||||
|
||||
(`system:masters` can bypass these)
|
||||
|
||||
- WithAuthorization
|
||||
|
||||
---
|
||||
|
||||
## After all these handlers ...
|
||||
|
||||
- We get to the "director" mentioned above
|
||||
|
||||
- Api Groups get installed into the "gorestfulhandler"
|
||||
([src](https://github.com/kubernetes/apiserver/blob/release-1.19/pkg/server/genericapiserver.go#L423))
|
||||
|
||||
- REST-ish resources are managed by various handlers
|
||||
(in [this directory](https://github.com/kubernetes/apiserver/blob/release-1.19/pkg/endpoints/handlers/))
|
||||
|
||||
- These files show us the code path for each type of request
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Request code path
|
||||
|
||||
- [create.go](https://github.com/kubernetes/apiserver/blob/release-1.19/pkg/endpoints/handlers/create.go):
|
||||
decode to HubGroupVersion; admission; mutating admission; store
|
||||
|
||||
- [delete.go](https://github.com/kubernetes/apiserver/blob/release-1.19/pkg/endpoints/handlers/delete.go):
|
||||
validating admission only; deletion
|
||||
|
||||
- [get.go](https://github.com/kubernetes/apiserver/blob/release-1.19/pkg/endpoints/handlers/get.go) (get, list):
|
||||
directly fetch from rest storage abstraction
|
||||
|
||||
- [patch.go](https://github.com/kubernetes/apiserver/blob/release-1.19/pkg/endpoints/handlers/patch.go):
|
||||
admission; mutating admission; patch
|
||||
|
||||
- [update.go](https://github.com/kubernetes/apiserver/blob/release-1.19/pkg/endpoints/handlers/update.go):
|
||||
decode to HubGroupVersion; admission; mutating admission; store
|
||||
|
||||
- [watch.go](https://github.com/kubernetes/apiserver/blob/release-1.19/pkg/endpoints/handlers/watch.go):
|
||||
similar to get.go, but with watch logic
|
||||
|
||||
(HubGroupVersion = in-memory, "canonical" version.)
|
||||
|
||||
???
|
||||
|
||||
:EN:- Kubernetes API server internals
|
||||
:FR:- Fonctionnement interne du serveur API
|
||||
@@ -273,6 +273,26 @@ class: extra-details
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Group-Version-Kind, or GVK
|
||||
|
||||
- A particular type will be identified by the combination of:
|
||||
|
||||
- the API group it belongs to (core, `apps`, `metrics.k8s.io`, ...)
|
||||
|
||||
- the version of this API group (`v1`, `v1beta1`, ...)
|
||||
|
||||
- the "Kind" itself (Pod, Role, Job, ...)
|
||||
|
||||
- "GVK" appears a lot in the API machinery code
|
||||
|
||||
- Conversions are possible between different versions and even between API groups
|
||||
|
||||
(e.g. when Deployments moved from `extensions` to `apps`)
|
||||
|
||||
---
|
||||
|
||||
## Update
|
||||
|
||||
- Let's update our namespace object
|
||||
@@ -334,6 +354,34 @@ We demonstrated *update* and *watch* semantics.
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Watch events
|
||||
|
||||
- `kubectl get --watch` shows changes
|
||||
|
||||
- If we add `--output-watch-events`, we can also see:
|
||||
|
||||
- the difference between ADDED and MODIFIED resources
|
||||
|
||||
- DELETED resources
|
||||
|
||||
.exercise[
|
||||
|
||||
- In one terminal, watch pods, displaying full events:
|
||||
```bash
|
||||
kubectl get pods --watch --output-watch-events
|
||||
```
|
||||
|
||||
- In another, run a short-lived pod:
|
||||
```bash
|
||||
kubectl run pause --image=alpine --rm -ti --restart=Never -- sleep 5
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
# Other control plane components
|
||||
|
||||
- API server ✔️
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
|
||||
- [HTTP basic auth](https://en.wikipedia.org/wiki/Basic_access_authentication)
|
||||
|
||||
(carrying user and password in an HTTP header)
|
||||
(carrying user and password in an HTTP header; [deprecated since Kubernetes 1.19](https://github.com/kubernetes/kubernetes/pull/89069))
|
||||
|
||||
- Authentication proxy
|
||||
|
||||
@@ -749,7 +749,7 @@ class: extra-details
|
||||
|
||||
:EN:- Authentication and authorization in Kubernetes
|
||||
:EN:- Authentication with tokens and certificates
|
||||
:EN:- Aithorization with RBAC (Role-Based Access Control)
|
||||
:EN:- Authorization with RBAC (Role-Based Access Control)
|
||||
:EN:- Restricting permissions with Service Accounts
|
||||
:EN:- Working with Roles, Cluster Roles, Role Bindings, etc.
|
||||
|
||||
|
||||
@@ -148,6 +148,28 @@ class: extra-details
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Setting a time limit
|
||||
|
||||
- It is possible to set a time limit (or deadline) for a job
|
||||
|
||||
- This is done with the field `spec.activeDeadlineSeconds`
|
||||
|
||||
(by default, it is unlimited)
|
||||
|
||||
- When the job is older than this time limit, all its pods are terminated
|
||||
|
||||
- Note that there can also be a `spec.activeDeadlineSeconds` field in pods!
|
||||
|
||||
- They can be set independently, and have different effects:
|
||||
|
||||
- the deadline of the job will stop the entire job
|
||||
|
||||
- the deadline of the pod will only stop an individual pod
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## What about `kubectl run` before v1.18?
|
||||
|
||||
- Creating a Deployment:
|
||||
@@ -176,12 +198,8 @@ class: extra-details
|
||||
|
||||
- can't express parallelism or completions of Jobs
|
||||
|
||||
- can't express Pods with multiple containers
|
||||
|
||||
- can't express healthchecks, resource limits
|
||||
|
||||
- etc.
|
||||
|
||||
- `kubectl create` and `kubectl run` are *helpers* that generate YAML manifests
|
||||
|
||||
- If we write these manifests ourselves, we can use all features and options
|
||||
|
||||
244
slides/k8s/cert-manager.md
Normal file
244
slides/k8s/cert-manager.md
Normal file
@@ -0,0 +1,244 @@
|
||||
# cert-manager
|
||||
|
||||
- cert-manager¹ facilitates certificate signing through the Kubernetes API:
|
||||
|
||||
- we create a Certificate object (that's a CRD)
|
||||
|
||||
- cert-manager creates a private key
|
||||
|
||||
- it signs that key ...
|
||||
|
||||
- ... or interacts with a certificate authority to obtain the signature
|
||||
|
||||
- it stores the resulting key+cert in a Secret resource
|
||||
|
||||
- These Secret resources can be used in many places (Ingress, mTLS, ...)
|
||||
|
||||
.footnote[.red[¹]Always lower case, words separated with a dash; see the [style guide](https://cert-manager.io/docs/faq/style/_.)]
|
||||
|
||||
---
|
||||
|
||||
## Getting signatures
|
||||
|
||||
- cert-manager can use multiple *Issuers* (another CRD), including:
|
||||
|
||||
- self-signed
|
||||
|
||||
- cert-manager acting as a CA
|
||||
|
||||
- the [ACME protocol](https://en.wikipedia.org/wiki/Automated_Certificate_Management_Environment]) (notably used by Let's Encrypt)
|
||||
|
||||
- [HashiCorp Vault](https://www.vaultproject.io/)
|
||||
|
||||
- Multiple issuers can be configured simultaneously
|
||||
|
||||
- Issuers can be available in a single namespace, or in the whole cluster
|
||||
|
||||
(then we use the *ClusterIssuer* CRD)
|
||||
|
||||
---
|
||||
|
||||
## cert-manager in action
|
||||
|
||||
- We will install cert-manager
|
||||
|
||||
- We will create a ClusterIssuer to obtain certificates with Let's Encrypt
|
||||
|
||||
(this will involve setting up an Ingress Controller)
|
||||
|
||||
- We will create a Certificate request
|
||||
|
||||
- cert-manager will honor that request and create a TLS Secret
|
||||
|
||||
---
|
||||
|
||||
## Installing cert-manager
|
||||
|
||||
- It can be installed with a YAML manifest, or with Helm
|
||||
|
||||
.exercise[
|
||||
|
||||
- Create the namespace for cert-manager:
|
||||
```bash
|
||||
kubectl create ns cert-manager
|
||||
```
|
||||
|
||||
- Add the Jetstack repository:
|
||||
```bash
|
||||
helm repo add jetstack https://charts.jetstack.io
|
||||
```
|
||||
|
||||
- Install cert-manager:
|
||||
```bash
|
||||
helm install cert-manager jetstack/cert-manager \
|
||||
--namespace cert-manager \
|
||||
--set installCRDs=true
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## ClusterIssuer manifest
|
||||
|
||||
```yaml
|
||||
@@INCLUDE[k8s/cm-clusterissuer.yaml]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Creating the ClusterIssuer
|
||||
|
||||
- The manifest shown on the previous slide is in @@LINK[k8s/cm-clusterissuer.yaml]
|
||||
|
||||
.exercise[
|
||||
|
||||
- Create the ClusterIssuer:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/cm-clusterissuer.yaml
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Certificate manifest
|
||||
|
||||
```yaml
|
||||
@@INCLUDE[k8s/cm-certificate.yaml]
|
||||
```
|
||||
|
||||
- The `name`, `secretName`, and `dnsNames` don't have to match
|
||||
|
||||
- There can be multiple `dnsNames`
|
||||
|
||||
- The `issuerRef` must match the ClusterIssuer that we created earlier
|
||||
|
||||
---
|
||||
|
||||
## Creating the Certificate
|
||||
|
||||
- The manifest shown on the previous slide is in @@LINK[k8s/cm-certificate.yaml]
|
||||
|
||||
.exercise[
|
||||
|
||||
- Edit the Certificate to update the domain name
|
||||
|
||||
(make sure to replace A.B.C.D with the IP address of one of your nodes!)
|
||||
|
||||
- Create the Certificate:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/cm-certificate.yaml
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## What's happening?
|
||||
|
||||
- cert-manager will create:
|
||||
|
||||
- the secret key
|
||||
|
||||
- a Pod, a Service, and an Ingress to complete the HTTP challenge
|
||||
|
||||
- then it waits for the challenge to complete
|
||||
|
||||
.exercise[
|
||||
|
||||
- View the resources created by cert-manager:
|
||||
```bash
|
||||
kubectl get pods,services,ingresses \
|
||||
--selector=acme.cert-manager.io/http01-solver=true
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## HTTP challenge
|
||||
|
||||
- The CA (in this case, Let's Encrypt) will fetch a particular URL:
|
||||
|
||||
`http://<our-domain>/.well-known/acme-challenge/<token>`
|
||||
|
||||
.exercise[
|
||||
|
||||
- Check the *path* of the Ingress in particular:
|
||||
```bash
|
||||
kubectl describe ingress
|
||||
--selector=acme.cert-manager.io/http01-solver=true
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## What's missing ?
|
||||
|
||||
--
|
||||
|
||||
An Ingress Controller! 😅
|
||||
|
||||
.exercise[
|
||||
|
||||
- Install an Ingress Controller:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/traefik-v2.yaml
|
||||
```
|
||||
|
||||
- Wait a little bit, and check that we now have a `kubernetes.io/tls` Secret:
|
||||
```bash
|
||||
kubectl get secrets
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Using the secret
|
||||
|
||||
- For bonus points, try to use the secret in an Ingress!
|
||||
|
||||
- This is what the manifest would look like:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: xyz
|
||||
spec:
|
||||
tls:
|
||||
- secretName: xyz.A.B.C.D.nip.io
|
||||
hosts:
|
||||
- xyz.A.B.C.D.nip.io
|
||||
rules:
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Let's Encrypt and nip.io
|
||||
|
||||
- Let's Encrypt has [rate limits](https://letsencrypt.org/docs/rate-limits/) per domain
|
||||
|
||||
(the limits only apply to the production environment, not staging)
|
||||
|
||||
- There is a limit of 50 certificates per registered domain
|
||||
|
||||
- If we try to use the production environment, we will probably hit the limit
|
||||
|
||||
- It's fine to use the staging environment for these experiments
|
||||
|
||||
(our certs won't validate in a browser, but we can always check
|
||||
the details of the cert to verify that it was issued by Let's Encrypt!)
|
||||
|
||||
???
|
||||
|
||||
:EN:- Obtaining certificates with cert-manager
|
||||
:FR:- Obtenir des certificats avec cert-manager
|
||||
193
slides/k8s/cni-internals.md
Normal file
193
slides/k8s/cni-internals.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# CNI internals
|
||||
|
||||
- Kubelet looks for a CNI configuration file
|
||||
|
||||
(by default, in `/etc/cni/net.d`)
|
||||
|
||||
- Note: if we have multiple files, the first one will be used
|
||||
|
||||
(in lexicographic order)
|
||||
|
||||
- If no configuration can be found, kubelet holds off on creating containers
|
||||
|
||||
(except if they are using `hostNetwork`)
|
||||
|
||||
- Let's see how exactly plugins are invoked!
|
||||
|
||||
---
|
||||
|
||||
## General principle
|
||||
|
||||
- A plugin is an executable program
|
||||
|
||||
- It is invoked with by kubelet to set up / tear down networking for a container
|
||||
|
||||
- It doesn't take any command-line argument
|
||||
|
||||
- However, it uses environment variables to know what to do, which container, etc.
|
||||
|
||||
- It reads JSON on stdin, and writes back JSON on stdout
|
||||
|
||||
- There will generally be multiple plugins invoked in a row
|
||||
|
||||
(at least IPAM + network setup; possibly more)
|
||||
|
||||
---
|
||||
|
||||
## Environment variables
|
||||
|
||||
- `CNI_COMMAND`: `ADD`, `DEL`, `CHECK`, or `VERSION`
|
||||
|
||||
- `CNI_CONTAINERID`: opaque identifier
|
||||
|
||||
(container ID of the "sandbox", i.e. the container running the `pause` image)
|
||||
|
||||
- `CNI_NETNS`: path to network namespace pseudo-file
|
||||
|
||||
(e.g. `/var/run/netns/cni-0376f625-29b5-7a21-6c45-6a973b3224e5`)
|
||||
|
||||
- `CNI_IFNAME`: interface name, usually `eth0`
|
||||
|
||||
- `CNI_PATH`: path(s) with plugin executables (e.g. `/opt/cni/bin`)
|
||||
|
||||
- `CNI_ARGS`: "extra arguments" (see next slide)
|
||||
|
||||
---
|
||||
|
||||
## `CNI_ARGS`
|
||||
|
||||
- Extra key/value pair arguments passed by "the user"
|
||||
|
||||
- "The user", here, is "kubelet" (or in an abstract way, "Kubernetes")
|
||||
|
||||
- This is used to pass the pod name and namespace to the CNI plugin
|
||||
|
||||
- Example:
|
||||
```
|
||||
IgnoreUnknown=1
|
||||
K8S_POD_NAMESPACE=default
|
||||
K8S_POD_NAME=web-96d5df5c8-jcn72
|
||||
K8S_POD_INFRA_CONTAINER_ID=016493dbff152641d334d9828dab6136c1ff...
|
||||
```
|
||||
|
||||
Note that technically, it's a `;`-separated list, so it really looks like this:
|
||||
```
|
||||
CNI_ARGS=IgnoreUnknown=1;K8S_POD_NAMESPACE=default;K8S_POD_NAME=web-96d...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JSON in, JSON out
|
||||
|
||||
- The plugin reads its configuration on stdin
|
||||
|
||||
- It writes back results in JSON
|
||||
|
||||
(e.g. allocated address, routes, DNS...)
|
||||
|
||||
⚠️ "Plugin configuration" is not always the same as "CNI configuration"!
|
||||
|
||||
---
|
||||
|
||||
## Conf vs Conflist
|
||||
|
||||
- The CNI configuration can be a single plugin configuration
|
||||
|
||||
- it will then contain a `type` field in the top-most structure
|
||||
|
||||
- it will be passed "as-is"
|
||||
|
||||
- It can also be a "conflist", containing a chain of plugins
|
||||
|
||||
(it will then contain a `plugins` field in the top-most structure)
|
||||
|
||||
- Plugins are then invoked in order (reverse order for `DEL` action)
|
||||
|
||||
- In that case, the input of the plugin is not the whole configuration
|
||||
|
||||
(see details on next slide)
|
||||
|
||||
---
|
||||
|
||||
## List of plugins
|
||||
|
||||
- When invoking a plugin in a list, the JSON input will be:
|
||||
|
||||
- the configuration of the plugin
|
||||
|
||||
- augmented with `name` (matching the conf list `name`)
|
||||
|
||||
- augmented with `prevResult` (which will be the output of the previous plugin)
|
||||
|
||||
- Conceptually, a plugin (generally the first one) will do the "main setup"
|
||||
|
||||
- Other plugins can do tuning / refinement (firewalling, traffic shaping...)
|
||||
|
||||
---
|
||||
|
||||
## Analyzing plugins
|
||||
|
||||
- Let's see what goes in and out of our CNI plugins!
|
||||
|
||||
- We will create a fake plugin that:
|
||||
|
||||
- saves its environment and input
|
||||
|
||||
- executes the real plugin with the saved input
|
||||
|
||||
- saves the plugin output
|
||||
|
||||
- passes the saved output
|
||||
|
||||
---
|
||||
|
||||
## Our fake plugin
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
PLUGIN=$(basename $0)
|
||||
cat > /tmp/cni.$$.$PLUGIN.in
|
||||
env | sort > /tmp/cni.$$.$PLUGIN.env
|
||||
echo "PPID=$PPID, $(readlink /proc/$PPID/exe)" > /tmp/cni.$$.$PLUGIN.parent
|
||||
$0.real < /tmp/cni.$$.$PLUGIN.in > /tmp/cni.$$.$PLUGIN.out
|
||||
EXITSTATUS=$?
|
||||
cat /tmp/cni.$$.$PLUGIN.out
|
||||
exit $EXITSTATUS
|
||||
```
|
||||
|
||||
Save this script as `/opt/cni/bin/debug` and make it executable.
|
||||
|
||||
---
|
||||
|
||||
## Substituting the fake plugin
|
||||
|
||||
- For each plugin that we want to instrument:
|
||||
|
||||
- rename the plugin from e.g. `ptp` to `ptp.real`
|
||||
|
||||
- symlink `ptp` to our `debug` plugin
|
||||
|
||||
- There is no need to change the CNI configuration or restart kubelet
|
||||
|
||||
---
|
||||
|
||||
## Create some pods and looks at the results
|
||||
|
||||
- Create a pod
|
||||
|
||||
- For each instrumented plugin, there will be files in `/tmp`:
|
||||
|
||||
`cni.PID.pluginname.in` (JSON input)
|
||||
|
||||
`cni.PID.pluginname.env` (environment variables)
|
||||
|
||||
`cni.PID.pluginname.parent` (parent process information)
|
||||
|
||||
`cni.PID.pluginname.out` (JSON output)
|
||||
|
||||
❓️ What is calling our plugins?
|
||||
|
||||
???
|
||||
|
||||
:EN:- Deep dive into CNI internals
|
||||
:FR:- La Container Network Interface (CNI) en détails
|
||||
@@ -210,6 +210,8 @@
|
||||
|
||||
(through files that get created in the container filesystem)
|
||||
|
||||
- That second link also includes a list of all the fields that can be used with the downward API
|
||||
|
||||
---
|
||||
|
||||
## Environment variables, pros and cons
|
||||
@@ -534,6 +536,8 @@ spec:
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Differences between configmaps and secrets
|
||||
|
||||
- Secrets are base64-encoded when shown with `kubectl get secrets -o yaml`
|
||||
@@ -548,6 +552,29 @@ spec:
|
||||
|
||||
(since they are two different kinds of resources)
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Immutable ConfigMaps and Secrets
|
||||
|
||||
- Since Kubernetes 1.19, it is possible to mark a ConfigMap or Secret as *immutable*
|
||||
|
||||
```bash
|
||||
kubectl patch configmap xyz --patch='{"immutable": true}'
|
||||
```
|
||||
|
||||
- This brings performance improvements when using lots of ConfigMaps and Secrets
|
||||
|
||||
(lots = tens of thousands)
|
||||
|
||||
- Once a ConfigMap or Secret has been marked as immutable:
|
||||
|
||||
- its content cannot be changed anymore
|
||||
- the `immutable` field can't be changed back either
|
||||
- the only way to change it is to delete and re-create it
|
||||
- Pods using it will have to be re-created as well
|
||||
|
||||
???
|
||||
|
||||
:EN:- Managing application configuration
|
||||
|
||||
@@ -190,6 +190,24 @@
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## How are these permissions set up?
|
||||
|
||||
- A bunch of roles and bindings are defined as constants in the API server code:
|
||||
|
||||
[auth/authorizer/rbac/bootstrappolicy/policy.go](https://github.com/kubernetes/kubernetes/blob/release-1.19/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy.go#L188)
|
||||
|
||||
- They are created automatically when the API server starts:
|
||||
|
||||
[registry/rbac/rest/storage_rbac.go](https://github.com/kubernetes/kubernetes/blob/release-1.19/pkg/registry/rbac/rest/storage_rbac.go#L140)
|
||||
|
||||
- We must use the correct Common Names (`CN`) for the control plane certificates
|
||||
|
||||
(since the bindings defined above refer to these common names)
|
||||
|
||||
---
|
||||
|
||||
## Service account tokens
|
||||
|
||||
- Each time we create a service account, the controller manager generates a token
|
||||
|
||||
334
slides/k8s/crd.md
Normal file
334
slides/k8s/crd.md
Normal file
@@ -0,0 +1,334 @@
|
||||
# Custom Resource Definitions
|
||||
|
||||
- CRDs are one of the (many) ways to extend the API
|
||||
|
||||
- CRDs can be defined dynamically
|
||||
|
||||
(no need to recompile or reload the API server)
|
||||
|
||||
- A CRD is defined with a CustomResourceDefinition resource
|
||||
|
||||
(CustomResourceDefinition is conceptually similar to a *metaclass*)
|
||||
|
||||
---
|
||||
|
||||
## A very simple CRD
|
||||
|
||||
The file @@LINK[k8s/coffee-1.yaml] describes a very simple CRD representing different kinds of coffee:
|
||||
|
||||
```yaml
|
||||
@@INCLUDE[k8s/coffee-1.yaml]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Creating a CRD
|
||||
|
||||
- Let's create the Custom Resource Definition for our Coffee resource
|
||||
|
||||
.exercise[
|
||||
|
||||
- Load the CRD:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/coffee-1.yaml
|
||||
```
|
||||
|
||||
- Confirm that it shows up:
|
||||
```bash
|
||||
kubectl get crds
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Creating custom resources
|
||||
|
||||
The YAML below defines a resource using the CRD that we just created:
|
||||
|
||||
```yaml
|
||||
kind: Coffee
|
||||
apiVersion: container.training/v1alpha1
|
||||
metadata:
|
||||
name: arabica
|
||||
spec:
|
||||
taste: strong
|
||||
```
|
||||
|
||||
.exercise[
|
||||
|
||||
- Create a few types of coffee beans:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/coffees.yaml
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Viewing custom resources
|
||||
|
||||
- By default, `kubectl get` only shows name and age of custom resources
|
||||
|
||||
.exercise[
|
||||
|
||||
- View the coffee beans that we just created:
|
||||
```bash
|
||||
kubectl get coffees
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
- We'll see in a bit how to improve that
|
||||
|
||||
---
|
||||
|
||||
## What can we do with CRDs?
|
||||
|
||||
There are many possibilities!
|
||||
|
||||
- *Operators* encapsulate complex sets of resources
|
||||
|
||||
(e.g.: a PostgreSQL replicated cluster; an etcd cluster...
|
||||
<br/>
|
||||
see [awesome operators](https://github.com/operator-framework/awesome-operators) and
|
||||
[OperatorHub](https://operatorhub.io/) to find more)
|
||||
|
||||
- Custom use-cases like [gitkube](https://gitkube.sh/)
|
||||
|
||||
- creates a new custom type, `Remote`, exposing a git+ssh server
|
||||
|
||||
- deploy by pushing YAML or Helm charts to that remote
|
||||
|
||||
- Replacing built-in types with CRDs
|
||||
|
||||
(see [this lightning talk by Tim Hockin](https://www.youtube.com/watch?v=ji0FWzFwNhA))
|
||||
|
||||
---
|
||||
|
||||
## What's next?
|
||||
|
||||
- Creating a basic CRD is quick and easy
|
||||
|
||||
- But there is a lot more that we can (and probably should) do:
|
||||
|
||||
- improve input with *data validation*
|
||||
|
||||
- improve output with *custom columns*
|
||||
|
||||
- And of course, we probably need a *controller* to go with our CRD!
|
||||
|
||||
(otherwise, we're just using the Kubernetes API as a fancy data store)
|
||||
|
||||
---
|
||||
|
||||
## Additional printer columns
|
||||
|
||||
- We can specify `additionalPrinterColumns` in the CRD
|
||||
|
||||
- This is similar to `-o custom-columns`
|
||||
|
||||
(map a column name to a path in the object, e.g. `.spec.taste`)
|
||||
|
||||
```yaml
|
||||
additionalPrinterColumns:
|
||||
- jsonPath: .spec.taste
|
||||
description: Subjective taste of that kind of coffee bean
|
||||
name: Taste
|
||||
type: string
|
||||
- jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
type: date
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using additional printer columns
|
||||
|
||||
- Let's update our CRD using @@LINK[k8s/coffee-3.yaml]
|
||||
|
||||
.exercise[
|
||||
|
||||
- Update the CRD:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/coffee-3.yaml
|
||||
```
|
||||
|
||||
- Look at our Coffee resources:
|
||||
```bash
|
||||
kubectl get coffees
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
Note: we can update a CRD without having to re-create the corresponding resources.
|
||||
|
||||
(Good news, right?)
|
||||
|
||||
---
|
||||
|
||||
## Data validation
|
||||
|
||||
- By default, CRDs are not *validated*
|
||||
|
||||
(we can put anything we want in the `spec`)
|
||||
|
||||
- When creating a CRD, we can pass an OpenAPI v3 schema
|
||||
|
||||
(which will then be used to validate resources)
|
||||
|
||||
- More advanced validation can also be done with admission webhooks, e.g.:
|
||||
|
||||
- consistency between parameters
|
||||
|
||||
- advanced integer filters (e.g. odd number of replicas)
|
||||
|
||||
- things that can change in one direction but not the other
|
||||
|
||||
---
|
||||
|
||||
## OpenAPI v3 scheme exapmle
|
||||
|
||||
This is what we have in @@LINK[k8s/coffee-3.yaml]:
|
||||
|
||||
```yaml
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
required: [ spec ]
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
properties:
|
||||
taste:
|
||||
description: Subjective taste of that kind of coffee bean
|
||||
type: string
|
||||
required: [ taste ]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation *a posteriori*
|
||||
|
||||
- Some of the "coffees" that we defined earlier *do not* pass validation
|
||||
|
||||
- How is that possible?
|
||||
|
||||
--
|
||||
|
||||
- Validation happens at *admission*
|
||||
|
||||
(when resources get written into the database)
|
||||
|
||||
- Therefore, we can have "invalid" resources in etcd
|
||||
|
||||
(they are invalid from the CRD perspective, but the CRD can be changed)
|
||||
|
||||
🤔 How should we handle that ?
|
||||
|
||||
---
|
||||
|
||||
## Versions
|
||||
|
||||
- If the data format changes, we can roll out a new version of the CRD
|
||||
|
||||
(e.g. go from `v1alpha1` to `v1alpha2`)
|
||||
|
||||
- In a CRD we can specify the versions that exist, that are *served*, and *stored*
|
||||
|
||||
- multiple versions can be *served*
|
||||
|
||||
- only one can be *stored*
|
||||
|
||||
- Kubernetes doesn't automatically migrate the content of the database
|
||||
|
||||
- However, it can convert between versions when resources are read/written
|
||||
|
||||
---
|
||||
|
||||
## Conversion
|
||||
|
||||
- When *creating* a new resource, the *stored* version is used
|
||||
|
||||
(if we create it with another version, it gets converted)
|
||||
|
||||
- When *getting* or *watching* resources, the *requested* version is used
|
||||
|
||||
(if it is stored with another version, it gets converted)
|
||||
|
||||
- By default, "conversion" only changes the `apiVersion` field
|
||||
|
||||
- ... But we can register *conversion webhooks*
|
||||
|
||||
(see [that doc page](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/#webhook-conversion) for details)
|
||||
|
||||
---
|
||||
|
||||
## Migrating database content
|
||||
|
||||
- We need to *serve* a version as long as we *store* objects in that version
|
||||
|
||||
(=as long as the database has at least one object with that version)
|
||||
|
||||
- If we want to "retire" a version, we need to migrate these objects first
|
||||
|
||||
- All we have to do is to read and re-write them
|
||||
|
||||
(the [kube-storage-version-migrator](https://github.com/kubernetes-sigs/kube-storage-version-migrator) tool can help)
|
||||
|
||||
---
|
||||
|
||||
## What's next?
|
||||
|
||||
- Generally, when creating a CRD, we also want to run a *controller*
|
||||
|
||||
(otherwise nothing will happen when we create resources of that type)
|
||||
|
||||
- The controller will typically *watch* our custom resources
|
||||
|
||||
(and take action when they are created/updated)
|
||||
|
||||
---
|
||||
|
||||
## CRDs in the wild
|
||||
|
||||
- [gitkube](https://storage.googleapis.com/gitkube/gitkube-setup-stable.yaml)
|
||||
|
||||
- [A redis operator](https://github.com/amaizfinance/redis-operator/blob/master/deploy/crds/k8s_v1alpha1_redis_crd.yaml)
|
||||
|
||||
- [cert-manager](https://github.com/jetstack/cert-manager/releases/download/v1.0.4/cert-manager.yaml)
|
||||
|
||||
*How big are these YAML files?*
|
||||
|
||||
*What's the size (e.g. in lines) of each resource?*
|
||||
|
||||
---
|
||||
|
||||
## CRDs in practice
|
||||
|
||||
- Production-grade CRDs can be extremely verbose
|
||||
|
||||
(because of the openAPI schema validation)
|
||||
|
||||
- This can (and usually will) be managed by a framework
|
||||
|
||||
---
|
||||
|
||||
## (Ab)using the API server
|
||||
|
||||
- If we need to store something "safely" (as in: in etcd), we can use CRDs
|
||||
|
||||
- This gives us primitives to read/write/list objects (and optionally validate them)
|
||||
|
||||
- The Kubernetes API server can run on its own
|
||||
|
||||
(without the scheduler, controller manager, and kubelets)
|
||||
|
||||
- By loading CRDs, we can have it manage totally different objects
|
||||
|
||||
(unrelated to containers, clusters, etc.)
|
||||
|
||||
???
|
||||
|
||||
:EN:- Custom Resource Definitions (CRDs)
|
||||
:FR:- Les CRDs *(Custom Resource Definitions)*
|
||||
@@ -108,6 +108,26 @@ The CA (or anyone else) never needs to know my private key.
|
||||
|
||||
---
|
||||
|
||||
## Warning
|
||||
|
||||
- The CSR API isn't really suited to issue user certificates
|
||||
|
||||
- It is primarily intended to issue control plane certificates
|
||||
|
||||
(for instance, deal with kubelet certificates renewal)
|
||||
|
||||
- The API was expanded a bit in Kubernetes 1.19 to encompass broader usage
|
||||
|
||||
- There are still lots of gaps in the spec
|
||||
|
||||
(e.g. how to specify expiration in a standard way)
|
||||
|
||||
- ... And no other implementation to this date
|
||||
|
||||
(but [cert-manager](https://cert-manager.io/docs/faq/#kubernetes-has-a-builtin-certificatesigningrequest-api-why-not-use-that) might eventually get there!)
|
||||
|
||||
---
|
||||
|
||||
## General idea
|
||||
|
||||
- We will create a Namespace named "users"
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
<!-- ##VERSION## -->
|
||||
|
||||
- Unfortunately, as of Kubernetes 1.17, the CLI cannot create daemon sets
|
||||
- Unfortunately, as of Kubernetes 1.19, the CLI cannot create daemon sets
|
||||
|
||||
--
|
||||
|
||||
@@ -431,15 +431,23 @@ class: extra-details
|
||||
|
||||
---
|
||||
|
||||
## Complex selectors
|
||||
## Selectors with multiple labels
|
||||
|
||||
- If a selector specifies multiple labels, they are understood as a logical *AND*
|
||||
|
||||
(In other words: the pods must match all the labels)
|
||||
(in other words: the pods must match all the labels)
|
||||
|
||||
- Kubernetes has support for advanced, set-based selectors
|
||||
- We cannot have a logical *OR*
|
||||
|
||||
(But these cannot be used with services, at least not yet!)
|
||||
(e.g. `app=api AND (release=prod OR release=preprod)`)
|
||||
|
||||
- We can, however, apply as many extra labels as we want to our pods:
|
||||
|
||||
- use selector `app=api AND prod-or-preprod=yes`
|
||||
|
||||
- add `prod-or-preprod=yes` to both sets of pods
|
||||
|
||||
- We will see later that in other places, we can use more advanced selectors
|
||||
|
||||
---
|
||||
|
||||
@@ -689,6 +697,95 @@ class: extra-details
|
||||
|
||||
- This gives us building blocks for canary and blue/green deployments
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Advanced label selectors
|
||||
|
||||
- As indicated earlier, service selectors are limited to a `AND`
|
||||
|
||||
- But in many other places in the Kubernetes API, we can use complex selectors
|
||||
|
||||
(e.g. Deployment, ReplicaSet, DaemonSet, NetworkPolicy ...)
|
||||
|
||||
- These allow extra operations; specifically:
|
||||
|
||||
- checking for presence (or absence) of a label
|
||||
|
||||
- checking if a label is (or is not) in a given set
|
||||
|
||||
- Relevant documentation:
|
||||
|
||||
[Service spec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#servicespec-v1-core),
|
||||
[LabelSelector spec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#labelselector-v1-meta),
|
||||
[label selector doc](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors)
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Example of advanced selector
|
||||
|
||||
```yaml
|
||||
theSelector:
|
||||
matchLabels:
|
||||
app: portal
|
||||
component: api
|
||||
matchExpressions:
|
||||
- key: release
|
||||
operator: In
|
||||
values: [ production, preproduction ]
|
||||
- key: signed-off-by
|
||||
operator: Exists
|
||||
```
|
||||
|
||||
This selector matches pods that meet *all* the indicated conditions.
|
||||
|
||||
`operator` can be `In`, `NotIn`, `Exists`, `DoesNotExist`.
|
||||
|
||||
A `nil` selector matches *nothing*, a `{}` selector matches *everything*.
|
||||
<br/>
|
||||
(Because that means "match all pods that meet at least zero condition".)
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Services and Endpoints
|
||||
|
||||
- Each Service has a corresponding Endpoints resource
|
||||
|
||||
(see `kubectl get endpoints` or `kubectl get ep`)
|
||||
|
||||
- That Endpoints resource is used by various controllers
|
||||
|
||||
(e.g. `kube-proxy` when setting up `iptables` rules for ClusterIP services)
|
||||
|
||||
- These Endpoints are populated (and updated) with the Service selector
|
||||
|
||||
- We can update the Endpoints manually, but our changes will get overwritten
|
||||
|
||||
- ... Except if the Service selector is empty!
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Empty Service selector
|
||||
|
||||
- If a service selector is empty, Endpoints don't get updated automatically
|
||||
|
||||
(but we can still set them manually)
|
||||
|
||||
- This lets us create Services pointing to arbitrary destinations
|
||||
|
||||
(potentially outside the cluster; or things that are not in pods)
|
||||
|
||||
- Another use-case: the `kubernetes` service in the `default` namespace
|
||||
|
||||
(its Endpoints are maintained automatically by the API server)
|
||||
|
||||
???
|
||||
|
||||
:EN:- Scaling with Daemon Sets
|
||||
|
||||
@@ -2,47 +2,65 @@
|
||||
|
||||
- Kubernetes resources can also be viewed with a web dashboard
|
||||
|
||||
- That dashboard is usually exposed over HTTPS
|
||||
|
||||
(this requires obtaining a proper TLS certificate)
|
||||
|
||||
- Dashboard users need to authenticate
|
||||
|
||||
- We are going to take a *dangerous* shortcut
|
||||
(typically with a token)
|
||||
|
||||
- The dashboard should be exposed over HTTPS
|
||||
|
||||
(to prevent interception of the aforementioned token)
|
||||
|
||||
- Ideally, this requires obtaining a proper TLS certificate
|
||||
|
||||
(for instance, with Let's Encrypt)
|
||||
|
||||
---
|
||||
|
||||
## The insecure method
|
||||
## Three ways to install the dashboard
|
||||
|
||||
- We could (and should) use [Let's Encrypt](https://letsencrypt.org/) ...
|
||||
- Our `k8s` directory has no less than three manifests!
|
||||
|
||||
- ... but we don't want to deal with TLS certificates
|
||||
- `dashboard-recommended.yaml`
|
||||
|
||||
- We could (and should) learn how authentication and authorization work ...
|
||||
(purely internal dashboard; user must be created manually)
|
||||
|
||||
- ... but we will use a guest account with admin access instead
|
||||
- `dashboard-with-token.yaml`
|
||||
|
||||
.footnote[.warning[Yes, this will open our cluster to all kinds of shenanigans. Don't do this at home.]]
|
||||
(dashboard exposed with NodePort; creates an admin user for us)
|
||||
|
||||
- `dashboard-insecure.yaml` aka *YOLO*
|
||||
|
||||
(dashboard exposed over HTTP; gives root access to anonymous users)
|
||||
|
||||
---
|
||||
|
||||
## Running a very insecure dashboard
|
||||
## `dashboard-insecure.yaml`
|
||||
|
||||
- We are going to deploy that dashboard with *one single command*
|
||||
- This will allow anyone to deploy anything on your cluster
|
||||
|
||||
- This command will create all the necessary resources
|
||||
(without any authentication whatsoever)
|
||||
|
||||
(the dashboard itself, the HTTP wrapper, the admin/guest account)
|
||||
- **Do not** use this, except maybe on a local cluster
|
||||
|
||||
- All these resources are defined in a YAML file
|
||||
(or a cluster that you will destroy a few minutes later)
|
||||
|
||||
- All we have to do is load that YAML file with with `kubectl apply -f`
|
||||
- On "normal" clusters, use `dashboard-with-token.yaml` instead!
|
||||
|
||||
---
|
||||
|
||||
## What's in the manifest?
|
||||
|
||||
- The dashboard itself
|
||||
|
||||
- An HTTP/HTTPS unwrapper (using `socat`)
|
||||
|
||||
- The guest/admin account
|
||||
|
||||
.exercise[
|
||||
|
||||
- Create all the dashboard resources, with the following command:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/insecure-dashboard.yaml
|
||||
kubectl apply -f ~/container.training/k8s/dashboard-insecure.yaml
|
||||
```
|
||||
|
||||
]
|
||||
@@ -89,11 +107,26 @@ The dashboard will then ask you which authentication you want to use.
|
||||
|
||||
--
|
||||
|
||||
.warning[By the way, we just added a backdoor to our Kubernetes cluster!]
|
||||
.warning[Remember, we just added a backdoor to our Kubernetes cluster!]
|
||||
|
||||
---
|
||||
|
||||
## Running the Kubernetes dashboard securely
|
||||
## Closing the backdoor
|
||||
|
||||
- Seriously, don't leave that thing running!
|
||||
|
||||
.exercise[
|
||||
|
||||
- Remove what we just created:
|
||||
```bash
|
||||
kubectl delete -f ~/container.training/k8s/dashboard-insecure.yaml
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## The risks
|
||||
|
||||
- The steps that we just showed you are *for educational purposes only!*
|
||||
|
||||
@@ -105,6 +138,99 @@ The dashboard will then ask you which authentication you want to use.
|
||||
|
||||
---
|
||||
|
||||
## `dashboard-with-token.yaml`
|
||||
|
||||
- This is a less risky way to deploy the dashboard
|
||||
|
||||
- It's not completely secure, either:
|
||||
|
||||
- we're using a self-signed certificate
|
||||
|
||||
- this is subject to eavesdropping attacks
|
||||
|
||||
- Using `kubectl port-forward` or `kubectl proxy` is even better
|
||||
|
||||
---
|
||||
|
||||
## What's in the manifest?
|
||||
|
||||
- The dashboard itself (but exposed with a `NodePort`)
|
||||
|
||||
- A ServiceAccount with `cluster-admin` privileges
|
||||
|
||||
(named `kubernetes-dashboard:cluster-admin`)
|
||||
|
||||
.exercise[
|
||||
|
||||
- Create all the dashboard resources, with the following command:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/dashboard-with-token.yaml
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Obtaining the token
|
||||
|
||||
- The manifest creates a ServiceAccount
|
||||
|
||||
- Kubernetes will automatically generate a token for that ServiceAccount
|
||||
|
||||
.exercise[
|
||||
|
||||
- Display the token:
|
||||
```bash
|
||||
kubectl --namespace=kubernetes-dashboard \
|
||||
describe secret cluster-admin-token
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
The token should start with `eyJ...` (it's a JSON Web Token).
|
||||
|
||||
Note that the secret name will actually be `cluster-admin-token-xxxxx`.
|
||||
<br/>
|
||||
(But `kubectl` prefix matches are great!)
|
||||
|
||||
---
|
||||
|
||||
## Connecting to the dashboard
|
||||
|
||||
.exercise[
|
||||
|
||||
- Check which port the dashboard is on:
|
||||
```bash
|
||||
kubectl get svc --namespace=kubernetes-dashboard
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
You'll want the `3xxxx` port.
|
||||
|
||||
|
||||
.exercise[
|
||||
|
||||
- Connect to http://oneofournodes:3xxxx/
|
||||
|
||||
<!-- ```open http://node1:3xxxx/``` -->
|
||||
|
||||
]
|
||||
|
||||
The dashboard will then ask you which authentication you want to use.
|
||||
|
||||
---
|
||||
|
||||
## Dashboard authentication
|
||||
|
||||
- Select "token" authentication
|
||||
|
||||
- Copy paste the token (starting with `eyJ...`) obtained earlier
|
||||
|
||||
- We're logged in!
|
||||
|
||||
---
|
||||
|
||||
## Other dashboards
|
||||
|
||||
- [Kube Web View](https://codeberg.org/hjacobs/kube-web-view)
|
||||
@@ -115,7 +241,7 @@ The dashboard will then ask you which authentication you want to use.
|
||||
|
||||
- see [vision and goals](https://kube-web-view.readthedocs.io/en/latest/vision.html#vision) for details
|
||||
|
||||
- [Kube Ops View](https://github.com/hjacobs/kube-ops-view)
|
||||
- [Kube Ops View](https://codeberg.org/hjacobs/kube-ops-view)
|
||||
|
||||
- "provides a common operational picture for multiple Kubernetes clusters"
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ The resulting YAML doesn't represent a valid DaemonSet.
|
||||
|
||||
- Try the same YAML file as earlier, with server-side dry run:
|
||||
```bash
|
||||
kubectl apply -f web.yaml --server-dry-run --validate=false -o yaml
|
||||
kubectl apply -f web.yaml --dry-run=server --validate=false -o yaml
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
455
slides/k8s/eck.md
Normal file
455
slides/k8s/eck.md
Normal file
@@ -0,0 +1,455 @@
|
||||
# An ElasticSearch Operator
|
||||
|
||||
- We will install [Elastic Cloud on Kubernetes](https://www.elastic.co/guide/en/cloud-on-k8s/current/k8s-quickstart.html), an ElasticSearch operator
|
||||
|
||||
- This operator requires PersistentVolumes
|
||||
|
||||
- We will install Rancher's [local path storage provisioner](https://github.com/rancher/local-path-provisioner) to automatically create these
|
||||
|
||||
- Then, we will create an ElasticSearch resource
|
||||
|
||||
- The operator will detect that resource and provision the cluster
|
||||
|
||||
- We will integrate that ElasticSearch cluster with other resources
|
||||
|
||||
(Kibana, Filebeat, Cerebro ...)
|
||||
|
||||
---
|
||||
|
||||
## Installing a Persistent Volume provisioner
|
||||
|
||||
(This step can be skipped if you already have a dynamic volume provisioner.)
|
||||
|
||||
- This provisioner creates Persistent Volumes backed by `hostPath`
|
||||
|
||||
(local directories on our nodes)
|
||||
|
||||
- It doesn't require anything special ...
|
||||
|
||||
- ... But losing a node = losing the volumes on that node!
|
||||
|
||||
.exercise[
|
||||
|
||||
- Install the local path storage provisioner:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/local-path-storage.yaml
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Making sure we have a default StorageClass
|
||||
|
||||
- The ElasticSearch operator will create StatefulSets
|
||||
|
||||
- These StatefulSets will instantiate PersistentVolumeClaims
|
||||
|
||||
- These PVCs need to be explicitly associated with a StorageClass
|
||||
|
||||
- Or we need to tag a StorageClass to be used as the default one
|
||||
|
||||
.exercise[
|
||||
|
||||
- List StorageClasses:
|
||||
```bash
|
||||
kubectl get storageclasses
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
We should see the `local-path` StorageClass.
|
||||
|
||||
---
|
||||
|
||||
## Setting a default StorageClass
|
||||
|
||||
- This is done by adding an annotation to the StorageClass:
|
||||
|
||||
`storageclass.kubernetes.io/is-default-class: true`
|
||||
|
||||
.exercise[
|
||||
|
||||
- Tag the StorageClass so that it's the default one:
|
||||
```bash
|
||||
kubectl annotate storageclass local-path \
|
||||
storageclass.kubernetes.io/is-default-class=true
|
||||
```
|
||||
|
||||
- Check the result:
|
||||
```bash
|
||||
kubectl get storageclasses
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
Now, the StorageClass should have `(default)` next to its name.
|
||||
|
||||
---
|
||||
|
||||
## Install the ElasticSearch operator
|
||||
|
||||
- The operator provides:
|
||||
|
||||
- a few CustomResourceDefinitions
|
||||
- a Namespace for its other resources
|
||||
- a ValidatingWebhookConfiguration for type checking
|
||||
- a StatefulSet for its controller and webhook code
|
||||
- a ServiceAccount, ClusterRole, ClusterRoleBinding for permissions
|
||||
|
||||
- All these resources are grouped in a convenient YAML file
|
||||
|
||||
.exercise[
|
||||
|
||||
- Install the operator:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/eck-operator.yaml
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Check our new custom resources
|
||||
|
||||
- Let's see which CRDs were created
|
||||
|
||||
.exercise[
|
||||
|
||||
- List all CRDs:
|
||||
```bash
|
||||
kubectl get crds
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
This operator supports ElasticSearch, but also Kibana and APM. Cool!
|
||||
|
||||
---
|
||||
|
||||
## Create the `eck-demo` namespace
|
||||
|
||||
- For clarity, we will create everything in a new namespace, `eck-demo`
|
||||
|
||||
- This namespace is hard-coded in the YAML files that we are going to use
|
||||
|
||||
- We need to create that namespace
|
||||
|
||||
.exercise[
|
||||
|
||||
- Create the `eck-demo` namespace:
|
||||
```bash
|
||||
kubectl create namespace eck-demo
|
||||
```
|
||||
|
||||
- Switch to that namespace:
|
||||
```bash
|
||||
kns eck-demo
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Can we use a different namespace?
|
||||
|
||||
Yes, but then we need to update all the YAML manifests that we
|
||||
are going to apply in the next slides.
|
||||
|
||||
The `eck-demo` namespace is hard-coded in these YAML manifests.
|
||||
|
||||
Why?
|
||||
|
||||
Because when defining a ClusterRoleBinding that references a
|
||||
ServiceAccount, we have to indicate in which namespace the
|
||||
ServiceAccount is located.
|
||||
|
||||
---
|
||||
|
||||
## Create an ElasticSearch resource
|
||||
|
||||
- We can now create a resource with `kind: ElasticSearch`
|
||||
|
||||
- The YAML for that resource will specify all the desired parameters:
|
||||
|
||||
- how many nodes we want
|
||||
- image to use
|
||||
- add-ons (kibana, cerebro, ...)
|
||||
- whether to use TLS or not
|
||||
- etc.
|
||||
|
||||
.exercise[
|
||||
|
||||
- Create our ElasticSearch cluster:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/eck-elasticsearch.yaml
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Operator in action
|
||||
|
||||
- Over the next minutes, the operator will create our ES cluster
|
||||
|
||||
- It will report our cluster status through the CRD
|
||||
|
||||
.exercise[
|
||||
|
||||
- Check the logs of the operator:
|
||||
```bash
|
||||
stern --namespace=elastic-system operator
|
||||
```
|
||||
|
||||
<!--
|
||||
```wait elastic-operator-0```
|
||||
```tmux split-pane -v```
|
||||
--->
|
||||
|
||||
- Watch the status of the cluster through the CRD:
|
||||
```bash
|
||||
kubectl get es -w
|
||||
```
|
||||
|
||||
<!--
|
||||
```longwait green```
|
||||
```key ^C```
|
||||
```key ^D```
|
||||
```key ^C```
|
||||
-->
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Connecting to our cluster
|
||||
|
||||
- It's not easy to use the ElasticSearch API from the shell
|
||||
|
||||
- But let's check at least if ElasticSearch is up!
|
||||
|
||||
.exercise[
|
||||
|
||||
- Get the ClusterIP of our ES instance:
|
||||
```bash
|
||||
kubectl get services
|
||||
```
|
||||
|
||||
- Issue a request with `curl`:
|
||||
```bash
|
||||
curl http://`CLUSTERIP`:9200
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
We get an authentication error. Our cluster is protected!
|
||||
|
||||
---
|
||||
|
||||
## Obtaining the credentials
|
||||
|
||||
- The operator creates a user named `elastic`
|
||||
|
||||
- It generates a random password and stores it in a Secret
|
||||
|
||||
.exercise[
|
||||
|
||||
- Extract the password:
|
||||
```bash
|
||||
kubectl get secret demo-es-elastic-user \
|
||||
-o go-template="{{ .data.elastic | base64decode }} "
|
||||
```
|
||||
|
||||
- Use it to connect to the API:
|
||||
```bash
|
||||
curl -u elastic:`PASSWORD` http://`CLUSTERIP`:9200
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
We should see a JSON payload with the `"You Know, for Search"` tagline.
|
||||
|
||||
---
|
||||
|
||||
## Sending data to the cluster
|
||||
|
||||
- Let's send some data to our brand new ElasticSearch cluster!
|
||||
|
||||
- We'll deploy a filebeat DaemonSet to collect node logs
|
||||
|
||||
.exercise[
|
||||
|
||||
- Deploy filebeat:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/eck-filebeat.yaml
|
||||
```
|
||||
|
||||
- Wait until some pods are up:
|
||||
```bash
|
||||
watch kubectl get pods -l k8s-app=filebeat
|
||||
```
|
||||
|
||||
<!--
|
||||
```wait Running```
|
||||
```key ^C```
|
||||
-->
|
||||
|
||||
- Check that a filebeat index was created:
|
||||
```bash
|
||||
curl -u elastic:`PASSWORD` http://`CLUSTERIP`:9200/_cat/indices
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Deploying an instance of Kibana
|
||||
|
||||
- Kibana can visualize the logs injected by filebeat
|
||||
|
||||
- The ECK operator can also manage Kibana
|
||||
|
||||
- Let's give it a try!
|
||||
|
||||
.exercise[
|
||||
|
||||
- Deploy a Kibana instance:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/eck-kibana.yaml
|
||||
```
|
||||
|
||||
- Wait for it to be ready:
|
||||
```bash
|
||||
kubectl get kibana -w
|
||||
```
|
||||
|
||||
<!--
|
||||
```longwait green```
|
||||
```key ^C```
|
||||
-->
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Connecting to Kibana
|
||||
|
||||
- Kibana is automatically set up to conect to ElasticSearch
|
||||
|
||||
(this is arranged by the YAML that we're using)
|
||||
|
||||
- However, it will ask for authentication
|
||||
|
||||
- It's using the same user/password as ElasticSearch
|
||||
|
||||
.exercise[
|
||||
|
||||
- Get the NodePort allocated to Kibana:
|
||||
```bash
|
||||
kubectl get services
|
||||
```
|
||||
|
||||
- Connect to it with a web browser
|
||||
|
||||
- Use the same user/password as before
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Setting up Kibana
|
||||
|
||||
After the Kibana UI loads, we need to click around a bit
|
||||
|
||||
.exercise[
|
||||
|
||||
- Pick "explore on my own"
|
||||
|
||||
- Click on Use Elasticsearch data / Connect to your Elasticsearch index"
|
||||
|
||||
- Enter `filebeat-*` for the index pattern and click "Next step"
|
||||
|
||||
- Select `@timestamp` as time filter field name
|
||||
|
||||
- Click on "discover" (the small icon looking like a compass on the left bar)
|
||||
|
||||
- Play around!
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Scaling up the cluster
|
||||
|
||||
- At this point, we have only one node
|
||||
|
||||
- We are going to scale up
|
||||
|
||||
- But first, we'll deploy Cerebro, an UI for ElasticSearch
|
||||
|
||||
- This will let us see the state of the cluster, how indexes are sharded, etc.
|
||||
|
||||
---
|
||||
|
||||
## Deploying Cerebro
|
||||
|
||||
- Cerebro is stateless, so it's fairly easy to deploy
|
||||
|
||||
(one Deployment + one Service)
|
||||
|
||||
- However, it needs the address and credentials for ElasticSearch
|
||||
|
||||
- We prepared yet another manifest for that!
|
||||
|
||||
.exercise[
|
||||
|
||||
- Deploy Cerebro:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/eck-cerebro.yaml
|
||||
```
|
||||
|
||||
- Lookup the NodePort number and connect to it:
|
||||
```bash
|
||||
kubectl get services
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Scaling up the cluster
|
||||
|
||||
- We can see on Cerebro that the cluster is "yellow"
|
||||
|
||||
(because our index is not replicated)
|
||||
|
||||
- Let's change that!
|
||||
|
||||
.exercise[
|
||||
|
||||
- Edit the ElasticSearch cluster manifest:
|
||||
```bash
|
||||
kubectl edit es demo
|
||||
```
|
||||
|
||||
- Find the field `count: 1` and change it to 3
|
||||
|
||||
- Save and quit
|
||||
|
||||
<!--
|
||||
```wait Please edit```
|
||||
```keys /count:```
|
||||
```key ^J```
|
||||
```keys $r3:x```
|
||||
```key ^J```
|
||||
-->
|
||||
|
||||
]
|
||||
|
||||
???
|
||||
|
||||
:EN:- Deploying ElasticSearch with ECK
|
||||
:FR:- Déployer ElasticSearch avec ECK
|
||||
152
slides/k8s/events.md
Normal file
152
slides/k8s/events.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Events
|
||||
|
||||
- Kubernetes has an internal structured log of *events*
|
||||
|
||||
- These events are ordinary resources:
|
||||
|
||||
- we can view them with `kubectl get events`
|
||||
|
||||
- they can be viewed and created through the Kubernetes API
|
||||
|
||||
- they are stored in Kubernetes default database (e.g. etcd)
|
||||
|
||||
- Most components will generate events to let us know what's going on
|
||||
|
||||
- Events can be *related* to other resources
|
||||
|
||||
---
|
||||
|
||||
## Reading events
|
||||
|
||||
- `kubectl get events` (or `kubectl get ev`)
|
||||
|
||||
- Can use `--watch`
|
||||
|
||||
⚠️ Looks like `tail -f`, but events aren't necessarily sorted!
|
||||
|
||||
- Can use `--all-namespaces`
|
||||
|
||||
- Cluster events (e.g. related to nodes) are in the `default` namespace
|
||||
|
||||
- Viewing all "non-normal" events:
|
||||
```bash
|
||||
kubectl get ev -A --field-selector=type!=Normal
|
||||
```
|
||||
|
||||
(as of Kubernetes 1.19, `type` can be either `Normal` or `Warning`)
|
||||
|
||||
---
|
||||
|
||||
## Reading events (take 2)
|
||||
|
||||
- When we use `kubectl describe` on an object, `kubectl` retrieves the associated events
|
||||
|
||||
.exercise[
|
||||
|
||||
- See the API requests happening when we use `kubectl describe`:
|
||||
```bash
|
||||
kubectl describe service kubernetes --namespace=default -v6 >/dev/null
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Generating events
|
||||
|
||||
- This is rarely (if ever) done manually
|
||||
|
||||
(i.e. by crafting some YAML)
|
||||
|
||||
- But controllers (e.g. operators) need this!
|
||||
|
||||
- It's not mandatory, but it helps with *operability*
|
||||
|
||||
(e.g. when we `kubectl describe` a CRD, we will see associated events)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Work in progress
|
||||
|
||||
- "Events" can be :
|
||||
|
||||
- "old-style" events (in core API group, aka `v1`)
|
||||
|
||||
- "new-style" events (in API group `events.k8s.io`)
|
||||
|
||||
- See [KEP 383](https://github.com/kubernetes/enhancements/blob/master/keps/sig-instrumentation/383-new-event-api-ga-graduation/README.md) in particular this [comparison between old and new APIs](https://github.com/kubernetes/enhancements/blob/master/keps/sig-instrumentation/383-new-event-api-ga-graduation/README.md#comparison-between-old-and-new-apis)
|
||||
|
||||
---
|
||||
|
||||
## Experimenting with events
|
||||
|
||||
- Let's create an event related to a Node, based on @@LINK[k8s/event-node.yaml]
|
||||
|
||||
.exercise[
|
||||
|
||||
- Edit `k8s/event-node.yaml`
|
||||
|
||||
- Update the `name` and `uid` of the `involvedObject`
|
||||
|
||||
- Create the event with `kubectl create -f`
|
||||
|
||||
- Look at the Node with `kubectl describe`
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Experimenting with events
|
||||
|
||||
- Let's create an event related to a Pod, based on @@LINK[k8s/event-pod.yaml]
|
||||
|
||||
.exercise[
|
||||
|
||||
- Create a pod
|
||||
|
||||
- Edit `k8s/event-pod.yaml`
|
||||
|
||||
- Edit the `involvedObject` section (don't forget the `uid`)
|
||||
|
||||
- Create the event with `kubectl create -f`
|
||||
|
||||
- Look at the Pod with `kubectl describe`
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Generating events in practice
|
||||
|
||||
- In Go, use an `EventRecorder` provided by the `kubernetes/client-go` library
|
||||
|
||||
- [EventRecorder interface](https://github.com/kubernetes/client-go/blob/release-1.19/tools/record/event.go#L87)
|
||||
|
||||
- [kubebuilder book example](https://book-v1.book.kubebuilder.io/beyond_basics/creating_events.html)
|
||||
|
||||
- It will take care of formatting / aggregating events
|
||||
|
||||
- To get an idea of what to put in the `reason` field, check [kubelet events](
|
||||
https://github.com/kubernetes/kubernetes/blob/release-1.19/pkg/kubelet/events/event.go)
|
||||
|
||||
---
|
||||
|
||||
## Cluster operator perspective
|
||||
|
||||
- Events are kept 1 hour by default
|
||||
|
||||
- This can be changed with the `--event-ttl` flag on the API server
|
||||
|
||||
- On very busy clusters, events can be kept on a separate etcd cluster
|
||||
|
||||
- This is done with the `--etcd-servers-overrides` flag on the API server
|
||||
|
||||
- Example:
|
||||
```
|
||||
--etcd-servers-overrides=/events#http://127.0.0.1:12379
|
||||
```
|
||||
|
||||
???
|
||||
|
||||
:EN:- Consuming and generating cluster events
|
||||
:FR:- Suivre l'activité du cluster avec les *events*
|
||||
@@ -4,224 +4,133 @@ There are multiple ways to extend the Kubernetes API.
|
||||
|
||||
We are going to cover:
|
||||
|
||||
- Controllers
|
||||
|
||||
- Dynamic Admission Webhooks
|
||||
|
||||
- Custom Resource Definitions (CRDs)
|
||||
|
||||
- Admission Webhooks
|
||||
|
||||
- The Aggregation Layer
|
||||
|
||||
But first, let's re(re)visit the API server ...
|
||||
|
||||
---
|
||||
|
||||
## Revisiting the API server
|
||||
|
||||
- The Kubernetes API server is a central point of the control plane
|
||||
|
||||
(everything connects to it: controller manager, scheduler, kubelets)
|
||||
- Everything connects to the API server:
|
||||
|
||||
- Almost everything in Kubernetes is materialized by a resource
|
||||
- users (that's us, but also automation like CI/CD)
|
||||
|
||||
- Resources have a type (or "kind")
|
||||
- kubelets
|
||||
|
||||
(similar to strongly typed languages)
|
||||
- network components (e.g. `kube-proxy`, pod network, NPC)
|
||||
|
||||
- We can see existing types with `kubectl api-resources`
|
||||
|
||||
- We can list resources of a given type with `kubectl get <type>`
|
||||
- controllers; lots of controllers
|
||||
|
||||
---
|
||||
|
||||
## Creating new types
|
||||
## Some controllers
|
||||
|
||||
- We can create new types with Custom Resource Definitions (CRDs)
|
||||
- `kube-controller-manager` runs built-on controllers
|
||||
|
||||
- CRDs are created dynamically
|
||||
(watching Deployments, Nodes, ReplicaSets, and much more)
|
||||
|
||||
(without recompiling or restarting the API server)
|
||||
- `kube-scheduler` runs the scheduler
|
||||
|
||||
- CRDs themselves are resources:
|
||||
(it's conceptually not different from another controller)
|
||||
|
||||
- we can create a new type with `kubectl create` and some YAML
|
||||
- `cloud-controller-manager` takes care of "cloud stuff"
|
||||
|
||||
- we can see all our custom types with `kubectl get crds`
|
||||
(e.g. provisioning load balancers, persistent volumes...)
|
||||
|
||||
- After we create a CRD, the new type works just like built-in types
|
||||
- Some components mentioned above are also controllers
|
||||
|
||||
(e.g. Network Policy Controller)
|
||||
|
||||
---
|
||||
|
||||
## A very simple CRD
|
||||
## More controllers
|
||||
|
||||
The YAML below describes a very simple CRD representing different kinds of coffee:
|
||||
- Cloud resources can also be managed by additional controllers
|
||||
|
||||
```yaml
|
||||
apiVersion: apiextensions.k8s.io/v1alpha1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: coffees.container.training
|
||||
spec:
|
||||
group: container.training
|
||||
version: v1alpha1
|
||||
scope: Namespaced
|
||||
names:
|
||||
plural: coffees
|
||||
singular: coffee
|
||||
kind: Coffee
|
||||
shortNames:
|
||||
- cof
|
||||
```
|
||||
(e.g. the [AWS Load Balancer Controller](https://github.com/kubernetes-sigs/aws-load-balancer-controller))
|
||||
|
||||
- Leveraging Ingress resources requires an Ingress Controller
|
||||
|
||||
(many options available here; we can even install multiple ones!)
|
||||
|
||||
- Many add-ons (including CRDs and operators) have controllers as well
|
||||
|
||||
🤔 *What's even a controller ?!?*
|
||||
|
||||
---
|
||||
|
||||
## Creating a CRD
|
||||
## What's a controller?
|
||||
|
||||
- Let's create the Custom Resource Definition for our Coffee resource
|
||||
According to the [documentation](https://kubernetes.io/docs/concepts/architecture/controller/):
|
||||
|
||||
.exercise[
|
||||
*Controllers are **control loops** that<br/>
|
||||
**watch** the state of your cluster,<br/>
|
||||
then make or request changes where needed.*
|
||||
|
||||
- Load the CRD:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/coffee-1.yaml
|
||||
```
|
||||
|
||||
- Confirm that it shows up:
|
||||
```bash
|
||||
kubectl get crds
|
||||
```
|
||||
|
||||
]
|
||||
*Each controller tries to move the current cluster state closer to the desired state.*
|
||||
|
||||
---
|
||||
|
||||
## Creating custom resources
|
||||
## What controllers do
|
||||
|
||||
The YAML below defines a resource using the CRD that we just created:
|
||||
- Watch resources
|
||||
|
||||
```yaml
|
||||
kind: Coffee
|
||||
apiVersion: container.training/v1alpha1
|
||||
metadata:
|
||||
name: arabica
|
||||
spec:
|
||||
taste: strong
|
||||
```
|
||||
- Make changes:
|
||||
|
||||
.exercise[
|
||||
- purely at the API level (e.g. Deployment, ReplicaSet controllers)
|
||||
|
||||
- Create a few types of coffee beans:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/coffees.yaml
|
||||
```
|
||||
- and/or configure resources (e.g. `kube-proxy`)
|
||||
|
||||
]
|
||||
- and/or provision resources (e.g. load balancer controller)
|
||||
|
||||
---
|
||||
|
||||
## Viewing custom resources
|
||||
## Extending Kubernetes with controllers
|
||||
|
||||
- By default, `kubectl get` only shows name and age of custom resources
|
||||
- Random example:
|
||||
|
||||
.exercise[
|
||||
- watch resources like Deployments, Services ...
|
||||
|
||||
- View the coffee beans that we just created:
|
||||
```bash
|
||||
kubectl get coffees
|
||||
```
|
||||
- read annotations to configure monitoring
|
||||
|
||||
]
|
||||
- Technically, this is not extending the API
|
||||
|
||||
- We can improve that, but it's outside the scope of this section!
|
||||
(but it can still be very useful!)
|
||||
|
||||
---
|
||||
|
||||
## What can we do with CRDs?
|
||||
## Other ways to extend Kubernetes
|
||||
|
||||
There are many possibilities!
|
||||
- Prevent or alter API requests before resources are committed to storage:
|
||||
|
||||
- *Operators* encapsulate complex sets of resources
|
||||
*Admission Control*
|
||||
|
||||
(e.g.: a PostgreSQL replicated cluster; an etcd cluster...
|
||||
<br/>
|
||||
see [awesome operators](https://github.com/operator-framework/awesome-operators) and
|
||||
[OperatorHub](https://operatorhub.io/) to find more)
|
||||
- Create new resource types leveraging Kubernetes storage facilities:
|
||||
|
||||
- Custom use-cases like [gitkube](https://gitkube.sh/)
|
||||
*Custom Resource Definitions*
|
||||
|
||||
- creates a new custom type, `Remote`, exposing a git+ssh server
|
||||
- Create new resource types with different storage or different semantics:
|
||||
|
||||
- deploy by pushing YAML or Helm charts to that remote
|
||||
*Aggregation Layer*
|
||||
|
||||
- Replacing built-in types with CRDs
|
||||
- Spoiler alert: often, we will combine multiple techniques
|
||||
|
||||
(see [this lightning talk by Tim Hockin](https://www.youtube.com/watch?v=ji0FWzFwNhA))
|
||||
|
||||
---
|
||||
|
||||
## Little details
|
||||
|
||||
- By default, CRDs are not *validated*
|
||||
|
||||
(we can put anything we want in the `spec`)
|
||||
|
||||
- When creating a CRD, we can pass an OpenAPI v3 schema (BETA!)
|
||||
|
||||
(which will then be used to validate resources)
|
||||
|
||||
- Generally, when creating a CRD, we also want to run a *controller*
|
||||
|
||||
(otherwise nothing will happen when we create resources of that type)
|
||||
|
||||
- The controller will typically *watch* our custom resources
|
||||
|
||||
(and take action when they are created/updated)
|
||||
|
||||
*
|
||||
Examples:
|
||||
[YAML to install the gitkube CRD](https://storage.googleapis.com/gitkube/gitkube-setup-stable.yaml),
|
||||
[YAML to install a redis operator CRD](https://github.com/amaizfinance/redis-operator/blob/master/deploy/crds/k8s_v1alpha1_redis_crd.yaml)
|
||||
*
|
||||
|
||||
---
|
||||
|
||||
## (Ab)using the API server
|
||||
|
||||
- If we need to store something "safely" (as in: in etcd), we can use CRDs
|
||||
|
||||
- This gives us primitives to read/write/list objects (and optionally validate them)
|
||||
|
||||
- The Kubernetes API server can run on its own
|
||||
|
||||
(without the scheduler, controller manager, and kubelets)
|
||||
|
||||
- By loading CRDs, we can have it manage totally different objects
|
||||
|
||||
(unrelated to containers, clusters, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Service catalog
|
||||
|
||||
- *Service catalog* is another extension mechanism
|
||||
|
||||
- It's not extending the Kubernetes API strictly speaking
|
||||
|
||||
(but it still provides new features!)
|
||||
|
||||
- It doesn't create new types; it uses:
|
||||
|
||||
- ClusterServiceBroker
|
||||
- ClusterServiceClass
|
||||
- ClusterServicePlan
|
||||
- ServiceInstance
|
||||
- ServiceBinding
|
||||
|
||||
- It uses the Open service broker API
|
||||
(and involve controllers as well!)
|
||||
|
||||
---
|
||||
|
||||
## Admission controllers
|
||||
|
||||
- Admission controllers are another way to extend the Kubernetes API
|
||||
|
||||
- Instead of creating new types, admission controllers can transform or vet API requests
|
||||
- Admission controllers can vet or transform API requests
|
||||
|
||||
- The diagram on the next slide shows the path of an API request
|
||||
|
||||
@@ -273,9 +182,9 @@ class: extra-details
|
||||
|
||||
---
|
||||
|
||||
## Admission Webhooks
|
||||
## Dynamic Admission Control
|
||||
|
||||
- We can setup *admission webhooks* to extend the behavior of the API server
|
||||
- We can set up *admission webhooks* to extend the behavior of the API server
|
||||
|
||||
- The API server will submit incoming API requests to these webhooks
|
||||
|
||||
@@ -307,6 +216,77 @@ class: extra-details
|
||||
|
||||
(to avoid e.g. triggering webhooks when setting up webhooks)
|
||||
|
||||
- The webhook server can be hosted in or out of the cluster
|
||||
|
||||
---
|
||||
|
||||
## Dynamic Admission Examples
|
||||
|
||||
- Policy control
|
||||
|
||||
([Kyverno](https://kyverno.io/),
|
||||
[Open Policy Agent](https://www.openpolicyagent.org/docs/latest/))
|
||||
|
||||
- Sidecar injection
|
||||
|
||||
(Used by some service meshes)
|
||||
|
||||
- Type validation
|
||||
|
||||
(More on this later, in the CRD section)
|
||||
|
||||
---
|
||||
|
||||
## Kubernetes API types
|
||||
|
||||
- Almost everything in Kubernetes is materialized by a resource
|
||||
|
||||
- Resources have a type (or "kind")
|
||||
|
||||
(similar to strongly typed languages)
|
||||
|
||||
- We can see existing types with `kubectl api-resources`
|
||||
|
||||
- We can list resources of a given type with `kubectl get <type>`
|
||||
|
||||
---
|
||||
|
||||
## Creating new types
|
||||
|
||||
- We can create new types with Custom Resource Definitions (CRDs)
|
||||
|
||||
- CRDs are created dynamically
|
||||
|
||||
(without recompiling or restarting the API server)
|
||||
|
||||
- CRDs themselves are resources:
|
||||
|
||||
- we can create a new type with `kubectl create` and some YAML
|
||||
|
||||
- we can see all our custom types with `kubectl get crds`
|
||||
|
||||
- After we create a CRD, the new type works just like built-in types
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
- Representing composite resources
|
||||
|
||||
(e.g. clusters like databases, messages queues ...)
|
||||
|
||||
- Representing external resources
|
||||
|
||||
(e.g. virtual machines, object store buckets, domain names ...)
|
||||
|
||||
- Representing configuration for controllers and operators
|
||||
|
||||
(e.g. custom Ingress resources, certificate issuers, backups ...)
|
||||
|
||||
- Alternate representations of other objects; services and service instances
|
||||
|
||||
(e.g. encrypted secret, git endpoints ...)
|
||||
|
||||
---
|
||||
|
||||
## The aggregation layer
|
||||
@@ -325,9 +305,57 @@ class: extra-details
|
||||
|
||||
- Example: `metrics-server`
|
||||
|
||||
(storing live metrics in etcd would be extremely inefficient)
|
||||
---
|
||||
|
||||
- Requires significantly more work than CRDs!
|
||||
## Why?
|
||||
|
||||
- Using a CRD for live metrics would be extremely inefficient
|
||||
|
||||
(etcd **is not** a metrics store; write performance is way too slow)
|
||||
|
||||
- Instead, `metrics-server`:
|
||||
|
||||
- collects metrics from kubelets
|
||||
|
||||
- stores them in memory
|
||||
|
||||
- exposes them as PodMetrics and NodeMetrics (in API group metrics.k8s.io)
|
||||
|
||||
- is registered as an APIService
|
||||
|
||||
---
|
||||
|
||||
## Drawbacks
|
||||
|
||||
- Requires a server
|
||||
|
||||
- ... that implements a non-trivial API (aka the Kubernetes API semantics)
|
||||
|
||||
- If we need REST semantics, CRDs are probably way simpler
|
||||
|
||||
- *Sometimes* synchronizing external state with CRDs might do the trick
|
||||
|
||||
(unless we want the external state to be our single source of truth)
|
||||
|
||||
---
|
||||
|
||||
## Service catalog
|
||||
|
||||
- *Service catalog* is another extension mechanism
|
||||
|
||||
- It's not extending the Kubernetes API strictly speaking
|
||||
|
||||
(but it still provides new features!)
|
||||
|
||||
- It doesn't create new types; it uses:
|
||||
|
||||
- ClusterServiceBroker
|
||||
- ClusterServiceClass
|
||||
- ClusterServicePlan
|
||||
- ServiceInstance
|
||||
- ServiceBinding
|
||||
|
||||
- It uses the Open service broker API
|
||||
|
||||
---
|
||||
|
||||
@@ -347,11 +375,5 @@ class: extra-details
|
||||
|
||||
???
|
||||
|
||||
:EN:- Extending the Kubernetes API
|
||||
:EN:- Custom Resource Definitions (CRDs)
|
||||
:EN:- The aggregation layer
|
||||
:EN:- Admission control and webhooks
|
||||
|
||||
:EN:- Overview of Kubernetes API extensions
|
||||
:FR:- Comment étendre l'API Kubernetes
|
||||
:FR:- Les CRDs *(Custom Resource Definitions)*
|
||||
:FR:- Extension via *aggregation layer*, *admission control*, *webhooks*
|
||||
|
||||
230
slides/k8s/finalizers.md
Normal file
230
slides/k8s/finalizers.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# Finalizers
|
||||
|
||||
- Sometimes, we.red[¹] want to prevent a resource from being deleted:
|
||||
|
||||
- perhaps it's "precious" (holds important data)
|
||||
|
||||
- perhaps other resources depend on it (and should be deleted first)
|
||||
|
||||
- perhaps we need to perform some clean up before it's deleted
|
||||
|
||||
- *Finalizers* are a way to do that!
|
||||
|
||||
.footnote[.red[¹]The "we" in that sentence generally stands for a controller.
|
||||
<br/>(We can also use finalizers directly ourselves, but it's not very common.)]
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
- Prevent deletion of a PersistentVolumeClaim which is used by a Pod
|
||||
|
||||
- Prevent deletion of a PersistentVolume which is bound to a PersistentVolumeClaim
|
||||
|
||||
- Prevent deletion of a Namespace that still contains objects
|
||||
|
||||
- When a LoadBalancer Service is deleted, make sure that the corresponding external resource (e.g. NLB, GLB, etc.) gets deleted.red[¹]
|
||||
|
||||
- When a CRD gets deleted, make sure that all the associated resources get deleted.red[²]
|
||||
|
||||
.footnote[.red[¹²]Finalizers are not the only solution for these use-cases.]
|
||||
|
||||
---
|
||||
|
||||
## How do they work?
|
||||
|
||||
- Each resource can have list of `finalizers` in its `metadata`, e.g.:
|
||||
|
||||
```yaml
|
||||
kind: PersistentVolumeClaim
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: my-pvc
|
||||
annotations: ...
|
||||
finalizers:
|
||||
- kubernetes.io/pvc-protection
|
||||
```
|
||||
|
||||
- If we try to delete an resource that has at least one finalizer:
|
||||
|
||||
- the resource is *not* deleted
|
||||
|
||||
- instead, its `deletionTimestamp` is set to the current time
|
||||
|
||||
- we are merely *marking the resource for deletion*
|
||||
|
||||
---
|
||||
|
||||
## What happens next?
|
||||
|
||||
- The controller that added the finalizer is supposed to:
|
||||
|
||||
- watch for resources with a `deletionTimestamp`
|
||||
|
||||
- execute necessary clean-up actions
|
||||
|
||||
- then remove the finalizer
|
||||
|
||||
- The resource is deleted once all the finalizers have been removed
|
||||
|
||||
(there is no timeout, so this could take forever)
|
||||
|
||||
- Until then, the resource can be used normally
|
||||
|
||||
(but no further finalizer can be *added* to the resource)
|
||||
|
||||
---
|
||||
|
||||
## Finalizers in review
|
||||
|
||||
Let's review the examples mentioned earlier.
|
||||
|
||||
For each of them, we'll see if there are other (perhaps better) options.
|
||||
|
||||
---
|
||||
|
||||
## Volume finalizer
|
||||
|
||||
- Kubernetes applies the following finalizers:
|
||||
|
||||
- `kubernetes.io/pvc-protection` on PersistentVolumeClaims
|
||||
|
||||
- `kubernetes.io/pv-protection` on PersistentVolumes
|
||||
|
||||
- This prevents removing them when they are in use
|
||||
|
||||
- Implementation detail: the finalizer is present *even when the resource is not in use*
|
||||
|
||||
- When the resource is ~~deleted~~ marked for deletion, the controller will check if the finalizer can be removed
|
||||
|
||||
(Perhaps to avoid race conditions?)
|
||||
|
||||
---
|
||||
|
||||
## Namespace finalizer
|
||||
|
||||
- Kubernetes applies a finalizer named `kubernetes`
|
||||
|
||||
- It prevents removing the namespace if it still contains objects
|
||||
|
||||
- *Can we remove the namespace anyway?*
|
||||
|
||||
- remove the finalizer
|
||||
|
||||
- delete the namespace
|
||||
|
||||
- force deletion
|
||||
|
||||
- It *seems to works* but, in fact, the objects in the namespace still exist
|
||||
|
||||
(and they will re-appear if we re-create the namespace)
|
||||
|
||||
See [this blog post](https://www.openshift.com/blog/the-hidden-dangers-of-terminating-namespaces) for more details about this.
|
||||
|
||||
---
|
||||
|
||||
## LoadBalancer finalizer
|
||||
|
||||
- Scenario:
|
||||
|
||||
We run a custom controller to implement provisioning of LoadBalancer Services.
|
||||
|
||||
When a Service with type=LoadBalancer is deleted, we want to make sure
|
||||
that the corresponding external resources are properly deleted.
|
||||
|
||||
- Rationale for using a finalizer:
|
||||
|
||||
Normally, we would watch and observe the deletion of the Service;
|
||||
but if the Service is deleted while our controller is down,
|
||||
we could "miss" the deletion and forget to clean up the external resource.
|
||||
|
||||
The finalizer ensures that we will "see" the deletion
|
||||
and clean up the external resource.
|
||||
|
||||
---
|
||||
|
||||
## Counterpoint
|
||||
|
||||
- We could also:
|
||||
|
||||
- Tag the external resources
|
||||
<br/>(to indicate which Kubernetes Service they correspond to)
|
||||
|
||||
- Periodically reconcile them against Kubernetes resources
|
||||
|
||||
- If a Kubernetes resource does no longer exist, delete the external resource
|
||||
|
||||
- This doesn't have to be a *pre-delete* hook
|
||||
|
||||
(unless we store important information in the Service, e.g. as annotations)
|
||||
|
||||
---
|
||||
|
||||
## CRD finalizer
|
||||
|
||||
- Scenario:
|
||||
|
||||
We have a CRD that represents a PostgreSQL cluster.
|
||||
|
||||
It provisions StatefulSets, Deployments, Services, Secrets, ConfigMaps.
|
||||
|
||||
When the CRD is deleted, we want to delete all these resources.
|
||||
|
||||
- Rationale for using a finalizer:
|
||||
|
||||
Same as previously; we could observe the CRD, but if it is deleted
|
||||
while the controller isn't running, we would miss the deletion,
|
||||
and the other resources would keep running.
|
||||
|
||||
---
|
||||
|
||||
## Counterpoint
|
||||
|
||||
- We could use the same technique as described before
|
||||
|
||||
(tag the resources with e.g. annotations, to associate them with the CRD)
|
||||
|
||||
- Even better: we could use `ownerReferences`
|
||||
|
||||
(this feature is *specifically* designed for that use-case!)
|
||||
|
||||
---
|
||||
|
||||
## CRD finalizer (take two)
|
||||
|
||||
- Scenario:
|
||||
|
||||
We have a CRD that represents a PostgreSQL cluster.
|
||||
|
||||
It provisions StatefulSets, Deployments, Services, Secrets, ConfigMaps.
|
||||
|
||||
When the CRD is deleted, we want to delete all these resources.
|
||||
|
||||
We also want to store a final backup of the database.
|
||||
|
||||
We also want to update final usage metrics (e.g. for billing purposes).
|
||||
|
||||
- Rationale for using a finalizer:
|
||||
|
||||
We need to take some actions *before* the resources get deleted, not *after*.
|
||||
|
||||
---
|
||||
|
||||
## Wrapping up
|
||||
|
||||
- Finalizers are a great way to:
|
||||
|
||||
- prevent deletion of a resource that is still in use
|
||||
|
||||
- have a "guaranteed" pre-delete hook
|
||||
|
||||
- They can also be (ab)used for other purposes
|
||||
|
||||
- Code spelunking exercise:
|
||||
|
||||
*check where finalizers are used in the Kubernetes code base and why!*
|
||||
|
||||
???
|
||||
|
||||
:EN:- Using "finalizers" to manage resource lifecycle
|
||||
:FR:- Gérer le cycle de vie des ressources avec les *finalizers*
|
||||
659
slides/k8s/hpa-v2.md
Normal file
659
slides/k8s/hpa-v2.md
Normal file
@@ -0,0 +1,659 @@
|
||||
# Scaling with custom metrics
|
||||
|
||||
- The HorizontalPodAutoscaler v1 can only scale on Pod CPU usage
|
||||
|
||||
- Sometimes, we need to scale using other metrics:
|
||||
|
||||
- memory
|
||||
|
||||
- requests per second
|
||||
|
||||
- latency
|
||||
|
||||
- active sessions
|
||||
|
||||
- items in a work queue
|
||||
|
||||
- ...
|
||||
|
||||
- The HorizontalPodAutoscaler v2 can do it!
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
⚠️ Autoscaling on custom metrics is fairly complex!
|
||||
|
||||
- We need some metrics system
|
||||
|
||||
(Prometheus is a popular option, but others are possible too)
|
||||
|
||||
- We need our metrics (latency, traffic...) to be fed in the system
|
||||
|
||||
(with Prometheus, this might require a custom exporter)
|
||||
|
||||
- We need to expose these metrics to Kubernetes
|
||||
|
||||
(Kubernetes doesn't "speak" the Prometheus API)
|
||||
|
||||
- Then we can set up autoscaling!
|
||||
|
||||
---
|
||||
|
||||
## The plan
|
||||
|
||||
- We will deploy the DockerCoins demo app
|
||||
|
||||
(one of its components has a bottleneck; its latency will increase under load)
|
||||
|
||||
- We will use Prometheus to collect and store metrics
|
||||
|
||||
- We will deploy a tiny HTTP latency monitor (a Prometheus *exporter*)
|
||||
|
||||
- We will deploy the "Prometheus adapter"
|
||||
|
||||
(mapping Prometheus metrics to Kubernetes-compatible metrics)
|
||||
|
||||
- We will create an HorizontalPodAutoscaler 🎉
|
||||
|
||||
---
|
||||
|
||||
## Deploying DockerCoins
|
||||
|
||||
- That's the easy part!
|
||||
|
||||
.exercise[
|
||||
|
||||
- Create a new namespace and switch to it:
|
||||
```bash
|
||||
kubectl create namespace customscaling
|
||||
kns customscaling
|
||||
```
|
||||
|
||||
- Deploy DockerCoins, and scale up the `worker` Deployment:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8/dockercoins.yaml
|
||||
kubectl scale deployment worker --replicas=10
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Current state of affairs
|
||||
|
||||
- The `rng` service is a bottleneck
|
||||
|
||||
(it cannot handle more than 10 requests/second)
|
||||
|
||||
- With enough traffic, its latency increases
|
||||
|
||||
(by about 100ms per `worker` Pod after the 3rd worker)
|
||||
|
||||
.exercise[
|
||||
|
||||
- Check the `webui` port and open it in your browser:
|
||||
```bash
|
||||
kubectl get service webui
|
||||
```
|
||||
|
||||
- Check the `rng` ClusterIP and test it with e.g. `httping`:
|
||||
```bash
|
||||
kubectl get service rng
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Measuring latency
|
||||
|
||||
- We will use a tiny custom Prometheus exporter, [httplat](https://github.com/jpetazzo/httplat)
|
||||
|
||||
- `httplat` exposes Prometheus metrics on port 9080 (by default)
|
||||
|
||||
- It monitors exactly one URL, that must be passed as a command-line argument
|
||||
|
||||
.exercise[
|
||||
|
||||
- Deploy `httplat`:
|
||||
```bash
|
||||
kubectl create deployment httplat -- httplat http://rng/
|
||||
```
|
||||
|
||||
- Expose it:
|
||||
```bash
|
||||
kubectl expose deployment httplat --port=9080
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Measuring latency in the real world
|
||||
|
||||
- We are using this tiny custom exporter for simplicity
|
||||
|
||||
- A more common method to collect latency is to use a service mesh
|
||||
|
||||
- A service mesh can usually collect latency for *all* services automatically
|
||||
|
||||
---
|
||||
|
||||
## Install Prometheus
|
||||
|
||||
- We will use the Prometheus community Helm chart
|
||||
|
||||
(because we can configure it dynamically with annotations)
|
||||
|
||||
.exercise[
|
||||
|
||||
- If it's not installed yet on the cluster, install Prometheus:
|
||||
```bash
|
||||
helm repo add prometheus-community
|
||||
https://prometheus-community.github.io/helm-charts
|
||||
helm upgrade prometheus prometheus-community/prometheus \
|
||||
--install \
|
||||
--namespace kube-system \
|
||||
--set server.service.type=NodePort \
|
||||
--set server.service.nodePort=30090 \
|
||||
--set server.persistentVolume.enabled=false \
|
||||
--set alertmanager.enabled=false
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Configure Prometheus
|
||||
|
||||
- We can use annotations to tell Prometheus to collect the metrics
|
||||
|
||||
.exercise[
|
||||
|
||||
- Tell Prometheus to "scrape" our latency exporter:
|
||||
```bash
|
||||
kubectl annotate service httplat \
|
||||
prometheus.io/scrape=true \
|
||||
prometheus.io/port=9080 \
|
||||
prometheus.io/path=/metrics
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
If you deployed Prometheus differently, you might have to configure it manually.
|
||||
|
||||
You'll need to instruct it to scrape http://httplat.customscaling.svc:9080/metrics.
|
||||
|
||||
---
|
||||
|
||||
## Make sure that metrics get collected
|
||||
|
||||
- Before moving on, confirm that Prometheus has our metrics
|
||||
|
||||
.exercise[
|
||||
|
||||
- Connect to Prometheus
|
||||
|
||||
(if you installed it like instructed above, it is exposed as a NodePort on port 30090)
|
||||
|
||||
- Check that `httplat` metrics are available
|
||||
|
||||
- You can try to graph the following PromQL expression:
|
||||
```
|
||||
rate(httplat_latency_seconds_sum[2m])/rate(httplat_latency_seconds_count[2m])
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Make sure that the exporter works:
|
||||
|
||||
- get the ClusterIP of the exporter with `kubectl get svc httplat`
|
||||
|
||||
- `curl http://<ClusterIP>:9080/metrics`
|
||||
|
||||
- check that the result includes the `httplat` histogram
|
||||
|
||||
- Make sure that Prometheus is scraping the exporter:
|
||||
|
||||
- go to `Status` / `Targets` in Prometheus
|
||||
|
||||
- make sure that `httplat` shows up in there
|
||||
|
||||
---
|
||||
|
||||
## Creating the autoscaling policy
|
||||
|
||||
- We need custom YAML (we can't use the `kubectl autoscale` command)
|
||||
|
||||
- It must specify `scaleTargetRef`, the resource to scale
|
||||
|
||||
- any resource with a `scale` sub-resource will do
|
||||
|
||||
- this includes Deployment, ReplicaSet, StatefulSet...
|
||||
|
||||
- It must specify one or more `metrics` to look at
|
||||
|
||||
- if multiple metrics are given, the autoscaler will "do the math" for each one
|
||||
|
||||
- it will then keep the largest result
|
||||
|
||||
---
|
||||
|
||||
## Details about the `metrics` list
|
||||
|
||||
- Each item will look like this:
|
||||
```yaml
|
||||
- type: <TYPE-OF-METRIC>
|
||||
<TYPE-OF-METRIC>:
|
||||
metric:
|
||||
name: <NAME-OF-METRIC>
|
||||
<...optional selector (mandatory for External metrics)...>
|
||||
target:
|
||||
type: <TYPE-OF-TARGET>
|
||||
<TYPE-OF-TARGET>: <VALUE>
|
||||
<describedObject field, for Object metrics>
|
||||
```
|
||||
|
||||
`<TYPE-OF-METRIC>` can be `Resource`, `Pods`, `Object`, or `External`.
|
||||
|
||||
`<TYPE-OF-TARGET>` can be `Utilization`, `Value`, or `AverageValue`.
|
||||
|
||||
Let's explain the 4 different `<TYPE-OF-METRIC>` values!
|
||||
|
||||
---
|
||||
|
||||
## `Resource`
|
||||
|
||||
Use "classic" metrics served by `metrics-server` (`cpu` and `memory`).
|
||||
|
||||
```yaml
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 50
|
||||
```
|
||||
|
||||
Compute average *utilization* (usage/requests) across pods.
|
||||
|
||||
It's also possible to specify `Value` or `AverageValue` instead of `Utilization`.
|
||||
|
||||
(To scale according to "raw" CPU or memory usage.)
|
||||
|
||||
---
|
||||
|
||||
## `Pods`
|
||||
|
||||
Use custom metrics. These are still "per-Pod" metrics.
|
||||
|
||||
```yaml
|
||||
- type: Pods
|
||||
pods:
|
||||
metric:
|
||||
name: packets-per-second
|
||||
target:
|
||||
type: AverageValue
|
||||
averageValue: 1k
|
||||
```
|
||||
|
||||
`type:` *must* be `AverageValue`.
|
||||
|
||||
(It cannot be `Utilization`, since these can't be used in Pod `requests`.)
|
||||
|
||||
---
|
||||
|
||||
## `Object`
|
||||
|
||||
Use custom metrics. These metrics are "linked" to any arbitrary resource.
|
||||
|
||||
(E.g. a Deployment, Service, Ingress, ...)
|
||||
|
||||
```yaml
|
||||
- type: Object
|
||||
object:
|
||||
metric:
|
||||
name: requests-per-second
|
||||
describedObject:
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
name: main-route
|
||||
target:
|
||||
type: AverageValue
|
||||
value: 100
|
||||
```
|
||||
|
||||
`type:` can be `Value` or `AverageValue` (see next slide for details).
|
||||
|
||||
---
|
||||
|
||||
## `Value` vs `AverageValue`
|
||||
|
||||
- `Value`
|
||||
|
||||
- use the value as-is
|
||||
|
||||
- useful to pace a client or producer
|
||||
|
||||
- "target a specific total load on a specific endpoint or queue"
|
||||
|
||||
- `AverageValue`
|
||||
|
||||
- divide the value by the number of pods
|
||||
|
||||
- useful to scale a server or consumer
|
||||
|
||||
- "scale our systems to meet a given SLA/SLO"
|
||||
|
||||
---
|
||||
|
||||
## `External`
|
||||
|
||||
Use arbitrary metrics. The series to use is specified with a label selector.
|
||||
|
||||
```yaml
|
||||
- type: External
|
||||
external:
|
||||
metric:
|
||||
name: queue_messages_ready
|
||||
selector: "queue=worker_tasks"
|
||||
target:
|
||||
type: AverageValue
|
||||
averageValue: 30
|
||||
```
|
||||
|
||||
The `selector` will be passed along when querying the metrics API.
|
||||
|
||||
Its meaninng is implementation-dependent.
|
||||
|
||||
It may or may not correspond to Kubernetes labels.
|
||||
|
||||
---
|
||||
|
||||
## One more thing ...
|
||||
|
||||
- We can give a `behavior` set of options
|
||||
|
||||
- Indicates:
|
||||
|
||||
- how much to scale up/down in a single step
|
||||
|
||||
- a *stabilization window* to avoid hysteresis effects
|
||||
|
||||
- The default stabilization window is 15 seconds for `scaleUp`
|
||||
|
||||
(we might want to change that!)
|
||||
|
||||
---
|
||||
|
||||
Putting togeher @@LINK[k8s/hpa-v2-pa-httplat.yaml]:
|
||||
|
||||
.small[
|
||||
```yaml
|
||||
@@INCLUDE[k8s/hpa-v2-pa-httplat.yaml]
|
||||
```
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Creating the autoscaling policy
|
||||
|
||||
- We will register the policy
|
||||
|
||||
- Of course, it won't quite work yet (we're missing the *Prometheus adapter*)
|
||||
|
||||
.exercise[
|
||||
|
||||
- Create the HorizontalPodAutoscaler:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/hpa-v2-pa-httplat.yaml
|
||||
```
|
||||
|
||||
- Check the logs of the `controller-manager`:
|
||||
```bash
|
||||
stern --namespace=kube-system --tail=10 controller-manager
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
After a little while we should see messages like this:
|
||||
```
|
||||
no custom metrics API (custom.metrics.k8s.io) registered
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `custom.metrics.k8s.io`
|
||||
|
||||
- The HorizontalPodAutoscaler will get the metrics *from the Kubernetes API itself*
|
||||
|
||||
- In our specific case, it will access a resource like this one:
|
||||
.small[
|
||||
```
|
||||
/apis/custom.metrics.k8s.io/v1beta1/namespaces/customscaling/services/httplat/httplat_latency_seconds
|
||||
```
|
||||
]
|
||||
|
||||
- By default, the Kubernetes API server doesn't implement `custom.metrics.k8s.io`
|
||||
|
||||
(we can have a look at `kubectl get apiservices`)
|
||||
|
||||
- We need to:
|
||||
|
||||
- start an API service implementing this API group
|
||||
|
||||
- register it with our API server
|
||||
|
||||
---
|
||||
|
||||
## The Prometheus adapter
|
||||
|
||||
- The Prometheus adapter is an open source project:
|
||||
|
||||
https://github.com/DirectXMan12/k8s-prometheus-adapter
|
||||
|
||||
- It's a Kubernetes API service implementing API group `custom.metrics.k8s.io`
|
||||
|
||||
- It maps the requests it receives to Prometheus metrics
|
||||
|
||||
- Exactly what we need!
|
||||
|
||||
---
|
||||
|
||||
## Deploying the Prometheus adapter
|
||||
|
||||
- There is ~~an app~~ a Helm chart for that
|
||||
|
||||
.exercise[
|
||||
|
||||
- Install the Prometheus adapter:
|
||||
```bash
|
||||
helm upgrade prometheus-adapter prometheus-community/prometheus-adapter \
|
||||
--install --namespace=kube-system \
|
||||
--set prometheus.url=http://prometheus-server.kube-system.svc \
|
||||
--set prometheus.port=80
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
- It comes with some default mappings
|
||||
|
||||
- But we will need to add `httplat` to these mappings
|
||||
|
||||
---
|
||||
|
||||
## Configuring the Prometheus adapter
|
||||
|
||||
- The Prometheus adapter can be configured/customized through a ConfigMap
|
||||
|
||||
- We are going to edit that ConfigMap, then restart the adapter
|
||||
|
||||
- We need to add a rule that will say:
|
||||
|
||||
- all the metrics series named `httplat_latency_seconds_sum` ...
|
||||
|
||||
- ... belong to *Services* ...
|
||||
|
||||
- ... the name of the Service and its Namespace are indicated by the `kubernetes_name` and `kubernetes_namespace` Prometheus tags respectively ...
|
||||
|
||||
- ... and the exact value to use should be the following PromQL expression
|
||||
|
||||
---
|
||||
|
||||
## The mapping rule
|
||||
|
||||
Here is the rule that we need to add to the configuration:
|
||||
|
||||
```yaml
|
||||
- seriesQuery: |
|
||||
httplat_latency_seconds_sum{kubernetes_namespace!="",kubernetes_name!=""}
|
||||
resources:
|
||||
overrides:
|
||||
kubernetes_namespace:
|
||||
resource: namespace
|
||||
kubernetes_name:
|
||||
resource: service
|
||||
name:
|
||||
matches: "httplat_latency_seconds_sum"
|
||||
as: "httplat_latency_seconds"
|
||||
metricsQuery: |
|
||||
rate(httplat_latency_seconds_sum{<<.LabelMatchers>>}[2m])
|
||||
/rate(httplat_latency_seconds_count{<<.LabelMatchers>>}[2m])
|
||||
```
|
||||
|
||||
(I built it following the [walkthrough](https://github.com/DirectXMan12/k8s-prometheus-adapter/blob/master/docs/config-walkthrough.md
|
||||
) in the Prometheus adapter documentation.)
|
||||
|
||||
---
|
||||
|
||||
## Editing the adapter's configuration
|
||||
|
||||
.exercise[
|
||||
|
||||
- Edit the adapter's ConfigMap:
|
||||
```bash
|
||||
kubectl edit configmap prometheus-adapter --namespace=kube-system
|
||||
```
|
||||
|
||||
- Add the new rule in the `rules` section, at the end of the configuration file
|
||||
|
||||
- Save, quit
|
||||
|
||||
- Restart the Prometheus adapter:
|
||||
```bash
|
||||
kubectl rollout restart deployment --namespace=kube-system prometheus-adapter
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Witness the marvel of custom autoscaling
|
||||
|
||||
(Sort of)
|
||||
|
||||
- After a short while, the `rng` Deployment will scale up
|
||||
|
||||
- It should scale up until the latency drops below 100ms
|
||||
|
||||
(and continue to scale up a little bit more after that)
|
||||
|
||||
- Then, since the latency will be well below 100ms, it will scale down
|
||||
|
||||
- ... and back up again, etc.
|
||||
|
||||
(See pictures on next slides!)
|
||||
|
||||
---
|
||||
|
||||
class: pic
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
class: pic
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## What's going on?
|
||||
|
||||
- The autoscaler's information is slightly out of date
|
||||
|
||||
(not by much; probably between 1 and 2 minute)
|
||||
|
||||
- It's enough to cause the oscillations to happen
|
||||
|
||||
- One possible fix is to tell the autoscaler to wait a bit after each action
|
||||
|
||||
- It will reduce oscillations, but will also slow down its reaction time
|
||||
|
||||
(and therefore, how fast it reacts to a peak of traffic)
|
||||
|
||||
---
|
||||
|
||||
## What's going on? Take 2
|
||||
|
||||
- As soon as the measured latency is *significantly* below our target (100ms) ...
|
||||
|
||||
the autoscaler tries to scale down
|
||||
|
||||
- If the latency is measured at 20ms ...
|
||||
|
||||
the autoscaler will try to *divide the number of pods by five!*
|
||||
|
||||
- One possible solution: apply a formula to the measured latency,
|
||||
so that values between e.g. 10 and 100ms get very close to 100ms.
|
||||
|
||||
- Another solution: instead of targetting for a specific latency,
|
||||
target a 95th percentile latency or something similar, using
|
||||
a more advanced PromQL expression (and leveraging the fact that
|
||||
we have histograms instead of raw values).
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Check that the adapter registered itself correctly:
|
||||
```bash
|
||||
kubectl get apiservices | grep metrics
|
||||
```
|
||||
|
||||
Check that the adapter correctly serves metrics:
|
||||
```bash
|
||||
kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1
|
||||
```
|
||||
|
||||
Check that our `httplat` metrics are available:
|
||||
```bash
|
||||
kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1\
|
||||
/namespaces/coins/services/httplat/httplat_latency_seconds
|
||||
```
|
||||
|
||||
Also check the logs of the `prometheus-adapter` and the `kube-controller-manager`.
|
||||
|
||||
---
|
||||
|
||||
## Useful links
|
||||
|
||||
- [Horizontal Pod Autoscaler walkthrough](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/) in the Kubernetes documentation
|
||||
|
||||
- [Autoscaling design proposal](https://github.com/kubernetes/community/tree/master/contributors/design-proposals/autoscaling)
|
||||
|
||||
- [Kubernetes custom metrics API alternative implementations](https://github.com/kubernetes/metrics/blob/master/IMPLEMENTATIONS.md)
|
||||
|
||||
- [Prometheus adapter configuration walkthrough](https://github.com/DirectXMan12/k8s-prometheus-adapter/blob/master/docs/config-walkthrough.md)
|
||||
|
||||
???
|
||||
|
||||
:EN:- Autoscaling with custom metrics
|
||||
:FR:- Suivi de charge avancé (HPAv2)
|
||||
403
slides/k8s/ingress-tls.md
Normal file
403
slides/k8s/ingress-tls.md
Normal file
@@ -0,0 +1,403 @@
|
||||
# Ingress and TLS certificates
|
||||
|
||||
- Most ingress controllers support TLS connections
|
||||
|
||||
(in a way that is standard across controllers)
|
||||
|
||||
- The TLS key and certificate are stored in a Secret
|
||||
|
||||
- The Secret is then referenced in the Ingress resource:
|
||||
```yaml
|
||||
spec:
|
||||
tls:
|
||||
- secretName: XXX
|
||||
hosts:
|
||||
- YYY
|
||||
rules:
|
||||
- ZZZ
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Obtaining a certificate
|
||||
|
||||
- In the next section, we will need a TLS key and certificate
|
||||
|
||||
- These usually come in [PEM](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail) format:
|
||||
```
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDATCCAemg...
|
||||
...
|
||||
-----END CERTIFICATE-----
|
||||
```
|
||||
|
||||
- We will see how to generate a self-signed certificate
|
||||
|
||||
(easy, fast, but won't be recognized by web browsers)
|
||||
|
||||
- We will also see how to obtain a certificate from [Let's Encrypt](https://letsencrypt.org/)
|
||||
|
||||
(requires the cluster to be reachable through a domain name)
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## In production ...
|
||||
|
||||
- A very popular option is to use the [cert-manager](https://cert-manager.io/docs/) operator
|
||||
|
||||
- It's a flexible, modular approach to automated certificate management
|
||||
|
||||
- For simplicity, in this section, we will use [certbot](https://certbot.eff.org/)
|
||||
|
||||
- The method shown here works well for one-time certs, but lacks:
|
||||
|
||||
- automation
|
||||
|
||||
- renewal
|
||||
|
||||
---
|
||||
|
||||
## Which domain to use
|
||||
|
||||
- If you're doing this in a training:
|
||||
|
||||
*the instructor will tell you what to use*
|
||||
|
||||
- If you're doing this on your own Kubernetes cluster:
|
||||
|
||||
*you should use a domain that points to your cluster*
|
||||
|
||||
- More precisely:
|
||||
|
||||
*you should use a domain that points to your ingress controller*
|
||||
|
||||
- If you don't have a domain name, you can use [nip.io](https://nip.io/)
|
||||
|
||||
(if your ingress controller is on 1.2.3.4, you can use `whatever.1.2.3.4.nip.io`)
|
||||
|
||||
---
|
||||
|
||||
## Setting `$DOMAIN`
|
||||
|
||||
- We will use `$DOMAIN` in the following section
|
||||
|
||||
- Let's set it now
|
||||
|
||||
.exercise[
|
||||
|
||||
- Set the `DOMAIN` environment variable:
|
||||
```bash
|
||||
export DOMAIN=...
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Method 1, self-signed certificate
|
||||
|
||||
- Thanks to `openssl`, generating a self-signed cert is just one command away!
|
||||
|
||||
.exercise[
|
||||
|
||||
- Generate a key and certificate:
|
||||
```bash
|
||||
openssl req \
|
||||
-newkey rsa -nodes -keyout privkey.pem \
|
||||
-x509 -days 30 -subj /CN=$DOMAIN/ -out cert.pem
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
This will create two files, `privkey.pem` and `cert.pem`.
|
||||
|
||||
---
|
||||
|
||||
## Method 2, Let's Encrypt with certbot
|
||||
|
||||
- `certbot` is an [ACME](https://tools.ietf.org/html/rfc8555) client
|
||||
|
||||
(Automatic Certificate Management Environment)
|
||||
|
||||
- We can use it to obtain certificates from Let's Encrypt
|
||||
|
||||
- It needs to listen to port 80
|
||||
|
||||
(to complete the [HTTP-01 challenge](https://letsencrypt.org/docs/challenge-types/))
|
||||
|
||||
- If port 80 is already taken by our ingress controller, see method 3
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## HTTP-01 challenge
|
||||
|
||||
- `certbot` contacts Let's Encrypt, asking for a cert for `$DOMAIN`
|
||||
|
||||
- Let's Encrypt gives a token to `certbot`
|
||||
|
||||
- Let's Encrypt then tries to access the following URL:
|
||||
|
||||
`http://$DOMAIN/.well-known/acme-challenge/<token>`
|
||||
|
||||
- That URL needs to be routed to `certbot`
|
||||
|
||||
- Once Let's Encrypt gets the response from `certbot`, it issues the certificate
|
||||
|
||||
---
|
||||
|
||||
## Running certbot
|
||||
|
||||
- There is a very convenient container image, `certbot/certbot`
|
||||
|
||||
- Let's use a volume to get easy access to the generated key and certificate
|
||||
|
||||
.exercise[
|
||||
|
||||
- Obtain a certificate from Let's Encrypt:
|
||||
```bash
|
||||
EMAIL=your.address@example.com
|
||||
docker run --rm -p 80:80 -v $PWD/letsencrypt:/etc/letsencrypt \
|
||||
certbot/certbot certonly \
|
||||
-m $EMAIL \
|
||||
--standalone --agree-tos -n \
|
||||
--domain $DOMAIN \
|
||||
--test-cert
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
This will get us a "staging" certificate.
|
||||
Remove `--test-cert` to obtain a *real* certificate.
|
||||
|
||||
---
|
||||
|
||||
## Copying the key and certificate
|
||||
|
||||
- If everything went fine:
|
||||
|
||||
- the key and certificate files are in `letsencrypt/live/$DOMAIN`
|
||||
|
||||
- they are owned by `root`
|
||||
|
||||
.exercise[
|
||||
|
||||
- Grant ourselves permissions on these files:
|
||||
```bash
|
||||
sudo chown -R $USER letsencrypt
|
||||
```
|
||||
|
||||
- Copy the certificate and key to the current directory:
|
||||
```bash
|
||||
cp letsencrypt/live/test/{cert,privkey}.pem .
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Method 3, certbot with Ingress
|
||||
|
||||
- Sometimes, we can't simply listen to port 80:
|
||||
|
||||
- we might already have an ingress controller there
|
||||
- our nodes might be on an internal network
|
||||
|
||||
- But we can define an Ingress to route the HTTP-01 challenge to `certbot`!
|
||||
|
||||
- Our Ingress needs to route all requests to `/.well-known/acme-challenge` to `certbot`
|
||||
|
||||
- There are at least two ways to do that:
|
||||
|
||||
- run `certbot` in a Pod (and extract the cert+key when it's done)
|
||||
- run `certbot` in a container on a node (and manually route traffic to it)
|
||||
|
||||
- We're going to use the second option
|
||||
|
||||
(mostly because it will give us an excuse to tinker with Endpoints resources!)
|
||||
|
||||
---
|
||||
|
||||
## The plan
|
||||
|
||||
- We need the following resources:
|
||||
|
||||
- an Endpoints¹ listing a hard-coded IP address and port
|
||||
<br/>(where our `certbot` container will be listening)
|
||||
|
||||
- a Service corresponding to that Endpoints
|
||||
|
||||
- an Ingress sending requests to `/.well-known/acme-challenge/*` to that Service
|
||||
<br/>(we don't even need to include a domain name in it)
|
||||
|
||||
- Then we need to start `certbot` so that it's listening on the right address+port
|
||||
|
||||
.footnote[¹Endpoints is always plural, because even a single resource is a list of endpoints.]
|
||||
|
||||
---
|
||||
|
||||
## Creating resources
|
||||
|
||||
- We prepared a YAML file to create the three resources
|
||||
|
||||
- However, the Endpoints needs to be adapted to put the current node's address
|
||||
|
||||
.exercise[
|
||||
|
||||
- Edit `~/containers.training/k8s/certbot.yaml`
|
||||
|
||||
(replace `A.B.C.D` with the current node's address)
|
||||
|
||||
- Create the resources:
|
||||
```bash
|
||||
kubectl apply -f ~/containers.training/k8s/certbot.yaml
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Obtaining the certificate
|
||||
|
||||
- Now we can run `certbot`, listening on the port listed in the Endpoints
|
||||
|
||||
(i.e. 8000)
|
||||
|
||||
.exercise[
|
||||
|
||||
- Run `certbot`:
|
||||
```bash
|
||||
EMAIL=your.address@example.com
|
||||
docker run --rm -p 8000:80 -v $PWD/letsencrypt:/etc/letsencrypt \
|
||||
certbot/certbot certonly \
|
||||
-m $EMAIL \
|
||||
--standalone --agree-tos -n \
|
||||
--domain $DOMAIN \
|
||||
--test-cert
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
This is using the staging environment.
|
||||
Remove `--test-cert` to get a production certificate.
|
||||
|
||||
---
|
||||
|
||||
## Copying the certificate
|
||||
|
||||
- Just like in the previous method, the certificate is in `letsencrypt/live/$DOMAIN`
|
||||
|
||||
(and owned by root)
|
||||
|
||||
.exercise[
|
||||
|
||||
- Grand ourselves permissions on these files:
|
||||
```bash
|
||||
sudo chown -R $USER letsencrypt
|
||||
```
|
||||
|
||||
- Copy the certificate and key to the current directory:
|
||||
```bash
|
||||
cp letsencrypt/live/$DOMAIN/{cert,privkey}.pem .
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Creating the Secret
|
||||
|
||||
- We now have two files:
|
||||
|
||||
- `privkey.pem` (the private key)
|
||||
|
||||
- `cert.pem` (the certificate)
|
||||
|
||||
- We can create a Secret to hold them
|
||||
|
||||
.exercise[
|
||||
|
||||
- Create the Secret:
|
||||
```bash
|
||||
kubectl create secret tls $DOMAIN --cert=cert.pem --key=privkey.pem
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Ingress with TLS
|
||||
|
||||
- To enable TLS for an Ingress, we need to add a `tls` section to the Ingress:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
tls:
|
||||
- secretName: DOMAIN
|
||||
hosts:
|
||||
- DOMAIN
|
||||
rules: ...
|
||||
```
|
||||
|
||||
- The list of hosts will be used by the ingress controller
|
||||
|
||||
(to know which certificate to use with [SNI](https://en.wikipedia.org/wiki/Server_Name_Indication))
|
||||
|
||||
- Of course, the name of the secret can be different
|
||||
|
||||
(here, for clarity and convenience, we set it to match the domain)
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## About the ingress controller
|
||||
|
||||
- Many ingress controllers can use different "stores" for keys and certificates
|
||||
|
||||
- Our ingress controller needs to be configured to use secrets
|
||||
|
||||
(as opposed to, e.g., obtain certificates directly with Let's Encrypt)
|
||||
|
||||
---
|
||||
|
||||
## Using the certificate
|
||||
|
||||
.exercise[
|
||||
|
||||
- Edit the Ingress manifest, `~/container.training/k8s/ingress.yaml`
|
||||
|
||||
- Uncomment the `tls` section
|
||||
|
||||
- Update the `secretName` and `hosts` list
|
||||
|
||||
- Create or update the Ingress:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/ingress.yaml
|
||||
```
|
||||
|
||||
- Check that the URL now works over `https`
|
||||
|
||||
(it might take a minute to be picked up by the ingress controller)
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Discussion
|
||||
|
||||
*To repeat something mentioned earlier ...*
|
||||
|
||||
- The methods presented here are for *educational purpose only*
|
||||
|
||||
- In most production scenarios, the certificates will be obtained automatically
|
||||
|
||||
- A very popular option is to use the [cert-manager](https://cert-manager.io/docs/) operator
|
||||
|
||||
???
|
||||
|
||||
:EN:- Ingress and TLS
|
||||
:FR:- Certificats TLS et *ingress*
|
||||
@@ -485,6 +485,8 @@ spec:
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Using multiple ingress controllers
|
||||
|
||||
- You can have multiple ingress controllers active simultaneously
|
||||
@@ -495,11 +497,13 @@ spec:
|
||||
|
||||
(e.g. one for internal, another for external traffic)
|
||||
|
||||
- The `kubernetes.io/ingress.class` annotation can be used to tell which one to use
|
||||
- To indicate which ingress controller should be used by a given Ingress resouce:
|
||||
|
||||
- It's OK if multiple ingress controllers configure the same resource
|
||||
- before Kubernetes 1.18, use the `kubernetes.io/ingress.class` annotation
|
||||
|
||||
(it just means that the service will be accessible through multiple paths)
|
||||
- since Kubernetes 1.18, use the `ingressClassName` field
|
||||
<br/>
|
||||
(which should refer to an existing `IngressClass` resource)
|
||||
|
||||
---
|
||||
|
||||
@@ -535,9 +539,9 @@ spec:
|
||||
|
||||
- [ingress.kubernetes.io/rewrite-target: /](https://github.com/kubernetes/contrib/tree/master/ingress/controllers/nginx/examples/rewrite)
|
||||
|
||||
- This should eventually stabilize
|
||||
- The Ingress spec stabilized in Kubernetes 1.19 ...
|
||||
|
||||
(remember that ingresses are currently `apiVersion: networking.k8s.io/v1beta1`)
|
||||
... without specifying these features! 😭
|
||||
|
||||
---
|
||||
|
||||
@@ -582,7 +586,7 @@ spec:
|
||||
- Example 3: canary for shipping physical goods
|
||||
|
||||
- 1% of orders are shipped with the canary process
|
||||
- the reamining 99% are shipped with the normal process
|
||||
- the remaining 99% are shipped with the normal process
|
||||
|
||||
- We're going to implement example 1 (per-request routing)
|
||||
|
||||
@@ -634,7 +638,7 @@ spec:
|
||||
servicePort: 80
|
||||
- path: /
|
||||
backend:
|
||||
serviceName: wensledale
|
||||
serviceName: wensleydale
|
||||
servicePort: 80
|
||||
- path: /
|
||||
backend:
|
||||
|
||||
686
slides/k8s/kubebuilder.md
Normal file
686
slides/k8s/kubebuilder.md
Normal file
@@ -0,0 +1,686 @@
|
||||
# Kubebuilder
|
||||
|
||||
- Writing a quick and dirty operator is (relatively) easy
|
||||
|
||||
- Doing it right, however ...
|
||||
|
||||
--
|
||||
|
||||
- We need:
|
||||
|
||||
- proper CRD with schema validation
|
||||
|
||||
- controller performing a reconcilation loop
|
||||
|
||||
- manage errors, retries, dependencies between resources
|
||||
|
||||
- maybe webhooks for admission and/or conversion
|
||||
|
||||
😱
|
||||
|
||||
---
|
||||
|
||||
## Frameworks
|
||||
|
||||
- There are a few frameworks available out there:
|
||||
|
||||
- [kubebuilder](https://github.com/kubernetes-sigs/kubebuilder)
|
||||
([book](https://book.kubebuilder.io/)):
|
||||
go-centric, very close to Kubernetes' core types
|
||||
|
||||
- [operator-framework](https://operatorframework.io/):
|
||||
higher level; also supports Ansible and Helm
|
||||
|
||||
- [KUDO](https://kudo.dev/):
|
||||
declarative operators written in YAML
|
||||
|
||||
- [KOPF](https://kopf.readthedocs.io/en/latest/):
|
||||
operators in Python
|
||||
|
||||
- ...
|
||||
|
||||
---
|
||||
|
||||
## Kubebuilder workflow
|
||||
|
||||
- Kubebuilder will create scaffolding for us
|
||||
|
||||
(Go stubs for types and controllers)
|
||||
|
||||
- Then we edit these types and controllers files
|
||||
|
||||
- Kubebuilder generates CRD manifests from our type definitions
|
||||
|
||||
(and regenerates the manifests whenver we update the types)
|
||||
|
||||
- It also gives us tools to quickly run the controller against a cluster
|
||||
|
||||
(not necessarily *on* the cluster)
|
||||
|
||||
---
|
||||
|
||||
## Our objective
|
||||
|
||||
- We're going to implement a *useless machine*
|
||||
|
||||
[basic example](https://www.youtube.com/watch?v=aqAUmgE3WyM)
|
||||
|
|
||||
[playful example](https://www.youtube.com/watch?v=kproPsch7i0)
|
||||
|
|
||||
[advanced example](https://www.youtube.com/watch?v=Nqk_nWAjBus)
|
||||
|
|
||||
[another advanced example](https://www.youtube.com/watch?v=eLtUB8ncEnA)
|
||||
|
||||
- A machine manifest will look like this:
|
||||
```yaml
|
||||
kind: Machine
|
||||
apiVersion: useless.container.training/v1alpha1
|
||||
metadata:
|
||||
name: machine-1
|
||||
spec:
|
||||
# Our useless operator will change that to "down"
|
||||
switchPosition: up
|
||||
```
|
||||
|
||||
- Each time we change the `switchPosition`, the operator will move it back to `down`
|
||||
|
||||
(This is inspired by the
|
||||
[uselessoperator](https://github.com/tilt-dev/uselessoperator)
|
||||
written by
|
||||
[L Körbes](https://twitter.com/ellenkorbes).
|
||||
Highly recommend!💯)
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Local vs remote
|
||||
|
||||
- Building Go code can be a little bit slow on our modest lab VMs
|
||||
|
||||
- It will typically be *much* faster on a local machine
|
||||
|
||||
- All the demos and labs in this section will run fine either way!
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Preparation
|
||||
|
||||
- Install Go
|
||||
|
||||
(on our VMs: `sudo snap install go --classic`)
|
||||
|
||||
- Install kubebuilder
|
||||
|
||||
([get a release](https://github.com/kubernetes-sigs/kubebuilder/releases/), untar, move the `kubebuilder` binary to the `$PATH`)
|
||||
|
||||
- Initialize our workspace:
|
||||
```bash
|
||||
mkdir useless
|
||||
cd useless
|
||||
go mod init container.training/useless
|
||||
kubebuilder init --domain container.training
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Create scaffolding
|
||||
|
||||
- Create a type and corresponding controller:
|
||||
```bash
|
||||
kubebuilder create api --group useless --version v1alpha1 --kind Machine
|
||||
```
|
||||
|
||||
- Answer `y` to both questions
|
||||
|
||||
- Then we need to edit the type that just got created!
|
||||
|
||||
---
|
||||
|
||||
## Edit type
|
||||
|
||||
Edit `api/v1alpha1/machine_types.go`.
|
||||
|
||||
Add the `switchPosition` field in the `spec` structure:
|
||||
|
||||
```go
|
||||
// MachineSpec defines the desired state of Machine
|
||||
type MachineSpec struct {
|
||||
// Position of the switch on the machine, for instance up or down.
|
||||
SwitchPosition string ``json:"switchPosition,omitempty"``
|
||||
}
|
||||
```
|
||||
|
||||
⚠️ The backticks above should be simple backticks, not double-backticks. Sorry.
|
||||
|
||||
---
|
||||
|
||||
## Go markers
|
||||
|
||||
We can use Go *marker comments* to give `controller-gen` extra details about how to handle our type, for instance:
|
||||
|
||||
```
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:JSONPath=".spec.switchPosition",name=Position,type=string
|
||||
```
|
||||
|
||||
(See
|
||||
[marker syntax](https://book.kubebuilder.io/reference/markers.html),
|
||||
[CRD generation](https://book.kubebuilder.io/reference/markers/crd.html),
|
||||
[CRD validation](https://book.kubebuilder.io/reference/markers/crd-validation.html)
|
||||
)
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Using CRD v1
|
||||
|
||||
- By default, kubebuilder generates v1alpha1 CRDs
|
||||
|
||||
- If we want to generate v1 CRDs:
|
||||
|
||||
- edit `Makefile`
|
||||
|
||||
- update `crd:crdVersions=v1`
|
||||
|
||||
---
|
||||
|
||||
## Installing the CRD
|
||||
|
||||
After making these changes, we can run `make install`.
|
||||
|
||||
This will build the Go code, but also:
|
||||
|
||||
- generate the CRD manifest
|
||||
|
||||
- and apply the manifest to the cluster
|
||||
|
||||
---
|
||||
|
||||
## Creating a machine
|
||||
|
||||
Edit `config/samples/useless_v1alpha1_machine.yaml`:
|
||||
|
||||
```yaml
|
||||
kind: Machine
|
||||
apiVersion: useless.container.training/v1alpha1
|
||||
metadata:
|
||||
name: machine-1
|
||||
spec:
|
||||
# Our useless operator will change that to "down"
|
||||
switchPosition: up
|
||||
```
|
||||
|
||||
... and apply it to the cluster.
|
||||
|
||||
---
|
||||
|
||||
## Designing the controller
|
||||
|
||||
- Our controller needs to:
|
||||
|
||||
- notice when a `switchPosition` is not `down`
|
||||
|
||||
- move it to `down` when that happens
|
||||
|
||||
- Later, we can add fancy improvements (wait a bit before moving it, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Reconciler logic
|
||||
|
||||
- Kubebuilder will call our *reconciler* when necessary
|
||||
|
||||
- When necessary = when changes happen ...
|
||||
|
||||
- on our resource
|
||||
|
||||
- or resources that it *watches* (related resources)
|
||||
|
||||
- After "doing stuff", the reconciler can return ...
|
||||
|
||||
- `ctrl.Result{},nil` = all is good
|
||||
|
||||
- `ctrl.Result{Requeue...},nil` = all is good, but call us back in a bit
|
||||
|
||||
- `ctrl.Result{},err` = something's wrong, try again later
|
||||
|
||||
---
|
||||
|
||||
## Loading an object
|
||||
|
||||
Open `controllers/machine_controller.go` and add that code in the `Reconcile` method:
|
||||
|
||||
```go
|
||||
var machine uselessv1alpha1.Machine
|
||||
|
||||
if err := r.Get(ctx, req.NamespacedName, &machine); err != nil {
|
||||
log.Info("error getting object")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
r.Log.Info(
|
||||
"reconciling",
|
||||
"machine", req.NamespaceName,
|
||||
"switchPosition", machine.Spec.SwitchPosition,
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Running the controller
|
||||
|
||||
Our controller is not done yet, but let's try what we have right now!
|
||||
|
||||
This will compile the controller and run it:
|
||||
```
|
||||
make run
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
- create a machine
|
||||
- change the `switchPosition`
|
||||
- delete the machine
|
||||
|
||||
--
|
||||
|
||||
🤔
|
||||
|
||||
---
|
||||
|
||||
## `IgnoreNotFound`
|
||||
|
||||
When we are called for object deletion, the object has *already* been deleted.
|
||||
|
||||
(Unless we're using finalizers, but that's another story.)
|
||||
|
||||
When we return `err`, the controller will try to access the object ...
|
||||
|
||||
... We need to tell it to *not* do that.
|
||||
|
||||
Don't just return `err`, but instead, wrap it around `client.IgnoreNotFound`:
|
||||
|
||||
```go
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
```
|
||||
|
||||
Update the code, `make run` again, create/change/delete again.
|
||||
|
||||
--
|
||||
|
||||
🎉
|
||||
|
||||
---
|
||||
|
||||
## Updating the machine
|
||||
|
||||
Let's try to update the machine like this:
|
||||
|
||||
```go
|
||||
if machine.Spec.SwitchPosition != "down" {
|
||||
machine.Spec.SwitchPosition = "down"
|
||||
if err := r.Update(ctx, &machine); err != nil {
|
||||
log.Info("error updating switch position")
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Again - update, `make run`, test.
|
||||
|
||||
---
|
||||
|
||||
## Spec vs Status
|
||||
|
||||
- Spec = desired state
|
||||
|
||||
- Status = observed state
|
||||
|
||||
- If Status is lost, the controller should be able to reconstruct it
|
||||
|
||||
(maybe with degraded behavior in the meantime)
|
||||
|
||||
- Status will almost always be a sub-resource
|
||||
|
||||
(so that it can be updated separately "cheaply")
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Spec vs Status (in depth)
|
||||
|
||||
- The `/status` subresource is handled differently by the API server
|
||||
|
||||
- Updates to `/status` don't alter the rest of the object
|
||||
|
||||
- Conversely, updates to the object ignore changes in the status
|
||||
|
||||
(See [the docs](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#status-subresource) for the fine print.)
|
||||
|
||||
---
|
||||
|
||||
## "Improving" our controller
|
||||
|
||||
- We want to wait a few seconds before flipping the switch
|
||||
|
||||
- Let's add the following line of code to the controller:
|
||||
```go
|
||||
time.Sleep(5 * time.Second)
|
||||
```
|
||||
|
||||
- `make run`, create a few machines, observe what happens
|
||||
|
||||
--
|
||||
|
||||
💡 Concurrency!
|
||||
|
||||
---
|
||||
|
||||
## Controller logic
|
||||
|
||||
- Our controller shouldn't block (think "event loop")
|
||||
|
||||
- There is a queue of objects that need to be reconciled
|
||||
|
||||
- We can ask to be put back on the queue for later processing
|
||||
|
||||
- When we need to block (wait for something to happen), two options:
|
||||
|
||||
- ask for a *requeue* ("call me back later")
|
||||
|
||||
- yield because we know we will be notified by another resource
|
||||
|
||||
---
|
||||
|
||||
## To requeue ...
|
||||
|
||||
`return ctrl.Result{RequeueAfter: 1 * time.Second}`
|
||||
|
||||
- That means: "try again in 1 second, and I will check if progress was made"
|
||||
|
||||
- This *does not* guarantee that we will be called exactly 1 second later:
|
||||
|
||||
- we might be called before (if other changes happen)
|
||||
|
||||
- we might be called after (if the controller is busy with other objects)
|
||||
|
||||
- If we are waiting for another resource to change, there is an even better way!
|
||||
|
||||
---
|
||||
|
||||
## ... or not to requeue
|
||||
|
||||
`return ctrl.Result{}, nil`
|
||||
|
||||
- That means: "no need to set an alarm; we'll be notified some other way"
|
||||
|
||||
- Use this if we are waiting for another resource to update
|
||||
|
||||
(e.g. a LoadBalancer to be provisioned, a Pod to be ready...)
|
||||
|
||||
- For this to work, we need to set a *watch* (more on that later)
|
||||
|
||||
---
|
||||
|
||||
## "Improving" our controller, take 2
|
||||
|
||||
- Let's store in the machine status the moment when we saw it
|
||||
|
||||
```go
|
||||
// +kubebuilder:printcolumn:JSONPath=".status.seenAt",name=Seen,type=date
|
||||
|
||||
type MachineStatus struct {
|
||||
// Time at which the machine was noticed by our controller.
|
||||
SeenAt *metav1.Time ``json:"seenAt,omitempty"``
|
||||
}
|
||||
```
|
||||
|
||||
⚠️ The backticks above should be simple backticks, not double-backticks. Sorry.
|
||||
|
||||
Note: `date` fields don't display timestamps in the future.
|
||||
|
||||
(That's why for this example it's simpler to use `seenAt` rather than `changeAt`.)
|
||||
|
||||
---
|
||||
|
||||
## Set `seenAt`
|
||||
|
||||
Let's add the following block in our reconciler:
|
||||
|
||||
```go
|
||||
if machine.Status.SeenAt == nil {
|
||||
now := metav1.Now()
|
||||
machine.Status.SeenAt = &now
|
||||
if err := r.Status().Update(ctx, &machine); err != nil {
|
||||
log.Info("error updating status.seenAt")
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
|
||||
}
|
||||
```
|
||||
|
||||
(If needed, add `metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"` to our imports.)
|
||||
|
||||
---
|
||||
|
||||
## Use `seenAt`
|
||||
|
||||
Our switch-position-changing code can now become:
|
||||
|
||||
```go
|
||||
if machine.Spec.SwitchPosition != "down" {
|
||||
now := metav1.Now()
|
||||
changeAt := machine.Status.SeenAt.Time.Add(5 * time.Second)
|
||||
if now.Time.After(changeAt) {
|
||||
machine.Spec.SwitchPosition = "down"
|
||||
if err := r.Update(ctx, &machine); err != nil {
|
||||
log.Info("error updating switch position")
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`make run`, create a few machines, tweak their switches.
|
||||
|
||||
---
|
||||
|
||||
## Owner and dependents
|
||||
|
||||
- Next, let's see how to have relationships between objects!
|
||||
|
||||
- We will now have two kinds of objects: machines, and switches
|
||||
|
||||
- Machines should have *at least* one switch, possibly *multiple ones*
|
||||
|
||||
- The position will now be stored in the switch, not the machine
|
||||
|
||||
- The machine will also expose the combined state of the switches
|
||||
|
||||
- The switches will be tied to their machine through a label
|
||||
|
||||
(See next slide for an example)
|
||||
|
||||
---
|
||||
|
||||
## Switches and machines
|
||||
|
||||
```
|
||||
[jp@hex ~]$ kubectl get machines
|
||||
NAME SWITCHES POSITIONS
|
||||
machine-cz2vl 3 ddd
|
||||
machine-vf4xk 1 d
|
||||
|
||||
[jp@hex ~]$ kubectl get switches --show-labels
|
||||
NAME POSITION SEEN LABELS
|
||||
switch-6wmjw down machine=machine-cz2vl
|
||||
switch-b8csg down machine=machine-cz2vl
|
||||
switch-fl8dq down machine=machine-cz2vl
|
||||
switch-rc59l down machine=machine-vf4xk
|
||||
```
|
||||
|
||||
(The field `status.positions` shows the first letter of the `position` of each switch.)
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
Create the new resource type (but don't create a controller):
|
||||
|
||||
```bash
|
||||
kubebuilder create api --group useless --version v1alpha1 --kind Switch
|
||||
```
|
||||
|
||||
Update `machine_types.go` and `switch_types.go`.
|
||||
|
||||
Implement the logic so that the controller flips all switches down immediately.
|
||||
|
||||
Then change it so that a given machine doesn't flip more than one switch every 5 seconds.
|
||||
|
||||
See next slides for hints!
|
||||
|
||||
---
|
||||
|
||||
## Listing objects
|
||||
|
||||
We can use the `List` method with filters:
|
||||
|
||||
```go
|
||||
var switches uselessv1alpha1.SwitchList
|
||||
|
||||
if err := r.List(ctx, &switches,
|
||||
client.InNamespace(req.Namespace),
|
||||
client.MatchingLabels{"machine": req.Name},
|
||||
); err != nil {
|
||||
log.Error(err, "unable to list switches of the machine")
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
log.Info("Found switches", "switches", switches)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Creating objects
|
||||
|
||||
We can use the `Create` method to create a new object:
|
||||
|
||||
```go
|
||||
sw := uselessv1alpha1.Switch{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: uselessv1alpha1.GroupVersion.String(),
|
||||
Kind: "Switch",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "switch-",
|
||||
Namespace: machine.Namespace,
|
||||
Labels: map[string]string{"machine": machine.Name},
|
||||
},
|
||||
Spec: uselessv1alpha1.SwitchSpec{
|
||||
Position: "down",
|
||||
},
|
||||
}
|
||||
if err := r.Create(ctx, &sw); err != nil { ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Watches
|
||||
|
||||
- Our controller will correctly flip switches when it starts
|
||||
|
||||
- It will also react to machine updates
|
||||
|
||||
- But it won't react if we directly touch the switches!
|
||||
|
||||
- By default, it only monitors machines, not switches
|
||||
|
||||
- We need to tell it to watch switches
|
||||
|
||||
- We also need to tell it how to map a switch to its machine
|
||||
|
||||
---
|
||||
|
||||
## Mapping a switch to its machine
|
||||
|
||||
Define the following helper function:
|
||||
|
||||
```go
|
||||
func (r *MachineReconciler) machineOfSwitch(obj handler.MapObject) []ctrl.Request {
|
||||
r.Log.Debug("mos", "obj", obj)
|
||||
return []ctrl.Request{
|
||||
ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: obj.Meta.GetLabels()["machine"],
|
||||
Namespace: obj.Meta.GetNamespace(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Telling the controller to watch switches
|
||||
|
||||
Update the `SetupWithManager` method in the controller:
|
||||
|
||||
```go
|
||||
func (r *MachineReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&uselessv1alpha1.Machine{}).
|
||||
Owns(&uselessv1alpha1.Switch{}).
|
||||
Watches(
|
||||
&source.Kind{Type: &uselessv1alpha1.Switch{}},
|
||||
&handler.EnqueueRequestsFromMapFunc{
|
||||
ToRequests: handler.ToRequestsFunc(r.machineOfSwitch),
|
||||
}).
|
||||
Complete(r)
|
||||
}
|
||||
```
|
||||
|
||||
After this, our controller should now react to switch changes.
|
||||
|
||||
---
|
||||
|
||||
## Bonus points
|
||||
|
||||
- Handle "scale down" of a machine (by deleting extraneous switches)
|
||||
|
||||
- Automatically delete switches when a machine is deleted
|
||||
|
||||
(ideally, using ownership information)
|
||||
|
||||
- Test corner cases (e.g. changing a switch label)
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- Useless Operator, by [L Körbes](https://twitter.com/ellenkorbes)
|
||||
|
||||
[code](https://github.com/tilt-dev/uselessoperator)
|
||||
|
|
||||
[video (EN)](https://www.youtube.com/watch?v=85dKpsFFju4)
|
||||
|
|
||||
[video (PT)](https://www.youtube.com/watch?v=Vt7Eg4wWNDw)
|
||||
|
||||
- Zero To Operator, by [Solly Ross](https://twitter.com/directxman12)
|
||||
|
||||
[code](https://pres.metamagical.dev/kubecon-us-2019/code)
|
||||
|
|
||||
[video](https://www.youtube.com/watch?v=KBTXBUVNF2I)
|
||||
|
|
||||
[slides](https://pres.metamagical.dev/kubecon-us-2019/)
|
||||
|
||||
- The [kubebuilder book](https://book.kubebuilder.io/)
|
||||
|
||||
???
|
||||
|
||||
:EN:- Implementing an operator with kubebuilder
|
||||
:FR:- Implémenter un opérateur avec kubebuilder
|
||||
@@ -296,7 +296,7 @@ class: extra-details
|
||||
|
||||
- When using `kubectl create deployment`, we cannot indicate the command to execute
|
||||
|
||||
(at least, not in Kubernetes 1.18)
|
||||
(at least, not in Kubernetes 1.18; but that changed in Kubernetes 1.19)
|
||||
|
||||
- We can:
|
||||
|
||||
@@ -338,12 +338,25 @@ class: extra-details
|
||||
kubectl get all
|
||||
```
|
||||
|
||||
<!-- ```hide kubectl wait pod --selector=run=pingpong --for condition=ready ``` -->
|
||||
<!-- ```hide kubectl wait pod --selector=app=pingpong --for condition=ready ``` -->
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## In Kubernetes 1.19
|
||||
|
||||
- Since Kubernetes 1.19, we can specify the command to run
|
||||
|
||||
- The command must be passed after two dashes:
|
||||
```bash
|
||||
kubectl create deployment pingpong --image=alpine -- ping 127.1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Viewing container output
|
||||
|
||||
- Let's use the `kubectl logs` command
|
||||
@@ -494,9 +507,7 @@ We'll see later how to address that shortcoming.
|
||||
```key ^J```
|
||||
```check```
|
||||
```key ^D```
|
||||
```tmux select-pane -t 1```
|
||||
```key ^C```
|
||||
```key ^D```
|
||||
-->
|
||||
|
||||
]
|
||||
|
||||
@@ -149,6 +149,28 @@
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Supporting other CPU architectures
|
||||
|
||||
- The `jpetazzo/httpenv` image is currently only available for `x86_64`
|
||||
|
||||
(the "classic" Intel 64 bits architecture found on most PCs and Macs)
|
||||
|
||||
- That image won't work on other architectures
|
||||
|
||||
(e.g. Raspberry Pi or other ARM-based machines)
|
||||
|
||||
- Note that Docker supports [multi-arch](https://www.docker.com/blog/multi-arch-build-and-images-the-simple-way/) images
|
||||
|
||||
(so *technically* we could make it work across multiple architectures)
|
||||
|
||||
- If you want to build `httpenv` for your own platform, here is the source:
|
||||
|
||||
https://github.com/jpetazzo/httpenv
|
||||
|
||||
---
|
||||
|
||||
## Creating a deployment for our HTTP server
|
||||
|
||||
- We will create a deployment with `kubectl create deployment`
|
||||
|
||||
637
slides/k8s/kyverno.md
Normal file
637
slides/k8s/kyverno.md
Normal file
@@ -0,0 +1,637 @@
|
||||
# Policy Management with Kyverno
|
||||
|
||||
- The Kubernetes permission management system is very flexible ...
|
||||
|
||||
- ... But it can't express *everything!*
|
||||
|
||||
- Examples:
|
||||
|
||||
- forbid using `:latest` image tag
|
||||
|
||||
- enforce that each Deployment, Service, etc. has an `owner` label
|
||||
<br/>(except in e.g. `kube-system`)
|
||||
|
||||
- enforce that each container has at least a `readinessProbe` healthcheck
|
||||
|
||||
- How can we address that, and express these more complex *policies?*
|
||||
|
||||
---
|
||||
|
||||
## Admission control
|
||||
|
||||
- The Kubernetes API server provides a generic mechanism called *admission control*
|
||||
|
||||
- Admission controllers will examine each write request, and can:
|
||||
|
||||
- approve/deny it (for *validating* admission controllers)
|
||||
|
||||
- additionally *update* the object (for *mutating* admission controllers)
|
||||
|
||||
- These admission controllers can be:
|
||||
|
||||
- plug-ins built into the Kubernetes API server
|
||||
<br/>(selectively enabled/disabled by e.g. command-line flags)
|
||||
|
||||
- webhooks registered dynamically with the Kubernetes API server
|
||||
|
||||
---
|
||||
|
||||
## What's Kyverno?
|
||||
|
||||
- Policy management solution for Kubernetes
|
||||
|
||||
- Open source (https://github.com/kyverno/kyverno/)
|
||||
|
||||
- Compatible with all clusters
|
||||
|
||||
(doesn't require to reconfigure the control plane, enable feature gates...)
|
||||
|
||||
- We don't endorse / support it in a particular way, but we think it's cool
|
||||
|
||||
- It's not the only solution!
|
||||
|
||||
(see e.g. [Open Policy Agent](https://www.openpolicyagent.org/docs/v0.12.2/kubernetes-admission-control/))
|
||||
|
||||
---
|
||||
|
||||
## What can Kyverno do?
|
||||
|
||||
- *Validate* resource manifests
|
||||
|
||||
(accept/deny depending on whether they conform to our policies)
|
||||
|
||||
- *Mutate* resources when they get created or updated
|
||||
|
||||
(to add/remove/change fields on the fly)
|
||||
|
||||
- *Generate* additional resources when a resource gets created
|
||||
|
||||
(e.g. when namespace is created, automatically add quotas and limits)
|
||||
|
||||
- *Audit* existing resources
|
||||
|
||||
(warn about resources that violate certain policies)
|
||||
|
||||
---
|
||||
|
||||
## How does it do it?
|
||||
|
||||
- Kyverno is implemented as a *controller* or *operator*
|
||||
|
||||
- It typically runs as a Deployment on our cluster
|
||||
|
||||
- Policies are defined as *custom resource definitions*
|
||||
|
||||
- They are implemented with a set of *dynamic admission control webhooks*
|
||||
|
||||
--
|
||||
|
||||
🤔
|
||||
|
||||
--
|
||||
|
||||
- Let's unpack that!
|
||||
|
||||
---
|
||||
|
||||
## Custom resource definitions
|
||||
|
||||
- When we install Kyverno, it will register new resource types:
|
||||
|
||||
- Policy and ClusterPolicy (per-namespace and cluster-scope policies)
|
||||
|
||||
- PolicyViolation and ClusterPolicyViolation (used in audit mode)
|
||||
|
||||
- GenerateRequest (used internally when generating resources asynchronously)
|
||||
|
||||
- We will be able to do e.g. `kubectl get policyviolations --all-namespaces`
|
||||
|
||||
(to see policy violations across all namespaces)
|
||||
|
||||
- Policies will be defined in YAML and registered/updated with e.g. `kubectl apply`
|
||||
|
||||
---
|
||||
|
||||
## Dynamic admission control webhooks
|
||||
|
||||
- When we install Kyverno, it will register a few webhooks for its use
|
||||
|
||||
(by creating ValidatingWebhookConfiguration and MutatingWebhookConfiguration resources)
|
||||
|
||||
- All subsequent resource modifications are submitted to these webhooks
|
||||
|
||||
(creations, updates, deletions)
|
||||
|
||||
---
|
||||
|
||||
## Controller
|
||||
|
||||
- When we install Kyverno, it creates a Deployment (and therefore, a Pod)
|
||||
|
||||
- That Pod runs the server used by the webhooks
|
||||
|
||||
- It also runs a controller that will:
|
||||
|
||||
- run optional checks in the background (and generate PolicyViolation objects)
|
||||
|
||||
- process GenerateRequest objects asynchronously
|
||||
|
||||
---
|
||||
|
||||
## Kyverno in action
|
||||
|
||||
- We're going to install Kyverno on our cluster
|
||||
|
||||
- Then, we will use it to implement a few policies
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Kyverno versions
|
||||
|
||||
- We're going to use version 1.2
|
||||
|
||||
- Version 1.3.0-rc came out in November 2020
|
||||
|
||||
- It introduces a few changes
|
||||
|
||||
(e.g. PolicyViolations are now PolicyReports)
|
||||
|
||||
- Expect this to change in the near future!
|
||||
|
||||
---
|
||||
|
||||
## Installing Kyverno
|
||||
|
||||
- Kyverno can be installed with a (big) YAML manifest
|
||||
|
||||
- ... or with Helm charts (which allows to customize a few things)
|
||||
|
||||
.exercise[
|
||||
|
||||
- Install Kyverno:
|
||||
```bash
|
||||
kubectl apply -f https://raw.githubusercontent.com/kyverno/kyverno\
|
||||
/v1.2.1/definitions/release/install.yaml
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Kyverno policies in a nutshell
|
||||
|
||||
- Which resources does it *select?*
|
||||
|
||||
- can specify resources to *match* and/or *exclude*
|
||||
|
||||
- can specify *kinds* and/or *selector* and/or users/roles doing the action
|
||||
|
||||
- Which operation should be done?
|
||||
|
||||
- validate, mutate, or generate
|
||||
|
||||
- For validation, whether it should *enforce* or *audit* failures
|
||||
|
||||
- Operation details (what exactly to validate, mutate, or generate)
|
||||
|
||||
---
|
||||
|
||||
## Immutable primary colors, take 1
|
||||
|
||||
- Our pods can have an optional `color` label
|
||||
|
||||
- If the label exists, it *must* be `red`, `green`, or `blue`
|
||||
|
||||
- One possible approach:
|
||||
|
||||
- *match* all pods that have a `color` label that is not `red`, `green`, or `blue`
|
||||
|
||||
- *deny* these pods
|
||||
|
||||
- We could also *match* all pods, then *deny* with a condition
|
||||
|
||||
---
|
||||
|
||||
## Testing without the policy
|
||||
|
||||
- First, let's create a pod with an "invalid" label
|
||||
|
||||
(while we still can!)
|
||||
|
||||
- We will use this later
|
||||
|
||||
.exercise[
|
||||
|
||||
- Create a pod:
|
||||
```bash
|
||||
kubectl run test-color-0 --image=nginx
|
||||
```
|
||||
|
||||
- Apply a color label:
|
||||
```bash
|
||||
kubectl label pod test-color-0 color=purple
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Our first Kyverno policy
|
||||
|
||||
.small[
|
||||
```yaml
|
||||
@@INCLUDE[k8s/kyverno-pod-color-1.yaml]
|
||||
```
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Load and try the policy
|
||||
|
||||
.exercise[
|
||||
|
||||
- Load the policy:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/kyverno-pod-color-1.yaml
|
||||
```
|
||||
|
||||
- Create a pod:
|
||||
```bash
|
||||
kubectl run test-color-1 --image=nginx
|
||||
```
|
||||
|
||||
- Try to apply a few color labels:
|
||||
```bash
|
||||
kubectl label pod test-color-1 color=purple
|
||||
kubectl label pod test-color-1 color=red
|
||||
kubectl label pod test-color-1 color-
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Immutable primary colors, take 2
|
||||
|
||||
- New rule: once a `color` label has been added, it cannot be changed
|
||||
|
||||
(i.e. if `color=red`, we can't change it to `color=blue`)
|
||||
|
||||
- Our approach:
|
||||
|
||||
- *match* all pods
|
||||
|
||||
- *deny* these pods if their `color` label has changed
|
||||
|
||||
- "Old" and "new" versions of the pod can be referenced through
|
||||
|
||||
`{{ request.oldObject }}` and `{{ request.object }}`
|
||||
|
||||
- Our label is available through `{{ request.object.metadata.labels.color }}`
|
||||
|
||||
- Again, other approaches are possible!
|
||||
|
||||
---
|
||||
|
||||
## Our second Kyverno policy
|
||||
|
||||
.small[
|
||||
```yaml
|
||||
@@INCLUDE[k8s/kyverno-pod-color-2.yaml]
|
||||
```
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Load and try the policy
|
||||
|
||||
.exercise[
|
||||
|
||||
- Load the policy:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/kyverno-pod-color-2.yaml
|
||||
```
|
||||
|
||||
- Create a pod:
|
||||
```bash
|
||||
kubectl run test-color-2 --image=nginx
|
||||
```
|
||||
|
||||
- Try to apply a few color labels:
|
||||
```bash
|
||||
kubectl label test-color-2 color=purple
|
||||
kubectl label test-color-2 color=red
|
||||
kubectl label test-color-2 color=blue --overwrite
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## `background`
|
||||
|
||||
- What is this `background: false` option, and why do we need it?
|
||||
|
||||
--
|
||||
|
||||
- Admission controllers are only invoked when we change an object
|
||||
|
||||
- Existing objects are not affected
|
||||
|
||||
(e.g. if we have a pod with `color=pink` *before* installing our policy)
|
||||
|
||||
- Kyvero can also run checks in the background, and report violations
|
||||
|
||||
(we'll see later how they are reported)
|
||||
|
||||
- `background: false` disables that
|
||||
|
||||
--
|
||||
|
||||
- Alright, but ... *why* do we need it?
|
||||
|
||||
---
|
||||
|
||||
## Accessing `AdmissionRequest` context
|
||||
|
||||
- In this specific policy, we want to prevent an *update*
|
||||
|
||||
(as opposed to a mere *create* operation)
|
||||
|
||||
- We want to compare the *old* and *new* version
|
||||
|
||||
(to check if a specific label was removed)
|
||||
|
||||
- The `AdmissionRequest` object has `object` and `oldObject` fields
|
||||
|
||||
(the `AdmissionRequest` object is the thing that gets submitted to the webhook)
|
||||
|
||||
- Kyverno lets us access the `AdmissionRequest` object
|
||||
|
||||
(and in particular, `{{ request.object }}` and `{{ request.oldObject }}`)
|
||||
|
||||
--
|
||||
|
||||
- Alright, but ... what's the link with `background: false`?
|
||||
|
||||
---
|
||||
|
||||
## `{{ request }}`
|
||||
|
||||
- The `{{ request }}` context is only available when there is an `AdmissionRequest`
|
||||
|
||||
- When a resource is "at rest", there is no `{{ request }}` (and no old/new)
|
||||
|
||||
- Therefore, a policy that uses `{{ request }}` cannot validate existing objects
|
||||
|
||||
(it can only be used when an object is actually created/updated/deleted)
|
||||
|
||||
---
|
||||
|
||||
## Immutable primary colors, take 3
|
||||
|
||||
- New rule: once a `color` label has been added, it cannot be removed
|
||||
|
||||
- Our approach:
|
||||
|
||||
- *match* all pods that *do not* have a `color` label
|
||||
|
||||
- *deny* these pods if they had a `color` label before
|
||||
|
||||
- "before" can be referenced through `{{ request.oldObject }}`
|
||||
|
||||
- Again, other approaches are possible!
|
||||
|
||||
---
|
||||
|
||||
## Our third Kyverno policy
|
||||
|
||||
.small[
|
||||
```yaml
|
||||
@@INCLUDE[k8s/kyverno-pod-color-3.yaml]
|
||||
```
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Load and try the policy
|
||||
|
||||
.exercise[
|
||||
|
||||
- Load the policy:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/kyverno-pod-color-3.yaml
|
||||
```
|
||||
|
||||
- Create a pod:
|
||||
```bash
|
||||
kubectl run test-color-3 --image=nginx
|
||||
```
|
||||
|
||||
- Try to apply a few color labels:
|
||||
```bash
|
||||
kubectl label test-color-3 color=purple
|
||||
kubectl label test-color-3 color=red
|
||||
kubectl label test-color-3 color-
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Background checks
|
||||
|
||||
- What about the `test-color-0` pod that we create initially?
|
||||
|
||||
(remember: we did set `color=purple`)
|
||||
|
||||
- Kyverno generated a ClusterPolicyViolation to indicate it
|
||||
|
||||
.exercise[
|
||||
|
||||
- Check that the pod still an "invalid" color:
|
||||
```bash
|
||||
kubectl get pods -L color
|
||||
```
|
||||
|
||||
- List ClusterPolicyViolations:
|
||||
```bash
|
||||
kubectl get clusterpolicyviolations
|
||||
kubectl get cpolv
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Generating objects
|
||||
|
||||
- When we create a Namespace, we also want to automatically create:
|
||||
|
||||
- a LimitRange (to set default CPU and RAM requests and limits)
|
||||
|
||||
- a ResourceQuota (to limit the resources used by the namespace)
|
||||
|
||||
- a NetworkPolicy (to isolate the namespace)
|
||||
|
||||
- We can do that with a Kyverno policy with a *generate* action
|
||||
|
||||
(it is mutually exclusive with the *validate* action)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
- The *generate* action must specify:
|
||||
|
||||
- the `kind` of resource to generate
|
||||
|
||||
- the `name` of the resource to generate
|
||||
|
||||
- its `namespace`, when applicable
|
||||
|
||||
- *either* a `data` structure, to be used to populate the resource
|
||||
|
||||
- *or* a `clone` reference, to copy an existing resource
|
||||
|
||||
Note: the `apiVersion` field appears to be optional.
|
||||
|
||||
---
|
||||
|
||||
## In practice
|
||||
|
||||
- We will use the policy @@LINK[k8s/kyverno-namespace-setup.yaml]
|
||||
|
||||
- We need to generate 3 resources, so we have 3 rules in the policy
|
||||
|
||||
- Excerpt:
|
||||
```yaml
|
||||
generate:
|
||||
kind: LimitRange
|
||||
name: default-limitrange
|
||||
namespace: "{{request.object.metadata.name}}"
|
||||
data:
|
||||
spec:
|
||||
limits:
|
||||
```
|
||||
|
||||
- Note that we have to specify the `namespace`
|
||||
|
||||
(and we infer it from the name of the resource being created, i.e. the Namespace)
|
||||
|
||||
---
|
||||
|
||||
## Lifecycle
|
||||
|
||||
- After generated objects have been created, we can change them
|
||||
|
||||
(Kyverno won't update them)
|
||||
|
||||
- Except if we use `clone` together with the `synchronize` flag
|
||||
|
||||
(in that case, Kyverno will watch the cloned resource)
|
||||
|
||||
- This is convenient for e.g. ConfigMaps shared between Namespaces
|
||||
|
||||
- Objects are generated only at *creation* (not when updating an old object)
|
||||
|
||||
---
|
||||
|
||||
## Asynchronous creation
|
||||
|
||||
- Kyverno creates resources asynchronously
|
||||
|
||||
(by creating a GenerateRequest resource first)
|
||||
|
||||
- This is useful when the resource cannot be created
|
||||
|
||||
(because of permissions or dependency issues)
|
||||
|
||||
- Kyverno will periodically loop through the pending GenerateRequests
|
||||
|
||||
- Once the ressource is created, the GenerateRequest is marked as Completed
|
||||
|
||||
---
|
||||
|
||||
## Footprint
|
||||
|
||||
- 5 CRDs: 4 user-facing, 1 internal (GenerateRequest)
|
||||
|
||||
- 5 webhooks
|
||||
|
||||
- 1 Service, 1 Deployment, 1 ConfigMap
|
||||
|
||||
- Internal resources (GenerateRequest) "parked" in a Namespace
|
||||
|
||||
- Kyverno packs a lot of features in a small footprint
|
||||
|
||||
---
|
||||
|
||||
## Strengths
|
||||
|
||||
- Kyverno is very easy to install
|
||||
|
||||
(it's harder to get easier than one `kubectl apply -f`)
|
||||
|
||||
- The setup of the webhooks is fully automated
|
||||
|
||||
(including certificate generation)
|
||||
|
||||
- It offers both namespaced and cluster-scope policies
|
||||
|
||||
(same thing for the policy violations)
|
||||
|
||||
- The policy language leverages existing constructs
|
||||
|
||||
(e.g. `matchExpressions`)
|
||||
|
||||
---
|
||||
|
||||
## Caveats
|
||||
|
||||
- By default, the webhook failure policy is `Ignore`
|
||||
|
||||
(meaning that there is a potential to evade policies if we can DOS the webhook)
|
||||
|
||||
- Advanced policies (with conditionals) have unique, exotic syntax:
|
||||
```yaml
|
||||
spec:
|
||||
=(volumes):
|
||||
=(hostPath):
|
||||
path: "!/var/run/docker.sock"
|
||||
```
|
||||
|
||||
- The `{{ request }}` context is powerful, but difficult to validate
|
||||
|
||||
(Kyverno can't know ahead of time how it will be populated)
|
||||
|
||||
- Policy validation is difficult
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Pods created by controllers
|
||||
|
||||
- When e.g. a ReplicaSet or DaemonSet creates a pod, it "owns" it
|
||||
|
||||
(the ReplicaSet or DaemonSet is listed in the Pod's `.metadata.ownerReferences`)
|
||||
|
||||
- Kyverno treats these Pods differently
|
||||
|
||||
- If my understanding of the code is correct (big *if*):
|
||||
|
||||
- it skips validation for "owned" Pods
|
||||
|
||||
- instead, it validates their controllers
|
||||
|
||||
- this way, Kyverno can report errors on the controller instead of the pod
|
||||
|
||||
- This can be a bit confusing when testing policies on such pods!
|
||||
|
||||
???
|
||||
|
||||
:EN:- Policy Management with Kyverno
|
||||
:FR:- Gestion de *policies* avec Kyverno
|
||||
@@ -214,7 +214,7 @@ class: extra-details
|
||||
|
||||
- Label *values* are up to 63 characters, with the same restrictions
|
||||
|
||||
- Annotations *values* can have arbitrary characeters (yes, even binary)
|
||||
- Annotations *values* can have arbitrary characters (yes, even binary)
|
||||
|
||||
- Maximum length isn't defined
|
||||
|
||||
|
||||
@@ -191,6 +191,8 @@ are a few tools that can help us.*
|
||||
|
||||
## Developer experience
|
||||
|
||||
*These questions constitute a quick "smoke test" for our strategy:*
|
||||
|
||||
- How do we on-board a new developer?
|
||||
|
||||
- What do they need to install to get a dev stack?
|
||||
@@ -199,8 +201,6 @@ are a few tools that can help us.*
|
||||
|
||||
- How does someone add a component to a stack?
|
||||
|
||||
*These questions are good "sanity checks" to validate our strategy!*
|
||||
|
||||
---
|
||||
|
||||
## Some guidelines
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
## Deploying Consul
|
||||
|
||||
- We will use a slightly different YAML file
|
||||
- Let's use a new manifest for our Consul cluster
|
||||
|
||||
- The only differences between that file and the previous one are:
|
||||
|
||||
@@ -66,15 +66,11 @@
|
||||
|
||||
- the corresponding `volumeMounts` in the Pod spec
|
||||
|
||||
- the label `consul` has been changed to `persistentconsul`
|
||||
<br/>
|
||||
(to avoid conflicts with the other Stateful Set)
|
||||
|
||||
.exercise[
|
||||
|
||||
- Apply the persistent Consul YAML file:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/persistent-consul.yaml
|
||||
kubectl apply -f ~/container.training/k8s/consul-3.yaml
|
||||
```
|
||||
|
||||
]
|
||||
@@ -97,7 +93,7 @@
|
||||
kubectl get pv
|
||||
```
|
||||
|
||||
- The Pod `persistentconsul-0` is not scheduled yet:
|
||||
- The Pod `consul-0` is not scheduled yet:
|
||||
```bash
|
||||
kubectl get pods -o wide
|
||||
```
|
||||
@@ -112,9 +108,9 @@
|
||||
|
||||
- In a Stateful Set, the Pods are started one by one
|
||||
|
||||
- `persistentconsul-1` won't be created until `persistentconsul-0` is running
|
||||
- `consul-1` won't be created until `consul-0` is running
|
||||
|
||||
- `persistentconsul-0` has a dependency on an unbound Persistent Volume Claim
|
||||
- `consul-0` has a dependency on an unbound Persistent Volume Claim
|
||||
|
||||
- The scheduler won't schedule the Pod until the PVC is bound
|
||||
|
||||
@@ -152,7 +148,7 @@
|
||||
|
||||
- Once a PVC is bound, its pod can start normally
|
||||
|
||||
- Once the pod `persistentconsul-0` has started, `persistentconsul-1` can be created, etc.
|
||||
- Once the pod `consul-0` has started, `consul-1` can be created, etc.
|
||||
|
||||
- Eventually, our Consul cluster is up, and backend by "persistent" volumes
|
||||
|
||||
@@ -160,7 +156,7 @@
|
||||
|
||||
- Check that our Consul clusters has 3 members indeed:
|
||||
```bash
|
||||
kubectl exec persistentconsul-0 consul members
|
||||
kubectl exec consul-0 -- consul members
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
@@ -34,11 +34,11 @@
|
||||
|
||||
- Download the `kubectl` binary from one of these links:
|
||||
|
||||
[Linux](https://storage.googleapis.com/kubernetes-release/release/v1.15.3/bin/linux/amd64/kubectl)
|
||||
[Linux](https://storage.googleapis.com/kubernetes-release/release/v1.19.2/bin/linux/amd64/kubectl)
|
||||
|
|
||||
[macOS](https://storage.googleapis.com/kubernetes-release/release/v1.15.3/bin/darwin/amd64/kubectl)
|
||||
[macOS](https://storage.googleapis.com/kubernetes-release/release/v1.19.2/bin/darwin/amd64/kubectl)
|
||||
|
|
||||
[Windows](https://storage.googleapis.com/kubernetes-release/release/v1.15.3/bin/windows/amd64/kubectl.exe)
|
||||
[Windows](https://storage.googleapis.com/kubernetes-release/release/v1.19.2/bin/windows/amd64/kubectl.exe)
|
||||
|
||||
- On Linux and macOS, make the binary executable with `chmod +x kubectl`
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ Exactly what we need!
|
||||
sudo chmod +x /usr/local/bin/stern
|
||||
```
|
||||
|
||||
- On macOS, we can also `brew install stern` or `port install stern`
|
||||
- On macOS, we can also `brew install stern` or `sudo port install stern`
|
||||
|
||||
<!-- ##VERSION## -->
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
- The Docker Engine is installed (and running) on these machines
|
||||
|
||||
- The Kubernetes packages are installed, but nothing is running
|
||||
- The Kubernetes binaries are installed, but nothing is running
|
||||
|
||||
- We will use `kubenet1` to run the control plane
|
||||
|
||||
|
||||
@@ -431,13 +431,13 @@ troubleshoot easily, without having to poke holes in our firewall.
|
||||
|
||||
- As always, the [Kubernetes documentation](https://kubernetes.io/docs/concepts/services-networking/network-policies/) is a good starting point
|
||||
|
||||
- The API documentation has a lot of detail about the format of various objects:
|
||||
- The API documentation has a lot of detail about the format of various objects: <!-- ##VERSION## -->
|
||||
|
||||
- [NetworkPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.12/#networkpolicy-v1-networking-k8s-io)
|
||||
- [NetworkPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#networkpolicy-v1-networking-k8s-io)
|
||||
|
||||
- [NetworkPolicySpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.12/#networkpolicyspec-v1-networking-k8s-io)
|
||||
- [NetworkPolicySpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#networkpolicyspec-v1-networking-k8s-io)
|
||||
|
||||
- [NetworkPolicyIngressRule](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.12/#networkpolicyingressrule-v1-networking-k8s-io)
|
||||
- [NetworkPolicyIngressRule](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#networkpolicyingressrule-v1-networking-k8s-io)
|
||||
|
||||
- etc.
|
||||
|
||||
|
||||
@@ -222,9 +222,9 @@ class: extra-details
|
||||
|
|
||||
[Simple example](https://medium.com/faun/writing-your-first-kubernetes-operator-8f3df4453234)
|
||||
|
||||
- Zalando Kubernetes Operator Pythonic Framework (KOPF)
|
||||
- Kubernetes Operator Pythonic Framework (KOPF)
|
||||
|
||||
[GitHub](https://github.com/zalando-incubator/kopf)
|
||||
[GitHub](https://github.com/nolar/kopf)
|
||||
|
|
||||
[Docs](https://kopf.readthedocs.io/)
|
||||
|
|
||||
@@ -240,6 +240,12 @@ class: extra-details
|
||||
|
|
||||
[Zookeeper example](https://github.com/kudobuilder/frameworks/tree/master/repo/stable/zookeeper)
|
||||
|
||||
- Kubebuilder (Go, very close to the Kubernetes API codebase)
|
||||
|
||||
[GitHub](https://github.com/kubernetes-sigs/kubebuilder)
|
||||
|
|
||||
[Book](https://book.kubebuilder.io/)
|
||||
|
||||
---
|
||||
|
||||
## Validation
|
||||
|
||||
@@ -1,19 +1,5 @@
|
||||
# Operators
|
||||
|
||||
- Operators are one of the many ways to extend Kubernetes
|
||||
|
||||
- We will define operators
|
||||
|
||||
- We will see how they work
|
||||
|
||||
- We will install a specific operator (for ElasticSearch)
|
||||
|
||||
- We will use it to provision an ElasticSearch cluster
|
||||
|
||||
---
|
||||
|
||||
## What are operators?
|
||||
|
||||
*An operator represents **human operational knowledge in software,**
|
||||
<br/>
|
||||
to reliably manage an application.
|
||||
@@ -119,455 +105,6 @@ Examples:
|
||||
|
||||
---
|
||||
|
||||
## One operator in action
|
||||
|
||||
- We will install [Elastic Cloud on Kubernetes](https://www.elastic.co/guide/en/cloud-on-k8s/current/k8s-quickstart.html), an ElasticSearch operator
|
||||
|
||||
- This operator requires PersistentVolumes
|
||||
|
||||
- We will install Rancher's [local path storage provisioner](https://github.com/rancher/local-path-provisioner) to automatically create these
|
||||
|
||||
- Then, we will create an ElasticSearch resource
|
||||
|
||||
- The operator will detect that resource and provision the cluster
|
||||
|
||||
---
|
||||
|
||||
## Installing a Persistent Volume provisioner
|
||||
|
||||
(This step can be skipped if you already have a dynamic volume provisioner.)
|
||||
|
||||
- This provisioner creates Persistent Volumes backed by `hostPath`
|
||||
|
||||
(local directories on our nodes)
|
||||
|
||||
- It doesn't require anything special ...
|
||||
|
||||
- ... But losing a node = losing the volumes on that node!
|
||||
|
||||
.exercise[
|
||||
|
||||
- Install the local path storage provisioner:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/local-path-storage.yaml
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Making sure we have a default StorageClass
|
||||
|
||||
- The ElasticSearch operator will create StatefulSets
|
||||
|
||||
- These StatefulSets will instantiate PersistentVolumeClaims
|
||||
|
||||
- These PVCs need to be explicitly associated with a StorageClass
|
||||
|
||||
- Or we need to tag a StorageClass to be used as the default one
|
||||
|
||||
.exercise[
|
||||
|
||||
- List StorageClasses:
|
||||
```bash
|
||||
kubectl get storageclasses
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
We should see the `local-path` StorageClass.
|
||||
|
||||
---
|
||||
|
||||
## Setting a default StorageClass
|
||||
|
||||
- This is done by adding an annotation to the StorageClass:
|
||||
|
||||
`storageclass.kubernetes.io/is-default-class: true`
|
||||
|
||||
.exercise[
|
||||
|
||||
- Tag the StorageClass so that it's the default one:
|
||||
```bash
|
||||
kubectl annotate storageclass local-path \
|
||||
storageclass.kubernetes.io/is-default-class=true
|
||||
```
|
||||
|
||||
- Check the result:
|
||||
```bash
|
||||
kubectl get storageclasses
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
Now, the StorageClass should have `(default)` next to its name.
|
||||
|
||||
---
|
||||
|
||||
## Install the ElasticSearch operator
|
||||
|
||||
- The operator provides:
|
||||
|
||||
- a few CustomResourceDefinitions
|
||||
- a Namespace for its other resources
|
||||
- a ValidatingWebhookConfiguration for type checking
|
||||
- a StatefulSet for its controller and webhook code
|
||||
- a ServiceAccount, ClusterRole, ClusterRoleBinding for permissions
|
||||
|
||||
- All these resources are grouped in a convenient YAML file
|
||||
|
||||
.exercise[
|
||||
|
||||
- Install the operator:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/eck-operator.yaml
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Check our new custom resources
|
||||
|
||||
- Let's see which CRDs were created
|
||||
|
||||
.exercise[
|
||||
|
||||
- List all CRDs:
|
||||
```bash
|
||||
kubectl get crds
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
This operator supports ElasticSearch, but also Kibana and APM. Cool!
|
||||
|
||||
---
|
||||
|
||||
## Create the `eck-demo` namespace
|
||||
|
||||
- For clarity, we will create everything in a new namespace, `eck-demo`
|
||||
|
||||
- This namespace is hard-coded in the YAML files that we are going to use
|
||||
|
||||
- We need to create that namespace
|
||||
|
||||
.exercise[
|
||||
|
||||
- Create the `eck-demo` namespace:
|
||||
```bash
|
||||
kubectl create namespace eck-demo
|
||||
```
|
||||
|
||||
- Switch to that namespace:
|
||||
```bash
|
||||
kns eck-demo
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
class: extra-details
|
||||
|
||||
## Can we use a different namespace?
|
||||
|
||||
Yes, but then we need to update all the YAML manifests that we
|
||||
are going to apply in the next slides.
|
||||
|
||||
The `eck-demo` namespace is hard-coded in these YAML manifests.
|
||||
|
||||
Why?
|
||||
|
||||
Because when defining a ClusterRoleBinding that references a
|
||||
ServiceAccount, we have to indicate in which namespace the
|
||||
ServiceAccount is located.
|
||||
|
||||
---
|
||||
|
||||
## Create an ElasticSearch resource
|
||||
|
||||
- We can now create a resource with `kind: ElasticSearch`
|
||||
|
||||
- The YAML for that resource will specify all the desired parameters:
|
||||
|
||||
- how many nodes we want
|
||||
- image to use
|
||||
- add-ons (kibana, cerebro, ...)
|
||||
- whether to use TLS or not
|
||||
- etc.
|
||||
|
||||
.exercise[
|
||||
|
||||
- Create our ElasticSearch cluster:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/eck-elasticsearch.yaml
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Operator in action
|
||||
|
||||
- Over the next minutes, the operator will create our ES cluster
|
||||
|
||||
- It will report our cluster status through the CRD
|
||||
|
||||
.exercise[
|
||||
|
||||
- Check the logs of the operator:
|
||||
```bash
|
||||
stern --namespace=elastic-system operator
|
||||
```
|
||||
|
||||
<!--
|
||||
```wait elastic-operator-0```
|
||||
```tmux split-pane -v```
|
||||
--->
|
||||
|
||||
- Watch the status of the cluster through the CRD:
|
||||
```bash
|
||||
kubectl get es -w
|
||||
```
|
||||
|
||||
<!--
|
||||
```longwait green```
|
||||
```key ^C```
|
||||
```key ^D```
|
||||
```key ^C```
|
||||
-->
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Connecting to our cluster
|
||||
|
||||
- It's not easy to use the ElasticSearch API from the shell
|
||||
|
||||
- But let's check at least if ElasticSearch is up!
|
||||
|
||||
.exercise[
|
||||
|
||||
- Get the ClusterIP of our ES instance:
|
||||
```bash
|
||||
kubectl get services
|
||||
```
|
||||
|
||||
- Issue a request with `curl`:
|
||||
```bash
|
||||
curl http://`CLUSTERIP`:9200
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
We get an authentication error. Our cluster is protected!
|
||||
|
||||
---
|
||||
|
||||
## Obtaining the credentials
|
||||
|
||||
- The operator creates a user named `elastic`
|
||||
|
||||
- It generates a random password and stores it in a Secret
|
||||
|
||||
.exercise[
|
||||
|
||||
- Extract the password:
|
||||
```bash
|
||||
kubectl get secret demo-es-elastic-user \
|
||||
-o go-template="{{ .data.elastic | base64decode }} "
|
||||
```
|
||||
|
||||
- Use it to connect to the API:
|
||||
```bash
|
||||
curl -u elastic:`PASSWORD` http://`CLUSTERIP`:9200
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
We should see a JSON payload with the `"You Know, for Search"` tagline.
|
||||
|
||||
---
|
||||
|
||||
## Sending data to the cluster
|
||||
|
||||
- Let's send some data to our brand new ElasticSearch cluster!
|
||||
|
||||
- We'll deploy a filebeat DaemonSet to collect node logs
|
||||
|
||||
.exercise[
|
||||
|
||||
- Deploy filebeat:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/eck-filebeat.yaml
|
||||
```
|
||||
|
||||
- Wait until some pods are up:
|
||||
```bash
|
||||
watch kubectl get pods -l k8s-app=filebeat
|
||||
```
|
||||
|
||||
<!--
|
||||
```wait Running```
|
||||
```key ^C```
|
||||
-->
|
||||
|
||||
- Check that a filebeat index was created:
|
||||
```bash
|
||||
curl -u elastic:`PASSWORD` http://`CLUSTERIP`:9200/_cat/indices
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Deploying an instance of Kibana
|
||||
|
||||
- Kibana can visualize the logs injected by filebeat
|
||||
|
||||
- The ECK operator can also manage Kibana
|
||||
|
||||
- Let's give it a try!
|
||||
|
||||
.exercise[
|
||||
|
||||
- Deploy a Kibana instance:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/eck-kibana.yaml
|
||||
```
|
||||
|
||||
- Wait for it to be ready:
|
||||
```bash
|
||||
kubectl get kibana -w
|
||||
```
|
||||
|
||||
<!--
|
||||
```longwait green```
|
||||
```key ^C```
|
||||
-->
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Connecting to Kibana
|
||||
|
||||
- Kibana is automatically set up to conect to ElasticSearch
|
||||
|
||||
(this is arranged by the YAML that we're using)
|
||||
|
||||
- However, it will ask for authentication
|
||||
|
||||
- It's using the same user/password as ElasticSearch
|
||||
|
||||
.exercise[
|
||||
|
||||
- Get the NodePort allocated to Kibana:
|
||||
```bash
|
||||
kubectl get services
|
||||
```
|
||||
|
||||
- Connect to it with a web browser
|
||||
|
||||
- Use the same user/password as before
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Setting up Kibana
|
||||
|
||||
After the Kibana UI loads, we need to click around a bit
|
||||
|
||||
.exercise[
|
||||
|
||||
- Pick "explore on my own"
|
||||
|
||||
- Click on Use Elasticsearch data / Connect to your Elasticsearch index"
|
||||
|
||||
- Enter `filebeat-*` for the index pattern and click "Next step"
|
||||
|
||||
- Select `@timestamp` as time filter field name
|
||||
|
||||
- Click on "discover" (the small icon looking like a compass on the left bar)
|
||||
|
||||
- Play around!
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Scaling up the cluster
|
||||
|
||||
- At this point, we have only one node
|
||||
|
||||
- We are going to scale up
|
||||
|
||||
- But first, we'll deploy Cerebro, an UI for ElasticSearch
|
||||
|
||||
- This will let us see the state of the cluster, how indexes are sharded, etc.
|
||||
|
||||
---
|
||||
|
||||
## Deploying Cerebro
|
||||
|
||||
- Cerebro is stateless, so it's fairly easy to deploy
|
||||
|
||||
(one Deployment + one Service)
|
||||
|
||||
- However, it needs the address and credentials for ElasticSearch
|
||||
|
||||
- We prepared yet another manifest for that!
|
||||
|
||||
.exercise[
|
||||
|
||||
- Deploy Cerebro:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/eck-cerebro.yaml
|
||||
```
|
||||
|
||||
- Lookup the NodePort number and connect to it:
|
||||
```bash
|
||||
kubectl get services
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Scaling up the cluster
|
||||
|
||||
- We can see on Cerebro that the cluster is "yellow"
|
||||
|
||||
(because our index is not replicated)
|
||||
|
||||
- Let's change that!
|
||||
|
||||
.exercise[
|
||||
|
||||
- Edit the ElasticSearch cluster manifest:
|
||||
```bash
|
||||
kubectl edit es demo
|
||||
```
|
||||
|
||||
- Find the field `count: 1` and change it to 3
|
||||
|
||||
- Save and quit
|
||||
|
||||
<!--
|
||||
```wait Please edit```
|
||||
```keys /count:```
|
||||
```key ^J```
|
||||
```keys $r3:x```
|
||||
```key ^J```
|
||||
-->
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Deploying our apps with operators
|
||||
|
||||
- It is very simple to deploy with `kubectl create deployment` / `kubectl expose`
|
||||
@@ -602,9 +139,9 @@ After the Kibana UI loads, we need to click around a bit
|
||||
|
||||
## Operators are not magic
|
||||
|
||||
- Look at the ElasticSearch resource definition
|
||||
- Look at this ElasticSearch resource definition:
|
||||
|
||||
(`~/container.training/k8s/eck-elasticsearch.yaml`)
|
||||
@@LINK[k8s/eck-elasticsearch.yaml]
|
||||
|
||||
- What should happen if we flip the TLS flag? Twice?
|
||||
|
||||
@@ -619,7 +156,4 @@ But we need to know exactly the scenarios that they can handle.*
|
||||
???
|
||||
|
||||
:EN:- Kubernetes operators
|
||||
:EN:- Deploying ElasticSearch with ECK
|
||||
|
||||
:FR:- Les opérateurs
|
||||
:FR:- Déployer ElasticSearch avec ECK
|
||||
|
||||
@@ -387,7 +387,7 @@ spec:
|
||||
|
||||
- Get a shell in the pod, as the `postgres` user:
|
||||
```bash
|
||||
kubectl exec -ti postgres-0 su postgres
|
||||
kubectl exec -ti postgres-0 -- su postgres
|
||||
```
|
||||
|
||||
<!--
|
||||
@@ -577,7 +577,7 @@ By "disrupt" we mean: "disconnect it from the network".
|
||||
|
||||
- Get a shell on the pod:
|
||||
```bash
|
||||
kubectl exec -ti postgres-0 su postgres
|
||||
kubectl exec -ti postgres-0 -- su postgres
|
||||
```
|
||||
|
||||
<!--
|
||||
|
||||
@@ -218,7 +218,7 @@ We need to:
|
||||
|
||||
---
|
||||
|
||||
## Step 2: add the `stable` repo
|
||||
## Step 2: add the `prometheus-community` repo
|
||||
|
||||
- This will add the repository containing the chart for Prometheus
|
||||
|
||||
@@ -230,7 +230,8 @@ We need to:
|
||||
|
||||
- Add the repository:
|
||||
```bash
|
||||
helm repo add stable https://kubernetes-charts.storage.googleapis.com/
|
||||
helm repo add prometheus-community \
|
||||
https://prometheus-community.github.io/helm-charts
|
||||
```
|
||||
|
||||
]
|
||||
@@ -247,7 +248,7 @@ We need to:
|
||||
|
||||
- Install Prometheus on our cluster:
|
||||
```bash
|
||||
helm upgrade prometheus stable/prometheus \
|
||||
helm upgrade prometheus prometheus-community/prometheus \
|
||||
--install \
|
||||
--namespace kube-system \
|
||||
--set server.service.type=NodePort \
|
||||
@@ -266,17 +267,19 @@ class: extra-details
|
||||
|
||||
## Explaining all the Helm flags
|
||||
|
||||
- `helm upgrade prometheus` → upgrade release "prometheus" to the latest version...
|
||||
- `helm upgrade prometheus` → upgrade the release named `prometheus` ...
|
||||
<br/>
|
||||
(a "release" is an instance of an app deployed with Helm)
|
||||
|
||||
(a "release" is a unique name given to an app deployed with Helm)
|
||||
- `prometheus-community/...` → of a chart located in the `prometheus-community` repo ...
|
||||
|
||||
- `stable/prometheus` → ... of the chart `prometheus` in repo `stable`
|
||||
- `.../prometheus` → in that repo, get the chart named `prometheus` ...
|
||||
|
||||
- `--install` → if the app doesn't exist, create it
|
||||
- `--install` → if the app doesn't exist, create it ...
|
||||
|
||||
- `--namespace kube-system` → put it in that specific namespace
|
||||
- `--namespace kube-system` → put it in that specific namespace ...
|
||||
|
||||
- And set the following *values* when rendering the chart's templates:
|
||||
- ... and set the following *values* when rendering the chart's templates:
|
||||
|
||||
- `server.service.type=NodePort` → expose the Prometheus server with a NodePort
|
||||
- `server.service.nodePort=30090` → set the specific NodePort number to use
|
||||
|
||||
@@ -56,8 +56,6 @@
|
||||
|
||||
- Exceeding the memory limit will cause the container to be killed
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Limits vs requests
|
||||
@@ -122,13 +120,17 @@ Each pod is assigned a QoS class (visible in `status.qosClass`).
|
||||
|
||||
- The semantics of memory and swap limits on Linux cgroups are complex
|
||||
|
||||
- In particular, it's not possible to disable swap for a cgroup
|
||||
- With cgroups v1, it's not possible to disable swap for a cgroup
|
||||
|
||||
(the closest option is to [reduce "swappiness"](https://unix.stackexchange.com/questions/77939/turning-off-swapping-for-only-one-process-with-cgroups))
|
||||
|
||||
- It is possible with cgroups v2 (see the [kernel docs](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html) and the [fbatx docs](https://facebookmicrosites.github.io/cgroup2/docs/memory-controller.html#using-swap))
|
||||
|
||||
- Cgroups v2 aren't widely deployed yet
|
||||
|
||||
- The architects of Kubernetes wanted to ensure that Guaranteed pods never swap
|
||||
|
||||
- The only solution was to disable swap entirely
|
||||
- The simplest solution was to disable swap entirely
|
||||
|
||||
---
|
||||
|
||||
@@ -518,6 +520,26 @@ services.nodeports 0 0
|
||||
|
||||
---
|
||||
|
||||
## Viewing a namespace limits and quotas
|
||||
|
||||
- `kubectl describe namespace` will display resource limits and quotas
|
||||
|
||||
.exercise[
|
||||
|
||||
- Try it out:
|
||||
```bash
|
||||
kubectl describe namespace default
|
||||
```
|
||||
|
||||
- View limits and quotas for *all* namespaces:
|
||||
```bash
|
||||
kubectl describe namespace
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Additional resources
|
||||
|
||||
- [A Practical Guide to Setting Kubernetes Requests and Limits](http://blog.kubecost.com/blog/requests-and-limits/)
|
||||
|
||||
310
slides/k8s/sealed-secrets.md
Normal file
310
slides/k8s/sealed-secrets.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# Sealed Secrets
|
||||
|
||||
- Kubernetes provides the "Secret" resource to store credentials, keys, passwords ...
|
||||
|
||||
- Secrets can be protected with RBAC
|
||||
|
||||
(e.g. "you can write secrets, but only the app's service account can read them")
|
||||
|
||||
- [Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets) is an operator that lets us store secrets in code repositories
|
||||
|
||||
- It uses asymetric cryptography:
|
||||
|
||||
- anyone can *encrypt* a secret
|
||||
|
||||
- only the cluster can *decrypt* a secret
|
||||
|
||||
---
|
||||
|
||||
## Principle
|
||||
|
||||
- The Sealed Secrets operator uses a *public* and a *private* key
|
||||
|
||||
- The public key is available publicly (duh!)
|
||||
|
||||
- We use the public key to encrypt secrets into a SealedSecret resource
|
||||
|
||||
- the SealedSecret resource can be stored in a code repo (even a public one)
|
||||
|
||||
- The SealedSecret resource is `kubectl apply`'d to the cluster
|
||||
|
||||
- The Sealed Secrets controller decrypts the SealedSecret with the private key
|
||||
|
||||
(this creates a classic Secret resource)
|
||||
|
||||
- Nobody else can decrypt secrets, since only the controller has the private key
|
||||
|
||||
---
|
||||
|
||||
## In action
|
||||
|
||||
- We will install the Sealed Secrets operator
|
||||
|
||||
- We will generate a Secret
|
||||
|
||||
- We will "seal" that Secret (generate a SealedSecret)
|
||||
|
||||
- We will load that SealedSecret on the cluster
|
||||
|
||||
- We will check that we now have a Secret
|
||||
|
||||
---
|
||||
|
||||
## Installing the operator
|
||||
|
||||
- The official installation is done through a single YAML file
|
||||
|
||||
- There is also a Helm chart if you prefer that
|
||||
|
||||
.exercise[
|
||||
|
||||
- Install the operator:
|
||||
.small[
|
||||
```bash
|
||||
kubectl apply -f \
|
||||
https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.13.1/controller.yaml
|
||||
```
|
||||
]
|
||||
|
||||
]
|
||||
|
||||
Note: it installs into `kube-system` by default.
|
||||
|
||||
If you change that, you will also need to inform `kubeseal` later on.
|
||||
|
||||
---
|
||||
|
||||
## Creating a Secret
|
||||
|
||||
- Let's create a normal (unencrypted) secret
|
||||
|
||||
.exercise[
|
||||
|
||||
- Create a Secret with a couple of API tokens:
|
||||
```bash
|
||||
kubectl create secret generic awskey \
|
||||
--from-literal=AWS_ACCESS_KEY_ID=AKI... \
|
||||
--from-literal=AWS_SECRET_ACCESS_KEY=abc123xyz... \
|
||||
--dry-run=client -o yaml > secret-aws.yaml
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
- Note the `--dry-run` and `-o yaml`
|
||||
|
||||
(we're just generating YAML, not sending the secrets to our Kubernetes cluster)
|
||||
|
||||
- We could also write the YAML from scratch or generate it with other tools
|
||||
|
||||
---
|
||||
|
||||
## Creating a Sealed Secret
|
||||
|
||||
- This is done with the `kubeseal` tool
|
||||
|
||||
- It will obtain the public key from the cluster
|
||||
|
||||
.exercise[
|
||||
|
||||
- Create the Sealed Secret:
|
||||
```bash
|
||||
kubeseal < secret-aws.yaml > sealed-secret-aws.json
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
- The file `sealed-secret-aws.json` can be committed to your public repo
|
||||
|
||||
(if you prefer YAML output, you can add `-o yaml`)
|
||||
|
||||
---
|
||||
|
||||
## Using a Sealed Secret
|
||||
|
||||
- Now let's `kubectl apply` that Sealed Secret to the cluster
|
||||
|
||||
- The Sealed Secret controller will "unseal" it for us
|
||||
|
||||
.exercise[
|
||||
|
||||
- Check that our Secret doesn't exist (yet):
|
||||
```bash
|
||||
kubectl get secrets
|
||||
```
|
||||
|
||||
- Load the Sealed Secret into the cluster:
|
||||
```bash
|
||||
kubectl create -f sealed-secret-aws.json
|
||||
```
|
||||
|
||||
- Check that the secret is now available:
|
||||
```bash
|
||||
kubectl get secrets
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Tweaking secrets
|
||||
|
||||
- Let's see what happens if we try to rename the Secret
|
||||
|
||||
(or use it in a different namespace)
|
||||
|
||||
.exercise[
|
||||
|
||||
- Delete both the Secret and the SealedSecret
|
||||
|
||||
- Edit `sealed-secret-aws.json`
|
||||
|
||||
- Change the name of the secret, or its namespace
|
||||
|
||||
(both in the SealedSecret metadata and in the Secret template)
|
||||
|
||||
- `kubectl apply -f` the new JSON file and observe the results 🤔
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Sealed Secrets are *scoped*
|
||||
|
||||
- A SealedSecret cannot be renamed or moved to another namespace
|
||||
|
||||
(at least, not by default!)
|
||||
|
||||
- Otherwise, it would allow to evade RBAC rules:
|
||||
|
||||
- if I can view Secrets in namespace `myapp` but not in namespace `yourapp`
|
||||
|
||||
- I could take a SealedSecret belonging to namespace `yourapp`
|
||||
|
||||
- ... and deploy it in `myapp`
|
||||
|
||||
- ... and view the resulting decrypted Secret!
|
||||
|
||||
- This can be changed with `--scope namespace-wide` or `--scope cluster-wide`
|
||||
|
||||
---
|
||||
|
||||
## Working offline
|
||||
|
||||
- We can obtain the public key from the server
|
||||
|
||||
(technically, as a PEM certificate)
|
||||
|
||||
- Then we can use that public key offline
|
||||
|
||||
(without contacting the server)
|
||||
|
||||
- Relevant commands:
|
||||
|
||||
`kubeseal --fetch-cert > seal.pem`
|
||||
|
||||
`kubeseal --cert seal.pem < secret.yaml > sealedsecret.json`
|
||||
|
||||
---
|
||||
|
||||
## Key rotation
|
||||
|
||||
- The controller generate new keys every month by default
|
||||
|
||||
- The keys are kept as TLS Secrets in the `kube-system` namespace
|
||||
|
||||
(named `sealed-secrets-keyXXXXX`)
|
||||
|
||||
- When keys are "rotated", old decryption keys are kept
|
||||
|
||||
(otherwise we can't decrypt previously-generated SealedSecrets)
|
||||
|
||||
---
|
||||
|
||||
## Key compromise
|
||||
|
||||
- If the *sealing* key (obtained with `--fetch-cert` is compromised):
|
||||
|
||||
*we don't need to do anything (it's a public key!)*
|
||||
|
||||
- However, if the *unsealing* key (the TLS secret in `kube-system`) is compromised ...
|
||||
|
||||
*we need to:*
|
||||
|
||||
- rotate the key
|
||||
|
||||
- rotate the SealedSecrets that were encrypted with that key
|
||||
<br/>
|
||||
(as they are compromised)
|
||||
|
||||
---
|
||||
|
||||
## Rotating the key
|
||||
|
||||
- By default, new keys are generated every 30 days
|
||||
|
||||
- To force the generation of a new key "right now":
|
||||
|
||||
- obtain an RFC1123 timestamp with `date -R`
|
||||
|
||||
- edit Deployment `sealed-secrets-controller` (in `kube-system`)
|
||||
|
||||
- add `--key-cutoff-time=TIMESTAMP` to the command-line
|
||||
|
||||
- *Then*, rotate the SealedSecrets that were encrypted with it
|
||||
|
||||
(generate new Secrets, then encrypt them with the new key)
|
||||
|
||||
---
|
||||
|
||||
## Discussion (the good)
|
||||
|
||||
- The footprint of the operator is rather small:
|
||||
|
||||
- only one CRD
|
||||
|
||||
- one Deployment, one Service
|
||||
|
||||
- a few RBAC-related objects
|
||||
|
||||
---
|
||||
|
||||
## Discussion (the less good)
|
||||
|
||||
- Events could be improved
|
||||
|
||||
- `no key to decrypt secret` when there is a name/namespace mismatch
|
||||
|
||||
- no event indicating that a SealedSecret was successfully unsealed
|
||||
|
||||
- Key rotation could be improved (how to find secrets corresponding to a key?)
|
||||
|
||||
- If the sealing keys are lost, it's impossible to unseal the SealedSecrets
|
||||
|
||||
(e.g. cluster reinstall)
|
||||
|
||||
- ... Which means that we need to back up the sealing keys
|
||||
|
||||
- ... Which means that we need to be super careful with these backups!
|
||||
|
||||
---
|
||||
|
||||
## Other approaches
|
||||
|
||||
- [Kamus](https://kamus.soluto.io/) ([git](https://github.com/Soluto/kamus)) offers "zero-trust" secrets
|
||||
|
||||
(the cluster cannot decrypt secrets; only the application can decrypt them)
|
||||
|
||||
- [Vault](https://learn.hashicorp.com/tutorials/vault/kubernetes-sidecar?in=vault/kubernetes) can do ... a lot
|
||||
|
||||
- dynamic secrets (generated on the fly for a consumer)
|
||||
|
||||
- certificate management
|
||||
|
||||
- integration outside of Kubernetes
|
||||
|
||||
- and much more!
|
||||
|
||||
???
|
||||
|
||||
:EN:- The Sealed Secrets Operator
|
||||
:FR:- L'opérateur *Sealed Secrets*
|
||||
@@ -24,8 +24,6 @@
|
||||
|
||||
- Gives you one cluster with one node
|
||||
|
||||
- Rather old version of Kubernetes
|
||||
|
||||
- Very easy to use if you are already using Docker Desktop:
|
||||
|
||||
go to Docker Desktop preferences and enable Kubernetes
|
||||
@@ -54,25 +52,21 @@
|
||||
|
||||
## k3d in action
|
||||
|
||||
- Get `k3d` beta 3 binary on https://github.com/rancher/k3d/releases
|
||||
- Install `k3d` (e.g. get the binary from https://github.com/rancher/k3d/releases)
|
||||
|
||||
- Create a simple cluster:
|
||||
```bash
|
||||
k3d create cluster petitcluster --update-kubeconfig
|
||||
```
|
||||
|
||||
- Use it:
|
||||
```bash
|
||||
kubectl config use-context k3d-petitcluster
|
||||
k3d cluster create petitcluster
|
||||
```
|
||||
|
||||
- Create a more complex cluster with a custom version:
|
||||
```bash
|
||||
k3d create cluster groscluster --update-kubeconfig \
|
||||
--image rancher/k3s:v1.18.3-k3s1 --masters 3 --workers 5 --api-port 6444
|
||||
```
|
||||
k3d cluster create groscluster \
|
||||
--image rancher/k3s:v1.18.9-k3s1 --servers 3 --agents 5
|
||||
|
||||
(note: API port seems to be necessary when running multiple clusters)
|
||||
(3 nodes for the control plane + 5 worker nodes)
|
||||
|
||||
- Clusters are automatically added to `.kube/config` file
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
- They are stopped in reverse order (from *R-1* to 0)
|
||||
|
||||
- Each pod know its identity (i.e. which number it is in the set)
|
||||
- Each pod knows its identity (i.e. which number it is in the set)
|
||||
|
||||
- Each pod can discover the IP address of the others easily
|
||||
|
||||
@@ -218,7 +218,9 @@ consul agent -data-dir=/consul/data -client=0.0.0.0 -server -ui \
|
||||
|
||||
- Replace X.X.X.X and Y.Y.Y.Y with the addresses of other nodes
|
||||
|
||||
- The same command-line can be used on all nodes (convenient!)
|
||||
- A node can add its own address (it will work fine)
|
||||
|
||||
- ... Which means that we can use the same command-line on all nodes (convenient!)
|
||||
|
||||
---
|
||||
|
||||
@@ -258,19 +260,13 @@ consul agent -data-dir=/consul/data -client=0.0.0.0 -server -ui \
|
||||
|
||||
## Putting it all together
|
||||
|
||||
- The file `k8s/consul.yaml` defines the required resources
|
||||
- The file `k8s/consul-1.yaml` defines the required resources
|
||||
|
||||
(service account, cluster role, cluster role binding, service, stateful set)
|
||||
(service account, role, role binding, service, stateful set)
|
||||
|
||||
- It has a few extra touches:
|
||||
- Inspired by this [excellent tutorial](https://github.com/kelseyhightower/consul-on-kubernetes) by Kelsey Hightower
|
||||
|
||||
- a `podAntiAffinity` prevents two pods from running on the same node
|
||||
|
||||
- a `preStop` hook makes the pod leave the cluster when shutdown gracefully
|
||||
|
||||
This was inspired by this [excellent tutorial](https://github.com/kelseyhightower/consul-on-kubernetes) by Kelsey Hightower.
|
||||
Some features from the original tutorial (TLS authentication between
|
||||
nodes and encryption of gossip traffic) were removed for simplicity.
|
||||
(many features from the original tutorial were removed for simplicity)
|
||||
|
||||
---
|
||||
|
||||
@@ -282,7 +278,7 @@ nodes and encryption of gossip traffic) were removed for simplicity.
|
||||
|
||||
- Create the stateful set and associated service:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/consul.yaml
|
||||
kubectl apply -f ~/container.training/k8s/consul-1.yaml
|
||||
```
|
||||
|
||||
- Check the logs as the pods come up one after another:
|
||||
@@ -297,7 +293,7 @@ nodes and encryption of gossip traffic) were removed for simplicity.
|
||||
|
||||
- Check the health of the cluster:
|
||||
```bash
|
||||
kubectl exec consul-0 consul members
|
||||
kubectl exec consul-0 -- consul members
|
||||
```
|
||||
|
||||
]
|
||||
@@ -306,6 +302,88 @@ nodes and encryption of gossip traffic) were removed for simplicity.
|
||||
|
||||
## Caveats
|
||||
|
||||
- The scheduler may place two Consul pods on the same node
|
||||
|
||||
- if that node fails, we lose two Consul pods at the same time
|
||||
- this will cause the cluster to fail
|
||||
|
||||
- Scaling down the cluster will cause it to fail
|
||||
|
||||
- when a Consul member leaves the cluster, it needs to inform the others
|
||||
- otherwise, the last remaining node doesn't have quorum and stops functioning
|
||||
|
||||
- This Consul cluster doesn't use real persistence yet
|
||||
|
||||
- data is stored in the containers' ephemeral filesystem
|
||||
- if a pod fails, its replacement starts from a blank slate
|
||||
|
||||
---
|
||||
|
||||
## Improving pod placement
|
||||
|
||||
- We need to tell the scheduler:
|
||||
|
||||
*do not put two of these pods on the same node!*
|
||||
|
||||
- This is done with an `affinity` section like the following one:
|
||||
```yaml
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
- labelSelector:
|
||||
matchExpressions:
|
||||
- key: app
|
||||
operator: In
|
||||
values:
|
||||
- consul
|
||||
topologyKey: kubernetes.io/hostname
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using a lifecycle hook
|
||||
|
||||
- When a Consul member leaves the cluster, it needs to execute:
|
||||
```bash
|
||||
consul leave
|
||||
```
|
||||
|
||||
- This is done with a `lifecycle` section like the following one:
|
||||
```yaml
|
||||
lifecycle:
|
||||
preStop:
|
||||
exec:
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- consul leave
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Running a better Consul cluster
|
||||
|
||||
- Let's try to add the scheduling constraint and lifecycle hook
|
||||
|
||||
- We can do that in the same namespace or another one (as we like)
|
||||
|
||||
- If we do that in the same namespace, we will see a rolling update
|
||||
|
||||
(pods will be replaced one by one)
|
||||
|
||||
.exercise[
|
||||
|
||||
- Deploy a better Consul cluster:
|
||||
```bash
|
||||
kubectl apply -f ~/container.training/k8s/consul-2.yaml
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Still no persistence, though
|
||||
|
||||
- We aren't using actual persistence yet
|
||||
|
||||
(no `volumeClaimTemplate`, Persistent Volume, etc.)
|
||||
|
||||
218
slides/k8s/user-cert.md
Normal file
218
slides/k8s/user-cert.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# Generating user certificates
|
||||
|
||||
- The most popular ways to authenticate users with Kubernetes are:
|
||||
|
||||
- TLS certificates
|
||||
|
||||
- JSON Web Tokens (OIDC or ServiceAccount tokens)
|
||||
|
||||
- We're going to see how to use TLS certificates
|
||||
|
||||
- We will generate a certificate for an user and give them some permissions
|
||||
|
||||
- Then we will use that certificate to access the cluster
|
||||
|
||||
---
|
||||
|
||||
## Heads up!
|
||||
|
||||
- The demos in this section require that we have access to our cluster's CA
|
||||
|
||||
- This is easy if we are using a cluster deployed with `kubeadm`
|
||||
|
||||
- Otherwise, we may or may not have access to the cluster's CA
|
||||
|
||||
- We may or may not be able to use the CSR API instead
|
||||
|
||||
---
|
||||
|
||||
## Check that we have access to the CA
|
||||
|
||||
- Make sure that you are logged on the node hosting the control plane
|
||||
|
||||
(if a cluster has been provisioned for you for a training, it's `node1`)
|
||||
|
||||
.exercise[
|
||||
|
||||
- Check that the CA key is here:
|
||||
```bash
|
||||
sudo ls -l /etc/kubernetes/pki
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
The output should include `ca.key` and `ca.crt`.
|
||||
|
||||
---
|
||||
|
||||
## How it works
|
||||
|
||||
- The API server is configured to accept all certificates signed by a given CA
|
||||
|
||||
- The certificate contains:
|
||||
|
||||
- the user name (in the `CN` field)
|
||||
|
||||
- the groups the user belongs to (as multiple `O` fields)
|
||||
|
||||
.exercise[
|
||||
|
||||
- Check which CA is used by the Kubernetes API server:
|
||||
```bash
|
||||
sudo grep crt /etc/kubernetes/manifests/kube-apiserver.yaml
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
This is the flag that we're looking for:
|
||||
```
|
||||
--client-ca-file=/etc/kubernetes/pki/ca.crt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Generating a key and CSR for our user
|
||||
|
||||
- These operations could be done on a separate machine
|
||||
|
||||
- We only need to transfer the CSR (Certificate Signing Request) to the CA
|
||||
|
||||
(we never need to expoes the private key)
|
||||
|
||||
.exercise[
|
||||
|
||||
- Generate a private key:
|
||||
```bash
|
||||
openssl genrsa 4096 > user.key
|
||||
```
|
||||
|
||||
- Generate a CSR:
|
||||
```bash
|
||||
openssl req -new -key user.key -subj /CN=jerome/O=devs/O=ops > user.csr
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Generating a signed certificate
|
||||
|
||||
- This has to be done on the machine holding the CA private key
|
||||
|
||||
(copy the `user.csr` file if needed)
|
||||
|
||||
.exercise[
|
||||
|
||||
- Verify the CSR paramters:
|
||||
```bash
|
||||
openssl req -in user.csr -text | head
|
||||
```
|
||||
|
||||
- Generate the certificate:
|
||||
```bash
|
||||
sudo openssl x509 -req \
|
||||
-CA /etc/kubernetes/pki/ca.crt -CAkey /etc/kubernetes/pki/ca.key \
|
||||
-in user.csr -days 1 -set_serial 1234 > user.crt
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
If you are using two separate machines, transfer `user.crt` to the other machine.
|
||||
|
||||
---
|
||||
|
||||
## Adding the key and certificate to kubeconfig
|
||||
|
||||
- We have to edit our `.kube/config` file
|
||||
|
||||
- This can be done relatively easily with `kubectl config`
|
||||
|
||||
.exercise[
|
||||
|
||||
- Create a new `user` entry in our `.kube/config` file:
|
||||
```bash
|
||||
kubectl config set-credentials jerome \
|
||||
--client-key=user.key --client-certificate=user.crt
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
The configuration file now points to our local files.
|
||||
|
||||
We could also embed the key and certs with the `--embed-certs` option.
|
||||
|
||||
(So that the kubeconfig file can be used without `user.key` and `user.crt`.)
|
||||
|
||||
---
|
||||
|
||||
## Using the new identity
|
||||
|
||||
- At the moment, we probably use the admin certificate generated by `kubeadm`
|
||||
|
||||
(with `CN=kubernetes-admin` and `O=system:masters`)
|
||||
|
||||
- Let's edit our *context* to use our new certificate instead!
|
||||
|
||||
.exercise[
|
||||
|
||||
- Edit the context:
|
||||
```bash
|
||||
kubectl config set-context --current --user=jerome
|
||||
```
|
||||
|
||||
- Try any command:
|
||||
```bash
|
||||
kubectl get pods
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
Access will be denied, but we should see that were correctly *authenticated* as `jerome`.
|
||||
|
||||
---
|
||||
|
||||
## Granting permissions
|
||||
|
||||
- Let's add some read-only permissions to the `devs` group (for instance)
|
||||
|
||||
.exercise[
|
||||
|
||||
- Switch back to our admin identity:
|
||||
```bash
|
||||
kubectl config set-context --current --user=kubernetes-admin
|
||||
```
|
||||
|
||||
- Grant permissions:
|
||||
```bash
|
||||
kubectl create clusterrolebinding devs-can-view \
|
||||
--clusterrole=view --group=devs
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
---
|
||||
|
||||
## Testing the new permissions
|
||||
|
||||
- As soon as we create the ClusterRoleBinding, all users in the `devs` group get access
|
||||
|
||||
- Let's verify that we can e.g. list pods!
|
||||
|
||||
.exercise[
|
||||
|
||||
- Switch to our user identity again:
|
||||
```bash
|
||||
kubectl config set-context --current --user=jerome
|
||||
```
|
||||
|
||||
- Test the permissions:
|
||||
```bash
|
||||
kubectl get pods
|
||||
```
|
||||
|
||||
]
|
||||
|
||||
???
|
||||
|
||||
:EN:- Authentication with user certificates
|
||||
:FR:- Identification par certificat TLS
|
||||
@@ -1,7 +1,7 @@
|
||||
## Versions installed
|
||||
|
||||
- Kubernetes 1.18.0
|
||||
- Docker Engine 19.03.8
|
||||
- Kubernetes 1.19.2
|
||||
- Docker Engine 19.03.13
|
||||
- Docker Compose 1.25.4
|
||||
|
||||
<!-- ##VERSION## -->
|
||||
|
||||
80
slides/kube-adv.yml
Normal file
80
slides/kube-adv.yml
Normal file
@@ -0,0 +1,80 @@
|
||||
title: |
|
||||
Advanced
|
||||
Kubernetes
|
||||
|
||||
chat: "`#kubernetes-training-november-30-december-4`"
|
||||
#chat: "[Gitter](https://gitter.im/jpetazzo/workshop-yyyymmdd-city)"
|
||||
|
||||
gitrepo: github.com/jpetazzo/container.training
|
||||
|
||||
slides: https://2020-11-nr.container.training/
|
||||
|
||||
#slidenumberprefix: "#SomeHashTag — "
|
||||
|
||||
exclude:
|
||||
- self-paced
|
||||
|
||||
content:
|
||||
- shared/title.md
|
||||
- logistics.md
|
||||
- k8s/intro.md
|
||||
- shared/about-slides.md
|
||||
#- shared/chat-room-im.md
|
||||
#- shared/chat-room-zoom-meeting.md
|
||||
#- shared/chat-room-zoom-webinar.md
|
||||
- shared/toc.md
|
||||
- #1
|
||||
- k8s/prereqs-admin.md
|
||||
- k8s/architecture.md
|
||||
- k8s/deploymentslideshow.md
|
||||
- k8s/dmuc.md
|
||||
- #2
|
||||
- k8s/multinode.md
|
||||
- k8s/cni.md
|
||||
- k8s/interco.md
|
||||
- #3
|
||||
- k8s/cni-internals.md
|
||||
- k8s/apilb.md
|
||||
- k8s/control-plane-auth.md
|
||||
- |
|
||||
# (Extra content)
|
||||
- k8s/staticpods.md
|
||||
- k8s/cluster-upgrade.md
|
||||
- #4
|
||||
- k8s/kustomize.md
|
||||
- k8s/helm-intro.md
|
||||
- k8s/helm-chart-format.md
|
||||
- k8s/helm-create-basic-chart.md
|
||||
- |
|
||||
# (Extra content)
|
||||
- k8s/helm-create-better-chart.md
|
||||
- k8s/helm-secrets.md
|
||||
- #5
|
||||
- k8s/extending-api.md
|
||||
- k8s/operators.md
|
||||
- k8s/sealed-secrets.md
|
||||
- k8s/crd.md
|
||||
- #6
|
||||
- k8s/ingress-tls.md
|
||||
- k8s/cert-manager.md
|
||||
- k8s/eck.md
|
||||
- #7
|
||||
- k8s/admission.md
|
||||
- k8s/kyverno.md
|
||||
- #8
|
||||
- k8s/aggregation-layer.md
|
||||
- k8s/metrics-server.md
|
||||
- k8s/prometheus.md
|
||||
- k8s/hpa-v2.md
|
||||
- #9
|
||||
- k8s/operators-design.md
|
||||
- k8s/kubebuilder.md
|
||||
- k8s/events.md
|
||||
- k8s/finalizers.md
|
||||
- |
|
||||
# (Extra content)
|
||||
- k8s/owners-and-dependents.md
|
||||
- k8s/apiserver-deepdive.md
|
||||
#- k8s/record.md
|
||||
- shared/thankyou.md
|
||||
|
||||
116
slides/kube-fullday.yml
Normal file
116
slides/kube-fullday.yml
Normal file
@@ -0,0 +1,116 @@
|
||||
title: |
|
||||
Deploying and Scaling Microservices
|
||||
with Kubernetes
|
||||
|
||||
#chat: "[Slack](https://dockercommunity.slack.com/messages/C7GKACWDV)"
|
||||
#chat: "[Gitter](https://gitter.im/jpetazzo/workshop-yyyymmdd-city)"
|
||||
chat: "In person!"
|
||||
|
||||
gitrepo: github.com/jpetazzo/container.training
|
||||
|
||||
slides: http://container.training/
|
||||
|
||||
#slidenumberprefix: "#SomeHashTag — "
|
||||
|
||||
exclude:
|
||||
- self-paced
|
||||
|
||||
content:
|
||||
- shared/title.md
|
||||
- logistics.md
|
||||
- k8s/intro.md
|
||||
- shared/about-slides.md
|
||||
- shared/chat-room-im.md
|
||||
#- shared/chat-room-zoom-meeting.md
|
||||
#- shared/chat-room-zoom-webinar.md
|
||||
- shared/toc.md
|
||||
-
|
||||
- shared/prereqs.md
|
||||
#- shared/webssh.md
|
||||
- shared/connecting.md
|
||||
#- k8s/versions-k8s.md
|
||||
- shared/sampleapp.md
|
||||
#- shared/composescale.md
|
||||
#- shared/hastyconclusions.md
|
||||
- shared/composedown.md
|
||||
- k8s/concepts-k8s.md
|
||||
- k8s/kubectlget.md
|
||||
-
|
||||
- k8s/kubectl-run.md
|
||||
- k8s/batch-jobs.md
|
||||
- k8s/labels-annotations.md
|
||||
- k8s/kubectl-logs.md
|
||||
- k8s/logs-cli.md
|
||||
- shared/declarative.md
|
||||
- k8s/declarative.md
|
||||
- k8s/deploymentslideshow.md
|
||||
- k8s/kubenet.md
|
||||
- k8s/kubectlexpose.md
|
||||
- k8s/shippingimages.md
|
||||
#- k8s/buildshiprun-selfhosted.md
|
||||
- k8s/buildshiprun-dockerhub.md
|
||||
- k8s/ourapponkube.md
|
||||
#- k8s/exercise-wordsmith.md
|
||||
-
|
||||
- k8s/yamldeploy.md
|
||||
- k8s/setup-overview.md
|
||||
#- k8s/setup-devel.md
|
||||
#- k8s/setup-managed.md
|
||||
#- k8s/setup-selfhosted.md
|
||||
#- k8s/dashboard.md
|
||||
#- k8s/kubectlscale.md
|
||||
- k8s/scalingdockercoins.md
|
||||
- shared/hastyconclusions.md
|
||||
- k8s/daemonset.md
|
||||
#- k8s/dryrun.md
|
||||
#- k8s/exercise-yaml.md
|
||||
#- k8s/localkubeconfig.md
|
||||
#- k8s/accessinternal.md
|
||||
#- k8s/kubectlproxy.md
|
||||
- k8s/rollout.md
|
||||
#- k8s/healthchecks.md
|
||||
#- k8s/healthchecks-more.md
|
||||
#- k8s/record.md
|
||||
-
|
||||
- k8s/namespaces.md
|
||||
- k8s/ingress.md
|
||||
#- k8s/ingress-tls.md
|
||||
#- k8s/kustomize.md
|
||||
#- k8s/helm-intro.md
|
||||
#- k8s/helm-chart-format.md
|
||||
#- k8s/helm-create-basic-chart.md
|
||||
#- k8s/helm-create-better-chart.md
|
||||
#- k8s/helm-secrets.md
|
||||
#- k8s/exercise-helm.md
|
||||
#- k8s/create-chart.md
|
||||
#- k8s/create-more-charts.md
|
||||
#- k8s/netpol.md
|
||||
#- k8s/authn-authz.md
|
||||
#- k8s/user-cert.md
|
||||
#- k8s/csr-api.md
|
||||
#- k8s/openid-connect.md
|
||||
#- k8s/podsecuritypolicy.md
|
||||
- k8s/volumes.md
|
||||
#- k8s/exercise-configmap.md
|
||||
#- k8s/build-with-docker.md
|
||||
#- k8s/build-with-kaniko.md
|
||||
- k8s/configuration.md
|
||||
#- k8s/logs-centralized.md
|
||||
#- k8s/prometheus.md
|
||||
#- k8s/statefulsets.md
|
||||
#- k8s/local-persistent-volumes.md
|
||||
#- k8s/portworx.md
|
||||
#- k8s/extending-api.md
|
||||
#- k8s/crd.md
|
||||
#- k8s/admission.md
|
||||
#- k8s/operators.md
|
||||
#- k8s/operators-design.md
|
||||
#- k8s/staticpods.md
|
||||
#- k8s/finalizers.md
|
||||
#- k8s/owners-and-dependents.md
|
||||
#- k8s/gitworkflows.md
|
||||
-
|
||||
- k8s/whatsnext.md
|
||||
- k8s/lastwords.md
|
||||
- k8s/links.md
|
||||
- shared/thankyou.md
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user