Compare commits

..

103 Commits

Author SHA1 Message Date
Jerome Petazzoni
3797fc6f9f woops update chat link 2020-11-30 16:56:26 +01:00
Jerome Petazzoni
242a3d9ddf Update course schedule 2020-11-30 16:31:12 +01:00
Jerome Petazzoni
a14f5e81ce Merge branch 'master' into 2020-11-nr 2020-11-30 00:29:48 +01:00
Jerome Petazzoni
29edb1aefe Minor tweaks after 1st NR session 2020-11-30 00:29:05 +01:00
Jerome Petazzoni
bd3c91f342 Update udemy promo codes 2020-11-23 12:26:04 +01:00
jsubirat
fa709f0cb4 Update kyverno.md
Adds missing `pod`s in the commands
2020-11-19 17:29:12 +01:00
jsubirat
543b44fb29 Update kyverno.md
Adds missing `pod` in the command
2020-11-19 17:28:54 +01:00
Jérôme Petazzoni
109f6503e4 Merge pull request #574 from jsubirat/patch-1
Update kyverno.md
2020-11-19 17:27:30 +01:00
Jérôme Petazzoni
337be57182 Merge pull request #575 from jsubirat/patch-2
Update kyverno.md
2020-11-19 17:27:03 +01:00
jsubirat
63d88236b2 Update kyverno.md
Adds missing `pod`s in the commands
2020-11-19 12:14:42 +01:00
jsubirat
799aa21302 Update kyverno.md
Adds missing `pod` in the command
2020-11-19 12:11:17 +01:00
Jerome Petazzoni
95247d6d39 put together NR EUR content 2020-11-16 08:27:39 +01:00
Jerome Petazzoni
536a9cc44b Update advanced TOC 2020-11-15 22:06:49 +01:00
Jerome Petazzoni
2ff3d88bab typo 2020-11-15 22:06:38 +01:00
Jerome Petazzoni
295ee9b6b4 Add warning about using CSR API for user certs 2020-11-15 19:29:45 +01:00
Jerome Petazzoni
17c5f6de01 Add cert-manager section 2020-11-15 19:29:35 +01:00
Jerome Petazzoni
556dbb965c Add networking.k8s.io permissions to Traefik v2 2020-11-15 18:44:17 +01:00
Jerome Petazzoni
32250f8053 Update section about swap with cgroups v2 info 2020-11-15 16:44:18 +01:00
Jerome Petazzoni
bdede6de07 Add aggregation layer details 2020-11-14 20:57:27 +01:00
Jerome Petazzoni
eefdc21488 Add details about /status 2020-11-14 19:10:04 +01:00
Jerome Petazzoni
e145428910 Add notes about backups 2020-11-14 14:39:43 +01:00
Jerome Petazzoni
76789b6113 Add Sealed Secrets 2020-11-14 14:35:49 +01:00
Jerome Petazzoni
f9660ba9dc Add kubebuilder tutorial 2020-11-13 18:46:16 +01:00
Jerome Petazzoni
c2497508f8 Add API server deep dive 2020-11-13 15:08:15 +01:00
Jerome Petazzoni
b5d3b213b1 Update CRD section 2020-11-13 12:50:55 +01:00
Jerome Petazzoni
b4c76ad11d Add CNI deep dive 2020-11-12 13:37:33 +01:00
Jerome Petazzoni
b251ff3812 --output-watch-events 2020-11-11 22:46:20 +01:00
Jerome Petazzoni
ede4ea0dd5 Add note about GVK 2020-11-11 21:17:54 +01:00
Jerome Petazzoni
2ab06c6dfd Add events section 2020-11-11 20:51:33 +01:00
Jerome Petazzoni
3a01deb039 Add section on finalizers 2020-11-11 15:05:33 +01:00
Jerome Petazzoni
b88f63e1f7 Update Docker Desktop and k3d instructions
Fixes #572
2020-11-10 17:55:02 +01:00
Jerome Petazzoni
918311ac51 Separate CRD and ECK; reorganize API extension chapter 2020-11-10 17:43:08 +01:00
Jerome Petazzoni
73e8110f09 Tweak 2020-11-10 17:43:08 +01:00
Jerome Petazzoni
ecb5106d59 Add provenance of default RBAC rules 2020-11-10 17:43:08 +01:00
Jérôme Petazzoni
e4d8cd4952 Merge pull request #573 from wrekone/master
Update ingress.md
2020-11-05 06:51:09 +01:00
Ben
c4aedbd327 Update ingress.md
fix typo
2020-11-04 20:19:34 -08:00
Jerome Petazzoni
2fb3584b1b Small update about selectors 2020-11-03 21:59:04 +01:00
Jerome Petazzoni
cb90cc9a1e Rename images 2020-10-31 11:32:16 +01:00
Jerome Petazzoni
bf28dff816 Add HPA v2 content using Prometheus Adapter 2020-10-30 17:55:46 +01:00
Jerome Petazzoni
b5cb871c69 Update Prometheus chart location 2020-10-29 17:39:14 +01:00
Jerome Petazzoni
aa8f538574 Add example to generate certs with local CA 2020-10-29 14:53:42 +01:00
Jerome Petazzoni
ebf2e23785 Add info about advanced label selectors 2020-10-29 12:32:01 +01:00
Jerome Petazzoni
0553a1ba8b Add chapter on Kyverno 2020-10-28 00:00:32 +01:00
Jerome Petazzoni
9d47177028 Add activeDeadlineSeconds explanation 2020-10-27 11:11:29 +01:00
Jerome Petazzoni
9d4a035497 Add Kompose, Skaffold, and Tilt. Move tools to a separate kubetools action. 2020-10-27 10:58:31 +01:00
Jerome Petazzoni
6fe74cb35c Add note about 'kubectl describe ns' 2020-10-24 16:23:36 +02:00
Jerome Petazzoni
43aa41ed51 Add note to remap_nodeports command 2020-10-24 16:23:21 +02:00
Jerome Petazzoni
f6e810f648 Add k9s and popeye 2020-10-24 11:27:33 +02:00
Jerome Petazzoni
4c710d6826 Add Krew support 2020-10-23 21:19:27 +02:00
Jerome Petazzoni
410c98399e Use empty values by default
This allows content rendering with an almost-empty YAML file
2020-10-22 14:13:11 +02:00
Jerome Petazzoni
19c9843a81 Add admission webhook content 2020-10-22 14:12:32 +02:00
Jerome Petazzoni
69d084e04a Update PSP (runtime/default instead of docker/default) 2020-10-20 22:11:26 +02:00
Jerome Petazzoni
1300d76890 Update dashboard content 2020-10-20 21:19:08 +02:00
Jerome Petazzoni
0040313371 Bump up admin clusters scripts 2020-10-20 16:53:24 +02:00
Jerome Petazzoni
c9e04b906d Bump up k8s bins; add 'k' alias and completion 2020-10-20 16:53:24 +02:00
Jérôme Petazzoni
41f66f4144 Merge pull request #571 from bbaassssiiee/bugfix/typo
typo: should read: characters
2020-10-20 11:29:32 +02:00
Bas Meijer
aced587fd0 characters 2020-10-20 11:03:59 +02:00
Jerome Petazzoni
749b3d1648 Add survey form 2020-10-13 16:05:33 +02:00
Jérôme Petazzoni
c40cc71bbc Merge pull request #570 from fc92/patch-2
update server-side dry run for recent kubectl
2020-10-11 23:22:28 +02:00
Jérôme Petazzoni
69b775ef27 Merge pull request #569 from fc92/patch-1
Update dashboard.md
2020-10-11 23:20:51 +02:00
fc92
3bfc14c5f7 update server-side dry run for recent kubectl
Error message :
$ kubectl apply -f web.yaml --server-dry-run --validate=false -o yaml                                                                   
Error: unknown flag: --server-dry-run                                                                                                   
See 'kubectl apply --help' for usage.

Doc : 
      --dry-run='none': Must be "none", "server", or "client". If client strategy, only print the object that would be                  
sent, without sending it. If server strategy, submit server-side request without persisting the resource.
2020-10-10 23:07:45 +02:00
fc92
97984af8a2 Update dashboard.md
Kube Ops View URL changed to
2020-10-10 22:12:21 +02:00
Jérôme Petazzoni
9b31c45899 Merge pull request #567 from christianbumann/patch-1
Add description for the -f flag
2020-10-08 08:37:26 +02:00
Jérôme Petazzoni
c0db28d439 Merge pull request #568 from christianbumann/patch-2
Fix typo
2020-10-08 08:36:38 +02:00
Jérôme Petazzoni
0e49bfa837 Merge pull request #566 from tullo/master
fix backend svc name in cheeseplate ingress
2020-10-08 08:36:11 +02:00
Christian Bumann
fc9c0a6285 Update Container_Network_Model.md 2020-10-08 08:16:53 +02:00
Christian Bumann
d4914fa168 Fix typo 2020-10-08 08:14:59 +02:00
Christian Bumann
e4edd9445c Add description for the -f flag 2020-10-07 14:00:19 +02:00
Andreas Amstutz
ba7deefce5 fix k8s version 2020-10-05 12:06:26 +02:00
Andreas
be104f1b44 fix backend svc name in cheeseplate ingress 2020-10-05 12:02:31 +02:00
Jerome Petazzoni
5c329b0b79 Bump versions 2020-10-04 20:59:36 +02:00
Jerome Petazzoni
78ffd22499 Typo fix 2020-10-04 15:53:40 +02:00
Jerome Petazzoni
33174a1682 Add clean command 2020-09-27 16:25:37 +02:00
Jerome Petazzoni
d402a2ea93 Add tailhist 2020-09-24 17:00:52 +02:00
Jerome Petazzoni
1fc3abcffd Add jid (JSON explorer tool) 2020-09-24 11:52:03 +02:00
Jerome Petazzoni
c1020f24b1 Add Ingress TLS chapter 2020-09-15 17:44:05 +02:00
Jerome Petazzoni
4fc81209d4 Skip comments in domain file 2020-09-14 17:43:11 +02:00
Jerome Petazzoni
ed841711c5 Fix 'list' command 2020-09-14 16:58:55 +02:00
Jerome Petazzoni
07457af6f7 Update Consul section 2020-09-11 22:30:18 +02:00
Jerome Petazzoni
2d4961fbd3 Add fwdays slides 2020-09-11 15:13:24 +02:00
Jerome Petazzoni
14679999be Big refactor of deployment script
Add support for OVHcloud, Hetzner; refactor Scaleway support
2020-09-09 19:37:15 +02:00
Jerome Petazzoni
29c6d2876a Reword sanity check 2020-09-08 11:08:58 +02:00
Jerome Petazzoni
a02e7429ad Add note about httpenv arch 2020-09-07 12:49:08 +02:00
Jerome Petazzoni
fee0be7f09 Update 'kubectl create deployment' for 1.19 2020-09-02 16:48:19 +02:00
Jerome Petazzoni
d98fcbce87 Update Ingress to 1.19 2020-09-02 13:34:11 +02:00
Jerome Petazzoni
35320837e5 Add info about immutable configmaps and secrets 2020-09-02 13:21:21 +02:00
Jerome Petazzoni
d73e597198 Small updates for Kubernetes 1.19 2020-09-02 13:08:04 +02:00
Jerome Petazzoni
b4c0378114 Add ips command to output tab-separated addresses 2020-08-31 16:31:59 +02:00
Jerome Petazzoni
efdc4fcfa9 bump versions 2020-08-26 12:38:51 +02:00
Jerome Petazzoni
c32fcc81bb Tweak 1-day content 2020-08-26 09:10:15 +02:00
Jerome Petazzoni
f6930042bd Mention downward API fields 2020-08-26 09:05:24 +02:00
Jerome Petazzoni
2e2767b090 Bump up kubectl versions in remote section 2020-08-19 13:38:49 +02:00
Jerome Petazzoni
115cc5e0c0 Add support for Scaleway Cloud instances 2020-08-15 14:02:24 +02:00
Jerome Petazzoni
d252fe254b Update DNS script 2020-08-15 12:34:08 +02:00
Jerome Petazzoni
7d96562042 Minor updates after LKE testing 2020-08-12 19:22:57 +02:00
Jerome Petazzoni
4ded8c699d typo 2020-08-05 18:23:37 +02:00
Jérôme Petazzoni
620a3df798 Merge pull request #563 from lucas-foodles/patch-1
Fix typo
2020-08-05 17:28:34 +02:00
Jerome Petazzoni
d28723f07a Add fwdays workshops 2020-08-04 17:21:31 +02:00
Jerome Petazzoni
f2334d2d1b Add skillsmatter dates 2020-07-30 19:11:43 +02:00
Jerome Petazzoni
ddf79eebc7 Add skillsmatter 2020-07-30 19:09:42 +02:00
Jerome Petazzoni
6467264ff5 Add Bret coupon codes; high five online october 2020-07-30 12:11:29 +02:00
lucas-foodles
55fcff9333 Fix typo 2020-07-29 10:46:17 +02:00
Jerome Petazzoni
8fb7ea3908 Use 'sudo port', as per #529 2020-07-09 15:32:21 +02:00
114 changed files with 8700 additions and 1091 deletions

View File

@@ -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

View File

@@ -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
View 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
View 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
View 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
View 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"

View File

@@ -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
View 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

View 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
View 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
View 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: ""

View File

@@ -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

View 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

View File

@@ -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:

View 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: {}

View 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: {}

View 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 }}"

View 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: []

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -1 +1 @@
traefik-v1.yaml
traefik-v2.yaml

View 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

View File

@@ -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"

View 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

View File

@@ -0,0 +1 @@
INFRACLASS=scaleway

View File

@@ -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
}

View File

@@ -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
}

View 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
}

View 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
}

View 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
}

View 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"
}

View File

@@ -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")
#######################

View File

@@ -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 \

View File

@@ -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:

View File

@@ -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

View File

@@ -25,5 +25,6 @@ steps:
- webssh
- tailhist
- kube
- kubetools
- cards
- kubetest

View File

@@ -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

View File

@@ -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}"

View File

@@ -15,7 +15,6 @@ for lib in lib/*.sh; do
done
DEPENDENCIES="
aws
ssh
curl
jq

View File

@@ -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

View File

@@ -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.")

View File

@@ -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

View File

@@ -1,4 +1,4 @@
# Logging (extra material)
# Logging
In this chapter, we will explain the different ways to send logs from containers.

View File

@@ -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:

View File

@@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -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

View File

@@ -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 &mdash; "
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

View File

@@ -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 &mdash; "
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

View File

@@ -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 &mdash; "
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
View 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)

View 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*

View 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

View File

@@ -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 ✔️

View File

@@ -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.

View File

@@ -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
View 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
View 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

View File

@@ -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

View File

@@ -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
View 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)*

View File

@@ -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"

View File

@@ -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

View File

@@ -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"

View File

@@ -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
View 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
View 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*

View File

@@ -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
View 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
View 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
![Latency over time](images/hpa-v2-pa-latency.png)
---
class: pic
![Number of pods over time](images/hpa-v2-pa-pods.png)
---
## 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
View 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*

View File

@@ -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
View 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

View File

@@ -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```
-->
]

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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
```
]

View File

@@ -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`

View File

@@ -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## -->

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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
```
<!--

View File

@@ -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

View File

@@ -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/)

View 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*

View File

@@ -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
---

View 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
View 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

View File

@@ -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
View 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 &mdash; "
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
View 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 &mdash; "
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