Every forecast in this post has been answered and none of them in its own terms: the feature it wanted to make the default is gone so completely that its name now belongs to the mechanism it was built to replace, the command it promised for v1.9 arrived four times under four other verbs, and the one-command upgrade is one command inside sixteen spread across two pages¶
Post — kubeadm v1.8 Released: Introducing Easy Upgrades for Kubernetes Clusters, 2017-10-25, Kubernetes v1.8. Written by Lucas Käldström (Weaveworks), thirteen months after the post that announced the tool.
As written — the post is a release note with a roadmap attached, and the roadmap is the longer half. It announces four things and predicts three.
The headline is "one-command upgrades of the control plane". You check first:
"This gives you information about which versions you can upgrade to, as well as the health of your cluster." Then, if you want to see what an upgrade would do before it does it:
You can examine the effects an upgrade will have on your system by specifying the --dry-run flag. In previous versions of kubeadm, upgrades were essentially blind in that you could only make assumptions about how an upgrade would impact your cluster. With the new dry run feature, there is no more mystery. You can see exactly what applying an upgrade would do before applying it.
And then you upgrade:
The second announcement is a feature gate, and it comes with a definition:
Self-hosting in this context refers to a specific way of setting up the control plane. The self-hosting concept was initially developed by CoreOS in their bootkube project. […] Self-hosting means that the control plane components, the API Server, Controller Manager and Scheduler are workloads themselves in the cluster they run. This means the control plane components can be managed using Kubernetes primitives, which has numerous advantages. […] Rolling upgrades in Kubernetes can be used for upgrades of the control plane components, and next to no extra code has to be written for that to work; it's one of Kubernetes' built-in primitives!
"Self-hosting won't be the default until v1.9.0, but users can easily test the feature in experimental clusters."
The third is modularity — "The inclusion of the kubeadm alpha phase command supports our aim to make kubeadm more modular, letting you invoke atomic sub-steps of the bootstrap process" — with a name attached to a date: "We hope that we can graduate the command to beta as kubeadm phase in v1.9.0."
The fourth is the least advertised and takes one sentence: "beginning with v1.8.0, kubeadm uploads your configuration to a ConfigMap inside of the cluster, and later reads that configuration when upgrading for a seamless user experience." Alongside it, a graduation with a release number on it: "The first certificate rotation feature has graduated to beta in v1.8, which is great to see. […] the Kubernetes node component kubelet can now rotate its client certificate automatically. We expect this area to improve continuously."
Between the announcements and the forecasts the post stops to draw a boundary, in a section called The scope of kubeadm:
kubeadm performs the actions necessary to get a minimum viable cluster up and running. It only cares about bootstrapping, not about provisioning machines, by design. Likewise, installing various nice-to-have addons by default like the Kubernetes Dashboard, some monitoring solution, cloud provider-specific addons, etc. is not in scope.
Then the forecasts, all three for v1.9: high availability as an alpha feature, kubeadm phase at
beta, and "we want to make self-hosting the default way to deploy your control plane: Kubernetes
becomes much easier to manage if we can rely on Kubernetes' own tools to manage the cluster
components."
Two of the post's own defects are worth noting before you start, because both are in the published
HTML and neither is yours. A section heading has collapsed into the paragraph above it, so the
sentence "We wanted to add a lot of new features and improvements in this cycle, and we
succeeded.Upgrades along with better introspectability." runs a full stop straight into a fragment
that was a title. And in the closing paragraph the word meetings has its individual letters
hyperlinked — meetings carries three separate links to the same Slack
channel, as does the lone o in "or check out" — which is what an editor's find-and-replace does
to a word when the pattern it matched was a single character.
As it runs now — kubeadm upgrade plan and kubeadm upgrade apply both work, do what the post
says they do, and are the commands the pinned documentation tells you to run. Everything else in
the post has moved, and the four kinds of movement are worth telling apart, because only the first
announces itself.
kubeadm init --feature-gates=SelfHosting=true fails, and fails at the gate name rather than
at anything about your cluster. SelfHosting is not a gate kubeadm has. It is not in the live table
on docs/reference/setup-tools/kubeadm/kubeadm-init.md, which holds exactly one row at the pin, and
it is not in the removed table beneath it, which holds eight —
2016/09 transcribes both in full, and
reading them for what is absent is the only use this exercise makes of them. So kubeadm's own
record of every gate it has ever offered does not remember offering this one.
kubeadm phase is not a command, and never was. What exists instead is kubeadm init phase,
kubeadm join phase, kubeadm reset phase and kubeadm upgrade phase — four reference pages under
docs/reference/setup-tools/kubeadm/, one per verb. kubeadm init phase appears 127 times across
the pinned documentation and kubeadm join phase 33; the string kubeadm phase appears twice, both
times as prose on docs/tasks/administer-cluster/kubeadm/kubeadm-certs.md:108,110 rather than as a
command anybody is told to run. The forecast graduated four times over and never got its name.
--dry-run survives and is undocumented where you would look for it. The flag still exists:
docs/reference/setup-tools/kubeadm/implementation-details.md:660-661 mentions it, in a subsection
about a different command — "A more verbose way to do the same thing is running kubeadm upgrade
apply --dry-run or kubeadm upgrade node --dry-run", where the same thing is kubeadm upgrade
diff, a subcommand the post did not have. The string --dry-run occurs zero times on
docs/tasks/administer-cluster/kubeadm/kubeadm-upgrade.md, the page you would actually follow to
perform an upgrade. The post gave the feature a five-sentence paragraph; the procedure gives it
nothing.
And the ConfigMap is load-bearing, which is the one prediction that came entirely true. The name
kubeadm-config appears in 59 files at the pin. implementation-details.md:665-673 lists what
kubeadm upgrade apply does, and two of its five steps are reading and rewriting that ConfigMap and
its sibling kubelet-config.
The fourth kind of movement leaves no error at all, and it is the one this exercise spends most of
its steps on. implementation-details.md is the page whose stated job is to say what kubeadm init
does. At :309 it opens a list — "Other API server flags that are set unconditionally are:" — and
the first entry is:
--insecure-port=0to avoid insecure connections to the api server—
docs/reference/setup-tools/kubeadm/implementation-details.md:311
--insecure-port has zero occurrences in
docs/reference/command-line-tools-reference/kube-apiserver.md, a 75KB generated page that
enumerates every flag the API server accepts. It has zero occurrences anywhere else in the pinned
documentation: implementation-details.md:311 is the string's only appearance in the tree. A Go
binary handed a flag its parser does not carry prints an error and exits, so an API server started
with that line does not start — which is checkable on your own cluster in about forty seconds, and
step 3 below checks it. Two flags above it are in the same condition: --cloud-provider and
--cloud-config, at :305-307, are likewise absent from kube-apiserver.md, and the page hedges
them itself — "this is experimental, alpha and will be removed in a future version" — a
parenthesis that has now outlasted the thing it was hedging.
The same list also carries a link that no longer lands. Of the eight admission plugins at
:317-334, seven link to headings that exist on
docs/reference/access-authn-authz/admission-controllers.md. The eighth,
PersistentVolumeLabel at :325, links to #persistentvolumelabel, and the plugin has no section
on that page — the string appears nowhere in the 42KB file. It is also the only one of the eight
whose entry predicts its own removal: "This admission controller is deprecated and will be removed
in a future version." The prediction was correct, the section went, and the link stayed.
The diff, and why — this post describes a plan the project abandoned, and reading it that way
requires care, because on the surface it looks like a post that came true. The command it advertises
still runs. The ConfigMap it mentions in passing is now the mechanism the upgrade path depends on.
High availability, the first of its three forecasts, shipped and is documented at
docs/setup/production-environment/tools/kubeadm/high-availability.md, reached via
kubeadm init --control-plane-endpoint and kubeadm join --control-plane. Nothing in the post
reads as embarrassing. What happened instead is that every forecast was answered by the same
pressure, and the pressure is stated in the post itself, two paragraphs after the forecast it
contradicts.
Self-hosting was a proposal about where the control plane lives, and the argument for it was
that living inside the cluster makes upgrades free: "Rolling upgrades in Kubernetes can be used for
upgrades of the control plane components, and next to no extra code has to be written for that to
work." That is a real and good argument. It also makes kubeadm's output a set of Deployments and
DaemonSets rather than five files in a directory, which means kubeadm's blast radius becomes the
cluster's own control loops, and a failed control-plane rollout becomes a failed rollout of the
thing that reconciles rollouts. The project went the other way. kubeadm upgrade apply at the pin
"upgrades the control plane manifest files on disk in /etc/kubernetes/manifests and waits for the
kubelet to restart the components if the files have changed" — five files and a directory watch.
The extra code the post hoped not to write was written; kubeadm-upgrade.md is 16KB of it seen from
the outside.
kubeadm phase as a top-level verb would have made phases composable — a phase is a thing you run,
and you assemble a bootstrap out of them. Phases as sub-nouns of init, join, reset and
upgrade are the opposite arrangement: you cannot run a phase, you can only run a phase of a verb
that already existed. The four *-phase.md pages are a deliberate narrowing, not an incomplete
graduation, and they all end the same way — each of the four carries a See also line pointing at
kubeadm alpha, the page whose body says there is nothing in it. Four signposts to an empty room,
which 2016/09 found from the other
direction.
--dry-run was the post's answer to a real complaint, and it survived as a flag while losing its
place in the sentence. Its demotion is the most legible of the four, because you can see the
replacement: kubeadm upgrade diff does the same job under a name that says what it produces, and
--dry-run became the more verbose way to spell it. A feature that gets a better name loses its old
one slowly, in the pages nobody rewrote.
So: the two predictions that failed are the two that would have widened what kubeadm owns, and the one that succeeded completely — the config ConfigMap — added nothing to kubeadm's remit at all. It made kubeadm remember a decision you had already given it. The section that decided all three sits between the announcements and the forecasts, and it was written by the same author in the same post: "kubeadm performs the actions necessary to get a minimum viable cluster up and running. It only cares about bootstrapping." A tool that publishes a scope will resolve its own roadmap against that scope, and the roadmap and the scope were two screens apart.
The vocabulary is the part that did not resolve. Self-hosted survived the feature's cancellation
and was reassigned, and the pinned documentation now carries the word four times in three senses.
docs/concepts/architecture/_index.md:161-175 lists four ways a control plane can be deployed and
makes two of them distinct entries:
- Static Pods
- Control plane components are deployed as static Pods, managed by the kubelet on specific nodes. This is a common approach used by tools like kubeadm.
- Self-hosted
- The control plane runs as Pods within the Kubernetes cluster itself, managed by Deployments and StatefulSets or other Kubernetes primitives.
—
docs/concepts/architecture/_index.md:167-172
That second entry is the post's definition, almost to the word, and it puts kubeadm on the other side of the line. Two other concept pages put kubeadm on this side of it, in a sentence they share verbatim:
The main use for static Pods is to run a self-hosted control plane: in other words, using the kubelet to supervise the individual control plane components. For example, kubeadm uses static Pods to run
kube-apiserver,kube-controller-manager,kube-scheduler, andetcdon control plane nodes.—
docs/concepts/workloads/pods/static-pods.md:16-20, and again atdocs/concepts/workloads/pods/_index.md:423-427
Three concept pages, two mutually exclusive definitions of one term, kubeadm named as the example on
both sides, and the definition that lost is the one printed twice. The fourth occurrence,
docs/reference/access-authn-authz/admission-controllers.md:108, uses the word about webhooks and is
a third sense again. Meanwhile the strings SelfHosting, self-hosting and bootkube occur
zero times in the whole tree. The feature is gone without a trace and its name is in three
places, because a cancelled feature stops compiling and a word does not.
The ladder belongs to the post's smallest verifiable claim — "The first certificate rotation feature has graduated to beta in v1.8" — and it takes two tables, because this area turned out to be two gates that went opposite ways.
RotateKubeletClientCertificate:
| stage | default | locked | releases |
|---|---|---|---|
| beta | true |
— | v1.8 – v1.18 |
| stable | true |
— | v1.19 – v1.21 |
with removed: true declared at file level.
RotateKubeletServerCertificate:
| stage | default | locked | releases |
|---|---|---|---|
| alpha | false |
— | v1.7 – v1.11 |
| beta | true |
— | v1.12 – |
The first table confirms the post exactly: beta at v1.8, the release the post is announcing, which
is the only claim in the piece that carries a version number and can be checked against the gate
tree. It then went stable at v1.19 and was removed at v1.21, and the removal was clean — the string
RotateKubeletClientCertificate occurs nowhere in the pinned documentation outside its own gate
file. Client certificate rotation is now simply what the kubelet does, configured by the
rotateCertificates field.
The second table has no end. RotateKubeletServerCertificate has been beta since v1.12 and is beta
at v1.37 — twenty-six minor releases in one stage, and "enabled by default" for all of them, which
is what makes the stasis invisible rather than merely long. The reason it never moved is written down
in the same place as the feature:
The CSR approving controllers implemented in core Kubernetes do not approve node serving certificates for security reasons. To use
RotateKubeletServerCertificateoperators need to run a custom approving controller, or manually approve the serving certificate requests.—
docs/reference/access-authn-authz/kubelet-tls-bootstrapping.md:450-456
The gate is on by default and enables a request that core Kubernetes has decided, on purpose, not to answer. That is not a stalled promotion; it is a feature whose last mile was ruled out of scope by a different SIG, and the gate has sat at beta for twenty-six releases because there is nothing left to promote it to. The post's "We expect this area to improve continuously" met a design refusal, not a backlog — and the same page still dates the whole business by the post's own release, in a sentence whose floor has never been raised:
Kubernetes v1.8 and higher kubelet implements features for enabling rotation of its client and/or serving certificates. Note, rotation of serving certificate is a beta feature and requires the
RotateKubeletServerCertificatefeature flag on the kubelet (enabled by default).—
docs/reference/access-authn-authz/kubelet-tls-bootstrapping.md:427-430
Twenty-nine minors after v1.8, v1.8 and higher is still the version the page names, and the word beta beside it is still accurate.
One page-level fact bears on the gates without being a ladder. kubeadm-init.md:137 states the rule
its own tables run on — "Feature gates are removed after a feature graduates to GA." Two rows of
the removed table beneath it were removed without graduating at all: PublicKeysECDSA (alpha 1.19,
no beta, no GA, removed 1.37) and UpgradeAddonsBeforeControlPlane (alpha 1.28, no beta, no GA,
removed 1.31). The table is transcribed in
2016/09; the rule above it is the new
observation, and it is the rule under which SelfHosting would have had to appear somewhere. It
appears nowhere, which means the gate was withdrawn before the page that tracks withdrawals existed
to track it.
Topology — pair: a control-plane node at
10.10.10.130 and one worker at 10.10.10.131. Two nodes is the smallest cluster on which the
post's headline claim can be counted rather than believed, because the control-plane procedure and
the worker procedure are two different pages at the pin and a single-node cluster only ever sends you
to the first one.
Provision with the five steps at #provision, then run
the node baseline on both nodes — and
stop before kubeadm init, because step 1 below runs against a machine that has kubeadm and no
cluster. Then ssh zain@10.10.10.130.
This is the one exercise in the tree that does not install the newest minor, and the deviation is
the subject rather than a shortcut: an upgrade needs somewhere to come from. Read the current minor
where the baseline's step 0 sends you, subtract one, and use that in the two lines of the baseline
that name $V — the package repository URL and the apt-get install — on both nodes. Everything
else in the baseline is unchanged. You will spend the second half of this exercise walking the
cluster forward one minor by hand, which is the only way to hold the post's sentence and the
procedure in view at the same time.
Do
- Before any cluster exists, run the post's gate command as printed, with
--dry-runso it cannot half-build anything:
Record the error text verbatim and say which part of the command line it objects to. Then open
kubeadm-init.md's two tables at the pin and answer a narrower question than "is the gate there":
given the rule printed at :137, which of the two tables would SelfHosting have to be in, and
what does its absence from both mean about when it was withdrawn?
2016/09 holds both tables; do not
re-transcribe them.
- Build the cluster at one minor behind:
kubeadm initon10.10.10.130with--pod-network-cidr=10.244.0.0/16, Flannel, then join the worker. Now read whatkubeadm initwrote:
Take the six flags listed at implementation-details.md:311-316 under "Other API server flags
that are set unconditionally are:" and mark each one present or absent in the file, copying the
value where it is present. Do not stop at the first absence. Then do the same for
--cloud-provider and --cloud-config at :305-307.
- Now take the page at its word. Add the line it says is set unconditionally to the manifest's command list:
sudo cp /etc/kubernetes/manifests/kube-apiserver.yaml ~/apiserver.yaml.bak
sudo sed -i '/- kube-apiserver/a\ - --insecure-port=0' /etc/kubernetes/manifests/kube-apiserver.yaml
Then watch, from the same node, in this order: kubectl get nodes, then
sudo crictl ps -a --name kube-apiserver, then the exited container's log via
sudo crictl logs <id>. Record the exact error string and say which process printed it. Note what
the kubelet is doing between your edit and your reading, and how you can tell. Then put the file
back — sudo cp ~/apiserver.yaml.bak /etc/kubernetes/manifests/kube-apiserver.yaml — and time how
long kubectl get nodes takes to answer again. Nothing here needs kubectl to be working, which
is the point of doing it in that order.
-
Follow the eight admission-plugin links at
implementation-details.md:317-334toadmission-controllers.md, one at a time, and find the one that lands on no heading. Then read what that entry says about itself that no other entry says, and answer: which happened first, the removal it predicted or the link that was supposed to describe it? -
Run the name the post forecast, then the names that exist:
kubeadm phase --help
kubeadm init phase --help
kubeadm join phase --help
kubeadm reset phase --help
kubeadm upgrade phase --help
Count the verbs that carry phase and say what a top-level kubeadm phase would have let you do
that these four do not. Then open any one of the four reference pages under
docs/reference/setup-tools/kubeadm/ and read its See also list to the end;
2016/09 already ran the command that
settles where it points.
- Change the package repository on the control-plane node to the current minor and install the new
kubeadmonly — not the kubelet, not kubectl. Then run the post's first command and the two the post did not have:
sudo kubeadm upgrade plan
sudo kubeadm upgrade diff
sudo kubeadm upgrade apply v<current>.<patch> --dry-run
Compare the last two outputs and say what --dry-run adds. Then search
kubeadm-upgrade.md — the page the pin sends you to for this procedure — for the flag the post
devoted a paragraph to, and record the number of hits.
-
Count the procedure before you run it. On
kubeadm-upgrade.md, count the fenced command blocks for the Debian path on a single control-plane node, from Determine which version to upgrade to through Verify the status of the cluster. Do not count the CentOS/RHEL/Fedora tabs, the block that shows expected output, or the two blocks under For the other control plane nodes. Then count the same way ondocs/tasks/administer-cluster/kubeadm/upgrading-linux-nodes.md, which is where the page sends you for the worker. Add them. Then find the one block that is the post's one-command upgrade, and name the step in between the two counts that has no command at all. -
Now actually do it: the full control-plane procedure to the current minor, then the full worker procedure. Then check what the upgrade wrote rather than what it printed:
kubectl get nodes -o wide
kubectl get cm -n kube-system kubeadm-config -o yaml
sudo grep image: /etc/kubernetes/manifests/kube-apiserver.yaml
Say which of those three the post predicted in one sentence, and which one is the post's self-hosting argument answered by other means.
-
Read
docs/concepts/architecture/_index.md:161-175, thendocs/concepts/workloads/pods/static-pods.md:16-20. Decide which of the first page's four deployment models the cluster you just upgraded matches. Then answer whether the control plane you upgraded in step 8 is self-hosted, once from each page, and say which of the two answers the post would have given. -
On the control-plane node, check where certificate rotation actually stands on the cluster in front of you:
sudo grep -E 'rotateCertificates|serverTLSBootstrap' /var/lib/kubelet/config.yaml sudo ls /var/lib/kubelet/pki/ kubectl get csrThen read
kubelet-tls-bootstrapping.md:427-430and the note at:450-456, and answer: which of the two rotations is switched on here, and which of the two gates in the ladder above is the one you cannot finish enabling no matter what you set?
Expect
Step 1: kubeadm rejects the gate name itself, before any preflight check about ports or
directories — the objection is to SelfHosting, not to the machine. Record the wording; the
question the step asks is answerable from kubeadm-init.md alone. The rule at :137 says gates are
removed after GA, so a gate that was withdrawn should appear in the removed table with blanks in
its Beta and GA columns, exactly as PublicKeysECDSA and UpgradeAddonsBeforeControlPlane do.
SelfHosting is in neither table, so it left before that table began recording departures.
Step 2: --insecure-port is not in the manifest. That one is certain, and it is certain by
deduction rather than by prediction: the flag is absent from the API server's own exhaustive flag
reference, so a running API server was not given it. The other seven are yours to record. This
exercise deliberately does not tell you what they are, because the pin does not settle them — a
generated flag reference says what the binary accepts, not what kubeadm passes — and an expectation
that guesses is the failure this whole tree exists to catch. Write down what you find; it is the
input to step 3 and to nothing else.
Step 3: the API server exits, the kubelet notices the manifest changed and creates the container
again, it exits again, and the loop continues for as long as the line is in the file. kubectl get
nodes fails with a connection error against 10.10.10.130:6443 rather than an authorization error
— the difference matters, because it tells you the process is gone rather than refusing you.
crictl ps -a shows the container in Exited with a recent finish time and an incrementing attempt
count; its log holds a single line naming the flag it could not parse. So the sentence at
implementation-details.md:311 is not merely stale: followed literally, on the page whose subject is
what kubeadm init does, it prevents the cluster from existing. Recovery takes a few seconds after
you restore the file, and needs no systemctl and no kubeadm — the kubelet's directory watch is
the whole mechanism, which is also the mechanism kubeadm upgrade apply uses in step 8.
Step 4: seven of the eight land on a heading. PersistentVolumeLabel does not; the string is absent
from admission-controllers.md entirely. It is also the only one of the eight whose entry carries
"This admission controller is deprecated and will be removed in a future version", so the list
predicted the removal, the section was deleted, and the link outlived both. The removal happened
first, which is the ordinary case: a page is edited by whoever owns it, and nothing walks the tree
afterwards asking who was pointing at the heading that went.
Step 5: kubeadm phase is not a command and the error says so. Four verbs carry phase as a
sub-noun, and each of the four pages ends with a See also line pointing at kubeadm alpha. The
thing a top-level kubeadm phase would have allowed is composition — a bootstrap assembled out of
phases in an order you chose, including orders no verb implements. What shipped is a phase you can
only run inside one of four verbs, which is a narrower feature wearing the same word, and it
shipped four times.
Step 6: kubeadm upgrade plan prints the current version, the version you can go to, a component
table and a component-config table. kubeadm upgrade diff prints a unified diff of the static Pod
manifests. --dry-run prints that diff plus everything else the apply would do, which is what
implementation-details.md:660-661 means by "a more verbose way to do the same thing." And the
count on kubeadm-upgrade.md is zero. The flag the post said removed the mystery from upgrades
is on no page that tells you how to upgrade.
Step 7: ten blocks on the control-plane page under that counting rule, six on the worker
page, sixteen for a two-node cluster — and the two pages are reached by a link, so the second six
are invisible from the first page until you get to the bottom of it. The post's one-command upgrade
is block five of the ten, sudo kubeadm upgrade apply. The step with no command at all is "Manually
upgrade your CNI provider plugin", which sends you to the addons page to find out whether your CNI
needs anything. The post's sentence is true about the command and was never a claim about the
procedure; what it could not have foreseen is that the control plane would turn out to be the cheap
part, and that the package manager, the drain, the kubelet, the CNI and the per-node repetition —
none of them inside the command — would be the rest.
Step 8: the upgrade succeeds and kubectl get nodes -o wide shows the new version on both nodes,
the control plane first. kubeadm-config records the new version, which is the post's single-sentence
prediction working exactly as described — it is the reason kubeadm upgrade apply needed no flags
carrying your original choices. The image: line in the manifest has a new tag, and nothing you ran
restarted the API server: the file changed and the kubelet did the rest, the same mechanism you drove
by hand in step 3. That is the post's self-hosting argument — "next to no extra code has to be
written" — answered by writing the code. kubeadm upgrade is that code, and it is a file rewrite
plus a directory watch rather than a rolling update.
Step 9: your cluster is the second entry, Static Pods. architecture/_index.md therefore says it
is not self-hosted, and static-pods.md says running a control plane this way is the main use of
static Pods and calls it self-hosted, naming kubeadm in both places. The post would have said no:
its definition is the one at :170-172, word for word, and by that definition the control plane you
upgraded is not self-hosted and never was. The two pages are not ambiguous about the same thing, they
are precise about different things, and the term now has to be read off whichever page you arrived
from.
Step 10: rotateCertificates: true is present, serverTLSBootstrap is absent, /var/lib/kubelet/pki/
holds kubelet-client-current.pem and a self-signed serving pair, and kubectl get csr shows the
bootstrap CSRs for both nodes in Approved,Issued. Client rotation is on and has no gate to turn
on — its gate went stable and was removed, which is what a finished feature looks like. Server
rotation is the one you cannot finish: setting serverTLSBootstrap: true makes the kubelet request a
serving certificate, and no controller in core Kubernetes will approve it, by the deliberate decision
recorded at :450-456. So the gate that is beta and on by default is the gate whose feature cannot
complete, and the gate that is gone is the one that worked. That self-signed serving pair is also
what the 2015 monitoring exercise has to
patch --kubelet-insecure-tls around before metrics-server will start: a design refusal in one
year's post is another year's failed rollout.
Read on — two questions the corpus answers and this exercise did not ask. kubeadm-init.md's
description of RootlessControlPlane — the single live gate in kubeadm's namespace, alpha since 1.22
and deprecated since 1.31 — configures "the kubeadm deployed control plane component static Pod
containers" to run as non-root. Read it and say what the only surviving kubeadm gate takes for
granted about where the control plane lives, and whether a self-hosted control plane could have had
that gate at all. Then read docs/setup/best-practices/node-conformance.md:29-31, which tells you to
"use http://localhost:8080 as the URL of the API server" — the port this post's recap paragraph
says was closed in v1.6 — and settle from the surrounding sentence whether that instruction is a
leftover or a correct statement about a different API server than the one you have been upgrading.
The answer is in the clause immediately before it, and getting it right is the difference between a
finding and a false alarm.
Teardown — #teardown. The cluster is one minor
behind nothing by the end of step 8, so it is an ordinary current-minor pair and could be kept —
but it has been upgraded in place rather than installed, and every later exercise assumes a cluster
that was installed, so take it down.