Project SeaSense: From Zero to Production on European Infrastructure
The Mission
Project SeaSense is building a global network of ocean sensors through citizen science and sailing communities. Sailors carry low-cost sensor buoys on their voyages, collecting water temperature, salinity, dissolved oxygen, pH, and other oceanographic data. The data is transmitted to a central database and made publicly available for climate research.
The nonprofit is based in Amsterdam. They had a working application — a backend API, a frontend, and a PostgreSQL database with TimescaleDB for time-series sensor data. What they did not have was infrastructure. No Kubernetes cluster. No CI/CD pipeline. No deployment process beyond a developer's laptop.
Aknostic joined as a pro-bono sponsor to build their production platform.
Starting Point
The SeaSense team knew how to write software. They did not know how to run it in production — and they should not have to. They are ocean scientists and engineers building sensor hardware, not platform operators.
The requirements were clear:
- Production-grade. Real users depend on this data. The platform needs to stay up.
- Highly available. Sensor data arrives continuously from boats at sea. Downtime means lost measurements that cannot be recaptured.
- European-hosted. An Amsterdam-based nonprofit collecting ocean data for European research institutions. The infrastructure should match.
- Operable by non-infrastructure people. No one on the team should need to learn kubectl. Deployments happen through Git.
- Cheap to run. Nonprofit budget. Every euro spent on infrastructure is a euro not spent on sensors.
Why Scaleway, Why CNCF
Every technology choice had to pass three tests: is it European or open source? Can it run without a dedicated platform team? Is it cost-effective at small scale?
Scaleway over AWS or GCP. Scaleway is French-owned, operates data centres in Paris, and prices aggressively for small workloads. Two dev1-m instances cost a fraction of equivalent EC2 or GKE nodes. More importantly, the data stays in France — no CLOUD Act exposure, no transatlantic data transfers.
OpenTofu over Terraform. After HashiCorp changed Terraform's license, the open-source fork OpenTofu became the safer choice. It is governed by the Linux Foundation, uses the same configuration language, and works with the same providers. For a project that values independence, depending on a single company's licensing decisions is unnecessary risk.
Flux CD for GitOps. The SeaSense team does not need to learn Kubernetes. They need to push code to Git and have it appear in production. Flux CD watches the repository and reconciles the cluster state automatically. No manual kubectl commands. No deployment scripts. Git is the single source of truth.
Everything else — CNCF or open source. Envoy Gateway for ingress. cert-manager for TLS. CloudNativePG for the database. SOPS with age encryption for secrets. No proprietary components. If Scaleway disappeared tomorrow, the entire stack could move to any Kubernetes provider with a configuration change.
Building the Platform
Infrastructure Layer
The foundation is a private Kubernetes cluster on Scaleway, defined in OpenTofu modules:
resource "scaleway_vpc" "main" {
name = "${var.project_name}-${var.environment}-vpc"
region = var.region
}
resource "scaleway_vpc_private_network" "main" {
name = "${var.project_name}-${var.environment}-network"
vpc_id = scaleway_vpc.main.id
region = var.region
ipv4_subnet {
subnet = var.subnet_cidr
}
}
resource "scaleway_vpc_public_gateway" "main" {
name = "${var.project_name}-${var.environment}-gateway"
type = var.gateway_type
zone = var.zone
ip_id = scaleway_vpc_public_gateway_ip.main.id
}
resource "scaleway_vpc_gateway_network" "main" {
gateway_id = scaleway_vpc_public_gateway.main.id
private_network_id = scaleway_vpc_private_network.main.id
enable_masquerade = true
ipam_config {
push_default_route = true
}
}The nodes have no public IP addresses. All egress routes through the VPC gateway with a static IP — useful for allowlisting and predictable networking. Two node pools span two availability zones in Paris:
resource "scaleway_k8s_cluster" "main" {
name = var.cluster_name
version = var.kubernetes_version
cni = var.cni # cilium
region = var.region
private_network_id = var.private_network_id
auto_upgrade {
enable = true
maintenance_window_day = var.maintenance_window_day
maintenance_window_start_hour = var.maintenance_window_start_hour
}
}
resource "scaleway_k8s_pool" "pool_1" {
cluster_id = scaleway_k8s_cluster.main.id
name = "${var.cluster_name}-pool-1"
node_type = var.node_type
size = var.node_count
autoscaling = true
autohealing = true
public_ip_disabled = true
zone = "fr-par-1"
depends_on = [time_sleep.wait_for_gateway]
}
resource "scaleway_k8s_pool" "pool_2" {
cluster_id = scaleway_k8s_cluster.main.id
name = "${var.cluster_name}-pool-2"
node_type = var.node_type
size = var.node_count
autoscaling = true
autohealing = true
public_ip_disabled = true
zone = "fr-par-2"
depends_on = [time_sleep.wait_for_gateway]
}The depends_on with time_sleep is a practical detail: private nodes need the gateway route to propagate before they can pull container images. Without the delay, node creation fails silently. We spent an afternoon figuring that one out.
GitOps Layer
Flux CD manages everything after the cluster exists. The deployment is structured in three phases with explicit dependencies:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: infrastructure-core
namespace: flux-system
spec:
interval: 10m
sourceRef:
kind: GitRepository
name: flux-system
path: ./gitops/infrastructure/core
prune: true
wait: true
decryption:
provider: sops
secretRef:
name: sops-age
healthChecks:
- apiVersion: apps/v1
kind: Deployment
name: cert-manager
namespace: cert-manager
- apiVersion: apps/v1
kind: Deployment
name: envoy-gateway
namespace: envoy-gateway-system
- apiVersion: apps/v1
kind: Deployment
name: cnpg-controller-manager
namespace: cnpg-system
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: gateway-config
namespace: flux-system
spec:
interval: 10m
sourceRef:
kind: GitRepository
name: flux-system
path: ./gitops/infrastructure/gateway-config
prune: true
wait: true
dependsOn:
- name: infrastructure-core
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: apps
namespace: flux-system
spec:
interval: 5m
sourceRef:
kind: GitRepository
name: flux-system
path: ./gitops/apps
prune: true
wait: true
dependsOn:
- name: gateway-config
decryption:
provider: sops
secretRef:
name: sops-agePhase one deploys infrastructure components: cert-manager, Envoy Gateway, CloudNativePG operator. Health checks ensure each is ready before proceeding. Phase two configures the gateway and TLS certificates. Phase three deploys the application. If cert-manager is not healthy, the gateway configuration waits. If the gateway is not ready, the application waits. No race conditions.
Secrets are encrypted with SOPS and age. The encryption key lives in the cluster as a Kubernetes secret, injected during Flux bootstrap. Encrypted secrets live in Git alongside everything else — reviewable, auditable, version-controlled. The .sops.yaml configuration targets only the data and stringData fields in YAML files under the gitops directory:
creation_rules:
- path_regex: gitops/.*\.yaml$
encrypted_regex: ^(data|stringData)$
age: age16fvm5fwdqcurnp7rr84dsdzdkp7jku2tp32xyc3f5r28njch2ypsnd9m40Ingress and TLS
Envoy Gateway handles HTTP routing with path-based rules. API requests go to the backend, everything else goes to the frontend:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: seasense
namespace: seasense
spec:
parentRefs:
- name: main
namespace: envoy-gateway-system
hostnames:
- api.projectseasense.org
rules:
- matches:
- path:
type: PathPrefix
value: /api/
backendRefs:
- name: seasense-backend
port: 8000
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: seasense-frontend
port: 80TLS certificates are provisioned automatically by cert-manager using Let's Encrypt with DNS-01 validation through Cloudflare. No manual certificate management. No renewal reminders. It just works.
CI/CD
A GitHub Actions pipeline orchestrates the full lifecycle: run OpenTofu to provision or update the cluster, then bootstrap Flux to take over application deployment. The pipeline checks whether Flux is already installed — if so, it triggers a reconciliation instead of re-bootstrapping. Run it ten times, same result.
The Database
We started with a simple PostgreSQL StatefulSet. It worked. Then we looked at what "production-grade" actually means for a database that stores irreplaceable ocean sensor data, and upgraded to CloudNativePG.
CloudNativePG is a Kubernetes operator purpose-built for PostgreSQL. It handles replication, failover, backups, and point-in-time recovery. For a team without a DBA, this is the difference between "we hope the database is fine" and "we know we can recover."
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: seasense-db
namespace: seasense
spec:
instances: 2
imageName: ghcr.io/clevyr/cloudnativepg-timescale:14-ts2
postgresql:
shared_preload_libraries:
- timescaledb
parameters:
shared_buffers: "64MB"
effective_cache_size: "256MB"
work_mem: "4MB"
max_connections: "20"
timescaledb.max_background_workers: "2"
bootstrap:
initdb:
database: seasense
owner: seasense
postInitTemplateSQL:
- CREATE EXTENSION IF NOT EXISTS timescaledb
storage:
size: 10Gi
backup:
barmanObjectStore:
destinationPath: s3://seasense-backups/
endpointURL: https://s3.fr-par.scw.cloud
s3Credentials:
accessKeyId:
name: cnpg-backup-credentials
key: ACCESS_KEY_ID
secretAccessKey:
name: cnpg-backup-credentials
key: ACCESS_SECRET_KEY
retentionPolicy: "30d"Two instances: one primary, one replica in a different availability zone. Automated daily backups to Scaleway S3 with 30-day retention. Point-in-time recovery from WAL archives. TimescaleDB pre-loaded for time-series sensor data.
The migration from StatefulSet to CloudNativePG happened in the second month. We were honest with the team: the StatefulSet was good enough to launch, but not good enough for data you cannot recollect. Ocean measurements from a sailing voyage are gone if you lose them. That conversation made the migration priority clear.
Making It Operable
The most important deliverable was not the infrastructure. It was the 442-line README.
The README walks through everything a developer needs to deploy, update, and troubleshoot the platform — without understanding Kubernetes internals. Deployment examples. Environment variable references. Common operations. Troubleshooting steps.
The deployment workflow for a SeaSense developer:
- Change application code or configuration
- Commit and push to the main branch
- Flux detects the change within 60 seconds
- The cluster reconciles to match the new state
- Done
No Docker commands. No Kubernetes manifests to edit. No SSH into servers. The platform is invisible — which is exactly how infrastructure should feel to a team focused on ocean science.
We also wrote planning documents for every significant change — the initial infrastructure setup (842 lines), the CloudNativePG migration (601 lines), incident reports with root cause analysis. When Aknostic's engagement ends, the team has a complete paper trail of every architectural decision and why it was made.
Outcomes
| Aspect | Detail | |--------|--------| | Infrastructure cost | < EUR 50/month (2x Scaleway dev1-m + S3 storage) | | Availability | 2 nodes across 2 availability zones (fr-par-1, fr-par-2) | | Database | HA PostgreSQL with TimescaleDB, automated daily backups, 30-day retention, PITR | | Vendor lock-in | Zero — 100% CNCF and open-source stack | | Deployment model | Git push → Flux reconciliation → production (< 60 seconds) | | Team requirement | No platform engineers needed for day-to-day operations | | Data residency | All data in Scaleway Paris (fr-par) | | Time to production | ~2 months from zero infrastructure |
Lessons Learned
-
Start with GitOps from day one. Retrofitting GitOps onto an existing cluster is painful. Starting with Flux from the first deployment means the team never learns bad habits like manual kubectl applies. Every change has a commit. Every rollback is a revert.
-
CloudNativePG is worth it even for small deployments. The operational overhead of the operator is near zero — it runs as a single pod using minimal resources. What you get in return — automated failover, backups, and PITR — is disproportionately valuable. For data you cannot recollect, "we'll set up backups later" is not an acceptable plan.
-
Documentation is the real deliverable. For a team without platform engineers, the README and planning documents matter more than the infrastructure itself. The infrastructure will need changes. The documentation is what enables the team to make those changes — or to onboard someone who can — without calling us.
-
European infrastructure is practical, not ideological. Scaleway's pricing for small workloads undercuts AWS and GCP. The dev1-m instances are cheap, the S3-compatible storage is cheap, and the managed Kubernetes offering is mature. We chose Scaleway because it was the right tool, not because of a flag on a map. The sovereignty is a bonus.