Skip to content

Commit 6d02c2e

Browse files
authored
refactor(langsmith-hosting): decouple LS IPs (#127)
* build(.gitignore): ignore output dirs * refactor(langsmith-hosting): create langsmith-network to decouple
1 parent c4a1255 commit 6d02c2e

File tree

23 files changed

+788
-535
lines changed

23 files changed

+788
-535
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,3 +271,8 @@ Pulumi.*.yaml
271271

272272
# Ignore Pulumi local backend state files.
273273
.pulumi/
274+
275+
276+
# Output files
277+
# -----------------------------------------------------------------------------
278+
output/
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[project]
2+
name = "langsmith-network"
3+
version = "0.1.0"
4+
description = "LangSmith Cloud NAT gateway IP addresses for firewall rules"
5+
requires-python = ">=3.12,<3.13"
6+
dependencies = []
7+
8+
[build-system]
9+
requires = ["hatchling"]
10+
build-backend = "hatchling.build"
11+
12+
[tool.hatch.build.targets.wheel]
13+
packages = ["src/langsmith_network"]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""LangSmith Cloud NAT gateway IP addresses for firewall rules."""
2+
3+
from .langsmith import EU, LANGSMITH, US
4+
5+
__all__ = [
6+
"EU",
7+
"LANGSMITH",
8+
"US",
9+
]
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""LangSmith Cloud NAT gateway IP addresses.
2+
3+
Source: https://docs.langchain.com/langsmith/deploy-to-cloud#allowlist-ip-addresses
4+
"""
5+
6+
US: tuple[str, ...] = (
7+
"34.9.99.224",
8+
"34.19.34.50",
9+
"34.19.93.202",
10+
"34.31.121.70",
11+
"34.41.178.137",
12+
"34.59.244.194",
13+
"34.68.27.146",
14+
"34.82.222.17",
15+
"34.121.166.52",
16+
"34.123.151.210",
17+
"34.135.61.140",
18+
"34.145.102.123",
19+
"34.169.45.153",
20+
"34.169.88.30",
21+
"35.197.29.146",
22+
"35.227.171.135",
23+
)
24+
25+
EU: tuple[str, ...] = (
26+
"34.13.244.114",
27+
"34.32.141.108",
28+
"34.32.145.240",
29+
"34.32.180.189",
30+
"34.34.69.108",
31+
"34.90.157.44",
32+
"34.90.213.236",
33+
"34.141.242.180",
34+
)
35+
36+
LANGSMITH: tuple[str, ...] = US + EU

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ members = [
2323
"tools/python/langsmith-hosting",
2424
"tools/python/pulumi-utils",
2525
"packages/python/langsmith-client",
26+
"packages/python/langsmith-network",
2627
"packages/python/azure-ai",
2728
]
2829

tools/python/langsmith-hosting/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Pulumi project that provisions AWS infrastructure for LangSmith Hybrid.
1212

1313
## Prerequisites
1414

15-
- AWS CLI configured with a named profile (set `awsProfile` in your stack config)
15+
- AWS CLI configured with an appropriate profile
1616
- [uv](https://docs.astral.sh/uv/) installed
1717
- Pulumi CLI installed
1818

@@ -39,3 +39,4 @@ Stack configuration lives in `Pulumi.dev.yaml`. Key settings:
3939
| `postgresInstanceClass` | RDS instance class | `db.t3.medium` |
4040
| `redisNodeType` | ElastiCache node type | `cache.t3.micro` |
4141
| `s3BucketPrefix` | S3 bucket name prefix | `langsmith` |
42+
| `extraPublicAccessCidrs` | Comma-separated CIDRs to add to the EKS API server allowlist | _(none)_ |

tools/python/langsmith-hosting/docs/architecture.md

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ langsmith-hosting/
1111
├── pyproject.toml # Dependencies (pulumi, pulumi-aws, pulumi-eks, ...)
1212
├── README.md
1313
├── docs/
14-
│ └── architecture.md # This file
14+
│ ├── architecture.md # This file
15+
│ └── cidr-entry-points.md # CIDR plugin discovery mechanism
1516
└── src/
1617
└── langsmith_hosting/
1718
├── __init__.py
1819
├── __main__.py # Entry point: wires all modules, exports outputs
20+
├── cidrs.py # CIDR utilities (get_cidrs, collapse_cidrs)
1921
├── config.py # Typed config loading from Pulumi stack config
20-
├── constants.py # PROJECT_NAME, TAGS
22+
├── constants.py # PROJECT_NAME, get_tags()
2123
├── vpc.py # VPC + subnets
2224
├── eks.py # EKS cluster + node group + addons
2325
├── postgres.py # RDS PostgreSQL
@@ -52,11 +54,21 @@ Orchestrates all modules in order:
5254
1. Loads `.env` and Pulumi stack config via `config.py`
5355
2. Gets AWS caller identity and region
5456
3. Creates VPC
55-
4. Creates EKS cluster (depends on VPC)
56-
5. Creates PostgreSQL (depends on VPC + random password)
57-
6. Creates Redis (depends on VPC)
58-
7. Creates S3 bucket (depends on VPC)
59-
8. Exports stack outputs
57+
4. Builds the EKS API server CIDR allowlist (LangSmith IPs + [entry point plugins](cidr-entry-points.md) + manual overrides)
58+
5. Creates EKS cluster (depends on VPC)
59+
6. Creates PostgreSQL (depends on VPC + random password)
60+
7. Creates Redis (depends on VPC)
61+
8. Creates S3 bucket (depends on VPC)
62+
9. Creates data plane (listener + KEDA + langgraph-dataplane Helm chart)
63+
10. Exports stack outputs
64+
65+
### `cidrs.py` -- CIDR Utilities
66+
67+
Helper functions for IP/CIDR manipulation:
68+
69+
- `get_cidrs()` -- appends `/32` (IPv4) or `/128` (IPv6) to bare IP addresses
70+
- `collapse_cidrs()` -- best-effort collapse of a CIDR list toward a target count (AWS EKS allows at most 40 public access CIDRs)
71+
- `AWS_EKS_MAX_PUBLIC_ACCESS_CIDRS` -- the 40-entry limit constant
6072

6173
### `config.py` -- Configuration
6274

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# CIDR Entry Points
2+
3+
The EKS API server allowlist is built from three sources, merged and
4+
deduplicated at deploy time:
5+
6+
```python
7+
_all_cidrs = list(
8+
dict.fromkeys(
9+
get_cidrs(LANGSMITH) # 1. Built-in LangSmith IPs
10+
+ tuple(_org_cidrs) # 2. Entry point plugins
11+
+ cfg.extra_public_access_cidrs # 3. Manual overrides
12+
)
13+
)
14+
```
15+
16+
Source 2 uses [Python entry points](https://packaging.python.org/en/latest/guides/creating-and-discovering-plugins/#using-package-metadata),
17+
the PyPA-standard plugin discovery mechanism (`importlib.metadata`). Any
18+
installed package can provide CIDRs by declaring an entry point in the
19+
`langsmith_hosting.cidrs` group.
20+
21+
## How it works
22+
23+
### Consumer side (langsmith-hosting)
24+
25+
`__main__.py` discovers all registered CIDR providers at runtime:
26+
27+
```python
28+
from importlib.metadata import entry_points
29+
30+
_org_cidrs: list[str] = []
31+
for _ep in entry_points(group="langsmith_hosting.cidrs"):
32+
_org_cidrs.extend(_ep.load())
33+
```
34+
35+
`entry_points(group=...)` scans every installed package's metadata for
36+
entries in that group. `ep.load()` performs the import and attribute
37+
lookup, returning the CIDR tuple.
38+
39+
### Provider side (any package)
40+
41+
A provider declares entry points in its `pyproject.toml`:
42+
43+
```toml
44+
[project.entry-points."langsmith_hosting.cidrs"]
45+
my-corp = "my_network.corporate:CORPORATE_CIDRS"
46+
```
47+
48+
Each entry follows the format `name = "module.path:ATTRIBUTE"`:
49+
50+
| Part | Meaning |
51+
| --- | --- |
52+
| `my-corp` | Human-readable name (used for inspection, not code) |
53+
| `my_network.corporate` | Python module to import |
54+
| `CORPORATE_CIDRS` | Attribute on that module — must be an iterable of CIDR strings |
55+
56+
### Concrete example
57+
58+
An internal networking package could declare:
59+
60+
```toml
61+
[project.entry-points."langsmith_hosting.cidrs"]
62+
corporate = "my_network.corporate:CORPORATE_CIDRS"
63+
```
64+
65+
When that package is installed (e.g., via `uv sync --all-packages`), its
66+
CIDRs are automatically discovered and included. When it is absent,
67+
`entry_points()` returns nothing and only the LangSmith IPs + manual
68+
overrides are used.
69+
70+
## Inspecting registered entry points
71+
72+
```bash
73+
uv run python -c "
74+
from importlib.metadata import entry_points
75+
for ep in entry_points(group='langsmith_hosting.cidrs'):
76+
print(f'{ep.name}: {ep.load()}')
77+
"
78+
```
79+
80+
## Adding a new CIDR provider
81+
82+
1. Create a Python package with a module that exports a tuple of CIDR
83+
strings (e.g., `my_network/firewalls.py` with `SCANNER_IPS`).
84+
2. Add the entry point to the package's `pyproject.toml`:
85+
86+
```toml
87+
[project.entry-points."langsmith_hosting.cidrs"]
88+
scanner = "my_network.firewalls:SCANNER_IPS"
89+
```
90+
91+
3. Install the package in the same environment as `langsmith-hosting`
92+
(or add it to the uv workspace).
93+
4. Run `uv sync --all-packages` to register the entry point.
94+
5. `pulumi preview` will now include the new CIDRs.

0 commit comments

Comments
 (0)