diff --git a/docs/reuse/tutorial/setup_edge.rst b/docs/reuse/tutorial/setup_edge.rst new file mode 100644 index 000000000..984495fb2 --- /dev/null +++ b/docs/reuse/tutorial/setup_edge.rst @@ -0,0 +1,90 @@ +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'll need to install Rockcraft with the +``--classic`` confinement to allow 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 + +.. warning:: + + This tutorial requires version ``3.2.0`` or later of Charmcraft. + Check the version of Charmcraft using ``charmcraft --version``. + +MicroK8s is required to deploy the |12FactorApp| application on Kubernetes. +Let's install MicroK8s using the ``1.31-strict/stable`` track: + +.. code-block:: bash + + sudo snap install microk8s --channel 1.31-strict/stable + sudo adduser $USER snap_microk8s + newgrp snap_microk8s + +Wait for MicroK8s to be ready: + +.. code-block:: bash + + 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 application + sudo microk8s enable registry + # Required to expose the application + sudo microk8s enable ingress + +Juju is required to deploy the |12FactorApp| application. +Install Juju using the ``3.5/stable`` track, and bootstrap a +development controller: + +.. code-block:: bash + + sudo snap install juju --channel 3.5/stable + mkdir -p ~/.local/share + juju bootstrap microk8s dev-controller + +.. note:: + + 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..f24549c88 100644 --- a/docs/reuse/tutorial/setup_stable.rst +++ b/docs/reuse/tutorial/setup_stable.rst @@ -1,4 +1,9 @@ -First, `install Multipass `_. +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,20 +18,23 @@ 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'll need to install Rockcraft with the +``--classic`` confinement to allow 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: +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 @@ -41,7 +49,8 @@ In order to create the charm, you'll need to install Charmcraft: ``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 @@ -49,22 +58,33 @@ MicroK8s is required to deploy the Flask application on Kubernetes. Install Micr sudo adduser $USER snap_microk8s newgrp snap_microk8s -Wait for MicroK8s to be ready using ``sudo microk8s status --wait-ready``. +Wait for MicroK8s to be ready: + +.. code-block:: bash + + 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: +Juju is required to deploy the |12FactorApp| application. +Install Juju using the ``3.5/stable`` track, and bootstrap a +development controller: .. code-block:: bash sudo snap install juju --channel 3.5/stable mkdir -p ~/.local/share juju bootstrap microk8s dev-controller + +.. note:: + + 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..d066c7edc --- /dev/null +++ b/docs/tutorial/code/django/task.yaml @@ -0,0 +1,276 @@ +########################################### +# 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-get update && sudo apt-get 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-allowed-hosts-debug] + juju config django-hello-world django-allowed-hosts=* django-debug=true + # [docs:config-allowed-hosts-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 + + # curl the Django app + curl http://django-hello-world --resolve django-hello-world:80:127.0.0.1 | grep Congratulations + + # [docs:config-allowed-hosts] + juju config django-hello-world django-allowed-hosts=django-hello-world + # [docs:config-allowed-hosts-end] + + # curl again + 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 127.0.0.1 -H "Host: django-hello-world" | 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 127.0.0.1 -H "Host: django-hello-world" | 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 127.0.0.1 -H "Host: django-hello-world" | grep Hi + + # Back out to main directory for clean-up + cd .. + + # [docs:clean-environment] + # 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..8e3dfea72 --- /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"} \ No newline at end of file diff --git a/docs/tutorial/code/fastapi/greeting_app.py b/docs/tutorial/code/fastapi/greeting_app.py new file mode 100644 index 000000000..9183838d1 --- /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")} \ No newline at end of file diff --git a/docs/tutorial/code/fastapi/greeting_charmcraft.yaml b/docs/tutorial/code/fastapi/greeting_charmcraft.yaml new file mode 100644 index 000000000..e737da7e0 --- /dev/null +++ b/docs/tutorial/code/fastapi/greeting_charmcraft.yaml @@ -0,0 +1,9 @@ +# configuration snippet for FastAPI application with a configuration + +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..d00d45731 --- /dev/null +++ b/docs/tutorial/code/fastapi/task.yaml @@ -0,0 +1,206 @@ +########################################### +# 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-get update && sudo apt-get 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 postgresql-k8s --query='status=="active"' --timeout 20m | juju status --relations + juju wait-for application fastapi-hello-world --query='status=="active"' --timeout 20m | 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 + + # Back out to main directory for cleanup + cd .. + + # [docs:clean-environment] + # 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..6b745e441 100644 --- a/docs/tutorial/code/flask/task.yaml +++ b/docs/tutorial/code/flask/task.yaml @@ -34,9 +34,12 @@ execute: | sudo apt-get update && sudo apt-get 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,22 +164,22 @@ 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 flask-hello-world --query='status=="active"' --timeout 30m | juju status --relations + juju wait-for application postgresql-k8s --query='status=="active"' --timeout 20m | juju status --relations + juju wait-for application flask-hello-world --query='status=="active"' --timeout 20m | juju status --relations juju status --relations @@ -191,7 +188,7 @@ 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 + # Back out to main directory for cleanup cd .. # [docs:clean-environment] @@ -199,9 +196,10 @@ execute: | 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/flask.rst b/docs/tutorial/flask.rst deleted file mode 100644 index df0ae4a72..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 a ``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 name in charmcraft.yaml 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 ``charmcraft.yaml`` which -will be passed as environment variables to the Flask application. Add the -following to the end of the ``charmcraft.yaml`` 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 ``charmcraft.yaml`` file. Open -``charmcraft.yaml`` 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 30537259f..b0cc4eb1d 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,44 @@ .. _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. Let’s get started! -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. -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:: - **rock** + If you're new to the charming world: Django 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. - An Ubuntu LTS-based OCI compatible container image designed to meet security, - stability, and reliability requirements for cloud-native software. - - **charm** +What you’ll need +---------------- - 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 working station, 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 +46,752 @@ 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_edge.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 change 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 +Create the Django application +----------------------------- -In order to create the rock, you'll need to install Rockcraft: +Let's start by creating the "Hello, world" Django application that +will be used for this tutorial. -.. code-block:: bash +Create a ``requirements.txt`` file, copy the following text into it and +then save it: - sudo snap install rockcraft --classic +.. literalinclude:: code/django/requirements.txt -``LXD`` will be required for building the rock. Make sure it is installed and -initialised: +.. note:: -.. code-block:: bash + The ``psycopg2-binary`` package is needed so the Django application can + connect to PostgreSQL. - sudo snap install lxd - lxd init --auto +Install the packages: -In order to create the charm, you'll need to install Charmcraft: +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:install-requirements] + :end-before: [docs:install-requirements-end] + :dedent: 2 -.. code-block:: bash +Create a new project using ``django-admin``: - sudo snap install charmcraft --channel latest/edge --classic +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:django-startproject] + :end-before: [docs:django-startproject-end] + :dedent: 2 -.. note:: +Run the Django application locally +---------------------------------- - 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. +We will test the Django application by visiting the app in a web +browser. -MicroK8s is required to deploy the Django application on Kubernetes. Install MicroK8s: +Change into the ``django_hello_world`` directory: -.. code-block:: bash +.. code:: bash - sudo snap install microk8s --channel 1.31-strict/stable - sudo adduser $USER snap_microk8s - newgrp snap_microk8s + cd django_hello_world -Wait for MicroK8s to be ready using ``sudo microk8s status --wait-ready``. Several -MicroK8s add-ons are required for deployment +Open the settings file of the application located at +``django_hello_world/settings.py``. Update the ``ALLOWED_HOSTS`` setting +to allow all traffic: -.. code-block:: bash +.. code:: python - 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 + ALLOWED_HOSTS = ['*'] -Juju is required to deploy the Django application. Install Juju and bootstrap a -development controller: +Save and close the ``settings.py`` file. -.. code-block:: bash +Now, run the Django application to verify that it works: - sudo snap install juju --channel 3.5/stable - mkdir -p ~/.local/share - juju bootstrap microk8s dev-controller +.. code:: bash -Finally, create a new directory for this tutorial and go inside it: + python3 manage.py runserver 0.0.0.0:8000 -.. code-block:: bash +.. note:: - mkdir django-hello-world - cd django-hello-world + Specifying ``0.0.0.0:8000`` allows for traffic outside of the Multipass VM. +Now we need the private IP address of the Multipass VM. Outside of the +Multipass VM, run: -Create the Django application ------------------------------ +.. code-block:: -Create a ``requirements.txt`` file, copy the following test into it and then -save it: + multipass info charm-dev | grep IP -.. code-block:: bash +.. note:: - Django + The ``grep`` command extracts a portion of the output to highlight the + IP address. -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:: 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: +Near the top of the ``settings.py`` file change the following settings +to be production ready: .. 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', '{ref}`]')) -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'), - } - } - -We'll need to update the ``requirements.txt`` file to include ``psycopg2-binary`` -so that the Django app can connect to PostgreSQL. + 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'), + } + } -Pack the rock: +Save and close the ``settings.py`` file. -.. code-block:: bash +Now let’s pack the rock: - 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=true`` may be required + in the pack command for older versions of Rockcraft. - ``ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS`` is required whilst the Django - extension is experimental. +Depending on your system and network, this step can take several minutes to +finish. -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: +Once Rockcraft has finished packing the Django rock, the +terminal will respond with something similar to +``Packed django-hello-world_0.1_amd64.rock``. -.. code-block:: bash - - ls *.rock -l - -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 an ``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:: + See more: `Ubuntu manpage | skopeo + `_ Create the charm ---------------- -Create a new directory for the charm and go inside it: - -.. code-block:: bash +From the ``django-hello-world`` directory, create a new directory for +the charm and change inside it: - mkdir charm - cd charm +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:create-charm-dir] + :end-before: [docs:create-charm-dir-end] + :dedent: 2 -We'll need a ``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: +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. -.. code-block:: bash +Initialize a charm named ``django-hello-world``: - charmcraft init --profile django-framework --name django-hello-world +.. literalinclude:: code/django/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. We will -need to connect to the PostgreSQL database. Open the ``charmcraft.yaml`` -file and add the following section to the end of the file: - -.. code-block:: yaml +The files will automatically be created in your working directory. - requires: - postgresql: - interface: postgresql_client - optional: false - limit: 1 +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: -The charm depends on several libraries. Download the libraries and pack -the charm: +.. literalinclude:: code/django/postgres_requires_charmcraft.yaml + :language: yaml -.. code-block:: bash +Now let’s pack the charm: - 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. + ``CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true`` may be required + in the pack command for older versions of Charmcraft. -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 django-hello-world_ubuntu-22.04-amd64.charm``. .. note:: - If you changed the name in ``charmcraft.yaml`` 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: +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 add-model django-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. +You can check the architecture of your system using +``dpkg --print-architecture``. -.. note:: +Set the Juju model constraints using - 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/django/task.yaml + :language: bash + :start-after: [docs:add-model-constraints] + :end-before: [docs:add-model-constraints-end] + :dedent: 2 -Now deploy the Django application using Juju: +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: -.. code-block:: bash +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:deploy-django-app] + :end-before: [docs:deploy-django-app-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 +Now let’s deploy PostgreSQL: -Deploy PostgreSQL and integrate with the Django application: +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:deploy-postgres] + :end-before: [docs:deploy-postgres-end] + :dedent: 2 -.. code-block:: bash +Integrate PostgreSQL with the Django application: - juju deploy postgresql-k8s --trust - juju integrate django-hello-world postgresql-k8s +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:integrate-postgres] + :end-before: [docs:integrate-postgres-end] + :dedent: 2 -.. 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`. - -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: +It will take a few minutes to deploy the Django application. You can +monitor the progress using -.. terminal:: - :input: juju status +.. code:: bash - django-hello-world dev-controller microk8s/localhost 3.5.3 unsupported 16:47:01+10:00 + juju status --relations --watch 2s - 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 +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. - 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 +Once the status of the App has gone to ``active``, you can stop watching +using :kbd:`Ctrl` + :kbd:`C`. -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: +.. seealso:: -.. code-block:: bash + See more: `Command 'juju status' `_ - juju config django-hello-world django-allowed-hosts=* django-debug=true +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: -.. note:: +.. terminal:: + :input: juju status - 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. + Model Controller Cloud/Region Version SLA Timestamp + django-hello-world dev-controller microk8s/localhost 3.5.3 unsupported 16:47:01+10:00 -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): + 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 -.. code-block:: bash + 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 - curl 10.1.157.80:8000 +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 +whilst we are testing. Set both configurations: -The Django app should again respond with: +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:config-allowed-hosts-debug] + :end-before: [docs:config-allowed-hosts-debug-end] + :dedent: 2 - The install worked successfully! Congratulations! +.. note:: + 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 + 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. -Add a root endpoint -------------------- +Let’s expose the application using ingress. Deploy the +``nginx-ingress-integrator`` charm and integrate it with the Django app: -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:deploy-nginx] + :end-before: [docs:deploy-nginx-end] + :dedent: 2 -.. code-block:: bash +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: - django-admin startapp greeting +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:config-nginx] + :end-before: [docs:config-nginx-end] + :dedent: 2 -Open the ``greetings/view.py`` file and replace the content with: +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.http import HttpResponse +.. code:: bash - def index(request): - return HttpResponse("Hello, world!\n") + django-hello-world -Create the ``greetings/urls.py`` file with the following contents: +Here, replace ```` with the same Multipass VM +private IP address you previously used. -.. code-block:: python +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!``. - from django.urls import path +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). Inside the Multipass +VM, set the configuration: - from . import views +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:config-allowed-hosts] + :end-before: [docs:config-allowed-hosts-end] + :dedent: 2 - urlpatterns = [ - path("", views.index, name="index"), - ] +Visiting http://django-hello-world should still respond with +``The install worked successfully! Congratulations!``. -Open the ``django_hello_world/urls.py`` file and edit the value of -``urlpatterns`` to include ``path('', include("greetings.url")``, -for example: +Add an initial app +------------------ -.. code-block:: python +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: - from django.contrib import admin - from django.urls import include, path +.. literalinclude:: code/django/task.yaml + :language: bash + :start-after: [docs:startapp-greeting] + :end-before: [docs:startapp-greeting-end] + :dedent: 2 - urlpatterns = [ - path("", include("greeting.urls")), - path("admin/", admin.site.urls), - ] +Open the ``greeting/views.py`` file and replace the content with: -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: +.. literalinclude:: code/django/views_greeting.py + :language: python -.. code-block:: yaml +Create the ``greeting/urls.py`` file with the following contents: - 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 +.. literalinclude:: code/django/urls_greeting.py + :language: python -Now we can deploy the new version of the Django application using: +Open the ``django_hello_world/urls.py`` file and edit the imports to +contain ``include`` and the value of ``urlpatterns`` to include +``path('', include("greeting.urls")`` like in the following example: -.. code-block:: bash +.. code-block:: python + :emphasize-lines: 2,5 - 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 + from django.contrib import admin + from django.urls import include, path -Now that we have a valid root endpoint, we can disable debug mode: + urlpatterns = [ + path("", include("greeting.urls")), + path("admin/", admin.site.urls), + ] -.. code-block:: bash +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: - juju config django-hello-world django-debug=false +.. 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 127.0.0.1 -H "Host: django-hello-world"`` inside the +Multipass VM. Either way, the Django application should respond with +``Hello, world!``. -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!``. +.. note:: + 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. 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 ``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 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: - -.. 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 run the pack and upload commands for 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 -``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. +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 127.0.0.1 -H "Host: django-hello-world"`` or +visiting http://django-hello-world in a web browser should result in the +Django application responding with ``Hello, world!`` again. -.. note:: - - 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 +Now let’s change the greeting: - 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 127.0.0.1 -H "Host: django-hello-world"`` 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 +- Building an OCI image using Rockcraft +- Packaging the application 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 12a8c0e9e..135f64ec4 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,43 @@ 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. +Let's get started! + +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. + +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 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 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. -- 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,107 +52,51 @@ 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: - -.. 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'll need to install Rockcraft: - -.. code-block:: bash - - sudo snap install rockcraft --channel latest/edge --classic - -``LXD`` will be required for building the rock. Make sure it is installed -and initialised: - -.. code-block:: bash - - sudo snap install lxd - lxd init --auto +.. include:: /reuse/tutorial/setup_edge.rst +.. |12FactorApp| replace:: FastAPI -In order to create the charm, you'll need to install Charmcraft: +Let's create a directory for this tutorial and change into it: .. code-block:: bash - sudo snap install charmcraft --channel latest/edge --classic - -MicroK8s is required to deploy the FastAPI application on Kubernetes. -Install MicroK8s: - -.. code-block:: bash - - sudo snap install microk8s --channel 1.31-strict/stable - sudo adduser $USER snap_microk8s - newgrp snap_microk8s + mkdir fastapi-hello-world + cd fastapi-hello-world -Wait for MicroK8s to be ready using ``sudo microk8s status --wait-ready``. -Several MicroK8s add-ons are required for deployment: +Finally, install ``python-venv`` and create a virtual environment: .. 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 - -Juju is required to deploy the FastAPI application. Install Juju and bootstrap -a development controller: + sudo apt-get update && sudo apt-get install python3-venv -y + python3 -m venv .venv + source .venv/bin/activate -.. code-block:: bash +Create the FastAPI application +------------------------------ - sudo snap install juju --channel 3.5/stable - mkdir -p ~/.local/share - juju bootstrap microk8s dev-controller +Start by creating the "Hello, world" FastAPI application that will be used for +this tutorial. -Finally, create a new directory for this tutorial and go inside it: +Create a ``requirements.txt`` file, copy the following text into it +and then save it: .. code-block:: bash - mkdir fastapi-hello-world - cd fastapi-hello-world + fastapi[standard] + psycopg2-binary .. note:: - 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. - - 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 ``psycopg2-binary`` package is needed so the FastAPI application can + connect to PostgreSQL. - -Create the FastAPI application ------------------------------- - -Start by creating the "Hello, world" FastAPI application that will be used for -this tutorial. - -Create a ``requirements.txt`` file, copy the following text into it and then save it: +Install the packages: .. code-block:: bash - fastapi[standard] + pip install -r requirements.txt -In the same directory, copy and save the following into a text file called ``app.py``: +In the same directory, copy and save the following into a text file +called ``app.py``: .. code-block:: python @@ -168,15 +112,6 @@ In the same directory, copy and save the following into a text file called ``app Run the FastAPI application locally ----------------------------------- -Install ``python3-venv`` and create a virtual environment: - -.. 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 - Now that we have a virtual environment with all the dependencies, let's run the FastAPI application to verify that it works: @@ -185,42 +120,78 @@ let's run the FastAPI application to verify that it works: fastapi dev app.py --port 8080 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: +endpoint. You will need a new terminal for this; use +``multipass shell charm-dev`` to open a new terminal in Multipass: .. code-block:: bash curl localhost:8080 -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`. +The FastAPI application should respond with ``{"message":"Hello World"}``. + +The FastAPI application looks good, so we can stop for now from the +original terminal using :kbd:`Ctrl` + :kbd:`C`. Pack the FastAPI application into a rock ---------------------------------------- -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: +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: .. code-block:: bash rockcraft init --profile fastapi-framework -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``. +The ``rockcraft.yaml`` file will be automatically created, with the name being +set based on your working directory. -.. note:: +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 + + 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: + + ... + +Verify that the ``name`` is ``fastapi-hello-world``. - 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. +Ensure that ``platforms`` includes the architecture of your host. Check +the architecture of your system: -Pack the rock: +.. code-block:: bash + + dpkg --print-architecture + +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: .. code-block:: bash @@ -228,27 +199,24 @@ Pack the rock: .. note:: - Depending on your system and network, this step can take a couple of minutes - to finish. - ``ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS`` is required while the FastAPI extension is experimental. -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: - -.. 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 FastAPI rock, +the terminal will respond with something similar to +``Packed fastapi-hello-world_0.1_amd64.rock``. .. note:: - 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. + 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 so that it can be deployed -in the Kubernetes cluster: +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 @@ -256,26 +224,37 @@ in the Kubernetes cluster: oci-archive:fastapi-hello-world_0.1_amd64.rock \ docker://localhost:32000/fastapi-hello-world:0.1 +.. seealso:: + + See more: `Ubuntu manpage | skopeo + `_ + Create the charm ---------------- -Create a new directory for the charm and go inside it: +From the ``fastapi-hello-world`` direcotyr, let's create a new directory +for the charm and change inside it: .. code-block:: bash mkdir charm cd charm -We'll need a ``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: +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. + +Initialize a charm named ``fastapi-hello-world``: .. code-block:: bash charmcraft init --profile fastapi-framework --name fastapi-hello-world +The files will automatically be created in your working directory. + The charm depends on several libraries. Download the libraries and pack the charm: .. code-block:: bash @@ -285,54 +264,69 @@ The charm depends on several libraries. Download the libraries and pack the char .. 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: +Depending on your system and network, this step may 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 fastapi-hello-world_ubuntu-24.04-amd64.charm``. .. note:: - If you changed the name in ``charmcraft.yaml`` 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 juju add-model fastapi-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. You can check the architecture of your system using +``dpkg --print-architecture``. - 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``. +Set the Juju model constraints using -Now the FastAPI application can be deployed using Juju: +.. code-block:: bash + + juju set-model-constraints -m fastapi-hello-world \ + arch=$(dpkg --print-architecture) + + +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: .. code-block:: bash juju deploy ./fastapi-hello-world_amd64.charm fastapi-hello-world \ --resource app-image=localhost:32000/fastapi-hello-world:0.1 -.. note:: +It will take a few minutes to deploy the FastAPI application. You can monitor +the progress using + +.. code:: bash + + juju status --watch 2s + - 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 @@ -350,9 +344,8 @@ 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 @@ -360,17 +353,19 @@ integrate it with the FastAPI app: juju integrate nginx-ingress-integrator fastapi-hello-world 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 juju config nginx-ingress-integrator \ service-hostname=fastapi-hello-world path-routes=/ -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. +Monitor ``juju status`` until everything has a status of ``active``. + +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,10 +377,11 @@ 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 @@ -399,19 +395,47 @@ following code into ``app.py``: 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: +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: 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 run the pack and upload commands for the rock: .. code-block:: bash + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack 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 ``charmcraft.yaml`` which -will be passed as environment variables to the FastAPI application. Add the +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: .. code-block:: yaml @@ -426,50 +450,49 @@ following to the end of the ``charmcraft.yaml`` file: .. 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 + CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack juju refresh fastapi-hello-world \ --path=./fastapi-hello-world_amd64.charm \ --resource app-image=localhost:32000/fastapi-hello-world:0.2 -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. +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. .. 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: +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. + +Now let's change the greeting: .. 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:: - - It may take a short time for the configuration to take effect. - +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,13 +500,15 @@ 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 ..``, +open the ``migrate.py`` file using a text editor and paste the +following code into it: .. code-block:: python @@ -513,14 +538,37 @@ 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``. +Increment the ``version`` in ``rockcraft.yaml`` to ``0.3`` such that the +top of the ``rockcraft.yaml`` file looks similar to the following: -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:: yaml + :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: .. code-block:: python @@ -557,19 +605,20 @@ its contents with the following code: return {"count": total_visitors} -Run ``ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack`` and upload -the newly created rock to the MicroK8s registry: +Let's run the pack and upload commands for the rock: .. code-block:: bash + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack 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 +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. Go back into the charm directory using ``cd charm``. -Open ``charmcraft.yaml`` in a text editor and add the following section at the -end of the file: +``charmcraft.yaml`` file. Open ``charmcraft.yaml`` in a text editor and +add the following section to the end: .. code-block:: yaml @@ -578,30 +627,32 @@ end of the file: interface: postgresql_client optional: false -Pack the charm using ``CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack`` -and refresh the deployment using Juju: +We can now pack and deploy the new version of the FastAPI app: .. code-block:: bash + CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack 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``: +Now let’s deploy PostgreSQL and integrate it with the FastAPI application: .. 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. +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: +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 @@ -614,11 +665,20 @@ 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: + +- Creating a FastAPI application +- Deploying the application locally +- Building an OCI image using Rockcraft +- Packaging the application 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 root directory for the tutorial: +in the rock directory ``fastapi-hello-world`` for the tutorial: .. code-block:: bash @@ -632,8 +692,7 @@ in the root directory for the tutorial: # Remove the juju model juju destroy-model fastapi-hello-world --destroy-storage -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 @@ -651,12 +710,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..58587dddb --- /dev/null +++ b/docs/tutorial/write-your-first-kubernetes-charm-for-a-flask-app.rst @@ -0,0 +1,644 @@ +.. _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. +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 +-------------- + +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 change 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, copy the following text into it +and then save it: + +.. literalinclude:: code/flask/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, copy and save the following into a text file +called ``app.py``: + +.. 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 + + 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 an ``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:: + + See more: `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. +You can check the architecture of your system using +``dpkg --print-architecture``. + +Set the Juju model constraints using + +.. 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 the +progress using + +.. 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.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 + +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 run the pack and upload commands for 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. + +.. 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. + +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 ..``, +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 + 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 run the pack and upload commands for 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 +- Building an OCI image using Rockcraft +- Packaging the application 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 4edf10e94..ed2877ce4 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,242 +1,220 @@ .. _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. +Let's get started! + +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. + +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 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 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 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. -- 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 - `_ - - -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 + Should you get stuck or notice issues, please get in touch on + `Matrix `_ or + `Discourse `_ -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 - - sudo snap install lxd lxd init --auto - -In order to create the charm, you'll need to install Charmcraft: +Set things up +------------- -.. code-block:: bash - - sudo snap install charmcraft --channel latest/edge --classic +.. include:: /reuse/tutorial/setup_edge.rst +.. |12FactorApp| replace:: Go -MicroK8s is required to deploy the FastAPI application on Kubernetes. Install MicroK8s: +Finally, let's create a new directory for this tutorial and +change into it: .. code-block:: bash - 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 - - 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: - -.. 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 - - 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. - + mkdir go-hello-world + cd go-hello-world Create the Go application ------------------------- -Start by creating the "Hello, world" Go application that will be used for this tutorial. +Start by creating the "Hello, world" Go application that will be +used for this tutorial. -Install ``go`` and initialise the Go module: +Install ``go`` and initialize the Go module: .. code-block:: bash - sudo snap install go --classic go mod init go-hello-world + sudo snap install go --classic + go mod init go-hello-world -Create a ``main.go`` file, copy the following text into it and then save it: +Create a ``main.go`` file, copy the following text into it and then +save it: .. code-block:: python package main import ( - "fmt" "log" "net/http" + "fmt" + "log" + "net/http" ) func helloWorldHandler(w http.ResponseWriter, req *http.Request) { - log.Printf("new hello world request") fmt.Fprintln(w, "Hello, world!") + 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) + log.Printf("starting hello world application") + http.HandleFunc("/", helloWorldHandler) + http.ListenAndServe(":8080", nil) } Run the Go application locally ------------------------------ -Build the Go application so it can be run: +First, we need to build the Go application so it can run: .. code-block:: bash go build . -Now that we have a binary compiled, let's run the Go application to verify that it -works: +Now that we have a binary compiled, let's run the Go application to verify +that it works: .. 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: +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: .. 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`. +The Go application should respond with ``Hello, world!``. + +The Go application looks good, so we can stop it for now from the +original terminal using :kbd:`Ctrl` + :kbd:`C`. Pack the Go application into a rock ----------------------------------- -First, we'll need a ``rockcraft.yaml`` file. Rockcraft will automate its creation and -tailoring for a Go application using the ``go-framework`` profile. +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:: bash rockcraft init --profile go-framework -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``. +The ``rockcraft.yaml`` file will automatically be created and set the name +based on your working directory. -.. note:: +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-block:: yaml + + 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: - 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. + ... -Pack the rock: +Verfiy that the ``name`` is ``go-hello-world``. + +Ensure that ``platforms`` includes the architecture of your host. Check +the architecture of your system: .. code-block:: bash - ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack + dpkg --print-architecture -.. note:: - Depending on your system and network, this step can take a couple of minutes to - finish. +If your host uses the ARM architecture, open ``rockcraft.yaml`` in a +text editor and include ``arm64`` in ``platforms``. -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: +Now let's pack the rock: .. code-block:: bash - ls *.rock -l + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack .. note:: - 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. + ``ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS`` is required while the Go + extension is experimental. -The rock needs to be copied to the Microk8s registry so that it can be deployed in the -Kubernetes cluster: +Depending on your system and network, this step can take several +minutes to finish. + +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:: + + If you are not on an ``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: .. code-block:: bash @@ -244,19 +222,29 @@ Kubernetes cluster: oci-archive:go-hello-world_0.1_amd64.rock \ docker://localhost:32000/go-hello-world:0.1 +.. seealso:: + + See more: `Ubuntu manpage | skopeo + `_ Create the charm ---------------- -Create a new directory for the charm and go inside it: +From the ``go-hello-world`` directory, let's create a new directory +for the charm and change inside it: .. code-block:: bash - mkdir charm cd charm + mkdir charm + cd charm + +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. -We'll need a ``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: +Initialize a charm named ``go-hello-world``: .. code-block:: bash @@ -273,58 +261,76 @@ The charm depends on several libraries. Download the libraries and pack the char .. 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 name in ``charmcraft.yaml`` 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 juju add-model go-hello-world -.. note:: +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. +You can check the architecture of your system using +``dpkg --print-architecture``. - 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``. +Set the Juju model constraints using -Now the Go application can be deployed using Juju: +.. code-block:: bash + + juju set-model-constraints -m go-hello-world \ + arch=$(dpkg --print-architecture) + +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 juju deploy ./go-hello-world_amd64.charm \ go-hello-world \ --resource app-image=localhost:32000/go-hello-world:0.1 -.. note:: +It will take a few minutes to deploy the Go application. You can monitor the +progress using + +.. code:: bash + + juju status --watch 2s - 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`. -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: +.. seealso:: + + See more: :external+juju:ref:`Juju | juju status ` + +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: .. terminal:: :input: juju status + Model Controller Cloud/Region Version SLA Timestamp go-hello-world microk8s microk8s/localhost 3.5.4 unsupported 14:35:07+02:00 App Version Status Scale Charm Channel Rev Address @@ -334,17 +340,16 @@ 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 -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: +Let's expose the application using ingress. Deploy the +``nginx-ingress-integrator`` charm and integrate it with the Go app: .. code-block:: bash - juju deploy nginx-ingress-integrator --trust juju integrate nginx-ingress-integrator - go-hello-world + juju deploy nginx-ingress-integrator --trust + juju integrate nginx-ingress-integrator go-hello-world -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: +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 @@ -357,52 +362,95 @@ 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~``. +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. It should return the +``Hello, world!`` greeting. + +.. note:: + + 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. 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``: +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 copy the following code into ``main.go``: .. code-block:: c package main import ( - "fmt" "log" "os" "net/http" + "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) + 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) + log.Printf("starting hello world application") + http.HandleFunc("/", helloWorldHandler) + http.ListenAndServe(":8080", nil) } -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. +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: 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 run the pack and upload commands for the rock: .. code-block:: bash + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack 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 -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 +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: .. code-block:: yaml @@ -412,25 +460,28 @@ environment variables to the Go application. Add the following to the end of the greeting: description: | The greeting to be returned by the Go application. - default: "Hello, world!" type: string + default: "Hello, world!" + type: string .. 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 Go app: .. code-block:: bash + CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack juju refresh go-hello-world \ --path=./go-hello-world_amd64.charm \ --resource app-image=localhost:32000/go-hello-world:0.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:: @@ -438,56 +489,53 @@ which should show the configuration option. 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: +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. + +Now let's change the greeting: .. 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. - -.. note:: - - It might take a short time for the configuration to take effect. - +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);" + 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);" .. 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: @@ -495,11 +543,12 @@ Change the permissions of the file ``migrate.sh`` so that it is executable: 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, update the -version to ``0.3`` 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 and add the following to the +end of the file: .. code-block:: yaml @@ -511,9 +560,38 @@ version to ``0.3`` and add the following to the end of the file: 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 +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: 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 @@ -523,27 +601,35 @@ replace its content with the following code: package main import ( - "database/sql" "fmt" "log" "net/http" "os" "time" + "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 + 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 { + 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, found := os.LookupEnv("APP_GREETING") + if !found { greeting = "Hello, world!" } @@ -551,90 +637,111 @@ replace its content with the following code: } 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 { + log.Printf("visitors request") + postgresqlURL := os.Getenv("POSTGRESQL_DB_CONNECT_STRING") + db, err := sql.Open("pgx", postgresqlURL) + if err != nil { return - } defer db.Close() + } + defer db.Close() - var numVisitors int err = db.QueryRow("SELECT count(*) from - visitors").Scan(&numVisitors) if err != nil { + 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) + } + fmt.Fprintf(w, "Number of visitors %d\n", numVisitors) } func main() { - log.Printf("starting hello world application") http.HandleFunc("/", - helloWorldHandler) http.HandleFunc("/visitors", visitorsHandler) + 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: +Check all the packages and their dependencies in the Go project with the +following command: .. code-block:: bash go mod tidy -Run ``ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack`` and upload the -newly created rock to the MicroK8s registry: +Let's run the pack and upload commands for the rock: .. code-block:: bash + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack 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 -Go back into the charm directory using ``cd charm``. 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: +Change back into the charm directory using ``cd charm``. + +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 requires: postgresql: - interface: postgresql_client optional: false + interface: postgresql_client + optional: false -Pack the charm using ``CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack`` -and refresh the deployment using Juju: +We can now pack and deploy the new version of the Go app: .. code-block:: bash + CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true charmcraft pack juju refresh go-hello-world \ - --path=./go-hello-world_amd64.charm \ --resource - app-image=localhost:32000/go-hello-world:0.3 + --path=./go-hello-world_amd64.charm \ + --resource app-image=localhost:32000/go-hello-world:0.3 -Deploy ``postgresql-k8s`` using Juju and integrate it with ``go-hello-world``: +Now let’s deploy PostgreSQL and integrate it with the Go application: .. code-block:: bash - juju deploy postgresql-k8s --trust juju integrate go-hello-world postgresql-k8s + juju deploy postgresql-k8s --trust + juju integrate go-hello-world postgresql-k8s -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. +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. -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: + +- Creating a Go application +- Deploying the application locally +- Building an OCI image using Rockcraft +- Packaging the application 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 root -directory for the tutorial: +If you'd like to reset your working environment, you can run the following +in the rock directory ``go-hello-world`` for the tutorial: .. code-block:: bash @@ -644,14 +751,13 @@ directory for the tutorial: go.mod go.sum # Remove the juju model juju destroy-model go-hello-world --destroy-storage -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 @@ -661,13 +767,26 @@ You can then proceed with its deletion: 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..089fea161 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