Manage configurations for a 12-factor app charm¶
Add a new configuration¶
A charm configuration can be added if your 12-factor app
requires environment variables, for example, to pass a
token for a service. Add the configuration in charmcraft.yaml
:
config:
options:
token:
description: The token for the service.
type: string
A user-defined configuration option will correspond to
an environment variable generated by the paas-charm
project to expose the configuration to the Flask workload.
In general, a configuration option config-option-name
will be mapped as FLASK_CONFIG_OPTION_NAME
where the
option name will be converted to upper case, dashes will be
converted to underscores and the FLASK_
prefix will be
added. In the example above, the token
configuration will
be mapped as the FLASK_TOKEN
environment variable. In
addition to the environment variable, the configuration is
also available in the Flask variable app.config
without
the FLASK_
prefix.
A user-defined configuration option will correspond to an
environment variable generated by the paas-charm
project
to expose the configuration to the Django workload. In general,
a configuration option config-option-name
will be mapped as
DJANGO_CONFIG_OPTION_NAME
where the option name will be
converted to upper case, dashes will be converted to underscores
and the DJANGO_
prefix will be added. In the example above,
the token
configuration will be mapped as the DJANGO_TOKEN
environment variable.
A user-defined configuration option will correspond to an environment
variable generated by the paas-charm
project to expose the
configuration to the FastAPI workload. In general, a configuration option
called config-option-name
will be mapped as APP_CONFIG_OPTION_NAME
where the option name will be converted to upper case, dashes will be
converted to underscores and the APP_
prefix will be added. In the
example above, the token
configuration will be mapped as the
APP_TOKEN
environment variable.
A user-defined configuration option will correspond to an environment
variable generated by the paas-charm
project to expose the
configuration to the Go workload. In general, a configuration option
config-option-name
will be mapped as APP_CONFIG_OPTION_NAME
where the option name will be converted to upper case, dashes will be
converted to underscores and the APP_
prefix will be added. In the
example above, the token
configuration will be mapped as the
APP_TOKEN
environment variable.
The configuration can be set on the deployed charm using:
juju config <app name> token=<token>
Add a custom action¶
For commands and processes that see shared or frequent use, custom Juju actions
can expedite your workflow. Actions are handled in the Ops
library, and Juju manages the action’s workflow via tasks and operations. To
implement a custom action, you need to declare the action in
charmcraft.yaml
and add an event handler to the Ops framework of your
charm.
See also
The custom action must be defined in the project file under the
actions
section. You should provide a name, a description, and any
associated parameters.
Add the custom action to your charm code by modifying the src/charm.py
file. You need to add the action under the class constructor function
(__init__
) as well as a dedicated function for the action. When you define
the action, follow the convention of appending _action
onto the
action name.
Once your web app is updated and deployed with Juju, you can call the custom
action using juju run
.
Example: check Flask app status and write output to container file¶
As another example, let’s say you want to add a custom action to your charmed Flask app that checks the status of the running app and writes the status to a log file in the app container. The action performs the following steps:
Sends a request to the Flask app at its available port.
If the request is successful, updates the app container with the status message in a logfile.
As a consistency check, reads the contents of the log file in the app container and outputs the status message as part of the action result. In practice, you could create a separate action for reading the log file.
Add the custom action to the project file:
actions:
updatelogfile:
description: Checks the running app and updates the log file.
params:
logfile:
type: string
Add import requests
to the start of src/charm.py
, then define
your custom action as part of the class and provide the function definition:
def __init__(self, *args: typing.Any) -> None:
"""Initialize the instance.
Args:
args: passthrough to CharmBase.
"""
super().__init__(*args)
self.framework.observe(self.on.updatelogfile_action,
self._on_updatelogfile_action)
def _on_updatelogfile_action(self, event: ops.ActionEvent) -> None:
"""Handle the updatelogfile action.
Args:
event: the action event object.
logfile: the output logfile in the container
"""
if not self.is_ready():
event.fail("flask-app container is not ready")
try:
response = requests.get(
f"http://127.0.0.1:{self._workload_config.port}", timeout=5
)
response.raise_for_status()
# push response to file in app container
self._container.push(event.params["logfile"], response.text)
output = "App response: " + response.text
# access file in container and read its contents
# (you could put this part in a separate action called readlogfile)
output_comp = self._container.pull(event.params["logfile"]).read()
output += "Output written to file: " + output_comp
event.set_results({"result": output})
except ops.pebble.PathError as e:
event.fail(str(e.message))
except requests.exceptions.RequestException as e:
# if it failed with http bad status code or the connection failed
if e.response is None:
event.fail(
f"unable to connect on port {self._workload_config.port}"
)
else:
event.fail(
f"workload responded with code {e.response.status_code}"
)
Build the charm using charmcraft pack
and deploy the app with Juju.
Finally, call the action using:
juju run <flask unit name> updatelogfile logfile=<full path to logfile>
If successful, the terminal will output something like:
~$
juju run flask-app/0 updatelogfile logfile="/tmp/example.log"
Running operation 1 with 1 task
- task 2 on unit-flask-app-0
Waiting for task 2...
result: |-
App response: Hello, world!
Output written to file: Hello, world!
Warning
Writing to a file in the app container is unstable because the Juju units are ephemeral, meaning that container files are not persistent in the case of the unit’s restart or deletion.
Manage secrets¶
A user secret can be added to a charm and all the keys and values in the secret will be exposed as environment variables. Add the secret configuration option in your project file:
config:
options:
api-token:
type: secret
description: Secret needed to access some API secret information
Once the charm is deployed, you can add a juju secret to the model:
~$
juju add-secret my-api-token value=1234 othervalue=5678
secret:cru00lvmp25c77qa0qrg
From this output, you can get the Juju secret ID. Grant the application access to view the value of the secret:
juju grant-secret my-api-token <app name>
Add the Juju secret ID to the application:
juju config <app name> api-token=secret:cru00lvmp25c77qa0qrg
The following environment variables are available for the application:
APP_API_TOKEN_VALUE: "1234"
APP_API_TOKEN_OTHERVALUE: "5678"
See also: How to manage secrets
The following environment variables are available for the application:
DJANGO_API_TOKEN_VALUE: "1234"
DJANGO_API_TOKEN_OTHERVALUE: "5678"
See also: How to manage secrets
The following environment variables are available for the application:
APP_API_TOKEN_VALUE: "1234"
APP_API_TOKEN_OTHERVALUE: "5678"
See also: How to manage secrets
The following environment variables are available for the application:
APP_API_TOKEN_VALUE: "1234"
APP_API_TOKEN_OTHERVALUE: "5678"
See also: How to manage secrets
Write a Kubernetes charm for an async Flask app¶
In this how-to guide you will configure a 12-factor Flask application to use asynchronous Gunicorn workers to be able to serve to multiple users easily.
Make the rock async¶
To make the rock async, make sure to put the following in its requirements.txt
file:
Flask
gevent
Pack the rock using rockcraft pack
and redeploy the charm with the new rock using
juju refresh.
Configure the async application¶
Now let’s enable async Gunicorn workers. We will
expect this configuration option to be available in the Flask app configuration
under the webserver-worker-class
key. Verify that the new configuration
has been added by running:
juju config flask-async-app | grep -A 6 webserver-worker-class:
The result should contain the key.
The worker class can be changed using Juju:
juju config flask-async-app webserver-worker-class=gevent
Test that the workers are operating in parallel by sending multiple simultaneous requests with curl:
curl --parallel --parallel-immediate --resolve flask-async-app:80:127.0.0.1 \
http://flask-async-app/io http://flask-async-app/io http://flask-async-app/io \
http://flask-async-app/io http://flask-async-app/io
and they will all return at the same time.
The results should arrive simultaneously and contain five instances of ok
:
ok
ok
ok
ok
ok
It can take up to a minute for the configuration to take effect. When the configuration changes, the charm will re-enter the active state.