Skip to content

docs: migrate 12-factor tutorials to Canonical Kubernetes - #2882

Open
florentianayuwono wants to merge 31 commits into
canonical:mainfrom
florentianayuwono:docs/migrate-tutorials-ck8s-isd-4330
Open

florentianayuwono wants to merge 31 commits into
canonical:mainfrom
florentianayuwono:docs/migrate-tutorials-ck8s-isd-4330

Conversation

@florentianayuwono

@florentianayuwono florentianayuwono commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to the how-to guide migration (#2743), addressing review feedback that the 12-factor tutorials still required MicroK8s. This migrates the tutorials to use Canonical Kubernetes (CK8s) exclusively — no MicroK8s alternative.

Changes

Shared tutorial setup (docs/reuse/tutorial/setup_stable.rst, setup_edge.rst)

  • Install and bootstrap Canonical Kubernetes (sudo snap install k8s --classic, sudo k8s bootstrap).
  • Enable local-storage (replaces MicroK8s' hostpath-storage) and ingress.
  • Enable the built-in load-balancer with an L2 address pool set to the VM's own IP. Unlike the MicroK8s ingress add-on, the CK8s ingress is fronted by a LoadBalancer service, so a load balancer is required for the ingress to be assigned a reachable address. The tutorial curl commands resolve the ingress host to that address ($INGRESS_IP) instead of the loopback address.
  • Register the cluster as a Juju cloud (sudo k8s config | juju add-k8s ...), since CK8s isn't autodetected like MicroK8s.
  • Set up a small local OCI registry on localhost:5000. CK8s has no bundled registry addon, so the tutorials run the static distribution registry as a systemd service and point containerd at it through /etc/containerd/hosts.d/localhost:5000/hosts.toml.

Six framework tutorials and their task.yaml tests (Django, Express, FastAPI, Flask, Go, Spring Boot)

  • Push the rock to the local registry with rockcraft.skopeo copy to localhost:5000, per reviewer request to use a registry rather than driving containerd directly in a tutorial.
  • Reference the charm resource image with the localhost:5000/ prefix.
  • Update prose and juju status sample output for CK8s.

How-to (docs/howto/manage-web-app-charms/integrate-web-app-charm.rst)

  • Make the load-balancer step Canonical K8s-only, consistent with migrating the 12-factor docs off MicroK8s.

Spread (spread.yaml)

  • The docs/tutorial/code/ suite deploys the same local registry and containerd hosts.d config so the executable tutorial tests exercise the documented registry workflow on CK8s.
  • Enable the load balancer and curl the ingress address directly, matching the documented commands (removing the earlier test-only loopback DNAT workaround).

Validation

  • Docs build passes with sphinx-build --fail-on-warning.
  • ⚠️ The task.yaml files are executable spread integration tests. They require a full Multipass + CK8s setup and a self-hosted docs runner, so their end-to-end execution on CK8s — particularly ingress via nginx-ingress-integrator — should be validated in CI.

  • I've followed the contribution guidelines.
  • I've signed the CLA.
  • I've successfully run make lint && make test.
  • I've added or updated any relevant documentation.
  • In documents I changed, I added a meta description if one was missing.
  • I've updated the relevant release notes.

Replace MicroK8s with Canonical Kubernetes throughout the 12-factor app
tutorials so they use CK8s exclusively, following up on the how-to guide
migration.

- Rework the shared tutorial setup (setup_stable.rst, setup_edge.rst) to
  install and bootstrap Canonical Kubernetes (k8s snap), enable
  local-storage and ingress, and register the cluster as a Juju cloud with
  'juju add-k8s' before bootstrapping.
- Replace the MicroK8s registry push (rockcraft.skopeo copy to
  localhost:32000) with a direct containerd image import ('ctr images
  import') and reference the image without the registry prefix in the six
  framework tutorials and their task.yaml tests.
- Update prose and 'juju status' sample output accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings September 15, 2026 15:21
Remove the MicroK8s options from the manage-web-app-charms how-to
guides so they use Canonical Kubernetes exclusively, matching the
tutorial migration.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The six executable task suites still provision MicroK8s, so their CK8s commands cannot run until the test environment is updated.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Migrates the 12-factor tutorials from MicroK8s to Canonical Kubernetes.

Changes:

  • Updates shared CK8s setup and Juju registration.
  • Replaces registry pushes with containerd image imports.
  • Refreshes tutorial instructions and sample output.
File summaries
File Summary
docs/tutorial/kubernetes-charm-spring-boot.rst Updates Spring Boot tutorial for CK8s.
docs/tutorial/kubernetes-charm-go.rst Updates Go tutorial for CK8s.
docs/tutorial/kubernetes-charm-flask.rst Updates Flask tutorial for CK8s.
docs/tutorial/kubernetes-charm-fastapi.rst Updates FastAPI tutorial for CK8s.
docs/tutorial/kubernetes-charm-express.rst Updates Express tutorial for CK8s.
docs/tutorial/kubernetes-charm-django.rst Updates Django tutorial for CK8s.
docs/tutorial/code/spring-boot/task.yaml Uses CK8s containerd image import; requires CK8s test-suite provisioning.
docs/tutorial/code/go/task.yaml Uses CK8s containerd image import; requires CK8s test-suite provisioning.
docs/tutorial/code/flask/task.yaml Uses CK8s containerd image import; requires CK8s test-suite provisioning.
docs/tutorial/code/fastapi/task.yaml Uses CK8s containerd image import; requires CK8s test-suite provisioning.
docs/tutorial/code/expressjs/task.yaml Uses CK8s containerd image import; requires CK8s test-suite provisioning.
docs/tutorial/code/django/task.yaml Uses CK8s containerd image import; requires CK8s test-suite provisioning.
docs/reuse/tutorial/setup_stable.rst Defines stable CK8s setup and Juju registration.
docs/reuse/tutorial/setup_edge.rst Defines edge CK8s setup and Juju registration.
Review details
  • Files reviewed: 16/16 changed files
  • Comments generated: 6
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/tutorial/code/django/task.yaml Outdated
Comment thread docs/tutorial/code/expressjs/task.yaml Outdated
Comment thread docs/tutorial/code/fastapi/task.yaml Outdated
Comment thread docs/tutorial/code/flask/task.yaml Outdated
Comment thread docs/tutorial/code/go/task.yaml Outdated
Comment thread docs/tutorial/code/spring-boot/task.yaml Outdated
The tutorial task.yaml files now import images with the k8s snap's
containerd (`ctr`) and deploy to a `k8s-cloud` Juju cloud, but the
shared docsuite only provisioned MicroK8s. Give docs/tutorial/code/ a
dedicated suite that installs the k8s snap, configures the docker.io
containerd mirror, bootstraps Canonical Kubernetes, enables
local-storage, ingress and the load balancer, and registers k8s-cloud
with Juju. The how-to code suite stays on MicroK8s.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@florentianayuwono

Copy link
Copy Markdown
Contributor Author

Addressed in 49c61e3: the six tutorial task suites no longer inherit the MicroK8s docsuite. docs/tutorial/code/ now has a dedicated spread suite that provisions Canonical Kubernetes instead:

  • installs the k8s snap (1.34-classic/stable) so /snap/k8s/current/bin/ctr is available for the image imports;
  • writes the docker.io pull-through mirror to /etc/containerd/hosts.d/docker.io/hosts.toml (the k8s snap's containerd path) before bootstrap;
  • k8s bootstrap, then enables local-storage, ingress, and the load-balancer (with an L2 pool set to the node IP — required because CK8s exposes ingress via a LoadBalancer service, unlike the MicroK8s host-port addon);
  • registers the cluster as the k8s-cloud Juju cloud (k8s config | juju add-k8s k8s-cloud --client) and bootstraps dev-controller on it.

The how-to code suite (docs/howto/code/) intentionally stays on MicroK8s, as those task.yaml files still use MicroK8s.

Note: these suites are manual: true and I can't run them locally, so CI validation is needed — in particular the nginx-ingress-integrator + Cilium ingress path.

Comment thread docs/tutorial/code/django/task.yaml Outdated
Comment thread docs/reuse/tutorial/setup_edge.rst Outdated
Comment thread docs/reuse/tutorial/setup_stable.rst Outdated

@alithethird alithethird left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some nitpicks, overall looking good.

Comment thread spread.yaml
florentianayuwono and others added 2 commits October 1, 2026 11:56
Address PR review feedback: avoid driving containerd directly (`ctr
images import`) in the 12-factor tutorials. Instead deploy a small
local OCI registry at localhost:5000 and push rocks to it with
`rockcraft.skopeo copy`, mirroring the previous MicroK8s registry flow.

- setup_stable.rst / setup_edge.rst: add a "Set up a local registry"
  section (distribution registry binary as a systemd service +
  containerd hosts.d config for localhost:5000).
- tutorial task.yaml (6 frameworks): push rocks via skopeo and
  reference images as localhost:5000/<app>:<ver>.
- tutorial .rst (6 frameworks): update prose/bullets to the registry
  and skopeo workflow.
- spread.yaml tutorial suite: deploy the registry + hosts.d config in
  place of the docker.io pull-through mirror block.
- Trim heading underlines in the setup files (review nitpick).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…s-ck8s-isd-4330

# Conflicts:
#	docs/howto/manage-web-app-charms/use-web-app-charm.rst

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread docs/tutorial/code/django/task.yaml Outdated
Comment thread docs/tutorial/code/expressjs/task.yaml Outdated
Comment thread docs/tutorial/code/fastapi/task.yaml Outdated
Comment thread docs/tutorial/code/flask/task.yaml Outdated
Comment thread docs/tutorial/code/go/task.yaml Outdated
Comment thread docs/tutorial/code/spring-boot/task.yaml Outdated
Comment thread spread.yaml Outdated
Comment thread docs/howto/manage-web-app-charms/integrate-web-app-charm.rst

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread docs/reuse/tutorial/setup_edge.rst
Comment thread docs/reuse/tutorial/setup_stable.rst
The Canonical Kubernetes ingress is fronted by a LoadBalancer service
rather than binding the node's host ports like the MicroK8s add-on, so
the documented `curl ... 127.0.0.1` requests could not reach the app.

Enable the built-in load balancer with an L2 address pool set to the
VM's own IP in the tutorial setup, and resolve the ingress host to that
address (`$INGRESS_IP`) instead of the loopback address. Align the
spread tutorial suite with the documented commands and drop the
test-only loopback DNAT workaround.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Address the critical command error and moderate registry exposure findings.

1 open finding
2 resolved since last review

🧠 Review effort: Lite

Comment thread docs/tutorial/kubernetes-charm-spring-boot.rst Outdated
Comment thread docs/reuse/tutorial/setup_edge.rst Outdated
Comment thread docs/reuse/tutorial/setup_edge.rst Outdated
Comment thread docs/reuse/tutorial/setup_stable.rst Outdated
Comment thread docs/reuse/tutorial/setup_stable.rst Outdated
florentianayuwono and others added 2 commits October 12, 2026 20:12
Co-authored-by: florentianayuwono <76247368+florentianayuwono@users.noreply.github.com>
Signed-off-by: florentianayuwono <76247368+florentianayuwono@users.noreply.github.com>
Remove a stray trailing double-backtick from a `curl` command in the
Spring Boot tutorial so the rendered code block can be copied verbatim.
Add a release note for the 12-factor tutorial migration to Canonical
Kubernetes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread docs/tutorial/kubernetes-charm-flask.rst
erinecon and others added 6 commits October 15, 2026 08:33
… tutorials

Replace the static local-binary OCI registry and containerd hosts.d
configuration with an in-cluster NodePort registry exposed on the node at
localhost:32000, mirroring the MicroK8s registry addon. Wait for the
load-balancer (MetalLB) webhook to become ready before configuring its
address pool to avoid a validation-webhook race, and reuse a single
INGRESS_IP variable defined during setup across the tutorial curls.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Waiting on `k8s status --wait-ready` does not guarantee that MetalLB's
validating webhook is serving, so configuring the load-balancer address
pool could still race the webhook and leave the feature in a Failed
state. Wait for the metallb-controller deployment (which hosts the
webhook) to roll out before setting the CIDRs, matching behaviour
verified on a live Canonical Kubernetes cluster.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Canonical Kubernetes exposes ingress through Cilium, whose ``cilium``
IngressClass is not the cluster default. Unlike the MicroK8s ingress
addon, Cilium does not claim classless Ingress resources, so the
nginx-ingress-integrator's Ingress was never assigned an address and
the integrator stalled on "Waiting for ingress IP availability".

Set ``ingress-class=cilium`` on the integrator so its Ingress targets
the Cilium controller. Validated end to end on a clean Canonical
Kubernetes VM.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The framework profiles generate charmcraft.yaml with ``base`` and
``platforms``, so ``charmcraft pack`` names the artifact
``<name>_<architecture>.charm``. The tutorials still showed the old
``bases``-style ``<name>_ubuntu-<series>-<architecture>.charm`` output
(django even showed 22.04). Correct the example output to match.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
florentianayuwono and others added 2 commits October 20, 2026 22:18
Use the single ``self-hosted-linux-amd64-noble-large`` runner label for
the docs-tests job instead of the multi-label set, which was mapping to a
starved runner pool and preventing the docs spread tests from running.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Switch the snap-build and spread-select jobs to the combined
``self-hosted-linux-amd64-noble-medium`` and ``spread-installed`` runner
labels, matching the docs-tests job.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@erinecon erinecon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for updating the tutorials! My initial issues have been resolved, but now I'm running into issues when integrating with a database. I also spotted a doc linting issue that we can resolve

I tested Express, FastAPI, and Flask

Comment thread docs/reuse/tutorial/setup_edge.rst Outdated
Comment thread docs/reuse/tutorial/setup_edge.rst
Comment thread docs/reuse/tutorial/setup_stable.rst Outdated
Comment thread docs/reuse/tutorial/setup_stable.rst
Comment thread docs/reuse/tutorial/setup_stable.rst
Comment thread docs/tutorial/kubernetes-charm-express.rst
Comment thread docs/tutorial/kubernetes-charm-fastapi.rst
Comment thread .github/workflows/spread-docs.yaml Outdated
jobs:
snap-build:
runs-on: [self-hosted, linux, amd64, noble, medium]
runs-on: self-hosted-linux-amd64-noble-medium

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linter is complaining about this change (and the other similar change later in this file):

.github/workflows/spread-docs.yaml:25:14: label "self-hosted-linux-amd64-noble-medium" is
unknown. available labels are "windows-latest", "windows-latest-8-cores", "windows-2025",
"windows-2022", "windows-11-arm", "ubuntu-slim", "ubuntu-latest", "ubuntu-latest-4-cores",
"ubuntu-latest-8-cores", "ubuntu-latest-16-cores", "ubuntu-24.04", "ubuntu-24.04-arm",
"ubuntu-22.04", "ubuntu-22.04-arm", "macos-latest", "macos-latest-xl", "macos-latest-xlarge",
"macos-latest-large", "macos-26-xlarge", "macos-26", "macos-15-intel", "macos-15-xlarge",
"macos-15-large", "macos-15", "macos-14-xl", "macos-14-xlarge", "macos-14-large", "macos-14",
"macos-13-xl", "macos-13-xlarge", "macos-13-large", "macos-13", "self-hosted", "x64", "arm",
"arm64", "linux", "macos", "windows", "linux", "spread-enabled", "spread-installed", "amd64",
"arm64", "ppc64el", "riscv64", "s390x", "focal", "jammy", "noble", "resolute", "medium",
"large", "xlarge", "2xlarge", "2xlarge-extra", "edge", "tiobe", "large-tiobe",
"2xlarge-tiobe". if it is a custom label for self-hosted runner, set list of labels in
actionlint.yaml config file [runner-label]

Comment thread docs/tutorial/kubernetes-charm-flask.rst
Comment on lines 657 to 663
Check the total number of visitors:

.. code-block:: bash

curl http://flask-hello-world/visitors --resolve flask-hello-world:80:127.0.0.1
curl http://flask-hello-world/visitors --resolve flask-hello-world:80:$INGRESS_IP

This request should return ``1`` after the previous request to the root endpoint.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not get a response from the visitors endpoint:

(.venv) ubuntu@charm-dev:~/flask-hello-world/charm$ curl http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP
Hi!
(.venv) ubuntu@charm-dev:~/flask-hello-world/charm$ curl http://flask-hello-world/visitors --resolve flask-hello-world:80:$INGRESS_IP
(.venv) ubuntu@charm-dev:~/flask-hello-world/charm$ curl http://flask-hello-world/visitors --resolve flask-hello-world:80:$INGRESS_IP
(.venv) ubuntu@charm-dev:~/flask-hello-world/charm$ curl http://flask-hello-world --resolve flask-hello-world:80:$INGRESS_IP
Hi!
(.venv) ubuntu@charm-dev:~/flask-hello-world/charm$ curl http://flask-hello-world/visitors --resolve flask-hello-world:80:$INGRESS_IP

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried again today with Flask and Go -- in both attempts, I did not get a response from the visitors endpoint.

I also tried the Go tutorial with both PostgreSQL 14 and 16 -- same lack of response.

Comment thread .github/workflows/spread-docs.yaml Outdated
Comment thread docs/reuse/tutorial/setup_edge.rst Outdated
florentianayuwono and others added 9 commits October 21, 2026 15:04
Co-authored-by: Erin Conley <erin.conley@canonical.com>
Signed-off-by: florentianayuwono <76247368+florentianayuwono@users.noreply.github.com>
Co-authored-by: Erin Conley <erin.conley@canonical.com>
Signed-off-by: florentianayuwono <76247368+florentianayuwono@users.noreply.github.com>
Co-authored-by: Erin Conley <erin.conley@canonical.com>
Signed-off-by: florentianayuwono <76247368+florentianayuwono@users.noreply.github.com>
Co-authored-by: Erin Conley <erin.conley@canonical.com>
Signed-off-by: florentianayuwono <76247368+florentianayuwono@users.noreply.github.com>
Increase the Multipass VM memory from 4 GB to 8 GB in the shared setup
pages and the per-framework prerequisites to avoid pack errors and I/O
timeouts during the build steps. Add "balancer('s)?" to the custom
wordlist so the load-balancer prose passes the documentation linter.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Django 6.1 requires PostgreSQL 15 or later, but the tutorial deployed
postgresql-k8s from the 14/edge channel (PostgreSQL 14), so the charm's
startup migration failed with "PostgreSQL 15 or later is required".
Deploy postgresql-k8s from 16/stable to match the Django requirement,
consistent with the upstream 12-factor Django tutorial.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The registry setup inlined the full Kubernetes manifest as a heredoc,
which made the shared setup pages long and hard to read. Move the
manifest into docs/reuse/tutorial/registry.yaml and download it with
curl in the tutorial, mirroring the upstream 26.04 tutorials. Add a
readiness probe so the registry rollout wait reflects actual readiness.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@erinecon erinecon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to successfully complete the Django tutorial 🎉

For the other tutorials with a dedicated section to integrate with a database -- I'm consistently seeing a lack of response from the visitors endpoint.

Comment on lines 657 to 663
Check the total number of visitors:

.. code-block:: bash

curl http://flask-hello-world/visitors --resolve flask-hello-world:80:127.0.0.1
curl http://flask-hello-world/visitors --resolve flask-hello-world:80:$INGRESS_IP

This request should return ``1`` after the previous request to the root endpoint.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried again today with Flask and Go -- in both attempts, I did not get a response from the visitors endpoint.

I also tried the Go tutorial with both PostgreSQL 14 and 16 -- same lack of response.

florentianayuwono and others added 3 commits October 22, 2026 00:47
Avoids the ca-certificates.crt staging conflict (canonical/rockcraft#1261)
that occurs with the ubuntu@24.04 base.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tstrap

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The cilium ingress class treats the ImplementationSpecific path / as an
exact match, so endpoints other than / returned an empty 404. Use the /.*
pattern and give the ingress more time to come up.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants