From 79fa5ad9fb50bc16277831dd4cdd22b7ab5b072c Mon Sep 17 00:00:00 2001 From: Erin Conley Date: Wed, 19 Feb 2025 13:52:19 -0500 Subject: [PATCH] docs: update 12-factor tutorials (#2085) Update the 12-factor tutorials. Specific updates: - Convert to reStructuredText - Incorporate feedback from UX session with Daniele - Update all tutorials for consistency (feedback from previous PRs, feedback from Daniele) --------- Co-authored-by: Alex Lowe Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Michael DuBelko --- docs/conf.py | 1 + docs/reuse/tutorial/setup_edge.rst | 92 ++ docs/reuse/tutorial/setup_stable.rst | 64 +- .../code/django/greeting_charmcraft.yaml | 7 + .../django/postgres_requires_charmcraft.yaml | 4 + docs/tutorial/code/django/requirements.txt | 2 + .../code/django/settings_init_rock.py | 129 +++ .../code/django/settings_local_run.py | 123 ++ docs/tutorial/code/django/task.yaml | 273 +++++ .../code/django/urls_django_hello_world.py | 23 + docs/tutorial/code/django/urls_greeting.py | 7 + docs/tutorial/code/django/views_greeting.py | 5 + .../django/views_greeting_configuration.py | 7 + docs/tutorial/code/fastapi/app.py | 7 + docs/tutorial/code/fastapi/greeting_app.py | 9 + .../code/fastapi/greeting_charmcraft.yaml | 9 + docs/tutorial/code/fastapi/requirements.txt | 2 + docs/tutorial/code/fastapi/task.yaml | 211 ++++ docs/tutorial/code/fastapi/visitors_app.py | 34 + .../code/fastapi/visitors_charmcraft.yaml | 6 + .../tutorial/code/fastapi/visitors_migrate.py | 21 + docs/tutorial/code/flask/requirements.txt | 1 + docs/tutorial/code/flask/task.yaml | 53 +- .../tutorial/code/go/greeting_charmcraft.yaml | 9 + docs/tutorial/code/go/greeting_main.txt | 23 + docs/tutorial/code/go/main.go | 18 + docs/tutorial/code/go/task.yaml | 240 ++++ .../tutorial/code/go/visitors_charmcraft.yaml | 6 + docs/tutorial/code/go/visitors_main.txt | 63 + docs/tutorial/code/go/visitors_migrate.sh | 3 + docs/tutorial/code/go/visitors_rockcraft.yaml | 11 + docs/tutorial/flask.rst | 525 --------- docs/tutorial/index.rst | 2 +- ...irst-kubernetes-charm-for-a-django-app.rst | 1032 +++++++++-------- ...rst-kubernetes-charm-for-a-fastapi-app.rst | 893 +++++++------- ...first-kubernetes-charm-for-a-flask-app.rst | 641 ++++++++++ ...ur-first-kubernetes-charm-for-a-go-app.rst | 963 +++++++-------- spread.yaml | 4 +- 38 files changed, 3586 insertions(+), 1937 deletions(-) create mode 100644 docs/reuse/tutorial/setup_edge.rst create mode 100644 docs/tutorial/code/django/greeting_charmcraft.yaml create mode 100644 docs/tutorial/code/django/postgres_requires_charmcraft.yaml create mode 100644 docs/tutorial/code/django/requirements.txt create mode 100644 docs/tutorial/code/django/settings_init_rock.py create mode 100644 docs/tutorial/code/django/settings_local_run.py create mode 100644 docs/tutorial/code/django/task.yaml create mode 100644 docs/tutorial/code/django/urls_django_hello_world.py create mode 100644 docs/tutorial/code/django/urls_greeting.py create mode 100644 docs/tutorial/code/django/views_greeting.py create mode 100644 docs/tutorial/code/django/views_greeting_configuration.py create mode 100644 docs/tutorial/code/fastapi/app.py create mode 100644 docs/tutorial/code/fastapi/greeting_app.py create mode 100644 docs/tutorial/code/fastapi/greeting_charmcraft.yaml create mode 100644 docs/tutorial/code/fastapi/requirements.txt create mode 100644 docs/tutorial/code/fastapi/task.yaml create mode 100644 docs/tutorial/code/fastapi/visitors_app.py create mode 100644 docs/tutorial/code/fastapi/visitors_charmcraft.yaml create mode 100644 docs/tutorial/code/fastapi/visitors_migrate.py create mode 100644 docs/tutorial/code/go/greeting_charmcraft.yaml create mode 100644 docs/tutorial/code/go/greeting_main.txt create mode 100644 docs/tutorial/code/go/main.go create mode 100644 docs/tutorial/code/go/task.yaml create mode 100644 docs/tutorial/code/go/visitors_charmcraft.yaml create mode 100644 docs/tutorial/code/go/visitors_main.txt create mode 100644 docs/tutorial/code/go/visitors_migrate.sh create mode 100644 docs/tutorial/code/go/visitors_rockcraft.yaml delete mode 100644 docs/tutorial/flask.rst create mode 100644 docs/tutorial/write-your-first-kubernetes-charm-for-a-flask-app.rst diff --git a/docs/conf.py b/docs/conf.py index 6beeff614..6a8adebc3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -129,6 +129,7 @@ "common/craft-parts/reference/plugins/go_use_plugin.rst", "common/craft-parts/reference/plugins/uv_plugin.rst", # Extra non-craft-parts exclusions can be added after this comment + "reuse/tutorial/*" ] rst_epilog = """ diff --git a/docs/reuse/tutorial/setup_edge.rst b/docs/reuse/tutorial/setup_edge.rst new file mode 100644 index 000000000..b357d43f1 --- /dev/null +++ b/docs/reuse/tutorial/setup_edge.rst @@ -0,0 +1,92 @@ +.. warning:: + + This tutorial requires version ``3.2.0`` or later of Charmcraft. + Check the version of Charmcraft using ``charmcraft --version``. + +First, install Multipass. + +.. seealso:: + + See more: `Multipass | + How to install Multipass `_ + +Use Multipass to launch an Ubuntu VM with the name ``charm-dev`` +from the 24.04 blueprint: + +.. code-block:: bash + + multipass launch --cpus 4 --disk 50G --memory 4G --name charm-dev 24.04 + +Once the VM is up, open a shell into it: + +.. code-block:: bash + + multipass shell charm-dev + +In order to create the rock, you need to install Rockcraft with +classic confinement, which grants it access to the whole file system: + +.. code-block:: bash + + sudo snap install rockcraft --classic + + +LXD will be required for building the rock. +Make sure it is installed and initialized: + +.. code-block:: bash + + lxd --version + lxd init --auto + + +If ``LXD`` is not installed, install it with ``sudo snap install lxd``. + +In order to create the charm, you'll need to install Charmcraft: + +.. code-block:: bash + + sudo snap install charmcraft --channel latest/edge --classic + + +MicroK8s is required to deploy the |12FactorApp| application on Kubernetes. +Let's install MicroK8s using the ``1.31-strict/stable`` track: + +.. code:: + + sudo snap install microk8s --channel 1.31-strict/stable + sudo adduser $USER snap_microk8s + newgrp snap_microk8s + +Several MicroK8s add-ons are required for deployment: + +.. code-block:: bash + + # Required for Juju to provide storage volumes + sudo microk8s enable hostpath-storage + # Required to host the OCI image of the application + sudo microk8s enable registry + # Required to expose the application + sudo microk8s enable ingress + +Check the status of MicroK8s: + +.. code-block:: bash + + sudo microk8s status --wait-ready + +If successful, the terminal will output ``microk8s is running`` +along with a list of enabled and disabled add-ons. + +Juju is required to deploy the |12FactorApp| application. +Install Juju using the ``3.6/stable`` track, and bootstrap a +development controller: + +.. code:: + + sudo snap install juju --channel 3.6/stable + mkdir -p ~/.local/share + juju bootstrap microk8s dev-controller + +It could take a few minutes to download the images. + diff --git a/docs/reuse/tutorial/setup_stable.rst b/docs/reuse/tutorial/setup_stable.rst index 7737854e9..3169f2dce 100644 --- a/docs/reuse/tutorial/setup_stable.rst +++ b/docs/reuse/tutorial/setup_stable.rst @@ -1,4 +1,17 @@ -First, `install Multipass `_. +.. warning:: + + This tutorial requires version ``3.2.0`` or later of Charmcraft. Check the + version of Charmcraft using ``charmcraft --version`` If you have an older + version of Charmcraft installed, use + ``sudo snap refresh charmcraft --channel latest/edge`` to get the latest + edge version of Charmcraft. + +First, install Multipass. + +.. seealso:: + + See more: `Multipass | + How to install Multipass `_ Use Multipass to launch an Ubuntu VM with the name ``charm-dev`` from the 24.04 blueprint: @@ -13,58 +26,67 @@ Once the VM is up, open a shell into it: multipass shell charm-dev -In order to create the rock, you'll need to install Rockcraft: +In order to create the rock, you need to install Rockcraft with +classic confinement, which grants it access to the whole file system: .. code-block:: bash sudo snap install rockcraft --classic -``LXD`` will be required for building the rock. -Make sure it is installed and initialised: +LXD will be required for building the rock. +Make sure it is installed and initialized: .. code-block:: bash - sudo snap install lxd + lxd --version lxd init --auto +If ``LXD`` is not installed, install it with ``sudo snap install lxd``. + In order to create the charm, you'll need to install Charmcraft: .. code-block:: bash sudo snap install charmcraft --channel latest/stable --classic -.. warning:: - - This tutorial requires version ``3.0.0`` or later of Charmcraft. Check the - version of Charmcraft using ``charmcraft --version`` If you have an older - version of Charmcraft installed, use - ``sudo snap refresh charmcraft --channel latest/edge`` to get the latest - edge version of Charmcraft. - -MicroK8s is required to deploy the Flask application on Kubernetes. Install MicroK8s: +MicroK8s is required to deploy the |12FactorApp| application on Kubernetes. +Let's install MicroK8s using the ``1.31-strict/stable`` track: -.. code-block:: bash +.. code:: sudo snap install microk8s --channel 1.31-strict/stable sudo adduser $USER snap_microk8s newgrp snap_microk8s -Wait for MicroK8s to be ready using ``sudo microk8s status --wait-ready``. + Several MicroK8s add-ons are required for deployment: .. code-block:: bash + # Required for Juju to provide storage volumes sudo microk8s enable hostpath-storage - # Required to host the OCI image of the Flask application + # Required to host the OCI image of the application sudo microk8s enable registry - # Required to expose the Flask application + # Required to expose the application sudo microk8s enable ingress -Juju is required to deploy the Flask application. -Install Juju and bootstrap a development controller: +Check the status of MicroK8s: .. code-block:: bash - sudo snap install juju --channel 3.5/stable + sudo microk8s status --wait-ready + +If successful, the terminal will output ``microk8s is running`` +along with a list of enabled and disabled add-ons. + +Juju is required to deploy the |12FactorApp| application. +Install Juju using the ``3.6/stable`` track, and bootstrap a +development controller: + +.. code:: + + sudo snap install juju --channel 3.6/stable mkdir -p ~/.local/share juju bootstrap microk8s dev-controller + +It could take a few minutes to download the images. diff --git a/docs/tutorial/code/django/greeting_charmcraft.yaml b/docs/tutorial/code/django/greeting_charmcraft.yaml new file mode 100644 index 000000000..991e8d937 --- /dev/null +++ b/docs/tutorial/code/django/greeting_charmcraft.yaml @@ -0,0 +1,7 @@ +config: + options: + greeting: + description: | + The greeting to be returned by the Django application. + default: "Hello, world!" + type: string diff --git a/docs/tutorial/code/django/postgres_requires_charmcraft.yaml b/docs/tutorial/code/django/postgres_requires_charmcraft.yaml new file mode 100644 index 000000000..239f014d8 --- /dev/null +++ b/docs/tutorial/code/django/postgres_requires_charmcraft.yaml @@ -0,0 +1,4 @@ +requires: + postgresql: + interface: postgresql_client + optional: false diff --git a/docs/tutorial/code/django/requirements.txt b/docs/tutorial/code/django/requirements.txt new file mode 100644 index 000000000..fb22bd75c --- /dev/null +++ b/docs/tutorial/code/django/requirements.txt @@ -0,0 +1,2 @@ +Django +psycopg2-binary diff --git a/docs/tutorial/code/django/settings_init_rock.py b/docs/tutorial/code/django/settings_init_rock.py new file mode 100644 index 000000000..fcc9b82dd --- /dev/null +++ b/docs/tutorial/code/django/settings_init_rock.py @@ -0,0 +1,129 @@ +""" +Django settings for django_hello_world project. + +Generated by 'django-admin startproject' using Django 5.1.4. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.1/ref/settings/ +""" + +from pathlib import Path +import json +import os +import secrets + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', secrets.token_hex(32)) + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = os.environ.get('DJANGO_DEBUG', 'false') == 'true' + +ALLOWED_HOSTS = json.loads(os.environ.get('DJANGO_ALLOWED_HOSTS', '[]')) + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'django_hello_world.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'django_hello_world.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.environ.get('POSTGRESQL_DB_NAME'), + 'USER': os.environ.get('POSTGRESQL_DB_USERNAME'), + 'PASSWORD': os.environ.get('POSTGRESQL_DB_PASSWORD'), + 'HOST': os.environ.get('POSTGRESQL_DB_HOSTNAME'), + 'PORT': os.environ.get('POSTGRESQL_DB_PORT'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.1/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/docs/tutorial/code/django/settings_local_run.py b/docs/tutorial/code/django/settings_local_run.py new file mode 100644 index 000000000..d633191ae --- /dev/null +++ b/docs/tutorial/code/django/settings_local_run.py @@ -0,0 +1,123 @@ +""" +Django settings for django_hello_world project. + +Generated by 'django-admin startproject' using Django 5.1.5. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.1/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-o-^flry43te!3t1unbrjw9kmt(4-)yghzfg(j5*0n79s#-km)y' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ['*'] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'django_hello_world.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'django_hello_world.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.1/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/docs/tutorial/code/django/task.yaml b/docs/tutorial/code/django/task.yaml new file mode 100644 index 000000000..69682c652 --- /dev/null +++ b/docs/tutorial/code/django/task.yaml @@ -0,0 +1,273 @@ +########################################### +# IMPORTANT +# Comments matter! +# The docs use the wrapping comments as +# markers for including said instructions +# as snippets in the docs. +########################################### +summary: Getting started with Django tutorial + +kill-timeout: 180m + +environment: + +execute: | + # Move everything to $HOME so that Juju deployment works + mv *.yaml *.py *.txt $HOME + cd $HOME + + # Don't use the staging store for this test + unset CHARMCRAFT_STORE_API_URL + unset CHARMCRAFT_UPLOAD_URL + unset CHARMCRAFT_REGISTRY_URL + + # Add setup instructions + # (Ran into issues in prepare section) + # (don't install charmcraft) + snap install rockcraft --classic + snap install lxd + lxd init --auto + snap install microk8s --channel=1.31-strict/stable + snap install juju --channel=3.5/stable + snap refresh juju --channel=3.5/stable --amend + + # Juju config setup + lxc network set lxdbr0 ipv6.address none + mkdir -p ~/.local/share + + # MicroK8s config setup + microk8s status --wait-ready + microk8s enable hostpath-storage + microk8s enable registry + microk8s enable ingress + + # Bootstrap controller + juju bootstrap microk8s dev-controller + + # Create working dir and cd + mkdir django-hello-world + cd django-hello-world + + # [docs:create-venv] + sudo apt update && sudo apt install python3-venv -y + python3 -m venv .venv + source .venv/bin/activate + # [docs:create-venv-end] + + mv $HOME/requirements.txt $HOME/django-hello-world/ + # [docs:install-requirements] + pip install -r requirements.txt + # [docs:install-requirements-end] + + # [docs:django-startproject] + django-admin startproject django_hello_world + # [docs:django-startproject-end] + + # cd into django project dir + cd django_hello_world + + # Update settings.py file + cat $HOME/settings_local_run.py > $HOME/django-hello-world/django_hello_world/django_hello_world/settings.py + + # Run Django app locally + python3 manage.py runserver 0.0.0.0:8000 & + + # Test the Django app + retry -n 5 --wait 2 curl --fail localhost:8000 + + curl localhost:8000 | grep Congratulations + + kill $! + cd $HOME/django-hello-world + + # [docs:create-rockcraft-yaml] + rockcraft init --profile django-framework + # [docs:create-rockcraft-yaml-end] + + sed -i "s/name: .*/name: django-hello-world/g" rockcraft.yaml + sed -i "s/amd64/$(dpkg --print-architecture)/g" rockcraft.yaml + + # Update settings.py file + cat $HOME/settings_init_rock.py > $HOME/django-hello-world/django_hello_world/django_hello_world/settings.py + + # [docs:pack] + rockcraft pack + # [docs:pack-end] + + # [docs:skopeo-copy] + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:django-hello-world_0.1_$(dpkg --print-architecture).rock \ + docker://localhost:32000/django-hello-world:0.1 + # [docs:skopeo-copy-end] + + # [docs:create-charm-dir] + mkdir charm + cd charm + # [docs:create-charm-dir-end] + + # [docs:charm-init] + charmcraft init --profile django-framework --name django-hello-world + # [docs:charm-init-end] + + # Add postgresql_client to charmcraft.yaml + cat $HOME/postgres_requires_charmcraft.yaml >> charmcraft.yaml + + # [docs:charm-pack] + charmcraft pack + # [docs:charm-pack-end] + + # [docs:add-juju-model] + juju add-model django-hello-world + # [docs:add-juju-model-end] + + # [docs:add-model-constraints] + juju set-model-constraints -m django-hello-world \ + arch=$(dpkg --print-architecture) + # [docs:add-model-constraints-end] + + # [docs:deploy-django-app] + juju deploy \ + ./django-hello-world_ubuntu-22.04-$(dpkg --print-architecture).charm \ + django-hello-world --resource \ + django-app-image=localhost:32000/django-hello-world:0.1 + # [docs:deploy-django-app-end] + + # [docs:deploy-postgres] + juju deploy postgresql-k8s --trust + # [docs:deploy-postgres-end] + + # [docs:integrate-postgres] + juju integrate django-hello-world postgresql-k8s + # [docs:integrate-postgres-end] + + # Check that django-hello-world and postgres are active idle + juju wait-for application postgresql-k8s --query='status=="active"' --timeout 10m + juju wait-for application django-hello-world --query='status=="active"' --timeout 10m + + # [docs:config-debug] + juju config django-hello-world django-debug=true + # [docs:config-debug-end] + + # [docs:deploy-nginx] + juju deploy nginx-ingress-integrator --channel=latest/stable --trust + juju integrate nginx-ingress-integrator django-hello-world + # [docs:deploy-nginx-end] + + # [docs:config-nginx] + juju config nginx-ingress-integrator \ + service-hostname=django-hello-world path-routes=/ + # [docs:config-nginx-end] + + # give Juju some time to deploy the apps + juju wait-for application django-hello-world --query='status=="active"' --timeout 10m + juju wait-for application nginx-ingress-integrator --query='status=="active"' --timeout 10m + + juju status --relations + sleep 30s + + # curl the Django app + curl http://django-hello-world --resolve django-hello-world:80:127.0.0.1 | grep Congratulations + + cd .. + cd django_hello_world + + # [docs:startapp-greeting] + django-admin startapp greeting + # [docs:startapp-greeting-end] + + # Update greeting/views.py file + cat $HOME/views_greeting.py > greeting/views.py + + # Create greeting/urls.py file + cat $HOME/urls_greeting.py > greeting/urls.py + + # Update django_hello_world/urls.py file + cat $HOME/urls_django_hello_world.py > django_hello_world/urls.py + + cd .. + sed -i "s/version: .*/version: 0.2/g" rockcraft.yaml + + # [docs:repack-update] + rockcraft pack + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:django-hello-world_0.2_$(dpkg --print-architecture).rock \ + docker://localhost:32000/django-hello-world:0.2 + # [docs:repack-update-end] + + # [docs:refresh-deployment] + cd charm + juju refresh django-hello-world \ + --path=./django-hello-world_ubuntu-22.04-$(dpkg --print-architecture).charm \ + --resource django-app-image=localhost:32000/django-hello-world:0.2 + # [docs:refresh-deployment-end] + + # [docs:disable-debug-mode] + juju config django-hello-world django-debug=false + # [docs:disable-debug-mode-end] + + # give Juju some time to refresh the app + juju wait-for application django-hello-world --query='status=="maintenance"' --timeout 10m + juju wait-for application django-hello-world --query='status=="active"' --timeout 10m + + # curl and check that the response is Hello, world! + curl http://django-hello-world --resolve django-hello-world:80:127.0.0.1 | grep Hello + + cd .. + # Update django_hello_world/greeting/views.py + cat $HOME/views_greeting_configuration.py > django_hello_world/greeting/views.py + + sed -i "s/version: .*/version: 0.3/g" rockcraft.yaml + + # [docs:repack-2nd-update] + rockcraft pack + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:django-hello-world_0.3_$(dpkg --print-architecture).rock \ + docker://localhost:32000/django-hello-world:0.3 + # [docs:repack-2nd-update-end] + + cd charm + + # Update greeting config in charmcraft.yaml + cat $HOME/greeting_charmcraft.yaml >> ./charmcraft.yaml + + # [docs:repack-refresh-2nd-deployment] + charmcraft pack + juju refresh django-hello-world \ + --path=./django-hello-world_ubuntu-22.04-$(dpkg --print-architecture).charm \ + --resource django-app-image=localhost:32000/django-hello-world:0.3 + # [docs:repack-refresh-2nd-deployment-end] + + # Wait for django-hello-world to be active + juju wait-for application django-hello-world --query='status=="active"' --timeout 10m + + # curl and check that the response is still Hello, world! + curl http://django-hello-world --resolve django-hello-world:80:127.0.0.1 | grep Hello + + # [docs:change-config] + juju config django-hello-world greeting='Hi!' + # [docs:change-config-end] + + # make sure that the application updates + juju wait-for application django-hello-world --query='status=="maintenance"' --timeout 10m + juju wait-for application django-hello-world --query='status=="active"' --timeout 10m + + # curl and check that the response is now Hi + curl http://django-hello-world --resolve django-hello-world:80:127.0.0.1 | grep Hi + + # [docs:clean-environment] + charmcraft clean + # Back out to main directory for cleanup + cd .. + rockcraft clean + # exit and delete the virtual environment + deactivate + rm -rf .venv + # delete all the files created during the tutorial + rm -rf charm __pycache__ django_hello_world + rm django-hello-world_0.1_$(dpkg --print-architecture).rock \ + django-hello-world_0.2_$(dpkg --print-architecture).rock \ + django-hello-world_0.3_$(dpkg --print-architecture).rock \ + rockcraft.yaml requirements.txt + # Remove the juju model + juju destroy-model django-hello-world --destroy-storage --no-prompt --force + # [docs:clean-environment-end] diff --git a/docs/tutorial/code/django/urls_django_hello_world.py b/docs/tutorial/code/django/urls_django_hello_world.py new file mode 100644 index 000000000..1aab4f0a9 --- /dev/null +++ b/docs/tutorial/code/django/urls_django_hello_world.py @@ -0,0 +1,23 @@ +""" +URL configuration for django_hello_world project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import include, path + +urlpatterns = [ + path("", include("greeting.urls")), + path('admin/', admin.site.urls), +] diff --git a/docs/tutorial/code/django/urls_greeting.py b/docs/tutorial/code/django/urls_greeting.py new file mode 100644 index 000000000..5119061b3 --- /dev/null +++ b/docs/tutorial/code/django/urls_greeting.py @@ -0,0 +1,7 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path("", views.index, name="index"), +] diff --git a/docs/tutorial/code/django/views_greeting.py b/docs/tutorial/code/django/views_greeting.py new file mode 100644 index 000000000..d7beb239b --- /dev/null +++ b/docs/tutorial/code/django/views_greeting.py @@ -0,0 +1,5 @@ +from django.http import HttpResponse + + +def index(request): + return HttpResponse("Hello, world!\n") diff --git a/docs/tutorial/code/django/views_greeting_configuration.py b/docs/tutorial/code/django/views_greeting_configuration.py new file mode 100644 index 000000000..b41d2a502 --- /dev/null +++ b/docs/tutorial/code/django/views_greeting_configuration.py @@ -0,0 +1,7 @@ +import os + +from django.http import HttpResponse + + +def index(request): + return HttpResponse(f"{os.environ.get('DJANGO_GREETING', 'Hello, world!')}\n") diff --git a/docs/tutorial/code/fastapi/app.py b/docs/tutorial/code/fastapi/app.py new file mode 100644 index 000000000..2a268787c --- /dev/null +++ b/docs/tutorial/code/fastapi/app.py @@ -0,0 +1,7 @@ +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/") +async def root(): + return {"message": "Hello World"} diff --git a/docs/tutorial/code/fastapi/greeting_app.py b/docs/tutorial/code/fastapi/greeting_app.py new file mode 100644 index 000000000..4e70892b2 --- /dev/null +++ b/docs/tutorial/code/fastapi/greeting_app.py @@ -0,0 +1,9 @@ +import os + +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/") +async def root(): + return {"message": os.getenv("APP_GREETING", "Hello World")} diff --git a/docs/tutorial/code/fastapi/greeting_charmcraft.yaml b/docs/tutorial/code/fastapi/greeting_charmcraft.yaml new file mode 100644 index 000000000..c4bf3f945 --- /dev/null +++ b/docs/tutorial/code/fastapi/greeting_charmcraft.yaml @@ -0,0 +1,9 @@ +# configuration snippet for FastAPI application + +config: + options: + greeting: + description: | + The greeting to be returned by the FastAPI application. + default: "Hello, world!" + type: string diff --git a/docs/tutorial/code/fastapi/requirements.txt b/docs/tutorial/code/fastapi/requirements.txt new file mode 100644 index 000000000..472fd3835 --- /dev/null +++ b/docs/tutorial/code/fastapi/requirements.txt @@ -0,0 +1,2 @@ +fastapi[standard] +psycopg2-binary diff --git a/docs/tutorial/code/fastapi/task.yaml b/docs/tutorial/code/fastapi/task.yaml new file mode 100644 index 000000000..1c72b96e3 --- /dev/null +++ b/docs/tutorial/code/fastapi/task.yaml @@ -0,0 +1,211 @@ +########################################### +# IMPORTANT +# Comments matter! +# The docs use the wrapping comments as +# markers for including said instructions +# as snippets in the docs. +########################################### +summary: Getting started with FastAPI tutorial + +kill-timeout: 180m + +environment: + +execute: | + # Move everything to $HOME so that Juju deployment works + mv *.yaml *.py *.txt $HOME + cd $HOME + + # Don't use the staging store for this test + unset CHARMCRAFT_STORE_API_URL + unset CHARMCRAFT_UPLOAD_URL + unset CHARMCRAFT_REGISTRY_URL + + # MicroK8s config setup + microk8s status --wait-ready + microk8s enable hostpath-storage + microk8s enable registry + microk8s enable ingress + + # Bootstrap controller + juju bootstrap microk8s dev-controller + + # [docs:create-venv] + sudo apt update && sudo apt install python3-venv -y + python3 -m venv .venv + source .venv/bin/activate + # [docs:create-venv-end] + + # [docs:install-requirements] + pip install -r requirements.txt + # [docs:install-requirements-end] + + fastapi dev app.py --port 8080 & + retry -n 5 --wait 2 curl --fail localhost:8080 + + # [docs:curl-fastapi] + curl localhost:8080 + # [docs:curl-fastapi-end] + + kill $! + + # [docs:create-rockcraft-yaml] + rockcraft init --profile fastapi-framework + # [docs:create-rockcraft-yaml-end] + + sed -i "s/name: .*/name: fastapi-hello-world/g" rockcraft.yaml + sed -i "s/amd64/$(dpkg --print-architecture)/g" rockcraft.yaml + + # [docs:pack] + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack + # [docs:pack-end] + + # [docs:skopeo-copy] + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:fastapi-hello-world_0.1_$(dpkg --print-architecture).rock \ + docker://localhost:32000/fastapi-hello-world:0.1 + # [docs:skopeo-copy-end] + + # [docs:create-charm-dir] + mkdir charm + cd charm + # [docs:create-charm-dir-end] + + # [docs:charm-init] + charmcraft init --profile fastapi-framework --name fastapi-hello-world + # [docs:charm-init-end] + + # update platforms in charmcraft.yaml file + sed -i "s/amd64/$(dpkg --print-architecture)/g" charmcraft.yaml + + # [docs:charm-pack] + CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack + # [docs:charm-pack-end] + + # [docs:add-juju-model] + juju add-model fastapi-hello-world + # [docs:add-juju-model-end] + + #[docs:add-model-constraints] + juju set-model-constraints \ + -m fastapi-hello-world arch=$(dpkg --print-architecture) + #[docs:add-model-constraints-end] + + # [docs:deploy-fastapi-app] + juju deploy \ + ./fastapi-hello-world_$(dpkg --print-architecture).charm \ + fastapi-hello-world --resource \ + app-image=localhost:32000/fastapi-hello-world:0.1 + # [docs:deploy-fastapi-app-end] + + # [docs:deploy-nginx] + juju deploy nginx-ingress-integrator --channel=latest/stable --trust + juju integrate nginx-ingress-integrator fastapi-hello-world + # [docs:deploy-nginx-end] + + # [docs:config-nginx] + juju config nginx-ingress-integrator \ + service-hostname=fastapi-hello-world path-routes=/ + # [docs:config-nginx-end] + + # give Juju some time to deploy the apps + juju wait-for application fastapi-hello-world --query='status=="active"' --timeout 10m + juju wait-for application nginx-ingress-integrator --query='status=="active"' --timeout 10m + + # [docs:curl-init-deployment] + curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 + # [docs:curl-init-deployment-end] + + cd .. + cat greeting_app.py > app.py + sed -i "s/version: .*/version: 0.2/g" rockcraft.yaml + + # [docs:docker-update] + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:fastapi-hello-world_0.2_$(dpkg --print-architecture).rock \ + docker://localhost:32000/fastapi-hello-world:0.2 + # [docs:docker-update-end] + + cat greeting_charmcraft.yaml >> ./charm/charmcraft.yaml + cd charm + + # [docs:refresh-deployment] + CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack + juju refresh fastapi-hello-world \ + --path=./fastapi-hello-world_$(dpkg --print-architecture).charm \ + --resource app-image=localhost:32000/fastapi-hello-world:0.2 + # [docs:refresh-deployment-end] + + # give Juju some time to refresh the app + juju wait-for application fastapi-hello-world --query='status=="active"' --timeout 10m + + # curl and check that the response is Hello + curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 | grep Hello + + # [docs:change-config] + juju config fastapi-hello-world greeting='Hi!' + # [docs:change-config-end] + + # make sure that the application updates + juju wait-for application fastapi-hello-world --query='status=="maintenance"' --timeout 10m + juju wait-for application fastapi-hello-world --query='status=="active"' --timeout 10m + + # curl and check that the response is now Hi + curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 | grep Hi + + cd .. + cat visitors_migrate.py >> migrate.py + cat visitors_app.py > app.py + sed -i "s/version: .*/version: 0.3/g" rockcraft.yaml + + # [docs:docker-2nd-update] + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:fastapi-hello-world_0.3_$(dpkg --print-architecture).rock \ + docker://localhost:32000/fastapi-hello-world:0.3 + # [docs:docker-2nd-update-end] + + cat visitors_charmcraft.yaml >> ./charm/charmcraft.yaml + cd charm + + # [docs:refresh-2nd-deployment] + CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack + juju refresh fastapi-hello-world \ + --path=./fastapi-hello-world_$(dpkg --print-architecture).charm \ + --resource app-image=localhost:32000/fastapi-hello-world:0.3 + # [docs:refresh-2nd-deployment-end] + + # [docs:deploy-postgres] + juju deploy postgresql-k8s --trust + juju integrate fastapi-hello-world postgresql-k8s + # [docs:deploy-postgres-end] + + # give Juju some time to deploy and refresh the apps + juju wait-for application fastapi-hello-world --query='status=="active"' --timeout 20m | juju status --relations + juju wait-for application postgresql-k8s --query='status=="active"' --timeout 20m | juju status --relations + juju wait-for application fastapi-hello-world --query='life=="alive" && status=="active"' --timeout 20m | juju status --relations + + juju status --relations + + curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 | grep Hi + curl http://fastapi-hello-world/visitors --resolve fastapi-hello-world:80:127.0.0.1 | grep 1 + curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 | grep Hi + curl http://fastapi-hello-world/visitors --resolve fastapi-hello-world:80:127.0.0.1 | grep 2 + + # [docs:clean-environment] + CHARMCRAFTCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft clean + # Back out to main directory for cleanup + cd .. + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft clean + # exit and delete the virtual environment + deactivate + rm -rf charm .venv __pycache__ + # delete all the files created during the tutorial + rm fastapi-hello-world_0.1_$(dpkg --print-architecture).rock \ + fastapi-hello-world_0.2_$(dpkg --print-architecture).rock \ + fastapi-hello-world_0.3_$(dpkg --print-architecture).rock \ + rockcraft.yaml app.py requirements.txt migrate.py + # Remove the juju model + juju destroy-model fastapi-hello-world --destroy-storage --no-prompt --force + # [docs:clean-environment-end] diff --git a/docs/tutorial/code/fastapi/visitors_app.py b/docs/tutorial/code/fastapi/visitors_app.py new file mode 100644 index 000000000..d74e25bdc --- /dev/null +++ b/docs/tutorial/code/fastapi/visitors_app.py @@ -0,0 +1,34 @@ +# FastAPI application that keeps track of visitors using a database + +import datetime +import os +from typing import Annotated + +from fastapi import FastAPI, Header +import psycopg2 + +app = FastAPI() +DATABASE_URI = os.environ["POSTGRESQL_DB_CONNECT_STRING"] + + +@app.get("/") +async def root(user_agent: Annotated[str | None, Header()] = None): + with psycopg2.connect(DATABASE_URI) as conn, conn.cursor() as cur: + timestamp = datetime.datetime.now() + + cur.execute( + "INSERT INTO visitors (timestamp, user_agent) VALUES (%s, %s)", + (timestamp, user_agent) + ) + conn.commit() + + return {"message": os.getenv("APP_GREETING", "Hello World")} + + +@app.get("/visitors") +async def visitors(): + with psycopg2.connect(DATABASE_URI) as conn, conn.cursor() as cur: + cur.execute("SELECT COUNT(*) FROM visitors") + total_visitors = cur.fetchone()[0] + + return {"count": total_visitors} diff --git a/docs/tutorial/code/fastapi/visitors_charmcraft.yaml b/docs/tutorial/code/fastapi/visitors_charmcraft.yaml new file mode 100644 index 000000000..2cc0cb84e --- /dev/null +++ b/docs/tutorial/code/fastapi/visitors_charmcraft.yaml @@ -0,0 +1,6 @@ +# requires snippet for FastAPI application with a database + +requires: + postgresql: + interface: postgresql_client + optional: false diff --git a/docs/tutorial/code/fastapi/visitors_migrate.py b/docs/tutorial/code/fastapi/visitors_migrate.py new file mode 100644 index 000000000..9101e06b1 --- /dev/null +++ b/docs/tutorial/code/fastapi/visitors_migrate.py @@ -0,0 +1,21 @@ +# Adds database to FastAPI application + +import os + +import psycopg2 + +DATABASE_URI = os.environ["POSTGRESQL_DB_CONNECT_STRING"] + +def migrate(): + with psycopg2.connect(DATABASE_URI) as conn, conn.cursor() as cur: + cur.execute(""" + CREATE TABLE IF NOT EXISTS visitors ( + timestamp TIMESTAMP NOT NULL, + user_agent TEXT NOT NULL + ); + """) + conn.commit() + + +if __name__ == "__main__": + migrate() diff --git a/docs/tutorial/code/flask/requirements.txt b/docs/tutorial/code/flask/requirements.txt index e3e9a71d9..630d4b819 100644 --- a/docs/tutorial/code/flask/requirements.txt +++ b/docs/tutorial/code/flask/requirements.txt @@ -1 +1,2 @@ Flask +psycopg2-binary diff --git a/docs/tutorial/code/flask/task.yaml b/docs/tutorial/code/flask/task.yaml index 64de8176a..0a610b234 100644 --- a/docs/tutorial/code/flask/task.yaml +++ b/docs/tutorial/code/flask/task.yaml @@ -31,12 +31,15 @@ execute: | juju bootstrap microk8s dev-controller # [docs:create-venv] - sudo apt-get update && sudo apt-get install python3-venv -y + sudo apt update && sudo apt install python3-venv -y python3 -m venv .venv source .venv/bin/activate - pip install -r requirements.txt # [docs:create-venv-end] + # [docs:install-requirements] + pip install -r requirements.txt + # [docs:install-requirements-end] + flask run -p 8000 & retry -n 5 --wait 2 curl --fail localhost:8000 @@ -57,10 +60,6 @@ execute: | rockcraft pack # [docs:pack-end] - # [docs:ls-rock] - ls *.rock -l - # [docs:ls-rock-end] - # [docs:skopeo-copy] rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ oci-archive:flask-hello-world_0.1_$(dpkg --print-architecture).rock \ @@ -80,24 +79,23 @@ execute: | charmcraft pack # [docs:charm-pack-end] - # [docs:ls-charm] - ls *.charm -l - # [docs:ls-charm-end] - # [docs:add-juju-model] juju add-model flask-hello-world # [docs:add-juju-model-end] + #[docs:add-model-constraints] juju set-model-constraints -m flask-hello-world arch=$(dpkg --print-architecture) + #[docs:add-model-constraints-end] - # [docs:deploy-juju-model] - juju deploy ./flask-hello-world_ubuntu-22.04-$(dpkg --print-architecture).charm \ + # [docs:deploy-flask-app] + juju deploy \ + ./flask-hello-world_ubuntu-22.04-$(dpkg --print-architecture).charm \ flask-hello-world --resource \ flask-app-image=localhost:32000/flask-hello-world:0.1 - # [docs:deploy-juju-model-end] + # [docs:deploy-flask-app-end] # [docs:deploy-nginx] - juju deploy nginx-ingress-integrator --channel=latest/edge --base ubuntu@20.04 + juju deploy nginx-ingress-integrator --channel=latest/stable --trust juju integrate nginx-ingress-integrator flask-hello-world # [docs:deploy-nginx-end] @@ -117,9 +115,9 @@ execute: | cd .. cat greeting_app.py > app.py sed -i "s/version: .*/version: 0.2/g" rockcraft.yaml - rockcraft pack # [docs:docker-update] + rockcraft pack rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ oci-archive:flask-hello-world_0.2_$(dpkg --print-architecture).rock \ docker://localhost:32000/flask-hello-world:0.2 @@ -127,9 +125,9 @@ execute: | cat greeting_charmcraft.yaml >> ./charm/charmcraft.yaml cd charm - charmcraft pack # [docs:refresh-deployment] + charmcraft pack juju refresh flask-hello-world \ --path=./flask-hello-world_ubuntu-22.04-$(dpkg --print-architecture).charm \ --resource flask-app-image=localhost:32000/flask-hello-world:0.2 @@ -156,10 +154,9 @@ execute: | cat visitors_migrate.py >> migrate.py cat visitors_app.py > app.py sed -i "s/version: .*/version: 0.3/g" rockcraft.yaml - echo "psycopg2-binary" >> requirements.txt - rockcraft pack # [docs:docker-2nd-update] + rockcraft pack rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ oci-archive:flask-hello-world_0.3_$(dpkg --print-architecture).rock \ docker://localhost:32000/flask-hello-world:0.3 @@ -167,21 +164,21 @@ execute: | cat visitors_charmcraft.yaml >> ./charm/charmcraft.yaml cd charm - charmcraft pack # [docs:refresh-2nd-deployment] + charmcraft pack juju refresh flask-hello-world \ --path=./flask-hello-world_ubuntu-22.04-$(dpkg --print-architecture).charm \ --resource flask-app-image=localhost:32000/flask-hello-world:0.3 # [docs:refresh-2nd-deployment-end] # [docs:deploy-postgres] - juju deploy postgresql-k8s --channel=14/stable --trust + juju deploy postgresql-k8s --trust juju integrate flask-hello-world postgresql-k8s # [docs:deploy-postgres-end] # give Juju some time to deploy and refresh the apps - juju wait-for application postgresql-k8s --query='status=="active"' --timeout 10m + juju wait-for application postgresql-k8s --query='status=="active"' --timeout 30m | juju status --relations juju wait-for application flask-hello-world --query='status=="active"' --timeout 30m | juju status --relations juju status --relations @@ -191,17 +188,19 @@ execute: | curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1 | grep Hi curl http://flask-hello-world/visitors --resolve flask-hello-world:80:127.0.0.1 | grep 2 - # Back out to main directory for clean-up - cd .. - # [docs:clean-environment] + charmcraft clean + # Back out to main directory for cleanup + cd .. + rockcraft clean # exit and delete the virtual environment deactivate rm -rf charm .venv __pycache__ # delete all the files created during the tutorial - rm flask-hello-world_0.1_$(dpkg --print-architecture).rock flask-hello-world_0.2_$(dpkg --print-architecture).rock \ - flask-hello-world_0.3_$(dpkg --print-architecture).rock rockcraft.yaml app.py \ - requirements.txt migrate.py + rm flask-hello-world_0.1_$(dpkg --print-architecture).rock \ + flask-hello-world_0.2_$(dpkg --print-architecture).rock \ + flask-hello-world_0.3_$(dpkg --print-architecture).rock \ + rockcraft.yaml app.py requirements.txt migrate.py # Remove the juju model juju destroy-model flask-hello-world --destroy-storage --no-prompt --force # [docs:clean-environment-end] diff --git a/docs/tutorial/code/go/greeting_charmcraft.yaml b/docs/tutorial/code/go/greeting_charmcraft.yaml new file mode 100644 index 000000000..b6bbf7c4a --- /dev/null +++ b/docs/tutorial/code/go/greeting_charmcraft.yaml @@ -0,0 +1,9 @@ +# configuration snippet for Go application + +config: + options: + greeting: + description: | + The greeting to be returned by the Go application. + default: "Hello, world!" + type: string diff --git a/docs/tutorial/code/go/greeting_main.txt b/docs/tutorial/code/go/greeting_main.txt new file mode 100644 index 000000000..03398cec9 --- /dev/null +++ b/docs/tutorial/code/go/greeting_main.txt @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" + "log" + "os" + "net/http" +) + +func helloWorldHandler(w http.ResponseWriter, req *http.Request) { + log.Printf("new hello world request") + greeting, found := os.LookupEnv("APP_GREETING") + if !found { + greeting = "Hello, world!" + } + fmt.Fprintln(w, greeting) +} + +func main() { + log.Printf("starting hello world application") + http.HandleFunc("/", helloWorldHandler) + http.ListenAndServe(":8080", nil) +} \ No newline at end of file diff --git a/docs/tutorial/code/go/main.go b/docs/tutorial/code/go/main.go new file mode 100644 index 000000000..4c010eba7 --- /dev/null +++ b/docs/tutorial/code/go/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + "log" + "net/http" +) + +func helloWorldHandler(w http.ResponseWriter, req *http.Request) { + log.Printf("new hello world request") + fmt.Fprintln(w, "Hello, world!") +} + +func main() { + log.Printf("starting hello world application") + http.HandleFunc("/", helloWorldHandler) + http.ListenAndServe(":8080", nil) +} \ No newline at end of file diff --git a/docs/tutorial/code/go/task.yaml b/docs/tutorial/code/go/task.yaml new file mode 100644 index 000000000..e9f2e9498 --- /dev/null +++ b/docs/tutorial/code/go/task.yaml @@ -0,0 +1,240 @@ +########################################### +# IMPORTANT +# Comments matter! +# The docs use the wrapping comments as +# markers for including said instructions +# as snippets in the docs. +########################################### +summary: Getting started with Go tutorial + +kill-timeout: 180m + +environment: + +execute: | + # Move everything to $HOME so that Juju deployment works + mv *.yaml *.go *.txt *.sh $HOME + cd $HOME + + # Don't use the staging store for this test + unset CHARMCRAFT_STORE_API_URL + unset CHARMCRAFT_UPLOAD_URL + unset CHARMCRAFT_REGISTRY_URL + + # Add setup instructions + # (Ran into issues in prepare section) + # (don't install charmcraft) + snap install rockcraft --classic + snap install lxd + lxd init --auto + snap install microk8s --channel=1.31-strict/stable + snap install juju --channel=3.5/stable + snap refresh juju --channel=3.5/stable --amend + + # Juju config setup + lxc network set lxdbr0 ipv6.address none + mkdir -p ~/.local/share + + # MicroK8s config setup + microk8s status --wait-ready + microk8s enable hostpath-storage + microk8s enable registry + microk8s enable ingress + + # Bootstrap controller + juju bootstrap microk8s dev-controller + + # [docs:create-working-dir] + mkdir go-hello-world + cd go-hello-world + # [docs:create-working-dir-end] + + cd .. + mv *.yaml *.go *.txt *.sh $HOME/go-hello-world + cd go-hello-world + + # [docs:install-init-go] + sudo snap install go --classic + go mod init go-hello-world + # [docs:install-init-go-end] + + # [docs:build-go] + go build . + # [docs:build-go-end] + + ./go-hello-world & + retry -n 5 --wait 2 curl --fail localhost:8080 + + # [docs:curl-go] + curl localhost:8080 + # [docs:curl-go-end] + + kill $! + + # [docs:create-rockcraft-yaml] + rockcraft init --profile go-framework + # [docs:create-rockcraft-yaml-end] + + sed -i "s/name: .*/name: go-hello-world/g" rockcraft.yaml + sed -i "s/amd64/$(dpkg --print-architecture)/g" rockcraft.yaml + + # [docs:pack] + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack + # [docs:pack-end] + + # [docs:skopeo-copy] + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:go-hello-world_0.1_$(dpkg --print-architecture).rock \ + docker://localhost:32000/go-hello-world:0.1 + # [docs:skopeo-copy-end] + + # [docs:create-charm-dir] + mkdir charm + cd charm + # [docs:create-charm-dir-end] + + # [docs:charm-init] + charmcraft init --profile go-framework --name go-hello-world + # [docs:charm-init-end] + + # update platforms in charmcraft.yaml file + sed -i "s/amd64/$(dpkg --print-architecture)/g" charmcraft.yaml + + # [docs:charm-pack] + CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack + # [docs:charm-pack-end] + + # [docs:add-juju-model] + juju add-model go-hello-world + # [docs:add-juju-model-end] + + #[docs:add-model-constraints] + juju set-model-constraints -m go-hello-world \ + arch=$(dpkg --print-architecture) + #[docs:add-model-constraints-end] + + # [docs:deploy-go-app] + juju deploy \ + ./go-hello-world_$(dpkg --print-architecture).charm \ + go-hello-world --resource \ + app-image=localhost:32000/go-hello-world:0.1 + # [docs:deploy-go-app-end] + + # [docs:deploy-nginx] + juju deploy nginx-ingress-integrator --channel=latest/stable --trust + juju integrate nginx-ingress-integrator go-hello-world + # [docs:deploy-nginx-end] + + # [docs:config-nginx] + juju config nginx-ingress-integrator \ + service-hostname=go-hello-world path-routes=/ + # [docs:config-nginx-end] + + # give Juju some time to deploy the apps + juju wait-for application go-hello-world --query='status=="active"' --timeout 10m + juju wait-for application nginx-ingress-integrator --query='status=="active"' --timeout 10m + + # [docs:curl-init-deployment] + curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1 + # [docs:curl-init-deployment-end] + + cd .. + cat greeting_main.txt > main.go + sed -i "s/version: .*/version: 0.2/g" rockcraft.yaml + + # [docs:docker-update] + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:go-hello-world_0.2_$(dpkg --print-architecture).rock \ + docker://localhost:32000/go-hello-world:0.2 + # [docs:docker-update-end] + + cat greeting_charmcraft.yaml >> ./charm/charmcraft.yaml + cd charm + + # [docs:refresh-deployment] + CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack + juju refresh go-hello-world \ + --path=./go-hello-world_$(dpkg --print-architecture).charm \ + --resource app-image=localhost:32000/go-hello-world:0.2 + # [docs:refresh-deployment-end] + + # give Juju some time to refresh the app + juju wait-for application go-hello-world --query='status=="active"' --timeout 10m + + # curl and check that the response is Hello + curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1 | grep Hello + + # [docs:change-config] + juju config go-hello-world greeting='Hi!' + # [docs:change-config-end] + + # make sure that the application updates + juju wait-for application go-hello-world --query='status=="maintenance"' --timeout 10m + juju wait-for application go-hello-world --query='status=="active"' --timeout 10m + + # curl and check that the response is now Hi + curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1 | grep Hi + + cd .. + cat visitors_migrate.sh >> migrate.sh + + # [docs:change-migrate-permissions] + chmod u+x migrate.sh + # [docs:change-migrate-permissions-end] + + cat visitors_rockcraft.yaml >> rockcraft.yaml + sed -i "s/version: .*/version: 0.3/g" rockcraft.yaml + + cat visitors_main.txt > main.go + + # [docs:check-go-app] + go mod tidy + # [docs:check-go-app-end] + + # [docs:docker-2nd-update] + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack + rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ + oci-archive:go-hello-world_0.3_$(dpkg --print-architecture).rock \ + docker://localhost:32000/go-hello-world:0.3 + # [docs:docker-2nd-update-end] + + cat visitors_charmcraft.yaml >> ./charm/charmcraft.yaml + cd charm + + # [docs:refresh-2nd-deployment] + CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack + juju refresh go-hello-world \ + --path=./go-hello-world_$(dpkg --print-architecture).charm \ + --resource app-image=localhost:32000/go-hello-world:0.3 + # [docs:refresh-2nd-deployment-end] + + # [docs:deploy-postgres] + juju deploy postgresql-k8s --trust + juju integrate go-hello-world postgresql-k8s + # [docs:deploy-postgres-end] + + # give Juju some time to deploy and refresh the apps + juju wait-for application postgresql-k8s --query='status=="active"' --timeout 20m | juju status --relations + juju wait-for application go-hello-world --query='status=="active"' --timeout 20m | juju status --relations + + curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1 | grep Hi + curl http://go-hello-world/visitors --resolve go-hello-world:80:127.0.0.1 | grep 1 + curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1 | grep Hi + curl http://go-hello-world/visitors --resolve go-hello-world:80:127.0.0.1 | grep 2 + + # [docs:clean-environment] + CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft clean + # Back out to main directory for cleanup + cd .. + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft clean + # exit and delete the charm dir + rm -rf charm + # delete all the files created during the tutorial + rm go-hello-world_0.1_$(dpkg --print-architecture).rock \ + go-hello-world_0.2_$(dpkg --print-architecture).rock \ + go-hello-world_0.3_$(dpkg --print-architecture).rock \ + rockcraft.yaml main.go migrate.sh go-hello-world go.mod go.sum + # Remove the juju model + juju destroy-model go-hello-world --destroy-storage --no-prompt --force + # [docs:clean-environment-end] diff --git a/docs/tutorial/code/go/visitors_charmcraft.yaml b/docs/tutorial/code/go/visitors_charmcraft.yaml new file mode 100644 index 000000000..7918b4dfe --- /dev/null +++ b/docs/tutorial/code/go/visitors_charmcraft.yaml @@ -0,0 +1,6 @@ +# requires snippet for Go application with a database + +requires: + postgresql: + interface: postgresql_client + optional: false diff --git a/docs/tutorial/code/go/visitors_main.txt b/docs/tutorial/code/go/visitors_main.txt new file mode 100644 index 000000000..8e8d1ed17 --- /dev/null +++ b/docs/tutorial/code/go/visitors_main.txt @@ -0,0 +1,63 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + "net/http" + "os" + "time" + + _ "github.com/jackc/pgx/v5/stdlib" +) + +func helloWorldHandler(w http.ResponseWriter, req *http.Request) { + log.Printf("new hello world request") + postgresqlURL := os.Getenv("POSTGRESQL_DB_CONNECT_STRING") + db, err := sql.Open("pgx", postgresqlURL) + if err != nil { + log.Printf("An error occurred while connecting to postgresql: %v", err) + return + } + defer db.Close() + + ua := req.Header.Get("User-Agent") + timestamp := time.Now() + _, err = db.Exec("INSERT into visitors (timestamp, user_agent) VALUES ($1, $2)", timestamp, ua) + if err != nil { + log.Printf("An error occurred while executing query: %v", err) + return + } + + greeting, found := os.LookupEnv("APP_GREETING") + if !found { + greeting = "Hello, world!" + } + + fmt.Fprintln(w, greeting) +} + +func visitorsHandler(w http.ResponseWriter, req *http.Request) { + log.Printf("visitors request") + postgresqlURL := os.Getenv("POSTGRESQL_DB_CONNECT_STRING") + db, err := sql.Open("pgx", postgresqlURL) + if err != nil { + return + } + defer db.Close() + + var numVisitors int + err = db.QueryRow("SELECT count(*) from visitors").Scan(&numVisitors) + if err != nil { + log.Printf("An error occurred while executing query: %v", err) + return + } + fmt.Fprintf(w, "Number of visitors %d\n", numVisitors) +} + +func main() { + log.Printf("starting hello world application") + http.HandleFunc("/", helloWorldHandler) + http.HandleFunc("/visitors", visitorsHandler) + http.ListenAndServe(":8080", nil) +} \ No newline at end of file diff --git a/docs/tutorial/code/go/visitors_migrate.sh b/docs/tutorial/code/go/visitors_migrate.sh new file mode 100644 index 000000000..af1270a58 --- /dev/null +++ b/docs/tutorial/code/go/visitors_migrate.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +PGPASSWORD="${POSTGRESQL_DB_PASSWORD}" psql -h "${POSTGRESQL_DB_HOSTNAME}" -U "${POSTGRESQL_DB_USERNAME}" "${POSTGRESQL_DB_NAME}" -c "CREATE TABLE IF NOT EXISTS visitors (timestamp TIMESTAMP NOT NULL, user_agent TEXT NOT NULL);" diff --git a/docs/tutorial/code/go/visitors_rockcraft.yaml b/docs/tutorial/code/go/visitors_rockcraft.yaml new file mode 100644 index 000000000..55f4c1f3c --- /dev/null +++ b/docs/tutorial/code/go/visitors_rockcraft.yaml @@ -0,0 +1,11 @@ +parts: + runtime-debs: + plugin: nil + stage-packages: + # Added manually for the migrations + - postgresql-client + runtime-slices: + plugin: nil + stage-packages: + # Added manually for the migrations + - bash_bins diff --git a/docs/tutorial/flask.rst b/docs/tutorial/flask.rst deleted file mode 100644 index e6d59bb94..000000000 --- a/docs/tutorial/flask.rst +++ /dev/null @@ -1,525 +0,0 @@ -================================================= -Write your first Kubernetes charm for a Flask app -================================================= - -Imagine you have a Flask application backed up by a database -such as PostgreSQL and need to deploy it. In a traditional setup, -this can be quite a challenge, but with Charmcraft you'll find -yourself packaging and deploying your Flask application in no time. -Let's get started! - -In this tutorial we will build a Kubernetes charm for a Flask -application using Charmcraft, so we can have a Flask application -up and running with Juju. - -This tutorial should take 90 minutes for you to complete. - -.. note:: - If you're new to the charming world: Flask applications are - specifically supported with a coordinated pair of profiles - for an OCI container image (**rock**) and corresponding - packaged software (**charm**) that allow for the application - to be deployed, integrated and operated on a Kubernetes - cluster with the Juju orchestration engine. - -What you'll need -================ - -- A workstation, e.g., a laptop, with amd64 or arm64 architecture which - has sufficient resources to launch a virtual machine with 4 CPUs, - 4 GB RAM, and a 50 GB disk -- Familiarity with Linux - -What you'll do -============== - -- Set things up -- Create the Flask application -- Run the Flask application locally -- Pack the Flask application into a rock called ``flask-hello-world`` -- Create the charm called ``flask-hello-world`` -- Deploy the Flask application and expose via ingress -- Enable ``juju config flask-hello-world greeting=`` -- Integrate with a database -- Clean up environment - -Set things up -============= - -.. include:: /reuse/tutorial/setup_stable.rst - -Finally, let's create a new directory for this tutorial and go -inside it: - -.. code-block:: bash - - mkdir flask-hello-world - cd flask-hello-world - -Create the Flask application -============================ - -Let's start by creating the "Hello, world" Flask application that -will be used for this tutorial. - -Create a ``requirements.txt`` file, copy the following text into it -and then save it: - -.. literalinclude:: code/flask/requirements.txt - -In the same directory, copy and save the following into a text file -called ``app.py``: - -.. literalinclude:: code/flask/app.py - :language: python - -Run the Flask application locally -================================= - -Let's install ``python3-venv`` and create a virtual environment: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:create-venv] - :end-before: [docs:create-venv-end] - :dedent: 2 - -Now that we have a virtual environment with all the dependencies, let's -run the Flask application to verify that it works: - -.. code-block:: bash - - flask run -p 8000 - -Test the Flask application by using ``curl`` to send a request to the root -endpoint. You will need a new terminal for this; use -``multipass shell charm-dev`` to get another terminal: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:curl-flask] - :end-before: [docs:curl-flask-end] - :dedent: 2 - -The Flask application should respond with ``Hello, world!``. The Flask -application looks good, so we can stop for now using -:kbd:`Ctrl` + :kbd:`C`. - -Pack the Flask application into a rock -====================================== - -First, we'll need a ``rockcraft.yaml`` file. Rockcraft will automate its -creation and tailoring for a Flask application by using the -``flask-framework`` profile: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:create-rockcraft-yaml] - :end-before: [docs:create-rockcraft-yaml-end] - :dedent: 2 - -The ``rockcraft.yaml`` file will automatically be created and set the name -based on your working directory. Choosing a different name or running on -a platform different from ``amd64`` will influence the names of the files -generated by Rockcraft. - -Open the file in a text editor and check that the ``name`` is -``flask-hello-world``. - -Ensure that ``platforms`` includes the architecture of your host. Check -the architecture of your system: - -.. code-block:: bash - - dpkg --print-architecture - - -If your host uses the ARM architecture, include ``arm64`` in ``platforms``. - -Now let's pack the rock: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:pack] - :end-before: [docs:pack-end] - :dedent: 2 - -Depending on your system and network, this step can take a couple of -minutes to finish. - -Once Rockcraft has finished packing the Flask rock, you'll find a new file -in your working directory with the ``.rock`` extension: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:ls-rock] - :end-before: [docs:ls-rock-end] - :dedent: 2 - -The rock needs to be copied to the MicroK8s registry so that it can be -deployed in the Kubernetes cluster: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:skopeo-copy] - :end-before: [docs:skopeo-copy-end] - :dedent: 2 - -.. seealso:: - - See more: `Ubuntu manpage | skopeo - `_ - -Create the charm -================ - -Let's create a new directory for the charm and go inside it: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:create-charm-dir] - :end-before: [docs:create-charm-dir-end] - :dedent: 2 - -We'll need the project's ``charmcraft.yaml``, ``requirements.txt`` and source code for -the charm. The source code contains the logic required to operate the Flask application. -Charmcraft will automate the creation of these files by using the ``flask-framework`` -profile: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:charm-init] - :end-before: [docs:charm-init-end] - :dedent: 2 - -The files will automatically be created in your working directory. -Let's pack the charm: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:charm-pack] - :end-before: [docs:charm-pack-end] - :dedent: 2 - -Depending on your system and network, this step can take a couple -of minutes to finish. - -Once Charmcraft has finished packing the charm, you'll find a new file in your -working directory with the ``.charm`` extension: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:ls-charm] - :end-before: [docs:ls-charm-end] - :dedent: 2 - -.. note:: - - If you changed the project name or are not on the amd64 platform, the name of the - ``.charm`` file will be different for you. - -Deploy the Flask application -============================ - -A Juju model is needed to deploy the application. Let's create a new model: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:add-juju-model] - :end-before: [docs:add-juju-model-end] - :dedent: 2 - -If you are not on a host with the ``amd64`` architecture, you will need to include -a constraint to the Juju model to specify your architecture. Check the -architecture of your system using ``dpkg --print-architecture``. - -For the ``arm64`` architecture, set the model constraints using - -.. code-block:: - - juju set-model-constraints -m flask-hello-world arch=arm64 - -Now the Flask application can be deployed using Juju: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:deploy-juju-model] - :end-before: [docs:deploy-juju-model-end] - :dedent: 2 - -It will take a few minutes to deploy the Flask application. You can monitor the -progress using ``juju status --watch 5s``. Once the status of the App has gone -to ``active``, you can stop watching using :kbd:`Ctrl` + :kbd:`C`. - -.. seealso:: - - See more: :external+juju:ref:`Juju | juju status ` - -The Flask application should now be running. We can monitor the status of the deployment -using ``juju status`` which should be similar to the following output: - -.. terminal:: - - Model Controller Cloud/Region Version SLA Timestamp - flask-hello-world dev-controller microk8s/localhost 3.1.8 unsupported 17:04:11+10:00 - - App Version Status Scale Charm Channel Rev Address Exposed Message - flask-hello-world active 1 flask-hello-world 0 10.152.183.166 no - - Unit Workload Agent Address Ports Message - flask-hello-world/0* active idle 10.1.87.213 - -The deployment is finished when the status shows ``active``. Let's expose the -application using ingress. Deploy the ``nginx-ingress-integrator`` charm and integrate -it with the Flask app: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:deploy-nginx] - :end-before: [docs:deploy-nginx-end] - :dedent: 2 - -The hostname of the app needs to be defined so that it is accessible via the ingress. -We will also set the default route to be the root endpoint: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:config-nginx] - :end-before: [docs:config-nginx-end] - :dedent: 2 - -Monitor ``juju status`` until everything has a status of ``active``. Test the -deployment using -``curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1`` to send -a request via the ingress to the root endpoint. It should still be returning -the ``Hello, world!`` greeting. - -.. note:: - - The ``--resolve flask-hello-world:80:127.0.0.1`` option to the ``curl`` - command is a way of resolving the hostname of the request without - setting a DNS record. - -Configure the Flask application -=============================== - -Now let's customise the greeting using a configuration option. We will expect this -configuration option to be available in the Flask app configuration under the -keyword ``GREETING``. Go back out to the root directory of the project using -``cd ..`` and copy the following code into ``app.py``: - -.. literalinclude:: code/flask/greeting_app.py - :language: python - -Open ``rockcraft.yaml`` and update the version to ``0.2``. Run ``rockcraft pack`` -again, then upload the new OCI image to the MicroK8s registry: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:docker-update] - :end-before: [docs:docker-update-end] - :dedent: 2 - -Change back into the charm directory using ``cd charm``. The ``flask-framework`` -Charmcraft extension supports adding configurations to the project file which will be -passed as environment variables to the Flask application. Add the following to the end -of the project file: - -.. code-block:: yaml - - config: - options: - greeting: - description: | - The greeting to be returned by the Flask application. - default: "Hello, world!" - type: string - -.. note:: - - Configuration options are automatically capitalised and ``-`` are replaced - by ``_``. A ``FLASK_`` prefix will also be added which will let Flask - identify which environment variables to include when running - ``app.config.from_prefixed_env()`` in ``app.py``. - -Run ``charmcraft pack`` again. We can now refresh the deployment to -make use of the new code: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:refresh-deployment] - :end-before: [docs:refresh-deployment-end] - :dedent: 2 - -Wait for ``juju status`` to show that the App is ``active`` again. Verify that -the new configuration has been added using -``juju config flask-hello-world | grep -A 6 greeting:`` which should show -the configuration option. - -.. note:: - - The ``grep`` command extracts a portion of the configuration to make - it easier to check whether the configuration option has been added. - -Using ``curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1`` -shows that the response is still ``Hello, world!`` as expected. -The greeting can be changed using Juju: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:change-config] - :end-before: [docs:change-config-end] - :dedent: 2 - -``curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1`` -now returns the updated ``Hi!`` greeting. - -.. note:: - - It might take a short time for the configuration to take effect. - -Integrate with a database -========================= - -Now let's keep track of how many visitors your application has received. -This will require integration with a database to keep the visitor count. -This will require a few changes: - -* We will need to create a database migration that creates the ``visitors`` table -* We will need to keep track how many times the root endpoint has been called - in the database -* We will need to add a new endpoint to retrieve the number of visitors from the - database - -Let's start with the database migration to create the required tables. -The charm created by the ``flask-framework`` extension will execute the -``migrate.py`` script if it exists. This script should ensure that the -database is initialised and ready to be used by the application. We will -create a ``migrate.py`` file containing this logic. - -Go back out to the tutorial root directory using ``cd ..``, open the ``migrate.py`` -file using a text editor and paste the following code into it: - -.. literalinclude:: code/flask/visitors_migrate.py - :language: python - -.. note:: - - The charm will pass the Database connection string in the - ``POSTGRESQL_DB_CONNECT_STRING`` environment variable once - postgres has been integrated with the charm. - -Open the ``rockcraft.yaml`` file in a text editor and update the version to ``0.3``. - -To be able to connect to postgresql from the Flask app the ``psycopg2-binary`` -dependency needs to be added in ``requirements.txt``. The app code also needs -to be updated to keep track of the number of visitors and to include a new -endpoint to retrieve the number of visitors to the app. Open ``app.py`` in -a text editor and replace its contents with the following code: - -.. collapse:: visitors_app.py - - .. literalinclude:: code/flask/visitors_app.py - :language: python - -Run ``rockcraft pack`` and upload the newly created rock to the MicroK8s registry: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:docker-2nd-update] - :end-before: [docs:docker-2nd-update-end] - :dedent: 2 - -Go back into the charm directory using ``cd charm``. The Flask app now requires a -database which needs to be declared in the project file. Open the project file in a text -editor and add the following section to the end: - -.. code-block:: yaml - - requires: - postgresql: - interface: postgresql_client - optional: false - -Pack the charm using ``charmcraft pack`` and refresh the deployment using Juju: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:refresh-2nd-deployment] - :end-before: [docs:refresh-2nd-deployment-end] - :dedent: 2 - -Deploy ``postgresql-k8s`` using Juju and integrate it with ``flask-hello-world``: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:deploy-postgres] - :end-before: [docs:deploy-postgres-end] - :dedent: 2 - -Wait for ``juju status`` to show that the App is ``active`` again. -Running ``curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1`` -should still return the ``Hi!`` greeting. - -To check the total visitors, use -``curl http://flask-hello-world/visitors --resolve flask-hello-world:80:127.0.0.1`` -which should return ``1`` after the previous request to the root endpoint and -should be incremented each time the root endpoint is requested. - -If we perform another request to -``curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1``, -``curl http://flask-hello-world/visitors --resolve flask-hello-world:80:127.0.0.1`` -will return ``2``. - -Clean up the environment -======================== - -If you'd like to reset your working environment, you can run the following -in the root directory for the tutorial: - -.. literalinclude:: code/flask/task.yaml - :language: bash - :start-after: [docs:clean-environment] - :end-before: [docs:clean-environment-end] - :dedent: 2 - -You can also clean up the Multipass instance. -Start by exiting it: - -.. code-block:: bash - - exit - -And then you can proceed with its deletion: - -.. code-block:: bash - - multipass delete charm-dev - multipass purge - -We've reached the end of this tutorial. We have created a Flask application, -deployed it locally, exposed it via ingress and integrated it with a database! - -Next steps -========== - -.. list-table:: - :widths: 30 30 - :header-rows: 1 - - * - If you are wondering... - - Visit... - * - "How do I...?" - - :ref:`How-to guides `, - :external+ops:ref:`Ops | How-to guides ` - * - "How do I debug?" - - `Charm debugging tools `_ - * - "How do I get in touch?" - - `Matrix channel `_ - * - "What is...?" - - :ref:`reference`, - :external+ops:ref:`Ops | Reference `, - :external+juju:ref:`Juju | Reference ` - * - "Why...?", "So what?" - - :external+ops:ref:`Ops | Explanation `, - :external+juju:ref:`Juju | Explanation ` diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index 615f25a43..b8fb78ecb 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -15,6 +15,6 @@ Our tutorial comes in multiple flavours -- pick your flavour of choice! :maxdepth: 2 write-your-first-kubernetes-charm-for-a-django-app - flask write-your-first-kubernetes-charm-for-a-fastapi-app + write-your-first-kubernetes-charm-for-a-flask-app write-your-first-kubernetes-charm-for-a-go-app diff --git a/docs/tutorial/write-your-first-kubernetes-charm-for-a-django-app.rst b/docs/tutorial/write-your-first-kubernetes-charm-for-a-django-app.rst index ef36d8ab3..d47135aa5 100644 --- a/docs/tutorial/write-your-first-kubernetes-charm-for-a-django-app.rst +++ b/docs/tutorial/write-your-first-kubernetes-charm-for-a-django-app.rst @@ -1,46 +1,45 @@ .. _write-your-first-kubernetes-charm-for-a-django-app: - Write your first Kubernetes charm for a Django app ================================================== +Imagine you have a Django application backed up by a database such as +PostgreSQL and need to deploy it. In a traditional setup, this can be +quite a challenge, but with Charmcraft you’ll find yourself packaging +and deploying your Django application in no time. -What you'll need ----------------- - -- A working station, e.g., a laptop, with amd64 architecture which has - sufficient resources to launch a virtual machine with 4 CPUs, 4GB RAM, - and a 50GB disk. - - * Note that a workstation with arm64 architecture can complete the - majority of this tutorial. -- Familiarity with Linux. -- About an hour of free time. - - -What you'll do --------------- +In this tutorial we will build a Kubernetes charm for a Django +application using Charmcraft, so we can have a Django application up and +running with Juju. Let’s get started! -Create a Django application. Use that to create a rock with ``rockcraft``. -Use that to create a charm with ``charmcraft``. Use that to test-deploy, configure, -etc., your Django application on a local Kubernetes cloud, ``microk8s``, with -``juju``. All of that multiple times, mimicking a real development process. +This tutorial should take 90 minutes for you to complete. .. note:: + If you're new to the charming world: Django applications are + specifically supported with a template to quickly generate a + **rock** (i.e., a special kind of OCI-compliant container image) + and a matching template to quickly generate a **charm** (i.e., + a software operator for cloud operations done with the Juju + orchestration engine). The result is Django applications that + can be easily deployed, configured, scaled, integrated, etc., + on any Kubernetes cluster. + +What you’ll need +---------------- - **rock** - - An Ubuntu LTS-based OCI compatible container image designed to meet security, - stability, and reliability requirements for cloud-native software. - - **charm** - - A package consisting of YAML files + Python code that will automate every - aspect of an application's lifecycle so it can be easily orchestrated with Juju. +- A local system, e.g., a laptop, with amd64 or arm64 architecture + which has sufficient resources to launch a virtual machine with 4 + CPUs, 4 GB RAM, and a 50 GB disk. +- Familiarity with Linux. - **Juju** +What you’ll do +-------------- - An orchestration engine for charmed applications. +Create a Django application. Use that to create a rock with +``rockcraft``. Use that to create a charm with ``charmcraft``. Use that +to test, deploy, configure, etc., your Django application on a local +Kubernetes cloud, ``microk8s``, with ``juju``. All of that multiple +times, mimicking a real development process. .. important:: @@ -48,652 +47,729 @@ etc., your Django application on a local Kubernetes cloud, ``microk8s``, with `Matrix `_ or `Discourse `_ - Set things up ------------- -Install Multipass. - - See more: `Multipass | How to install Multipass - `_ +.. include:: /reuse/tutorial/setup_stable.rst +.. |12FactorApp| replace:: Django -Use Multipass to launch an Ubuntu VM with the name ``charm-dev`` from the 22.04 -blueprint. +Let’s create a new directory for this tutorial and enter into it: -.. code-block:: bash +.. code:: bash - multipass launch --cpus 4 --disk 50G --memory 4G --name charm-dev 22.04 + mkdir django-hello-world + cd django-hello-world -Once the VM is up, open a shell into it: +Finally, install ``python3-venv`` and create a virtual environment: -.. code-block:: bash +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:create-venv] + :end-before: [docs:create-venv-end] + :dedent: 2 - multipass shell charm-dev - -In order to create the rock, you'll need to install Rockcraft: +Create the Django application +----------------------------- -.. code-block:: bash +Let's start by creating the "Hello, world" Django application that +will be used for this tutorial. - sudo snap install rockcraft --classic +Create a ``requirements.txt`` file using ``touch requirements.txt``. +Then, open the file in a text editor using ``nano requirements.txt``, +copy the following text into it and then save the file: -``LXD`` will be required for building the rock. Make sure it is installed and -initialised: +.. literalinclude:: code/django/requirements.txt + :caption: requirements.txt -.. code-block:: bash +.. note:: - sudo snap install lxd - lxd init --auto + The ``psycopg2-binary`` package is needed so the Django application can + connect to PostgreSQL. -In order to create the charm, you'll need to install Charmcraft: +Install the packages: -.. code-block:: bash +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:install-requirements] + :end-before: [docs:install-requirements-end] + :dedent: 2 - sudo snap install charmcraft --channel latest/edge --classic +Create a new project using ``django-admin``: -.. note:: +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:django-startproject] + :end-before: [docs:django-startproject-end] + :dedent: 2 - This tutorial requires version ``3.2.0`` or later of Charmcraft. Check the - version of Charmcraft using ``charmcraft --version``. If you have an older - version of Charmcraft installed, use ``sudo snap refresh charmcraft --channel - latest/edge`` to get the latest edge version of Charmcraft. +Run the Django application locally +---------------------------------- -MicroK8s is required to deploy the Django application on Kubernetes. Install MicroK8s: +We will test the Django application by visiting the app in a web +browser. -.. code-block:: bash +Change into the ``/django_hello_world`` directory: - sudo snap install microk8s --channel 1.31-strict/stable - sudo adduser $USER snap_microk8s - newgrp snap_microk8s +.. code:: bash -Wait for MicroK8s to be ready using ``sudo microk8s status --wait-ready``. Several -MicroK8s add-ons are required for deployment + cd django_hello_world -.. code-block:: bash +Open the settings file of the application located at +``django_hello_world/settings.py``. Update the ``ALLOWED_HOSTS`` setting +to allow all traffic: - sudo microk8s enable hostpath-storage - # Required to host the OCI image of the Django application - sudo microk8s enable registry - # Required to expose the Django application - sudo microk8s enable ingress +.. code:: python -Juju is required to deploy the Django application. Install Juju and bootstrap a -development controller: + ALLOWED_HOSTS = ['*'] -.. code-block:: bash +Save and close the ``settings.py`` file. - sudo snap install juju --channel 3.5/stable - mkdir -p ~/.local/share - juju bootstrap microk8s dev-controller +Now, run the Django application to verify that it works: -Finally, create a new directory for this tutorial and go inside it: +.. code:: bash -.. code-block:: bash + python3 manage.py runserver 0.0.0.0:8000 - mkdir django-hello-world - cd django-hello-world +.. note:: + Specifying ``0.0.0.0:8000`` allows for traffic outside of the Multipass VM. -Create the Django application ------------------------------ +Now we need the private IP address of the Multipass VM. Outside of the +Multipass VM, run: -Create a ``requirements.txt`` file, copy the following test into it and then -save it: +.. code-block:: -.. code-block:: bash + multipass info charm-dev | grep IP - Django -Install ``python3-venv`` and create a virtual environment: +With the Multipass IP address, we can visit the Django app in a web +browser. Open a new tab and visit +``http://:8000``, replacing +```` with your VM’s private IP address. -.. code-block:: bash +The Django application should respond in the browser with +``The install worked successfully! Congratulations!``. - sudo apt-get update && sudo apt-get install python3-venv -y - python3 -m venv .venv - source .venv/bin/activate - pip install -r requirements.txt +The Django application looks good, so we can stop it for now from the +original terminal of the Multipass VM using :kbd:`Ctrl` + :kbd:`C`. -Create a new project using ``django-admin``: +Pack the Django application into a rock +--------------------------------------- -.. code-block:: bash +First, we’ll need a ``rockcraft.yaml`` file. Using the +``django-framework`` profile, Rockcraft will automate the creation of +``rockcraft.yaml`` and tailor the file for a Django application. Change +back into the ``/django-hello-world`` directory and initialize the rock: - django-admin startproject django_hello_world +.. code:: bash + cd .. + rockcraft init --profile django-framework -Run the Django application locally ----------------------------------- +The ``rockcraft.yaml`` file will automatically be created and set the +name based on your working directory, ``/django-hello-world``. -Change into the ``django_hello_world`` directory and run the Django -application to verify that it works: +Check out the contents of ``rockcraft.yaml``: -.. code-block:: bash +.. code:: bash - cd django_hello_world - python3 manage.py runserver + cat rockcraft.yaml -Test the Django application by using ``curl`` to send a request to the -root endpoint. You may need a new terminal for this; if you are using -Multipass, use ``multipass shell charm-dev`` to get another terminal: +The top of the file should look similar to the following snippet: -.. code-block:: bash +.. code-block:: yaml + :caption: rockcraft.yaml - curl localhost:8000 + name: django-hello-world + # see https://documentation.ubuntu.com/rockcraft/en/1.6.0/explanation/bases/ + # for more information about bases and using 'bare' bases for chiselled rocks + base: ubuntu@22.04 # the base environment for this Django application + version: '0.1' # just for humans. Semantic versioning is recommended + summary: A summary of your Django application # 79 char long summary + description: | + This is django-hello-world's description. You have a paragraph or two to tell the + most important story about it. Keep it under 100 words though, + we live in tweetspace and your description wants to look good in the + container registries out there. + # the platforms this rock should be built on and run on. + # you can check your architecture with `dpkg --print-architecture` + platforms: + amd64: + # arm64: + # ppc64el: + # s390x: -The Django application should respond with: + ... - The install worked successfully! Congratulations! +Verify that the ``name`` is ``django-hello-world``. -.. note:: +Ensure that ``platforms`` includes the architecture of your host. Check +the architecture of your system: - The response from the Django application includes HTML and CSS which - makes it difficult to read in a terminal. +.. code:: bash -The Django application looks good, so you can stop it for now using -:kbd:`Ctrl` + :kbd:`C`. + dpkg --print-architecture +If your host uses the ARM architecture, open ``rockcraft.yaml`` in a +text editor and include ``arm64`` under ``platforms``. -Pack the Django application into a rock ---------------------------------------- +Django applications require a database. Django will use a sqlite +database by default. This won’t work on Kubernetes because the database +would disappear every time the pod is restarted (e.g., to perform an +upgrade) and this database would not be shared by all containers as the +application is scaled. We’ll use Juju later to easily deploy a database. -First, we'll need a ``rockcraft.yaml`` file. Rockcraft will automate -its creation and tailoring for a Django application by using the -``django-framework`` profile: +We’ll need to update the ``settings.py`` file to prepare for integrating +the app with a database. From the ``/django-hello-world`` directory, open +``django_hello_world/django_hello_world/settings.py`` and update the +imports to include ``json``, ``os`` and ``secrets``. The top of the +``settings.py`` file should look similar to the following snippet: -.. code-block:: bash +.. code-block:: python + :emphasize-lines: 15,16,17 - cd .. - rockcraft init --profile django-framework + """ + Django settings for django_hello_world project. -The ``rockcraft.yaml`` file will automatically be created and set -the name based on your working directory. Open it in a text editor -and check that the ``name`` is ``django-hello-world``. Ensure that -``platforms`` includes the architecture of your host. For example, -if your host uses the ARM architecture, include ``arm64`` in -``platforms``. + Generated by 'django-admin startproject' using Django 5.1.4. -.. note:: + For more information on this file, see + https://docs.djangoproject.com/en/5.1/topics/settings/ - For this tutorial, we'll use the name ``django-hello-world`` and - assume that you are on the ``amd64`` platform. Check the - architecture of your system using ``dpkg --print-architecture``. - Choosing a different name or running on a different platform will - influence the names of the files generated by Rockcraft. + For the full list of settings and their values, see + https://docs.djangoproject.com/en/5.1/ref/settings/ + """ -Django applications require a database. Django will use a sqlite -database by default. This won't work on Kubernetes because the -database would disappear every time the pod is restarted (e.g., to -perform an upgrade) and this database would not be shared by all -containers as the application is scaled. We'll use Juju later to easily -deploy a database. + from pathlib import Path -We'll need to update the ``settings.py`` file to prepare for integrating -the app with a database. Open ``django_hello_world/django_hello_world/settings.py`` -and include ``import json``, ``impost os``, and ``import secrets`` along with -the other imports at the top of the file. + import json + import os + import secrets -Near the top of the ``settings.py`` file, change the following sections to be -production-ready: +We need to change some settings to be production ready. +Near the top of the ``settings.py`` file, change the ``SECRET_KEY``, +``DEBUG`` and ``ALLOWED_HOSTS`` variables to: .. code-block:: python + :emphasize-lines: 2,5,7 - # SECURITY WARNING: keep the secret key used in production secret! - SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', secrets.token_hex(32)) + # SECURITY WARNING: keep the secret key used in production secret! + SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', secrets.token_hex(32)) - # SECURITY WARNING: don't run with debug turned on in production! - DEBUG = os.environ.get('DJANGO_DEBUG', 'false') == 'true' + # SECURITY WARNING: don't run with debug turned on in production! + DEBUG = os.environ.get('DJANGO_DEBUG', 'false') == 'true' - ALLOWED_HOSTS = json.loads(os.environ.get('DJANGO_ALLOWED_HOSTS', '{ref}`]')) + ALLOWED_HOSTS = json.loads(os.environ.get('DJANGO_ALLOWED_HOSTS', '[]')) -Go further down to the Database section and change the ``DATABASES`` variable to: +We will also use PostgreSQL as the database for our Django app. In +``settings.py``, go further down to the Database section and change the +``DATABASES`` variable to: .. code-block:: python + :emphasize-lines: 3-8 - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.environ.get('POSTGRESQL_DB_NAME'), - 'USER': os.environ.get('POSTGRESQL_DB_USERNAME'), - 'PASSWORD': os.environ.get('POSTGRESQL_DB_PASSWORD'), - 'HOST': os.environ.get('POSTGRESQL_DB_HOSTNAME'), - 'PORT': os.environ.get('POSTGRESQL_DB_PORT'), - } - } + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.environ.get('POSTGRESQL_DB_NAME'), + 'USER': os.environ.get('POSTGRESQL_DB_USERNAME'), + 'PASSWORD': os.environ.get('POSTGRESQL_DB_PASSWORD'), + 'HOST': os.environ.get('POSTGRESQL_DB_HOSTNAME'), + 'PORT': os.environ.get('POSTGRESQL_DB_PORT'), + } + } -We'll need to update the ``requirements.txt`` file to include ``psycopg2-binary`` -so that the Django app can connect to PostgreSQL. +Save and close the ``settings.py`` file. -Pack the rock: +Now let’s pack the rock: -.. code-block:: bash - - ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:pack] + :end-before: [docs:pack-end] + :dedent: 2 .. note:: - Depending on your network, this step can take a couple of minutes to finish. - - ``ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS`` is required whilst the Django - extension is experimental. - -Once Rockcraft has finished packing the Django rock, you'll find a new file in -your working directory with the ``.rock`` extension. View its contents: + In older versions of Rockcraft, you might need to set + ``ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true`` before the pack command. -.. code-block:: bash +Depending on your system and network, this step can take several minutes to +finish. - ls *.rock -l +Once Rockcraft has finished packing the Django rock, the +terminal will respond with something similar to +``Packed django-hello-world_0.1_amd64.rock``. -The rock needs to be copied to the MicroK8s registry so that it can be deployed -in the Kubernetes cluster: +.. note:: -.. code-block:: bash + If you are not on the ``amd64`` platform, the name of the ``.rock`` file + will be different for you. - rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ - oci-archive:django-hello-world_0.1_amd64.rock \ - docker://localhost:32000/django-hello-world:0.1 +The rock needs to be copied to the MicroK8s registry, which stores OCI +archives so they can be downloaded and deployed in a Kubernetes cluster. +Copy the rock: -.. note:: +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:skopeo-copy] + :end-before: [docs:skopeo-copy-end] + :dedent: 2 - If you changed the ``name`` or ``version`` in ``rockcraft.yaml`` or - are not on an ``amd64`` platform, the name of the ``.rock`` file - will be different for you. +.. seealso:: + `Ubuntu manpage | skopeo + `_ Create the charm ---------------- -Create a new directory for the charm and go inside it: +From the ``/django-hello-world`` directory, create a new directory for +the charm and change inside it: -.. code-block:: bash +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:create-charm-dir] + :end-before: [docs:create-charm-dir-end] + :dedent: 2 - mkdir charm - cd charm +Using the ``django-framework`` profile, Charmcraft will automate the +creation of the files needed for our charm, including a +``charmcraft.yaml``, ``requirements.txt`` and source code for the charm. +The source code contains the logic required to operate the Django +application. -We'll need a project file named ``charmcraft.yaml``, ``requirements.txt`` and source -code for the charm. The source code contains the logic required to operate the Django -application. Charmcraft will automate the creation of these files using the -``django-framework`` profile: +Initialize a charm named ``django-hello-world``: -.. code-block:: bash +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:charm-init] + :end-before: [docs:charm-init-end] + :dedent: 2 - charmcraft init --profile django-framework --name django-hello-world +The files will automatically be created in your working directory. -The files will automatically be created in your working directory. We will need to -connect to the PostgreSQL database. Open the project file and add the following section -to the end of the file: - -.. code-block:: yaml +We will need to connect the Django application to the PostgreSQL database. +Open the ``charmcraft.yaml`` file and add the following section to the end +of the file: - requires: - postgresql: - interface: postgresql_client - optional: false - limit: 1 +.. literalinclude:: code/django/postgres_requires_charmcraft.yaml + :language: yaml -The charm depends on several libraries. Download the libraries and pack -the charm: +Now let’s pack the charm: -.. code-block:: bash - - CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft fetch-libs - CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:charm-pack] + :end-before: [docs:charm-pack-end] + :dedent: 2 .. note:: - Depending on your network, this step can take a couple of minutes - to finish. - -Once Charmcraft has finished packing the charm, you'll find a new file in -your working directory with the charm extension. View its contents: + ``CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true`` may be required + in the pack command for older versions of Charmcraft. -.. code-block:: bash +Depending on your system and network, this step can take several +minutes to finish. - ls *.charm -l +Once Charmcraft has finished packing the charm, the terminal will +respond with something similar to +``Packed django-hello-world_ubuntu-22.04-amd64.charm``. .. note:: - If you changed the project name or are not on the ``amd64`` platform, the name of - the ``.charm`` file will be different for you. - + If you are not on the ``amd64`` platform, the name of the ``.charm`` + file will be different for you. Deploy the Django application ----------------------------- -A Juju model is needed to deploy the application. Create a new model: - -.. code-block:: bash - - juju add-model django-hello-world - -.. note:: - - If you are not on a host with the ``amd64`` architecture, you will - need to include a constraint to the Juju model to specify your - architecture. For example, using the ``arm64`` architecture, you - would use ``juju set-model-constraints -m django-hello-world arch=arm64``. - Check the architecture of your system using ``dpkg --print-architecture``. - -Now deploy the Django application using Juju: +A Juju model is needed to handle Kubernetes resources while deploying +the Django application. Let’s create a new model: -.. code-block:: bash +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:add-juju-model] + :end-before: [docs:add-juju-model-end] + :dedent: 2 - juju deploy ./django-hello-world_ubuntu-22.04-amd64.charm \ - django-hello-world \ - --resource django-app-image=localhost:32000/django-hello-world:0.1 +If you are not on a host with the ``amd64`` architecture, you will need +to include a constraint to the Juju model to specify your architecture. -Deploy PostgreSQL and integrate with the Django application: +Set the Juju model constraints with: -.. code-block:: bash +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:add-model-constraints] + :end-before: [docs:add-model-constraints-end] + :dedent: 2 - juju deploy postgresql-k8s --trust - juju integrate django-hello-world postgresql-k8s +Now let’s use the OCI image we previously uploaded to deploy the Django +application. Deploy using Juju by specifying the OCI image name with the +``--resource`` option: -.. note:: - - It will take a few minutes to deploy the Django application. You can monitor - the progress using ``juju status --watch 5s``. Once the status of the app - changes to ``active``, you can stop watching using :kbd:`Ctrl` + :kbd:`C`. +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:deploy-django-app] + :end-before: [docs:deploy-django-app-end] + :dedent: 2 -The Django application should now be running. You can see the status of the -deployment using ``juju status``, which should be similar to the following output: +Now let’s deploy PostgreSQL: -.. terminal:: - :input: juju status +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:deploy-postgres] + :end-before: [docs:deploy-postgres-end] + :dedent: 2 - django-hello-world dev-controller microk8s/localhost 3.5.3 unsupported 16:47:01+10:00 +Integrate PostgreSQL with the Django application: - App Version Status Scale Charm Channel Rev Address Exposed Message - django-hello-world active 1 django-hello-world 3 10.152.183.126 no - postgresql-k8s 14.11 active 1 postgresql-k8s 14/stable 281 10.152.183.197 no +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:integrate-postgres] + :end-before: [docs:integrate-postgres-end] + :dedent: 2 - Unit Workload Agent Address Ports Message - django-hello-world/0* active idle 10.1.157.80 - postgresql-k8s/0* active idle 10.1.157.78 Primary +It will take a few minutes to deploy the Django application. You can +monitor its progress with: -To be able to test the deployment, we need to include the IP address in -the allowed hosts configuration. We'll also enable debug mode for now while -we are testing. Both can be done using: +.. code:: bash -.. code-block:: bash + juju status --relations --watch 2s - juju config django-hello-world django-allowed-hosts=* django-debug=true +The ``--relations`` flag will list the currently enabled integrations. +It can take a couple of minutes for the apps to finish the deployment. +During this time, the Django app may enter a ``blocked`` state as it +waits to become integrated with the PostgreSQL database. -.. note:: +Once the status of the App has gone to ``active``, you can stop watching +using :kbd:`Ctrl` + :kbd:`C`. - Setting the Django allowed hosts to ``*`` and turning on debug mode should - not be done in production, where you should set the actual hostname of - the actual application and disable debug mode. We will do this in the tutorial - for now and later demonstrate how we can set these to production-ready values. +.. seealso:: -Test the deployment using ``curl`` to send a request to the root endpoint. The IP -address is the ``Address`` listed in the ``Unit`` section of the ``juju status`` -output (e.g., ``10.1.157.80`` in the sample output above): + See more: `Command 'juju status' `_ -.. code-block:: bash +The Django application should now be running. We can see the status of +the deployment using ``juju status`` which should be similar to the +following output: - curl 10.1.157.80:8000 +.. terminal:: + :input: juju status -The Django app should again respond with: + Model Controller Cloud/Region Version SLA Timestamp + django-hello-world dev-controller microk8s/localhost 3.6.2 unsupported 16:47:01+10:00 - The install worked successfully! Congratulations! + App Version Status Scale Charm Channel Rev Address Exposed Message + django-hello-world active 1 django-hello-world 3 10.152.183.126 no + postgresql-k8s 14.11 active 1 postgresql-k8s 14/stable 281 10.152.183.197 no + Unit Workload Agent Address Ports Message + django-hello-world/0* active idle 10.1.157.80 + postgresql-k8s/0* active idle 10.1.157.78 Primary -Add a root endpoint -------------------- +To be able to test the deployment, we need to enable debug mode for now. +Set the configuration: -The generated Django application does not come with a root endpoint, which is why -we had to initially enable debug mode for testing. Let's add a root endpoint that -returns a ``Hello, world!`` greeting. We will need to go back out to the root -directory for the tutorial and go into the ``django_hello_world`` directory using -``cd ../django_hello_world``. Add a new Django app using: +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:config-debug] + :end-before: [docs:config-debug-end] + :dedent: 2 -.. code-block:: bash +.. note:: - django-admin startapp greeting + Turning on debug mode should not be done in production. We will do this in + the tutorial for now and later disable debug mode. -Open the ``greetings/view.py`` file and replace the content with: +Let’s expose the application using ingress. Deploy the +``nginx-ingress-integrator`` charm and integrate it with the Django app: -.. code-block:: python +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:deploy-nginx] + :end-before: [docs:deploy-nginx-end] + :dedent: 2 - from django.http import HttpResponse +The hostname of the app needs to be defined so that it is accessible via +the ingress. We will also set the default route to be the root endpoint: - def index(request): - return HttpResponse("Hello, world!\n") +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:config-nginx] + :end-before: [docs:config-nginx-end] + :dedent: 2 -Create the ``greetings/urls.py`` file with the following contents: +Monitor ``juju status`` until everything has a status of ``active``. -.. code-block:: python +Now we will visit the Django app in a web browser. Outside of the +Multipass VM, open your machine’s ``/etc/hosts`` file in a text editor +and add a line like the following: - from django.urls import path +.. code:: bash - from . import views + django-hello-world - urlpatterns = [ - path("", views.index, name="index"), - ] +Here, replace ```` with the same Multipass VM +private IP address you previously used. -Open the ``django_hello_world/urls.py`` file and edit the value of -``urlpatterns`` to include ``path('', include("greetings.url")``, -for example: +Now you can open a new tab and visit http://django-hello-world. The +Django app should respond in the browser with +``The install worked successfully! Congratulations!``. -.. code-block:: python +Add an initial app +------------------ - from django.contrib import admin - from django.urls import include, path +The generated Django application does not come with an app, which is why +we had to initially enable debug mode for testing. Let’s add a greeting +app that returns a ``Hello, world!`` greeting. We will need to go back +out to the ``/django-hello-world`` directory where the rock is and enter +into the ``/django_hello_world`` directory where the Django application +is. Let’s add a new Django app: - urlpatterns = [ - path("", include("greeting.urls")), - path("admin/", admin.site.urls), - ] +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:startapp-greeting] + :end-before: [docs:startapp-greeting-end] + :dedent: 2 -Since we're changing the applications, we should update the version of it. -Go back to the root directory of the tutorial using ``cd ..`` and change the -``version`` in ``rockcraft.yaml`` to ``0.2``. Pack and upload the rock using -similar commands as before: +Open the ``greeting/views.py`` file and replace the content with: -.. code-block:: yaml +.. literalinclude:: code/django/views_greeting.py + :language: python - ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack - rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ - oci-archive:django-hello-world_0.2_amd64.rock \ - docker://localhost:32000/django-hello-world:0.2 +Create the ``greeting/urls.py`` file with the following contents: -Now we can deploy the new version of the Django application using: +.. literalinclude:: code/django/urls_greeting.py + :language: python -.. code-block:: bash +Open the ``django_hello_world/urls.py`` file and edit the imports for +``django.urls`` and the value of ``urlpatterns`` like in the following example: - cd charm - juju refresh django-hello-world \ - --path=./django-hello-world_ubuntu-22.04-amd64.charm \ - --resource django-app-image=localhost:32000/django-hello-world:0.2 +.. code-block:: python + :emphasize-lines: 2,5 -Now that we have a valid root endpoint, we can disable debug mode: + from django.contrib import admin + from django.urls import include, path -.. code-block:: bash + urlpatterns = [ + path("", include("greeting.urls")), + path("admin/", admin.site.urls), + ] - juju config django-hello-world django-debug=false +Since we’re changing the application we should update the version of the +rock. Go back to the ``/django-hello-world`` directory where the rock is +and change the ``version`` in ``rockcraft.yaml`` to ``0.2``. The top of +the ``rockcraft.yaml`` file should look similar to the following: -Use ``juju status --watch 5s`` again to wait until the app is active again. -The IP address will have changed so we need to retrieve it again using -``juju status``. Now we can call the root endpoint using ``curl 10.1.157.80:8000`` -and the Django application should respond with ``Hello, world!``. +.. code-block:: yaml + :emphasize-lines: 5 + + name: django-hello-world + # see https://documentation.ubuntu.com/rockcraft/en/1.6.0/explanation/bases/ + # for more information about bases and using 'bare' bases for chiselled rocks + base: ubuntu@22.04 # the base environment for this Django application + version: '0.2' # just for humans. Semantic versioning is recommended + summary: A summary of your Django application # 79 char long summary + description: | + This is django-hello-world's description. You have a paragraph or two to tell the + most important story about it. Keep it under 100 words though, + we live in tweetspace and your description wants to look good in the + container registries out there. + # the platforms this rock should be built on and run on. + # you can check your architecture with `dpkg --print-architecture` + platforms: + amd64: + # arm64: + # ppc64el: + # s390x: + + ... + +Now let’s pack and upload the rock using similar commands as before: + +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:repack-update] + :end-before: [docs:repack-update-end] + :dedent: 2 + +Now we can deploy the new version of the Django application from the +``/charm`` directory using: + +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:refresh-deployment] + :end-before: [docs:refresh-deployment-end] + :dedent: 2 + +Now that we have the greeting app, we can disable debug mode: + +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:disable-debug-mode] + :end-before: [docs:disable-debug-mode-end] + :dedent: 2 + +Use ``juju status --watch 2s`` again to wait until the App is active +again. You may visit http://django-hello-world from a web browser, or +you can use +``curl http://django-hello-world --resolve django-hello-world:80:127.0.0.1`` +inside the Multipass VM. Either way, the Django application should respond +with ``Hello, world!``. Enable a configuration ---------------------- -To demonstrate how to provide configuration to the Django application, we will -make the greeting configurable. Go back out to the tutorial root using ``cd ..``. -Open the ``django_hello_world/greeting/view.py`` file and replace the content +To demonstrate how to provide a configuration to the Django application, +we will make the greeting configurable. We will expect this +configuration option to be available in the Django app configuration under the +keyword ``DJANGO_GREETING``. Go back out to the rock +directory ``/django-hello-world`` using ``cd ..``. From there, open the +``django_hello_world/greeting/views.py`` file and replace the content with: -.. code-block:: python - - import os - - from django.http import HttpResponse - - def index(request): - return HttpResponse(f"{os.environ.get('DJANGO_GREETING', 'Hello, world!')}\n") +.. literalinclude:: code/django/views_greeting_configuration.py + :language: python -Increment the ``version`` in ``rockcraft.yaml`` to ``0.3`` and run the pack and upload -commands for the rock: +Increment the ``version`` in ``rockcraft.yaml`` to ``0.3`` such that the +top of the ``rockcraft.yaml`` file looks similar to the following: .. code-block:: yaml - - ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack - rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ - oci-archive:django-hello-world_0.3_amd64.rock \ - docker://localhost:32000/django-hello-world:0.3 - -Change back into the charm directory using ``cd charm``. The ``django-framework`` -Charmcraft extension supports adding configurations to the project file, which will be -passed as environment variables to the Django application. Add the following to the end -of the project file: - -.. code-block:: yaml - - config: - options: - greeting: - description: | - The greeting to be returned by the Django application. - default: "Hello, world!" - type: string + :emphasize-lines: 5 + + name: django-hello-world + # see https://documentation.ubuntu.com/rockcraft/en/1.6.0/explanation/bases/ + # for more information about bases and using 'bare' bases for chiselled rocks + base: ubuntu@22.04 # the base environment for this Django application + version: '0.3' # just for humans. Semantic versioning is recommended + summary: A summary of your Django application # 79 char long summary + description: | + This is django-hello-world's description. You have a paragraph or two to tell the + most important story about it. Keep it under 100 words though, + we live in tweetspace and your description wants to look good in the + container registries out there. + # the platforms this rock should be built on and run on. + # you can check your architecture with `dpkg --print-architecture` + platforms: + amd64: + # arm64: + # ppc64el: + # s390x: + + ... + +Let’s pack and upload the rock: + +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:repack-2nd-update] + :end-before: [docs:repack-2nd-update-end] + :dedent: 2 + +Change back into the charm directory using ``cd charm``. + +The ``django-framework`` Charmcraft extension supports adding +configurations in ``charmcraft.yaml`` which will be passed as +environment variables to the Django application. Add the following to +the end of the ``charmcraft.yaml`` file: + +.. literalinclude:: code/django/greeting_charmcraft.yaml + :language: yaml .. note:: - Configuration options are automatically capitalised and dashes are replaced - by underscores. A ``DJANGO_`` prefix will also be added to ensure that - environment variables are namespaced. + Configuration options are automatically capitalized and ``-`` are + replaced by ``_``. A ``DJANGO_`` prefix will also be added as a + namespace for app configurations. We can now pack and deploy the new version of the Django app: -.. code-block:: bash - - CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack - juju refresh django-hello-world \ - --path=./django-hello-world_ubuntu-22.04-amd64.charm \ - --resource django-app-image=localhost:32000/django-hello-world:0.3 - -After briefly monitoring ``juju status``, the application should go back -to ``active`` again. Sending a request to the root endpoint using -``curl 10.1.157.81:8000`` (after getting the IP address from ``juju status``) -should result in the Django application responding with ``Hello, world!`` -again. We can change the greeting using -``juju config django-hello-world greeting='Hi!'``. After we wait a moment -for the app to restart, ``curl 10.1.157.81:8000`` should now respond with ``Hi!``. - - -Expose the app using ingress ----------------------------- +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:repack-refresh-2nd-deployment] + :end-before: [docs:repack-refresh-2nd-deployment-end] + :dedent: 2 -.. note:: - - This step of the tutorial only works for hosts with the ``amd64`` architecture. - For other architectures, skip this step. - -As a final step, let's expose the application using ingress. Deploy the -``nginx-ingress-integrator`` charm and integrate it with the Django app: - -.. code-block:: bash - - juju deploy nginx-ingress-integrator - juju integrate nginx-ingress-integrator django-hello-world - -.. note:: - - RBAC is enabled in the ``charm-dev`` Multipass blueprint. Run - ``juju trust nginx-ingress-integrator --scope cluster`` if you're - using the ``charm-dev`` blueprint. - -The hostname of the app needs to be defined so that it is accessible via the -ingress. We will also set the default route to be the root endpoint: - -.. code-block:: bash - - juju config nginx-ingress-integrator \ - service-hostname=django-hello-world path-routes=/ - -Monitor ``jujus status`` until everything has a status of ``active``. Use +After we wait for a bit monitoring ``juju status`` the application +should go back to ``active`` again. Sending a request to the root +endpoint using ``curl http://django-hello-world --resolve django-hello-world:80:127.0.0.1`` -to send a request via the ingress. It should still be returning the ``Hi!`` -greeting. +or visiting http://django-hello-world in a web browser should result in the +Django application responding with ``Hello, world!`` again. -.. note:: +Now let’s change the greeting: - The ``-H "Host: django-hello-world"`` option to the ``curl`` command - is a way of setting the hostname of the request without setting a - DNS record. - -We can now also change the Django allowed hosts to ``django-hello-world`` -which is a production-ready value (for production, you will need to set up -a DNS record): - -.. code-block:: bash - - juju config django-hello-world django-allowed-hosts=django-hello-world - -Running ``curl 127.0.0.1 -H "Host: django-hello-world"`` should still get the -Django app to respond with ``Hi!``. +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:change-config] + :end-before: [docs:change-config-end] + :dedent: 2 +After we wait for a moment for the app to be restarted, using +``curl http://django-hello-world --resolve django-hello-world:80:127.0.0.1`` +or visiting http://django-hello-world should now respond with ``Hi!``. Tear things down ---------------- -You've reached the end of this tutorial. You have created a Django application, -deployed it locally, built an OCI image for it and deployed it using Juju. We -then integrated it with PostgreSQL to be production-ready, demonstrated how to -add a root endpoint and how to configure the application. Finally, we exposed -our application using an ingress. - -If you'd like to reset your working environment, you can run the following -in the root directory for this tutorial: - -.. code-block:: bash +We’ve reached the end of this tutorial. We went through the entire +development process, including: - cd .. - deactivate - rm -rf charm .venv django_hello_world +- Creating a Django application +- Deploying the application locally +- Packaging the application using Rockcraft +- Building the application with Ops code using Charmcraft +- Deplyoing the application using Juju +- Integrating the application with PostgreSQL to be production ready +- Exposing the application using an ingress +- Adding an initial app and configuring the application -Then, delete all the files created during the tutorial: +If you’d like to reset your working environment, you can run the +following in the rock directory ``/django-hello-world`` for the tutorial: -.. code-block:: bash +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:clean-environment] + :end-before: [docs:clean-environment-end] + :dedent: 2 - rm django-hello-world_0.1_amd64.rock \ - django-hello-world_0.2_amd64.rock \ - django-hello-world_0.3_amd64.rock \ - rockcraft.yaml requirements.txt +You can also clean up your Multipass instance. Start by exiting it: -And remove the Juju model: +.. code:: bash -.. code-block:: bash - - juju destroy-model django-hello-world --destroy-storage - -If you created an instance using Multipass, you can also clean it up. -Start by exiting it: - -.. code-block:: bash - - exit + exit And then you can proceed with its deletion: -.. code-block:: bash - - multipass delete charm-dev - multipass purge +.. code:: bash + multipass delete charm-dev + multipass purge Next steps ---------- -By the end of this tutorial, you will have built a charm and evolved it -in a number of practical ways, but there is a lot more to explore: - -+-------------------------+----------------------+ -| If you are wondering... | Visit... | -+=========================+======================+ -| "How do I...?" | :ref:`how-to-guides` | -+-------------------------+----------------------+ -| "What is...?" | :ref:`reference` | -+-------------------------+----------------------+ +By the end of this tutorial you will have built a charm and evolved it +in a number of typical ways. But there is a lot more to explore: + +.. list-table:: + :widths: 30 30 + :header-rows: 1 + + * - If you are wondering... + - Visit... + * - "How do I...?" + - :ref:`How-to guides `, + :external+ops:ref:`Ops | How-to guides ` + * - "How do I debug?" + - `Charm debugging tools `_ + * - "How do I get in touch?" + - `Matrix channel `_ + * - "What is...?" + - :ref:`reference`, + :external+ops:ref:`Ops | Reference `, + :external+juju:ref:`Juju | Reference ` + * - "Why...?", "So what?" + - :external+ops:ref:`Ops | Explanation `, + :external+juju:ref:`Juju | Explanation ` diff --git a/docs/tutorial/write-your-first-kubernetes-charm-for-a-fastapi-app.rst b/docs/tutorial/write-your-first-kubernetes-charm-for-a-fastapi-app.rst index ee5cb8e42..06608a463 100644 --- a/docs/tutorial/write-your-first-kubernetes-charm-for-a-fastapi-app.rst +++ b/docs/tutorial/write-your-first-kubernetes-charm-for-a-fastapi-app.rst @@ -4,43 +4,44 @@ Write your first Kubernetes charm for a FastAPI app =================================================== +Imagine you have a FastAPI application backed up by a database +such as PostgreSQL and need to deploy it. In a traditional setup, +this can be quite a challenge, but with Charmcraft you'll find +yourself packaging and deploying your FastAPI application in no time. + +In this tutorial we will build a Kubernetes charm for a FastAPI +application using Charmcraft, so we can have a FastAPI application +up and running with Juju. Let's get started! + +This tutorial should take 90 minutes for you to complete. + +.. note:: + If you're new to the charming world: FastAPI applications are + specifically supported with a template to quickly generate a + **rock** (i.e., a special kind of OCI-compliant container image) + and a matching template to quickly generate a **charm** (i.e., + a software operator for cloud operations done with the Juju + orchestration engine). The result is FastAPI applications that + can be easily deployed, configured, scaled, integrated, etc., + on any Kubernetes cluster. What you'll need ---------------- -- A working station, e.g., a laptop, with amd64 architecture which has - sufficient resources to launch a virtual machine with 4 CPUs, 4GB RAM, - and a 50GB disk. - - * Note that a workstation with arm64 architecture can complete the - majority of this tutorial. +- A local system, e.g., a laptop, with amd64 or arm64 architecture which + has sufficient resources to launch a virtual machine with 4 CPUs, + 4 GB RAM, and a 50 GB disk. - Familiarity with Linux. -- About 90 minutes of free time. What you'll do -------------- -Create a FastAPI application. Use that to create a rock with ``rockcraft``. Use -that to create a charm with ``charmcraft``. Use that to test-deploy, configure, etc., -your Django application on a local Kubernetes cloud, ``microk8s``, with ``juju``. -All of that multiple times, mimicking a real development process. - -.. note:: - - **rock** - - An Ubuntu LTS-based OCI compatible container image designed to meet security, - stability, and reliability requirements for cloud-native software. - - **charm** - - A package consisting of YAML files + Python code that will automate every - aspect of an application's lifecycle so it can be easily orchestrated with Juju. - - **Juju** - - An orchestration engine for charmed applications. +Create a FastAPI application. Use that to create a rock with +``rockcraft``. Use that to create a charm with ``charmcraft``. Use that +to test, deploy, configure, etc., your FastAPI application on a local +Kubernetes cloud, ``microk8s``, with ``juju``. All of that multiple +times, mimicking a real development process. .. important:: @@ -52,287 +53,326 @@ All of that multiple times, mimicking a real development process. Set things up ------------- -Install Multipass. - - See more: `Multipass | How to install Multipass - `_ - -Use Multipass to launch an Ubuntu VM with the name charm-dev from the 24.04 blueprint: +.. include:: /reuse/tutorial/setup_edge.rst +.. |12FactorApp| replace:: FastAPI -.. code-block:: bash - - multipass launch --cpus 4 --disk 50G --memory 4G --name charm-dev 24.04 - -Once the VM is up, open a shell into it: +Let's create a directory for this tutorial and enter into it: .. code-block:: bash - multipass shell charm-dev - -In order to create the rock, you'll need to install Rockcraft: + mkdir fastapi-hello-world + cd fastapi-hello-world -.. code-block:: bash +Finally, install ``python-venv`` and create a virtual environment: - sudo snap install rockcraft --channel latest/edge --classic +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:create-venv] + :end-before: [docs:create-venv-end] + :dedent: 2 -``LXD`` will be required for building the rock. Make sure it is installed -and initialised: +Create the FastAPI application +------------------------------ -.. code-block:: bash +Start by creating the "Hello, world" FastAPI application that will be used for +this tutorial. - sudo snap install lxd - lxd init --auto +Create a ``requirements.txt`` file using ``touch requirements.txt``. +Then, open the file in a text editor using ``nano requirements.txt``, +copy the following text into it and then save the file: -In order to create the charm, you'll need to install Charmcraft: +.. literalinclude:: code/fastapi/requirements.txt + :caption: requirements.txt -.. code-block:: bash +.. note:: - sudo snap install charmcraft --channel latest/edge --classic + The ``psycopg2-binary`` package is needed so the FastAPI application can + connect to PostgreSQL. -MicroK8s is required to deploy the FastAPI application on Kubernetes. -Install MicroK8s: +Install the packages: -.. code-block:: bash +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:install-requirements] + :end-before: [docs:install-requirements-end] + :dedent: 2 - sudo snap install microk8s --channel 1.31-strict/stable - sudo adduser $USER snap_microk8s - newgrp snap_microk8s +In the same directory, create a file called ``app.py``. +Then copy and save the following code into the file: -Wait for MicroK8s to be ready using ``sudo microk8s status --wait-ready``. -Several MicroK8s add-ons are required for deployment: +.. literalinclude:: code/fastapi/app.py + :language: python -.. code-block:: bash - sudo microk8s enable hostpath-storage - # Required to host the OCI image of the FastAPI application - sudo microk8s enable registry - # Required to expose the FastAPI application - sudo microk8s enable ingress +Run the FastAPI application locally +----------------------------------- -Juju is required to deploy the FastAPI application. Install Juju and bootstrap -a development controller: +Now that we have a virtual environment with all the dependencies, +let's run the FastAPI application to verify that it works: .. code-block:: bash - sudo snap install juju --channel 3.5/stable - mkdir -p ~/.local/share - juju bootstrap microk8s dev-controller - -Finally, create a new directory for this tutorial and go inside it: - -.. code-block:: bash + fastapi dev app.py --port 8080 - mkdir fastapi-hello-world - cd fastapi-hello-world +Test the FastAPI application by using ``curl`` to send a request to the root +endpoint. You will need a new terminal for this; use +``multipass shell charm-dev`` to open a new terminal in Multipass: -.. note:: +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:curl-fastapi] + :end-before: [docs:curl-fastapi-end] + :dedent: 2 - This tutorial requires version ``3.0.0`` or later of Charmcraft. Check which - version of Charmcraft you have installed using ``charmcraft --version``. If - you have an older version of Charmcraft installed, use - ``sudo snap refresh charmcraft --channel latest/edge`` to get the latest edge - version of Charmcraft. +The FastAPI application should respond with ``{"message":"Hello World"}``. - This tutorial requires version ``1.5.4`` or later of Rockcraft. Check which - version of Rockcraft you have installed using ``rockcraft --version``. If you - have an older version of Rockcraft installed, use - ``sudo snap refresh rockcraft --channel latest/edge`` to get the latest edge - version of Rockcraft. +The FastAPI application looks good, so we can stop for now from the +original terminal using :kbd:`Ctrl` + :kbd:`C`. -Create the FastAPI application ------------------------------- +Pack the FastAPI application into a rock +---------------------------------------- -Start by creating the "Hello, world" FastAPI application that will be used for -this tutorial. +First, we'll need a ``rockcraft.yaml`` file. Using the +``fastapi-framework`` profile, Rockcraft will automate the creation of +``rockcraft.yaml`` and tailor the file for a FastAPI application. +From the ``/fastapi-hello-world`` directory, initialize the rock: -Create a ``requirements.txt`` file, copy the following text into it and then save it: +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:create-rockcraft-yaml] + :end-before: [docs:create-rockcraft-yaml-end] + :dedent: 2 -.. code-block:: bash +The ``rockcraft.yaml`` file will be automatically created, with the name being +set based on your working directory. - fastapi[standard] +Check out the contents of ``rockcraft.yaml``: -In the same directory, copy and save the following into a text file called ``app.py``: +.. code:: bash -.. code-block:: python + cat rockcraft.yaml - from fastapi import FastAPI +The top of the file should look similar to the following snippet: - app = FastAPI() +.. code:: yaml + :caption: rockcraft.yaml - @app.get("/") - async def root(): - return {"message": "Hello World"} + name: fastapi-hello-world + # see https://documentation.ubuntu.com/rockcraft/en/latest/explanation/bases/ + # for more information about bases and using 'bare' bases for chiselled rocks + base: ubuntu@24.04 # the base environment for this FastAPI application + version: '0.1' # just for humans. Semantic versioning is recommended + summary: A summary of your FastAPI application # 79 char long summary + description: | + This is fastapi project's description. You have a paragraph or two to tell the + most important story about it. Keep it under 100 words though, + we live in tweetspace and your description wants to look good in the + container registries out there. + # the platforms this rock should be built on and run on. + # you can check your architecture with `dpkg --print-architecture` + platforms: + amd64: + # arm64: + # ppc64el: + # s390x: + ... -Run the FastAPI application locally ------------------------------------ +Verify that the ``name`` is ``fastapi-hello-world``. -Install ``python3-venv`` and create a virtual environment: +Ensure that ``platforms`` includes the architecture of your host. Check +the architecture of your system: .. code-block:: bash - sudo apt-get update && sudo apt-get install python3-venv -y - python3 -m venv .venv - source .venv/bin/activate - pip install -r requirements.txt + dpkg --print-architecture -Now that we have a virtual environment with all the dependencies, -let's run the FastAPI application to verify that it works: +If your host uses the ARM architecture, open ``rockcraft.yaml`` in a +text editor and include ``arm64`` in ``platforms``. -.. code-block:: bash +Now let's pack the rock: - fastapi dev app.py --port 8080 +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:pack] + :end-before: [docs:pack-end] + :dedent: 2 -Test the FastAPI application by using ``curl`` to send a request to the root -endpoint. You may need a new terminal for this; if you are using Multipass, use -``multipass shell charm-dev`` to get another terminal: +.. note:: -.. code-block:: bash + ``ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS`` is required while the FastAPI + extension is experimental. - curl localhost:8080 +Depending on your system and network, this step can take several +minutes to finish. -The FastAPI application should respond with ``{"message":"Hello World"}``. The -FastAPI application looks good, so we can stop for now using :kbd:`Ctrl` + -:kbd:`C`. +Once Rockcraft has finished packing the FastAPI rock, +the terminal will respond with something similar to +``Packed fastapi-hello-world_0.1_amd64.rock``. +.. note:: -Pack the FastAPI application into a rock ----------------------------------------- + If you are not on the ``amd64`` platform, the name of the ``.rock`` file + will be different for you. -First, we'll need a ``rockcraft.yaml`` file. Rockcraft will automate its creation -and tailoring for a FastAPI application by using the ``fastapi-framework`` profile: +The rock needs to be copied to the MicroK8s registry, which stores OCI +archives so they can be downloaded and deployed in the Kubernetes cluster. +Copy the rock: -.. code-block:: bash +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:skopeo-copy] + :end-before: [docs:skopeo-copy-end] + :dedent: 2 - rockcraft init --profile fastapi-framework +.. seealso:: -The ``rockcraft.yaml`` file will be automatically created, with its name being -set based on your working directory. Open the file in a text editor and ensure -that the ``name`` is ``fastapi-hello-world`` and that ``platforms`` includes -the architecture of your host. For example, if your host uses the ARM -architecture, include ``arm64`` in ``platforms``. + `Ubuntu manpage | skopeo + `_ -.. note:: - For this tutorial, we'll use the name ``fastapi-hello-world`` and assume that - you are on the ``amd64`` platform. Check the architecture of your system using - ``dpkg --print-architecture``. Choosing a different name or running on a - different platform will influence the names of the files generated by Rockcraft. +Create the charm +---------------- -Pack the rock: +From the ``/fastapi-hello-world`` direcotyr, let's create a new directory +for the charm and change inside it: -.. code-block:: bash +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:create-charm-dir] + :end-before: [docs:create-charm-dir-end] + :dedent: 2 - ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack +Using the ``fastapi-framework`` profile, Charmcraft will automate the +creation of the files needed for our charm, including a +``charmcraft.yaml``, ``requirements.txt`` and source code for the charm. +The source code contains the logic required to operate the FastAPI +application. -.. note:: +Initialize a charm named ``fastapi-hello-world``: - Depending on your system and network, this step can take a couple of minutes - to finish. +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:charm-init] + :end-before: [docs:charm-init-end] + :dedent: 2 - ``ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS`` is required while the FastAPI - extension is experimental. +The files will automatically be created in your working directory. -Once Rockcraft has finished packing the FastAPI rock, you'll find a new file -in your working directory with the ``.rock`` extension. View its contents: +Check out the contents of ``charmcraft.yaml``: .. code-block:: bash - ls *.rock -l + cat charmcraft.yaml -.. note:: +The top of the file should look similar to the following snippet: - If you changed the ``name`` or ``version`` in ``rockcraft.yaml`` or are not - on the ``amd64`` platform, the name of the ``.rock`` file will be different - for you. +.. code:: yaml -The rock needs to be copied to the MicroK8s registry so that it can be deployed -in the Kubernetes cluster: + # This file configures Charmcraft. + # See https://juju.is/docs/sdk/charmcraft-config for guidance. -.. code-block:: bash + name: fastapi-hello-world - rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ - oci-archive:fastapi-hello-world_0.1_amd64.rock \ - docker://localhost:32000/fastapi-hello-world:0.1 + type: charm + base: ubuntu@24.04 -Create the charm ----------------- + # the platforms this charm should be built on and run on. + # you can check your architecture with `dpkg --print-architecture` + platforms: + amd64: + # arm64: + # ppc64el: + # s390x: -Create a new directory for the charm and go inside it: + # (Required) + summary: A very short one-line summary of the FastAPI application. -.. code-block:: bash + ... - mkdir charm - cd charm +Verify that the ``name`` is ``fastapi-hello-world``. Ensure that ``platforms`` +includes the architecture of your host. If your host uses the ARM architecture, +open ``charmcraft.yaml`` in a text editor and include ``arm64`` +in ``platforms``. -We'll need a project file named ``charmcraft.yaml``, ``requirements.txt`` and source -code for the charm. The source code contains the logic required to operate the FastAPI -application. Charmcraft will automate the creation of these files by using the -``fastapi-framework`` profile: +Let's pack the charm -.. code-block:: bash - - charmcraft init --profile fastapi-framework --name fastapi-hello-world - -The charm depends on several libraries. Download the libraries and pack the charm: - -.. code-block:: bash - - CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft fetch-libs - CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:charm-pack] + :end-before: [docs:charm-pack-end] + :dedent: 2 .. note:: - Depending on your system and network, this step may take a couple of minutes - to finish. - ``CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS`` is required while the FastAPI extension is experimental. -Once Charmcraft has finished packing the charm, you'll find a new file in your -working directory with the ``.charm`` extension. View its contents: - -.. code-block:: bash +Depending on your system and network, this step may take several +minutes to finish. - ls *.charm -l +Once Charmcraft has finished packing the charm, the terminal will +respond with something similar to +``Packed fastapi-hello-world_ubuntu-24.04-amd64.charm``. .. note:: - If you changed the project name or are not on the ``amd64`` platform, the name of - the ``.charm`` file will be different for you. + If you are not on the ``amd64`` platform, the name of the ``.charm`` + file will be different for you. Deploy the FastAPI application ------------------------------ -A Juju model is needed to deploy the application. Let's create a new model: +A Juju model is needed to handle Kubernetes resources while deploying +the FastAPI application. Let's create a new model: -.. code-block:: bash +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:add-juju-model] + :end-before: [docs:add-juju-model-end] + :dedent: 2 - juju add-model fastapi-hello-world +If you are not on a host with the ``amd64`` architecture, you will +need to include a constraint to the Juju model to specify your +architecture. -.. note:: +Set the Juju model constraints with: - If you are not on a host with the ``amd64`` architecture, you will - need to include a constraint to the Juju model to specify your - architecture. For example, using the ``arm64`` architecture, you - would use ``juju set-model-constraints -m django-hello-world arch=arm64``. - Check the architecture of your system using ``dpkg --print-architecture``. +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:add-model-constraints] + :end-before: [docs:add-model-constraints-end] + :dedent: 2 -Now the FastAPI application can be deployed using Juju: -.. code-block:: bash +Now let’s use the OCI image we previously uploaded to deploy the FastAPI +application. Deploy using Juju by specifying the OCI image name with the +``--resource`` option: - juju deploy ./fastapi-hello-world_amd64.charm fastapi-hello-world \ - --resource app-image=localhost:32000/fastapi-hello-world:0.1 +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:deploy-fastapi-app] + :end-before: [docs:deploy-fastapi-app-end] + :dedent: 2 + +It will take a few minutes to deploy the FastAPI application. You can monitor +its progress with: + +.. code:: bash + + juju status --watch 2s -.. note:: - It will take a few minutes to deploy the FastAPI application. You can monitor - the progress using ``juju status --watch 5s``. Once the status of the app - changes to ``active``, you can stop watching using :kbd:`Ctrl` + :kbd:`C`. +It can take a couple of minutes for the app to finish the deployment. +Once the status of the App has gone to ``active``, you can stop watching +using :kbd:`Ctrl` + :kbd:`C`. + +.. seealso:: + + See more: :external+juju:ref:`Juju | juju status ` The FastAPI application should now be running. We can monitor the status of the deployment using ``juju status``, which should be similar to the following @@ -342,7 +382,7 @@ output: :input: juju status Model Controller Cloud/Region Version SLA Timestamp - fastapi-hello-world dev-controller microk8s/localhost 3.5.4 unsupported 13:45:18+10:00 + fastapi-hello-world dev-controller microk8s/localhost 3.6.2 unsupported 13:45:18+10:00 App Version Status Scale Charm Channel Rev Address Exposed Message fastapi-hello-world active 1 fastapi-hello-world 0 10.152.183.53 no @@ -350,27 +390,30 @@ output: Unit Workload Agent Address Ports Message fastapi-hello-world/0* active idle 10.1.157.75 -The deployment is finished when the status shows ``active``. Let's expose the -application using ingress. Deploy the ``nginx-ingress-integrator`` charm and -integrate it with the FastAPI app: +Let's expose the application using ingress. Deploy the +``nginx-ingress-integrator`` charm and integrate it with the FastAPI app: -.. code-block:: bash - - juju deploy nginx-ingress-integrator - juju integrate nginx-ingress-integrator fastapi-hello-world +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:deploy-nginx] + :end-before: [docs:deploy-nginx-end] + :dedent: 2 The hostname of the app needs to be defined so that it is accessible via -the ingress. We will also set the default route to be the endpoint: +the ingress. We will also set the default route to be the root endpoint: -.. code-block:: bash +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:config-nginx] + :end-before: [docs:config-nginx-end] + :dedent: 2 - juju config nginx-ingress-integrator \ - service-hostname=fastapi-hello-world path-routes=/ +Monitor ``juju status`` until everything has a status of ``active``. -Monitor ``juju status`` until everything has a status of ``active``. Use -``curl http://fastapi-hello-world --resolve fast-api-hello-world:80:127.0.0.1`` -to send a request via the ingress. It should return the ``{"message":"Hello World"}`` -greeting. +Test the deployment using +``curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1`` +to send a request via the ingress. It should return the +``{"message":"Hello World"}`` greeting. .. note:: @@ -382,94 +425,101 @@ greeting. Configure the FastAPI application --------------------------------- -Let's customise the greeting using a configuration option. We will expect this -configuration option to be available in the environment variable ``APP_GREETING``. -Go back out to the root directory of the project using ``cd ..`` and copy the -following code into ``app.py``: +To demonstrate how to provide a configuration to the FastAPI application, +we will make the greeting configurable. We will expect this +configuration option to be available in the FastAPI app configuration under the +keyword ``APP_GREETING``. Change back to the ``/fastapi-hello-world`` directory +using ``cd ..`` and copy the following code into ``app.py``: -.. code-block:: python +.. literalinclude:: code/fastapi/greeting_app.py + :language: python - import os - - from fastapi import FastAPI - - app = FastAPI() - - @app.get("/") - async def root(): - return {"message": os.getenv("APP_GREETING", "Hello World")} - -Open ``rockcraft.yaml`` and update the version to ``0.2``. Run -``ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack`` again, -then upload the new OCI image to the MicroK8s registry: - -.. code-block:: bash - - rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ - oci-archive:fastapi-hello-world_0.2_amd64.rock \ - docker://localhost:32000/fastapi-hello-world:0.2 - -Change back into the charm directory using ``cd charm``. The ``fastapi-framework`` -Charmcraft extension supports adding configurations to the project file which will be -passed as environment variables to the FastAPI application. Add the following to the end -of the project file: +Increment the ``version`` in ``rockcraft.yaml`` to ``0.2`` such that the +top of the ``rockcraft.yaml`` file looks similar to the following: .. code-block:: yaml - - config: - options: - greeting: - description: | - The greeting to be returned by the FastAPI application. - default: "Hello, world!" - type: string + :emphasize-lines: 5 + + name: fastapi-hello-world + # see https://documentation.ubuntu.com/rockcraft/en/latest/explanation/bases/ + # for more information about bases and using 'bare' bases for chiselled rocks + base: ubuntu@24.04 # the base environment for this FastAPI application + version: '0.2' # just for humans. Semantic versioning is recommended + summary: A summary of your FastAPI application # 79 char long summary + description: | + This is fastapi project's description. You have a paragraph or two to tell the + most important story about it. Keep it under 100 words though, + we live in tweetspace and your description wants to look good in the + container registries out there. + # the platforms this rock should be built on and run on. + # you can check your architecture with `dpkg --print-architecture` + platforms: + amd64: + # arm64: + # ppc64el: + # s390x: + + ... + +Let’s pack and upload the rock: + +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:docker-update] + :end-before: [docs:docker-update-end] + :dedent: 2 + +Change back into the charm directory using ``cd charm``. + +The ``fastapi-framework`` Charmcraft extension supports adding +configurations to ``charmcraft.yaml`` which will be passed as +environment variables to the FastAPI application. Add the +following to the end of the ``charmcraft.yaml`` file: + +.. literalinclude:: code/fastapi/greeting_charmcraft.yaml + :language: yaml .. note:: - Configuration options are automatically capitalised and dashes are replaced by - underscores. An ``APP_`` prefix will also be added to ensure that environment - variables are namespaced. + Configuration options are automatically capitalized and ``-`` are replaced + by ``_``. An ``APP_`` prefix will also be added as a namespace + for app configurations. -Run ``CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack`` again. The -deployment can now be refreshed to make use of the new code: +We can now pack and deploy the new version of the FastAPI app: -.. code-block:: bash +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:refresh-deployment] + :end-before: [docs:refresh-deployment-end] + :dedent: 2 - juju refresh fastapi-hello-world \ - --path=./fastapi-hello-world_amd64.charm \ - --resource app-image=localhost:32000/fastapi-hello-world:0.2 +After we wait for a bit monitoring ``juju status`` the application +should go back to ``active`` again. Verify that the +new configuration has been added using +``juju config fastapi-hello-world | grep -A 6 greeting:`` which should show +the configuration option. -Wait for ``juju status`` to show that the App is ``active`` again. Verify that the -new configuration has been added using ``juju config fastapi-hello-world | grep --A 6 greeting:`` which should show the configuration option. +Using ``curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1`` +shows that the response is still ``{"message":"Hello, world!"}`` as expected. -.. note:: - - The ``grep`` command extracts a portion of the configuration to make it easier to - check whether the configuration option has been added. - -Running ``http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1`` -shows that the response is still ``{"message":"Hello, world!"}`` as expected. The -greeting can be changed using Juju: - -.. code-block:: bash - - juju config fastapi-hello-world greeting='Hi!' - -``curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1`` now -returns the updated ``{"message":"Hi!"}`` greeting. - -.. note:: +Now let's change the greeting: - It may take a short time for the configuration to take effect. +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:change-config] + :end-before: [docs:change-config-end] + :dedent: 2 +After we wait for a moment for the app to be restarted, using +``curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1`` +should now return the updated ``{"message":"Hi!"}`` greeting. Integrate with a database ------------------------- -Now let's keep track of how many visitors your application has received. This will -require integration with a database to keep the visitor count. This will require -a few changes: +Now let's keep track of how many visitors your application has received. +This will require integration with a database to keep the visitor count. +This will require a few changes: - We will need to create a database migration that creates the ``visitors`` table. - We will need to keep track of how many times the root endpoint has been called @@ -477,35 +527,18 @@ a few changes: - We will need to add a new endpoint to retrieve the number of visitors from the database. +Let's start with the database migration to create the required tables. The charm created by the ``fastapi-framework`` extension will execute the ``migrate.py`` script if it exists. This script should ensure that the -database is initialised and ready to be used by the application. We will create -a ``migrate.py`` file containing this logic. +database is initialized and ready to be used by the application. We will +create a ``migrate.py`` file containing this logic. -Go back out to the tutorial root directory using ``cd ..``. Create the -``migrate.py`` file using a text editor and paste the following code into it: +Go back out to the ``/fastapi-hello-world`` directory using ``cd ..``, +create the ``migrate.py`` file, open the file using a text editor +and paste the following code into it: -.. code-block:: python - - import os - - import psycopg2 - - DATABASE_URI = os.environ["POSTGRESQL_DB_CONNECT_STRING"] - - def migrate(): - with psycopg2.connect(DATABASE_URI) as conn, conn.cursor() as cur: - cur.execute(""" - CREATE TABLE IF NOT EXISTS visitors ( - timestamp TIMESTAMP NOT NULL, - user_agent TEXT NOT NULL - ); - """) - conn.commit() - - - if __name__ == "__main__": - migrate() +.. literalinclude:: code/fastapi/visitors_migrate.py + :language: python .. note:: @@ -513,94 +546,86 @@ Go back out to the tutorial root directory using ``cd ..``. Create the ``POSTGRESQL_DB_CONNECT_STRING`` environment variable once postgres has been integrated with the charm. -Open the ``rockcraft.yaml`` file in a text editor and update the version -to ``0.3``. - -To be able to connect to postgresql from the FastAPI app, the ``psycopg2-binary`` -dependency needs to be added in ``requirements.txt``. The app code also needs to -be updated to keep track of the number of visitors and to include a new endpoint -to retrieve the number of visitors. Open ``app.py`` in a text editor and replace -its contents with the following code: - -.. code-block:: python - - import datetime - import os - from typing import Annotated - - from fastapi import FastAPI, Header - import psycopg2 - - app = FastAPI() - DATABASE_URI = os.environ["POSTGRESQL_DB_CONNECT_STRING"] - - - @app.get("/") - async def root(user_agent: Annotated[str | None, Header()] = None): - with psycopg2.connect(DATABASE_URI) as conn, conn.cursor() as cur: - timestamp = datetime.datetime.now() - - cur.execute( - "INSERT INTO visitors (timestamp, user_agent) VALUES (%s, %s)", - (timestamp, user_agent) - ) - conn.commit() - - return {"message": os.getenv("APP_GREETING", "Hello World")} - - - @app.get("/visitors") - async def visitors(): - with psycopg2.connect(DATABASE_URI) as conn, conn.cursor() as cur: - cur.execute("SELECT COUNT(*) FROM visitors") - total_visitors = cur.fetchone()[0] - - return {"count": total_visitors} - -Run ``ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack`` and upload -the newly created rock to the MicroK8s registry: - -.. code-block:: bash - - rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ - oci-archive:fastapi-hello-world_0.3_amd64.rock \ - docker://localhost:32000/fastapi-hello-world:0.3 - -The FastAPI app now requires a database which needs to be declared in the project file. -Go back into the charm directory using ``cd charm``. Open the project file in a text -editor and add the following section at the end of the file: +Increment the ``version`` in ``rockcraft.yaml`` to ``0.3`` such that the +top of the ``rockcraft.yaml`` file looks similar to the following: .. code-block:: yaml - - requires: - postgresql: - interface: postgresql_client - optional: false - -Pack the charm using ``CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack`` -and refresh the deployment using Juju: - -.. code-block:: bash - - juju refresh fastapi-hello-world \ - --path=./fastapi-hello-world_amd64.charm \ - --resource app-image=localhost:32000/fastapi-hello-world:0.3 - -Deploy ``postgresql-k8s`` using Juju and integrate it with ``fastapi-hello-world``: - -.. code-block:: bash - - juju deploy postgresql-k8s --trust - juju integrate fastapi-hello-world postgresql-k8s - -Wait for ``juju status`` to show that the App is ``active`` again. Executing -``curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1`` should -still return the ``{"message":"Hi!"}`` greeting. - -To check the local visitors, use ``curl http://fastapi-hello-world/visitors --resolve -fastapi-hello-world:80:127.0.0.1``, which should return ``{"count":1}`` after the -previous request to the root endpoint. This should be incremented each time the root -endpoint is requested. If we repeat this process, the output should be as follows: + :emphasize-lines: 5 + + name: fastapi-hello-world + # see https://documentation.ubuntu.com/rockcraft/en/latest/explanation/bases/ + # for more information about bases and using 'bare' bases for chiselled rocks + base: ubuntu@24.04 # the base environment for this FastAPI application + version: '0.3' # just for humans. Semantic versioning is recommended + summary: A summary of your FastAPI application # 79 char long summary + description: | + This is fastapi project's description. You have a paragraph or two to tell the + most important story about it. Keep it under 100 words though, + we live in tweetspace and your description wants to look good in the + container registries out there. + # the platforms this rock should be built on and run on. + # you can check your architecture with `dpkg --print-architecture` + platforms: + amd64: + # arm64: + # ppc64el: + # s390x: + + ... + +The app code also needs to be updated to keep track of the number of visitors +and to include a new endpoint to retrieve the number of visitors to the +app. Open ``app.py`` in a text editor and replace its contents with the +following code: + +.. collapse:: visitors_app.py + + .. literalinclude:: code/fastapi/visitors_app.py + :language: python + +Let’s pack and upload the rock: + +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:docker-2nd-update] + :end-before: [docs:docker-2nd-update-end] + :dedent: 2 + +Change back into the charm directory using ``cd charm``. + +The FastAPI app now requires a database which needs to be declared in the +``charmcraft.yaml`` file. Open ``charmcraft.yaml`` in a text editor and +add the following section to the end: + +.. literalinclude:: code/fastapi/visitors_charmcraft.yaml + :language: yaml + +We can now pack and deploy the new version of the FastAPI app: + +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:refresh-2nd-deployment] + :end-before: [docs:refresh-2nd-deployment-end] + :dedent: 2 + +Now let’s deploy PostgreSQL and integrate it with the FastAPI application: + +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:deploy-postgres] + :end-before: [docs:deploy-postgres-end] + :dedent: 2 + +Wait for ``juju status`` to show that the App is ``active`` again. Running +``curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1`` +should still return the ``{"message":"Hi!"}`` greeting. + +To check the local visitors, use +``curl http://fastapi-hello-world/visitors +--resolve fastapi-hello-world:80:127.0.0.1``, which should return +``{"count":1}`` after the previous request to the root endpoint. This should +be incremented each time the root endpoint is requested. If we repeat +this process, the output should be as follows: .. terminal:: :input: curl http://fastapi-hello-world --resolve fastapi-hello-world:80:127.0.0.1 @@ -613,26 +638,28 @@ endpoint is requested. If we repeat this process, the output should be as follow Tear things down ---------------- -We've reached the end of this tutorial. We have created a FastAPI application, -deployed it locally, integrated it with a database and exposed it via ingress! +We’ve reached the end of this tutorial. We went through the entire +development process, including: -If you'd like to reset your working environment, you can run the following -in the root directory for the tutorial: +- Creating a FastAPI application +- Deploying the application locally +- Packaging the application using Rockcraft +- Building the application with Ops code using Charmcraft +- Deplyoing the application using Juju +- Exposing the application using an ingress +- Configuring the application +- Integrating the application with a database -.. code-block:: bash +If you'd like to reset your working environment, you can run the following +in the rock directory ``/fastapi-hello-world`` for the tutorial: - # exit and delete the virtual environment - deactivate - rm -rf charm .venv __pycache__ - # delete all the files created during the tutorial - rm fastapi-hello-world_0.1_amd64.rock fastapi-hello-world_0.2_amd64.rock \ - fastapi-hello-world_0.3_amd64.rock rockcraft.yaml app.py \ - requirements.txt migrate.py - # Remove the juju model - juju destroy-model fastapi-hello-world --destroy-storage +.. literalinclude:: code/fastapi/task.yaml + :language: bash + :start-after: [docs:clean-environment] + :end-before: [docs:clean-environment-end] + :dedent: 2 -If you created an instance using Multipass, you can also clean it up. -Start by exiting it: +You can also clean up your Multipass instance. Start by exiting it: .. code-block:: bash @@ -650,12 +677,26 @@ Next steps ---------- By the end of this tutorial, you will have built a charm and evolved it -in a number of practical ways, but there is a lot more to explore: - -+-------------------------+----------------------+ -| If you are wondering... | Visit... | -+=========================+======================+ -| "How do I...?" | :ref:`how-to-guides` | -+-------------------------+----------------------+ -| "What is...?" | :ref:`reference` | -+-------------------------+----------------------+ +in a number of typical ways, but there is a lot more to explore: + +.. list-table:: + :widths: 30 30 + :header-rows: 1 + + * - If you are wondering... + - Visit... + * - "How do I...?" + - :ref:`How-to guides `, + :external+ops:ref:`Ops | How-to guides ` + * - "How do I debug?" + - `Charm debugging tools `_ + * - "How do I get in touch?" + - `Matrix channel `_ + * - "What is...?" + - :ref:`reference`, + :external+ops:ref:`Ops | Reference `, + :external+juju:ref:`Juju | Reference ` + * - "Why...?", "So what?" + - :external+ops:ref:`Ops | Explanation `, + :external+juju:ref:`Juju | Explanation ` + diff --git a/docs/tutorial/write-your-first-kubernetes-charm-for-a-flask-app.rst b/docs/tutorial/write-your-first-kubernetes-charm-for-a-flask-app.rst new file mode 100644 index 000000000..b0319097e --- /dev/null +++ b/docs/tutorial/write-your-first-kubernetes-charm-for-a-flask-app.rst @@ -0,0 +1,641 @@ +.. _write-your-first-kubernetes-charm-for-a-flask-app: + +Write your first Kubernetes charm for a Flask app +================================================= + +Imagine you have a Flask application backed up by a database +such as PostgreSQL and need to deploy it. In a traditional setup, +this can be quite a challenge, but with Charmcraft you'll find +yourself packaging and deploying your Flask application in no time. + +In this tutorial we will build a Kubernetes charm for a Flask +application using Charmcraft, so we can have a Flask application +up and running with Juju. Let's get started! + +This tutorial should take 90 minutes for you to complete. + +.. note:: + If you're new to the charming world: Flask applications are + specifically supported with a template to quickly generate a + **rock** (i.e., a special kind of OCI-compliant container image) + and a matching template to quickly generate a **charm** (i.e., + a software operator for cloud operations done with the Juju + orchestration engine). The result is Flask applications that + can be easily deployed, configured, scaled, integrated, etc., + on any Kubernetes cluster. + +What you'll need +---------------- + +- A local system, e.g., a laptop, with amd64 or arm64 architecture which + has sufficient resources to launch a virtual machine with 4 CPUs, + 4 GB RAM, and a 50 GB disk. +- Familiarity with Linux. + +What you'll do +-------------- + +Create a Flask application. Use that to create a rock with +``rockcraft``. Use that to create a charm with ``charmcraft``. Use that +to test, deploy, configure, etc., your Flask application on a local +Kubernetes cloud, ``microk8s``, with ``juju``. All of that multiple +times, mimicking a real development process. + +.. important:: + + Should you get stuck or notice issues, please get in touch on + `Matrix `_ or + `Discourse `_ + +Set things up +------------- + +.. include:: /reuse/tutorial/setup_stable.rst +.. |12FactorApp| replace:: Flask + +Let's create a new directory for this tutorial and enter into it: + +.. code-block:: bash + + mkdir flask-hello-world + cd flask-hello-world + +Finally, install ``python3-venv`` and create a virtual environment: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:create-venv] + :end-before: [docs:create-venv-end] + :dedent: 2 + +Create the Flask application +---------------------------- + +Let's start by creating the "Hello, world" Flask application that +will be used for this tutorial. + +Create a ``requirements.txt`` file using ``touch requirements.txt``. +Then, open the file in a text editor using ``nano requirements.txt``, +copy the following text into it and then save the file: + +.. literalinclude:: code/flask/requirements.txt + :caption: requirements.txt + +.. note:: + + The ``psycopg2-binary`` package is needed so the Flask application can + connect to PostgreSQL. + +Install the packages: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:install-requirements] + :end-before: [docs:install-requirements-end] + :dedent: 2 + +In the same directory, create a file called ``app.py``. +Then copy and save the following code into the file: + +.. literalinclude:: code/flask/app.py + :language: python + +Run the Flask application locally +--------------------------------- + +Now that we have a virtual environment with all the dependencies, let's +run the Flask application to verify that it works: + +.. code-block:: bash + + flask run -p 8000 + +Test the Flask application by using ``curl`` to send a request to the root +endpoint. You will need a new terminal for this; use +``multipass shell charm-dev`` to open a new terminal in Multipass: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:curl-flask] + :end-before: [docs:curl-flask-end] + :dedent: 2 + +The Flask application should respond with ``Hello, world!``. + +The Flask application looks good, so we can stop it for now from the +original terminal using :kbd:`Ctrl` + :kbd:`C`. + +Pack the Flask application into a rock +-------------------------------------- + +First, we'll need a ``rockcraft.yaml`` file. Using the +``flask-framework`` profile, Rockcraft will automate the creation of +``rockcraft.yaml`` and tailor the file for a Flask application. +From the ``/flask-hello-world`` directory, initialize the rock: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:create-rockcraft-yaml] + :end-before: [docs:create-rockcraft-yaml-end] + :dedent: 2 + +The ``rockcraft.yaml`` file will automatically be created and set the name +based on your working directory. + +Check out the contents of ``rockcraft.yaml``: + +.. code:: bash + + cat rockcraft.yaml + +The top of the file should look similar to the following snippet: + +.. code:: yaml + :caption: rockcraft.yaml + + name: flask-hello-world + # see https://documentation.ubuntu.com/rockcraft/en/1.6.0/explanation/bases/ + # for more information about bases and using 'bare' bases for chiselled rocks + base: ubuntu@22.04 # the base environment for this Flask application + version: '0.1' # just for humans. Semantic versioning is recommended + summary: A summary of your Flask application # 79 char long summary + description: | + This is flask-hello-world's description. You have a paragraph or two to tell the + most important story about it. Keep it under 100 words though, + we live in tweetspace and your description wants to look good in the + container registries out there. + # the platforms this rock should be built on and run on. + # you can check your architecture with `dpkg --print-architecture` + platforms: + amd64: + # arm64: + # ppc64el: + # s390x: + + ... + + +Verify that the ``name`` is ``flask-hello-world``. + +Ensure that ``platforms`` includes the architecture of your host. Check +the architecture of your system: + +.. code-block:: bash + + dpkg --print-architecture + + +If your host uses the ARM architecture, open ``rockcraft.yaml`` in a +text editor and include ``arm64`` under ``platforms``. + +Now let's pack the rock: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:pack] + :end-before: [docs:pack-end] + :dedent: 2 + +Depending on your system and network, this step can take several +minutes to finish. + +Once Rockcraft has finished packing the Flask rock, +the terminal will respond with something similar to +``Packed flask-hello-world_0.1_amd64.rock``. + +.. note:: + + If you are not on the ``amd64`` platform, the name of the ``.rock`` file + will be different for you. + +The rock needs to be copied to the MicroK8s registry, which stores OCI +archives so they can be downloaded and deployed in the Kubernetes cluster. +Copy the rock: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:skopeo-copy] + :end-before: [docs:skopeo-copy-end] + :dedent: 2 + +.. seealso:: + + `Ubuntu manpage | skopeo + `_ + +Create the charm +---------------- + +From the ``/flask-hello-world`` directory, let's create a new directory +for the charm and change inside it: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:create-charm-dir] + :end-before: [docs:create-charm-dir-end] + :dedent: 2 + +Using the ``flask-framework`` profile, Charmcraft will automate the +creation of the files needed for our charm, including a +``charmcraft.yaml``, ``requirements.txt`` and source code for the charm. +The source code contains the logic required to operate the Flask +application. + +Initialize a charm named ``flask-hello-world``: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:charm-init] + :end-before: [docs:charm-init-end] + :dedent: 2 + +The files will automatically be created in your working directory. +Let's pack the charm: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:charm-pack] + :end-before: [docs:charm-pack-end] + :dedent: 2 + +Depending on your system and network, this step can take several +minutes to finish. + +Once Charmcraft has finished packing the charm, the terminal will +respond with something similar to +``Packed flask-hello-world_ubuntu-24.04-amd64.charm``. + +.. note:: + + If you are not on the ``amd64`` platform, the name of the ``.charm`` + file will be different for you. + +Deploy the Flask application +---------------------------- + +A Juju model is needed to handle Kubernetes resources while deploying +the Flask application. Let's create a new model: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:add-juju-model] + :end-before: [docs:add-juju-model-end] + :dedent: 2 + +If you are not on a host with the ``amd64`` architecture, you will need to include +to include a constraint to the Juju model to specify your architecture. + +Set the Juju model constraints with: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:add-model-constraints] + :end-before: [docs:add-model-constraints-end] + :dedent: 2 + +Now let’s use the OCI image we previously uploaded to deploy the Flask +application. Deploy using Juju by specifying the OCI image name with the +``--resource`` option: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:deploy-flask-app] + :end-before: [docs:deploy-flask-app-end] + :dedent: 2 + +It will take a few minutes to deploy the Flask application. You can monitor its +progress with: + +.. code:: bash + + juju status --watch 2s + +It can take a couple of minutes for the app to finish the deployment. +Once the status of the App has gone to ``active``, you can stop watching +using :kbd:`Ctrl` + :kbd:`C`. + +.. seealso:: + + See more: :external+juju:ref:`Juju | juju status ` + +The Flask application should now be running. We can monitor the status of +the deployment using ``juju status`` which should be similar to the +following output: + +.. terminal:: + :input: juju status + + Model Controller Cloud/Region Version SLA Timestamp + flask-hello-world dev-controller microk8s/localhost 3.6.2 unsupported 17:04:11+10:00 + + App Version Status Scale Charm Channel Rev Address Exposed Message + flask-hello-world active 1 flask-hello-world 0 10.152.183.166 no + + Unit Workload Agent Address Ports Message + flask-hello-world/0* active idle 10.1.87.213 + +Let's expose the application using ingress. Deploy the +``nginx-ingress-integrator`` charm and integrate it with the Flask app: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:deploy-nginx] + :end-before: [docs:deploy-nginx-end] + :dedent: 2 + +The hostname of the app needs to be defined so that it is accessible via +the ingress. We will also set the default route to be the root endpoint: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:config-nginx] + :end-before: [docs:config-nginx-end] + :dedent: 2 + +Monitor ``juju status`` until everything has a status of ``active``. + +Test the deployment using +``curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1`` +to send a request via the ingress. It should return the +``Hello, world!`` greeting. + +.. note:: + + The ``--resolve flask-hello-world:80:127.0.0.1`` option to the ``curl`` + command is a way of resolving the hostname of the request without + setting a DNS record. + +Configure the Flask application +------------------------------- + +To demonstrate how to provide a configuration to the Flask application, +we will make the greeting configurable. We will expect this +configuration option to be available in the Flask app configuration under the +keyword ``GREETING``. Change back to the ``/flask-hello-world`` directory using +``cd ..`` and copy the following code into ``app.py``: + +.. literalinclude:: code/flask/greeting_app.py + :language: python + +Increment the ``version`` in ``rockcraft.yaml`` to ``0.2`` such that the +top of the ``rockcraft.yaml`` file looks similar to the following: + +.. code-block:: yaml + :emphasize-lines: 5 + + name: flask-hello-world + # see https://documentation.ubuntu.com/rockcraft/en/1.6.0/explanation/bases/ + # for more information about bases and using 'bare' bases for chiselled rocks + base: ubuntu@22.04 # the base environment for this Flask application + version: '0.2' # just for humans. Semantic versioning is recommended + summary: A summary of your Flask application # 79 char long summary + description: | + This is flask-hello-world's description. You have a paragraph or two to tell the + most important story about it. Keep it under 100 words though, + we live in tweetspace and your description wants to look good in the + container registries out there. + # the platforms this rock should be built on and run on. + # you can check your architecture with `dpkg --print-architecture` + platforms: + amd64: + # arm64: + # ppc64el: + # s390x: + + ... + +Let’s pack and upload the rock: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:docker-update] + :end-before: [docs:docker-update-end] + :dedent: 2 + +Change back into the charm directory using ``cd charm``. + +The ``flask-framework`` Charmcraft extension supports adding +configurations to ``charmcraft.yaml`` which will be passed as +environment variables to the Flask application. Add the following to +the end of the ``charmcraft.yaml`` file: + +.. literalinclude:: code/flask/greeting_charmcraft.yaml + :language: yaml + +.. note:: + + Configuration options are automatically capitalized and ``-`` are replaced + by ``_``. A ``FLASK_`` prefix will also be added as a namespace + for app configurations. + +We can now pack and deploy the new version of the Flask app: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:refresh-deployment] + :end-before: [docs:refresh-deployment-end] + :dedent: 2 + +After we wait for a bit monitoring ``juju status`` the application +should go back to ``active`` again. Verify that +the new configuration has been added using +``juju config flask-hello-world | grep -A 6 greeting:`` which should show +the configuration option. + +Using ``curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1`` +shows that the response is still ``Hello, world!`` as expected. + +Now let’s change the greeting: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:change-config] + :end-before: [docs:change-config-end] + :dedent: 2 + +After we wait for a moment for the app to be restarted, using +``curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1`` +should now return the updated ``Hi!`` greeting. + +Integrate with a database +------------------------- + +Now let's keep track of how many visitors your application has received. +This will require integration with a database to keep the visitor count. +This will require a few changes: + +* We will need to create a database migration that creates the ``visitors`` table. +* We will need to keep track how many times the root endpoint has been called + in the database. +* We will need to add a new endpoint to retrieve the number of visitors from the + database. + +Let's start with the database migration to create the required tables. +The charm created by the ``flask-framework`` extension will execute the +``migrate.py`` script if it exists. This script should ensure that the +database is initialized and ready to be used by the application. We will +create a ``migrate.py`` file containing this logic. + +Go back out to the ``/flask-hello-world`` directory using ``cd ..``, +create the ``migrate.py`` file, open the file using a text editor +and paste the following code into it: + +.. literalinclude:: code/flask/visitors_migrate.py + :language: python + +.. note:: + + The charm will pass the Database connection string in the + ``POSTGRESQL_DB_CONNECT_STRING`` environment variable once + PostgreSQL has been integrated with the charm. + +Increment the ``version`` in ``rockcraft.yaml`` to ``0.3`` such that the +top of the ``rockcraft.yaml`` file looks similar to the following: + +.. code-block:: yaml + :emphasize-lines: 5 + + name: flask-hello-world + # see https://documentation.ubuntu.com/rockcraft/en/1.6.0/explanation/bases/ + # for more information about bases and using 'bare' bases for chiselled rocks + base: ubuntu@22.04 # the base environment for this Flask application + version: '0.3' # just for humans. Semantic versioning is recommended + summary: A summary of your Flask application # 79 char long summary + description: | + This is flask-hello-world's description. You have a paragraph or two to tell the + most important story about it. Keep it under 100 words though, + we live in tweetspace and your description wants to look good in the + container registries out there. + # the platforms this rock should be built on and run on. + # you can check your architecture with `dpkg --print-architecture` + platforms: + amd64: + # arm64: + # ppc64el: + # s390x: + + ... + +The app code also needs to be updated to keep track of the number of visitors +and to include a new endpoint to retrieve the number of visitors to the +app. Open ``app.py`` in a text editor and replace its contents with the +following code: + +.. collapse:: visitors_app.py + + .. literalinclude:: code/flask/visitors_app.py + :language: python + +Let’s pack and upload the rock: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:docker-2nd-update] + :end-before: [docs:docker-2nd-update-end] + :dedent: 2 + +Change back into the charm directory using ``cd charm``. + +The Flask app now requires a database which needs to be declared in the +``charmcraft.yaml`` file. Open ``charmcraft.yaml`` in a text editor and +add the following section to the end of the file: + +.. literalinclude:: code/flask/visitors_charmcraft.yaml + :language: yaml + +We can now pack and deploy the new version of the Flask app: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:refresh-2nd-deployment] + :end-before: [docs:refresh-2nd-deployment-end] + :dedent: 2 + +Now let’s deploy PostgreSQL and integrate it with the Flask application: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:deploy-postgres] + :end-before: [docs:deploy-postgres-end] + :dedent: 2 + +Wait for ``juju status`` to show that the App is ``active`` again. +Running ``curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1`` +should still return the ``Hi!`` greeting. + +To check the total visitors, use +``curl http://flask-hello-world/visitors --resolve flask-hello-world:80:127.0.0.1`` +which should return ``1`` after the previous request to the root endpoint, +This should be incremented each time the root endpoint is requested. If we +repeat this process, the output should be as follows: + +.. terminal:: + :input: curl http://flask-hello-world --resolve flask-hello-world:80:127.0.0.1 + + Hi! + :input: curl http://flask-hello-world/visitors --resolve flask-hello-world:80:127.0.0.1 + 2 + +Tear things down +---------------- + +We’ve reached the end of this tutorial. We went through the entire +development process, including: + +- Creating a Flask application +- Deploying the application locally +- Packaging the application using Rockcraft +- Building the application with Ops code using Charmcraft +- Deplyoing the application using Juju +- Exposing the application using an ingress +- Configuring the application +- Integrating the application with a database + +If you'd like to reset your working environment, you can run the following +in the rock directory ``/flask-hello-world`` for the tutorial: + +.. literalinclude:: code/flask/task.yaml + :language: bash + :start-after: [docs:clean-environment] + :end-before: [docs:clean-environment-end] + :dedent: 2 + +You can also clean up your Multipass instance. Start by exiting it: + +.. code-block:: bash + + exit + +And then you can proceed with its deletion: + +.. code-block:: bash + + multipass delete charm-dev + multipass purge + +Next steps +---------- + +By the end of this tutorial you will have built a charm and evolved it +in a number of typical ways. But there is a lot more to explore: + +.. list-table:: + :widths: 30 30 + :header-rows: 1 + + * - If you are wondering... + - Visit... + * - "How do I...?" + - :ref:`How-to guides `, + :external+ops:ref:`Ops | How-to guides ` + * - "How do I debug?" + - `Charm debugging tools `_ + * - "How do I get in touch?" + - `Matrix channel `_ + * - "What is...?" + - :ref:`reference`, + :external+ops:ref:`Ops | Reference `, + :external+juju:ref:`Juju | Reference ` + * - "Why...?", "So what?" + - :external+ops:ref:`Ops | Explanation `, + :external+juju:ref:`Juju | Explanation ` diff --git a/docs/tutorial/write-your-first-kubernetes-charm-for-a-go-app.rst b/docs/tutorial/write-your-first-kubernetes-charm-for-a-go-app.rst index 72aa77a5e..d59385b9b 100644 --- a/docs/tutorial/write-your-first-kubernetes-charm-for-a-go-app.rst +++ b/docs/tutorial/write-your-first-kubernetes-charm-for-a-go-app.rst @@ -1,356 +1,400 @@ .. _write-your-first-kubernetes-charm-for-a-go-app: - Write your first Kubernetes charm for a Go app ============================================== +Imagine you have a Go application backed up by a database +such as PostgreSQL and need to deploy it. In a traditional setup, +this can be quite a challenge, but with Charmcraft you'll find +yourself packaging and deploying your Go application in no time. + +In this tutorial we will build a Kubernetes charm for a Go +application using Charmcraft, so we can have a Go application +up and running with Juju. Let's get started! + +This tutorial should take 90 minutes for you to complete. + +.. note:: + If you're new to the charming world: Go applications are + specifically supported with a template to quickly generate a + **rock** (i.e., a special kind of OCI-compliant container image) + and a matching template to quickly generate a **charm** (i.e., + a software operator for cloud operations done with the Juju + orchestration engine). The result is Go applications that + can be easily deployed, configured, scaled, integrated, etc., + on any Kubernetes cluster. What you'll need: ----------------- -- A working station, e.g., a laptop, with amd64 architecture which has sufficient - resources to launch a virtual machine with 4 CPUs, 4GB RAM, and a 50GB disk. - - * Note that a workstation with arm64 architecture can complete the majority of this - tutorial. +- A local system, e.g., a laptop, with amd64 or arm64 architecture which + has sufficient resources to launch a virtual machine with 4 CPUs, + 4 GB RAM, and a 50 GB disk. - Familiarity with Linux. -- About 90 minutes of free time. - What you'll do: --------------- -Create a Go application. Use that to create a rock with ``rockcraft``. Use that to -create a charm with ``charmcraft``. Use that to test-deploy, configure, etc., your Go -application on a local Kubernetes cloud, ``microk8s``, with ``juju``. All of that -multiple, times, mimicking a real development process. - -.. note:: - - **rock** - - An Ubuntu LTS-based OCI compatible container image designed to meet security, - stability, and reliability requirements for cloud-native software. - - **charm** - - A package consisting of YAML files + Python code that will automate every aspect of - an application's lifecycle so it can be easily orchestrated with Juju. - - **Juju** - - An orchestration engine for charmed applications. +Create a Go application. Use that to create a rock with +``rockcraft``. Use that to create a charm with ``charmcraft``. Use that +to test, deploy, configure, etc., your Go application on a local +Kubernetes cloud, ``microk8s``, with ``juju``. All of that multiple +times, mimicking a real development process. .. important:: - Should you get stuck or notice issues, please get in touch on `Matrix - `_ or `Discourse - `_ - + Should you get stuck or notice issues, please get in touch on + `Matrix `_ or + `Discourse `_ -Set things up: --------------- -Install Multipass. - - See more: `Multipass | How to install Multipass - `_ - -Use Multipass to launch an Ubuntu VM with the name ``charm-dev`` from the 22.04 -blueprint. - -.. code-block:: bash - - multipass launch --cpus 4 --disk 50G --memory 4G --name charm-dev 22.04 - -Once the VM is up, open a shell into it: - -.. code-block:: bash - - multipass shell charm-dev - -In order to create the rock, you'll need to install Rockcraft: - -.. code-block:: bash - - sudo snap install rockcraft --classic - -``LXD`` will be required for building the rock. Make sure it is installed and -initialised: - -.. code-block:: bash +Set things up +------------- - sudo snap install lxd lxd init --auto +.. include:: /reuse/tutorial/setup_edge.rst +.. |12FactorApp| replace:: Go -In order to create the charm, you'll need to install Charmcraft: +Finally, let's create a new directory for this tutorial and +enter into it: -.. code-block:: bash +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:create-working-dir] + :end-before: [docs:create-working-dir-end] + :dedent: 2 - sudo snap install charmcraft --channel latest/edge --classic +Create the Go application +------------------------- -MicroK8s is required to deploy the FastAPI application on Kubernetes. Install MicroK8s: +Start by creating the "Hello, world" Go application that will be +used for this tutorial. -.. code-block:: bash +Install ``go`` and initialize the Go module: - sudo snap install microk8s --channel 1.31-strict/stable sudo adduser $USER - snap_microk8s newgrp snap_microk8s +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:install-init-go] + :end-before: [docs:install-init-go-end] + :dedent: 2 -Wait for MicroK8s to be ready using ``sudo microk8s status --wait-ready``. Several -MicroK8s add-ons are required for deployment: +Create a ``main.go`` file using ``touch main.go``. +Then, open the file in a text editor using ``nano main.go``, +copy the following text into it and then save the file: -.. code-block:: bash +.. literalinclude:: code/go/main.go + :caption: main.go + :language: go - sudo microk8s enable hostpath-storage # Required to host the OCI image of the - FastAPI application sudo microk8s enable registry # Required to expose the FastAPI - application sudo microk8s enable ingress -Juju is required to deploy the Go application. Install Juju and bootstrap a development -controller: +Run the Go application locally +------------------------------ -.. code-block:: bash +First, we need to build the Go application so it can run: - sudo snap install juju --channel 3.5/stable mkdir -p ~/.local/share juju bootstrap - microk8s dev-controller +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:build-go] + :end-before: [docs:build-go-end] + :dedent: 2 -Finally, create a new directory for this tutorial and go inside it: +Now that we have a binary compiled, let's run the Go application to verify +that it works: .. code-block:: bash - mkdir go-hello-world cd go-hello-world - -.. note:: - - This tutorial requires version ``3.2.0`` or later of Charmcraft. Check which version - of Charmcraft you have installed using ``charmcraft --version``. If you have an - older version of Charmcraft installed, use ``sudo snap refresh charmcraft --channel - latest/edge`` to get the latest edge version of Charmcraft. - - This tutorial requires version ``1.5.4`` or later of Rockcraft. Check which version - of Rockcraft you have installed using ``rockcraft --version``. If you have an older - version of Rockcraft installed, use ``sudo snap refresh rockcraft --channel - latest/edge`` to get the latest edge version of Rockcraft. + ./go-hello-world +Test the Go application by using ``curl`` to send a request to the root +endpoint. You will need a new terminal for this; use +``multipass shell charm-dev`` to open a new terminal in Multipass: -Create the Go application -------------------------- +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:curl-go] + :end-before: [docs:curl-go-end] + :dedent: 2 -Start by creating the "Hello, world" Go application that will be used for this tutorial. +The Go application should respond with ``Hello, world!``. -Install ``go`` and initialise the Go module: +The Go application looks good, so we can stop it for now from the +original terminal using :kbd:`Ctrl` + :kbd:`C`. -.. code-block:: bash - sudo snap install go --classic go mod init go-hello-world +Pack the Go application into a rock +----------------------------------- -Create a ``main.go`` file, copy the following text into it and then save it: +First, we'll need a ``rockcraft.yaml`` file. Using the +``go-framework`` profile, Rockcraft will automate the creation of +``rockcraft.yaml`` and tailor the file for a Go application. +From the ``/go-hello-world`` directory, initialize the rock: -.. code-block:: python +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:create-rockcraft-yaml] + :end-before: [docs:create-rockcraft-yaml-end] + :dedent: 2 - package main +The ``rockcraft.yaml`` file will automatically be created and set the name +based on your working directory. - import ( - "fmt" "log" "net/http" - ) +Check out the contents of ``rockcraft.yaml``: - func helloWorldHandler(w http.ResponseWriter, req *http.Request) { - log.Printf("new hello world request") fmt.Fprintln(w, "Hello, world!") - } +.. code:: bash - func main() { - log.Printf("starting hello world application") http.HandleFunc("/", - helloWorldHandler) http.ListenAndServe(":8080", nil) - } + cat rockcraft.yaml +The top of the file should look similar to the following snippet: -Run the Go application locally ------------------------------- +.. code-block:: yaml + :caption: rockcraft.yaml -Build the Go application so it can be run: + name: go-hello-world + # see https://documentation.ubuntu.com/rockcraft/en/latest/explanation/bases/ + # for more information about bases and using 'bare' bases for chiselled rocks + base: bare # as an alternative, a ubuntu base can be used + build-base: ubuntu@24.04 # build-base is required when the base is bare + version: '0.1' # just for humans. Semantic versioning is recommended + summary: A summary of your Go application # 79 char long summary + description: | + This is go-hello-world's description. You have a paragraph or two to tell the + most important story about it. Keep it under 100 words though, + we live in tweetspace and your description wants to look good in the + container registries out there. + # the platforms this rock should be built on and run on. + # you can check your architecture with `dpkg --print-architecture` + platforms: + amd64: + # arm64: + # ppc64el: + # s390x: -.. code-block:: bash + ... - go build . +Verfiy that the ``name`` is ``go-hello-world``. -Now that we have a binary compiled, let's run the Go application to verify that it -works: +Ensure that ``platforms`` includes the architecture of your host. Check +the architecture of your system: .. code-block:: bash - ./go-hello-world - -Test the Go application by using ``curl`` to send a request to the root endpoint. You -may need a new terminal for this; if you are using Multipass, use ``multipass shell -charm-dev`` to get another terminal: + dpkg --print-architecture -.. code-block:: bash - - curl localhost:8080 -The Go application should respond with ``Hello, world!``. The Go application looks good, -so we can stop for now using :kbd:`Ctrl` + :kbd:`C`. +If your host uses the ARM architecture, open ``rockcraft.yaml`` in a +text editor and include ``arm64`` in ``platforms``. +Now let's pack the rock: -Pack the Go application into a rock ------------------------------------ +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:pack] + :end-before: [docs:pack-end] + :dedent: 2 -First, we'll need a ``rockcraft.yaml`` file. Rockcraft will automate its creation and -tailoring for a Go application using the ``go-framework`` profile. +.. note:: -.. code-block:: bash + ``ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS`` is required while the Go + extension is experimental. - rockcraft init --profile go-framework +Depending on your system and network, this step can take several +minutes to finish. -The ``rockcraft.yaml`` file will be created automatically, with its name being set based -on your working directory. Open the file in a text editor and check that the ``name`` is -``go-hello-world``. Ensure that ``platforms`` includes the architecture of your host. -For example, if your host uses the ARM architecture, include ``arm64`` in ``platforms``. +Once Rockcraft has finished packing the Go rock, +the terminal will respond with something similar to +``Packed go-hello-world_0.1_amd64.rock``. .. note:: - For this tutorial, we'll use the name ``go-hello-world`` and assume you are on the - ``amd64`` platform. Check the architecture of your system using ``dpkg - --print-architecture``. Choosing a different name or running a different platform - will influence the names of the files generated by Rockcraft. + If you are not on the ``amd64`` platform, the name of the ``.rock`` file + will be different for you. -Pack the rock: +The rock needs to be copied to the MicroK8s registry, which stores OCI +archives so they can be downloaded and deployed in the Kubernetes cluster. +Copy the rock: -.. code-block:: bash +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:skopeo-copy] + :end-before: [docs:skopeo-copy-end] + :dedent: 2 - ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack +.. seealso:: -.. note:: + `Ubuntu manpage | skopeo + `_ - Depending on your system and network, this step can take a couple of minutes to - finish. +Create the charm +---------------- -Once Rockcraft has finished packing the Go rock, you'll find a new file in your working -directory with the ``.rock`` extension. View its contents: +From the ``/go-hello-world`` directory, let's create a new directory +for the charm and change inside it: -.. code-block:: bash +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:create-charm-dir] + :end-before: [docs:create-charm-dir-end] + :dedent: 2 - ls *.rock -l +Using the ``go-framework`` profile, Charmcraft will automate the +creation of the files needed for our charm, including a +``charmcraft.yaml``, ``requirements.txt`` and source code for the charm. +The source code contains the logic required to operate the Go +application. -.. note:: +Initialize a charm named ``go-hello-world``: - If you changed the ``name`` or ``version`` in ``rockcraft.yaml`` or are not on the - ``amd64`` platform, the name of the ``.rock`` file will be different for you. +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:charm-init] + :end-before: [docs:charm-init-end] + :dedent: 2 -The rock needs to be copied to the Microk8s registry so that it can be deployed in the -Kubernetes cluster: +The files will automatically be created in your working directory. + +Check out the contents of ``charmcraft.yaml``: .. code-block:: bash - rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ - oci-archive:go-hello-world_0.1_amd64.rock \ - docker://localhost:32000/go-hello-world:0.1 + cat charmcraft.yaml +The top of the file should look similar to the following snippet: -Create the charm ----------------- +.. code:: yaml -Create a new directory for the charm and go inside it: + # This file configures Charmcraft. + # See https://juju.is/docs/sdk/charmcraft-config for guidance. -.. code-block:: bash + name: go-hello-world - mkdir charm cd charm + type: charm -We'll need a project file named ``charmcraft.yaml``, ``requirements.txt`` and source -code for the charm. The source code contains the logic required to operate the Go -application. Charmcraft will automate the creation of these files by using the -``go-framework`` profile: + base: ubuntu@24.04 -.. code-block:: bash + # the platforms this charm should be built on and run on. + # you can check your architecture with `dpkg --print-architecture` + platforms: + amd64: + # arm64: + # ppc64el: + # s390x: - charmcraft init --profile go-framework --name go-hello-world + # (Required) + summary: A very short one-line summary of the Go application. -The files will automatically be created in your working directory. + ... -The charm depends on several libraries. Download the libraries and pack the charm: +Verify that the ``name`` is ``go-hello-world``. Ensure that ``platforms`` +includes the architecture of your host. If your host uses the ARM architecture, +open ``charmcraft.yaml`` in a text editor and include ``arm64`` +in ``platforms``. -.. code-block:: bash +Let's pack the charm: - CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft fetch-libs - CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:charm-pack] + :end-before: [docs:charm-pack-end] + :dedent: 2 .. note:: - Depending on your system and network, this step can take a couple of minutes to - finish. + ``CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS`` is required while the Go + extension is experimental. -Once Charmcraft has finished packing the charm, you'll find a new file in your working -directory with the ``.charm`` extension. View its contents: +Depending on your system and network, this step can take several +minutes to finish. -.. code-block:: bash - - ls *.charm -l +Once Charmcraft has finished packing the charm, the terminal will +respond with something similar to +``Packed go-hello-world_ubuntu-24.04-amd64.charm``. .. note:: - If you changed the project name or are not on the ``amd64`` platform, the name of - the ``.charm`` file will be different for you. + If you are not on the ``amd64`` platform, the name of the ``.charm`` + file will be different for you. Deploy the Go application ------------------------- -A Juju model is needed to deploy the application. Let's create a enw model: +A Juju model is needed to handle Kubernetes resources while deploying +the Go application. Let's create a new model: -.. code-block:: bash +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:add-juju-model] + :end-before: [docs:add-juju-model-end] + :dedent: 2 - juju add-model go-hello-world +If you are not on a host with the ``amd64`` architecture, you will need to include +to include a constraint to the Juju model to specify your architecture. -.. note:: +Set the Juju model constraints with: - If you are not on a host with the ``amd64`` architecture, you will need to include a - constraint to the Juju model to specify your architecture. For example, using the - ``arm64`` architecture, you would use ``juju set-model-constraints -m - django-hello-world arch=arm64``. Check the architecture of your system using ``dpkg - --print-architecture``. +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:add-model-constraints] + :end-before: [docs:add-model-constraints-end] + :dedent: 2 -Now the Go application can be deployed using Juju: +Now let’s use the OCI image we previously uploaded to deploy the Go +application. Deploy using Juju by specifying the OCI image name with the +``--resource`` option: -.. code-block:: bash +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:deploy-go-app] + :end-before: [docs:deploy-go-app-end] + :dedent: 2 - juju deploy ./go-hello-world_amd64.charm \ - go-hello-world \ --resource app-image=localhost:32000/go-hello-world:0.1 +It will take a few minutes to deploy the Go application. You can monitor its +progress with: -.. note:: +.. code:: bash - It will take a few minutes to deploy the FastAPI application. You can monitor the - progress using ``juju status --watch 5s``. Once the status of the app changes to - ``active``, you can stop watching using :kbd:`Ctrl` + :kbd:`C`. + juju status --watch 2s -The Go application should now be running. We can monitor the status of the deployment -using ``juju status``, which should be similar to the following output: +It can take a couple of minutes for the app to finish the deployment. +Once the status of the App has gone to ``active``, you can stop watching +using :kbd:`Ctrl` + :kbd:`C`. -.. terminal:: - :input: juju status +.. seealso:: - go-hello-world microk8s microk8s/localhost 3.5.4 unsupported 14:35:07+02:00 + See more: :external+juju:ref:`Juju | juju status ` - App Version Status Scale Charm Channel Rev Address - Exposed Message go-hello-world active 1 go-hello-world - 0 10.152.183.229 no +The Go application should now be running. We can monitor the status of +the deployment using ``juju status``, which should be similar to the +following output: - Unit Workload Agent Address Ports Message go-hello-world/0* - active idle 10.1.157.79 +.. terminal:: + :input: juju status -The deployment is finished when the status shows ``active``. Let's expose the -application using ingress. Deploy the ``nginx-ingress-integrator`` charm and integrate -it with the Go app: + Model Controller Cloud/Region Version SLA Timestamp + go-hello-world dev-controller microk8s/localhost 3.6.2 unsupported 14:35:07+02:00 -.. code-block:: bash + App Version Status Scale Charm Channel Rev Address Exposed Message + go-hello-world active 1 go-hello-world 0 10.152.183.229 no - juju deploy nginx-ingress-integrator --trust juju integrate nginx-ingress-integrator - go-hello-world + Unit Workload Agent Address Ports Message + go-hello-world/0* active idle 10.1.157.79 -The hostname of the app needs to be defined so that it is accessible via the ingress. We -will also set the default route to be the root endpoint: +Let's expose the application using ingress. Deploy the +``nginx-ingress-integrator`` charm and integrate it with the Go app: -.. code-block:: bash +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:deploy-nginx] + :end-before: [docs:deploy-nginx-end] + :dedent: 2 - juju config nginx-ingress-integrator \ - service-hostname=go-hello-world path-routes=/ +The hostname of the app needs to be defined so that it is accessible via +the ingress. We will also set the default route to be the root endpoint: + +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:config-nginx] + :end-before: [docs:config-nginx-end] + :dedent: 2 .. note:: @@ -358,317 +402,322 @@ will also set the default route to be the root endpoint: the default port, it can be done with the configuration option ``app-port`` that will be exposed as the ``APP_PORT`` to the Go application. -Monitor ``juju status`` until everything has a status of ``active``. Use ``curl -http://go-hello-world --resolve go-hello-world:80:127.0.0.1`` to send a request via the -ingress. The Go application should respond with ``Hello, world~``. - - -Configure the Go application ----------------------------- - -Now let's customise the greeting using a configuration option. We will expect this -configuration option to be available in the Go app configuration under the keyword -``GREETING``. Go back out to the root directory of the project using ``cd ..`` and copy -the following code into ``main.go``: - -.. code-block:: c - - package main +Monitor ``juju status`` until everything has a status of ``active``. - import ( - "fmt" "log" "os" "net/http" - ) +Use ``curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1`` +to send a request via the ingress. It should return the +``Hello, world!`` greeting. - func helloWorldHandler(w http.ResponseWriter, req *http.Request) { - log.Printf("new hello world request") greeting, found := - os.LookupEnv("APP_GREETING") if !found { - greeting = "Hello, world!" - } fmt.Fprintln(w, greeting) - } +.. note:: - func main() { - log.Printf("starting hello world application") http.HandleFunc("/", - helloWorldHandler) http.ListenAndServe(":8080", nil) - } + The ``--resolve go-hello-world:80:127.0.0.1`` option to the ``curl`` + command is a way of resolving the hostname of the request without + setting a DNS record. -Open ``rockcraft.yaml`` and update the version to ``0.2``. Run -``ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack`` again, then upload the -new OCI image to the MicroK8s registry. +Configure the Go application +---------------------------- -.. code-block:: bash +To demonstrate how to provide a configuration to the Go application, +we will make the greeting configurable. We will expect this +configuration option to be available in the Go app configuration under the +keyword ``GREETING``. Change back to the ``/go-hello-world`` directory using +``cd ..`` and replace the code into ``main.go`` with the following: - rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ - oci-archive:go-hello-world_0.2_amd64.rock \ - docker://localhost:32000/go-hello-world:0.2 +.. literalinclude:: code/go/greeting_main.txt + :language: go -Change back into the charm directory using ``cd charm``. The ``go-framework`` Charmcraft -extension supports adding configurations to the project file, which will be passed as -environment variables to the Go application. Add the following to the end of the project -file: +Increment the ``version`` in ``rockcraft.yaml`` to ``0.2`` such that the +top of the ``rockcraft.yaml`` file looks similar to the following: .. code-block:: yaml - - config: - options: - greeting: - description: | - The greeting to be returned by the Go application. - default: "Hello, world!" type: string + :emphasize-lines: 6 + + name: go-hello-world + # see https://documentation.ubuntu.com/rockcraft/en/latest/explanation/bases/ + # for more information about bases and using 'bare' bases for chiselled rocks + base: bare # as an alternative, a ubuntu base can be used + build-base: ubuntu@24.04 # build-base is required when the base is bare + version: '0.2' # just for humans. Semantic versioning is recommended + summary: A summary of your Go application # 79 char long summary + description: | + This is go-hello-world's description. You have a paragraph or two to tell the + most important story about it. Keep it under 100 words though, + we live in tweetspace and your description wants to look good in the + container registries out there. + # the platforms this rock should be built on and run on. + # you can check your architecture with `dpkg --print-architecture` + platforms: + amd64: + # arm64: + # ppc64el: + # s390x: + + ... + +Let’s pack and upload the rock: + +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:docker-update] + :end-before: [docs:docker-update-end] + :dedent: 2 + +Change back into the charm directory using ``cd charm``. + +The ``go-framework`` Charmcraft extension supports adding configurations +to ``charmcraft.yaml``, which will be passed as environment variables to +the Go application. Add the following to the end of the +``charmcraft.yaml`` file: + +.. literalinclude:: code/go/greeting_charmcraft.yaml + :language: yaml .. note:: - Configuration options are automatically capitalised and dashes are replaced by - underscores. An ``APP_`` prefix will also be added to ensure that environment - variables are namespaced. - -Run ``CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack`` again. The -deployment can now be refreshed to make use of the new code: + Configuration options are automatically capitalized and ``-`` are replaced + by ``_``. An ``APP_`` prefix will also be added as a namespace + for app configurations. -.. code-block:: bash +We can now pack and deploy the new version of the Go app: - juju refresh go-hello-world \ - --path=./go-hello-world_amd64.charm \ --resource - app-image=localhost:32000/go-hello-world:0.2 +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:refresh-deployment] + :end-before: [docs:refresh-deployment-end] + :dedent: 2 -Wait for ``juju status`` to show that the App is ``active`` again. Verify that the new -configuration has been added using ``juju config go-hello-world | grep -A 6 greeting:``, +After we wait for a bit monitoring ``juju status`` the application +should go back to ``active`` again. Verify that the new configuration +has been added using +``juju config go-hello-world | grep -A 6 greeting:``, which should show the configuration option. -.. note:: - - The ``grep`` command extracts a portion of the configuration to make it easier to - check whether the configuration option has been added. - -Using ``curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1`` shows that -the response is still ``Hello, world!`` as expected. The greeting can be changed using -Juju: - -.. code-block:: bash - - juju config go-hello-world greeting='Hi!' - -``curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1`` now returns the -updated ``Hi!`` greeting. +Using ``curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1`` +shows that the response is still ``Hello, world!`` as expected. -.. note:: +Now let's change the greeting: - It might take a short time for the configuration to take effect. +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:change-config] + :end-before: [docs:change-config-end] + :dedent: 2 +After we wait for a moment for the app to be restarted, using +``curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1`` +should now return the updated ``Hi!`` greeting. Integrate with a database ------------------------- -Now let's keep track of how many visitors your application has received. This will -require integration with a database to keep the visitor count. This will require a few -changes: +Now let's keep track of how many visitors your application has received. +This will require integration with a database to keep the visitor count. +This will require a few changes: - We will need to create a database migration that creates the ``visitors`` table. -- We will need to keep track how many times the root endpoint has been called in the - database. -- We will need to add a new endpoint to retrieve the number of visitors from the -- database. +- We will need to keep track how many times the root endpoint has been called + in the database. +- We will need to add a new endpoint to retrieve the number of visitors from + the database. -The charm created by the ``go-framework`` extension will execute the ``migrate.sh`` -script if it exists. This script should ensure that the database is initialised and -ready to be used by the application. We will create a ``migrate.sh`` file containing the -logic. +Let's start with the database migration to create the required tables. +The charm created by the ``go-framework`` extension will execute the +``migrate.sh`` script if it exists. This script should ensure that the +database is initialized and ready to be used by the application. We will +create a ``migrate.sh`` file containing this logic. -Go back out to the tutorial root directory using ``cd ..``. Create the ``migrate.sh`` -file using a text editor and paste the following code into it: +Go back out to the ``/go-hello-world`` directory using ``cd ..``. +Create the ``migrate.sh`` file using a text editor and paste the +following code into it: -.. code-block:: bash - - #!/bin/bash - - PGPASSWORD="${POSTGRESQL_DB_PASSWORD}" psql -h "${POSTGRESQL_DB_HOSTNAME}" -U - "${POSTGRESQL_DB_USERNAME}" "${POSTGRESQL_DB_NAME}" -c "CREATE TABLE IF NOT EXISTS - visitors (timestamp TIMESTAMP NOT NULL, user_agent TEXT NOT NULL);" +.. literalinclude:: code/go/visitors_migrate.sh + :language: bash .. note:: The charm will pass the Database connection string in the - ``POSTGRESQL_DB_CONNECT_STRING`` environment variable once PostgreSQL has been - integrated with the charm. + ``POSTGRESQL_DB_CONNECT_STRING`` environment variable once + PostgreSQL has been integrated with the charm. Change the permissions of the file ``migrate.sh`` so that it is executable: -.. code-block:: bash +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:change-migrate-permissions] + :end-before: [docs:change-migrate-permissions-end] + :dedent: 2 - chmod u+x migrate.sh +For the migrations to work, we need the ``postgresql-client`` package +installed in the rock. By default, the ``go-framework`` uses the ``base`` +base, so we will also need to install a shell interpreter. Let's do it as a +slice, so that the rock does not include unnecessary files. Open the +``rockcraft.yaml`` file using a text editor and add the following to the +end of the file: -For the migrations to work, we need the ``postgresql-client`` package installed in the -rock. By default, the ``go-framework`` uses the ``base`` base, so we will also need to -install a shell interpreter. Let's do it as a slice, so that the rock does not include -unnecessary files. Open the ``rockcraft.yaml`` file using a text editor, update the -version to ``0.3`` and add the following to the end of the file: +.. literalinclude:: code/go/visitors_rockcraft.yaml + :language: yaml -.. code-block:: yaml +Increment the ``version`` in ``rockcraft.yaml`` to ``0.3`` such that the +top of the ``rockcraft.yaml`` file looks similar to the following: - parts: - runtime-debs: - plugin: nil stage-packages: - - postgresql-client - runtime-slices: - plugin: nil stage-packages: - - bash_bins - -To be able to connect to PostgreSQL from the Go app, the library ``pgx`` will be used. -The app code needs to be updated to keep track of the number of visitors and to include -a new endpoint to retrieve the number of visitors. Open ``main.go`` in a text editor and +.. code-block:: yaml + :emphasize-lines: 6 + + name: go-hello-world + # see https://documentation.ubuntu.com/rockcraft/en/latest/explanation/bases/ + # for more information about bases and using 'bare' bases for chiselled rocks + base: bare # as an alternative, a ubuntu base can be used + build-base: ubuntu@24.04 # build-base is required when the base is bare + version: '0.3' # just for humans. Semantic versioning is recommended + summary: A summary of your Go application # 79 char long summary + description: | + This is go-hello-world's description. You have a paragraph or two to tell the + most important story about it. Keep it under 100 words though, + we live in tweetspace and your description wants to look good in the + container registries out there. + # the platforms this rock should be built on and run on. + # you can check your architecture with `dpkg --print-architecture` + platforms: + amd64: + # arm64: + # ppc64el: + # s390x: + + ... + +To be able to connect to PostgreSQL from the Go app, the library +``pgx`` will be used. The app code needs to be updated to keep track of +the number of visitors and to include a new endpoint to retrieve the +number of visitors. Open ``main.go`` in a text editor and replace its content with the following code: .. dropdown:: main.go - .. code-block:: c - - package main - - import ( - "database/sql" "fmt" "log" "net/http" "os" "time" - - _ "github.com/jackc/pgx/v5/stdlib" - ) - - func helloWorldHandler(w http.ResponseWriter, req *http.Request) { - log.Printf("new hello world request") postgresqlURL := - os.Getenv("POSTGRESQL_DB_CONNECT_STRING") db, err := sql.Open("pgx", - postgresqlURL) if err != nil { - log.Printf("An error occurred while connecting to postgresql: - %v", err) return - } defer db.Close() - - ua := req.Header.Get("User-Agent") timestamp := time.Now() _, err = - db.Exec("INSERT into visitors (timestamp, user_agent) VALUES ($1, $2)", - timestamp, ua) if err != nil { - log.Printf("An error occurred while executing query: %v", err) - return - } - - greeting, found := os.LookupEnv("APP_GREETING") if !found { - greeting = "Hello, world!" - } - - fmt.Fprintln(w, greeting) - } - - func visitorsHandler(w http.ResponseWriter, req *http.Request) { - log.Printf("visitors request") postgresqlURL := - os.Getenv("POSTGRESQL_DB_CONNECT_STRING") db, err := sql.Open("pgx", - postgresqlURL) if err != nil { - return - } defer db.Close() - - var numVisitors int err = db.QueryRow("SELECT count(*) from - visitors").Scan(&numVisitors) if err != nil { - log.Printf("An error occurred while executing query: %v", err) - return - } fmt.Fprintf(w, "Number of visitors %d\n", numVisitors) - } - - func main() { - log.Printf("starting hello world application") http.HandleFunc("/", - helloWorldHandler) http.HandleFunc("/visitors", visitorsHandler) - http.ListenAndServe(":8080", nil) - } - -Check all the packages and their dependencies in the Go project with the following -command: + .. literalinclude:: code/go/visitors_main.txt + :language: go -.. code-block:: bash +Check all the packages and their dependencies in the Go project with the +following command: - go mod tidy +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:check-go-app] + :end-before: [docs:check-go-app-end] + :dedent: 2 -Run ``ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack`` and upload the -newly created rock to the MicroK8s registry: +Let’s pack and upload the rock: -.. code-block:: bash +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:docker-2nd-update] + :end-before: [docs:docker-2nd-update-end] + :dedent: 2 - rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ - oci-archive:go-hello-world_0.3_amd64.rock \ - docker://localhost:32000/go-hello-world:0.3 +Change back into the charm directory using ``cd charm``. -Go back into the charm directory using ``cd charm``. The Go app now requires a database -which needs to be declared in the project file. Open the project file in a text -editor and add the following section to the end of the file: +The Go app now requires a database which needs to be declared in the +``charmcraft.yaml`` file. Open ``charmcraft.yaml`` in a text editor and +add the following section to the end of the file: -.. code-block:: yaml +.. literalinclude:: code/go/visitors_charmcraft.yaml + :language: yaml - requires: - postgresql: - interface: postgresql_client optional: false +We can now pack and deploy the new version of the Go app: -Pack the charm using ``CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack`` -and refresh the deployment using Juju: +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:refresh-2nd-deployment] + :end-before: [docs:refresh-2nd-deployment-end] + :dedent: 2 -.. code-block:: bash - - juju refresh go-hello-world \ - --path=./go-hello-world_amd64.charm \ --resource - app-image=localhost:32000/go-hello-world:0.3 +Now let’s deploy PostgreSQL and integrate it with the Go application: -Deploy ``postgresql-k8s`` using Juju and integrate it with ``go-hello-world``: - -.. code-block:: bash +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:deploy-postgres] + :end-before: [docs:deploy-postgres-end] + :dedent: 2 - juju deploy postgresql-k8s --trust juju integrate go-hello-world postgresql-k8s +Wait for ``juju status`` to show that the App is ``active`` again. +Running ``curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1`` +should still return the ``Hi!`` greeting. -Wait for ``juju status`` to show that the App is ``active`` again. Executing ``curl -http://go-hello-world --resolve go-hello-world:80:127.0.0.1`` should still return the -``Hi!`` greeting. - -To check the local visitors, use ``curl http://go-hello-world/visitors --resolve -go-hello-world:80:127.0.0.1``, which should return ``Number of visitors 1`` after the -previous request to the root endpoint. This should be incremented each time the root -endpoint is requested. If we repeat this process, the output should be as follows: +To check the local visitors, use +``curl http://go-hello-world/visitors --resolve go-hello-world:80:127.0.0.1``, +which should return ``Number of visitors 1`` after the +previous request to the root endpoint. +This should be incremented each time the root endpoint is requested. If we +repeat this process, the output should be as follows: .. terminal:: :input: curl http://go-hello-world --resolve go-hello-world:80:127.0.0.1 - Hi! :input: curl http://go-hello-world/visitors --resolve - go-hello-world:80:127.0.0.1 Number of visitors 2 + Hi! + :input: curl http://go-hello-world/visitors --resolve go-hello-world:80:127.0.0.1 + Number of visitors 2 Tear things down ---------------- -We've reached the end of this tutorial. We have created a Go application, deployed it -locally, integrated it with a database and exposed it via ingress! +We’ve reached the end of this tutorial. We went through the entire +development process, including: -If you'd like to reset your working environment, you can run the following in the root -directory for the tutorial: +- Creating a Go application +- Deploying the application locally +- Packaging the application using Rockcraft +- Building the application with Ops code using Charmcraft +- Deplyoing the application using Juju +- Exposing the application using an ingress +- Configuring the application +- Integrating the application with a database -.. code-block:: bash +If you'd like to reset your working environment, you can run the following +in the rock directory ``/go-hello-world`` for the tutorial: - cd .. rm -rf charm # delete all the files created during the tutorial rm - go-hello-world_0.1_amd64.rock go-hello-world_0.2_amd64.rock \ - go-hello-world_0.3_amd64.rock rockcraft.yaml main.go \ migrate.sh go-hello-world - go.mod go.sum - # Remove the juju model juju destroy-model go-hello-world --destroy-storage +.. literalinclude:: code/go/task.yaml + :language: bash + :start-after: [docs:clean-environment] + :end-before: [docs:clean-environment-end] + :dedent: 2 -If you created an instance using Multipass, you can also clean it up. Start by exiting -it: +You can also clean up your Multipass instance. Start by exiting it: .. code-block:: bash exit -You can then proceed with its deletion: +And then you can proceed with its deletion: .. code-block:: bash - multipass delete charm-dev multipass purge + multipass delete charm-dev + multipass purge Next steps ---------- -By the end of this tutorial, you will have built a charm and evolved it in a number of -practical ways, but there is a lot more to explore: - -+-------------------------+----------------------+ -| If you are wondering... | Visit... | -+=========================+======================+ -| "How do I...?" | :ref:`how-to-guides` | -+-------------------------+----------------------+ -| "What is...?" | :ref:`reference` | -+-------------------------+----------------------+ +By the end of this tutorial you will have built a charm and evolved it +in a number of typical ways. But there is a lot more to explore: + +.. list-table:: + :widths: 30 30 + :header-rows: 1 + + * - If you are wondering... + - Visit... + * - "How do I...?" + - :ref:`How-to guides `, + :external+ops:ref:`Ops | How-to guides ` + * - "How do I debug?" + - `Charm debugging tools `_ + * - "How do I get in touch?" + - `Matrix channel `_ + * - "What is...?" + - :ref:`reference`, + :external+ops:ref:`Ops | Reference `, + :external+juju:ref:`Juju | Reference ` + * - "Why...?", "So what?" + - :external+ops:ref:`Ops | Explanation `, + :external+juju:ref:`Juju | Explanation ` diff --git a/spread.yaml b/spread.yaml index 3a2fb0fed..f49152d58 100644 --- a/spread.yaml +++ b/spread.yaml @@ -23,7 +23,7 @@ backends: google: key: '$(HOST: echo "$SPREAD_GOOGLE_KEY")' location: snapd-spread/us-east1-b - halt-timeout: 2h + halt-timeout: 3h systems: - ubuntu-18.04-64: workers: 1 @@ -155,7 +155,7 @@ suites: - ubuntu-22.04-64 manual: true prepare: | - juju_channel=3.5/stable + juju_channel=3.6/stable microk8s_channel=1.31-strict/stable mkdir -p ~/.local/share # Workaround for Juju not being able to create the directory