Getting started with COS Lite on Canonical Kubernetes

In this tutorial you deploy a single-node COS Lite appliance, backed by hostPath storage.

Prerequisites

  • A 4cpu8gb node or better, with at least 40Gi disk space (see Sizing guide for production deployments).

  • Juju v3.6 installed (doc).

  • Canonical Kubernetes (k8s snap) installed, with local-storage (doc) and load-balancer (doc) enabled.

  • Proxy (doc) and DNS (doc) for K8s are configured (if applicable).

  • K8s cloud added to Juju (doc).

Deploy COS Lite using Terraform

To deploy the COS Lite solution, create this root module and set the model to cos-lite:

terraform {
  required_version = ">= 1.5"
  required_providers {
    juju = {
      source  = "juju/juju"
      version = ">= 1.0"
    }
  }
}

variable "model" {
  type = string
}

data "juju_model" "model" {
  name  = var.model
  owner = "admin"
}

module "cos-lite" {
  source       = "git::https://github.com/canonical/observability-stack//terraform/cos-lite?ref=track/3.1"
  model        = { uuid = data.juju_model.model.uuid }
  risk         = "stable"
  internal_tls = true
}
Customize COS Lite

You can customize the deployment of COS Lite by overriding the defaults of these variables:

# We use constraints to set AntiAffinity in K8s
# https://discourse.charmhub.io/t/pod-priority-and-affinity-in-juju-charms/4091/13?u=jose

# FIXME: Passing an empty constraints value to the Juju Terraform provider currently
# causes the operation to fail due to https://github.com/juju/terraform-provider-juju/issues/344
# Therefore, we set a default value of "arch=amd64" for all applications.

variable "risk" {
  description = "Risk level that the applications are (unless overwritten by individual channels) deployed from"
  type        = string
  default     = "stable"
}

variable "model" {
  description = "Model configuration. When `uuid` is set, an existing model is looked up; otherwise a new model is created with the given fields. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/model"
  type = object({
    uuid = optional(string)
    name = optional(string, "cos-lite")
    cloud = optional(object({
      name   = string
      region = optional(string)
    }))
    annotations       = optional(map(string))
    config            = optional(map(string))
    constraints       = optional(string)
    credential        = optional(string)
    target_controller = optional(string)
  })
  default = {}

  validation {
    condition = var.model.uuid == null || (
      var.model.annotations == null &&
      var.model.cloud == null &&
      var.model.config == null &&
      var.model.constraints == null &&
      var.model.credential == null &&
      var.model.target_controller == null
    )
    error_message = "When `model.uuid` is set, the model already exists; do not also set `annotations`, `cloud`, `config`, `constraints`, `credential`, or `target_controller`."
  }

  validation {
    condition     = var.model.uuid != null || (var.model.name != null && length(var.model.name) > 0)
    error_message = "`model.name` must be non-empty when creating a model (i.e. when `model.uuid` is null)."
  }

  validation {
    condition     = var.model.uuid == null || can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", var.model.uuid))
    error_message = "`model.uuid` must be a valid UUID."
  }
}

# -------------- # Network configurations --------------

variable "internal_tls" {
  description = "Specify whether to use TLS or not for internal COS communication. By default, TLS is enabled using self-signed-certificates"
  type        = bool
  default     = true
}

variable "external_certificates_offer_url" {
  description = "A Juju offer URL (e.g. admin/external-ca.certificates) of a CA providing the 'tls_certificates' integration for Traefik to supply it with server certificates."
  type        = string
  default     = null

  validation {
    condition = (
      (var.external_certificates_offer_url == null && var.external_ca_cert_offer_url == null) ||
      (var.external_certificates_offer_url != null && var.external_ca_cert_offer_url != null)
    )
    error_message = "external_certificates_offer_url and external_ca_cert_offer_url must be supplied together (either both set or both null)."
  }
}

variable "external_ca_cert_offer_url" {
  description = "A Juju offer URL (e.g. admin/external-ca.send-ca-cert) of a CA providing the 'certificate_transfer' integration for applications to trust ingress via Traefik."
  type        = string
  default     = null
}

variable "postgresql_offer_url" {
  description = "A Juju offer URL (e.g. admin/postgresql.database) of a PostgreSQL service providing the 'postgresql_client' integration for applications to connect to the database."
  type        = string
  default     = null

  validation {
    condition     = !(var.postgresql_offer_url == null && var.grafana.units > 1)
    error_message = "postgresql_offer_url must be supplied when Grafana is scaled > 1 due to its database requirements."
  }
}

# -------------- # Ingress configurations --------------

variable "ingress" {
  description = "Per-component toggle for ingress integrations"
  type = object({
    alertmanager            = optional(bool, true)
    catalogue               = optional(bool, true)
    grafana                 = optional(bool, true)
    loki                    = optional(bool, true)
    opentelemetry_collector = optional(bool, true)
    prometheus              = optional(bool, true)
  })
  default = {}
}

# -------------- # Application configurations --------------

variable "alertmanager" {
  type = object({
    app_name           = optional(string, "alertmanager")
    config             = optional(map(string), {})
    constraints        = optional(string, "arch=amd64")
    resources          = optional(map(string), {})
    revision           = optional(number, null)
    storage_directives = optional(map(string), {})
    units              = optional(number, 1)
  })
  default     = {}
  description = "Application configuration for Alertmanager. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application"
}

variable "catalogue" {
  type = object({
    app_name           = optional(string, "catalogue")
    config             = optional(map(string), {})
    constraints        = optional(string, "arch=amd64")
    resources          = optional(map(string), {})
    revision           = optional(number, null)
    storage_directives = optional(map(string), {})
    units              = optional(number, 1)
  })
  default     = {}
  description = "Application configuration for Catalogue. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application"
}

variable "grafana" {
  type = object({
    app_name           = optional(string, "grafana")
    config             = optional(map(string), {})
    constraints        = optional(string, "arch=amd64")
    resources          = optional(map(string), {})
    revision           = optional(number, null)
    storage_directives = optional(map(string), {})
    units              = optional(number, 1)
  })
  default     = {}
  description = "Application configuration for Grafana. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application"
}

variable "loki" {
  type = object({
    app_name           = optional(string, "loki")
    config             = optional(map(string), {})
    constraints        = optional(string, "arch=amd64")
    resources          = optional(map(string), {})
    revision           = optional(number, null)
    storage_directives = optional(map(string), {})
    units              = optional(number, 1)
  })
  default     = {}
  description = "Application configuration for Loki. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application"
}

variable "opentelemetry_collector" {
  type = object({
    app_name           = optional(string, "otelcol")
    config             = optional(map(string), {})
    constraints        = optional(string, "arch=amd64")
    resources          = optional(map(string), {})
    revision           = optional(number, null)
    storage_directives = optional(map(string), {})
    units              = optional(number, 1)
  })
  default     = {}
  description = "Application configuration for OpenTelemetry Collector. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application"
}

variable "prometheus" {
  type = object({
    app_name           = optional(string, "prometheus")
    config             = optional(map(string), {})
    constraints        = optional(string, "arch=amd64")
    resources          = optional(map(string), {})
    revision           = optional(number, null)
    storage_directives = optional(map(string), {})
    units              = optional(number, 1)
  })
  default     = {}
  description = "Application configuration for Prometheus. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application"
}

variable "ssc" {
  type = object({
    app_name           = optional(string, "ca")
    config             = optional(map(string), {})
    constraints        = optional(string, "arch=amd64")
    revision           = optional(number, null)
    storage_directives = optional(map(string), {})
    units              = optional(number, 1)
  })
  default     = {}
  description = "Application configuration for self-signed-certificates. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application"
}

variable "traefik" {
  type = object({
    app_name           = optional(string, "traefik")
    config             = optional(map(string), {})
    constraints        = optional(string, "arch=amd64")
    resources          = optional(map(string), {})
    revision           = optional(number, null)
    storage_directives = optional(map(string), {})
    units              = optional(number, 1)
  })
  default     = {}
  description = "Application configuration for Traefik. For more details: https://registry.terraform.io/providers/juju/juju/latest/docs/resources/application"
}

Then, use terraform to deploy the module:

terraform init
terraform apply -var="model=cos-lite"

You can watch the model as it settles with:

juju status --relations --watch=5s

The status of your deployment should eventually be very similar to the following:

juju status --relations --storage
Model     Controller  Cloud/Region  Version  SLA          Timestamp
cos-lite  kub         k8s           3.6.21   unsupported  11:39:52+02:00

App           Version  Status  Scale  Charm                     Channel      Rev  Address         Exposed  Message
alertmanager  0.31.0   active      1  alertmanager-k8s          dev/edge     202  10.152.183.170  no
ca                     active      1  self-signed-certificates  1/edge       637  10.152.183.71   no
catalogue              active      1  catalogue-k8s             dev/edge     125  10.152.183.24   no
grafana       12.4.2   active      1  grafana-k8s               dev/edge     186  10.152.183.59   no
loki          3.7.1    active      1  loki-k8s                  dev/edge     226  10.152.183.29   no
prometheus    3.11.1   active      1  prometheus-k8s            dev/edge     292  10.152.183.104  no
traefik       2.11.0   active      1  traefik-k8s               latest/edge  292  10.152.183.108  no       Serving at http://192.168.178.192

Unit             Workload  Agent      Address     Ports  Message
alertmanager/0*  active    idle       10.1.0.207
ca/0*            active    idle       10.1.0.155
catalogue/0*     active    idle       10.1.0.193
grafana/0*       active    idle       10.1.0.229
loki/0*          active    idle       10.1.0.133
prometheus/0*    active    idle       10.1.0.83
traefik/0*       active    idle       10.1.0.93          Serving at http://192.168.178.192

Offer                            Application   Charm                     Rev  Connected  Endpoint              Interface                Role
alertmanager-karma-dashboard     alertmanager  alertmanager-k8s          202  0/0        karma-dashboard       karma_dashboard          provider
certificates                     ca            self-signed-certificates  637  0/0        certificates          tls-certificates         provider
grafana-dashboards               grafana       grafana-k8s               186  0/0        grafana-dashboard     grafana_dashboard        requirer
loki-logging                     loki          loki-k8s                  226  0/0        logging               loki_push_api            provider
prometheus-metrics-endpoint      prometheus    prometheus-k8s            292  0/0        metrics-endpoint      prometheus_scrape        requirer
prometheus-receive-remote-write  prometheus    prometheus-k8s            292  0/0        receive-remote-write  prometheus_remote_write  provider
send-ca-cert                     ca            self-signed-certificates  637  0/0        send-ca-cert          certificate_transfer     provider

Integration provider                Requirer                     Interface              Type     Message
alertmanager:alerting               loki:alertmanager            alertmanager_dispatch  regular
alertmanager:alerting               prometheus:alertmanager      alertmanager_dispatch  regular
alertmanager:grafana-dashboard      grafana:grafana-dashboard    grafana_dashboard      regular
alertmanager:grafana-source         grafana:grafana-source       grafana_datasource     regular
alertmanager:replicas               alertmanager:replicas        alertmanager_replica   peer
alertmanager:self-metrics-endpoint  prometheus:metrics-endpoint  prometheus_scrape      regular
ca:certificates                     alertmanager:certificates    tls-certificates       regular
ca:certificates                     catalogue:certificates       tls-certificates       regular
ca:certificates                     grafana:certificates         tls-certificates       regular
ca:certificates                     loki:certificates            tls-certificates       regular
ca:certificates                     prometheus:certificates      tls-certificates       regular
ca:send-ca-cert                     traefik:receive-ca-cert      certificate_transfer   regular
catalogue:catalogue                 alertmanager:catalogue       catalogue              regular
catalogue:catalogue                 grafana:catalogue            catalogue              regular
catalogue:catalogue                 prometheus:catalogue         catalogue              regular
catalogue:replicas                  catalogue:replicas           catalogue_replica      peer
grafana:grafana                     grafana:grafana              grafana_peers          peer
grafana:metrics-endpoint            prometheus:metrics-endpoint  prometheus_scrape      regular
grafana:replicas                    grafana:replicas             grafana_replicas       peer
loki:grafana-dashboard              grafana:grafana-dashboard    grafana_dashboard      regular
loki:grafana-source                 grafana:grafana-source       grafana_datasource     regular
loki:metrics-endpoint               prometheus:metrics-endpoint  prometheus_scrape      regular
loki:replicas                       loki:replicas                loki_replica           peer
prometheus:grafana-dashboard        grafana:grafana-dashboard    grafana_dashboard      regular
prometheus:grafana-source           grafana:grafana-source       grafana_datasource     regular
prometheus:prometheus-peers         prometheus:prometheus-peers  prometheus_peers       peer
traefik:ingress                     alertmanager:ingress         ingress                regular
traefik:ingress                     catalogue:ingress            ingress                regular
traefik:ingress                     grafana:ingress              ingress                regular
traefik:ingress-per-unit            loki:ingress                 ingress_per_unit       regular
traefik:ingress-per-unit            prometheus:ingress           ingress_per_unit       regular
traefik:metrics-endpoint            prometheus:metrics-endpoint  prometheus_scrape      regular
traefik:peers                       traefik:peers                traefik_peers          peer

Storage Unit    Storage ID                Type        Pool        Mountpoint                                      Size     Status    Message
alertmanager/0  data/8                    filesystem  kubernetes  /var/lib/juju/storage/data/0                    1.0 GiB  attached  Successfully provisioned volume pvc-cf00986d-5428-467b-89f9-a0788e996702
grafana/0       database/6                filesystem  kubernetes  /var/lib/juju/storage/database/0                1.0 GiB  attached  Successfully provisioned volume pvc-eddf2615-6147-4e78-8224-fcf69e8080c5
loki/0          active-index-directory/4  filesystem  kubernetes  /var/lib/juju/storage/active-index-directory/0  1.0 GiB  attached  Successfully provisioned volume pvc-3d7b9b8d-1d50-469c-b5ad-94201e517d41
loki/0          loki-chunks/5             filesystem  kubernetes  /var/lib/juju/storage/loki-chunks/0             1.0 GiB  attached  Successfully provisioned volume pvc-aa88c858-aa5f-4981-8330-7eb8245ea581
prometheus/0    database/3                filesystem  kubernetes  /var/lib/juju/storage/database/0                1.0 GiB  attached  Successfully provisioned volume pvc-e8ae59b8-e046-4041-9a44-9bde5b9950ae
traefik/0       configurations/2          filesystem  kubernetes  /var/lib/juju/storage/configurations/0          1.0 GiB  attached  Successfully provisioned volume pvc-29e9fd9a-b787-4056-9cd3-5135b6a1c442

Now COS Lite is good to go: you can relate software with it to begin the monitoring!

Obtain the Grafana admin password,

juju run grafana/leader get-admin-password

then head over to the URL listed in the output and use the provided password to log in.