File size: 10,581 Bytes
558db1e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# Deployment Module

## Abstract

The `deploy/` directory contains the infrastructure-as-code definitions required to deploy the Portfolio Engine as a production-grade, containerised microservice on Kubernetes. The deployment architecture follows a stateless application pattern backed by managed PostgreSQL and Redis services, orchestrated via Helm charts and Docker Compose for local development parity.

---

## 1. Architectural Overview

The production deployment comprises three services:

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Kubernetes Cluster                          β”‚
β”‚                                                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  portfolio-engine β”‚  β”‚  PostgreSQL  β”‚  β”‚      Redis       β”‚  β”‚
β”‚  β”‚  (Python 3.11)   │──│  (v15)       β”‚  β”‚  (v7)            β”‚  β”‚
β”‚  β”‚  Uvicorn / FastAPIβ”‚  β”‚  Persistent  β”‚  β”‚  Ephemeral Cache β”‚  β”‚
β”‚  β”‚  Port 8080       β”‚  β”‚  Port 5432   β”‚  β”‚  Port 6379       β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

- **Portfolio Engine:** Stateless Python application serving the FastAPI-based REST API via Uvicorn. Handles all optimisation, backtesting, and reporting logic.
- **PostgreSQL 15:** Persistent storage for daily price data, portfolio state, and transaction history. The engine writes to the `daily_prices` table and reads during the data-loading phase.
- **Redis 7:** Optional caching layer for rate-limited API responses and inter-process synchronisation in multi-replica deployments.

---

## 2. Container Image β€” `Dockerfile`

```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
CMD ["python", "core_engine.py", "--headless"]
```

**Design Decisions:**

- **Base image:** `python:3.11-slim` minimises attack surface and image size while retaining the `manylinux` wheels required by NumPy, SciPy, and CVXPY.
- **Dependency installation before source copy:** Exploits Docker layer caching. Dependencies change infrequently; source code changes do not invalidate the pip install layer.
- **Headless mode:** The default entrypoint runs the engine in non-interactive mode, suitable for scheduled batch jobs. For API serving, override with `uvicorn api:app --host 0.0.0.0 --port 8080`.

---

## 3. Local Development β€” `docker-compose.yml`

The Compose file provisions all three services for local development with production-equivalent topology.

### Service Configuration

| Service   | Image          | Ports    | Health Check                                          |
|-----------|----------------|----------|-------------------------------------------------------|
| `db`      | `postgres:15`  | 5432     | `pg_isready -U engine_user -d portfolio_db`           |
| `redis`   | `redis:7`      | 6379     | `redis-cli ping`                                      |
| `engine`  | Build from `.` | 8080     | `curl -f http://localhost:8080/health`                |

### Dependency Ordering

The engine container specifies `depends_on` with `condition: service_healthy`, ensuring that PostgreSQL and Redis are fully operational before the application starts. This prevents race conditions during cold-start data loading.

### Environment Variables

| Variable       | Value                                                            |
|----------------|------------------------------------------------------------------|
| `DATABASE_URL` | `postgresql://engine_user:engine_password@db:5432/portfolio_db`  |
| `REDIS_URL`    | `redis://redis:6379/0`                                           |

**Security Note.** The Compose file uses plaintext credentials for local development only. Production deployments must inject secrets via Kubernetes Secrets, HashiCorp Vault, or an equivalent secrets manager.

### Usage

```bash
# Build and start all services
docker-compose up --build

# Run in detached mode
docker-compose up -d

# View logs
docker-compose logs -f engine

# Tear down
docker-compose down -v
```

---

## 4. Kubernetes Deployment β€” Helm Chart

### 4.1 Chart Metadata β€” `Chart.yaml`

| Field          | Value                                  |
|----------------|----------------------------------------|
| `apiVersion`   | `v2` (Helm 3)                          |
| `name`         | `portfolio-engine`                     |
| `description`  | Quantitative Portfolio Allocation Engine |
| `type`         | `application`                          |
| `version`      | `0.1.0` (chart version)               |
| `appVersion`   | `1.0` (application version)            |

### 4.2 Configurable Values β€” `values.yaml`

| Parameter               | Default              | Description                                    |
|-------------------------|----------------------|------------------------------------------------|
| `replicaCount`          | `2`                  | Number of engine pod replicas                  |
| `image.repository`      | `portfolio-engine`   | Container registry path                        |
| `image.pullPolicy`      | `IfNotPresent`       | Image pull policy                              |
| `image.tag`             | `latest`             | Image tag                                      |
| `service.type`          | `ClusterIP`          | Kubernetes service type                        |
| `service.port`          | `8080`               | Service port                                   |
| `env[0] (API_KEY)`      | `changeme`           | External data API key                          |
| `env[1] (DATABASE_URL)` | (empty)              | PostgreSQL connection string                   |
| `resources.limits.cpu`  | `2000m`              | CPU limit per pod                              |
| `resources.limits.memory` | `4Gi`              | Memory limit per pod                           |
| `resources.requests.cpu` | `500m`              | CPU request per pod                            |
| `resources.requests.memory` | `1Gi`            | Memory request per pod                         |

**Resource Sizing Rationale.** The 4Gi memory limit accommodates the in-memory covariance matrices and Monte Carlo simulation arrays for portfolios of up to ~200 assets. The 2-core CPU limit supports CVXPY's OSQP solver, which benefits from vectorised linear algebra but does not parallelise across cores.

### 4.3 Templates

| Template             | Purpose                                                              |
|----------------------|----------------------------------------------------------------------|
| `_helpers.tpl`       | Reusable Go template functions for generating consistent names and labels |
| `deployment.yaml`    | Kubernetes Deployment resource defining pod spec, container image, ports, environment variables, and resource constraints |
| `service.yaml`       | Kubernetes Service resource exposing the engine pods via ClusterIP    |

### 4.4 Deployment Commands

```bash
# Install the chart
helm install portfolio-engine ./deploy/helm/portfolio-engine \
  --set env[0].value="YOUR_API_KEY" \
  --set env[1].value="postgresql://user:pass@host:5432/db"

# Upgrade an existing release
helm upgrade portfolio-engine ./deploy/helm/portfolio-engine \
  --set image.tag="v2.0"

# Uninstall
helm uninstall portfolio-engine

# Dry-run to preview generated manifests
helm template portfolio-engine ./deploy/helm/portfolio-engine
```

---

## 5. CI/CD Pipeline β€” `.github/workflows/ci.yml`

The GitHub Actions workflow runs on every push and pull request to `main`:

| Step                  | Tool         | Purpose                                              |
|-----------------------|--------------|------------------------------------------------------|
| Checkout              | `actions/checkout@v3` | Clone repository                              |
| Python Setup          | `actions/setup-python@v4` | Install Python 3.11                       |
| Install Dependencies  | `pip install .[dev]`  | Install production and development dependencies |
| Lint                  | `black --check .`     | Enforce PEP 8 code formatting                |
| Type Check            | `mypy .`              | Static type analysis                         |
| Test                  | `pytest tests/`       | Run the full test suite                      |

**Pipeline Philosophy.** The CI pipeline enforces a "green trunk" policy: no merge is permitted unless all linting, typing, and testing gates pass. This is consistent with the engine's design principle of no overfittingβ€”code quality gates prevent regressions that could silently corrupt optimisation results.

---

## 6. Production Considerations

### Scaling

The engine is designed for horizontal scaling behind a load balancer. Each pod is stateless; all persistent state resides in PostgreSQL. Redis can be used for distributed rate-limiting across replicas to honour external API quotas (max 2 requests/second globally).

### Observability

The engine emits structured JSON logs (see `output/engine.log`) compatible with ELK, Datadog, or Grafana Loki. Health check endpoints at `/health` support Kubernetes liveness and readiness probes.

### Security

- API keys must never be committed to version control. Use Kubernetes Secrets or a secrets manager.
- The `DATABASE_URL` should use TLS-encrypted connections (`sslmode=require`) in production.
- Container images should be scanned for CVEs before deployment (e.g., Trivy, Snyk Container).

---

## References

- Kubernetes Documentation. (2024). *Deployments*. https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
- Helm Documentation. (2024). *Chart Template Guide*. https://helm.sh/docs/chart_template_guide/
- Docker Documentation. (2024). *Best practices for writing Dockerfiles*. https://docs.docker.com/develop/develop-images/dockerfile_best-practices/