80 lines
3.6 KiB
Markdown
80 lines
3.6 KiB
Markdown
# Deploy a Java + React app from its `docker-compose.yml`
|
|
|
|
Deploys an existing containerized app to Kubernetes with Harmony by
|
|
**importing its `docker-compose.yml`** — no hand-written manifests, no Argo.
|
|
The compose file stays the source of truth for the app's *base* shape;
|
|
deploy-only concerns are typed Rust.
|
|
|
|
```
|
|
app/ the "customer" project (their existing repo)
|
|
docker-compose.yml ← source of truth: images, ports, env, volumes
|
|
backend/ (Java + SQLite, Dockerfile)
|
|
frontend/ (React + nginx, Dockerfile)
|
|
Harmony.toml identity only — the app name (ADR-026 §3)
|
|
.harmony/contexts.toml deploy targets (local / prod), checked in
|
|
src/
|
|
compose.rs import docker-compose → typed model (loud on the unsupported)
|
|
chart.rs model + profile knobs → a hydrated helm chart (typed k8s)
|
|
publish.rs build images + (push to a registry | import to k3d)
|
|
score.rs ComposeAppScore: helm upgrade --install + Ingress
|
|
deploy.rs ComposeDeploy — the declarative builder (impls HarmonyApp)
|
|
main.rs the whole app, declared
|
|
```
|
|
|
|
## The app, declared (`main.rs`)
|
|
|
|
```rust
|
|
let app = ComposeDeploy::from_dir("timesheet", "./app")?
|
|
.expose("frontend", "timesheet.example.harmony.mcd")
|
|
.with(Postgres::managed()); // a managed CNPG database, wired in
|
|
harmony_cli::app::app_main(app).await
|
|
```
|
|
|
|
That declaration gets you `ship` / `deploy` / `status` / `logs` over any
|
|
`--context`, all from `harmony_app`. See
|
|
[Application CLI](../../docs/guides/application-cli.md) and
|
|
[Application Capabilities](../../docs/guides/application-capabilities.md).
|
|
|
|
## Design
|
|
|
|
- **compose = base, profile = deploy knobs.** The importer reads only images,
|
|
ports, env, volumes. Replicas, storage class, TLS, rolling strategy come
|
|
from the `Profile` the context carries (ADR-026 §6/§7), not from compose or
|
|
`Harmony.toml`. A behavioral knob in either is a defect.
|
|
- **Capabilities compose upward.** `.with(Postgres::managed())` /
|
|
`.with(Monitoring::new()...)` / `.with(ZitadelAuth::oidc(...))` each deploy
|
|
their own Scores and wire into the app **by reference** (a DB URL via
|
|
`secretKeyRef`, a client_id via `configMapKeyRef`) — no `ApplicationScore`,
|
|
no ArgoCD.
|
|
- **Same definition, local → prod.** Only the context changes; the *same*
|
|
`ComposeAppScore` converges on local k3d or a tenant cluster.
|
|
- **Loud, never lossy.** Bind mounts and unparseable ports are hard errors;
|
|
`depends_on`/`command`/`restart` are warned, never silently dropped.
|
|
|
|
## Run it (local k3d)
|
|
|
|
```sh
|
|
k3d cluster create compose-local --servers 1 --wait
|
|
kubectl --context k3d-compose-local create namespace timesheet
|
|
|
|
cd examples/compose_java_react # so the CLI finds .harmony/contexts.toml
|
|
cargo run --bin compose-deploy -- ship --context local # build → k3d import → deploy (+ Postgres)
|
|
cargo run --bin compose-deploy -- status --context local
|
|
cargo run --bin compose-deploy -- logs --context local --tail 50
|
|
```
|
|
|
|
Running from elsewhere? Point at the contexts file with
|
|
`--config <path>/.harmony/contexts.toml` (a context is always required — there
|
|
is no default, so you never hit the wrong cluster).
|
|
|
|
**Production** uses the same verbs against a prod context, with cluster
|
|
credentials brokered from OpenBao.
|
|
|
|
## Storage note (SQLite)
|
|
|
|
`Profile::Local` deploys single-replica with `Recreate` on a `ReadWriteOnce`
|
|
volume — safe for the demo's single-writer SQLite (one pod at a time). Prod
|
|
(`Profile::Prod`) replicates with `RollingUpdate` on `ReadWriteMany`; for a
|
|
real multi-writer store use `.with(Postgres::managed())` (the demo already
|
|
provisions it) and point the app at `DATABASE_URL`.
|