feat: add deploy-static-site action, site-lib, images; remove deploy-oci-site
Content now served from S3 at runtime via shared static-server image. - deploy-static-site: reads creds from Nomad vars, builds site, pushes tarball to S3, generates per-domain Nomad job JSON, deploys - generate-job.py: emits Nomad job JSON for a static site deployment - site-lib/flake.nix: mkSite helper, packages.default + devShells only - images/flake.nix: shared static-server OCI image (sws + awscli2 + tools) - images CI: builds and pushes static-server on images/flake.nix changes - deploy-oci-site: removed (superseded by deploy-static-site) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
04c2b06c14
commit
55652569b2
7 changed files with 294 additions and 147 deletions
|
|
@ -1,97 +0,0 @@
|
||||||
name: Deploy OCI Site
|
|
||||||
description: Build a Nix flake OCI image, push to registry, and deploy via Nomad
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
domain:
|
|
||||||
description: 'Domain the site is served at (e.g. toph.so)'
|
|
||||||
required: true
|
|
||||||
|
|
||||||
registry:
|
|
||||||
description: 'Container registry host'
|
|
||||||
required: false
|
|
||||||
default: 'registry.toph.so'
|
|
||||||
|
|
||||||
s3-endpoint:
|
|
||||||
description: 'S3 endpoint for the Nix binary cache'
|
|
||||||
required: false
|
|
||||||
default: 'https://s3.toph.so'
|
|
||||||
|
|
||||||
nomad-addr:
|
|
||||||
description: 'Nomad API address'
|
|
||||||
required: false
|
|
||||||
default: 'http://172.17.0.1:4646'
|
|
||||||
|
|
||||||
smoke-test:
|
|
||||||
description: 'Run a smoke test against the domain after deploy'
|
|
||||||
required: false
|
|
||||||
default: 'true'
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- name: Install tools
|
|
||||||
shell: bash
|
|
||||||
run: nix profile install nixpkgs#skopeo nixpkgs#nomad
|
|
||||||
|
|
||||||
- name: Build OCI image
|
|
||||||
shell: bash
|
|
||||||
run: nix build .#ociImage --out-link result-image
|
|
||||||
|
|
||||||
- name: Push image to registry
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
IMAGE_TAG=$(nix eval --raw .#packages.x86_64-linux.ociImage.imageTag)
|
|
||||||
skopeo copy \
|
|
||||||
docker-archive:./result-image \
|
|
||||||
docker://${{ inputs.registry }}/${{ inputs.domain }}:${IMAGE_TAG}
|
|
||||||
skopeo copy \
|
|
||||||
docker-archive:./result-image \
|
|
||||||
docker://${{ inputs.registry }}/${{ inputs.domain }}:latest
|
|
||||||
echo "IMAGE_TAG=${IMAGE_TAG}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Sign and push Nix closure to cache
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "${NIX_SIGNING_KEY}" > /tmp/nix-key
|
|
||||||
nix store sign -k /tmp/nix-key --recursive ./result-image
|
|
||||||
nix copy \
|
|
||||||
--to "s3://nix-cache?endpoint=${{ inputs.s3-endpoint }}&access-key-id=${S3_ACCESS_KEY}&secret-access-key=${S3_SECRET_KEY}" \
|
|
||||||
./result-image
|
|
||||||
rm /tmp/nix-key
|
|
||||||
env:
|
|
||||||
NIX_SIGNING_KEY: ${{ env.NIX_SIGNING_KEY }}
|
|
||||||
S3_ACCESS_KEY: ${{ env.S3_ACCESS_KEY }}
|
|
||||||
S3_SECRET_KEY: ${{ env.S3_SECRET_KEY }}
|
|
||||||
|
|
||||||
- name: Register Nomad job
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
nomad job run "${{ github.action_path }}/static-site.hcl"
|
|
||||||
env:
|
|
||||||
NOMAD_ADDR: ${{ inputs.nomad-addr }}
|
|
||||||
|
|
||||||
- name: Dispatch deployment
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
nomad job dispatch \
|
|
||||||
-meta "image_tag=${IMAGE_TAG}" \
|
|
||||||
-meta "domain=${{ inputs.domain }}" \
|
|
||||||
static-site
|
|
||||||
env:
|
|
||||||
NOMAD_ADDR: ${{ inputs.nomad-addr }}
|
|
||||||
|
|
||||||
- name: Smoke test
|
|
||||||
if: inputs.smoke-test == 'true'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "Waiting for deployment..."
|
|
||||||
for i in $(seq 1 12); do
|
|
||||||
if curl -sf --max-time 5 "https://${{ inputs.domain }}/" > /dev/null; then
|
|
||||||
echo "OK — https://${{ inputs.domain }}/ is up"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "Attempt ${i}/12 — retrying in 5s..."
|
|
||||||
sleep 5
|
|
||||||
done
|
|
||||||
echo "Smoke test failed: https://${{ inputs.domain }}/ did not respond"
|
|
||||||
exit 1
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
job "static-site" {
|
|
||||||
namespace = "static-sites"
|
|
||||||
type = "service"
|
|
||||||
|
|
||||||
parameterized {
|
|
||||||
meta_required = ["image_tag", "domain"]
|
|
||||||
}
|
|
||||||
|
|
||||||
group "site" {
|
|
||||||
count = 1
|
|
||||||
|
|
||||||
network {
|
|
||||||
port "http" { to = 8080 }
|
|
||||||
}
|
|
||||||
|
|
||||||
service {
|
|
||||||
name = "static-site-${NOMAD_META_domain}"
|
|
||||||
port = "http"
|
|
||||||
provider = "nomad"
|
|
||||||
|
|
||||||
tags = [
|
|
||||||
"traefik.enable=true",
|
|
||||||
"traefik.http.routers.${NOMAD_META_domain}.rule=Host(`${NOMAD_META_domain}`)",
|
|
||||||
"traefik.http.routers.${NOMAD_META_domain}.entrypoints=websecure",
|
|
||||||
"traefik.http.routers.${NOMAD_META_domain}.tls.certresolver=letsencrypt",
|
|
||||||
]
|
|
||||||
|
|
||||||
check {
|
|
||||||
type = "http"
|
|
||||||
path = "/"
|
|
||||||
interval = "30s"
|
|
||||||
timeout = "5s"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task "server" {
|
|
||||||
driver = "docker"
|
|
||||||
|
|
||||||
config {
|
|
||||||
image = "registry.toph.so/${NOMAD_META_domain}:${NOMAD_META_image_tag}"
|
|
||||||
ports = ["http"]
|
|
||||||
}
|
|
||||||
|
|
||||||
resources {
|
|
||||||
cpu = 50
|
|
||||||
memory = 64
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
98
deploy-static-site/action.yaml
Normal file
98
deploy-static-site/action.yaml
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
name: Deploy Static Site
|
||||||
|
description: Build site with Nix, push tarball to S3, deploy via Nomad with shared static-server image
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
domain:
|
||||||
|
description: 'Domain the site is served at (e.g. toph.so)'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
nomad-addr:
|
||||||
|
description: 'Nomad API address'
|
||||||
|
required: false
|
||||||
|
default: 'http://172.17.0.1:4646'
|
||||||
|
|
||||||
|
server-image:
|
||||||
|
description: 'OCI image for the static server'
|
||||||
|
required: false
|
||||||
|
default: 'registry.toph.so/static-server:latest'
|
||||||
|
|
||||||
|
datacenter:
|
||||||
|
description: 'Nomad datacenter'
|
||||||
|
required: false
|
||||||
|
default: 'contabo'
|
||||||
|
|
||||||
|
smoke-test:
|
||||||
|
description: 'Run a smoke test against the domain after deploy'
|
||||||
|
required: false
|
||||||
|
default: 'true'
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Install tools
|
||||||
|
shell: bash
|
||||||
|
run: nix profile install nixpkgs#nomad nixpkgs#awscli2 nixpkgs#jq
|
||||||
|
|
||||||
|
- name: Read Nomad vars
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
S3_VARS=$(nomad var get -out json static-sites/s3)
|
||||||
|
echo "AWS_ACCESS_KEY_ID=$(echo "$S3_VARS" | jq -r '.Items.access_key')" >> $GITHUB_ENV
|
||||||
|
echo "AWS_SECRET_ACCESS_KEY=$(echo "$S3_VARS" | jq -r '.Items.secret_key')" >> $GITHUB_ENV
|
||||||
|
echo "AWS_ENDPOINT_URL=$(echo "$S3_VARS" | jq -r '.Items.endpoint')" >> $GITHUB_ENV
|
||||||
|
echo "S3_BUCKET=$(echo "$S3_VARS" | jq -r '.Items.bucket')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
NIX_VARS=$(nomad var get -out json static-sites/nix)
|
||||||
|
echo "NIX_SIGNING_KEY=$(echo "$NIX_VARS" | jq -r '.Items.signing_key')" >> $GITHUB_ENV
|
||||||
|
env:
|
||||||
|
NOMAD_ADDR: ${{ inputs.nomad-addr }}
|
||||||
|
NOMAD_TOKEN: ${{ env.NOMAD_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build site
|
||||||
|
shell: bash
|
||||||
|
run: nix build .#default --out-link result-site
|
||||||
|
|
||||||
|
- name: Sign and push Nix closure to S3 cache
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "${NIX_SIGNING_KEY}" > /tmp/nix-key
|
||||||
|
nix store sign -k /tmp/nix-key --recursive ./result-site
|
||||||
|
nix copy \
|
||||||
|
--to "s3://${S3_BUCKET}?endpoint=${AWS_ENDPOINT_URL}&access-key-id=${AWS_ACCESS_KEY_ID}&secret-access-key=${AWS_SECRET_ACCESS_KEY}" \
|
||||||
|
./result-site
|
||||||
|
rm /tmp/nix-key
|
||||||
|
|
||||||
|
- name: Upload site tarball to S3
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
SITE_HASH=$(git rev-parse --short=12 HEAD)
|
||||||
|
echo "SITE_HASH=${SITE_HASH}" >> $GITHUB_ENV
|
||||||
|
tar czf /tmp/site.tar.gz -C result-site .
|
||||||
|
aws s3 cp /tmp/site.tar.gz "s3://${S3_BUCKET}/sites/${{ inputs.domain }}/${SITE_HASH}.tar.gz"
|
||||||
|
|
||||||
|
- name: Deploy Nomad job
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
python3 "${{ github.action_path }}/generate-job.py" | nomad job run -json -
|
||||||
|
env:
|
||||||
|
NOMAD_ADDR: ${{ inputs.nomad-addr }}
|
||||||
|
NOMAD_TOKEN: ${{ env.NOMAD_TOKEN }}
|
||||||
|
DOMAIN: ${{ inputs.domain }}
|
||||||
|
SERVER_IMAGE: ${{ inputs.server-image }}
|
||||||
|
DATACENTER: ${{ inputs.datacenter }}
|
||||||
|
|
||||||
|
- name: Smoke test
|
||||||
|
if: inputs.smoke-test == 'true'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "Waiting for deployment..."
|
||||||
|
for i in $(seq 1 12); do
|
||||||
|
if curl -sf --max-time 5 "https://${{ inputs.domain }}/" > /dev/null; then
|
||||||
|
echo "OK — https://${{ inputs.domain }}/ is up"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Attempt ${i}/12 — retrying in 5s..."
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
echo "Smoke test failed: https://${{ inputs.domain }}/ did not respond"
|
||||||
|
exit 1
|
||||||
108
deploy-static-site/generate-job.py
Normal file
108
deploy-static-site/generate-job.py
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generate a Nomad job JSON for a static site deployment.
|
||||||
|
Reads from environment variables, prints JSON to stdout.
|
||||||
|
Pipe to: nomad job run -json -
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
domain = os.environ["DOMAIN"]
|
||||||
|
site_hash = os.environ["SITE_HASH"]
|
||||||
|
server_image = os.environ.get("SERVER_IMAGE", "registry.toph.so/static-server:latest")
|
||||||
|
datacenter = os.environ.get("DATACENTER", "contabo")
|
||||||
|
s3_bucket = os.environ["S3_BUCKET"]
|
||||||
|
|
||||||
|
job_id = "site-" + domain.replace(".", "-")
|
||||||
|
|
||||||
|
startup_cmd = (
|
||||||
|
f"mkdir -p /var/www && "
|
||||||
|
f"aws s3 cp s3://{s3_bucket}/sites/{domain}/{site_hash}.tar.gz - "
|
||||||
|
f"| tar xz -C /var/www/ && "
|
||||||
|
f"exec static-web-server --port 8080 --root /var/www"
|
||||||
|
)
|
||||||
|
|
||||||
|
nomad_template_data = (
|
||||||
|
'{{ with nomadVar "static-sites/s3" }}'
|
||||||
|
"AWS_ACCESS_KEY_ID={{ .access_key }}\n"
|
||||||
|
"AWS_SECRET_ACCESS_KEY={{ .secret_key }}\n"
|
||||||
|
"AWS_ENDPOINT_URL={{ .endpoint }}\n"
|
||||||
|
"{{ end }}"
|
||||||
|
)
|
||||||
|
|
||||||
|
job = {
|
||||||
|
"Job": {
|
||||||
|
"ID": job_id,
|
||||||
|
"Name": job_id,
|
||||||
|
"Namespace": "static-sites",
|
||||||
|
"Type": "service",
|
||||||
|
"Datacenters": [datacenter],
|
||||||
|
"Update": {
|
||||||
|
"MinHealthyTime": 5000000000, # 5s in nanoseconds
|
||||||
|
"HealthyDeadline": 60000000000, # 60s in nanoseconds
|
||||||
|
"MaxParallel": 1,
|
||||||
|
},
|
||||||
|
"TaskGroups": [
|
||||||
|
{
|
||||||
|
"Name": "site",
|
||||||
|
"Count": 1,
|
||||||
|
"Networks": [
|
||||||
|
{
|
||||||
|
"DynamicPorts": [
|
||||||
|
{"Label": "http", "To": 8080}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Services": [
|
||||||
|
{
|
||||||
|
"Name": job_id,
|
||||||
|
"Provider": "nomad",
|
||||||
|
"PortLabel": "http",
|
||||||
|
"Tags": [
|
||||||
|
"traefik.enable=true",
|
||||||
|
f"traefik.http.routers.{job_id}.rule=Host(`{domain}`)",
|
||||||
|
f"traefik.http.routers.{job_id}.entrypoints=websecure",
|
||||||
|
f"traefik.http.routers.{job_id}.tls.certresolver=letsencrypt",
|
||||||
|
],
|
||||||
|
"Checks": [
|
||||||
|
{
|
||||||
|
"Type": "http",
|
||||||
|
"Path": "/",
|
||||||
|
"Interval": 30000000000, # 30s
|
||||||
|
"Timeout": 5000000000, # 5s
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Tasks": [
|
||||||
|
{
|
||||||
|
"Name": "server",
|
||||||
|
"Driver": "docker",
|
||||||
|
"Config": {
|
||||||
|
"image": server_image,
|
||||||
|
"command": "/bin/bash",
|
||||||
|
"args": ["-c", startup_cmd],
|
||||||
|
"ports": ["http"],
|
||||||
|
},
|
||||||
|
"Templates": [
|
||||||
|
{
|
||||||
|
"EmbeddedTmpl": nomad_template_data,
|
||||||
|
"DestPath": "secrets/s3.env",
|
||||||
|
"Envvars": True,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Resources": {
|
||||||
|
"CPU": 100,
|
||||||
|
"MemoryMB": 128,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json.dump(job, sys.stdout, indent=2)
|
||||||
|
print()
|
||||||
23
images/.forgejo/workflows/build-images.yaml
Normal file
23
images/.forgejo/workflows/build-images.yaml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
name: Build and Push Images
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'images/flake.nix'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-static-server:
|
||||||
|
runs-on: nix
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build static-server image
|
||||||
|
run: nix build ./images#staticServer --out-link result-static-server
|
||||||
|
|
||||||
|
- name: Push to registry
|
||||||
|
run: |
|
||||||
|
nix shell nixpkgs#skopeo -c skopeo copy \
|
||||||
|
docker-archive:./result-static-server \
|
||||||
|
docker://registry.toph.so/static-server:latest
|
||||||
30
images/flake.nix
Normal file
30
images/flake.nix
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
description = "Shared infrastructure OCI images";
|
||||||
|
|
||||||
|
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs }:
|
||||||
|
let
|
||||||
|
system = "x86_64-linux";
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
# Single image used by every static-site Nomad job.
|
||||||
|
# At container startup it downloads the site tarball from S3, then serves it.
|
||||||
|
# The Nomad job spec overrides Cmd with the domain- and hash-specific fetch+serve command.
|
||||||
|
packages.${system}.staticServer = pkgs.dockerTools.buildLayeredImage {
|
||||||
|
name = "static-server";
|
||||||
|
tag = "latest";
|
||||||
|
contents = with pkgs; [
|
||||||
|
static-web-server
|
||||||
|
awscli2
|
||||||
|
bash
|
||||||
|
coreutils
|
||||||
|
gnutar
|
||||||
|
gzip
|
||||||
|
cacert
|
||||||
|
];
|
||||||
|
config.ExposedPorts."8080/tcp" = { };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
35
site-lib/flake.nix
Normal file
35
site-lib/flake.nix
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
description = "Shared Nix library for static site flakes";
|
||||||
|
|
||||||
|
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs }: {
|
||||||
|
lib.mkSite =
|
||||||
|
{ self
|
||||||
|
, nixpkgs
|
||||||
|
, src
|
||||||
|
, system ? "x86_64-linux"
|
||||||
|
, buildPhase ? "true"
|
||||||
|
, installPhase ? ''
|
||||||
|
mkdir -p $out
|
||||||
|
cp -r dist/. $out/
|
||||||
|
''
|
||||||
|
, devPackages ? []
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
|
||||||
|
site = pkgs.stdenv.mkDerivation {
|
||||||
|
name = "site";
|
||||||
|
inherit src buildPhase installPhase;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages.${system}.default = site;
|
||||||
|
|
||||||
|
devShells.${system}.default = pkgs.mkShell {
|
||||||
|
packages = devPackages;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue