================================================= 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: `Command '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...?" - `SDK How-to docs `_ * - "How do I debug?" - `Charm debugging tools `_ * - "How do I get in touch?" - `Matrix channel `_ * - "What is...?" - `SDK Reference docs `_ * - "Why...?", "So what?" - `SDK Explanation docs `_