Claude
Skills
Sign in
Back

langchain-multi-env-setup

Included with Lifetime
$97 forever

Build reliable dev / staging / prod isolation for LangChain 1.0 services — Pydantic `Settings` + `SecretStr`, cloud Secret Manager in prod, per-env prompt and model version pinning, env-specific checkpointer and observability. Use when graduating from `.env`-in-dev to real prod infra, or debugging a config that loaded the wrong values in the wrong env. Trigger with "langchain multi-env", "langchain pydantic settings", "langchain secret manager", "langchain env config", "langchain prod setup".

Cloud & DevOpssaaslangchainlanggraphpythonlangchain-1.0configpydanticmulti-env

What this skill does

# LangChain Multi-Env Setup (Python)

## Overview

A team ships a LangChain 1.0 service to staging with `python-dotenv` loading
`.env.staging` into `os.environ`. Security audits —
`docker exec STAGING-POD env` prints `ANTHROPIC_API_KEY=sk-ant-api03-...` in
plain text. Anyone with `kubectl exec`, any sidecar, any core dump, any
error tracker that auto-captures process env sees the key. This is pain
**P37**: secrets loaded from `.env` in production containers leak via `env`.

A second failure chains. A developer runs the staging deploy from a shell
where `LANGCHAIN_ENV=production` was set hours earlier. The loader picks
the prod `.env`, staging answers with a prompt commit tuned only for the
prod model tier, latency doubles. Two root causes: no type-safe env gate,
no startup validation that would have caught the mismatched model id.

Both are one refactor:

```python
# BAD — dotenv populates os.environ; any process with container access sees it
from dotenv import load_dotenv
load_dotenv(".env.production")
api_key = os.environ["ANTHROPIC_API_KEY"]  # P37: leaks via `docker exec env`

# GOOD — SecretStr in a validated Settings object, pulled from Secret Manager
from pydantic import SecretStr
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    env: Literal["dev", "staging", "prod"]
    anthropic_api_key: SecretStr

settings = build_settings()  # pulls from GCP Secret Manager in prod
api_key = settings.anthropic_api_key.get_secret_value()
# repr(settings) prints `SecretStr('**********')` — safe to log
```

This skill owns the per-env **config plumbing** — `Settings` skeleton,
Secret Manager integration, per-env pinning, startup smoke test. It does
**not** own the full secrets lifecycle (rotation, revocation, scope) —
that belongs to `langchain-security-basics`.

Pin: `langchain-core 1.0.x`, `langchain-anthropic 1.0.x`, `pydantic >= 2.5`,
`pydantic-settings >= 2.1`. Pain anchors: **P37** (primary), **P20**
(checkpointer schema — cross-ref `langchain-langgraph-checkpointing`).

Two numbers: **smoke test < 10 seconds**; **env-var count ~15-30** (more
than 30 means `Settings` is absorbing feature flags and should split).

## Prerequisites

- Python 3.10+ (3.11+ recommended for `Literal` and `StrEnum` ergonomics)
- `langchain-core >= 1.0, < 2.0`
- `pydantic >= 2.5`, `pydantic-settings >= 2.1`
- One secret backend: GCP Secret Manager (`google-cloud-secret-manager`),
  AWS Secrets Manager (`boto3`), or HashiCorp Vault (`hvac`)
- Completed `langchain-sdk-patterns` — the `Settings` object is injected into
  the chain factories from that skill

## Instructions

Run these six steps in order — each adds one invariant the next step depends on:

1. Define a `Settings` class with `SecretStr` keys, `Literal` env, and fail-fast validation.
2. Add a per-env loader — file in dev, env vars in staging, Secret Manager in prod.
3. Use the cloud Secret Manager client to pull keys into memory only.
4. Pin `model_id`, `prompt_commit_hash`, and `vector_index_name` per env.
5. Configure the checkpointer per env — memory in dev, Postgres elsewhere.
6. Run a startup smoke test under 10 seconds before the HTTP server binds.

### Step 1 — Create a Settings class with SecretStr and fail-fast validation

```python
from typing import Literal
from pydantic import SecretStr, HttpUrl, Field, ValidationError
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=None,              # see Step 2 — loader picks the file
        env_file_encoding="utf-8",
        case_sensitive=False,
        extra="forbid",             # reject unknown env vars — typo detection
    )

    # --- env switch (drives everything else) ---
    env: Literal["dev", "staging", "prod"] = Field(..., alias="LANGCHAIN_ENV")

    # --- secrets (always SecretStr — never str) ---
    anthropic_api_key: SecretStr = Field(..., alias="ANTHROPIC_API_KEY")
    openai_api_key: SecretStr = Field(..., alias="OPENAI_API_KEY")
    langsmith_api_key: SecretStr = Field(..., alias="LANGSMITH_API_KEY")

    # --- per-env pinning (see Step 4) ---
    model_id: str = Field(..., alias="LANGCHAIN_MODEL_ID")
    prompt_commit_hash: str = Field(..., alias="LANGCHAIN_PROMPT_COMMIT")
    vector_index_name: str = Field(..., alias="LANGCHAIN_VECTOR_INDEX")

    # --- endpoints (validated URLs — typo caught at startup) ---
    checkpointer_url: HttpUrl | None = Field(None, alias="LANGCHAIN_CHECKPOINTER_URL")
    otel_endpoint: HttpUrl = Field(..., alias="OTEL_EXPORTER_OTLP_ENDPOINT")

    # --- budget guards (per-env) ---
    max_cost_usd_per_day: float = Field(10.0, alias="LANGCHAIN_DAILY_BUDGET_USD")
    max_rpm: int = Field(60, alias="LANGCHAIN_MAX_RPM")
```

`SecretStr` masks `repr(settings)` to `SecretStr('**********')` — a routine
`logger.info(settings)` cannot leak the key. The only way to read plaintext
is `.get_secret_value()`, which greps like a sore thumb in review.
`extra="forbid"` catches typos (`LANGCHIN_MODEL_ID`) at import time.
`HttpUrl` rejects `http:/otel:4318` before the exporter wastes 60s on DNS.

See [Settings Skeleton](references/settings-skeleton.md) for the full class.

### Step 2 — Per-env config loading (file OR Secret Manager, never both)

```python
import os
from pathlib import Path

def build_settings() -> Settings:
    env = os.environ.get("LANGCHAIN_ENV", "dev")

    if env == "dev":
        # Local dev: .env.dev file, values checked into 1Password not git
        return Settings(_env_file=Path(".env.dev"))

    if env == "staging":
        # CI / staging: env vars injected by the orchestrator
        # (GitHub Actions secrets, k8s envFrom: secretRef, etc.)
        return Settings()  # reads os.environ directly

    if env == "prod":
        # Prod: pull from Secret Manager into memory ONLY
        values = pull_from_secret_manager()
        return Settings(**values)

    raise ValueError(f"unknown LANGCHAIN_ENV: {env!r}")
```

Three loaders, one class. Dev touches a file on disk. Staging inherits env
vars from the orchestrator — `envFrom: secretRef` is readable via
`docker exec env`, but the blast radius is bounded and rotation is weekly.

Prod is the P37 fix: `pull_from_secret_manager()` builds a dict and passes
kwargs to `Settings(...)`. Values land in the instance attribute and
**never touch `os.environ`**. A subprocess will not inherit them.

### Step 3 — Secret Manager pull (GCP example; AWS / Vault in reference)

```python
from google.cloud import secretmanager

def pull_from_secret_manager() -> dict[str, str]:
    client = secretmanager.SecretManagerServiceClient()
    project = os.environ["GCP_PROJECT_ID"]
    secret_names = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "LANGSMITH_API_KEY"]
    out: dict[str, str] = {}
    for name in secret_names:
        resource = f"projects/{project}/secrets/{name}/versions/latest"
        response = client.access_secret_version(request={"name": resource})
        out[name] = response.payload.data.decode("utf-8")
    # Non-secret passthrough (model id, prompt hash, endpoints)
    for key in ["LANGCHAIN_ENV", "LANGCHAIN_MODEL_ID", "LANGCHAIN_PROMPT_COMMIT",
                "LANGCHAIN_VECTOR_INDEX", "LANGCHAIN_CHECKPOINTER_URL",
                "OTEL_EXPORTER_OTLP_ENDPOINT"]:
        if key in os.environ:
            out[key] = os.environ[key]
    return out
```

No `os.environ[k] = v` line. The dict goes straight into
`Settings(**values)`. Workload-identity IAM handles auth; no static key on
disk. For AWS / Vault see [Secret Manager Integration](references/secret-manager-integration.md).

### Step 4 — Per-env model and prompt pinning

Dev, staging, and prod run **different** model ids and **different** prompt
commit hashes. Pinning happens at env-var level so app code is env-agnostic
(see the Env Matrix below for values). One function reads
`settings.prompt_commit_hash` and pulls from LangSmith
(cross-ref `langchain-prompt-engineering`):

```python
from langsmit

Related in Cloud & DevOps