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