diff --git a/neon-branching-tutorial/.gitignore b/neon-branching-tutorial/.gitignore new file mode 100644 index 0000000..092d53f --- /dev/null +++ b/neon-branching-tutorial/.gitignore @@ -0,0 +1,40 @@ +# Dependencies +node_modules/ + +# Environment variables +.env +.env.local +.env.*.local + +# Logs +logs/ +*.log +npm-debug.log* + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Coverage directory +coverage/ + +# Build output +dist/ +build/ + +# IDE and editor files +.idea/ +.vscode/ +*.swp +*.swo +*~ +.DS_Store + +# Kubernetes secrets (never commit these!) +*-secret.yaml +db-secret.yaml + +# Neon credentials +.neonctl \ No newline at end of file diff --git a/neon-branching-tutorial/Makefile b/neon-branching-tutorial/Makefile new file mode 100644 index 0000000..fb2fdfb --- /dev/null +++ b/neon-branching-tutorial/Makefile @@ -0,0 +1,29 @@ +.PHONY: build deploy clean setup-neon-secret + +IMAGE_NAME := signadot/neon-demo-users:latest + +build: + docker build -t $(IMAGE_NAME) -f ./docker/users-service.Dockerfile ./pkg/users-service + +deploy: + kubectl apply -f ./k8s + +clean: + kubectl delete -f ./k8s --ignore-not-found + +setup-neon-secret: + @echo "Creating Neon API credentials secret in signadot namespace..." + @read -p "Enter your Neon API key: " NEON_API_KEY; \ + kubectl create secret generic neon-api-credentials \ + --namespace=signadot \ + --from-literal=NEON_API_KEY=$$NEON_API_KEY \ + --dry-run=client -o yaml | kubectl apply -f - + +setup-db-secret: + @echo "Creating database credentials secret..." + @read -p "Enter your Neon connection string: " DATABASE_URL; \ + kubectl create secret generic users-db-credentials \ + --from-literal=DATABASE_URL="$$DATABASE_URL" \ + --dry-run=client -o yaml | kubectl apply -f - + +all: build deploy \ No newline at end of file diff --git a/neon-branching-tutorial/README.md b/neon-branching-tutorial/README.md new file mode 100644 index 0000000..5ab53c6 --- /dev/null +++ b/neon-branching-tutorial/README.md @@ -0,0 +1,347 @@ +# True "Branch-Based Environments": Combining Signadot Sandboxes with Neon DB Branching + +Ephemeral sandbox environments solve many problems for microservices teams. You can spin up an isolated copy of your service, test your changes, and tear it down. No conflicts with other developers. No waiting for a shared staging slot. + +But here's the catch: your sandbox service still connects to the same staging database as everyone else. One developer's test writes pollute another's queries. Schema migrations break active tests. Seed data disappears mid-run. The application layer is isolated, but the data layer is not. + +This guide shows you how to fix that problem. You will combine Signadot Sandboxes with Neon's database branching to create true full-stack isolation. Every sandbox gets its own application fork and its own database branch. When the sandbox dies, the database branch dies with it. + +## What You Will Build + +The end-to-end system works as follows: + +1. A developer creates a Signadot Sandbox +2. A Resource Plugin automatically creates a Neon database branch and exposes the connection string as an output +3. The sandbox pod starts with a connection string pointing to the isolated branch +4. The developer runs tests against isolated data +5. The developer deletes the sandbox +6. The Resource Plugin deletes the Neon branch automatically + +No shared state. No test pollution. No manual cleanup scripts. + +## How It Works + +The architecture relies on two key technologies working together. + +**Neon Database Branching**: Neon uses copy-on-write storage to create instant database branches. A branch inherits all schema and data from its parent but operates independently. Writes to a branch don't affect the parent, and branches can be created or deleted in seconds with minimal storage overhead. + +**Signadot Resource Plugins**: Resource Plugins extend Signadot's sandbox lifecycle with custom provisioning logic. When a sandbox starts, the plugin runs a create workflow. When the sandbox terminates, the plugin runs a delete workflow. Outputs from the create workflow (like connection strings) can be injected directly into sandbox pods. + + +## Prerequisites + +Before you begin, ensure you have: + +- `kubectl` and `minikube` installed +- A [Neon account](https://neon.tech) with an API key +- The `neonctl` CLI installed and authenticated +- A [Signadot account](https://www.signadot.com/) with the operator installed in your cluster +- The `signadot` CLI installed and authenticated + +## Baseline Environment + +We'll set up a users microservice connected to a Neon database, then demonstrate how sandboxes can get isolated database branches. + +### Step 1: Clone the Example Repository + +The example repository contains a pre-built users microservice and all necessary Kubernetes manifests: + +```bash +mkdir -p ~/git/signadot/ +cd ~/git/signadot/ +git clone https://github.com/signadot/examples.git +cd examples/neon-branching-tutorial +``` + +### Step 2: Set Up the Neon Database + +Create a Neon project: + +```bash +neonctl projects create --name users-demo +``` +![Create a Neon project](./images/img-001.png) + +Note the project ID from the output (e.g., `sparkling-queen-66410086`). You'll need it throughout this tutorial. + +Retrieve the connection string and create the schema: + +```bash +neonctl connection-string main \ + --project-id \ + --database-name neondb +``` + +Connect to the database and run the schema file: + +```bash +psql "" -f schema.sql +``` + +The `schema.sql` file creates a `users` table and inserts three seed records. Every sandbox branch will inherit this data. + +Generate an API key for the Resource Plugin: + +1. Go to the [Neon Console](https://console.neon.tech/) +2. Navigate to **Account Settings > Personal API keys** +3. Click **Create new API key** and save it securely + +### Step 3: Deploy to Minikube + +Start minikube and build the demo image: + +```bash +minikube start + +eval $(minikube docker-env) +make build +``` + +Create the required secrets: + +```bash +make setup-db-secret # Enter your Neon connection string when prompted +make setup-neon-secret # Enter your Neon API key when prompted +``` + +Deploy the baseline service: + +```bash +make deploy +``` + +Verify the deployment: + +```bash +kubectl get pods -l app=users-service +``` +![Verify the deployment](./images/img-002.png) + +You should see pods in `Running` state with `2/2` containers (the service plus the Signadot routing sidecar). + +### Step 4: Install the Resource Plugin + +The Resource Plugin bridges Signadot and Neon. Take a look at `neon-branch-plugin.yaml`: + +```yaml +name: neon-branch +spec: + description: Creates and deletes Neon database branches for sandbox isolation + + runner: + image: node:20-alpine + namespace: signadot + podTemplateOverlay: | + spec: + containers: + - name: main + env: + - name: NEON_API_KEY + valueFrom: + secretKeyRef: + name: neon-api-credentials + key: NEON_API_KEY + + create: + - name: createbranch + inputs: + - name: project-id + valueFromSandbox: true + as: + env: NEON_PROJECT_ID + - name: parent-branch + valueFromSandbox: true + as: + env: PARENT_BRANCH + - name: database-name + valueFromSandbox: true + as: + env: DATABASE_NAME + script: | + #!/bin/sh + set -e + npm install -g neonctl + + SAFE_NAME=$(echo "${SIGNADOT_SANDBOX_NAME}" | tr -d '-') + BRANCH_NAME="sandbox${SAFE_NAME}" + + neonctl branches create \ + --project-id "${NEON_PROJECT_ID}" \ + --name "${BRANCH_NAME}" \ + --parent "${PARENT_BRANCH}" \ + --output json > /tmp/branch-output.json + + BRANCH_ID=$(cat /tmp/branch-output.json | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + + CONNECTION_STRING=$(neonctl connection-string "${BRANCH_NAME}" \ + --project-id "${NEON_PROJECT_ID}" \ + --database-name "${DATABASE_NAME}") + + mkdir -p /outputs + echo -n "${BRANCH_NAME}" > /outputs/branch-name + echo -n "${BRANCH_ID}" > /outputs/branch-id + echo -n "${CONNECTION_STRING}" > /outputs/connection-string + + outputs: + - name: branch-name + valueFromPath: /outputs/branch-name + - name: branch-id + valueFromPath: /outputs/branch-id + - name: connection-string + valueFromPath: /outputs/connection-string + + delete: + - name: deletebranch + inputs: + - name: project-id + valueFromSandbox: true + as: + env: NEON_PROJECT_ID + - name: branch-name + valueFromStep: + name: createbranch + output: branch-name + as: + env: BRANCH_NAME + script: | + #!/bin/sh + set -e + npm install -g neonctl + neonctl branches delete "${BRANCH_NAME}" --project-id "${NEON_PROJECT_ID}" +``` + +The plugin has three main sections: + +- **runner**: Uses `node:20-alpine` with the Neon API key injected via `podTemplateOverlay`. The runner executes in the `signadot` namespace where the API key secret exists. +- **create**: Installs `neonctl`, creates a branch named after the sandbox, retrieves the connection string, and exposes it as an output. The script sanitizes the sandbox name by removing hyphens since Neon branch names work best with alphanumeric characters. +- **delete**: Reads the branch name from the create step's output (using `valueFromStep`) and deletes it. + +Apply the plugin: + +```bash +signadot resourceplugin apply -f neon-branch-plugin.yaml +``` + +### Step 5: Configure the Sandbox Specification + +The sandbox spec ties everything together. Review `users-sandbox.yaml`: + +```yaml +name: "@{sandbox-name}" +spec: + description: "Users service sandbox with isolated Neon database branch" + cluster: "@{cluster}" + + resources: + - name: usersDb + plugin: neon-branch + params: + project-id: "@{neon-project-id}" + parent-branch: "main" + database-name: "neondb" + + forks: + - forkOf: + kind: Deployment + namespace: default + name: users-service + customizations: + env: + - name: DATABASE_URL + valueFrom: + resource: + name: usersDb + outputKey: createbranch.connection-string + + defaultRouteGroup: + endpoints: + - name: users-api + target: http://users-service.default.svc:3000 +``` + +The key sections: + +- **resources**: Invokes the `neon-branch` plugin with project parameters passed at apply time. +- **forks**: Creates a copy of the `users-service` Deployment with the `DATABASE_URL` overridden. The `valueFrom.resource` field references the plugin output directly using the format `.`. No intermediate Kubernetes Secret is required. +- **defaultRouteGroup**: Creates a preview URL for accessing the sandboxed service. + +## Using Sandboxes + +Create a sandbox with an isolated database branch: + +```bash +signadot sandbox apply -f users-sandbox.yaml \ + --set sandbox-name=my-feature \ + --set cluster= \ + --set neon-project-id= +``` + +### Verify Branch Creation + +Check the Neon branches: + +```bash +neonctl branches list --project-id +``` + +![Check the Neon branches](./images/img-003.png) + +You should see both `main` and `sandboxmyfeature` branches. + +### Test Data Isolation + +Query the sandbox endpoint to see the inherited seed data: + +```bash +curl -H "signadot-api-key: " \ + "https://users-api--my-feature.preview.signadot.com/users" +``` + +![Query the sandbox](./images/img-004.png) + +Create a test user in the sandbox: + +```bash +curl -X POST \ + -H "signadot-api-key: " \ + -H "Content-Type: application/json" \ + -d '{"name": "Sandbox User", "email": "sandbox@test.example"}' \ + "https://users-api--my-feature.preview.signadot.com/users" +``` + +![Create a test user](./images/img-005.png) + +Verify the main branch remains unaffected: + +```bash +neonctl connection-string main --project-id --database-name neondb +psql "" -c "SELECT * FROM users WHERE email = 'sandbox@test.example';" +``` + +![Verify the main branch](./images/img-006.png) + +The query returns zero rows. The sandbox user exists only in the branch. + +### Cleanup + +Delete the sandbox: + +```bash +signadot sandbox delete my-feature +``` + +The Resource Plugin's delete workflow automatically removes the Neon branch: + +```bash +neonctl branches list --project-id +``` + +![Verify cleanup](./images/img-007.png) + +Only the `main` branch remains. + +## Conclusion + +Each Signadot Sandbox now gets its own forked microservice pods and its own isolated Neon database branch. The Resource Plugin handles the entire lifecycle: creating branches on sandbox creation, exposing connection strings through built-in outputs, and cleaning them up on deletion. Test data cannot leak between sandboxes, and schema migrations in one branch cannot break tests in another. + +The cost efficiency makes this practical for everyday use. Neon branches use copy-on-write storage, so you only pay for data that changes. Signadot sandboxes share baseline cluster resources. Branch creation and teardown complete in seconds. Every developer gets an isolated app and database for every pull request. \ No newline at end of file diff --git a/neon-branching-tutorial/build.sh b/neon-branching-tutorial/build.sh new file mode 100644 index 0000000..15c7ba8 --- /dev/null +++ b/neon-branching-tutorial/build.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +echo "Building users-service image..." +docker build -t signadot/neon-demo-users:latest -f ./docker/users-service.Dockerfile ./pkg/users-service + +echo "Build complete!" +echo "Images built:" +docker images | grep signadot/neon-demo \ No newline at end of file diff --git a/neon-branching-tutorial/docker/users-service.Dockerfile b/neon-branching-tutorial/docker/users-service.Dockerfile new file mode 100644 index 0000000..11a50d1 --- /dev/null +++ b/neon-branching-tutorial/docker/users-service.Dockerfile @@ -0,0 +1,22 @@ +FROM node:20-alpine + +WORKDIR /app + +# Copy package files first for better layer caching +COPY package*.json ./ + +# Install dependencies +RUN npm install --production + +# Copy application code +COPY app.js ./ + +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 + +USER nodejs + +EXPOSE 3000 + +CMD ["npm", "start"] \ No newline at end of file diff --git a/neon-branching-tutorial/images/img-001.png b/neon-branching-tutorial/images/img-001.png new file mode 100644 index 0000000..7e962c2 Binary files /dev/null and b/neon-branching-tutorial/images/img-001.png differ diff --git a/neon-branching-tutorial/images/img-002.png b/neon-branching-tutorial/images/img-002.png new file mode 100644 index 0000000..6d9b0cf Binary files /dev/null and b/neon-branching-tutorial/images/img-002.png differ diff --git a/neon-branching-tutorial/images/img-003.png b/neon-branching-tutorial/images/img-003.png new file mode 100644 index 0000000..d5ad00c Binary files /dev/null and b/neon-branching-tutorial/images/img-003.png differ diff --git a/neon-branching-tutorial/images/img-004.png b/neon-branching-tutorial/images/img-004.png new file mode 100644 index 0000000..b6957df Binary files /dev/null and b/neon-branching-tutorial/images/img-004.png differ diff --git a/neon-branching-tutorial/images/img-005.png b/neon-branching-tutorial/images/img-005.png new file mode 100644 index 0000000..874a096 Binary files /dev/null and b/neon-branching-tutorial/images/img-005.png differ diff --git a/neon-branching-tutorial/images/img-006.png b/neon-branching-tutorial/images/img-006.png new file mode 100644 index 0000000..b13dbd4 Binary files /dev/null and b/neon-branching-tutorial/images/img-006.png differ diff --git a/neon-branching-tutorial/images/img-007.png b/neon-branching-tutorial/images/img-007.png new file mode 100644 index 0000000..e886320 Binary files /dev/null and b/neon-branching-tutorial/images/img-007.png differ diff --git a/neon-branching-tutorial/k8s/deployment.yaml b/neon-branching-tutorial/k8s/deployment.yaml new file mode 100644 index 0000000..a80a392 --- /dev/null +++ b/neon-branching-tutorial/k8s/deployment.yaml @@ -0,0 +1,50 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: users-service + namespace: default + labels: + app: users-service +spec: + replicas: 1 + selector: + matchLabels: + app: users-service + template: + metadata: + labels: + app: users-service + annotations: + sidecar.signadot.com/inject: "true" + spec: + containers: + - name: users-service + image: signadot/neon-demo-users:latest + imagePullPolicy: Never + ports: + - containerPort: 3000 + env: + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: users-db-credentials + key: DATABASE_URL + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: /health + port: 3000 + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 5 \ No newline at end of file diff --git a/neon-branching-tutorial/k8s/service.yaml b/neon-branching-tutorial/k8s/service.yaml new file mode 100644 index 0000000..ca0c21b --- /dev/null +++ b/neon-branching-tutorial/k8s/service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: users-service + namespace: default +spec: + selector: + app: users-service + ports: + - port: 3000 + targetPort: 3000 \ No newline at end of file diff --git a/neon-branching-tutorial/neon-branch-plugin.yaml b/neon-branching-tutorial/neon-branch-plugin.yaml new file mode 100644 index 0000000..540977d --- /dev/null +++ b/neon-branching-tutorial/neon-branch-plugin.yaml @@ -0,0 +1,110 @@ +name: neon-branch +spec: + description: Creates and deletes Neon database branches for sandbox isolation + + runner: + image: node:20-alpine + namespace: signadot + podTemplateOverlay: | + spec: + containers: + - name: main + env: + - name: NEON_API_KEY + valueFrom: + secretKeyRef: + name: neon-api-credentials + key: NEON_API_KEY + + create: + - name: createbranch + inputs: + - name: project-id + valueFromSandbox: true + as: + env: NEON_PROJECT_ID + - name: parent-branch + valueFromSandbox: true + as: + env: PARENT_BRANCH + - name: database-name + valueFromSandbox: true + as: + env: DATABASE_NAME + script: | + #!/bin/sh + set -e + + # Install neonctl + npm install -g neonctl + + # Generate a unique branch name using the sandbox name + # Remove any hyphens from sandbox name for Neon branch naming + SAFE_NAME=$(echo "${SIGNADOT_SANDBOX_NAME}" | tr -d '-') + BRANCH_NAME="sandbox${SAFE_NAME}" + + echo "Creating Neon branch: ${BRANCH_NAME}" + echo "Project ID: ${NEON_PROJECT_ID}" + echo "Parent branch: ${PARENT_BRANCH}" + + # Create the branch + neonctl branches create \ + --project-id "${NEON_PROJECT_ID}" \ + --name "${BRANCH_NAME}" \ + --parent "${PARENT_BRANCH}" \ + --output json > /tmp/branch-output.json + + cat /tmp/branch-output.json + + # Extract the branch ID + BRANCH_ID=$(cat /tmp/branch-output.json | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + echo "Created branch ID: ${BRANCH_ID}" + + # Get the connection string for the new branch + CONNECTION_STRING=$(neonctl connection-string "${BRANCH_NAME}" \ + --project-id "${NEON_PROJECT_ID}" \ + --database-name "${DATABASE_NAME}") + + echo "Connection string retrieved successfully" + + # Write outputs to files for Signadot to capture + mkdir -p /outputs + echo -n "${BRANCH_NAME}" > /outputs/branch-name + echo -n "${BRANCH_ID}" > /outputs/branch-id + echo -n "${CONNECTION_STRING}" > /outputs/connection-string + + echo "Branch creation complete" + + outputs: + - name: branch-name + valueFromPath: /outputs/branch-name + - name: branch-id + valueFromPath: /outputs/branch-id + - name: connection-string + valueFromPath: /outputs/connection-string + + delete: + - name: deletebranch + inputs: + - name: project-id + valueFromSandbox: true + as: + env: NEON_PROJECT_ID + - name: branch-name + valueFromStep: + name: createbranch + output: branch-name + as: + env: BRANCH_NAME + script: | + #!/bin/sh + set -e + + # Install neonctl + npm install -g neonctl + + echo "Deleting Neon branch: ${BRANCH_NAME}" + neonctl branches delete "${BRANCH_NAME}" \ + --project-id "${NEON_PROJECT_ID}" + + echo "Cleanup complete" \ No newline at end of file diff --git a/neon-branching-tutorial/pkg/users-service/app.js b/neon-branching-tutorial/pkg/users-service/app.js new file mode 100644 index 0000000..891abd1 --- /dev/null +++ b/neon-branching-tutorial/pkg/users-service/app.js @@ -0,0 +1,78 @@ +const express = require('express'); +const { Pool } = require('pg'); + +const app = express(); +app.use(express.json()); + +// Read connection string from environment variable +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + ssl: { rejectUnauthorized: false }, +}); + +// Health check endpoint +app.get('/health', async (req, res) => { + try { + await pool.query('SELECT 1'); + res.json({ status: 'healthy' }); + } catch (err) { + res.status(500).json({ status: 'unhealthy', error: err.message }); + } +}); + +// Get all users +app.get('/users', async (req, res) => { + try { + const result = await pool.query('SELECT * FROM users ORDER BY id'); + res.json(result.rows); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// Get user by ID +app.get('/users/:id', async (req, res) => { + try { + const { id } = req.params; + const result = await pool.query('SELECT * FROM users WHERE id = $1', [id]); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'User not found' }); + } + res.json(result.rows[0]); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// Create a user +app.post('/users', async (req, res) => { + try { + const { name, email } = req.body; + const result = await pool.query( + 'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *', + [name, email] + ); + res.status(201).json(result.rows[0]); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// Delete a user +app.delete('/users/:id', async (req, res) => { + try { + const { id } = req.params; + const result = await pool.query('DELETE FROM users WHERE id = $1 RETURNING *', [id]); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'User not found' }); + } + res.json({ message: 'User deleted', user: result.rows[0] }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`Users service running on port ${PORT}`); +}); \ No newline at end of file diff --git a/neon-branching-tutorial/pkg/users-service/package.json b/neon-branching-tutorial/pkg/users-service/package.json new file mode 100644 index 0000000..0a82e21 --- /dev/null +++ b/neon-branching-tutorial/pkg/users-service/package.json @@ -0,0 +1,16 @@ +{ + "name": "users-service", + "version": "1.0.0", + "description": "Users microservice for Signadot + Neon branching demo", + "main": "app.js", + "scripts": { + "start": "node app.js" + }, + "dependencies": { + "express": "^4.18.2", + "pg": "^8.11.3" + }, + "engines": { + "node": ">=18.0.0" + } +} \ No newline at end of file diff --git a/neon-branching-tutorial/schema.sql b/neon-branching-tutorial/schema.sql new file mode 100644 index 0000000..6c01195 --- /dev/null +++ b/neon-branching-tutorial/schema.sql @@ -0,0 +1,16 @@ +-- Schema setup for Neon branching tutorial +-- Run this against your Neon database main branch + +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Seed data +INSERT INTO users (name, email) VALUES + ('Alice Johnson', 'alice@example.com'), + ('Bob Smith', 'bob@example.com'), + ('Carol Williams', 'carol@example.com') +ON CONFLICT (email) DO NOTHING; \ No newline at end of file diff --git a/neon-branching-tutorial/users-sandbox.yaml b/neon-branching-tutorial/users-sandbox.yaml new file mode 100644 index 0000000..8d378e5 --- /dev/null +++ b/neon-branching-tutorial/users-sandbox.yaml @@ -0,0 +1,34 @@ +name: "@{sandbox-name}" +spec: + description: "Users service sandbox with isolated Neon database branch" + cluster: "@{cluster}" + + labels: + team: platform + service: users-service + + resources: + - name: usersDb + plugin: neon-branch + params: + project-id: "@{neon-project-id}" + parent-branch: "main" + database-name: "neondb" + + forks: + - forkOf: + kind: Deployment + namespace: default + name: users-service + customizations: + env: + - name: DATABASE_URL + valueFrom: + resource: + name: usersDb + outputKey: createbranch.connection-string + + defaultRouteGroup: + endpoints: + - name: users-api + target: http://users-service.default.svc:3000 \ No newline at end of file