How to build a 12-Factor app charm

See also: 12-Factor app charm

Initialise a charm for a 12-Factor application

Use charmcraft init and specify the relevant profile:

charmcraft init --profile <profile>

Charmcraft automatically creates charmcraft.yaml, requirements.txt and source code for the charm in your current directory. You will need to check charmcraft.yaml and README.md and verify that the charm’s name and description are correct.

See also: Command ‘charmcraft init’

Example: Flask application

Specify the flask-framework profile:

charmcraft init --profile flask-framework
Example: Django application

Specify the django-framework profile:

charmcraft init --profile django-framework
Example: FastAPI application

Specify the fastapi-framework profile:

charmcraft init --profile fastapi-framework
Example: Go application

Specify the go-framework profile:

charmcraft init --profile go-framework

Add a 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.

The configuration can be set on the deployed charm using juju config <app name> token=<token>.

See also: How to add a configuration to a charm, Configuration Handling – Flask Documentation^

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.

The configuration can be set on the deployed charm using juju config <app name> token=<token>.

See also: How to add a configuration to a charm

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 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>.

See also: How to add a configuration to a charm

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>.

See also: How to add a configuration to a charm

Add an integration

A charm integration can be added to your charmed 12-Factor app by providing the integration and endpoint definition in charmcraft.yaml:

requires:
  <endpoint name>:
    interface: <endpoint interface name>
    optional: false

Here, <endpoint name> corresponds to the endpoint of the application with which you want the integration, and <endpoint interface name> is the endpoint schema to which this relation conforms. Both the <endpoint name> and <endpoint interface name> must coincide with the structs defined in that particular application’s charm’s charmcraft.yaml file. The key optional with value False means that the charm will get blocked and stop the services if the integration is not provided.

You can provide the integration to your deployed 12-Factor app using juju integrate <12-Factor app charm> <endpoint name>. After the integration has been established, the connection string and other configuration options will be available as environment variables that you may use to configure your 12-Factor application.

For example, if you wish to integrate your 12-Factor application with PostgreSQL (machine or k8s charm), add the following endpoint definition to charmcraft.yaml:

requires:
  postgresql:
    interface: postgresql_client
    optional: True

Provide the integration to your deployed 12-Factor app with juju integrate <12-Factor app charm> postgresql. This integration creates the following environment variables you may use to configure your 12-Factor application:

  • POSTGRESQL_DB_CONNECT_STRING
  • POSTGRESQL_DB_SCHEME
  • POSTGRESQL_DB_NETLOC
  • POSTGRESQL_DB_PATH
  • POSTGRESQL_DB_PARAMS
  • POSTGRESQL_DB_QUERY
  • POSTGRESQL_DB_FRAGMENT
  • POSTGRESQL_DB_USERNAME
  • POSTGRESQL_DB_PASSWORD
  • POSTGRESQL_DB_HOSTNAME
  • POSTGRESQL_DB_PORT

See also: How to add an integration to a charm

Create an admin user for a Django charm

Use the create-superuser action to create a new Django admin account:

juju run <app name> create-superuser username=<username> email=<email>

You must provide the username and email address.

(If your workload depends on a database) Migrate the database

If your app depends on a database, it is common to run a database migration script before app startup which, for example, creates or modifies tables. This can be done by including the migrate.sh script in the root of your project. It will be executed with the same environment variables and context as the 12-Factor app.

If the migration script fails, it will retry upon update-status. The migration script will run on every unit. The script is assumed to be idempotent (i.e., can be run multiple times) and that it can be run on multiple units simultaneously without issue. Handling multiple migration scripts that run concurrently can be achieved by, for example, locking any tables during the migration.

Include extra files in the OCI image

The following files are included in the image by default from the root of the project:

  • app (does not apply to the go-framework)
  • app.py (does not apply to the go-framework)
  • migrate
  • migrate.sh
  • migrate.py (does not apply to the go-framework)
  • static
  • templates

To change this, the following snippet needs to be added to the rockcraft.yaml:

parts:
  flask-framework/install-app:
    prime:
      - flask/app/.env
      - flask/app/app.py
      - flask/app/webapp
      - flask/app/templates
      - flask/app/static

Note the flask/app/ prefix that is required followed by the relative path to the project root.

parts:
  fastapi-framework/install-app:
    prime:
      - app/.env
      - app/app.py
      - app/webapp
      - app/templates
      - app/static

Note the app/ prefix that is required followed by the relative path to the project root.

parts:
  go-framework/assets:
    prime:
      - app/templates
      - app/static
      - app/migrate.sh

Note the app/ prefix that is required followed by the relative path to the project root.

Include additional debs in the OCI image

If your app requires debs – for example, to connect to a database – add the following snippet to the rockcraft.yaml:

parts:
  flask-framework/dependencies:
    stage-packages:
      # list required packages or slices for your flask application below.
      - libpq-dev
parts:
  django-framework/dependencies:
    stage-packages:
      # list required packages or slices for your Django application below.
      - libpq-dev
parts:
  fastapi-framework/dependencies:
    stage-packages:
      # list required packages or slices for your FastAPI application below.
      - libpq-dev
parts:
  runtime-debs:
    plugin: nil
    stage-packages:
      - postgresql-client

For the go-framework, a deb could be needed for example to use an external command in the migration process.

Update the OCI image

After making a change to your app;

  1. make sure that any new files will be included in the new OCI image. See Including extra files in the OCI image
  2. Run rockcraft pack to create the new OCI image
  3. Run rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false oci-archive:<path to rock file> docker://localhost:32000/<rock name>:<rock version> to upload the OCI image to the registry
  4. Run juju refresh <app name> --path=<relative path to .charm file> --resource flask-app-image=<localhost:32000/<rock name>:<rock version>> to deploy the new OCI image

After making a change to your app;

  1. make sure that any new files will be included in the new OCI image. See Including extra files in the OCI image
  2. Run rockcraft pack to create the new OCI image
  3. Run rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false oci-archive:<path to rock file> docker://localhost:32000/<rock name>:<rock version> to upload the OCI image to the registry
  4. Run juju refresh <app name> --path=<relative path to .charm file> --resource django-app-image=<localhost:32000/<rock name>:<rock version>> to deploy the new OCI image

After making a change to your app;

  1. make sure that any new files will be included in the new OCI image. See Including extra files in the OCI image
  2. Run rockcraft pack to create the new OCI image
  3. Run rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false oci-archive:<path to rock file> docker://localhost:32000/<rock name>:<rock version> to upload the OCI image to the registry
  4. Run juju refresh <app name> --path=<relative path to .charm file> --resource app-image=<localhost:32000/<rock name>:<rock version>> to deploy the new OCI image

After making a change to your app;

  1. make sure that any new files will be included in the new OCI image. See Including extra files in the OCI image
  2. Run rockcraft pack to create the new OCI image
  3. Run rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false oci-archive:<path to rock file> docker://localhost:32000/<rock name>:<rock version> to upload the OCI image to the registry
  4. Run juju refresh <app name> --path=<relative path to .charm file> --resource app-image=<localhost:32000/<rock name>:<rock version>> to deploy the new OCI image

Add a Juju secret to the charm

A Juju 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 charmcraft.yaml:

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

The output from the previous command will look something like:

secret:cru00lvmp25c77qa0qrg

From the ouput of the previous command, 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:

  • FLASK_API_TOKEN_VALUE: “1234”
  • FLASK_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