Skip to content

Commit

Permalink
Support Injecting Secrets into Apps Running in the Cloud
Browse files Browse the repository at this point in the history
Adds a new '--secret' flag to 'lightning run app':

lightning run app --cloud --secret MY_SECRET=my-secret-name app.py

When the Lightning App runs in the cloud, the 'MY_SECRET'
environment variable will be populated with the value of the
referenced Secret. The value of the Secret is encrypted in the
database, and will only be decrypted and accessible to the
Flow/Work processes in the cloud.
  • Loading branch information
alecmerdler committed Sep 14, 2022
1 parent c2378bd commit abfd373
Show file tree
Hide file tree
Showing 11 changed files with 201 additions and 26 deletions.
8 changes: 4 additions & 4 deletions docs/source-app/glossary/environment_variables.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@
Environment Variables
*********************

If your app is using secrets or values you don't want to expose in your app code such as API keys or access tokens, you can use environment variables.
If your App is using configuration values you don't want to commit with your App source code, you can use environment variables.

Lightning allows you to set environment variables when running the app from the CLI with the `lightning run app` command. You can use environment variables to pass any value such as API keys or other similar configurations to the app, avoiding having to stick them in the source code.
Lightning allows you to set environment variables when running the App from the CLI with the `lightning run app` command. You can use environment variables to pass any values to the App, and avoiding sticking those values in the source code.

Set one or multiple variables using the **--env** option:

.. code:: bash
lightning run app app.py --cloud --env FOO=BAR --env BAZ=FAZ
The environment variables are available in all flows and works, and can be accessed as follows:
Environment variables are available in all Flows and Works, and can be accessed as follows:

.. code:: python
Expand All @@ -24,4 +24,4 @@ The environment variables are available in all flows and works, and can be acces
print(os.environ["BAZ"]) # FAZ
.. note::
Environment variables are currently not encrypted.
Environment variables are not encrypted. For sensitive values, we recommend using :ref:`Encrypted Secrets <secrets>`.
57 changes: 57 additions & 0 deletions docs/source-app/glossary/secrets.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
.. _secrets:

#################
Encrypted Secrets
#################

We understand that many Apps require access to private data like API keys, access tokens, database passwords, or other credentials. And that you need to protect this data.

Secrets provie a secure way to make private data like API keys or passwords accessible to your app, without hardcoding. You can use secrets to authenticate third-party services/solutions.

.. tip::
For non-sensitive configuration values, we recommend using :ref:`plain-text Environment Variables <environment_variables>`.

*******************
Overview of Secrets
*******************

The ``--secret`` option has been added to the **lightning run app** command. ``--secret`` can be used by itself or alongside ``--env``.

When a Lightning App (App) **runs in the cloud**, the Secret can be exposed to the App using environment variables.
The value of the Secret is encrypted in the Lightning.ai database, and is only decrypted and accessible to
LightningFlow (Flow) or LightningWork (Work) processes in the cloud (when you use the ``--cloud`` option running your App).

----

*********************
Use Encrypted Secrets
*********************

First, a Secret must be created using the admin web UI. Once you create a Secret, you can bind it to any of your Apps. You do not need to create a new Secret for each App if the Secret value is the same.

.. note::
Secret names must start with a letter and can only contain letters, numbers, dashes, and periods. The Secret names must comply with `RFC1123 naming conventions <https://www.rfc-editor.org/rfc/rfc1123>`_. The Secret value has no restrictions.

In the example below, we already used the admin UI to create a Secret named ``my-secret`` with the value ``some-value``` and will bind it to the environment variable ``MY_APP_SECRET`` within our App. The binding is accomplished by using the ``--secret`` option when running the App from the Lightning CLI.

The ``--secret``` option works similar to ``--env``, but instead of providing a value, you provide the name of the Secret which contains the value that you want to bind to the environment variable:

.. code:: bash
lightning run app app.py --cloud --secret MY_APP_SECRET=my-secret
The environment variables are available in all Flows and Works, and can be accessed as follows:

.. code:: python
import os
print(os.environ["MY_APP_SECRET"])
The code above will print out ``some-value``.

The ``--secret`` option can be used for multiple Secrets, and alongside the ``--env`` option:

.. code:: bash
lightning run app app.py --cloud --env FOO=bar --secret MY_APP_SECRET=my-secret --secret ANOTHER_SECRET=another-secret
1 change: 1 addition & 0 deletions docs/source-app/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ Keep Learning
DAG <glossary/dag>
Event Loop <glossary/event_loop>
Environment Variables <glossary/environment_variables>
Encrypted Secrets <glossary/secrets>
Frontend <workflows/add_web_ui/glossary_front_end.rst>
Apple and Android mobile devices with Lighting Apps <glossary/ios_and_android>
REST API <glossary/restful_api/restful_api>
Expand Down
2 changes: 1 addition & 1 deletion requirements/app/base.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
lightning-cloud==0.5.3
lightning-cloud==0.5.4
packaging
deepdiff>=5.7.0, <=5.8.1
starsessions>=1.2.1, <2.0 # strict
Expand Down
15 changes: 2 additions & 13 deletions src/lightning_app/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,39 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).


## [UnReleased] - 2022-MM-DD

### Added

- Add `load_state_dict` and `state_dict` ([#14100](https://github.com/Lightning-AI/lightning/pull/14100))

- Add `--secret` option to CLI to allow binding Secrets to app environment variables when running in the cloud ([#14612](https://github.com/Lightning-AI/lightning/pull/14612))

### Changed

- Application storage prefix moved from `app_id` to `project_id/app_id` ([#14583](https://github.com/Lightning-AI/lightning/pull/14583))
- LightningCloud client calls to use key word arguments instead of positional arguments ([#14685](https://github.com/Lightning-AI/lightning/pull/14685)


- Improve Lightning App connect logic by disconnecting automatically ([#14532](https://github.com/Lightning-AI/lightning/pull/14532))


### Fixed

- Resolved a bug where the state change detection using DeepDiff won't worked with Path, Drive objects ([#14465](https://github.com/Lightning-AI/lightning/pull/14465))

- Resolved a bug where the wrong client was passed to collect cloud logs ([#14684](https://github.com/Lightning-AI/lightning/pull/14684))


- Unification of app template: moved `app.py` to root dir for `lightning init app <app_name>` template ([#13853](https://github.com/Lightning-AI/lightning/pull/13853))



- Fixed a bug where the uploaded command file wasn't properly parsed ([#14532](https://github.com/Lightning-AI/lightning/pull/14532))


- Resolved `LightningApp(..., debug=True)` ([#14464](https://github.com/Lightning-AI/lightning/pull/14464))


## [0.6.0] - 2022-09-08

### Added

- Introduce lightning connect ([#14452](https://github.com/Lightning-AI/lightning/pull/14452))
- Adds `PanelFrontend` to easily create complex UI in Python ([#13531](https://github.com/Lightning-AI/lightning/pull/13531))
- Add support for `Lightning App Commands` through the `configure_commands` hook on the Lightning Flow and the `ClientCommand` ([#13602](https://github.com/Lightning-AI/lightning/pull/13602))
- Add support for `Lightning App Commands` through the `configure_commands` hook on the Lightning Flow and the `ClientCommand` ([#13602](https://github.com/Lightning-AI/lightning/pull/13602))
- Add support for Lightning AI BYOC cluster management ([#13835](https://github.com/Lightning-AI/lightning/pull/13835))
- Add support to see Lightning AI BYOC cluster logs ([#14334](https://github.com/Lightning-AI/lightning/pull/14334))
- Add support to run Lightning apps on Lightning AI BYOC clusters ([#13894](https://github.com/Lightning-AI/lightning/pull/13894))
Expand Down Expand Up @@ -80,14 +72,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
- Pinning starsessions to 1.x ([#14333](https://github.com/Lightning-AI/lightning/pull/14333))
- Parsed local package versions ([#13933](https://github.com/Lightning-AI/lightning/pull/13933))


## [0.5.6] - 2022-08-16

### Fixed

- Resolved a bug where the `install` command was not installing the latest version of an app/component by default ([#14181](https://github.com/Lightning-AI/lightning/pull/14181))


## [0.5.5] - 2022-08-9

### Deprecated
Expand All @@ -100,7 +90,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
- Resolved a bug about a race condition when sending the work state through the caller_queue ([#14074](https://github.com/Lightning-AI/lightning/pull/14074))
- Fixed Start Lightning App on Cloud if Repo Begins With Name "Lightning" ([#14025](https://github.com/Lightning-AI/lightning/pull/14025))


## [0.5.4] - 2022-08-01

### Changed
Expand Down
17 changes: 14 additions & 3 deletions src/lightning_app/cli/lightning_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ def _run_app(
blocking: bool,
open_ui: bool,
env: tuple,
secret: tuple,
):
file = _prepare_file(file)

Expand All @@ -314,10 +315,17 @@ def _run_app(
"Caching is a property of apps running in cloud. "
"Using the flag --no-cache in local execution is not supported."
)
if secret:
raise click.ClickException(
"Secrets can only be used for apps running in cloud. "
"Using the option --secret in local execution is not supported."
)

env_vars = _format_input_env_variables(env)
os.environ.update(env_vars)

secrets = _format_input_env_variables(secret)

def on_before_run(*args, **kwargs):
if open_ui and not without_server:
click.launch(get_app_url(runtime_type, *args, **kwargs))
Expand All @@ -336,6 +344,7 @@ def on_before_run(*args, **kwargs):
on_before_run=on_before_run,
name=name,
env_vars=env_vars,
secrets=secrets,
cluster_id=cluster_id,
)
if runtime_type == RuntimeType.CLOUD:
Expand Down Expand Up @@ -371,7 +380,8 @@ def run():
default=True,
help="Decide whether to launch the app UI in a web browser",
)
@click.option("--env", type=str, default=[], multiple=True, help="Env variables to be set for the app.")
@click.option("--env", type=str, default=[], multiple=True, help="Environment variables to be set for the app.")
@click.option("--secret", type=str, default=[], multiple=True, help="Secret variables to be set for the app.")
@click.option("--app_args", type=str, default=[], multiple=True, help="Collection of arguments for the app.")
def run_app(
file: str,
Expand All @@ -383,10 +393,11 @@ def run_app(
blocking: bool,
open_ui: bool,
env: tuple,
app_args: List[str],
secret: tuple,
app_args: tuple,
):
"""Run an app from a file."""
_run_app(file, cloud, cluster_id, without_server, no_cache, name, blocking, open_ui, env)
_run_app(file, cloud, cluster_id, without_server, no_cache, name, blocking, open_ui, env, secret)


@_main.group(hidden=True)
Expand Down
11 changes: 10 additions & 1 deletion src/lightning_app/runners/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
from lightning_app.utilities.dependency_caching import get_hash
from lightning_app.utilities.packaging.app_config import AppConfig, find_config_file
from lightning_app.utilities.packaging.lightning_utils import _prepare_lightning_wheels_and_requirements
from lightning_app.utilities.secrets import _names_to_ids

logger = Logger(__name__)

Expand Down Expand Up @@ -98,8 +99,16 @@ def dispatch(

print(f"The name of the app is: {app_config.name}")

work_reqs: List[V1Work] = []
v1_env_vars = [V1EnvVar(name=k, value=v) for k, v in self.env_vars.items()]

if len(self.secrets.values()) > 0:
secret_names_to_ids = _names_to_ids(self.secrets.values())
env_vars_from_secrets = [
V1EnvVar(name=k, from_secret=secret_names_to_ids[v]) for k, v in self.secrets.items()
]
v1_env_vars.extend(env_vars_from_secrets)

work_reqs: List[V1Work] = []
for flow in self.app.flows:
for work in flow.works(recurse=False):
work_requirements = "\n".join(work.cloud_build_config.requirements)
Expand Down
16 changes: 14 additions & 2 deletions src/lightning_app/runners/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ def dispatch(
blocking: bool = True,
on_before_run: Optional[Callable] = None,
name: str = "",
env_vars: Dict[str, str] = {},
env_vars: Dict[str, str] = None,
secrets: Dict[str, str] = None,
cluster_id: str = None,
) -> Optional[Any]:
"""Bootstrap and dispatch the application to the target.
Expand All @@ -43,6 +44,7 @@ def dispatch(
on_before_run: Callable to be executed before run.
name: Name of app execution
env_vars: Dict of env variables to be set on the app
secrets: Dict of secrets to be passed as environment variables to the app
cluster_id: the Lightning AI cluster to run the app on. Defaults to managed Lightning AI cloud
"""
from lightning_app.runners.runtime_type import RuntimeType
Expand All @@ -54,11 +56,20 @@ def dispatch(
runtime_cls: Type[Runtime] = runtime_type.get_runtime()
app = load_app_from_file(str(entrypoint_file))

env_vars = {} if env_vars is None else env_vars
secrets = {} if secrets is None else secrets

if blocking:
app.stage = AppStage.BLOCKING

runtime = runtime_cls(
app=app, entrypoint_file=entrypoint_file, start_server=start_server, host=host, port=port, env_vars=env_vars
app=app,
entrypoint_file=entrypoint_file,
start_server=start_server,
host=host,
port=port,
env_vars=env_vars,
secrets=secrets,
)
# a cloud dispatcher will return the result while local
# dispatchers will be running the app in the main process
Expand All @@ -78,6 +89,7 @@ class Runtime:
done: bool = False
backend: Optional[Union[str, Backend]] = "multiprocessing"
env_vars: Dict[str, str] = field(default_factory=dict)
secrets: Dict[str, str] = field(default_factory=dict)

def __post_init__(self):
if isinstance(self.backend, str):
Expand Down
26 changes: 26 additions & 0 deletions src/lightning_app/utilities/secrets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import Dict, List

from lightning_app.utilities.cloud import _get_project
from lightning_app.utilities.network import LightningClient


def _names_to_ids(secret_names: List[str]) -> Dict[str, str]:
"""Returns the name/ID pair for each given Secret name.
Raises a `ValueError` if any of the given Secret names do not exist.
"""
lightning_client = LightningClient()

project = _get_project(lightning_client)
secrets = lightning_client.secret_service_list_secrets(project_id=project.project_id)

secret_names_to_ids: Dict[str, str] = {}
for secret in secrets.secrets:
if secret.name in secret_names:
secret_names_to_ids[secret.name] = secret.id

for secret_name in secret_names:
if secret_name not in secret_names_to_ids.keys():
raise ValueError(f"Secret with name '{secret_name}' not found")

return secret_names_to_ids
25 changes: 23 additions & 2 deletions tests/tests_app/cli/test_run_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ def _lightning_app_run_and_logging(self, *args, **kwargs):

with caplog.at_level(logging.INFO):
with mock.patch("lightning_app.LightningApp._run", _lightning_app_run_and_logging):

runner = CliRunner()
result = runner.invoke(
run_app,
Expand Down Expand Up @@ -70,6 +69,7 @@ def test_lightning_run_cluster_without_cloud(monkeypatch):
open_ui=False,
no_cache=True,
env=("FOO=bar",),
secret=(),
)


Expand All @@ -80,7 +80,7 @@ def test_lightning_run_app_cloud(mock_dispatch: mock.MagicMock, open_ui, caplog,
"""This test validates the command has ran properly when --cloud argument is passed.
It tests it by checking if the click.launch is called with the right url if --open-ui was true and also checks the
call to `dispatch` for the right arguments
call to `dispatch` for the right arguments.
"""
monkeypatch.setattr("lightning_app.runners.cloud.logger", logging.getLogger())

Expand All @@ -95,6 +95,7 @@ def test_lightning_run_app_cloud(mock_dispatch: mock.MagicMock, open_ui, caplog,
open_ui=open_ui,
no_cache=True,
env=("FOO=bar",),
secret=("BAR=my-secret",),
)
# capture logs.
# TODO(yurij): refactor the test, check if the actual HTTP request is being sent and that the proper admin
Expand All @@ -108,5 +109,25 @@ def test_lightning_run_app_cloud(mock_dispatch: mock.MagicMock, open_ui, caplog,
name="",
no_cache=True,
env_vars={"FOO": "bar"},
secrets={"BAR": "my-secret"},
cluster_id="",
)


def test_lightning_run_app_secrets(monkeypatch):
"""Validates that running apps only supports the `--secrets` argument if the `--cloud` argument is passed."""
monkeypatch.setattr("lightning_app.runners.cloud.logger", logging.getLogger())

with pytest.raises(click.exceptions.ClickException):
_run_app(
file=os.path.join(_PROJECT_ROOT, "tests/tests_app/core/scripts/app_metadata.py"),
cloud=False,
cluster_id="test-cluster",
without_server=False,
name="",
blocking=False,
open_ui=False,
no_cache=True,
env=(),
secret=("FOO=my-secret"),
)

0 comments on commit abfd373

Please sign in to comment.