Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add secrets config and vault secret actions #22

Merged
merged 25 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/integration_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ jobs:
with:
channel: 1.25-strict/stable
modules: '["test_charm.py", "test_scaling.py", "test_vault.py"]'
juju-channel: 3.3/stable
juju-channel: 3.4/stable
self-hosted-runner: false
microk8s-addons: "dns ingress rbac storage metallb:10.15.119.2-10.15.119.4 registry"
5 changes: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,15 @@ deployment, follow the following steps:
charmcraft pack

# Build ROCK file and push it to local registry:
cd resource_sample_py && make build_rock
make -C resource_sample_py build_rock

# Deploy the charm:
juju deploy ./temporal-worker-k8s_ubuntu-22.04-amd64.charm --resource temporal-worker-image=localhost:32000/temporal-worker-rock
juju config temporal-worker-k8s --file=path/to/config.yaml

# Refresh the charm after updating
juju refresh --path="./temporal-worker-k8s_ubuntu-22.04-amd64.charm" temporal-worker-k8s --force-units --resource temporal-worker-image=localhost:32000/temporal-worker-rock

# Check progress:
juju status --relations --watch 2s
juju debug-log
Expand Down
103 changes: 88 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ connect to a deployed Temporal server.

### Deploying

To deploy the Temporal Worker operator, you can start by creating a Temporal
To deploy the Charmed Temporal Worker, you can start by creating a Temporal
workflow, or use the one provided in
[`resource_sample_py`](./resource_sample_py/). Once done, the project can be
built as a [ROCK](https://documentation.ubuntu.com/rockcraft/en/stable/) and
pushed to the [local registry](https://microk8s.io/docs/registry-built-in) by
running the following command inside the `resource_sample_py` directory:

```bash
make build_rock
make -C resource_sample_py build_rock
```

The Temporal Worker operator can then be deployed and connected to a deployed
The Charmed Temporal Worker can then be deployed and connected to a deployed
Temporal server using the Juju command line as follows:

```bash
Expand Down Expand Up @@ -60,20 +60,93 @@ Note: The only requirement for the ROCK is to have a `scripts/start-worker.sh`
file, which will be used as the entry point for the charm to start the workload
container.

### Adding Environment Variables
### Adding Secrets & Environment Variables

The Temporal Worker operator can be used to inject environment variables that
can be ingested by your workflows. This can be done using the Juju command line
as follows:
The Charmed Temporal Worker allows the user to configure multiple sources of
environment variables and secrets to be injected into the workload container and
consumed by the user's workflow definitions. These sources can be configured
through the `secrets` config parameter of the charm. Below are the three sources
of environment variables and secrets currently supported. A user may choose to
use one, all or none of them. Once the `secrets.yaml` file is ready, it can be
configured into the charm as follows:

```bash
juju attach-resource temporal-worker-k8s env-file=path/to/.env
juju config temporal-worker-k8s secrets=@/path/to/secrets.yaml
```

#### **`.env`**
These secrets can then be injested by the workflows by using the `os` package as
kelkawi-a marked this conversation as resolved.
Show resolved Hide resolved
follows:

```python
import os
value1 = os.getenv("key1")
```
VALUE=123

#### Direct Environment Variables

These are usually values that are not secret and can be stored as plaintext. An
example is setting the application environment to `staging` or `production`.
They can be set as follows:

##### **`secrets.yaml`**

```yaml
secrets:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if we called it environment.yaml (since not all envs are secrets, but all secrets are envs)
what if we put env as root:

env:
    plain (or default):
     - key1: value1
    juju:
     ...
    vault:
     ...

or a more k8s-ish syntax

env: 
   - name: key1
     value: val1
   - name: key2
     valueFromSecret:
        type: juju
        id: secret-id
        key: key2 
   - name: key3
     valueFromSecret:
        type: vault
        id: secret-path
        key: key3      

or a more compact form of this:

env: 
   - name: key1
     value: val1
   - name: key2
     valueFromSecret: juju@secret-id:key2
   - name: key3
     valueFromSecret: vault@secret-path:key3 

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I went with the low impact change, as I'd like to avoid having the config look k8s-ish, and the compact form might have readability issues.

env:
- key1: value1
- key2: value2
```

#### Juju User Secrets (Requires Juju 3.3+)

[Juju secrets](https://juju.is/docs/juju/manage-secrets) are values which can be
stored in the model and accessed by the charm. To do so, you must first add the
secret and grant the charm access to it:

```bash
juju add-secret my-secret key1=value1 key2=value2

# Output: secret:<secret_id>

juju grant-secret my-secret temporal-worker-k8s
```

The secrets can then be configured into the charm as follows:

##### **`secrets.yaml`**

```yaml
secrets:
juju:
- secret-id: <secret_id>
key: key1
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: the key here matches both the key in the secret and the name of the environment variable right? I think there might be cases when that could be annoying because as a developer you are obliged to use an env that you might not know from the beginning

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand but yes the key in the secret must match the key that the user specified in their code. What would be an alternative?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a bit related to the convo Amber touched on. People who set secrets should in general be different from the ones who write the code. So in the code you know you need some environment variable, but you should be able to call it whatever you want (say MY_ENV). Similarly, the person who writes the secret, can put a whatever key (say my_secret). Only at the moment of the deployment, you should say: load my_secret value into MY_ENV. We should not assume that the programmer knows that the key is my_secret and do os.getenv('my_secret') from the beginning.

Copy link
Collaborator Author

@kelkawi-a kelkawi-a Jul 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. So for juju and Vault, I guess we will need to add an optional key of what the env variable name should be? Something like:

juju:
   - secret-id: <secret_id>
     src-key: key
     dest-key: MY_ENV

Or it could be (I like this more):

juju:
   - secret-id: <secret_id>
     key-name: MY_ENV
     from-key: key

Any suggestions?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is fine, but then i think you would need to keep the env consistent:

env:
   - name: MY_ENV1
     value: bla bla
juju:
   - name: MY_ENV2
     secret-id: secret-id
     key: key2
vault:
   - name: MY_ENV3
     secret-path: secret-path
     key: key3

gtato marked this conversation as resolved.
Show resolved Hide resolved
- secret-id: <secret_id>
key: key2
```

#### Vault

The Vault section below outlines how the Charmed Temporal Worker can be related
to the [Vault operator charm](https://charmhub.io/vault-k8s) for storing secrets
securely. Once done, the charm can be configured to fetch secrets from Vault and
inject them as variables into the workload container. The secrets can be
configured into the charm as follows:

##### **`secrets.yaml`**

```yaml
secrets:
vault:
- path: my-secrets
key: key1
- path: my-secrets
key: key2
```

These secrets can then be added to Vault by running the following charm action:

```bash
juju run temporal-worker-k8s/leader add-vault-secret path="my-secrets" key="key1" value="value1"
gtato marked this conversation as resolved.
Show resolved Hide resolved
```

## Verifying
Expand Down Expand Up @@ -112,7 +185,7 @@ juju scale-application temporal-worker-k8s <num_of_replicas_required_replicas>

## Error Monitoring

The Temporal Worker operator has a built-in Sentry interceptor which can be used
The Charmed Temporal Worker has a built-in Sentry interceptor which can be used
to intercept and capture errors from the Temporal SDK. To enable it, run the
following commands:

Expand All @@ -124,7 +197,7 @@ juju config temporal-worker-k8s sentry-environment="staging"

## Observability

The Temporal Worker operator charm can be related to the
The Charmed Temporal Worker can be related to the
[Canonical Observability Stack](https://charmhub.io/topics/canonical-observability-stack)
in order to collect logs and telemetry. To deploy cos-lite and expose its
endpoints as offers, follow these steps:
Expand All @@ -151,19 +224,19 @@ juju relate temporal-worker-k8s admin/cos.prometheus
# Access grafana with username "admin" and password:
juju run grafana/0 -m cos get-admin-password --wait 1m
# Grafana is listening on port 3000 of the app ip address.
# Dashboard can be accessed under "Temporal Worker SDK Metrics", make sure to select the juju model which contains your Temporal worker operator charm.
# Dashboard can be accessed under "Temporal Worker SDK Metrics", make sure to select the juju model which contains your Charmed Temporal Worker.
```

## Vault

The Temporal Worker operator charm can be related to the
The Charmed Temporal Worker can be related to the
[Vault operator charm](https://charmhub.io/vault-k8s) to securely store
credentials that can be accessed by workflows. This is the recommended way of
storing workflow-related credentials in production environments. To enable this,
run the following commands:

```bash
juju deploy vault-k8s --channel 1.15/edge
juju deploy vault-k8s --channel 1.16/edge

# After following Vault doc instructions to unseal Vault
juju relate temporal-worker-k8s vault-k8s
Expand Down
30 changes: 30 additions & 0 deletions actions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,33 @@

restart:
description: Restart the Temporal worker.

add-vault-secret:
description: |
Creates a secret in Vault.

If a secret already exists at the same path, it either
updates it if it's an existing key or appends it if it's
a new one.
params:
path:
description: The path to create the secret in.
type: string
key:
description: The key to create.
type: string
value:
description: The value to create.
type: string
required: [path, key, value]

get-vault-secret:
description: Reads a secret from Vault.
params:
path:
description: The path to create the secret in.
type: string
key:
description: The key to create.
type: string
required: [path, key]
36 changes: 36 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,39 @@ options:
description: Client certificate URL for OIDC authentication.
default: ""
type: string

secrets:
description: |
This configuration is used to manage and retrieve sensitive information required
by the application from different sources. The `secrets` configuration supports
the following sources:

- **Environment Variables**: Secrets can be provided directly as environment variables.
- **Juju**: Secrets can be managed and retrieved using Juju's secret storage capabilities.
- **Vault**: Secrets can be securely stored and accessed from a HashiCorp Vault instance.

The application will prioritize these sources in the following order: Vault, Juju,
and then environment variables. If a secret is not found in the higher priority
sources, it will fallback to the next available source. This ensures that the
application can function correctly in various deployment scenarios while maintaining
security and flexibility.

Sample structure:

```yaml
secrets:
env:
- key1: value1
- key2: value2
juju:
- secret-id: <secret_id>
key: sensitive1
- secret-id: <secret_id>
key: sensitive2
vault:
- path: my-secrets
key: key1
- path: my-secrets
key: key2
```
type: string
11 changes: 6 additions & 5 deletions metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ issues: https://github.com/canonical/temporal-worker-k8s-operator/issues
assumes:
- k8s-api

storage:
certs:
type: filesystem
minimum-size: 5M

peers:
peer:
interface: temporal
Expand All @@ -39,11 +44,7 @@ containers:
resources:
temporal-worker-image:
type: oci-image
description: OCI image containing Python package.
env-file:
type: file
description: .env file containing environment variables to be sourced to the workload container.
filename: '.env'
description: OCI image containing Temporal worker definition.

provides:
metrics-endpoint:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ profile = "black"
# Linting tools configuration
[tool.flake8]
max-line-length = 120
max-doc-length = 99
max-doc-length = 120
max-complexity = 10
exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
select = ["E", "W", "F", "C", "N", "R", "D", "H"]
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ ops==2.14.0
cosl==0.0.6
python-dotenv==1.0.0
pytest-interface-tester==2.0.1
hvac==2.3.0
3 changes: 1 addition & 2 deletions resource_sample_py/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ protobuf = "^3.2.0"
PyYAML = "^6.0"
temporal-lib-py = "^1.3.1"
python-json-logger = "^2.0.4"
urllib3 = "1.26.16"
hvac = "2.2.0"
urllib3 = "^1.26.16"

[tool.poetry.group.dev.dependencies]
pytest = "^7.1.3"
Expand Down
6 changes: 5 additions & 1 deletion resource_sample_py/resource_sample/activities/activity1.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@

from common.messages import ComposeGreetingInput
from temporalio import activity
import os


# Basic activity that logs and does string concatenation
@activity.defn(name="compose_greeting")
async def compose_greeting(arg: ComposeGreetingInput) -> str:
activity.logger.info("Running activity with parameter %s" % arg)
return f"{arg.greeting}, {arg.name}!"
env_var = os.getenv("message")
juju_secret1 = os.getenv("juju-secret1")

return f"{env_var} {juju_secret1}"
35 changes: 4 additions & 31 deletions resource_sample_py/resource_sample/activities/activity2.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,15 @@

import os

import hvac
from common.messages import ComposeGreetingInput
from temporalio import activity

vault_client = None
if os.getenv("TWC_VAULT_ADDR"):
vault_client = hvac.Client(
url=os.getenv("TWC_VAULT_ADDR"),
verify=os.getenv("TWC_VAULT_CERT_PATH"),
)

vault_client.auth.approle.login(
role_id=os.getenv("TWC_VAULT_ROLE_ID"),
secret_id=os.getenv("TWC_VAULT_ROLE_SECRET_ID"),
)


# Basic activity that logs and does string concatenation
@activity.defn(name="vault_test")
async def vault_test(arg: ComposeGreetingInput) -> str:
activity.logger.info("Running activity with parameter %s" % arg)

hvac_secret = {
"greeting": arg.greeting,
}

vault_client.secrets.kv.v2.create_or_update_secret(
path="credentials",
mount_point=os.getenv("TWC_VAULT_MOUNT"),
secret=hvac_secret,
)

read_secret_result = vault_client.secrets.kv.v2.read_secret(
path="credentials",
mount_point=os.getenv("TWC_VAULT_MOUNT"),
)

greeting = read_secret_result["data"]["data"]["greeting"]
return f"{greeting}, {arg.name}!"
sensitive1 = os.getenv("vault-secret1")
sensitive2 = os.getenv("vault-secret2")

return f"{sensitive1} {sensitive2}"
Loading
Loading