about summary refs log tree commit diff
path: root/ops/infra
diff options
context:
space:
mode:
Diffstat (limited to 'ops/infra')
-rw-r--r--ops/infra/.skip-subtree2
-rwxr-xr-xops/infra/dns/import11
-rw-r--r--ops/infra/dns/kontemplate-works15
-rw-r--r--ops/infra/dns/oslo-pub8
-rw-r--r--ops/infra/dns/root-tazj-in33
-rw-r--r--ops/infra/gcp/.gitignore3
-rw-r--r--ops/infra/gcp/default.tf111
-rw-r--r--ops/infra/kubernetes/cgit/config.yaml73
-rw-r--r--ops/infra/kubernetes/gemma/config.lisp19
-rw-r--r--ops/infra/kubernetes/https-cert/cert.yaml8
-rw-r--r--ops/infra/kubernetes/https-lb/ingress.yaml35
-rw-r--r--ops/infra/kubernetes/nginx/nginx.conf59
-rw-r--r--ops/infra/kubernetes/nginx/nginx.yaml60
-rw-r--r--ops/infra/kubernetes/nixery/config.yaml67
-rw-r--r--ops/infra/kubernetes/nixery/id_nixery.pub1
-rw-r--r--ops/infra/kubernetes/nixery/known_hosts2
-rw-r--r--ops/infra/kubernetes/nixery/secrets.yaml18
-rw-r--r--ops/infra/kubernetes/nixery/ssh_config4
-rw-r--r--ops/infra/kubernetes/primary-cluster.yaml38
-rw-r--r--ops/infra/kubernetes/tazblog/config.yaml34
20 files changed, 601 insertions, 0 deletions
diff --git a/ops/infra/.skip-subtree b/ops/infra/.skip-subtree
new file mode 100644
index 000000000000..cee24b75793c
--- /dev/null
+++ b/ops/infra/.skip-subtree
@@ -0,0 +1,2 @@
+Code under //ops/infra is mostly configuration for other tools, not
+Nix derivations to be built.
diff --git a/ops/infra/dns/import b/ops/infra/dns/import
new file mode 100755
index 000000000000..e79e426b5553
--- /dev/null
+++ b/ops/infra/dns/import
@@ -0,0 +1,11 @@
+#!/bin/sh
+set -ue
+
+# Imports a zone file into a Google Cloud DNS zone of the same name
+readonly ZONE="${1}"
+
+gcloud dns record-sets import "${ZONE}" \
+       --project composite-watch-759 \
+       --zone-file-format \
+       --delete-all-existing \
+       --zone "${ZONE}"
diff --git a/ops/infra/dns/kontemplate-works b/ops/infra/dns/kontemplate-works
new file mode 100644
index 000000000000..326a129d2105
--- /dev/null
+++ b/ops/infra/dns/kontemplate-works
@@ -0,0 +1,15 @@
+;;  -*- mode: zone; -*-
+;; Do not delete these
+kontemplate.works. 21600 IN NS ns-cloud-d1.googledomains.com.
+kontemplate.works. 21600 IN NS ns-cloud-d2.googledomains.com.
+kontemplate.works. 21600 IN NS ns-cloud-d3.googledomains.com.
+kontemplate.works. 21600 IN NS ns-cloud-d4.googledomains.com.
+kontemplate.works. 21600 IN SOA ns-cloud-d1.googledomains.com. cloud-dns-hostmaster.google.com. 4 21600 3600 259200 300
+
+;; Github site setup
+kontemplate.works. 60 IN A 185.199.108.153
+kontemplate.works. 60 IN A 185.199.109.153
+kontemplate.works. 60 IN A 185.199.110.153
+kontemplate.works. 60 IN A 185.199.111.153
+
+www.kontemplate.works. 60 IN CNAME tazjin.github.io.
diff --git a/ops/infra/dns/oslo-pub b/ops/infra/dns/oslo-pub
new file mode 100644
index 000000000000..674687484b90
--- /dev/null
+++ b/ops/infra/dns/oslo-pub
@@ -0,0 +1,8 @@
+;; Do not delete these
+oslo.pub. 21600 IN NS ns-cloud-c1.googledomains.com.
+oslo.pub. 21600 IN NS ns-cloud-c2.googledomains.com.
+oslo.pub. 21600 IN NS ns-cloud-c3.googledomains.com.
+oslo.pub. 21600 IN NS ns-cloud-c4.googledomains.com.
+oslo.pub. 21600 IN SOA ns-cloud-c1.googledomains.com. cloud-dns-hostmaster.google.com. 4 21600 3600 1209600 300
+
+oslo.pub. 60 IN A 46.21.106.241
diff --git a/ops/infra/dns/root-tazj-in b/ops/infra/dns/root-tazj-in
new file mode 100644
index 000000000000..43db5834a0ca
--- /dev/null
+++ b/ops/infra/dns/root-tazj-in
@@ -0,0 +1,33 @@
+;; -*- mode: zone; -*-
+;; Do not delete these
+tazj.in. 21600 IN NS ns-cloud-a1.googledomains.com.
+tazj.in. 21600 IN NS ns-cloud-a2.googledomains.com.
+tazj.in. 21600 IN NS ns-cloud-a3.googledomains.com.
+tazj.in. 21600 IN NS ns-cloud-a4.googledomains.com.
+tazj.in. 21600 IN SOA ns-cloud-a1.googledomains.com. cloud-dns-hostmaster.google.com. 123 21600 3600 1209600 300
+
+;; Email setup
+tazj.in. 300 IN MX 1 aspmx.l.google.com.
+tazj.in. 300 IN MX 5 alt1.aspmx.l.google.com.
+tazj.in. 300 IN MX 5 alt2.aspmx.l.google.com.
+tazj.in. 300 IN MX 10 alt3.aspmx.l.google.com.
+tazj.in. 300 IN MX 10 alt4.aspmx.l.google.com.
+tazj.in. 300 IN TXT "v=spf1 include:_spf.google.com ~all"
+google._domainkey.tazj.in. 21600 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9AphX/WJf8zVXQB5Jk0Ry1MI6ARa6vEyAoJtpjpt9Nbm7XU4qVWFRJm+L0VFd5EZ5YDPJTIZ90lJE3/B8vae2ipnoGbJbj8LaVSzzIPMbWmhPhX3fkLJFdkv7xRDMDn730iYXRlfkgv6GsqbS8vZt7mzxx4mpnePTI323yjRVkwRW8nGVbsmB25ZoG1/0985" "kg4mSYxzWeJ2ozCPFhT4sfMtZMXe/4QEkJz/zkod29KZfFJmLgEaf73WLdBX8kdwbhuh2PYXt/PwzUrRzF5ujVCsSaTZwdRVPErcf+yo4NvedelTjjs8rFVfoJiaDD1q2bQ3w0gDEBWPdC2VP7k9zwIDAQAB"
+
+;; Site verifications
+tazj.in. 3600 IN TXT "keybase-site-verification=gC4kzEmnLzY7F669PjN-pw2Cf__xHqcxQ08Gb-W9dhE"
+tazj.in. 300 IN TXT "google-site-verification=d3_MI1OwD6q2OT42Vvh0I9w2u3Q5KFBu-PieNUE1Fig"
+www.tazj.in. 3600 IN TXT "keybase-site-verification=ER8m_byyqAhzeIy9TyzkAU1H2p2yHtpvImuB_XrRF2U"
+
+;; Blog "storage engine"
+blog.tazj.in. 21600 IN NS ns-cloud-c1.googledomains.com.
+blog.tazj.in. 21600 IN NS ns-cloud-c2.googledomains.com.
+blog.tazj.in. 21600 IN NS ns-cloud-c3.googledomains.com.
+blog.tazj.in. 21600 IN NS ns-cloud-c4.googledomains.com.
+
+;; Webpage records setup
+tazj.in.       300 IN A 34.98.120.189
+www.tazj.in.   300 IN A 34.98.120.189
+git.tazj.in.   300 IN A 34.98.120.189
+files.tazj.in. 300 IN CNAME c.storage.googleapis.com.
diff --git a/ops/infra/gcp/.gitignore b/ops/infra/gcp/.gitignore
new file mode 100644
index 000000000000..96c7538dda8a
--- /dev/null
+++ b/ops/infra/gcp/.gitignore
@@ -0,0 +1,3 @@
+.terraform
+*.tfstate
+*.tfstate.backup
diff --git a/ops/infra/gcp/default.tf b/ops/infra/gcp/default.tf
new file mode 100644
index 000000000000..2cb57836fa6d
--- /dev/null
+++ b/ops/infra/gcp/default.tf
@@ -0,0 +1,111 @@
+# Terraform configuration for the GCP project 'tazjins-infrastructure'
+
+provider "google" {
+  project = "tazjins-infrastructure"
+  region  = "europe-north1"
+}
+
+# Configure a storage bucket in which to keep Terraform state and
+# other data, such as Nixery's layers.
+resource "google_storage_bucket" "tazjins-data" {
+  name     = "tazjins-data"
+  location = "EU"
+}
+
+terraform {
+  backend "gcs" {
+    bucket = "tazjins-data"
+    prefix = "terraform"
+  }
+}
+
+# Configure enabled APIs
+resource "google_project_services" "primary" {
+  project = "tazjins-infrastructure"
+  services = [
+    "bigquery-json.googleapis.com",
+    "bigquerystorage.googleapis.com",
+    "cloudapis.googleapis.com",
+    "clouddebugger.googleapis.com",
+    "cloudfunctions.googleapis.com",
+    "cloudkms.googleapis.com",
+    "cloudtrace.googleapis.com",
+    "compute.googleapis.com",
+    "container.googleapis.com",
+    "containerregistry.googleapis.com",
+    "datastore.googleapis.com",
+    "dns.googleapis.com",
+    "iam.googleapis.com",
+    "iamcredentials.googleapis.com",
+    "logging.googleapis.com",
+    "monitoring.googleapis.com",
+    "oslogin.googleapis.com",
+    "pubsub.googleapis.com",
+    "run.googleapis.com",
+    "servicemanagement.googleapis.com",
+    "serviceusage.googleapis.com",
+    "sourcerepo.googleapis.com",
+    "sql-component.googleapis.com",
+    "storage-api.googleapis.com",
+    "storage-component.googleapis.com",
+  ]
+}
+
+
+# Configure the main Kubernetes cluster in which services are deployed
+resource "google_container_cluster" "primary" {
+  name     = "tazjin-cluster"
+  location = "europe-north1"
+
+  remove_default_node_pool = true
+  initial_node_count       = 1
+}
+
+resource "google_container_node_pool" "primary_nodes" {
+  name       = "primary-nodes"
+  location   = "europe-north1"
+  cluster    = google_container_cluster.primary.name
+  node_count = 1
+
+  node_config {
+    preemptible  = true
+    machine_type = "n1-standard-2"
+
+    oauth_scopes = [
+      "storage-rw",
+      "logging-write",
+      "monitoring",
+      "https://www.googleapis.com/auth/source.read_only",
+    ]
+  }
+}
+
+# Configure a service account for which GCS URL signing keys can be created.
+resource "google_service_account" "nixery" {
+  account_id   = "nixery"
+  display_name = "Nixery service account"
+}
+
+# Configure Cloud KMS for secret encryption
+resource "google_kms_key_ring" "tazjins_keys" {
+  name     = "tazjins-keys"
+  location = "europe-north1"
+
+  lifecycle {
+    prevent_destroy = true
+  }
+}
+
+resource "google_kms_crypto_key" "kontemplate_key" {
+  name     = "kontemplate-key"
+  key_ring = google_kms_key_ring.tazjins_keys.id
+
+  lifecycle {
+    prevent_destroy = true
+  }
+}
+
+# Configure the git repository that contains everything.
+resource "google_sourcerepo_repository" "depot" {
+  name = "depot"
+}
diff --git a/ops/infra/kubernetes/cgit/config.yaml b/ops/infra/kubernetes/cgit/config.yaml
new file mode 100644
index 000000000000..43bfe9d7fb45
--- /dev/null
+++ b/ops/infra/kubernetes/cgit/config.yaml
@@ -0,0 +1,73 @@
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: gcsr-secrets
+type: Opaque
+data:
+  username: "Z2l0LXRhemppbi5nbWFpbC5jb20="
+  # This credential is a GCSR 'gitcookie' token.
+  password: '{{ passLookup "gcsr-tazjin-password" | b64enc }}'
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: cgit
+  labels:
+    app: cgit
+spec:
+  replicas: 2
+  selector:
+    matchLabels:
+      app: cgit
+  template:
+    metadata:
+      labels:
+        app: cgit
+    spec:
+      securityContext:
+        runAsUser: 1000
+        runAsGroup: 1000
+        fsGroup: 1000
+      containers:
+      - name: cgit
+        image: nixery.local/shell/web.cgit-taz:{{ gitHEAD }}
+        command: [ "cgit-launch" ]
+        env:
+          - name: HOME
+            value: /git
+        volumeMounts:
+          - name: git-volume
+            mountPath: /git
+      - name: sync-gcsr
+        image: nixery.local/shell/ops.sync-gcsr:{{ gitHEAD }}
+        command: [ "sync-gcsr" ]
+        env:
+          - name: SYNC_USER
+            valueFrom:
+              secretKeyRef:
+                name: gcsr-secrets
+                key: username
+          - name: SYNC_PASS
+            valueFrom:
+              secretKeyRef:
+                name: gcsr-secrets
+                key: password
+        volumeMounts:
+          - name: git-volume
+            mountPath: /git
+      volumes:
+        - name: git-volume
+          emptyDir: {}
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: cgit
+spec:
+  selector:
+    app: cgit
+  ports:
+    - protocol: TCP
+      port: 80
+      targetPort: 8080
diff --git a/ops/infra/kubernetes/gemma/config.lisp b/ops/infra/kubernetes/gemma/config.lisp
new file mode 100644
index 000000000000..517a658cf150
--- /dev/null
+++ b/ops/infra/kubernetes/gemma/config.lisp
@@ -0,0 +1,19 @@
+(config :port 4242
+        :data-dir "/var/lib/gemma/")
+
+(deftask bathroom/wipe-mirror 7)
+(deftask bathroom/wipe-counter 7)
+
+;; Bedroom tasks
+(deftask bedroom/change-sheets 7)
+(deftask bedroom/vacuum 10)
+
+;; Kitchen tasks
+(deftask kitchen/normal-trash 3)
+(deftask kitchen/green-trash 5)
+(deftask kitchen/blue-trash 5)
+(deftask kitchen/wipe-counters 3)
+(deftask kitchen/vacuum 5 "Kitchen has more crumbs and such!")
+
+;; Entire place
+(deftask clean-windows 60)
diff --git a/ops/infra/kubernetes/https-cert/cert.yaml b/ops/infra/kubernetes/https-cert/cert.yaml
new file mode 100644
index 000000000000..c7a85275ae67
--- /dev/null
+++ b/ops/infra/kubernetes/https-cert/cert.yaml
@@ -0,0 +1,8 @@
+---
+apiVersion: networking.gke.io/v1beta1
+kind: ManagedCertificate
+metadata:
+  name: {{ .domain | replace "." "-" }}
+spec:
+  domains:
+    - {{ .domain }}
diff --git a/ops/infra/kubernetes/https-lb/ingress.yaml b/ops/infra/kubernetes/https-lb/ingress.yaml
new file mode 100644
index 000000000000..069771a4217f
--- /dev/null
+++ b/ops/infra/kubernetes/https-lb/ingress.yaml
@@ -0,0 +1,35 @@
+# This resource configures the HTTPS load balancer that is used as the
+# entrypoint to all HTTPS services running in the cluster.
+---
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+  name: https-ingress
+  annotations:
+    networking.gke.io/managed-certificates: tazj-in, git-tazj-in, www-tazj-in, oslo-pub
+spec:
+  rules:
+    # Route blog to the blog ...
+    - host: tazj.in
+      http:
+        paths:
+          - path: /*
+            backend:
+              serviceName: tazblog
+              servicePort: 8000
+    # Route git.tazj.in to the cgit pods
+    - host: git.tazj.in
+      http:
+        paths:
+          - path: /*
+            backend:
+              serviceName: nginx
+              servicePort: 6756
+    # Route oslo.pub to the nginx instance which serves redirects
+    - host: oslo.pub
+      http:
+        paths:
+          - path: /
+            backend:
+              serviceName: nginx
+              servicePort: 6756
diff --git a/ops/infra/kubernetes/nginx/nginx.conf b/ops/infra/kubernetes/nginx/nginx.conf
new file mode 100644
index 000000000000..918aa6067806
--- /dev/null
+++ b/ops/infra/kubernetes/nginx/nginx.conf
@@ -0,0 +1,59 @@
+daemon off;
+worker_processes  1;
+error_log stderr;
+pid /run/nginx.pid;
+
+events {
+    worker_connections  1024;
+}
+
+http {
+    log_format json_combined escape=json
+    '{'
+        '"time_local":"$time_local",'
+        '"remote_addr":"$remote_addr",'
+        '"remote_user":"$remote_user",'
+        '"request":"$request",'
+        '"status": "$status",'
+        '"body_bytes_sent":"$body_bytes_sent",'
+        '"request_time":"$request_time",'
+        '"http_referrer":"$http_referer",'
+        '"http_user_agent":"$http_user_agent"'
+        '}';
+
+    access_log /dev/stdout json_combined;
+
+    sendfile        on;
+    keepalive_timeout  65;
+
+    server {
+        listen 80 default_server;
+        location / {
+            return 200 "ok";
+        }
+    }
+
+    server {
+        listen       80;
+        server_name  oslo.pub;
+
+        location / {
+            return 302 https://www.google.com/maps/d/viewer?mid=1pJIYY9cuEdt9DuMTbb4etBVq7hs;
+        }
+    }
+
+    server {
+        listen       80;
+        server_name  git.tazj.in;
+
+        # Static assets must always hit the root.
+        location ~ ^/(favicon\.ico|cgit\.(css|png))$ {
+           proxy_pass http://cgit;
+        }
+
+        # Everything else hits the depot directly.
+        location / {
+            proxy_pass http://cgit/cgit.cgi/depot/;
+        }
+    }
+}
diff --git a/ops/infra/kubernetes/nginx/nginx.yaml b/ops/infra/kubernetes/nginx/nginx.yaml
new file mode 100644
index 000000000000..983b265bafab
--- /dev/null
+++ b/ops/infra/kubernetes/nginx/nginx.yaml
@@ -0,0 +1,60 @@
+# Deploy an nginx instance which serves ... redirects.
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: nginx-conf
+data:
+  nginx.conf: {{ insertFile "nginx.conf" | toJson }}
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx
+  labels:
+    app: nginx
+spec:
+  replicas: 2
+  selector:
+    matchLabels:
+      app: nginx
+  template:
+    metadata:
+      labels:
+        app: nginx
+        config: {{ insertFile "nginx.conf" | sha1sum }}
+    spec:
+      containers:
+        - name: tazblog
+          image: nixery.local/shell/third_party.nginx:{{ .version }}
+          command: ["/bin/bash", "-c"]
+          args:
+            - |
+              cd /run
+              echo 'nogroup:x:30000:nobody' >> /etc/group
+              echo 'nobody:x:30000:30000:nobody:/tmp:/bin/bash' >> /etc/passwd
+              exec nginx -c /etc/nginx/nginx.conf
+          volumeMounts:
+            - name: nginx-conf
+              mountPath: /etc/nginx
+            - name: nginx-rundir
+              mountPath: /run
+      volumes:
+        - name: nginx-conf
+          configMap:
+            name: nginx-conf
+        - name: nginx-rundir
+          emptyDir: {}
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: nginx
+spec:
+  type: NodePort
+  selector:
+    app: nginx
+  ports:
+    - protocol: TCP
+      port: 6756
+      targetPort: 80
diff --git a/ops/infra/kubernetes/nixery/config.yaml b/ops/infra/kubernetes/nixery/config.yaml
new file mode 100644
index 000000000000..0775e79b5843
--- /dev/null
+++ b/ops/infra/kubernetes/nixery/config.yaml
@@ -0,0 +1,67 @@
+# Deploys an instance of Nixery into the cluster.
+#
+# The service via which Nixery is exposed has a private DNS entry
+# pointing to it, which makes it possible to resolve `nixery.local`
+# in-cluster without things getting nasty.
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nixery
+  namespace: kube-public
+  labels:
+    app: nixery
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: nixery
+  template:
+    metadata:
+      labels:
+        app: nixery
+    spec:
+      containers:
+      - name: nixery
+        image: eu.gcr.io/tazjins-infrastructure/nixery:{{ .version }}
+        volumeMounts:
+          - name: nixery-secrets
+            mountPath: /var/nixery
+        env:
+          - name: BUCKET
+            value: {{ .bucket}}
+          - name: PORT
+            value: "{{ .port }}"
+          - name: GOOGLE_APPLICATION_CREDENTIALS
+            value: /var/nixery/gcs-key.json
+          - name: GCS_SIGNING_KEY
+            value: /var/nixery/gcs-key.pem
+          - name: GCS_SIGNING_ACCOUNT
+            value: {{ .account }}
+          - name: GIT_SSH_COMMAND
+            value: 'ssh -F /var/nixery/ssh_config'
+          - name: NIXERY_PKGS_REPO
+            value: {{ .repo }}
+          - name: NIX_POPULARITY_URL
+            value: 'https://storage.googleapis.com/nixery-layers/popularity/{{ .popularity }}'
+      volumes:
+        - name: nixery-secrets
+          secret:
+            secretName: nixery-secrets
+            defaultMode: 256
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: nixery
+  namespace: kube-public
+  annotations:
+    cloud.google.com/load-balancer-type: "Internal"
+spec:
+  selector:
+    app: nixery
+  type: LoadBalancer
+  ports:
+  - protocol: TCP
+    port: 80
+    targetPort: 8080
diff --git a/ops/infra/kubernetes/nixery/id_nixery.pub b/ops/infra/kubernetes/nixery/id_nixery.pub
new file mode 100644
index 000000000000..dc3fd617d0a1
--- /dev/null
+++ b/ops/infra/kubernetes/nixery/id_nixery.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCzBM6ydst77jDHNcTFWKD9Fw4SReqyNEEp2MtQBk2wt94U4yLp8MQIuNeOEn1GaDEX4RGCxqai/2UVF1w9ZNdU+v2fXcKWfkKuGQH2XcNfXor2cVNObd40H78++iZiv3nmM/NaEdkTbTBbi925cRy9u5FgItDgsJlyKNRglCb0fr6KlgpvWjL20dp/eeZ8a/gLniHK8PnEsgERQSvJnsyFpxxVhxtoUiyLWpXDl4npf/rQr0eRDf4Q5sN/nbTwksapPHfze8dKcaoA7A2NqT3bJ6DPGrwVCzGRtGw/SXJwFwmmtAl9O6BklpeReyiknSxc+KOtrjDW6O0r6yvymD5Z nixery
diff --git a/ops/infra/kubernetes/nixery/known_hosts b/ops/infra/kubernetes/nixery/known_hosts
new file mode 100644
index 000000000000..6a2f84b5fb60
--- /dev/null
+++ b/ops/infra/kubernetes/nixery/known_hosts
@@ -0,0 +1,2 @@
+github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
+140.82.118.4 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
diff --git a/ops/infra/kubernetes/nixery/secrets.yaml b/ops/infra/kubernetes/nixery/secrets.yaml
new file mode 100644
index 000000000000..d9a674d2c9fc
--- /dev/null
+++ b/ops/infra/kubernetes/nixery/secrets.yaml
@@ -0,0 +1,18 @@
+# The secrets below are encrypted using keys stored in Cloud KMS and
+# templated in by kontemplate when deploying.
+#
+# Not all of the values are actually secret (see the matching)
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: nixery-secrets
+  namespace: kube-public
+type: Opaque
+data:
+  gcs-key.json: {{ passLookup "nixery-gcs-json" | b64enc }}
+  gcs-key.pem: {{ passLookup "nixery-gcs-pem" | b64enc }}
+  id_nixery: {{ printf "%s\n" (passLookup "nixery-ssh-private") | b64enc }}
+  id_nixery.pub: {{ insertFile "id_nixery.pub" | b64enc }}
+  known_hosts: {{ insertFile "known_hosts" | b64enc }}
+  ssh_config: {{ insertFile "ssh_config" | b64enc }}
diff --git a/ops/infra/kubernetes/nixery/ssh_config b/ops/infra/kubernetes/nixery/ssh_config
new file mode 100644
index 000000000000..78afbb0b039d
--- /dev/null
+++ b/ops/infra/kubernetes/nixery/ssh_config
@@ -0,0 +1,4 @@
+Match host *
+      User tazjin@google.com
+      IdentityFile /var/nixery/id_nixery
+      UserKnownHostsFile /var/nixery/known_hosts
diff --git a/ops/infra/kubernetes/primary-cluster.yaml b/ops/infra/kubernetes/primary-cluster.yaml
new file mode 100644
index 000000000000..1d5d33e0bb26
--- /dev/null
+++ b/ops/infra/kubernetes/primary-cluster.yaml
@@ -0,0 +1,38 @@
+# Kontemplate configuration for the primary GKE cluster in the project
+# 'tazjins-infrastructure'.
+---
+context: gke_tazjins-infrastructure_europe-north1_tazjin-cluster
+include:
+  # SSL certificates (provisioned by Google)
+  - name: tazj-in-cert
+    path: https-cert
+    values:
+      domain: tazj.in
+  - name: www-tazj-in-cert
+    path: https-cert
+    values:
+      domain: www.tazj.in
+  - name: git-tazj-in-cert
+    path: https-cert
+    values:
+      domain: git.tazj.in
+  - name: oslo-pub-cert
+    path: https-cert
+    values:
+      domain: oslo.pub
+
+  # Services
+  - name: nixery
+    values:
+      port: 8080
+      version: xkm36vrbcnzxdccybzdrx4qzfcfqfrhg
+      bucket: tazjins-data
+      account: nixery@tazjins-infrastructure.iam.gserviceaccount.com
+      repo: ssh://tazjin@gmail.com@source.developers.google.com:2022/p/tazjins-infrastructure/r/depot
+      popularity: 'popularity-nixos-unstable-3140fa89c51233397f496f49014f6b23216667c2.json'
+  - name: tazblog
+  - name: cgit
+  - name: https-lb
+  - name: nginx
+    values:
+      version: a349d5e9145ae9a6c89f62ec631f01fb180de546
diff --git a/ops/infra/kubernetes/tazblog/config.yaml b/ops/infra/kubernetes/tazblog/config.yaml
new file mode 100644
index 000000000000..dc63ce8e4b38
--- /dev/null
+++ b/ops/infra/kubernetes/tazblog/config.yaml
@@ -0,0 +1,34 @@
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: tazblog
+  labels:
+    app: tazblog
+spec:
+  replicas: 2
+  selector:
+    matchLabels:
+      app: tazblog
+  template:
+    metadata:
+      labels:
+        app: tazblog
+    spec:
+      containers:
+      - name: tazblog
+        image: nixery.local/shell/web.tazblog:{{ gitHEAD }}
+        command: [ "tazblog" ]
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: tazblog
+spec:
+  type: NodePort
+  selector:
+    app: tazblog
+  ports:
+    - protocol: TCP
+      port: 8000
+      targetPort: 8000