From 18ca9072a8230d9be0f4ade9f6a83c48b8c27b93 Mon Sep 17 00:00:00 2001 From: Tracey Jaquith Date: Mon, 30 Dec 2024 11:49:00 -0500 Subject: [PATCH 01/10] start of CI/CD yml tunnel --- .dockerignore | 2 +- .github/workflows/cicd.yml | 10 + .github/workflows/pages.yml | 50 ++++ .gitlab-ci.yml | 95 ------- Caddyfile | 11 + Dockerfile | 22 +- README.md | 545 ------------------------------------ aliases | 312 --------------------- build.sh | 94 ------- build.yml | 23 -- deploy.sh | 346 ----------------------- hello-world.hcl | 57 ---- img/architecture.drawio.svg | 452 ------------------------------ img/overview.drawio.svg | 180 ------------ img/overview2.drawio.svg | 299 -------------------- img/prod.jpg | Bin 27724 -> 0 bytes img/protect.jpg | Bin 27738 -> 0 bytes img/secrets.jpg | Bin 47183 -> 0 bytes logo.jpg | Bin 2285 -> 0 bytes project.nomad | 459 ------------------------------ test/test.sh | 434 ---------------------------- vsync | 10 - 22 files changed, 74 insertions(+), 3327 deletions(-) create mode 100644 .github/workflows/cicd.yml create mode 100644 .github/workflows/pages.yml delete mode 100644 .gitlab-ci.yml create mode 100644 Caddyfile delete mode 100644 README.md delete mode 100644 aliases delete mode 100755 build.sh delete mode 100644 build.yml delete mode 100755 deploy.sh delete mode 100644 hello-world.hcl delete mode 100644 img/architecture.drawio.svg delete mode 100644 img/overview.drawio.svg delete mode 100644 img/overview2.drawio.svg delete mode 100644 img/prod.jpg delete mode 100644 img/protect.jpg delete mode 100644 img/secrets.jpg delete mode 100644 logo.jpg delete mode 100644 project.nomad delete mode 100755 test/test.sh delete mode 100755 vsync diff --git a/.dockerignore b/.dockerignore index 6b8710a..c1c9f4d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1 @@ -.git +.git* diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml new file mode 100644 index 0000000..fc2687a --- /dev/null +++ b/.github/workflows/cicd.yml @@ -0,0 +1,10 @@ +on: [push, workflow_dispatch] +jobs: + cicd: + # https://github.com/internetarchive/cicd + uses: internetarchive/cicd/.github/workflows/cicd.yml@main + with: + NOMAD_VAR_HOSTNAMES: '["nomad","nomad.archive.org"]' + NOMAD_VAR_MEMORY: 100 # xxx + secrets: + NOMAD_TOKEN_EXT: ${{ secrets.NOMAD_TOKEN_EXT }} diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..7eee3fb --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,50 @@ +# https://docs.github.com/en/actions/using-workflows/reusing-workflows + +name: copy repo & deploy to GitHub Pages + +on: + workflow_call: + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + lfs: true + fetch-depth: 0 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./ + + # Deploy to GitHub Pages + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 7545c46..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,95 +0,0 @@ -# NOTE: keep in mind this file is _included_ by _other_ repos, and thus the env var names -# are not _always_ related to _this_ repo ;-) - -# A GitLab group (ideally) or project that wants to deploy to a nomad cluster, -# will need to set [Settings] [CI/CD] [Variables] -# NOMAD_ADDR -# NOMAD_TOKEN -# to whatever your Nomad cluster was setup to. - - -# NOTE: very first pipeline, the [build] below will make sure this is created -image: registry.gitlab.com/internetarchive/nomad/master - -stages: - - build - - test - - deploy - - cleanup - - -include: - # GitLab Auto DevOps' stock CI/CD [build] phase: - - remote: 'https://gitlab.com/internetarchive/nomad/-/raw/master/build.yml' - -test-ourself: - stage: test - image: ${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}:${CI_COMMIT_SHA} - script: - - env -i zsh -euax test/test.sh - rules: - - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - when: never - - if: '$CI_PROJECT_PATH_SLUG == "internetarchive-nomad"' - -deploy: - stage: deploy - script: - # https://gitlab.com/internetarchive/nomad/-/blob/master/deploy.sh - - /deploy.sh - environment: - name: $CI_COMMIT_REF_SLUG - url: https://$HOSTNAME - on_stop: stop_review - rules: - - if: '$NOMAD_VAR_NO_DEPLOY' - when: never - - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - when: never - - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' - -deploy-serverless: - stage: deploy - script: - - | - if [[ -n "$CI_REGISTRY" && -n "$CI_REGISTRY_USER" ]]; then - echo "Logging in to GitLab Container Registry with CI credentials..." - - # this filters stderr of `podman login`, w/o merging stdout & stderr together - set +x - { echo "$CI_REGISTRY_PASSWORD" | podman --remote login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" 2>&1 1>&3 | ( grep -E -v "^WARNING! Your password will be stored unencrypted in |^Configure a credential helper to remove this warning. See|^https://docs.docker.com/engine/reference/commandline/login/#credentials-store" || true ) 1>&2; } 3>&1 - fi - - set -x - image_tagged="$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA" - image_latest="$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:latest" - podman --remote tag $image_tagged $image_latest - podman --remote push $image_latest - rules: - - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - when: never - - if: '$CI_COMMIT_BRANCH && $NOMAD_VAR_SERVERLESS' - - -stop_review: - # See: - # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml - stage: cleanup - variables: - GIT_STRATEGY: none - script: - - /deploy.sh stop - environment: - name: $CI_COMMIT_REF_SLUG - action: stop - dependencies: [] - allow_failure: true - rules: - - if: '$CI_COMMIT_BRANCH == "main"' - when: never - - if: '$CI_COMMIT_BRANCH == "master"' - when: never - - if: '$NOMAD_VAR_NO_DEPLOY' - when: never - - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' - when: manual diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..4e629fb --- /dev/null +++ b/Caddyfile @@ -0,0 +1,11 @@ +{ + admin off +} + +# We answer all requests with the contents of this file: +# https://raw.githubusercontent.com/internetarchive/nomad/refs/heads/master/.gitlab-ci.yml + +:5000 { + rewrite * /internetarchive/nomad/refs/heads/master/.gitlab-ci.yml + reverse_proxy https://raw.githubusercontent.com +} diff --git a/Dockerfile b/Dockerfile index c527829..bbc7f76 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,3 @@ -FROM denoland/deno:alpine +FROM caddy:alpine -# add `nomad` -RUN mkdir -m777 /usr/local/sbin && \ - cd /usr/local/sbin && \ - wget -qO nomad.zip https://releases.hashicorp.com/nomad/1.7.6/nomad_1.7.6_linux_amd64.zip && \ - unzip nomad.zip && \ - rm nomad.zip && \ - chmod 777 nomad && \ - # podman for build.sh - apk add bash zsh jq podman && \ - # using podman not docker - ln -s /usr/bin/podman /usr/bin/docker - -COPY build.sh deploy.sh / - -# revisit this: -# USER deno - -# NOTE: `nomad` binary needed for other repositories using us for CI/CD - but drop from _our_ webapp. -CMD rm /usr/local/sbin/nomad /usr/bin/podman && su deno -c 'deno eval "import { serve } from \"https://deno.land/std/http/server.ts\"; serve(() => new Response(\"hai\"), { port: 5000 })"' +COPY Caddyfile /etc/caddy/ diff --git a/README.md b/README.md deleted file mode 100644 index 1cb9f70..0000000 --- a/README.md +++ /dev/null @@ -1,545 +0,0 @@ -Code, setup, and information to: -- setup automatic deployment to Nomad clusters from GitLab's standard CI/CD pipelines -- interact with, monitor, and customize deployments - - -[[_TOC_]] - - -# Overview -Deployment leverages a simple `.gitlab-ci.yml` using GitLab runners & CI/CD ([build] and [test]); -then switches to custom [deploy] phase to deploy docker containers into `nomad`. - -This also contains demo "hi world" webapp. - - -Uses: -- [nomad](https://www.nomadproject.io) **deployment** (management, scheduling) -- [consul](https://www.consul.io) **networking** (service discovery, healthchecking, secrets storage) -- [caddy](https://caddyserver.com/) **routing** (load balancing, automatic https) - -![Architecture](img/overview2.drawio.svg) - - -## Want to deploy to nomad? 🚀 -- verify project's [Settings] [CI/CD] [Variables] has either Group or Project level settings for: - - `NOMAD_TOKEN` `MY-TOKEN` - - `NOMAD_ADDR` `https://MY-HOSTNAME` or `BASE_DOMAIN` `example.com` - - (archive.org admins will often have set this already for you at the group-level) -- simply make your project have this simple `.gitlab-ci.yml` in top-level dir: -```yaml -include: - - remote: 'https://gitlab.com/internetarchive/nomad/-/raw/master/.gitlab-ci.yml' -``` -- if you want a [test] phase, you can add this to the `.gitlab-ci.yml` file above: -```yaml -test: - stage: test - image: ${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}:${CI_COMMIT_SHA} - script: - - cd /app # or wherever in your image - - npm test # or whatever your test scripts/steps are -``` -- [optional] you can _instead_ copy [the included file](.gitlab-ci.yml) and customize/extend it. -- [optional] you can copy this [project.nomad](project.nomad) file into your repo top level and customize/extend it if desired -- _... but there's a good chance you won't need to_ 😎 - -_**Note:** For urls like https://archive.org/services/project -- watch out for routes defined in your app with trailing slashes – they may redirect to project.dev.archive.org. More information [here](https://git.archive.org/services/pyhi/-/blob/main/README.md#notes)._ - -### Customizing -There are various options that can be used in conjunction with the `project.nomad` and `.gitlab-ci.yml` files, keys: -```text -NOMAD_VAR_CHECK_PATH -NOMAD_VAR_CHECK_PROTOCOL -NOMAD_VAR_CHECK_TIMEOUT -NOMAD_VAR_CONSUL_PATH -NOMAD_VAR_COUNT -NOMAD_VAR_COUNT_CANARIES -NOMAD_VAR_CPU -NOMAD_VAR_FORCE_PULL -NOMAD_VAR_HEALTH_TIMEOUT -NOMAD_VAR_HOSTNAMES -NOMAD_VAR_IS_BATCH -NOMAD_VAR_MEMORY -NOMAD_VAR_MULTI_CONTAINER -NOMAD_VAR_NAMESPACE -NOMAD_VAR_NETWORK_MODE -NOMAD_VAR_NO_DEPLOY -NOMAD_VAR_PERSISTENT_VOLUME -NOMAD_VAR_PORTS -NOMAD_VAR_SERVERLESS -NOMAD_VAR_VOLUMES -``` -- See the top of [project.nomad](project.nomad) -- Our customizations always prefix with `NOMAD_VAR_`. -- You can simply insert them, with values, in your project's `.gitlab-ci.yml` file before including _our_ `.gitlab-ci.yml` like above. -- Examples 👇 -#### Don't actually deploy containers to nomad -Perhaps your project just wants to leverage the CI (Continuous Integration) for [buil] and/or [test] steps - but not CD (Continuous Deployment). An example might be a back-end container that runs elsewhere and doesn't have web listener. -```yaml -variables: - NOMAD_VAR_NO_DEPLOY: 'true' -``` - -#### Custom default RAM expectations from (default) 300 MB to 1 GB -This value is the _expected_ value for your container's average running needs/usage, helpful for `nomad` scheduling purposes. It is a "soft limit" and we use *ten times* this amount to be the amount used for a "hard limit". If your allocated container exceeds the hard limit, the container may be restarted by `nomad` if there is memory pressure on the Virtual Machine the container is running on. -```yaml -variables: - NOMAD_VAR_MEMORY: 1000 -``` -#### Custom default CPU expectations from (default) 100 MHz to 1 GHz -This value is the _expected_ value for your container's average running needs/usage, helpful for `nomad` scheduling purposes. It is a "soft limit". If your allocated container exceeds your specified limit, the container _may_ be restarted by `nomad` if there is CPU pressure on the Virtual Machine the container is running on. (So far, CPU-based restarts seem very rare in practice, since most VMs tend to "fill" up from aggregate container RAM requirements first 😊) -```yaml -variables: - NOMAD_VAR_CPU: 1000 -``` -#### Custom healthcheck, change from (default) HTTP to TCP: -This can be useful if your webapp serves using websockets, doesnt respond to http, or typically takes too long (or can't) respond with a `200 OK` status. (Think of it like switching to just a `ping` on your main port your webapp listens on). -```yaml -variables: - NOMAD_VAR_CHECK_PROTOCOL: 'tcp' -``` -#### Custom healthcheck, change path from (default) `/` to `/healthcheck`: -```yaml -variables: - NOMAD_VAR_CHECK_PATH: '/healthcheck' -``` -#### Custom healthcheck run time, change from (default) `2s` (2 seconds) to `1m` (one minute) -If your healthcheck may take awhile to run & succeed, you can increase the amount of time the `consul` healthcheck allows your HTTP request to run. -```yaml -variables: - NOMAD_VAR_CHECK_TIMEOUT: '1m' -``` -#### Custom time to start healthchecking after container re/start from (default) `20s` (20 second) to `3m` (3 minutes) -If your container takes awhile, after startup, to settle before healthchecking can work reliably, you can extend the wait time for the first healthcheck to run. -```yaml -variables: - NOMAD_VAR_HEALTH_TIMEOUT: '3m' -``` -#### Custom running container count from (default) 1 to 3 -You can run more than one container for increased reliability, more request processing, and more reliable uptimes (in the event of one or more Virtual Machines hosting containers having issues). - -For archive.org users, we suggest instead to switch your production deploy to our alternate production cluster. - -Keep in mind, you will have 2+ containers running simultaneously (_usually_, but not always, on different VMs). So if your webapp uses any shared resources, like backends not in containers, or "persistent volumes", that you will need to think about concurrency, potentially multiple writers, etc. 😊 -```yaml -variables: - NOMAD_VAR_COUNT: 3 -``` -#### Custom make NFS `/home/` available in running containers, readonly -Allow your containers to see NFS `/home/` home directories, readonly. -```yaml -variables: - NOMAD_VAR_VOLUMES: '["/home:/home:ro"]' -``` -#### Custom make NFS `/home/` available in running containers, read/write -Allow your containers to see NFS `/home/` home directories, readable and writable. Please be highly aware of operational security in your container when using this (eg: switch your `USER` in your `Dockerfile` to another non-`root` user; use "prepared statements" with any DataBase interactions; use [https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP](Content Security Policy) in all your pages to eliminate [https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting](XSS attacks, etc.) -```yaml -variables: - NOMAD_VAR_VOLUMES: '["/home:/home:rw"]' -``` -#### Custom hostname for your `main` branch deploy -Your deploy will get a nice semantic hostname by default, based upon "[slugged](https://en.wikipedia.org/wiki/Clean_URL#Slug)" formula like: https://[GITLAB_GROUP]-[GITLAB_PROJECT_OR_REPO_NAME]-[BRANCH_NAME]. However, you can override this if needed. This custom hostname will only pertain to a branch named `main` (or `master` [sic]) -```yaml -variables: - NOMAD_VAR_HOSTNAMES: '["www.example.com"]' -``` -#### Custom hostnameS for your `main` branch deploy -Similar to prior example, but you can have your main deployment respond to multiple hostnames if desired. -```yaml -variables: - NOMAD_VAR_HOSTNAMES: '["www.example.com", "store.example.com"]' -``` - -#### Multiple containers in same job spec -If you want to run multiple containers in the same job and group, set this to true. For example, you might want to run a Postgresql 3rd party container from bitnami, and have the main/http front-end container talk to it. Being in the same group will ensure all containers run on the same VM; which makes communication between them extremely easy. You simply need to inspect environment variables. - -You can see a minimal example of two containers with a "front end" talking to a "backend" here -https://gitlab.com/internetarchive/nomad-multiple-tasks - -See also a [postgres DB setup example](#postgres-db). -```yaml -variables: - NOMAD_VAR_MULTI_CONTAINER: 'true' -``` - -#### Force `docker pull` before container starts -If your deployment's job spec doesn't change between pipelines for some reason, you can set this to ensure `docker pull` always happens before your container starts up. A good example where you might see this is a periodic/batch/cron process that fires up a pipeline without any repository commit. Depending on your workflow and `Dockerfile` from there, if you see "stale" versions of containers, use this customization. -```yaml -variables: - NOMAD_VAR_FORCE_PULL: 'true' -``` - -#### Turn off [deploy canaries](https://learn.hashicorp.com/tutorials/nomad/job-blue-green-and-canary-deployments) -When a new deploy is happening, live traffic continues to old deploy about to be replaced, while a new deploy fires off in the background and `nomad` begins healthchecking. Only once it seems healthy, is traffic cutover to the new container and the old container removed. (If unhealthy, new container is removed). That can mean *two* deploys can run simultaneously. Depending on your setup and constraints, you might not want this and can disable canaries with this snippet below. (Keep in mind your deploy will temporarily 404 during re-deploy *without* using blue/green deploys w/ canaries). -```yaml -variables: - NOMAD_VAR_COUNT_CANARIES: 0 -``` - -#### Change your deploy to a cron-like batch/periodic -If you deployment is something you want to run periodically, instead of continuously, you can use this variable to switch to a nomad `type="batch"` -```yaml -variables: - NOMAD_VAR_IS_BATCH: 'true' -``` -Combine your `NOMAD_VAR_IS_BATCH` override, with a small `job.nomad` file in your repo to setup your cron behaviour. - -Example `job.nomad` file contents, to run the deploy every hour at 15m past the hour: -```ini -type = "batch" -periodic { - cron = "15 * * * * *" - prohibit_overlap = false # must be false cause of kv env vars task -} -``` - -#### Custom deploy networking -If your admin allows it, there might be some useful reasons to use VM host networking for your deploy. A good example is "relaying" UDP *broadcast* messages in/out of a container. Please see Tracey if interested, archive folks. :) -```yaml -variables: - NOMAD_VAR_NETWORK_MODE: 'host' -``` - -#### Custom namespacing -A job can be limited to a specific 'namespace' for purposes of ACL 'gating'. -In the example below, a cluster admin could create a custom `NOMAD_TOKEN` that only allows the -bearer to access jobs part of the namespace `team-titan`. -```yaml -variables: - NOMAD_VAR_NAMESPACE: 'team-titan' -``` - - - -#### More customizations -There are even more, less common, ways to customize your deploys. - -With other variables, like `NOMAD_VAR_PORTS`, you can use dynamic port allocation, setup daemons that use raw TCP, and more. - -Please see the top area of [project.nomad](project.nomad) for "Persistent Volumes" (think a "disk" that survives container restarts), additional open ports into your webapp, and more. - -See also [this section](#optional-add-ons-to-your-project) below. - -### Deploying to production nomad cluster (archive.org only) -Our production cluster has 3 VMs and will deploy your repo to a running container on each VM, using `haproxy` load balancer to balance requests. - -This should ensure much higher availability and handle more requests. - -Keep in mind if your deployment uses a "persistent volume" or talks to other backend services, they'll be getting traffic and access from multiple containers simultaneously. - -Setting up your repo to deploy to production is easy! - -- add a CI/CD Secret `NOMAD_TOKEN_PROD` with the nomad cluster value (ask tracey or robK) - - make it: protected, masked, hidden -![Production CI/CD Secret](img/prod.jpg) -- Make a new branch named `production` (presumably from your repo's latest `main` or `master` branch) - - It should now deploy your project to a different `NOMAD_ADDR` url - - Your default hostname domain will change from `.dev.archive.org` to `.prod.archive.org` -- [GitLab only] - [Protect the `production` branch](https://docs.gitlab.com/ee/user/project/protected_branches.html) - - suggest using same settings as your `main` or `master` (or default) branch -![Protect a branch](img/protect.jpg) - - -### Deploying to staging nomad cluster (archive.org only) -Our staging cluster will deploy your repo to a running container on one of its VMs. - -Setting up your repo to deploy to staging is easy! - -- add a CI/CD Secret `NOMAD_TOKEN_STAGING` with the nomad cluster value (ask tracey or robK) - - make it: protected, masked, hidden (similar to `production` section above) -- Make a new branch named `staging` (presumably from your repo's latest `main` or `master` branch) - - It should now deploy your project to a different `NOMAD_ADDR` url - - Your default hostname domain will change from `.dev.archive.org` to `.staging.archive.org` -- [GitLab only] - [Protect the `staging` branch](https://docs.gitlab.com/ee/user/project/protected_branches.html) - - suggest using same settings as your `main` or `master` (or default) branch, changing `production` to `staging` here: -![Protect a branch](img/protect.jpg) - - -### Deploying to ext nomad cluster (archive.org only) -Our "ext" cluster will deploy your repo to a running container on one of its VMs. - -Setting up your repo to deploy to ext is easy! - -- add a CI/CD Secret `NOMAD_TOKEN_EXT` with the nomad cluster value (ask tracey or robK) - - make it: protected, masked, hidden (similar to `production` section above) -- Make a new branch named `ext` (presumably from your repo's latest `main` or `master` branch) - - It should now deploy your project to a different `NOMAD_ADDR` url - - Your default hostname domain will change from `.dev.archive.org` to `.ext.archive.org` -- [GitLab only] - [Protect the `ext` branch](https://docs.gitlab.com/ee/user/project/protected_branches.html) - - suggest using same settings as your `main` or `master` (or default) branch, changing `production` to `ext` here: -![Protect a branch](img/protect.jpg) - - -## Laptop access -- create `$HOME/.config/nomad` and/or get it from an admin who setup your Nomad cluster - - @see top of [aliases](aliases) - - `brew install nomad` - - `source $HOME/.config/nomad` - - better yet: - - `git clone https://gitlab.com/internetarchive/nomad` - - adjust next line depending on where you checked out the above repo - - add this to your `$HOME/.bash_profile` or `$HOME/.zshrc` etc. - - `FI=$HOME/nomad/aliases && [ -e $FI ] && source $FI` - - then `nomad status` should work nicely - - @see [aliases](aliases) for lots of handy aliases.. -- you can then also use your browser to visit [$NOMAD_ADDR/ui/jobs](https://MY-HOSTNAME:4646/ui/jobs) - - and enter your `$NOMAD_TOKEN` in the ACL requirement - - -# Setup a Nomad Cluster -- we use HinD: https://github.com/internetarchive/hind - - you can customize the install with various environment variables - -Other alternatives: -- have DNS domain you can point to a VM? - - nomad/consul with $5/mo VM (or on-prem) - - [[1/2] Setup GitLab, Nomad, Consul & Fabio](https://tracey.archive.org/devops/2021-03-31) - - [[2/2] Add GitLab Runner & Setup full CI/CD pipelines](https://tracey.archive.org/devops/2021-04-07) -- have DNS domain and want on-prem GitLab? - - nomad/consul/gitlab/runners with $20/mo VM (or on-prem) - - [[1/2] Setup GitLab, Nomad, Consul & Fabio](https://tracey.archive.org/devops/2021-03-31) - - [[2/2] Add GitLab Runner & Setup full CI/CD pipelines](https://tracey.archive.org/devops/2021-04-07) -- no DNS - run on mac/linux laptop? - - [[1/3] setup GitLab & GitLab Runner on your Mac](https://tracey.archive.org/devops/2021-02-17) - - [[2/3] setup Nomad & Consul on your Mac](https://tracey.archive.org/devops/2021-02-24) - - [[3/3] connect: GitLab, GitLab Runner, Nomad & Consul](https://tracey.archive.org/devops/2021-03-10) - - -# Monitoring GUI urls (via ssh tunnelling above) -![Cluster Overview](https://tracey.archive.org/images/nomad-ui4.jpg) -- nomad really nice overview (see `Topology` link ☝) - - https://[NOMAD-HOST]:4646 (eg: `$NOMAD_ADDR`) - - then enter your `$NOMAD_TOKEN` -- @see [aliases](aliases) `nom-tunnel` - - http://localhost:8500 # consul - - -# Inspect, poke around -```bash -nomad node status -nomad node status -allocs -nomad server members - - -nomad job run example.nomad -nomad job status -nomad job status example - -nomad job deployments -t '{{(index . 0).ID}}' www-nomad -nomad job history -json www-nomad - -nomad alloc logs -stderr -f $(nomad job status www-nomad |egrep -m1 '\srun\s' |cut -f1 -d' ') - - -# get CPU / RAM stats and allocations -nomad node status -self - -nomad node status # OR pick a node's 1st column, then -nomad node status 01effcb8 - -# get list of all services, urls, and more, per nomad -wget -qO- --header "X-Nomad-Token: $NOMAD_TOKEN" $NOMAD_ADDR/v1/jobs |jq . -wget -qO- --header "X-Nomad-Token: $NOMAD_TOKEN" $NOMAD_ADDR/v1/job/JOB-NAME |jq . - - -# get list of all services and urls, per consul -consul catalog services -tags -wget -qO- 'http://127.0.0.1:8500/v1/catalog/services' |jq . -``` - -# Optional add-ons to your project - -## Secrets -In your project/repo Settings, set CI/CD environment variables starting with `NOMAD_SECRET_`, marked `Masked` but _not_ `Protected`, eg: -![Secrets](img/secrets.jpg) -and they will show up in your running container as environment variables, named with the lead `NOMAD_SECRET_` removed. Thus, you can get `DATABASE_URL` (etc.) set in your running container - but not have it anywhere else in your docker image and not printed/shown during CI/CD pipeline phase logging. - - -## Persistent Volumes -Persistent Volumes (PV) are like mounted disks that get setup before your container starts and _mount_ in as a filesystem into your running container. They are the only things that survive a running deployment update (eg: a new CI/CD pipeline), container restart, or system move to another cluster VM - hence _Persistent_. - -You can use PV to store files and data - especially nice for databases or otherwise (eg: retain `/var/lib/postgresql` through restarts, etc.) - -Here's how you'd update your project's `.gitlab-ci.yml` file, -by adding these lines (suggest near top of your file): -```yaml -variables: - NOMAD_VAR_PERSISTENT_VOLUME: '/pv' -``` -Then the dir `/pv/` will show up (blank to start with) in your running container. - -If you'd like to have the mounted dir show up somewhere besides `/pv` in your container, -you can setup like: -```yaml -variables: - NOMAD_VAR_PERSISTENT_VOLUME: '/var/lib/postgresql' -``` - -Please verify added/updated files persist through two repo CI/CD pipelines before adding important data and files. Your DevOps teams will try to ensure the VM that holds the data is backed up - but that does not happen by default without some extra setup. Your DevOps team must ensure each VM in the cluster has (the same) shared `/pv/` directory. We presently use NFS for this (after some data corruption issues with glusterFS and rook/ceph). - - -## Postgres DB -We have a [postgresql example](https://git.archive.org/www/dwebcamp2019), visible to archive.org folks. But the gist, aside from a CI/CD Variable/Secret `POSTGRESQL_PASSWORD`, is below. - -_Keep in mind if you setup something like a database in a container, using a Persistent Volume (like below) you can get multiple containers each trying to write to your database backing store filesystem (one for production; one temporarily for production re-deploy "canary"; and similar 1 or 2 for every deployed branch (which is probably not what you want). So you might want to look into `NOMAD_VAR_COUNT` and `NOMAD_VAR_COUNT_CANARIES` in that case._ - -It's recommended to run the DB container during the prestart hook as a "sidecar" service (this will cause it to finish starting before any other group tasks initialize, avoiding service start failures due to unavailable DB, see [nomad task dependencies](https://www.hashicorp.com/blog/hashicorp-nomad-task-dependencies) for more info) - -`.gitlab-ci.yml`: -```yaml -variables: - NOMAD_VAR_MULTI_CONTAINER: 'true' - NOMAD_VAR_PORTS: '{ 5000 = "http", 5432 = "db" }' - NOMAD_VAR_PERSISTENT_VOLUME: '/bitnami/postgresql' - NOMAD_VAR_CHECK_PROTOCOL: 'tcp' - # avoid 2+ containers running where both try to write to database - NOMAD_VAR_COUNT: 1 - NOMAD_VAR_COUNT_CANARIES: 0 - -include: - - remote: 'https://gitlab.com/internetarchive/nomad/-/raw/master/.gitlab-ci.yml' -``` -`vars.nomad`: -```ini -# used in @see group.nomad -variable "POSTGRESQL_PASSWORD" { - type = string - default = "" -} -``` -`group.nomad`: -```ini -task "db" { - driver = "docker" - lifecycle { - sidecar = true - hook = "prestart" - } - config { - image = "docker.io/bitnami/postgresql:11.7.0-debian-10-r9" - ports = ["db"] - volumes = ["/pv/${var.CI_PROJECT_PATH_SLUG}:/bitnami/postgresql"] - } - template { - data = <| .env && python ... -``` - ---- - -## Two `group`s, within same `job`, wanting to talk to each other -Normally, we strongly suggest all `task`s be together in the same `group`. -That will ensure all task containers are run on the same VM, and all tasks will get automatically managed and setup `env` vars, eg: -```ini -NOMAD_ADDR_backend=211.204.226.244:27344 -NOMAD_ADDR_http=211.204.226.244:23945 -``` - -However, if for some reason you want to split your tasks into 2+ `group { .. }` stanzas, -here is how you can get the containers to talk to each other (using `consul` and templating): -- https://github.com/hashicorp/nomad/issues/5455#issuecomment-482490116 -You'd end up putting your 2nd `group` in a file named `job.nomad` in the top of your repo. - ---- - -# GitHub repo integrations -## GitHub Actions -- We use GitHub Actions to create [build], [test], and [deploy] CI/CD pipelines. -- There is a lot of great information and links to example repos here: https://github.com/internetarchive/cicd#readme - -## GitHub Customizing -- You can use the same `NOMAD_VAR_` options above to tailor your deploy in the [#Customizing](#Customizing) section above. [Documentation and examples here](https://github.com/internetarchive/cicd#readme). - -## GitHub Secrets -- You can add GitHub secrets to your repo from the GitHub GUI ([documentation](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository)). You then need to get those secrets to pass through to the [deploy] phase, using the `NOMAD_SECRETS` setting in the GitHub Actions workflow yaml file. -- Note that you may want to test with repository or organizational level secrets before proceeding to setup environment secrets ( [documentation around creating secrets for an environment](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository) ) -- Here is an example GH repo that passes 2 GH secrets into the [deploy] phase. Each secret will wind up as environment variable that your servers can read, or your `RUN`/`CMD` entrypoint can read: - - https://github.com/traceypooh/staticman/blob/main/.github/workflows/cicd.yml - - [entrypoint setup](https://github.com/traceypooh/staticman/blob/main/Dockerfile) - - [entrypoint script](https://github.com/traceypooh/staticman/blob/main/entrypoint.sh) - ---- - -# Helpful links -- https://youtube.com/watch?v=3K1bSGN7zGA 'HashiConf Digital June 2020 - Full Opening Keynote' -- https://www.nomadproject.io/docs/install/production/deployment-guide/ -- https://learn.hashicorp.com/nomad/managing-jobs/configuring-tasks -- https://www.burgundywall.com/post/continuous-deployment-gitlab-and-nomad -- https://weekly-geekly.github.io/articles/453322/index.html -- https://www.haproxy.com/blog/haproxy-and-consul-with-dns-for-service-discovery/ -- https://www.youtube.com/watch?v=gf43TcWjBrE Kelsey Hightower, HashiConf 2016 -- https://blog.tjll.net/reverse-proxy-hot-dog-eating-contest-caddy-vs-nginx/#results -- https://github.com/hashicorp/consul-template/issues/200#issuecomment-76596830 - -## Pick your container stack / testimonials -- https://www.hashicorp.com/blog/hashicorp-joins-the-cncf/ -- https://www.nomadproject.io/intro/who-uses-nomad/ - - + http://jet.com/walmart -- https://medium.com/velotio-perspectives/how-much-do-you-really-know-about-simplified-cloud-deployments-b74d33637e07 -- https://blog.cloudflare.com/how-we-use-hashicorp-nomad/ -- https://www.hashicorp.com/resources/ncbi-legacy-migration-hybrid-cloud-consul-nomad/ -- https://thenewstack.io/fargate-grows-faster-than-kubernetes-among-aws-customers/ -- https://github.com/rishidot/Decision-Makers-Guide/blob/master/Decision%20Makers%20Guide%20-%20Nomad%20Vs%20Kubernetes%20-%20Oct%202019.pdf -- https://medium.com/@trevor00/building-container-platforms-part-one-introduction-4ee2338eb11 - - - -# Multi-node architecture -![Architecture](img/architecture.drawio.svg) - - -# Requirements for archive.org CI/CD -- docker exec ✅ - - pop into deployed container and poke around - similar to `ssh` - - @see [aliases](aliases) `nom-ssh` -- docker cp ✅ - - hot-copy edited file into _running_ deploy (avoid full pipeline to see changes) - - @see [aliases](aliases) `nom-cp` - - hook in VSCode - [sync-rsync](https://marketplace.visualstudio.com/items?itemName=vscode-ext.sync-rsync) - package to 'copy (into container) on save' -- secrets ✅ -- load balancers ✅ -- 2+ instances HPA ✅ -- PV ✅ -- http/2 ✅ -- auto http => https ✅ -- web sockets ✅ -- auto-embed HSTS in https headers, similar to kubernetes ✅ - - eg: `Strict-Transport-Security: max-age=15724800; includeSubdomains` - - -# Constraints -In the past, we've made it so certain jobs are "constrained" to run on specifc 1+ cluster VM. - -Here's how you can do it: -You can manually add this to 1+ VM `/etc/nomad/nomad.hcl` file: -```ini -client { - meta { - "kind" = "tcp-vm" - } -} -``` - -You can add this as a new file named `job.nomad` in the top of a project/repo: -```ini -constraint { - attribute = "${meta.kind}" - operator = "set_contains" - value = "tcp-vm" -} -``` - -Then deploys for this repo will *only* deploy to your specific VMs. diff --git a/aliases b/aliases deleted file mode 100644 index a81920a..0000000 --- a/aliases +++ /dev/null @@ -1,312 +0,0 @@ -#!/bin/bash - - -# look for NOMAD_ADDR and NOMAD_TOKEN -[ -e $HOME/.config/nomad ] && source $HOME/.config/nomad - - -# If not running interactively, don't setup autocomplete -if [ ! -z "$PS1" ]; then - # nomad/consul autocompletes - if [ "$ZSH_VERSION" = "" ]; then - which nomad >/dev/null && complete -C $(which nomad) nomad - which consul >/dev/null && complete -C $(which consul) consul - else - # https://apple.stackexchange.com/questions/296477/ - ( which compdef 2>&1 |fgrep -q ' not found' ) && autoload -Uz compinit && compinit - - which nomad >/dev/null && autoload -U +X bashcompinit && bashcompinit - which nomad >/dev/null && complete -o nospace -C $(which nomad) nomad - which consul >/dev/null && complete -o nospace -C $(which consul) consul - fi -fi - - -function nom-app() { - # finds the webapp related to given job/CWD and opens it in browser - [ $# -eq 1 ] && JOB=$1 - [ $# -ne 1 ] && JOB=$(nom-job-from-cwd) - - _nom-url - - URL=$(echo "$URL" |head -1) - - [ "$URL" = "" ] && echo "URL not found - is service running? try:\n nomad status $JOB" && return - open "$URL" -} - - -function nom-ssh() { - # simple way to pop in (ssh-like) to a given job - # Usage: [job name, eg: x-thumb] -OR- no args will use CWD to determine job - [ $# -ge 1 ] && JOB=$1 - [ $# -lt 1 ] && JOB=$(nom-job-from-cwd) - [ $# -ge 2 ] && TASK=$2 # for rarer TaskGroup case where 2+ Tasks spec-ed in same Job - [ $# -lt 2 ] && TASK=http - - ALLOC=$(nomad job status $JOB |egrep -m1 '\srun\s' |cut -f1 -d' ') - echo "nomad alloc exec -i -t -task $TASK $ALLOC" - - if [ $# -ge 3 ]; then - shift - shift - nomad alloc exec -i -t -task $TASK $ALLOC "$@" - else - nomad alloc exec -i -t -task $TASK $ALLOC \ - sh -c '([ -e /bin/zsh ] && zsh) || ([ -e /bin/bash ] && bash) || ([ -e /bin/sh ] && sh)' - fi -} - - -function nom-sshn() { - # simple way to pop in (ssh-like) to a given job with 2+ allocations/containers - local N=${1:?"Usage: [container/allocation number, starting with 1]"} - - local ALLOC=$(nomad job status $JOB |egrep '\srun\s' |head -n $N |tail -1 |cut -f1 -d' ') - echo "nomad alloc exec -i -t $ALLOC" - - nomad alloc exec -i -t $ALLOC \ - sh -c '([ -e /bin/zsh ] && zsh) || ([ -e /bin/bash ] && bash) || ([ -e /bin/sh ] && sh)' -} - - -function nom-cp() { - # copies a laptop local file into running deploy (avoids full pipeline just to see changes) - - # first, see if this is vscode sync-rsync - local VSCODE= - [ "$#" -ge 4 ] && ( echo "$@" |fgrep -q .vscode ) && VSCODE=1 - - if [ $VSCODE ]; then - # fish out file name from what VSCode 'sync-rsync' package sends us -- should be 2nd to last arg - local FILE=$(echo "$@" |rev |tr -s ' ' |cut -f2 -d' ' |rev) - # switch dirs to make aliases work - local DIR=$(dirname "$FILE") - cd "$DIR" - local BRANCH=$(git rev-parse --abbrev-ref HEAD) - local JOB=$(nom-job-from-cwd) - local ALLOC=$(nom-job-to-alloc) - local TASK=http - cd - - - else - local FILE=${1:?"Usage: [src file, locally qualified while 'cd'-ed inside a repo]"} - local BRANCH=$(git rev-parse --abbrev-ref HEAD) - local JOB=$(nom-job-from-cwd) - local ALLOC=$(nom-job-to-alloc) - [ $# -ge 2 ] && TASK=$2 # for rarer TaskGroup case where 2+ Tasks spec-ed in sam Job - [ $# -lt 2 ] && TASK=http - fi - - # now split the FILE name into two pieces -- 'the root of the git tree' and 'the rest' - local DIR=$(dirname "$FILE") - local TOP=$(git -C "$DIR" rev-parse --show-toplevel) - local REST=$(echo "$FILE" | perl -pe "s|^$TOP||; s|^/+||;") - - - for var in FILE DIR TOP REST BRANCH JOB ALLOC; do - echo $var="${(P)var}" - done - echo - - if [ $VSCODE ]; then - local MAIN= - [ "$BRANCH" = "main" ] && MAIN=true - [ "$BRANCH" = "master" ] && MAIN=true - - local RSYNC= - [ $MAIN ] && RSYNC=true - [ ! $MAIN ] && [ "$RSYNC_BRANCHES" ] && RSYNC=true - - [ $RSYNC ] && ( set -x; rsync "$@" ) - - # this is a special exception project where we DONT want to ALSO copy file to nomad deploy - [ $MAIN ] && [ "$JOB" = "ia-petabox" ] && [ ! $NOM_CP_PETABOX_MAIN ] && exit 0 - fi - - - if [ "$JOB" = "" -o "$ALLOC" = "" ]; then - # no relevant job & alloc found - nothing to do - echo 'has this branch run a full pipeline and deployed a Review App yet?' - return - fi - - # HinD updated nomad clusters w/ latest nomad seem to *not* get the stdin close properly - # (and thus hang). So timeout/kill after 2s :( tracey 2024/3 ) - set +e - cat "$FILE" | ( set -x; set +e; nomad alloc exec -i -task $TASK "$ALLOC" sh -c "timeout 2 cat >| '$REST'" ) - echo SUCCESS -} - - -function nom-logs() { - # simple way to view logs for a given job - [ $# -eq 1 ] && JOB=$1 - [ $# -ne 1 ] && JOB=$(nom-job-from-cwd) - # NOTE: the 2nd $JOB is useful for when a job has 2+ tasks (eg: `kv` or DB/redis, etc.) - nomad alloc logs -f -job $JOB http -} - - -function nom-logs-err() { - # simple way to view logs for a given job - [ $# -eq 1 ] && JOB=$1 - [ $# -ne 1 ] && JOB=$(nom-job-from-cwd) - nomad alloc logs -stderr -f -job $JOB http -} - - -function nom-status() { - # prints detailed status for a repo's service and deployment - [ $# -eq 1 ] && JOB=$1 - [ $# -ne 1 ] && JOB=$(nom-job-from-cwd) - - line - echo "nomad status $JOB" - line - nomad status $JOB | grep --color=always -iE 'unhealthy|healthy|$' - line - echo 'nomad alloc status -stats $(nom-job-to-alloc '$JOB')' - line - nomad alloc status -stats $(nom-job-to-alloc $JOB) | grep --color=always -iE 'unhealthy|healthy|Job Version.*|Node Name.*|$' - line -} - - -function nom-urls() { - # Lists all current urls for the services deployed to current nomad cluster (eg: webapps) - # Ideally, this is a faster single-shot call. But to avoid requiring either `consul` addr - # and ACL token _in addition_ to `nomad` - we'll just use `nomad` directly instead. - # consul catalog services -tags - for JOB in $(curl -sH "X-Nomad-Token: ${NOMAD_TOKEN?}" ${NOMAD_ADDR?}/v1/jobs \ - | jq -r '.[] | select(.Type=="service") | "\(.Name)"') - do - _nom-url - echo $URL - done |sort -} - - -function _nom-url() { - # logically private helper function - URL=$(curl -sH "X-Nomad-Token: ${NOMAD_TOKEN?}" ${NOMAD_ADDR?}/v1/job/$JOB \ - | jq -r '.TaskGroups[0].Services[0].Tags' \ - | fgrep . |fgrep -v redirect=308 |tr -d '", ' |perl -pe 's/:443//; s=^urlprefix\-=https://=;' - ) -} - - -function nom-resubmit() { - # Retrieves current job spec from nomad cluster and resubmits it to nomad. - # Useful for when a job has exceedded a setup timeout, is (nonideally) marked 'dead', etc. - [ $# -eq 1 ] && JOB=$1 - [ $# -ne 1 ] && JOB=$(nom-job-from-cwd) - - nomad inspect ${JOB?} |tee .$JOB - - # in case we are trying to _move_ an active/OK deploy - nomad stop ${JOB?} - sleep 5 - - curl -XPOST -H "Content-Type: application/json" -H "X-Nomad-Token: $NOMAD_TOKEN" -d @.${JOB?} \ - $NOMAD_ADDR/v1/jobs - - rm -f .$JOB -} - - -function d() { - # show docker running containers and local images - [ "$#" = "0" ] && clear -x - - local SUDO= - [ $(uname) = "Linux" ] && local SUDO=sudo - [ ! -e /usr/bin/docker ] && local docker=podman - - $SUDO $docker ps -a --format "table {{.ID}}\t{{.Image}}\t{{.Status}}\t{{.Names}}\t{{.State}}" | $SUDO cat >| $HOME/.dockps - chmod 666 $HOME/.dockps - for i in STATE running restarting created paused removing exited dead; do - cat $HOME/.dockps |egrep "$i$" |perl -pe 's/'$i'$//' - done - rm -f $HOME/.dockps - - line - $SUDO $docker images -} - -function nom() { - # quick way to get an overview of a nomad server when ssh-ed into it - d - line - nomad server members - line - nomad status - line -} - - -function nom-job-from-cwd() { - # print the nomad job name based on the current project - # parse out repo info, eg: 'ia-petabox' -- ensure clone-over-ssh or clone-over-https work - local GURL TMP GROUP_PROJECT PROJECT BRANCH SLUG JOB - GURL=$(git config --get remote.origin.url) - [[ "$GURL" =~ https:// ]] && TMP=$(echo "$GURL" |cut -f4- -d/) - [[ "$GURL" =~ https:// ]] || TMP=$(echo "$GURL" |rev |cut -f1 -d: |rev) - GROUP_PROJECT=$(echo "$TMP" |perl -pe 's/\.git//' |tr A-Z a-z |tr / -) - - PROJECT=$(git rev-parse --absolute-git-dir |egrep --color -o '.*?.git' |rev |cut -f2 -d/ |rev) - BRANCH=$(git rev-parse --abbrev-ref HEAD) - SLUG=$(echo "$BRANCH" |tr '/_.' '-' |tr A-Z a-z) - JOB=$GROUP_PROJECT - [ "$SLUG" = "main" -o "$SLUG" = "master" -o "$SLUG" = "staging" -o "$SLUG" = "production" ] || JOB="${JOB}-${SLUG}" - echo $(echo "$JOB" |cut -b1-63) -} - - - -function nom-image-from-cwd() { - # print the registry image based on the current project - # parse out repo info, eg: 'ia-petabox' -- ensure clone-over-ssh or clone-over-https work - local GURL GROUP_PROJECT BRANCH SLUG JOB - GURL=$(git config --get remote.origin.url) - [[ "$GURL" =~ https:// ]] && GROUP_PROJECT=$(echo "$GURL" |cut -f4- -d/) - [[ "$GURL" =~ https:// ]] || GROUP_PROJECT=$(echo "$GURL" |rev |cut -f1 -d: |rev) - - BRANCH=$(git rev-parse --abbrev-ref HEAD) - SLUG=$(echo "$BRANCH" |tr '/_.' '-' |tr A-Z a-z) - echo $(echo "registry.archive.org/$GROUP_PROJECT/$SLUG" |cut -b1-63) -} - - - - -function nom-job-to-alloc() { - # prints alloc of a given job (when in high-availability and 2+ allocations, picks one at random) - # Usage: [job name, eg: x-thumb] -OR- no args will use CWD to determine job - [ $# -eq 1 ] && JOB=$1 - [ $# -ne 1 ] && JOB=$(nom-job-from-cwd) - nomad job status $JOB |egrep -m1 '\srun\s' |cut -f1 -d' ' -} - - -function line () { - # horizontal line break - perl -e 'print "_"x100; print "\n\n";' -} - - -function nom-tunnel() { - # Sets up an ssh tunnel in the background to be able to talk to nomad cluster's consul. - [ "$NOMAD_ADDR" = "" ] && echo "Please set NOMAD_ADDR environment variable first" && return - local HOST=$(echo "$NOMAD_ADDR" | sed 's/:4646\/*$//' |sed 's/^https*:\/\///') - ssh -fNA -L 8500:localhost:8500 $HOST -} - - -function web-logs-tail() { - # admin script that can more easily "tail -f" the caddy (JSON) web logs - ( - set -x - tail -f /var/log/caddy/access.log | jq -r '"\(.request.host)\(.request.uri)\t\t\(.request.headers."User-Agent")"' - ) -} diff --git a/build.sh b/build.sh deleted file mode 100755 index 5cc1bcc..0000000 --- a/build.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/bin/bash -e - -# Build stage script for Auto-DevOps - -# FROM: registry.gitlab.com/internetarchive/auto-build-image/main -# which was -# FROM registry.gitlab.com/gitlab-org/cluster-integration/auto-build-image:v1.14.0 -# -# then pulled the unused heroku/buildpack stuff/clutter - -# Wondering how to do podman-in-podman? Of course we are. Here's a minimal example: -# -# SOCK=$(sudo podman info |grep -F podman.sock |rev |cut -f1 -d ' ' |rev) -# podman run --rm --privileged --net=host --cgroupns=host -v $SOCK:$SOCK registry.gitlab.com/internetarchive/nomad/master zsh -c 'podman --remote ps -a' - -set -o pipefail - -filter_docker_warning() { - grep -E -v "^WARNING! Your password will be stored unencrypted in |^Configure a credential helper to remove this warning. See|^https://docs.docker.com/engine/reference/commandline/login/#credentials-store" || true -} - -docker_login_filtered() { - # $1 - username, $2 - password, $3 - registry - # this filters the stderr of the `podman --remote login`, without merging stdout and stderr together - { echo "$2" | podman --remote login -u "$1" --password-stdin "$3" 2>&1 1>&3 | filter_docker_warning 1>&2; } 3>&1 -} - -gl_write_auto_build_variables_file() { - echo "CI_APPLICATION_TAG=$CI_APPLICATION_TAG@$(podman --remote image inspect --format='{{ index (split (index .RepoDigests 0) "@") 1 }}' "$image_tagged")" > gl-auto-build-variables.env -} - - -if [[ -z "$CI_COMMIT_TAG" ]]; then - export CI_APPLICATION_REPOSITORY=${CI_APPLICATION_REPOSITORY:-$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG} - export CI_APPLICATION_TAG=${CI_APPLICATION_TAG:-$CI_COMMIT_SHA} -else - export CI_APPLICATION_REPOSITORY=${CI_APPLICATION_REPOSITORY:-$CI_REGISTRY_IMAGE} - export CI_APPLICATION_TAG=${CI_APPLICATION_TAG:-$CI_COMMIT_TAG} -fi - -DOCKER_BUILDKIT=1 -image_tagged="$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" -image_latest="$CI_APPLICATION_REPOSITORY:latest" - -if [[ -n "$CI_REGISTRY" && -n "$CI_REGISTRY_USER" ]]; then - echo "Logging in to GitLab Container Registry with CI credentials..." - docker_login_filtered "$CI_REGISTRY_USER" "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" -fi - - -# xxx seccomp for IA git repos -# o/w opening seccomp profile failed: open /etc/containers/seccomp.json: no such file or directory -build_args=( - --cache-from "$CI_APPLICATION_REPOSITORY" - $AUTO_DEVOPS_BUILD_IMAGE_EXTRA_ARGS - --security-opt seccomp=unconfined - --tag "$image_tagged" -) - -if [ "$NOMAD_VAR_SERVERLESS" = "" ]; then - build_args+=(--tag "$image_latest") -fi - -if [[ -n "${DOCKERFILE_PATH}" ]]; then - build_args+=(-f "$DOCKERFILE_PATH") -fi - -if [[ -n "$AUTO_DEVOPS_BUILD_IMAGE_FORWARDED_CI_VARIABLES" ]]; then - build_secret_file_path=/tmp/auto-devops-build-secrets - "$(dirname "$0")"/export-build-secrets > "$build_secret_file_path" # xxx /build/export-build-secrets - build_args+=( - --secret "id=auto-devops-build-secrets,src=$build_secret_file_path" - ) -fi - - -( - set -x - podman --remote buildx build "${build_args[@]}" --progress=plain . 2>&1 -) - -( - set -x - podman --remote push "$image_tagged" -) -if [ "$NOMAD_VAR_SERVERLESS" = "" ]; then - ( - set -x - podman --remote push "$image_latest" - ) -fi - - -gl_write_auto_build_variables_file diff --git a/build.yml b/build.yml deleted file mode 100644 index 66e2624..0000000 --- a/build.yml +++ /dev/null @@ -1,23 +0,0 @@ -# Tracey 3/2024: -# This was adapted & simplified from: -# https://gitlab.com/gitlab-org/gitlab/-/raw/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml - -build: - stage: build - # If need to rebuild this image while runners are down, `cd` to this directory, then, as root: - # podman login registry.gitlab.com - # podman build --net=host --tag registry.gitlab.com/internetarchive/nomad/master . && sudo podman push registry.gitlab.com/internetarchive/nomad/master - image: registry.gitlab.com/internetarchive/nomad/master - variables: - DOCKER_HOST: 'unix:///run/podman/podman.sock' - DOCKER_TLS_CERTDIR: '' - DOCKER_BUILDKIT: 1 - script: - - /build.sh - artifacts: - reports: - dotenv: gl-auto-build-variables.env - rules: - - if: '$BUILD_DISABLED' - when: never - - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' diff --git a/deploy.sh b/deploy.sh deleted file mode 100755 index c430a3d..0000000 --- a/deploy.sh +++ /dev/null @@ -1,346 +0,0 @@ -#!/bin/bash -e - -function verbose() { - if [ "$NOMAD_VAR_VERBOSE" ]; then - echo "$@"; - fi -} - - -function main() { - if [ "$NOMAD_TOKEN" = test ]; then - # during testing, set any var that isn't set, to an empty string, when the var gets used later - NOMAD_VAR_NO_DEPLOY=${NOMAD_VAR_NO_DEPLOY:-""} - GITHUB_ACTIONS=${GITHUB_ACTIONS:-""} - NOMAD_VAR_HOSTNAMES=${NOMAD_VAR_HOSTNAMES:-""} - CI_REGISTRY_READ_TOKEN=${CI_REGISTRY_READ_TOKEN:-""} - NOMAD_VAR_COUNT=${NOMAD_VAR_COUNT:-""} - NOMAD_SECRETS=${NOMAD_SECRETS:-""} - NOMAD_ADDR=${NOMAD_ADDR:-""} - NOMAD_TOKEN_PROD=${NOMAD_TOKEN_PROD:-""} - NOMAD_TOKEN_STAGING=${NOMAD_TOKEN_STAGING:-""} - NOMAD_TOKEN_EXT=${NOMAD_TOKEN_EXT:-""} - PRIVATE_REPO=${PRIVATE_REPO:-""} - fi - - - # IF someone set this programmatically in their project yml `before_script:` tag, etc., exit - if [ "$NOMAD_VAR_NO_DEPLOY" ]; then exit 0; fi - - if [ "$GITHUB_ACTIONS" ]; then github-setup; fi - - ############################### NOMAD VARS SETUP ############################## - - # auto-convert from pre-2022 var name - if [ "$BASE_DOMAIN" = "" ]; then - BASE_DOMAIN="$KUBE_INGRESS_BASE_DOMAIN" - fi - - MAIN_OR_PROD_OR_STAGING_OR_EXT= - MAIN_OR_PROD_OR_STAGING_OR_EXT_SLUG= - PRODUCTION= - STAGING= - EXT= - if [ "$CI_COMMIT_REF_SLUG" = "main" -o "$CI_COMMIT_REF_SLUG" = "master" ]; then - MAIN_OR_PROD_OR_STAGING_OR_EXT=1 - MAIN_OR_PROD_OR_STAGING_OR_EXT_SLUG=1 - elif [ "$CI_COMMIT_REF_SLUG" = "production" ]; then - PRODUCTION=1 - MAIN_OR_PROD_OR_STAGING_OR_EXT=1 - MAIN_OR_PROD_OR_STAGING_OR_EXT_SLUG=1 - elif [ "$BASE_DOMAIN" = "prod.archive.org" ]; then - # NOTE: this is _very_ unusual -- but it's where a repo can elect to have - # another branch name (not `production`) deploy to production cluster via (typically) various - # gitlab CI/CD variables pegged to that branch name. - PRODUCTION=1 - MAIN_OR_PROD_OR_STAGING_OR_EXT=1 - elif [ "$CI_COMMIT_REF_SLUG" = "staging" ]; then - STAGING=1 - MAIN_OR_PROD_OR_STAGING_OR_EXT=1 - MAIN_OR_PROD_OR_STAGING_OR_EXT_SLUG=1 - elif [ "$CI_COMMIT_REF_SLUG" = "ext" ]; then - EXT=1 - MAIN_OR_PROD_OR_STAGING_OR_EXT=1 - MAIN_OR_PROD_OR_STAGING_OR_EXT_SLUG=1 - fi - - - # some archive.org specific production/staging/ext deployment detection & var updates first - if [[ "$BASE_DOMAIN" == *.archive.org ]]; then - if [ $PRODUCTION ]; then - export BASE_DOMAIN=prod.archive.org - if [[ "$CI_PROJECT_PATH_SLUG" == internetarchive-emularity-* ]]; then - export BASE_DOMAIN=ux-b.archive.org - fi - elif [ $STAGING ]; then - export BASE_DOMAIN=staging.archive.org - elif [ $EXT ]; then - export BASE_DOMAIN=ext.archive.org - fi - - if [ $PRODUCTION ]; then - if [ "$NOMAD_TOKEN_PROD" != "" ]; then - export NOMAD_TOKEN="$NOMAD_TOKEN_PROD" - echo using nomad production token - fi - if [ "$NOMAD_VAR_COUNT" = "" ]; then - export NOMAD_VAR_COUNT=3 - fi - elif [ $STAGING ]; then - if [ "$NOMAD_TOKEN_STAGING" != "" ]; then - export NOMAD_TOKEN="$NOMAD_TOKEN_STAGING" - echo using nomad staging token - fi - elif [ $EXT ]; then - if [ "$NOMAD_TOKEN_EXT" != "" ]; then - export NOMAD_TOKEN="$NOMAD_TOKEN_EXT" - echo using nomad ext token - fi - fi - fi - - export BASE_DOMAIN - - - # Make a nice "slug" that is like [GROUP]-[PROJECT]-[BRANCH], each component also "slugged", - # where "-main", "-master", "-production", "-staging", "-ext" are omitted. - # Respect DNS 63 max chars limit. - export BRANCH_PART="" - if [ ! $MAIN_OR_PROD_OR_STAGING_OR_EXT_SLUG ]; then - export BRANCH_PART="-${CI_COMMIT_REF_SLUG}" - fi - export NOMAD_VAR_SLUG=$(echo "${CI_PROJECT_PATH_SLUG}${BRANCH_PART}" |cut -b1-63) - # make nice (semantic) hostname, based on the slug, eg: - # services-timemachine.x.archive.org - # ia-petabox-webdev-3939-fix-things.x.archive.org - # however, if repo has list of 1+ custom hostnames it wants to use instead for main/master branch - # review app, then use them and log during [deploy] phase the first hostname in the list - export HOSTNAME="${NOMAD_VAR_SLUG}.${BASE_DOMAIN}" - # NOTE: YAML or CI/CD Variable `NOMAD_VAR_HOSTNAMES` is *IGNORED* -- and automatic $HOSTNAME above - # is used for branches not main/master/production/staging/ext - - # make even nicer names for archive.org processing cluster deploys - if [ "$BASE_DOMAIN" = "work.archive.org" ]; then - export HOSTNAME="${CI_PROJECT_NAME}${BRANCH_PART}.${BASE_DOMAIN}" - fi - - if [ "$NOMAD_ADDR" = "" ]; then - export NOMAD_ADDR=https://$BASE_DOMAIN - if [ "$BASE_DOMAIN" = archive.org ]; then - # an archive.org specific adjustment - export NOMAD_ADDR=https://dev.archive.org - fi - fi - - if [ "$NOMAD_VAR_HOSTNAMES" != "" -a "$BASE_DOMAIN" != "" ]; then - # Now auto-append .$BASE_DOMAIN to any hostname that isn't a fully qualified domain name - export NOMAD_VAR_HOSTNAMES=$(deno eval 'const fqdns = JSON.parse(Deno.env.get("NOMAD_VAR_HOSTNAMES")).map((e) => e.includes(".") ? e : e.concat(".").concat(Deno.env.get("BASE_DOMAIN"))); console.log(fqdns)') - fi - - if [ "$MAIN_OR_PROD_OR_STAGING_OR_EXT" -a "$NOMAD_VAR_HOSTNAMES" != "" ]; then - export HOSTNAME=$(echo "$NOMAD_VAR_HOSTNAMES" |cut -f1 -d, |tr -d '[]" ' |tr -d "'") - else - NOMAD_VAR_HOSTNAMES= - - if [ "$PRODUCTION" -o "$STAGING" -o "$EXT" ]; then - export HOSTNAME="${CI_PROJECT_NAME}.$BASE_DOMAIN" - fi - fi - - - if [ "$NOMAD_VAR_HOSTNAMES" = "" ]; then - export NOMAD_VAR_HOSTNAMES='["'$HOSTNAME'"]' - fi - - - if [[ "$NOMAD_ADDR" == *crawl*.archive.org:* ]]; then # nixxx - export NOMAD_VAR_CONSUL_PATH='/usr/local/bin/consul' - fi - - - if [ "$CI_REGISTRY_READ_TOKEN" = "0" ]; then unset CI_REGISTRY_READ_TOKEN; fi - - ############################### NOMAD VARS SETUP ############################## - - - - if [ "$ARG1" = "stop" ]; then - nomad stop $NOMAD_VAR_SLUG - exit 0 - fi - - - - echo using nomad cluster $NOMAD_ADDR - echo deploying to https://$HOSTNAME - - # You can have your own/custom `project.nomad` in the top of your repo - or we'll just use - # this fully parameterized nice generic 'house style' project. - # - # Create project.hcl - including optional insertions that a repo might elect to inject - REPODIR="$(pwd)" - cd /tmp - if [ -e "$REPODIR/project.nomad" ]; then - cp "$REPODIR/project.nomad" project.nomad - else - rm -f project.nomad - wget -q https://gitlab.com/internetarchive/nomad/-/raw/master/project.nomad - fi - - verbose "Replacing variables internal to project.nomad." - - ( - grep -F -B10000 VARS.NOMAD--INSERTS-HERE project.nomad - # if this filename doesnt exist in repo, this line noops - cat "$REPODIR/vars.nomad" 2>/dev/null || echo - grep -F -A10000 VARS.NOMAD--INSERTS-HERE project.nomad - ) >| tmp.nomad - cp tmp.nomad project.nomad - ( - grep -F -B10000 JOB.NOMAD--INSERTS-HERE project.nomad - # if this filename doesnt exist in repo, this line noops - cat "$REPODIR/job.nomad" 2>/dev/null || echo - grep -F -A10000 JOB.NOMAD--INSERTS-HERE project.nomad - ) >| tmp.nomad - cp tmp.nomad project.nomad - ( - grep -F -B10000 GROUP.NOMAD--INSERTS-HERE project.nomad - # if this filename doesnt exist in repo, this line noops - cat "$REPODIR/group.nomad" 2>/dev/null || echo - grep -F -A10000 GROUP.NOMAD--INSERTS-HERE project.nomad - ) >| tmp.nomad - cp tmp.nomad project.nomad - - verbose "project.nomad -> project.hcl" - - cp project.nomad project.hcl - - verbose "NOMAD_VAR_SLUG variable substitution" - # Do the one current substitution nomad v1.0.3 can't do now (apparently a bug) - sed -ix "s/NOMAD_VAR_SLUG/$NOMAD_VAR_SLUG/" project.hcl - - case "$NOMAD_ADDR" in - https://work.archive.org|https://hind.archive.org|https://dev.archive.org|https://ext.archive.org) - # HinD cluster(s) use `podman` driver instead of `docker` - sed -ix 's/driver\s*=\s*"docker"/driver="podman"/' project.hcl # xxx - sed -ix 's/memory_hard_limit/# memory_hard_limit/' project.hcl # xxx - ;; - esac - - verbose "Handling NOMAD_SECRETS." - if [ "$NOMAD_SECRETS" = "" ]; then - # Set NOMAD_SECRETS to JSON encoded key/val hashmap of env vars starting w/ "NOMAD_SECRET_" - # (w/ NOMAD_SECRET_ prefix omitted), then convert to HCL style hashmap string (chars ":" => "=") - echo '{}' >| env.env - ( env | grep -qE ^NOMAD_SECRET_ ) && ( - echo NOMAD_SECRETS=$(deno eval 'console.log(JSON.stringify(Object.fromEntries(Object.entries(Deno.env.toObject()).filter(([k, v]) => k.startsWith("NOMAD_SECRET_")).map(([k ,v]) => [k.replace(/^NOMAD_SECRET_/,""), v]))))' | sed 's/":"/"="/g') >| env.env - ) - else - # this alternate clause allows GitHub Actions to send in repo secrets to us, as a single secret - # variable, as our JSON-like hashmap of keys (secret/env var names) and values - cat >| env.env << EOF -NOMAD_SECRETS=$NOMAD_SECRETS -EOF - fi - - verbose "copy current env vars starting with "CI_" to "NOMAD_VAR_CI_" variants & inject them into shell" - deno eval 'Object.entries(Deno.env.toObject()).map(([k, v]) => console.log("export NOMAD_VAR_"+k+"="+JSON.stringify(v)))' | grep -E '^export NOMAD_VAR_CI_' >| ci.env - source ci.env - rm ci.env - - if [ "$NOMAD_TOKEN" = test ]; then - nomad run -output -var-file=env.env project.hcl >| project.json - exit 0 - fi - - set -x - nomad validate -var-file=env.env project.hcl - nomad plan -var-file=env.env project.hcl 2>&1 |sed 's/\(password[^ \t]*[ \t]*\).*/\1 ... /' |tee plan.log || echo - export INDEX=$(grep -E -o -- '-check-index [0-9]+' plan.log |tr -dc 0-9) - - # some clusters sometimes fail to fetch deployment :( -- so let's retry 5x - for RETRIES in $(seq 1 5); do - set -o pipefail - nomad run -var-file=env.env -check-index $INDEX project.hcl 2>&1 |tee check.log - if [ "$?" = "0" ]; then - if grep -E 'Status[ ]*=[ ]*failed' check.log; then - # for example, unhealthy 5x, unable to roll back, ends up failing - exit 1 - fi - - # This particular fail case output doesnt seem to exit non-zero -- so we have to check for it - # ==> 2023-03-29T17:21:15Z: Error fetching deployment - if ! grep -F 'Error fetching deployment' check.log; then - echo deployed to https://$HOSTNAME - return - fi - fi - - echo retrying.. - sleep 10 - continue - done - exit 1 -} - - -function github-setup() { - # Converts from GitHub env vars to GitLab-like env vars - - # You must add these as Secrets to your repository: - # NOMAD_TOKEN - # NOMAD_TOKEN_PROD (optional) - # NOMAD_TOKEN_STAGING (optional) - # NOMAD_TOKEN_EXT (optional) - - # You may override the defaults via passed-in args from your repository: - # BASE_DOMAIN - # NOMAD_ADDR - # https://github.com/internetarchive/cicd - - - # Example of the (limited) GitHub ENV vars that are avail to us: - # GITHUB_REPOSITORY=internetarchive/dyno - - # (registry host) - export CI_REGISTRY=ghcr.io - - local GITHUB_REPOSITORY_LC=$(echo "${GITHUB_REPOSITORY?}" |tr A-Z a-z) - - # eg: ghcr.io/internetarchive/dyno:main (registry image) - export CI_GITHUB_IMAGE="${CI_REGISTRY?}/${GITHUB_REPOSITORY_LC?}:${GITHUB_REF_NAME?}" - # since the registry image :part uses a _branch name_ and not a commit id (like gitlab), - # we can end up with a stale deploy if we happen to redeploy to the same VM. so force a pull. - export NOMAD_VAR_FORCE_PULL=true - - # eg: dyno (project name) - export CI_PROJECT_NAME=$(basename "${GITHUB_REPOSITORY_LC?}") - - # eg: main (branchname) xxxd slugme - export CI_COMMIT_REF_SLUG="${GITHUB_REF_NAME?}" - - # eg: internetarchive-dyno xxxd better slugification - export CI_PROJECT_PATH_SLUG=$(echo "${GITHUB_REPOSITORY_LC?}" |tr '/.' - |cut -b1-63 | sed 's/[^a-z0-9\-]//g') - - if [ "$PRIVATE_REPO" = "false" ]; then - # turn off `docker login`` before pulling registry image, since it seems like the TOKEN expires - # and makes re-deployment due to containers changing hosts not work.. sometimes? always? - unset CI_REGISTRY_READ_TOKEN - fi - - - # unset any blank vars that come in from GH actions - for i in $(env | grep -E '^NOMAD_VAR_[A-Z0-9_]+=$' |cut -f1 -d=); do - unset $i - done - - # see if we should do nothing - if [ "$NOMAD_VAR_NO_DEPLOY" ]; then exit 0; fi - if [ "${NOMAD_TOKEN}${NOMAD_TOKEN_PROD}${NOMAD_TOKEN_STAGING}${NOMAD_TOKEN_EXT}" = "" ]; then exit 0; fi -} - - -ARG1= -if [ $# -gt 0 ]; then ARG1=$1; fi - -main diff --git a/hello-world.hcl b/hello-world.hcl deleted file mode 100644 index 661f948..0000000 --- a/hello-world.hcl +++ /dev/null @@ -1,57 +0,0 @@ -# Minimal basic project using only GitLab CI/CD std. variables -# Run like: nomad run hello-world.hcl - -# Variables used below and their defaults if not set externally -variables { - # These all pass through from GitLab [build] phase. - # Some defaults filled in w/ example repo "bai" in group "internetarchive" - # (but all 7 get replaced during normal GitLab CI/CD from CI/CD variables). - CI_REGISTRY = "registry.gitlab.com" # registry hostname - CI_REGISTRY_IMAGE = "registry.gitlab.com/internetarchive/bai" # registry image location - CI_COMMIT_REF_SLUG = "main" # branch name, slugged - CI_COMMIT_SHA = "latest" # repo's commit for current pipline - CI_PROJECT_PATH_SLUG = "internetarchive-bai" # repo and group it is part of, slugged - CI_REGISTRY_USER = "" # set for each pipeline and .. - CI_REGISTRY_PASSWORD = "" # .. allows pull from private registry - - # Switch this, locally edit your /etc/hosts, or otherwise. as is, webapp will appear at: - # https://internetarchive-bai-main.x.archive.org/ - BASE_DOMAIN = "x.archive.org" -} - -job "hello-world" { - datacenters = ["dc1"] - group "group" { - network { - port "http" { - to = 5000 - } - } - service { - tags = ["https://${var.CI_PROJECT_PATH_SLUG}-${var.CI_COMMIT_REF_SLUG}.${var.BASE_DOMAIN}"] - port = "http" - check { - type = "http" - port = "http" - path = "/" - interval = "10s" - timeout = "2s" - } - } - task "web" { - driver = "docker" - - config { - image = "${var.CI_REGISTRY_IMAGE}/${var.CI_COMMIT_REF_SLUG}:${var.CI_COMMIT_SHA}" - - ports = [ "http" ] - - auth { - server_address = "${var.CI_REGISTRY}" - username = "${var.CI_REGISTRY_USER}" - password = "${var.CI_REGISTRY_PASSWORD}" - } - } - } - } -} diff --git a/img/architecture.drawio.svg b/img/architecture.drawio.svg deleted file mode 100644 index 51fdfda..0000000 --- a/img/architecture.drawio.svg +++ /dev/null @@ -1,452 +0,0 @@ - - - - - - - - - -
-
-
- Hashistack -
-
-
-
- - Hashistack - -
-
- - - - - - virtual machine 1               - - - - - - - - - -
-
-
- Nomad daemon -
-
-
-
- - Nomad daemon - -
-
- - - - - -
-
-
- webapp2 -
-
-
-
- - webapp2 - -
-
- - - - - - - - - -
-
-
- docker -
-  daemon -
-
-
-
- - docker... - -
-
- - - - - -
-
-
- webapp2 -
-
-
-
- - webapp2 - -
-
- - - - - - - - - - -
-
-
- svc -
- discovery -
-
-
-
- - svc... - -
-
- - - - - -
-
-
- Consul -
- daemon -
-
-
-
- - Consul... - -
-
- - - - - - -
-
-
- Fabio -
- loadbalancer -
-
- (has https certs) -
-
-
-
- - Fabio... - -
-
- - - - - -      virtual    machine 2 - - - - - - - - - -
-
-
- Nomad daemon -
-
-
-
- - Nomad daemon - -
-
- - - - - -
-
-
- webapp2 -
-
-
-
- - webapp2 - -
-
- - - - - - - - - -
-
-
- docker -
-  daemon -
-
-
-
- - docker... - -
-
- - - - - -
-
-
- webapp1 -
-
-
-
- - webapp1 - -
-
- - - - - - - - - - -
-
-
- Consul -
- daemon -
-
-
-
- - Consul... - -
-
- - - - - - - - -
-
-
- http to -
- webapp1 -
-
-
-
- - http to... - -
-
- - - - - - - - -
-
-
- http to -
- webapp2 -
-
-
-
- - http to... - -
-
- - - - - - -
-
-
- httpS to either -
- webapp1 -
- webapp2 -
-
-
-
- - httpS to either... - -
-
- - - - - - - - -
-
-
- browser -
-
-
-
- - browser - -
-
- - - - - -
-
-
- service discovery -
-
-
-
- - service discovery - -
-
- - - - - - - -
-
-
- gitlab CI/CD pipeline -
- or admin -
-
-
-
- - gitla... - -
-
- - - - - -
-
-
- web -
-
-
-
- - web - -
-
- - -
- - - - - Viewer does not support full SVG 1.1 - - - -
\ No newline at end of file diff --git a/img/overview.drawio.svg b/img/overview.drawio.svg deleted file mode 100644 index 92c3d84..0000000 --- a/img/overview.drawio.svg +++ /dev/null @@ -1,180 +0,0 @@ - - - - - - - - - -
-
-
- Hashistack -
-
-
-
- - Hashistack - -
-
- - - - - - -
-
-
- browser -
-
-
-
- - browser - -
-
- - - - - - -
-
-
-
- - loadbalancer  - -
-
-
-
-
- - loadbalancer  - -
-
- - - - -
-
-
- http to webapp -
-
-
-
- - http to webapp - -
-
- - - - -
-
-
-
-
- - webapp - -
-
-
-
-
-
-
-
-
-
-
-
-
- - webapp... - -
-
- - - - - - -
-
-
- httpS to webapp -
-
-
-
- - httpS to webapp - -
-
- - - - - - -
-
-
- http daemon -
-
-
-
- - http daem... - -
-
- - - - -
-
-
- DB -
-
-
-
- - DB - -
-
- - - - -
- - - - - Viewer does not support full SVG 1.1 - - - -
\ No newline at end of file diff --git a/img/overview2.drawio.svg b/img/overview2.drawio.svg deleted file mode 100644 index 5de17a6..0000000 --- a/img/overview2.drawio.svg +++ /dev/null @@ -1,299 +0,0 @@ - - - - - - - - - -
-
-
- Hashistack -
-
-
-
- - Hashistack - -
-
- - - - - - -
-
-
- browser -
-
-
-
- - browser - -
-
- - - - - - -
-
-
-
- - Fabio - - loadbalancer  -
-
-
-
-
- - Fabio loadbalancer  - -
-
- - - - -
-
-
- http to webapp -
-
-
-
- - http to webapp - -
-
- - - - -
-
-
-
- webapp - - TaskGroup - -
-
-
-
-
-
-
-
-
-
-
-
-
- - webapp TaskGroup... - -
-
- - - - - - -
-
-
- httpS to webapp -
-
-
-
- - httpS to webapp - -
-
- - - - - - -
-
-
- http daemon -
-
-
-
- - http daem... - -
-
- - - - -
-
-
- DB -
-
-
-
- - DB - -
-
- - - - - - - - - - - - -
-
-
- gitlab CI/CD  -
- or admin -
-
-
-
- - gitlab CI/CD... - -
-
- - - - - - - - - -
-
-
- - Nomad -
- daemon -
-
-
-
-
- - Nomad... - -
-
- - - - - - -
-
-
- - docker -
- daemon -
-
-
-
-
- - docker... - -
-
- - - - -
-
-
- - Vault -
- daemon -
-
-
-
-
- - Vault... - -
-
- - - - - - -
-
-
- - Consul -
- daemon -
-
-
-
-
- - Consul... - -
-
- - -
- - - - - Viewer does not support full SVG 1.1 - - - -
\ No newline at end of file diff --git a/img/prod.jpg b/img/prod.jpg deleted file mode 100644 index 55c7cca2a7ea711b660ea1a17fa5df93271b0dbd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27724 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 QSN|Ub6aN=-3i`<300Q+oIsgCw diff --git a/img/protect.jpg b/img/protect.jpg deleted file mode 100644 index 5fc325ca4198baa19cc30ab148b3f9aad7181b08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27738 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 diff --git a/img/secrets.jpg b/img/secrets.jpg deleted file mode 100644 index 44caab5e8c91780421a7911e59934cb04a446d22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47183 zcmeFZ1yo$k)+T(KZrnAvHWJ+3oe(s*1$TD|!4iUNfFL2bOK=VD?$Wpu+(Lu|{?7Ye zn{UniX70bf|DQE8x1hS7uG+QF+4~e-Pw(1`hoy&20OyIkk~{zc0e~X>2YA>5?B#sz ztN}np1z-jM00lq*;Q?Sc0>OWPH+BHz4-5bd@D>2TN#Ou8yv2i8^&Eu1jzVN}z<eZTTf3{VGa&w4|X$47jr9i3l}F2Uo%$@E_O~1 zKvcrl)y%@d%9GmM%Er!FjP|^(la|`fQjAuYPlZ#(RmRHJPSM}p>Y2Z)mW98Ag^(q! zggCXRuduI^tCN+d8MUvIqqB#wuNdtg%7x+hcQ*$u^&cXh4q~)=D(ciSF78&;eC)jJ zoV4)S?v~cVnzHhLnG1g>M*EkW^zrdw_u*l8akt^%5)u;P;N<4u=4OLSuzC17dz$&O zIeXCkRfDXRhlRVHtEZidGxcwcX67zlo?^7L9Ple}{N)nA;ot66ociD8-yZn42mbAW ze|zBH9{9Hh{_pd^-`S3pGo0f1z_|?Ya0&>j!>N)BoG-aj^RROQg3?MVklz#xUjD%J ze?Xi$ikRhipce8u81>!BH|duT+vvVZGBVHAHPz&mp342f@X&3|TwNUzZ~(x`+0$K9 zL7G}m-+&r<7f$cU0epZ2z%aA$aFtS5fBIYJU)w+Q|Lx^=;WrHgfLXTRy8hPvKL>Cv z;j{?OB-P>K&Xyh)PH@}>$I=$A?w$aE@Y^mrA5YicmO};%|Bo>GxNXcG&8gL8~)Q4_)PfyNLx8;P(PKCrIs;sv@>@%^R$Ai z{Vy&~j$Uxz{ZW2D0Sqf=?Pu`zHw`BTqya0y8PEWzfv12BAPaA005iZ5umj8icR2Ed zkHc@t)5;g_69ABLarJYzv$6H0mV!S;D{3WY3wBm&Zcc7a0QkKoe#-#BwdNns8bs9b z_db^b_`17=^ZvxY_nE&1fTnN&Acp-06qZuKnYL@ z)B;UFJJ1ab0HeSZFb}K%o4_7$3|s)Wz#Rw#LIz=i@IfRXDiA%01;h;!1c`&>Kue&=riaF^aBBmfPp}OK!L!3 zz=0r$AcdfepoL(JV1wX>;E(VMAr9dkLLNc|LL)*q!YIN#!Y0Bo!WS?AMh6ptX~1k? zL9h&14Qv3m2D^h_f}_Bx;5={@xD`AIo&|4$Pr%qlEfyGBPxr$rY+*Fd*J4@Q57{tEF~;UtRSpRtVXOEtTSv> zY{RSJ>`Cks98??z92p!loR>J6I4wAfIA3t_ak+5SaGh{taLaIqaS!p3 z@fh&r@GS9O;pOA?;_cvr@#*ko@GbDe@C)$!@%IRj2p9>J22`ULD2rdZm2zd#0 z2z?1N2|p2T5`l>rh?IyNiQbWWzNPG-{7i*UB~E2W^_HrW>WG?2x%`T|-Gh0&wZ3)0)tr_m46 ze`R1~&}DeT(8O@aNX)3j=+9WrxXOgdB*Emyl*csp80oR#V~5A@A5SqOF!M3nGG{VR zvLLYVv)Hk`XPIV2WEEm{X3b+=WW!*SV)JGzXWL>YVt>jW%HG6&#sTFp;7H^c+-bnHuB~27Ycj|0SaA;h>FUJiHeI#6iOCK zrAil11fB#w=~Ko~ex{tEy#18cOtrf4eqRpi3rQM^0tz)E9s`FJ>RyRp^Q;$P0 zP;XS9RNqd&*#Ox<$DqjIi=muhis7CSpHaBcf-$49uko-6sfnY>r{~zu&7RkpBAV)& zmYM!EQ!~pm`(mzSo@IVvA!Ctdacn7R`PTBYm6%nM)q%B`b&~a=jhIcc&1YK)+f>^V zI~ltSyGwgT`w#YC9n>6(9UdHY9jl#CoJ^fsobj9;ocmlTU3^`pU0GbiT{qo?-BR2x z+?CynJU|}C9?hNv@XdG3i^=P?*Os@qcc%9@A03}MUtC{j-!VUCzev9We|i7H07!sE zK+g-B7a=dU0;K|TUxHqmzw8O33wjl_7pxFm5`q@u5HcRh8JZM&6Q`ikOJ@T;BI z3a`tp6mvy`il+nC3Ymzs~1@14J2pi?kd zC|FopL|PPG^sCspc(X*aq_0$E7vi-m}uH*E`pz(Kper+&?rRKhQfUHP|^MHq<^W zJlrxOIMOuAKiW9PH`XxDH{LM8KhZcTFxfmMG}SsSI^8iNG1EOOJ3BC^G&ef0IzPRj zy|A=sw79usxpcVfxO@rohTW|MuOhBSt>LVtu9L6lZ9Lwn-sIo>v?aSWw*72-WyfOY zc-Le1=iaM*%>C2@s)N!)?!)%a@}H-U43GAXU61ch!cTEev(A{#>dz(5$1e0P_AcEo zAFg7qNv?};cyD^XsDD|%b-KO#8ug9jTgi99?}I;de;nNT{6zlw?$_gAt@q0Js}D{O z53XkJW`A&j-vLDgYj~LPsu%z;^#K5X3eL|A|Kx0cNWjC5R6Kt{aGLnv$A9v=Kcx5p zAb$}6v;qL&={NwSXTYfuTtBuEJT8e1?-v0;w~)WpKc)bHhcyrPpVz>UfK!vThlgu8 z??g@nfM1CZ4|n+w55Mx^TxSIUy8Qm+ioc~%rvV_C8m?1rhN|w5>Hqlh&;{V4fI=X$ zU=S^UfC~cSf*$$+D!2`ZaAE>4e-#EHfFX!T$SA02=x~7s9C)M^3`T%}5fOhAd{7{K z9Dv{=;?ZzPA>nJ7A=A1MaEB!3qR>g#bQ5Y$UqE@x-9u5)h#nD>kkT_SGCgMI<>MC+ z6cUz^m6KOcRC@AEOIt@5PRT4Rt*mWq?d&}~y}W&V{rtmTy$*j95gC<~{5B;u?Ol3C zUVcGgQE^FWS#4c?Lt|5OOKVSWU;n`1(D2C2?A-jq;?go~YkOyRZ~x%%^U>wi_05;t zuiw7^_-z*m0RL&$-z@tNcHzS9LV!TP5ai!>fe?J)6^si(q~SuslhQyobHk_Q4nZN1 zPRy<8My2D?ydX4ppGG5s@@~;z{xvNNa#*6E35ttXzZm#))YbhgD0eFf8pcXmPZ$bKj zHgZi1>^Ve1wuqsR)?N<-2k3JiGfnQmsv2cIP_M(^5>%qMB>F?8q@zVgW6uS(TQnGPtPfd4@SnLYp-RCoE`wHgSh zj_4i$&miX1V4g2d;%tUbTV}2Anz(rVZiY7Rku$!ZdMQ`R^rV_VCRX-4`D-8VMoc$Z z-XT#3b*ThCHhMvcGF|yo@&0kl=xs6U3hHxK9`>0orwKL2>SvPV0+Eh4hMhRwxrLSM zmGrS?B{i@6TnBP6c=)9-LnXK-?RQNxye#2!8Q?$jcU zR~%oZDl&x(+UCI4JKVs2xeW4Y?zm%_q^mI>1JQ*|VGdHxmv5)c@~LUI0`LPB5D`Xml^xz^ZcLR!4&U(_nX zo+J;8o{eige|=AZiO}Ap=7KcC27B5!hJ;QysB^SLS8We;HMM64QRzJ&c{R=uQ(OCW zRjY>Ty&I|b`=D(K+GG99r`bR5ZM>B)-D@J2{V`v~fHuTh%|BgybMC!+DS;cF=u8)O zaui$=?;=){b$uLl;QW%z)?=JSMr^I}Q?E-Z4Ar}5aLv{C0iYuQ4LKNie=66v6%c*} zes(9Sa$AloTt3v5f`{;Ce~4s9qO>Y{Yb?V+ywIvRy4-?1YSPN<0bo^{ToqFk!}8m- z*~ppuiJx!(*qVk%Z`3r~2oKgVD0_dV>Mmi@er3NOvK1@Q+YzuLS~uz0U6k#!L6?|i zx#^$1VE`N+=0(95{QutdVfrgJo(Wsr8z1+}NJ*78vfOp)>$ug$%(X*Uc*JON9p4Eo zr6rv9Rr%mvig}b?V%=gfWy+C%Q6^Hg8T$wKv6|5)dKAp7k7hBU!tqbKf{>-4HIod5 zCA^6?7U>~YtDLUhyOv)4fZCQw{I|3q>nBxd{KRoq387L#bPzA%&pM)-(d=(Tow=)~ z!8hD&Dg}qGc6h65K zT}Z96;y@F~Mi@>(_9X|_a{^`=KGrm+Fg76pU!_i64qumadmyselsOStf_=+(zfT{wGp11h7sxx$SGE3je%I!$# z-BARAz#M3tpG`nDoFSAlOnNcf z_6AIh=6hQ;vY?&4Kr$W88c~5t48_e#T$=OYClYvVaJK$9DsYw-X>gXd@@7*VBuDU1 zD?;~up2Sy$t5SO=i8+P)!+UKufIveIURAJOkC}yUg6Kqp{mEYv07s82j>6cbV?vS# z8AD`*saJQIzKD8l2UGmF+UGr8pKvWE#r@ux zS6^$BKri4Sz@x_?h^0H`@5^eW=RIq9F9pHC6V491Je9=LP|?*TktVi`6WVpPn*%iD z2>zk+z1Q#&LK9O%_=j8IGkz|DA}A>Mg`)Hy#8ud%q-%gC$$+oh?;2-7 zC5f2I?k#>y!v_Gpyz=ky#|rQPkoo`DqVN8?Vq{##+ZBGjzSrghJhC4*q&m@8RqA?; zhgGi9&F6_kJpf&4EZ-LS#LEf4tZhV8RzX~0y@Z7Y+4^bRMua}hVX5Y5OkXE_$NlBZ z8Q<{g)#vB+JK~Mi1hMj9@SiSYzV8YfqP~O$3#KeKTvPN%sUC~^G$s@@;p8=Ci?AM`(|AqSVrlm(?#z-atJHi%42S#1m|VqGXUOB~m@w{TpdS=qacOX7Cu zyLAEIH6K?=OXel90atsD6AC7_3@905m~)r~cZ%3|2p;w&I3Zv^SV)<|!oZjJlXF0d zFQ3Tk2-tUfhy%lEo8F85G_be3@C9>*gE)1xO#Pfyt6kCD84*NpOJ_~u1&Sqj?i_tteISDBJATzU}YD~Y0G>^iZnaM1*isE<% zp_D?V4QBf0eyrUbx6h^l{WTUW$68X;9-&2*uCU1_Xb}}Z>*QXWH==2mXesO?>(gbHfGa;g4##k65Z~FB)gVj{|l#%!M!RBb2rfFuXD~{TN5JYZIJ2urQJBKXiV~R_hJt>){w9! zldX6Pp-pY;i}k3*2LQ&f@pVAzG1&*R1Wyl2Z|4WVfg}e@RYk3jrSJ&Wx?X+8UewqW zJ*rYhv`K9(I81HnP5gB~)6Z=?z>d!rUkmeBx?_{=0Ls-~*ax=eE{(nHFds4Bs?|f7 zgMo^<*{`vy`Wg29z2%=h9$7I>g78h^yJs8f`jL|Ps7W97yS)xe<1Q?0s=yXv(J5rq z>TmHH;X+mQ$7z^$j>+v@T@-}rKHGN07sQ_Ep~$>xE-%<$D7!DJKs7!e({|I+DO}6Z z0HN(6@q-->W$hQ)VK2uPb@rMDY6S5V%Z~EVTeO4C^K(Vx1}cqgg^lNC)OexoQ)G6W zXSsRQu@xX-6F+RuCQOpNA|t%Pwdvf*9aB5spn4i)Ve6On<`srwrq5WNYCIbn`9U9>Ji z%Giunm`?WgsR27jf`T9YsTSiGQ5pXIci_lI*ySb)8|o!tJ8Ip0L+{!Ih_i*wDHBv%X%l#!vM!q4I0EMhHz? zsA|9zGA-W*&YGQpcOI#tsjnr^s1w>qkCszTyJZqiu+kFhBnhF7`a*-PRn)gN;`)V9 z61;t4$j2IIqtW3+f*7X(fI`JnWlIGyH+;{O^Km~=?x%E@cWtUWt!(R1j+@1xbbA4TLZIO6~;&{rb z-k5v5G6M!8saH_^s5Szn?tH0C2uV)Ti|8j$Us5ovK1~6wXAL^Bqpvh;@?cyV zNXP`wXMG+2{c1Zmj@!8;)}F_^MB^y9ovG;9w3f%O$-|*p2MBG(B2Q~(oW_gZZ23R)x~)*u^(>r_Dv{v|_R?C# z^@~SG0sCiPC|*w2{}QVz%y3xRZAsFjj!l<_APCM~HnAlcGXCHv@mg6^mPxwZHdMlf z=_vM4WA9NCm#)Pg$He^nRAc=9HoDlJONMaH^4Eu7&*)l(-QrWG!b!*}{mwNWfF6kg z6eqSB%t*_7OA(T{<&?#))bE!?YE^&l?31MS90f*p!ZEI5qf)FVbW%w{(;LZguENO^ z0H49R?gHV>FLCtZ_h(M)N6OK9nkv2Sul44eQ3Sk%94%VLeLR*8jnojwhFg`bts{10 z3*Y8$l9QLxWef+QA38z2dyr&bukBpzCX!&hVZ}`e(Uw?)a5MlO!`oudRKsC+M$g%W zoEX>d`&EL^SK1~5`faGiZB)>1^i}w^Wd?H~UeyBE9D0h~gSosN6 zWnF?Khh0-hBOk3WP^jVGt94Tx64$?*uyU^}u_~cCMTVY|&C(H*kokKZlnz(R)_e>w zY+EUXZrqjsjKa%uN{)A+{{jTpxU%X52s-rQ6Ni7eS;1w6THxeWjkVmunFj$csFwCt-Bh zbp$vpCMQQCzh3{ueyup$fb7RXgO^U@;5{tJRRgKwiOV7X^0n*_p8RTyn$_9-%8FYF z`g-SJkA5lp2E;pcq?D(GSvj0^BfV}kQJ(Z|WKR-(F)n3DZM*F_)2@Myx2%`2vYB~@ z+Q?WqKC|=eZK%A0!C6kdAsMcnsbC~ZjN;hO+fgo7`M@7K@`+oc4s6VU!j~2wPHUC; zhR|PGzugVSmP7)6s3S|1+P5x5eARhof?lLY(;R0VOPK6=OjZgwQHNb+*}z%#>|mhl zI{gO~R?$~=QKrr0B1*K$*`EkBWwzeqkp)C+xO`$?v|P7nj~NkWKQLb+*#r;TXR1tj zeqyj`u8mC9C!lK^y5eC@lB8d{nWRbq%vX&Z{KJb&x{KEflk7SS%G&Km>9|o*6SAc} z;?s~rJ@iRCBO1sITi998O!O+NXu=hDf{Br?qDHO-=sO%^+cARWykKwM@H%PkoCCo$Dn?8izSGl6IN!Ub;;gKAOvjWsk6y*O0!Ccr6dJGvXSB_I`L_6M zo~|7o>AgR#VGx@lJV55t%ws|QbC7tjBf!gG?X*Ana}N?4I|tqCE6_-76Zy?BLPbY> zu@d5VUH`jt=(ROj=Kf2e33u_s&mo#e!cM8>j!qmq*L1AJrYSUUcX39fAe5U^$c#ay z$JPAhNE!O~qh1h;Q7t<>hDa0XkdGv_zL*)g91#sj4K?-k&8c=(j2S}o%C3x3cxOxI zFPL7+t8&FjR^`56$)#^wVQWK7l6%*b&hP+m$%EglHS>#vC9fB_U>=TJH!^_t3c|Hb z&!Bje)%(sH&0j_8BvH4H-#f)ogO(vFOfdP9_g2&#>1vtX#PcXtk?NQY4*f9 zsYl3jq1l6gD~r=hqd^gdJU&T$?{&ye8kn8#$i`}MwF7D7)=RZWQs#ofPl7$vmk(i1 zVbTkUj{FyyzW3IGF$pVvjG`gK`@vk5US!b^0J7MxDfukeXy+cHegb!L{bi_vYDzrf zSzQ1@&606pl-+{5B7y8H#dDINObIU}PK2FKQS;XFN+^3Ye`29i6CRy>;a5Qo8j*OT z)Abp}l};ttf?9nVeMh7>Y+| zy4fh*$CLi6Z+eBHoRcoQB$|5=cfX0Nq_DiulRnTA0;k`xGIDsmgD;F}lkr93$k>%g zi*1DM)k%MZrXa~+1K=(^e_NQZ@^V@=X-%rQYkONUfs4cS$cGTTH%NRa{AEAZeaEn_ zY3`=w3z#dAJD@(rLe`VZlsg8)$#j*fozriUZ~iy~v7_3>JqIy+UvSIDF~K5EHQxdK42C67PXe+@&z!m6J?|jP`Pth#6pIQ%R;FSuK zQ;fU%$H1|c6D4qi-OA@w{^VbIFdDM&O308+1xoJ{qfHCOoE(f}iSHy{R6o8p-n%bQ z?AKI^?QMNj(s$g)tx&?N6-TIZarZ&+7~qN%UR3tUwQFi(IP**%w?fmWPZE7S0Yd3= zOMaTx3wt^fY5A%S$ca6Fv;5U3cFCPXn>}sD`Km%2oJwZpyCc+4s7un@wD0x1fr`xXMztDTD#%lKnsDbiRfEkc z>AyrV8Oe8f-k;vceZ>ExJ;UpC5SFruU+$>IEaPNF<{$UUhH<@)&*9XJoll{z6=$&6 zu;A!5VN(_lj?w|Li*w*{#T_e^{hjm1hU3*&%U`Pp#Ge;((^P6doQga8-C3GHZjs;) zmL#VXWGrG4oR3Q5c^-P-$gZ`!Gm<`HWZ&A6b{jA5)xu_5H`s#k%6$vvyMNA*==Goh zADfKc4AIeY)$6ejmJSiSOl2A=^NQ52T4y_=m+P%9yelN{+G~*6aD&f=k+?nT<322u z4sM;&GFgY1vDTF+u%3-+d8&`~1&e_Q^UBo}=)Z9s{~YoxbNsM9rhNR#9Ck(V*qJ_Y zOhsgrfNAWdX|8k@kl*=iHv7wr9nae8=P8vysub7JyZ!+oWD21H|LMR&y(4Vn|1rJ(^=f2{Tek1TpfWp%FGt9*v@;?kE< zOjRDP|AV8n|9I2}ewh7%5LH;Rnkj;M_BO|M$ND+vmJg_lah9)ZMQz8}%<)Sz5wUr! z5{`=%zD)~xoWj#=;VI<8X`{*4UotDLNA#;x1rOq^Wk@Ntu+I^ckcBG6ES|+H%-YNq z%Fy!Obs&TOSFRdOaGh@8zQIk+fY$=%%J2lZC7%j8kWH8H(&O z-%%{h8bV+q!3uO@@O|rL)b^XW`uG~yTEfmL-gn1lAvH}^PPfF_D&KR1=NAbuDGy(c z;wV9CTWu8VX20@gm!Uzinw2UhaDHA*V}tPk%$avi55NM~$mz`xHD!R?q*pcZJ8E>c;XRMeIx7PnaThD+h@==6F*xkl;O##I zhW{~0{09cvuiT4mKLBn)DQfD5L4)nj&p5715C5aH|4(`kewe$Jz_Uw*RVDKZ;I?-d zq+94skolJmh^Yzoy9+;$I`bSQW21clRGRN>-j<>T0|^}HSqZu!lSrS~a{~hW=;Qoo z<&#)X+(A~8MwIztmy+aap*I6EJ{22=N-lw-Gc4yrWcpz*@@K_NWdXU*8 zm|YFcQa{1&FqJ+aBE<6 zLyqoZLWG*@`d`MqZ{-_akACjZk7U3qNF&Ci#XzDf62{Oi;Vv�HuVZh=wwg1sEF2 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%0D)E>t|wCN*%FWX~$FFSNvo>K7@n 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 Bo8$lh diff --git a/logo.jpg b/logo.jpg deleted file mode 100644 index d8a5351537309d8069f5c2bf3de0daecf3b71c77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2285 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 diff --git a/project.nomad b/project.nomad deleted file mode 100644 index 2ff454b..0000000 --- a/project.nomad +++ /dev/null @@ -1,459 +0,0 @@ -# Variables used below and their defaults if not set externally -variables { - # These all pass through from GitLab [build] phase. - # Some defaults filled in w/ example repo "bai" in group "internetarchive" - # (but all 7 get replaced during normal GitLab CI/CD from CI/CD variables). - CI_REGISTRY = "registry.gitlab.com" # registry hostname - CI_REGISTRY_IMAGE = "registry.gitlab.com/internetarchive/bai" # registry image location - CI_COMMIT_REF_SLUG = "master" # branch name, slugged - CI_COMMIT_SHA = "latest" # repo's commit for current pipline - CI_PROJECT_PATH_SLUG = "internetarchive-bai" # repo and group it is part of, slugged - - # NOTE: if repo is public, you can ignore these next 3 registry related vars - CI_REGISTRY_USER = "" # set for each pipeline and .. - CI_REGISTRY_PASSWORD = "" # .. allows pull from private registry - # optional CI/CD registry read token which allows rerun of deploy phase anytime later - CI_REGISTRY_READ_TOKEN = "" # preferred name - - - # This autogenerates from https://gitlab.com/internetarchive/nomad/-/blob/master/.gitlab-ci.yml - # & normally has "-$CI_COMMIT_REF_SLUG" appended, but is omitted for "main" or "master" branches. - # You should not change this. - SLUG = "internetarchive-bai" - - - # The remaining vars can be optionally set/overriden in a repo via CI/CD variables in repo's - # setting or repo's `.gitlab-ci.yml` file. - # Each CI/CD var name should be prefixed with 'NOMAD_VAR_'. - - # default 300 MB - MEMORY = 300 - # default 100 MHz - CPU = 100 - - # A repo can set this to "tcp" - can help for debugging 1st deploy - CHECK_PROTOCOL = "http" - # What path healthcheck should use and require a 200 status answer for succcess - CHECK_PATH = "/" - # Allow individual, periodic healthchecks this much time to answer with 200 status - CHECK_TIMEOUT = "2s" - # Dont start first healthcheck until container up at least this long (adjust for slow startups) - HEALTH_TIMEOUT = "20s" - - # How many running containers should you deploy? - # https://learn.hashicorp.com/tutorials/nomad/job-rolling-update - COUNT = 1 - - COUNT_CANARIES = 1 - - NETWORK_MODE = "bridge" - - NAMESPACE = "default" - - # only used for github repos - CI_GITHUB_IMAGE = "" - - CONSUL_PATH = "/usr/bin/consul" - - FORCE_PULL = false - - # For jobs with 2+ containers (and tasks) (so we can setup ports properly) - MULTI_CONTAINER = false - - # Persistent Volume - set to a (fully qualified) dest dir inside your container, if you need a PV. - # We suggest "/pv". - PERSISTENT_VOLUME = "" - - /* You can overrride this for type="batch" and "cron-like" jobs (they rerun periodically & exit). - Combine this var override, with a small `job.nomad` in your repo to setup a cron, - with contents in the file like this, to run every hour at 15m past the hour: - type = "batch" - periodic { - cron = "15 * * * * *" - prohibit_overlap = false # must be false cause of kv env vars task - } - */ - IS_BATCH = false - - # There are more variables immediately after this - but they are "lists" or "maps" and need - # special definitions to not have defaults or overrides be treated as strings. -} - -variable "PORTS" { - # You must have at least one key/value pair, with a single value of 'http'. - # Each value is a string that refers to your port later in the project jobspec. - # - # Note: use -1 for your port to tell nomad & docker to *dynamically* assign you a random high port - # then your repo can read the environment variable: NOMAD_PORT_http upon startup to know - # what your main daemon HTTP listener should listen on. - # - # Note: if your port *only* talks TCP directly (or some variant of it, like IRC) and *not* HTTP, - # then make your port number (key) *negative AND less than -1*. - # Don't worry -- we'll use the abs() of it; - # negative numbers makes them easily identifiable and partition-able below ;-) - # - # Note: if you want an extra port to only use HTTP and not HTTPS, add 10000 to your desired - # port number (so for 18989, the public url will be http://... mapped internally to :8989 ). - # - # Examples: - # NOMAD_VAR_PORTS='{ 5000 = "http" }' - # NOMAD_VAR_PORTS='{ -1 = "http" }' - # NOMAD_VAR_PORTS='{ 5000 = "http", 666 = "cool-ness" }' - # NOMAD_VAR_PORTS='{ 8888 = "http", 8012 = "backend", 7777 = "extra-service" }' - # NOMAD_VAR_PORTS='{ 5000 = "http", -7777 = "irc" }' - # NOMAD_VAR_PORTS='{ 5000 = "http", 18989 = "db" }' - type = map(string) - default = { 5000 = "http" } -} - -variable "HOSTNAMES" { - # This autogenerates from https://gitlab.com/internetarchive/nomad/-/blob/master/.gitlab-ci.yml - # but you can override to 1 or more custom hostnames if desired, eg: - # NOMAD_VAR_HOSTNAMES='["www.example.com", "site.example.com"]' - type = list(string) - default = ["group-project-branch-slug.example.com"] -} - -variable "VOLUMES" { - # Pass in a list of [host VM => container] direct pass through of volumes, eg: - # NOMAD_VAR_VOLUMES='["/usr/games:/usr/games:ro"]' - type = list(string) - default = [] -} - -variable "NOMAD_SECRETS" { - # this is automatically populated with NOMAD_SECRET_ env vars by @see .gitlab-ci.yml - type = map(string) - default = {} -} - - -locals { - # Ignore all this. really :) - - # Copy hashmap, but remove map key/val for the main/default port (defaults to 5000). - # Then split hashmap in two: one for HTTP port mappings; one for TCP (only; rare) port mappings. - ports_main = {for k, v in var.PORTS: k => v if v == "http"} - ports_extra_tmp = {for k, v in var.PORTS: k => v if v != "http"} - ports_extra_tmp2 = {for k, v in local.ports_extra_tmp: k => v if k > -2} - ports_extra_https = {for k, v in local.ports_extra_tmp2: k => v if k < 10000} - ports_extra_http = {for k, v in local.ports_extra_tmp: abs(k - 10000) => v if k > 10000} - ports_extra_tcp = {for k, v in local.ports_extra_tmp: abs(k) => v if k < -1} - # 1st docker container configures all ports *unless* MULTI_CONTAINER is true, then just main port - ports_docker = values(var.MULTI_CONTAINER ? local.ports_main : var.PORTS) - - # Now create a hashmap of *all* ports to be used, but abs() any portnumber key < -1 - ports_all = merge(local.ports_main, local.ports_extra_https, local.ports_extra_http, local.ports_extra_tcp, {}) - - # Use CI_GITHUB_IMAGE if set, otherwise use GitLab vars interpolated string - docker_image = var.CI_GITHUB_IMAGE != "" ? var.CI_GITHUB_IMAGE : "${var.CI_REGISTRY_IMAGE}/${var.CI_COMMIT_REF_SLUG}:${var.CI_COMMIT_SHA}" - # " - - # GitLab docker login user/pass timeout rather quickly. If admin set CI_REGISTRY_READ_TOKEN key - # in the group/repo [Settings] [CI/CD] [Variables] - then use a token-based alternative to deploy. - # Effectively, use CI_REGISTRY_READ_TOKEN variant if set; else use CI_REGISTRY_* PAIR - docker_user = var.CI_REGISTRY_READ_TOKEN != "" ? "deploy-token" : var.CI_REGISTRY_USER - docker_pass = [for s in [var.CI_REGISTRY_READ_TOKEN, var.CI_REGISTRY_PASSWORD] : s if s != ""] - # Make [true] (array of length 1) if all docker password vars are "" - docker_no_login = length(local.docker_pass) > 0 ? [] : [true] - - - # If job is using secrets and CI/CD Variables named like "NOMAD_SECRET_*" then set this - # string to a KEY=VAL line per CI/CD variable. If job is not using secrets, set to "". - kv = join("\n", [for k, v in var.NOMAD_SECRETS : join("", concat([k, "='", v, "'"]))]) - - volumes = concat( - var.VOLUMES, - var.PERSISTENT_VOLUME == "" ? [] : ["/pv/${var.CI_PROJECT_PATH_SLUG}:${var.PERSISTENT_VOLUME}"], - ) - - auto_promote = var.COUNT_CANARIES > 0 ? true : false - - # make boolean-like array that can logically omit 2 `dynamic` blocks below for type=batch - service_type = var.IS_BATCH ? [] : ["service"] - - # split the 1st hostname into non-domain and domain parts - host0parts = split(".", var.HOSTNAMES[0]) - host0 = local.host0parts[0] - host0domain = join(".", slice(local.host0parts, 1, length(local.host0parts))) - - legacy = var.CI_PROJECT_PATH_SLUG == "www-dweb-ipfs" ? true : (var.CI_PROJECT_PATH_SLUG == "www-dweb-webtorrent" ? true : false) # xxx - - legacy2 = local.host0domain == "staging.archive.org" || local.host0domain == "prod.archive.org" || var.HOSTNAMES[0] == "polyfill.archive.org" || var.HOSTNAMES[0] == "esm.archive.org" || var.HOSTNAMES[0] == "purl.archive.org" || var.HOSTNAMES[0] == "popcorn.archive.org" # xxx - - tags = local.legacy2 ? merge( - {for portnum, portname in local.ports_extra_https: portname => [ - # If the main deploy hostname is `card.example.com`, and a 2nd port is named `backend`, - # then make its hostname be `card-backend.example.com` - "urlprefix-${local.host0}-${portname}.${local.host0domain}" - ]}, - {for portnum, portname in local.ports_extra_http: portname => [ - "urlprefix-${local.host0}-${portname}.${local.host0domain} proto=http" - ]}, - {for portnum, portname in local.ports_extra_tcp: portname => [ - "urlprefix-:${portnum} proto=tcp" - ]}, - ) : merge( - {for portnum, portname in local.ports_extra_https: portname => [ - # If the main deploy hostname is `card.example.com`, and a 2nd port is named `backend`, - # then make its hostname be `card-backend.example.com` - local.legacy ? "https://${var.HOSTNAMES[0]}:${portnum}" : "https://${local.host0}-${portname}.${local.host0domain}" // xxx - ]}, - {for portnum, portname in local.ports_extra_http: portname => [ - "http://${local.host0}-${portname}.${local.host0domain}" - ]}, - {for portnum, portname in local.ports_extra_tcp: portname => []}, - ) -} - - -# VARS.NOMAD--INSERTS-HERE - - -# NOTE: for main or master branch: NOMAD_VAR_SLUG === CI_PROJECT_PATH_SLUG -job "NOMAD_VAR_SLUG" { - datacenters = ["dc1"] - namespace = "${var.NAMESPACE}" - - dynamic "update" { - for_each = local.service_type - content { - # https://learn.hashicorp.com/tutorials/nomad/job-rolling-update - max_parallel = 1 - # https://learn.hashicorp.com/tutorials/nomad/job-blue-green-and-canary-deployments - canary = var.COUNT_CANARIES - auto_promote = local.auto_promote - min_healthy_time = "30s" - healthy_deadline = "10m" - progress_deadline = "11m" - auto_revert = true - } - } - - dynamic "group" { - for_each = [ "${var.SLUG}" ] - labels = ["${group.value}"] - content { - count = var.COUNT - - restart { - attempts = 3 - delay = "15s" - interval = "30m" - mode = "fail" - } - network { - dynamic "port" { - # port.key == portnumber - # port.value == portname - for_each = local.ports_all - labels = [ "${port.value}" ] - content { - to = port.key - } - } - } - - - # The "service" stanza instructs Nomad to register this task as a service - # in the service discovery engine, which is currently Consul. This will - # make the service addressable after Nomad has placed it on a host and - # port. - # - # For more information and examples on the "service" stanza, please see - # the online documentation at: - # - # https://www.nomadproject.io/docs/job-specification/service.html - # - service { - name = "${var.SLUG}" - task = "http" - - tags = [for HOST in var.HOSTNAMES: local.legacy2 ? "urlprefix-${HOST}" : "https://${HOST}"] - - canary_tags = [for HOST in var.HOSTNAMES: "https://canary-${HOST}"] - - port = "http" - check { - name = "alive" - type = "${var.CHECK_PROTOCOL}" - path = "${var.CHECK_PATH}" - port = "http" - interval = "10s" - timeout = "${var.CHECK_TIMEOUT}" - check_restart { - limit = 3 # auto-restart task when healthcheck fails 3x in a row - - # give container (eg: having issues) custom time amount to stay up for debugging before - # 1st health check (eg: "3600s" value would be 1hr) - grace = "${var.HEALTH_TIMEOUT}" - } - } - } - - dynamic "service" { - for_each = merge(local.ports_extra_https, local.ports_extra_http, local.ports_extra_tcp) - content { - # service.key == portnumber - # service.value == portname - name = "${var.SLUG}--${service.value}" - task = var.MULTI_CONTAINER ? service.value : "http" - # NOTE: Empty tags list if MULTI_CONTAINER (private internal ports like DB) - tags = var.MULTI_CONTAINER ? [] : local.tags[service.value] - - port = "${service.value}" - check { - name = "alive" - type = "tcp" - path = "${var.CHECK_PATH}" - port = "${service.value}" - interval = "10s" - timeout = "${var.CHECK_TIMEOUT}" - } - check_restart { - grace = "${var.HEALTH_TIMEOUT}" - } - } - } - - task "http" { - driver = "docker" - - # UGH - have to copy/paste this next block twice -- first for no docker login needed; - # second for docker login needed (job spec will assemble in just one). - # This is because we can't put dynamic content *inside* the 'config { .. }' stanza. - dynamic "config" { - for_each = local.docker_no_login - content { - image = "${local.docker_image}" - image_pull_timeout = "20m" - network_mode = "${var.NETWORK_MODE}" - ports = local.ports_docker - volumes = local.volumes - force_pull = var.FORCE_PULL - memory_hard_limit = "${var.MEMORY * 10}" # NOTE: not podman driver compatible - } - } - dynamic "config" { - for_each = slice(local.docker_pass, 0, min(1, length(local.docker_pass))) - content { - image = "${local.docker_image}" - image_pull_timeout = "20m" - network_mode = "${var.NETWORK_MODE}" - ports = local.ports_docker - volumes = local.volumes - force_pull = var.FORCE_PULL - memory_hard_limit = "${var.MEMORY * 10}" # NOTE: not podman driver compatible - - auth { - # server_address = "${var.CI_REGISTRY}" - username = local.docker_user - password = "${config.value}" - } - } - } - - resources { - # The MEMORY var now becomes a **soft limit** - # We will 10x that for a **hard limit** - cpu = "${var.CPU}" - memory = "${var.MEMORY}" - memory_max = "${var.MEMORY * 10}" - } - - - dynamic "template" { - # Secrets get stored in consul kv store, with the key [SLUG], when your project has set a - # CI/CD variable like NOMAD_SECRET_[SOMETHING]. - # Setup the nomad job to dynamically pull secrets just before the container starts - - # and insert them into the running container as environment variables. - for_each = slice(keys(var.NOMAD_SECRETS), 0, min(1, length(keys(var.NOMAD_SECRETS)))) - content { - change_mode = "noop" - destination = "secrets/kv.env" - env = true - data = "{{ key \"${var.SLUG}\" }}" - } - } - - template { - # Pass in useful hostname(s), repo & branch info to container's runtime as env vars - change_mode = "noop" - destination = "secrets/ci.env" - env = true - data = <&1 | tee $LOG - set -e - while [ $# -gt 0 ]; do - EXPECT=$1 - shift - grep "$EXPECT" $LOG - done -} - -function tags() { - STR=$(jq -cr '[..|objects|.Tags//empty]' /tmp/project.json) - if [ "$STR" != "$1" ]; then - set +x - echo "services tags: $STR not expected: $1" - exit 1 - fi -} - -function ctags() { - STR=$(jq -cr '[..|objects|.CanaryTags//empty]' /tmp/project.json) - if [ "$STR" != "$1" ]; then - set +x - echo "services canary tags: $STR not expected: $1" - exit 1 - fi -} - -function slug() { - STR=$(jq -cr '.Job.ID' /tmp/project.json) - if [ "$STR" != "$1" ]; then - set +x - echo "slug/job name: $STR not expected: $1" - exit 1 - fi -} - -function prodtest() { - CI_PROJECT_NAME=$(echo "$CI_PROJECT_PATH_SLUG" |cut -f2- -d-) - BASE_DOMAIN=${BASE_DOMAIN:-"prod.archive.org"} # default to prod.archive.org unless caller set it - NOMAD_TOKEN_PROD=test - expects "deploying to https://$CI_HOSTNAME" -} - -# test various deploy scenarios (verify expected hostname and cluster get used) -# NOTE: the CI_ * vars are normally auto-poplated by CI/CD GL (gitlab) yaml setup -# NOTE: the GITHUB_* vars are normally auto-poplated in CI/CD GH Actions by GH (github) -( - banner GL to dev - BASE_DOMAIN=dev.archive.org - CI_PROJECT_NAME=av - CI_COMMIT_REF_SLUG=main - CI_PROJECT_PATH_SLUG=www-$CI_PROJECT_NAME - expects 'nomad cluster https://dev.archive.org' \ - 'deploying to https://www-av.dev.archive.org' - tags '[["https://www-av.dev.archive.org"]]' - ctags '[["https://canary-www-av.dev.archive.org"]]' - slug www-av -) -( - banner GL to dev, custom hostname - BASE_DOMAIN=dev.archive.org - CI_PROJECT_NAME=av - CI_COMMIT_REF_SLUG=main - CI_PROJECT_PATH_SLUG=www-$CI_PROJECT_NAME - NOMAD_VAR_HOSTNAMES='["av"]' - expects 'nomad cluster https://dev.archive.org' \ - 'deploying to https://av.dev.archive.org' - tags '[["https://av.dev.archive.org"]]' - ctags '[["https://canary-av.dev.archive.org"]]' - slug www-av -) -( - echo GL to prod, via alt/unusual branch name, custom hostname - BASE_DOMAIN=prod.archive.org - CI_PROJECT_NAME=av - CI_COMMIT_REF_SLUG=avinfo - CI_PROJECT_PATH_SLUG=www-$CI_PROJECT_NAME - NOMAD_VAR_HOSTNAMES='["avinfo"]' - NOMAD_TOKEN_PROD=test - expects 'nomad cluster https://prod.archive.org' \ - 'deploying to https://avinfo.prod.archive.org' \ - 'using nomad production token' - tags '[["urlprefix-avinfo.prod.archive.org"]]' - ctags '[["https://canary-avinfo.prod.archive.org"]]' - slug www-av-avinfo -) -( - echo GL to prod, via alt/unusual branch name, custom hostname - BASE_DOMAIN=prod.archive.org - CI_PROJECT_NAME=plausible - CI_COMMIT_REF_SLUG=plausible-ait - CI_PROJECT_PATH_SLUG=services-$CI_PROJECT_NAME - NOMAD_VAR_HOSTNAMES='["plausible-ait"]' - NOMAD_TOKEN_PROD=test - expects 'nomad cluster https://prod.archive.org' \ - 'deploying to https://plausible-ait.prod.archive.org' \ - 'using nomad production token' -) -( - echo GL to dev, branch, so custom hostname ignored - banner GL to dev, w/ 2+ custom hostnames - BASE_DOMAIN=dev.archive.org - CI_PROJECT_NAME=av - CI_COMMIT_REF_SLUG=main - CI_PROJECT_PATH_SLUG=www-$CI_PROJECT_NAME - NOMAD_VAR_HOSTNAMES='["av1", "av2.dweb.me", "poohbot.com"]' - expects 'nomad cluster https://dev.archive.org' \ - 'deploying to https://av1.dev.archive.org' - # NOTE: subtle -- with multiple names to single port deploy, we expect a list of 3 hostnames - # applying to *one* service - tags '[["https://av1.dev.archive.org","https://av2.dweb.me","https://poohbot.com"]]' - ctags '[["https://canary-av1.dev.archive.org","https://canary-av2.dweb.me","https://canary-poohbot.com"]]' -) -( - banner GL to dev, branch, so custom hostname ignored - BASE_DOMAIN=dev.archive.org - CI_PROJECT_NAME=av - CI_COMMIT_REF_SLUG=tofu - CI_PROJECT_PATH_SLUG=www-$CI_PROJECT_NAME - NOMAD_VAR_HOSTNAMES='["av"]' - expects 'nomad cluster https://dev.archive.org' \ - 'deploying to https://www-av-tofu.dev.archive.org' - slug www-av-tofu -) -( - banner GL to prod - BASE_DOMAIN=dev.archive.org - CI_PROJECT_NAME=plausible - CI_COMMIT_REF_SLUG=production - CI_PROJECT_PATH_SLUG=services-$CI_PROJECT_NAME - NOMAD_TOKEN_PROD=test - expects 'nomad cluster https://prod.archive.org' \ - 'deploying to https://plausible.prod.archive.org' \ - 'using nomad production token' - tags '[["urlprefix-plausible.prod.archive.org"]]' - ctags '[["https://canary-plausible.prod.archive.org"]]' -) -( - banner GL to ext - BASE_DOMAIN=dev.archive.org - CI_PROJECT_NAME=av - CI_COMMIT_REF_SLUG=ext - CI_PROJECT_PATH_SLUG=www-$CI_PROJECT_NAME - NOMAD_TOKEN_EXT=test - expects 'nomad cluster https://ext.archive.org' \ - 'deploying to https://av.ext.archive.org' \ - 'using nomad ext token' - tags '[["https://av.ext.archive.org"]]' - ctags '[["https://canary-av.ext.archive.org"]]' -) -( - banner GL to prod, custom hostname - BASE_DOMAIN=dev.archive.org - CI_PROJECT_NAME=plausible - CI_COMMIT_REF_SLUG=production - CI_PROJECT_PATH_SLUG=services-$CI_PROJECT_NAME - NOMAD_VAR_HOSTNAMES='["plausible-ait.prod.archive.org"]' - NOMAD_TOKEN_PROD=test - expects 'nomad cluster https://prod.archive.org' \ - 'deploying to https://plausible-ait.prod.archive.org' \ - 'using nomad production token' -) -( - banner GH to dev - GITHUB_ACTIONS=1 - GITHUB_REPOSITORY=internetarchive/emularity-engine - GITHUB_REF_NAME=tofu - BASE_DOMAIN=dev.archive.org - expects 'nomad cluster https://dev.archive.org' \ - 'deploying to https://internetarchive-emularity-engine-tofu.dev.archive.org' -) -( - banner GH to staging - GITHUB_ACTIONS=1 - GITHUB_REPOSITORY=internetarchive/emularity-engine - GITHUB_REF_NAME=staging - BASE_DOMAIN=dev.archive.org - NOMAD_TOKEN_PROD=test - expects 'nomad cluster https://staging.archive.org' \ - 'deploying to https://emularity-engine.staging.archive.org' -) -( - banner GH to production - GITHUB_ACTIONS=1 - GITHUB_REPOSITORY=internetarchive/emularity-engine - GITHUB_REF_NAME=production - BASE_DOMAIN=dev.archive.org - NOMAD_TOKEN_PROD=test - expects 'nomad cluster https://ux-b.archive.org' \ - 'deploying to https://emularity-engine.ux-b.archive.org' \ - 'using nomad production token' -) -( - banner "GL repo using 'main' branch to be like 'production'" - BASE_DOMAIN=prod.archive.org - CI_PROJECT_NAME=offshoot - CI_COMMIT_REF_SLUG=main - CI_PROJECT_PATH_SLUG=www-$CI_PROJECT_NAME - NOMAD_TOKEN_PROD=test - NOMAD_VAR_HOSTNAMES='["offshoot"]' - expects 'nomad cluster https://prod.archive.org' \ - 'deploying to https://offshoot.prod.archive.org' - slug www-offshoot -) -( - banner GL repo using one HTTP-only port and 2+ ports/names, to dev - BASE_DOMAIN=dev.archive.org - CI_PROJECT_NAME=lcp - CI_COMMIT_REF_SLUG=main - CI_PROJECT_PATH_SLUG=services-$CI_PROJECT_NAME - NOMAD_VAR_PORTS='{ 9999 = "http" , 18989 = "lcp", 8990 = "lsd" }' - expects 'nomad cluster https://dev.archive.org' \ - 'deploying to https://services-lcp.dev.archive.org' - # NOTE: subtle -- with multiple ports (one thus one service per port), we expect 3 services - # eacho with its own hostname - tags '[["https://services-lcp.dev.archive.org"],["http://services-lcp-lcp.dev.archive.org"],["https://services-lcp-lsd.dev.archive.org"]]' - ctags '[["https://canary-services-lcp.dev.archive.org"]]' -) -( - banner GL repo using one HTTP-only port and 2+ ports/names, to prod - BASE_DOMAIN=dev.archive.org - CI_PROJECT_NAME=lcp - CI_COMMIT_REF_SLUG=production - CI_PROJECT_PATH_SLUG=services-$CI_PROJECT_NAME - NOMAD_VAR_PORTS='{ 9999 = "http" , 18989 = "lcp", 8990 = "lsd" }' - NOMAD_TOKEN_PROD=test - expects 'nomad cluster https://prod.archive.org' \ - 'deploying to https://lcp.prod.archive.org' \ - 'using nomad production token' - # NOTE: subtle -- with multiple ports (one thus one service per port), we expect 3 services - # eacho with its own hostname - tags '[["urlprefix-lcp.prod.archive.org"],["urlprefix-lcp-lcp.prod.archive.org proto=http"],["urlprefix-lcp-lsd.prod.archive.org"]]' - ctags '[["https://canary-lcp.prod.archive.org"]]' -) -( - banner GL repo using one TCP-only port and 2+ ports/names - BASE_DOMAIN=dev.archive.org - CI_PROJECT_NAME=scribe-c2 - CI_COMMIT_REF_SLUG=main - CI_PROJECT_PATH_SLUG=services-$CI_PROJECT_NAME - NOMAD_VAR_PORTS='{ 9999 = "http" , -7777 = "tcp", 8889 = "reg" }' - expects 'nomad cluster https://dev.archive.org' \ - 'deploying to https://services-scribe-c2.dev.archive.org' - # NOTE: subtle -- with multiple ports (one thus one service per port), we'd normally expect 3 services - # eacho with its own hostname -- but one is TCP so the middle Service gets an *empty* list of tags. - tags '[["https://services-scribe-c2.dev.archive.org"],[],["https://services-scribe-c2-reg.dev.archive.org"]]' - ctags '[["https://canary-services-scribe-c2.dev.archive.org"]]' -) - - -# a bunch of quick, simple production deploy tests validating hostnames -( - CI_PROJECT_PATH_SLUG=services-article-exchange - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=article-exchange.prod.archive.org - prodtest -) -( - CI_PROJECT_PATH_SLUG=services-atlas - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=atlas.prod.archive.org - prodtest -) -( - CI_PROJECT_PATH_SLUG=services-bwhogs - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=bwhogs.prod.archive.org - prodtest -) -( - CI_PROJECT_PATH_SLUG=services-ids-logic - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=ids-logic.prod.archive.org - prodtest -) -( - CI_PROJECT_PATH_SLUG=services-lcp - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=lcp.prod.archive.org - prodtest -) -( - CI_PROJECT_PATH_SLUG=services-microfilmmonitor - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=microfilmmonitor.prod.archive.org - prodtest -) -( - CI_PROJECT_PATH_SLUG=services-oclc-ill - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=oclc-ill.prod.archive.org - prodtest -) -( - CI_PROJECT_PATH_SLUG=services-odyssey - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=odyssey.prod.archive.org - prodtest -) -( - CI_PROJECT_PATH_SLUG=services-opds - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=opds.prod.archive.org - prodtest -) -( - CI_PROJECT_PATH_SLUG=services-plausible - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=plausible.prod.archive.org - prodtest -) -( - CI_PROJECT_PATH_SLUG=services-rapid-slackbot - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=rapid-slackbot.prod.archive.org - prodtest -) -( - CI_PROJECT_PATH_SLUG=services-scribe-serial-helper - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=scribe-serial-helper.prod.archive.org - prodtest -) -( - CI_PROJECT_PATH_SLUG=www-av - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=av.prod.archive.org - prodtest -) -( - CI_PROJECT_PATH_SLUG=www-bookserver - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=bookserver.prod.archive.org - prodtest -) -( - CI_PROJECT_PATH_SLUG=www-iiif - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=iiif.prod.archive.org - prodtest -) -( - CI_PROJECT_PATH_SLUG=www-nginx - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=nginx.prod.archive.org - prodtest -) -( - CI_PROJECT_PATH_SLUG=www-rendertron - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=rendertron.prod.archive.org - prodtest -) - - -# a bunch of quick, _custom HOSTNAMES_, production deploy tests validating hostnames -( - NOMAD_VAR_HOSTNAMES='["popcorn.archive.org"]' - CI_PROJECT_PATH_SLUG=www-popcorn - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=popcorn.archive.org - prodtest -) -( - NOMAD_VAR_HOSTNAMES='["polyfill.archive.org"]' - CI_PROJECT_PATH_SLUG=www-polyfill-io-production - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=polyfill.archive.org - prodtest -) -( - NOMAD_VAR_HOSTNAMES='["purl.archive.org"]' - CI_PROJECT_PATH_SLUG=www-purl - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=purl.archive.org - prodtest -) -( - NOMAD_VAR_HOSTNAMES='["esm.archive.org"]' - CI_PROJECT_PATH_SLUG=www-esm - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=esm.archive.org - prodtest -) -( - NOMAD_VAR_HOSTNAMES='["cantaloupe.prod.archive.org"]' - CI_PROJECT_PATH_SLUG=services-ia-iiif-cantaloupe-experiment - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=cantaloupe.prod.archive.org - prodtest -) -( - NOMAD_VAR_HOSTNAMES='["plausible-ait.prod.archive.org"]' - CI_PROJECT_PATH_SLUG=services-plausible - CI_COMMIT_REF_SLUG=production-ait - CI_HOSTNAME=plausible-ait.prod.archive.org - prodtest -) -( - NOMAD_VAR_HOSTNAMES='["parse_dates"]' - CI_PROJECT_PATH_SLUG=services-parse-dates - CI_COMMIT_REF_SLUG=production - CI_HOSTNAME=parse_dates.prod.archive.org - prodtest -) - -banner SUCCESS diff --git a/vsync b/vsync deleted file mode 100755 index cafd07c..0000000 --- a/vsync +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/zsh -e - -# Shell script version of `nom-cp` alias -# Typically used with `sync-rsync` extension to VSCode. - -mydir=${0:a:h} - -source $mydir/aliases - -nom-cp "$@" From 85e4cd6df8174370c413574cb2f7c487269c2436 Mon Sep 17 00:00:00 2001 From: Tracey Jaquith Date: Mon, 30 Dec 2024 11:54:48 -0500 Subject: [PATCH 02/10] basic info --- README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..f87c51b --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +deploy that responds with gitlab CI/CD pipeline instructions From 2dfc2f553d8eb802aaa7703ea2cd6bd730bc6705 Mon Sep 17 00:00:00 2001 From: Tracey Jaquith Date: Mon, 30 Dec 2024 12:06:17 -0500 Subject: [PATCH 03/10] start out --- .gitlab-ci.yml | 113 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..c2ace58 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,113 @@ +# NOTE: keep in mind this file is _included_ by _other_ repos, and thus the env var names +# are not _always_ related to _this_ repo ;-) + +# A GitLab group (ideally) or project that wants to deploy to a nomad cluster, +# will need to set [Settings] [CI/CD] [Variables] +# NOMAD_ADDR +# NOMAD_TOKEN +# to whatever your Nomad cluster was setup to. + + +# NOTE: very first pipeline, the [build] below will make sure this is created +image: registry.gitlab.com/internetarchive/nomad/master + +stages: + - build + - test + - deploy + - cleanup + +build: + # Tracey 3/2024: + # This was adapted & simplified from: + # https://gitlab.com/gitlab-org/gitlab/-/raw/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml + stage: build + # If need to rebuild this image while runners are down, `cd` to this directory, then, as root: + # podman login registry.gitlab.com + # podman build --net=host --tag registry.gitlab.com/internetarchive/nomad/master . && sudo podman push registry.gitlab.com/internetarchive/nomad/master + image: registry.gitlab.com/internetarchive/nomad/master + variables: + DOCKER_HOST: 'unix:///run/podman/podman.sock' + DOCKER_TLS_CERTDIR: '' + DOCKER_BUILDKIT: 1 + script: + - /build.sh + artifacts: + reports: + dotenv: gl-auto-build-variables.env + rules: + - if: '$BUILD_DISABLED' + when: never + - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' + +test-ourself: + stage: test + image: ${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}:${CI_COMMIT_SHA} + script: + - env -i zsh -euax test/test.sh + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + when: never + - if: '$CI_PROJECT_PATH_SLUG == "internetarchive-nomad"' + +deploy: + stage: deploy + script: + # https://gitlab.com/internetarchive/nomad/-/blob/master/deploy.sh + - /deploy.sh + environment: + name: $CI_COMMIT_REF_SLUG + url: https://$HOSTNAME + on_stop: stop_review + rules: + - if: '$NOMAD_VAR_NO_DEPLOY' + when: never + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + when: never + - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' + +deploy-serverless: + stage: deploy + script: + - | + if [[ -n "$CI_REGISTRY" && -n "$CI_REGISTRY_USER" ]]; then + echo "Logging in to GitLab Container Registry with CI credentials..." + + # this filters stderr of `podman login`, w/o merging stdout & stderr together + set +x + { echo "$CI_REGISTRY_PASSWORD" | podman --remote login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" 2>&1 1>&3 | ( grep -E -v "^WARNING! Your password will be stored unencrypted in |^Configure a credential helper to remove this warning. See|^https://docs.docker.com/engine/reference/commandline/login/#credentials-store" || true ) 1>&2; } 3>&1 + fi + + set -x + image_tagged="$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA" + image_latest="$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:latest" + podman --remote tag $image_tagged $image_latest + podman --remote push $image_latest + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + when: never + - if: '$CI_COMMIT_BRANCH && $NOMAD_VAR_SERVERLESS' + + +stop_review: + # See: + # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml + stage: cleanup + variables: + GIT_STRATEGY: none + script: + - /deploy.sh stop + environment: + name: $CI_COMMIT_REF_SLUG + action: stop + dependencies: [] + allow_failure: true + rules: + - if: '$CI_COMMIT_BRANCH == "main"' + when: never + - if: '$CI_COMMIT_BRANCH == "master"' + when: never + - if: '$NOMAD_VAR_NO_DEPLOY' + when: never + - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' + when: manual From 622cc947590f66612263c1796b3529bc2a3421cc Mon Sep 17 00:00:00 2001 From: Tracey Jaquith Date: Mon, 30 Dec 2024 12:11:33 -0500 Subject: [PATCH 04/10] comments, re-indents, etc (nothing substantitive) --- .dockerignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index c1c9f4d..f4b1198 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,2 @@ -.git* +.git +.github From 5b8549430f6014a72f63e80d0ae9f24710409842 Mon Sep 17 00:00:00 2001 From: Tracey Jaquith Date: Mon, 30 Dec 2024 12:15:50 -0500 Subject: [PATCH 05/10] [initial create] --- ci.yml | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 ci.yml diff --git a/ci.yml b/ci.yml new file mode 100644 index 0000000..c2ace58 --- /dev/null +++ b/ci.yml @@ -0,0 +1,113 @@ +# NOTE: keep in mind this file is _included_ by _other_ repos, and thus the env var names +# are not _always_ related to _this_ repo ;-) + +# A GitLab group (ideally) or project that wants to deploy to a nomad cluster, +# will need to set [Settings] [CI/CD] [Variables] +# NOMAD_ADDR +# NOMAD_TOKEN +# to whatever your Nomad cluster was setup to. + + +# NOTE: very first pipeline, the [build] below will make sure this is created +image: registry.gitlab.com/internetarchive/nomad/master + +stages: + - build + - test + - deploy + - cleanup + +build: + # Tracey 3/2024: + # This was adapted & simplified from: + # https://gitlab.com/gitlab-org/gitlab/-/raw/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml + stage: build + # If need to rebuild this image while runners are down, `cd` to this directory, then, as root: + # podman login registry.gitlab.com + # podman build --net=host --tag registry.gitlab.com/internetarchive/nomad/master . && sudo podman push registry.gitlab.com/internetarchive/nomad/master + image: registry.gitlab.com/internetarchive/nomad/master + variables: + DOCKER_HOST: 'unix:///run/podman/podman.sock' + DOCKER_TLS_CERTDIR: '' + DOCKER_BUILDKIT: 1 + script: + - /build.sh + artifacts: + reports: + dotenv: gl-auto-build-variables.env + rules: + - if: '$BUILD_DISABLED' + when: never + - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' + +test-ourself: + stage: test + image: ${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}:${CI_COMMIT_SHA} + script: + - env -i zsh -euax test/test.sh + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + when: never + - if: '$CI_PROJECT_PATH_SLUG == "internetarchive-nomad"' + +deploy: + stage: deploy + script: + # https://gitlab.com/internetarchive/nomad/-/blob/master/deploy.sh + - /deploy.sh + environment: + name: $CI_COMMIT_REF_SLUG + url: https://$HOSTNAME + on_stop: stop_review + rules: + - if: '$NOMAD_VAR_NO_DEPLOY' + when: never + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + when: never + - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' + +deploy-serverless: + stage: deploy + script: + - | + if [[ -n "$CI_REGISTRY" && -n "$CI_REGISTRY_USER" ]]; then + echo "Logging in to GitLab Container Registry with CI credentials..." + + # this filters stderr of `podman login`, w/o merging stdout & stderr together + set +x + { echo "$CI_REGISTRY_PASSWORD" | podman --remote login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" 2>&1 1>&3 | ( grep -E -v "^WARNING! Your password will be stored unencrypted in |^Configure a credential helper to remove this warning. See|^https://docs.docker.com/engine/reference/commandline/login/#credentials-store" || true ) 1>&2; } 3>&1 + fi + + set -x + image_tagged="$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA" + image_latest="$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:latest" + podman --remote tag $image_tagged $image_latest + podman --remote push $image_latest + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + when: never + - if: '$CI_COMMIT_BRANCH && $NOMAD_VAR_SERVERLESS' + + +stop_review: + # See: + # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml + stage: cleanup + variables: + GIT_STRATEGY: none + script: + - /deploy.sh stop + environment: + name: $CI_COMMIT_REF_SLUG + action: stop + dependencies: [] + allow_failure: true + rules: + - if: '$CI_COMMIT_BRANCH == "main"' + when: never + - if: '$CI_COMMIT_BRANCH == "master"' + when: never + - if: '$NOMAD_VAR_NO_DEPLOY' + when: never + - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' + when: manual From 735d2d7511cbf8f5de60137ef8964ce0093e0df3 Mon Sep 17 00:00:00 2001 From: Tracey Jaquith Date: Mon, 30 Dec 2024 12:20:38 -0500 Subject: [PATCH 06/10] tunneling --- .github/workflows/cicd.yml | 1 + Caddyfile | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index fc2687a..bddc5dd 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -6,5 +6,6 @@ jobs: with: NOMAD_VAR_HOSTNAMES: '["nomad","nomad.archive.org"]' NOMAD_VAR_MEMORY: 100 # xxx + NOMAD_VAR_CHECK_PROTOCOL: 'tcp' secrets: NOMAD_TOKEN_EXT: ${{ secrets.NOMAD_TOKEN_EXT }} diff --git a/Caddyfile b/Caddyfile index 4e629fb..641c714 100644 --- a/Caddyfile +++ b/Caddyfile @@ -3,9 +3,9 @@ } # We answer all requests with the contents of this file: -# https://raw.githubusercontent.com/internetarchive/nomad/refs/heads/master/.gitlab-ci.yml +# https://internetarchive.github.io/nomad/ci.yml :5000 { - rewrite * /internetarchive/nomad/refs/heads/master/.gitlab-ci.yml - reverse_proxy https://raw.githubusercontent.com + rewrite * /nomad/ci.yml + reverse_proxy https://internetarchive.github.io } From 6ea2416b8a35171a46b88a0336cdf9db69a980f1 Mon Sep 17 00:00:00 2001 From: Tracey Jaquith Date: Mon, 30 Dec 2024 12:29:00 -0500 Subject: [PATCH 07/10] tunnel fix --- Caddyfile | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Caddyfile b/Caddyfile index 641c714..b018cbf 100644 --- a/Caddyfile +++ b/Caddyfile @@ -2,10 +2,14 @@ admin off } -# We answer all requests with the contents of this file: -# https://internetarchive.github.io/nomad/ci.yml - :5000 { - rewrite * /nomad/ci.yml - reverse_proxy https://internetarchive.github.io + # We answer all requests with the contents of this file: + # https://internetarchive.github.io/nomad/ci.yml + rewrite * /nomad/ci.yml + + reverse_proxy { + to https://internetarchive.github.io + # like CLI `--change-host-header`: + header_up Host {upstream_hostport} + } } From bb658f567f591101f699742289b14c9000d5c365 Mon Sep 17 00:00:00 2001 From: Tracey Jaquith Date: Mon, 30 Dec 2024 13:08:01 -0500 Subject: [PATCH 08/10] Simpler setup for CI/CD instructions server. Revert "start of CI/CD yml tunnel" This reverts commit 18ca9072a8230d9be0f4ade9f6a83c48b8c27b93. --- Caddyfile | 14 +- Dockerfile | 23 +- README.md | 546 +++++++++++++++++++++++++++++++++++- aliases | 312 +++++++++++++++++++++ build.sh | 94 +++++++ build.yml | 23 ++ deploy.sh | 346 +++++++++++++++++++++++ hello-world.hcl | 57 ++++ img/architecture.drawio.svg | 452 +++++++++++++++++++++++++++++ img/overview.drawio.svg | 180 ++++++++++++ img/overview2.drawio.svg | 299 ++++++++++++++++++++ img/prod.jpg | Bin 0 -> 27724 bytes img/protect.jpg | Bin 0 -> 27738 bytes img/secrets.jpg | Bin 0 -> 47183 bytes logo.jpg | Bin 0 -> 2285 bytes project.nomad | 459 ++++++++++++++++++++++++++++++ test/test.sh | 434 ++++++++++++++++++++++++++++ vsync | 10 + 18 files changed, 3236 insertions(+), 13 deletions(-) create mode 100644 aliases create mode 100755 build.sh create mode 100644 build.yml create mode 100755 deploy.sh create mode 100644 hello-world.hcl create mode 100644 img/architecture.drawio.svg create mode 100644 img/overview.drawio.svg create mode 100644 img/overview2.drawio.svg create mode 100644 img/prod.jpg create mode 100644 img/protect.jpg create mode 100644 img/secrets.jpg create mode 100644 logo.jpg create mode 100644 project.nomad create mode 100755 test/test.sh create mode 100755 vsync diff --git a/Caddyfile b/Caddyfile index b018cbf..e78d078 100644 --- a/Caddyfile +++ b/Caddyfile @@ -2,14 +2,8 @@ admin off } -:5000 { - # We answer all requests with the contents of this file: - # https://internetarchive.github.io/nomad/ci.yml - rewrite * /nomad/ci.yml - - reverse_proxy { - to https://internetarchive.github.io - # like CLI `--change-host-header`: - header_up Host {upstream_hostport} - } +:8888 { + # We answer all requests this CI/CD yaml file from this repo + file_server + rewrite * /.gitlab-ci.yml } diff --git a/Dockerfile b/Dockerfile index bbc7f76..6f19747 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,22 @@ -FROM caddy:alpine +FROM denoland/deno:alpine -COPY Caddyfile /etc/caddy/ +# add `nomad` +RUN mkdir -m777 /usr/local/sbin && \ + cd /usr/local/sbin && \ + wget -qO nomad.zip https://releases.hashicorp.com/nomad/1.7.6/nomad_1.7.6_linux_amd64.zip && \ + unzip nomad.zip && \ + rm nomad.zip && \ + chmod 777 nomad && \ + # podman for build.sh + apk add bash zsh jq podman caddy && \ + # using podman not docker + ln -s /usr/bin/podman /usr/bin/docker + +WORKDIR /app +COPY .gitlab-ci.yml Caddyfile . + +COPY build.sh deploy.sh / + +USER deno + +CMD ["/usr/bin/caddy", "run"] diff --git a/README.md b/README.md index f87c51b..1cb9f70 100644 --- a/README.md +++ b/README.md @@ -1 +1,545 @@ -deploy that responds with gitlab CI/CD pipeline instructions +Code, setup, and information to: +- setup automatic deployment to Nomad clusters from GitLab's standard CI/CD pipelines +- interact with, monitor, and customize deployments + + +[[_TOC_]] + + +# Overview +Deployment leverages a simple `.gitlab-ci.yml` using GitLab runners & CI/CD ([build] and [test]); +then switches to custom [deploy] phase to deploy docker containers into `nomad`. + +This also contains demo "hi world" webapp. + + +Uses: +- [nomad](https://www.nomadproject.io) **deployment** (management, scheduling) +- [consul](https://www.consul.io) **networking** (service discovery, healthchecking, secrets storage) +- [caddy](https://caddyserver.com/) **routing** (load balancing, automatic https) + +![Architecture](img/overview2.drawio.svg) + + +## Want to deploy to nomad? 🚀 +- verify project's [Settings] [CI/CD] [Variables] has either Group or Project level settings for: + - `NOMAD_TOKEN` `MY-TOKEN` + - `NOMAD_ADDR` `https://MY-HOSTNAME` or `BASE_DOMAIN` `example.com` + - (archive.org admins will often have set this already for you at the group-level) +- simply make your project have this simple `.gitlab-ci.yml` in top-level dir: +```yaml +include: + - remote: 'https://gitlab.com/internetarchive/nomad/-/raw/master/.gitlab-ci.yml' +``` +- if you want a [test] phase, you can add this to the `.gitlab-ci.yml` file above: +```yaml +test: + stage: test + image: ${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}:${CI_COMMIT_SHA} + script: + - cd /app # or wherever in your image + - npm test # or whatever your test scripts/steps are +``` +- [optional] you can _instead_ copy [the included file](.gitlab-ci.yml) and customize/extend it. +- [optional] you can copy this [project.nomad](project.nomad) file into your repo top level and customize/extend it if desired +- _... but there's a good chance you won't need to_ 😎 + +_**Note:** For urls like https://archive.org/services/project -- watch out for routes defined in your app with trailing slashes – they may redirect to project.dev.archive.org. More information [here](https://git.archive.org/services/pyhi/-/blob/main/README.md#notes)._ + +### Customizing +There are various options that can be used in conjunction with the `project.nomad` and `.gitlab-ci.yml` files, keys: +```text +NOMAD_VAR_CHECK_PATH +NOMAD_VAR_CHECK_PROTOCOL +NOMAD_VAR_CHECK_TIMEOUT +NOMAD_VAR_CONSUL_PATH +NOMAD_VAR_COUNT +NOMAD_VAR_COUNT_CANARIES +NOMAD_VAR_CPU +NOMAD_VAR_FORCE_PULL +NOMAD_VAR_HEALTH_TIMEOUT +NOMAD_VAR_HOSTNAMES +NOMAD_VAR_IS_BATCH +NOMAD_VAR_MEMORY +NOMAD_VAR_MULTI_CONTAINER +NOMAD_VAR_NAMESPACE +NOMAD_VAR_NETWORK_MODE +NOMAD_VAR_NO_DEPLOY +NOMAD_VAR_PERSISTENT_VOLUME +NOMAD_VAR_PORTS +NOMAD_VAR_SERVERLESS +NOMAD_VAR_VOLUMES +``` +- See the top of [project.nomad](project.nomad) +- Our customizations always prefix with `NOMAD_VAR_`. +- You can simply insert them, with values, in your project's `.gitlab-ci.yml` file before including _our_ `.gitlab-ci.yml` like above. +- Examples 👇 +#### Don't actually deploy containers to nomad +Perhaps your project just wants to leverage the CI (Continuous Integration) for [buil] and/or [test] steps - but not CD (Continuous Deployment). An example might be a back-end container that runs elsewhere and doesn't have web listener. +```yaml +variables: + NOMAD_VAR_NO_DEPLOY: 'true' +``` + +#### Custom default RAM expectations from (default) 300 MB to 1 GB +This value is the _expected_ value for your container's average running needs/usage, helpful for `nomad` scheduling purposes. It is a "soft limit" and we use *ten times* this amount to be the amount used for a "hard limit". If your allocated container exceeds the hard limit, the container may be restarted by `nomad` if there is memory pressure on the Virtual Machine the container is running on. +```yaml +variables: + NOMAD_VAR_MEMORY: 1000 +``` +#### Custom default CPU expectations from (default) 100 MHz to 1 GHz +This value is the _expected_ value for your container's average running needs/usage, helpful for `nomad` scheduling purposes. It is a "soft limit". If your allocated container exceeds your specified limit, the container _may_ be restarted by `nomad` if there is CPU pressure on the Virtual Machine the container is running on. (So far, CPU-based restarts seem very rare in practice, since most VMs tend to "fill" up from aggregate container RAM requirements first 😊) +```yaml +variables: + NOMAD_VAR_CPU: 1000 +``` +#### Custom healthcheck, change from (default) HTTP to TCP: +This can be useful if your webapp serves using websockets, doesnt respond to http, or typically takes too long (or can't) respond with a `200 OK` status. (Think of it like switching to just a `ping` on your main port your webapp listens on). +```yaml +variables: + NOMAD_VAR_CHECK_PROTOCOL: 'tcp' +``` +#### Custom healthcheck, change path from (default) `/` to `/healthcheck`: +```yaml +variables: + NOMAD_VAR_CHECK_PATH: '/healthcheck' +``` +#### Custom healthcheck run time, change from (default) `2s` (2 seconds) to `1m` (one minute) +If your healthcheck may take awhile to run & succeed, you can increase the amount of time the `consul` healthcheck allows your HTTP request to run. +```yaml +variables: + NOMAD_VAR_CHECK_TIMEOUT: '1m' +``` +#### Custom time to start healthchecking after container re/start from (default) `20s` (20 second) to `3m` (3 minutes) +If your container takes awhile, after startup, to settle before healthchecking can work reliably, you can extend the wait time for the first healthcheck to run. +```yaml +variables: + NOMAD_VAR_HEALTH_TIMEOUT: '3m' +``` +#### Custom running container count from (default) 1 to 3 +You can run more than one container for increased reliability, more request processing, and more reliable uptimes (in the event of one or more Virtual Machines hosting containers having issues). + +For archive.org users, we suggest instead to switch your production deploy to our alternate production cluster. + +Keep in mind, you will have 2+ containers running simultaneously (_usually_, but not always, on different VMs). So if your webapp uses any shared resources, like backends not in containers, or "persistent volumes", that you will need to think about concurrency, potentially multiple writers, etc. 😊 +```yaml +variables: + NOMAD_VAR_COUNT: 3 +``` +#### Custom make NFS `/home/` available in running containers, readonly +Allow your containers to see NFS `/home/` home directories, readonly. +```yaml +variables: + NOMAD_VAR_VOLUMES: '["/home:/home:ro"]' +``` +#### Custom make NFS `/home/` available in running containers, read/write +Allow your containers to see NFS `/home/` home directories, readable and writable. Please be highly aware of operational security in your container when using this (eg: switch your `USER` in your `Dockerfile` to another non-`root` user; use "prepared statements" with any DataBase interactions; use [https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP](Content Security Policy) in all your pages to eliminate [https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting](XSS attacks, etc.) +```yaml +variables: + NOMAD_VAR_VOLUMES: '["/home:/home:rw"]' +``` +#### Custom hostname for your `main` branch deploy +Your deploy will get a nice semantic hostname by default, based upon "[slugged](https://en.wikipedia.org/wiki/Clean_URL#Slug)" formula like: https://[GITLAB_GROUP]-[GITLAB_PROJECT_OR_REPO_NAME]-[BRANCH_NAME]. However, you can override this if needed. This custom hostname will only pertain to a branch named `main` (or `master` [sic]) +```yaml +variables: + NOMAD_VAR_HOSTNAMES: '["www.example.com"]' +``` +#### Custom hostnameS for your `main` branch deploy +Similar to prior example, but you can have your main deployment respond to multiple hostnames if desired. +```yaml +variables: + NOMAD_VAR_HOSTNAMES: '["www.example.com", "store.example.com"]' +``` + +#### Multiple containers in same job spec +If you want to run multiple containers in the same job and group, set this to true. For example, you might want to run a Postgresql 3rd party container from bitnami, and have the main/http front-end container talk to it. Being in the same group will ensure all containers run on the same VM; which makes communication between them extremely easy. You simply need to inspect environment variables. + +You can see a minimal example of two containers with a "front end" talking to a "backend" here +https://gitlab.com/internetarchive/nomad-multiple-tasks + +See also a [postgres DB setup example](#postgres-db). +```yaml +variables: + NOMAD_VAR_MULTI_CONTAINER: 'true' +``` + +#### Force `docker pull` before container starts +If your deployment's job spec doesn't change between pipelines for some reason, you can set this to ensure `docker pull` always happens before your container starts up. A good example where you might see this is a periodic/batch/cron process that fires up a pipeline without any repository commit. Depending on your workflow and `Dockerfile` from there, if you see "stale" versions of containers, use this customization. +```yaml +variables: + NOMAD_VAR_FORCE_PULL: 'true' +``` + +#### Turn off [deploy canaries](https://learn.hashicorp.com/tutorials/nomad/job-blue-green-and-canary-deployments) +When a new deploy is happening, live traffic continues to old deploy about to be replaced, while a new deploy fires off in the background and `nomad` begins healthchecking. Only once it seems healthy, is traffic cutover to the new container and the old container removed. (If unhealthy, new container is removed). That can mean *two* deploys can run simultaneously. Depending on your setup and constraints, you might not want this and can disable canaries with this snippet below. (Keep in mind your deploy will temporarily 404 during re-deploy *without* using blue/green deploys w/ canaries). +```yaml +variables: + NOMAD_VAR_COUNT_CANARIES: 0 +``` + +#### Change your deploy to a cron-like batch/periodic +If you deployment is something you want to run periodically, instead of continuously, you can use this variable to switch to a nomad `type="batch"` +```yaml +variables: + NOMAD_VAR_IS_BATCH: 'true' +``` +Combine your `NOMAD_VAR_IS_BATCH` override, with a small `job.nomad` file in your repo to setup your cron behaviour. + +Example `job.nomad` file contents, to run the deploy every hour at 15m past the hour: +```ini +type = "batch" +periodic { + cron = "15 * * * * *" + prohibit_overlap = false # must be false cause of kv env vars task +} +``` + +#### Custom deploy networking +If your admin allows it, there might be some useful reasons to use VM host networking for your deploy. A good example is "relaying" UDP *broadcast* messages in/out of a container. Please see Tracey if interested, archive folks. :) +```yaml +variables: + NOMAD_VAR_NETWORK_MODE: 'host' +``` + +#### Custom namespacing +A job can be limited to a specific 'namespace' for purposes of ACL 'gating'. +In the example below, a cluster admin could create a custom `NOMAD_TOKEN` that only allows the +bearer to access jobs part of the namespace `team-titan`. +```yaml +variables: + NOMAD_VAR_NAMESPACE: 'team-titan' +``` + + + +#### More customizations +There are even more, less common, ways to customize your deploys. + +With other variables, like `NOMAD_VAR_PORTS`, you can use dynamic port allocation, setup daemons that use raw TCP, and more. + +Please see the top area of [project.nomad](project.nomad) for "Persistent Volumes" (think a "disk" that survives container restarts), additional open ports into your webapp, and more. + +See also [this section](#optional-add-ons-to-your-project) below. + +### Deploying to production nomad cluster (archive.org only) +Our production cluster has 3 VMs and will deploy your repo to a running container on each VM, using `haproxy` load balancer to balance requests. + +This should ensure much higher availability and handle more requests. + +Keep in mind if your deployment uses a "persistent volume" or talks to other backend services, they'll be getting traffic and access from multiple containers simultaneously. + +Setting up your repo to deploy to production is easy! + +- add a CI/CD Secret `NOMAD_TOKEN_PROD` with the nomad cluster value (ask tracey or robK) + - make it: protected, masked, hidden +![Production CI/CD Secret](img/prod.jpg) +- Make a new branch named `production` (presumably from your repo's latest `main` or `master` branch) + - It should now deploy your project to a different `NOMAD_ADDR` url + - Your default hostname domain will change from `.dev.archive.org` to `.prod.archive.org` +- [GitLab only] - [Protect the `production` branch](https://docs.gitlab.com/ee/user/project/protected_branches.html) + - suggest using same settings as your `main` or `master` (or default) branch +![Protect a branch](img/protect.jpg) + + +### Deploying to staging nomad cluster (archive.org only) +Our staging cluster will deploy your repo to a running container on one of its VMs. + +Setting up your repo to deploy to staging is easy! + +- add a CI/CD Secret `NOMAD_TOKEN_STAGING` with the nomad cluster value (ask tracey or robK) + - make it: protected, masked, hidden (similar to `production` section above) +- Make a new branch named `staging` (presumably from your repo's latest `main` or `master` branch) + - It should now deploy your project to a different `NOMAD_ADDR` url + - Your default hostname domain will change from `.dev.archive.org` to `.staging.archive.org` +- [GitLab only] - [Protect the `staging` branch](https://docs.gitlab.com/ee/user/project/protected_branches.html) + - suggest using same settings as your `main` or `master` (or default) branch, changing `production` to `staging` here: +![Protect a branch](img/protect.jpg) + + +### Deploying to ext nomad cluster (archive.org only) +Our "ext" cluster will deploy your repo to a running container on one of its VMs. + +Setting up your repo to deploy to ext is easy! + +- add a CI/CD Secret `NOMAD_TOKEN_EXT` with the nomad cluster value (ask tracey or robK) + - make it: protected, masked, hidden (similar to `production` section above) +- Make a new branch named `ext` (presumably from your repo's latest `main` or `master` branch) + - It should now deploy your project to a different `NOMAD_ADDR` url + - Your default hostname domain will change from `.dev.archive.org` to `.ext.archive.org` +- [GitLab only] - [Protect the `ext` branch](https://docs.gitlab.com/ee/user/project/protected_branches.html) + - suggest using same settings as your `main` or `master` (or default) branch, changing `production` to `ext` here: +![Protect a branch](img/protect.jpg) + + +## Laptop access +- create `$HOME/.config/nomad` and/or get it from an admin who setup your Nomad cluster + - @see top of [aliases](aliases) + - `brew install nomad` + - `source $HOME/.config/nomad` + - better yet: + - `git clone https://gitlab.com/internetarchive/nomad` + - adjust next line depending on where you checked out the above repo + - add this to your `$HOME/.bash_profile` or `$HOME/.zshrc` etc. + - `FI=$HOME/nomad/aliases && [ -e $FI ] && source $FI` + - then `nomad status` should work nicely + - @see [aliases](aliases) for lots of handy aliases.. +- you can then also use your browser to visit [$NOMAD_ADDR/ui/jobs](https://MY-HOSTNAME:4646/ui/jobs) + - and enter your `$NOMAD_TOKEN` in the ACL requirement + + +# Setup a Nomad Cluster +- we use HinD: https://github.com/internetarchive/hind + - you can customize the install with various environment variables + +Other alternatives: +- have DNS domain you can point to a VM? + - nomad/consul with $5/mo VM (or on-prem) + - [[1/2] Setup GitLab, Nomad, Consul & Fabio](https://tracey.archive.org/devops/2021-03-31) + - [[2/2] Add GitLab Runner & Setup full CI/CD pipelines](https://tracey.archive.org/devops/2021-04-07) +- have DNS domain and want on-prem GitLab? + - nomad/consul/gitlab/runners with $20/mo VM (or on-prem) + - [[1/2] Setup GitLab, Nomad, Consul & Fabio](https://tracey.archive.org/devops/2021-03-31) + - [[2/2] Add GitLab Runner & Setup full CI/CD pipelines](https://tracey.archive.org/devops/2021-04-07) +- no DNS - run on mac/linux laptop? + - [[1/3] setup GitLab & GitLab Runner on your Mac](https://tracey.archive.org/devops/2021-02-17) + - [[2/3] setup Nomad & Consul on your Mac](https://tracey.archive.org/devops/2021-02-24) + - [[3/3] connect: GitLab, GitLab Runner, Nomad & Consul](https://tracey.archive.org/devops/2021-03-10) + + +# Monitoring GUI urls (via ssh tunnelling above) +![Cluster Overview](https://tracey.archive.org/images/nomad-ui4.jpg) +- nomad really nice overview (see `Topology` link ☝) + - https://[NOMAD-HOST]:4646 (eg: `$NOMAD_ADDR`) + - then enter your `$NOMAD_TOKEN` +- @see [aliases](aliases) `nom-tunnel` + - http://localhost:8500 # consul + + +# Inspect, poke around +```bash +nomad node status +nomad node status -allocs +nomad server members + + +nomad job run example.nomad +nomad job status +nomad job status example + +nomad job deployments -t '{{(index . 0).ID}}' www-nomad +nomad job history -json www-nomad + +nomad alloc logs -stderr -f $(nomad job status www-nomad |egrep -m1 '\srun\s' |cut -f1 -d' ') + + +# get CPU / RAM stats and allocations +nomad node status -self + +nomad node status # OR pick a node's 1st column, then +nomad node status 01effcb8 + +# get list of all services, urls, and more, per nomad +wget -qO- --header "X-Nomad-Token: $NOMAD_TOKEN" $NOMAD_ADDR/v1/jobs |jq . +wget -qO- --header "X-Nomad-Token: $NOMAD_TOKEN" $NOMAD_ADDR/v1/job/JOB-NAME |jq . + + +# get list of all services and urls, per consul +consul catalog services -tags +wget -qO- 'http://127.0.0.1:8500/v1/catalog/services' |jq . +``` + +# Optional add-ons to your project + +## Secrets +In your project/repo Settings, set CI/CD environment variables starting with `NOMAD_SECRET_`, marked `Masked` but _not_ `Protected`, eg: +![Secrets](img/secrets.jpg) +and they will show up in your running container as environment variables, named with the lead `NOMAD_SECRET_` removed. Thus, you can get `DATABASE_URL` (etc.) set in your running container - but not have it anywhere else in your docker image and not printed/shown during CI/CD pipeline phase logging. + + +## Persistent Volumes +Persistent Volumes (PV) are like mounted disks that get setup before your container starts and _mount_ in as a filesystem into your running container. They are the only things that survive a running deployment update (eg: a new CI/CD pipeline), container restart, or system move to another cluster VM - hence _Persistent_. + +You can use PV to store files and data - especially nice for databases or otherwise (eg: retain `/var/lib/postgresql` through restarts, etc.) + +Here's how you'd update your project's `.gitlab-ci.yml` file, +by adding these lines (suggest near top of your file): +```yaml +variables: + NOMAD_VAR_PERSISTENT_VOLUME: '/pv' +``` +Then the dir `/pv/` will show up (blank to start with) in your running container. + +If you'd like to have the mounted dir show up somewhere besides `/pv` in your container, +you can setup like: +```yaml +variables: + NOMAD_VAR_PERSISTENT_VOLUME: '/var/lib/postgresql' +``` + +Please verify added/updated files persist through two repo CI/CD pipelines before adding important data and files. Your DevOps teams will try to ensure the VM that holds the data is backed up - but that does not happen by default without some extra setup. Your DevOps team must ensure each VM in the cluster has (the same) shared `/pv/` directory. We presently use NFS for this (after some data corruption issues with glusterFS and rook/ceph). + + +## Postgres DB +We have a [postgresql example](https://git.archive.org/www/dwebcamp2019), visible to archive.org folks. But the gist, aside from a CI/CD Variable/Secret `POSTGRESQL_PASSWORD`, is below. + +_Keep in mind if you setup something like a database in a container, using a Persistent Volume (like below) you can get multiple containers each trying to write to your database backing store filesystem (one for production; one temporarily for production re-deploy "canary"; and similar 1 or 2 for every deployed branch (which is probably not what you want). So you might want to look into `NOMAD_VAR_COUNT` and `NOMAD_VAR_COUNT_CANARIES` in that case._ + +It's recommended to run the DB container during the prestart hook as a "sidecar" service (this will cause it to finish starting before any other group tasks initialize, avoiding service start failures due to unavailable DB, see [nomad task dependencies](https://www.hashicorp.com/blog/hashicorp-nomad-task-dependencies) for more info) + +`.gitlab-ci.yml`: +```yaml +variables: + NOMAD_VAR_MULTI_CONTAINER: 'true' + NOMAD_VAR_PORTS: '{ 5000 = "http", 5432 = "db" }' + NOMAD_VAR_PERSISTENT_VOLUME: '/bitnami/postgresql' + NOMAD_VAR_CHECK_PROTOCOL: 'tcp' + # avoid 2+ containers running where both try to write to database + NOMAD_VAR_COUNT: 1 + NOMAD_VAR_COUNT_CANARIES: 0 + +include: + - remote: 'https://gitlab.com/internetarchive/nomad/-/raw/master/.gitlab-ci.yml' +``` +`vars.nomad`: +```ini +# used in @see group.nomad +variable "POSTGRESQL_PASSWORD" { + type = string + default = "" +} +``` +`group.nomad`: +```ini +task "db" { + driver = "docker" + lifecycle { + sidecar = true + hook = "prestart" + } + config { + image = "docker.io/bitnami/postgresql:11.7.0-debian-10-r9" + ports = ["db"] + volumes = ["/pv/${var.CI_PROJECT_PATH_SLUG}:/bitnami/postgresql"] + } + template { + data = <| .env && python ... +``` + +--- + +## Two `group`s, within same `job`, wanting to talk to each other +Normally, we strongly suggest all `task`s be together in the same `group`. +That will ensure all task containers are run on the same VM, and all tasks will get automatically managed and setup `env` vars, eg: +```ini +NOMAD_ADDR_backend=211.204.226.244:27344 +NOMAD_ADDR_http=211.204.226.244:23945 +``` + +However, if for some reason you want to split your tasks into 2+ `group { .. }` stanzas, +here is how you can get the containers to talk to each other (using `consul` and templating): +- https://github.com/hashicorp/nomad/issues/5455#issuecomment-482490116 +You'd end up putting your 2nd `group` in a file named `job.nomad` in the top of your repo. + +--- + +# GitHub repo integrations +## GitHub Actions +- We use GitHub Actions to create [build], [test], and [deploy] CI/CD pipelines. +- There is a lot of great information and links to example repos here: https://github.com/internetarchive/cicd#readme + +## GitHub Customizing +- You can use the same `NOMAD_VAR_` options above to tailor your deploy in the [#Customizing](#Customizing) section above. [Documentation and examples here](https://github.com/internetarchive/cicd#readme). + +## GitHub Secrets +- You can add GitHub secrets to your repo from the GitHub GUI ([documentation](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository)). You then need to get those secrets to pass through to the [deploy] phase, using the `NOMAD_SECRETS` setting in the GitHub Actions workflow yaml file. +- Note that you may want to test with repository or organizational level secrets before proceeding to setup environment secrets ( [documentation around creating secrets for an environment](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository) ) +- Here is an example GH repo that passes 2 GH secrets into the [deploy] phase. Each secret will wind up as environment variable that your servers can read, or your `RUN`/`CMD` entrypoint can read: + - https://github.com/traceypooh/staticman/blob/main/.github/workflows/cicd.yml + - [entrypoint setup](https://github.com/traceypooh/staticman/blob/main/Dockerfile) + - [entrypoint script](https://github.com/traceypooh/staticman/blob/main/entrypoint.sh) + +--- + +# Helpful links +- https://youtube.com/watch?v=3K1bSGN7zGA 'HashiConf Digital June 2020 - Full Opening Keynote' +- https://www.nomadproject.io/docs/install/production/deployment-guide/ +- https://learn.hashicorp.com/nomad/managing-jobs/configuring-tasks +- https://www.burgundywall.com/post/continuous-deployment-gitlab-and-nomad +- https://weekly-geekly.github.io/articles/453322/index.html +- https://www.haproxy.com/blog/haproxy-and-consul-with-dns-for-service-discovery/ +- https://www.youtube.com/watch?v=gf43TcWjBrE Kelsey Hightower, HashiConf 2016 +- https://blog.tjll.net/reverse-proxy-hot-dog-eating-contest-caddy-vs-nginx/#results +- https://github.com/hashicorp/consul-template/issues/200#issuecomment-76596830 + +## Pick your container stack / testimonials +- https://www.hashicorp.com/blog/hashicorp-joins-the-cncf/ +- https://www.nomadproject.io/intro/who-uses-nomad/ + - + http://jet.com/walmart +- https://medium.com/velotio-perspectives/how-much-do-you-really-know-about-simplified-cloud-deployments-b74d33637e07 +- https://blog.cloudflare.com/how-we-use-hashicorp-nomad/ +- https://www.hashicorp.com/resources/ncbi-legacy-migration-hybrid-cloud-consul-nomad/ +- https://thenewstack.io/fargate-grows-faster-than-kubernetes-among-aws-customers/ +- https://github.com/rishidot/Decision-Makers-Guide/blob/master/Decision%20Makers%20Guide%20-%20Nomad%20Vs%20Kubernetes%20-%20Oct%202019.pdf +- https://medium.com/@trevor00/building-container-platforms-part-one-introduction-4ee2338eb11 + + + +# Multi-node architecture +![Architecture](img/architecture.drawio.svg) + + +# Requirements for archive.org CI/CD +- docker exec ✅ + - pop into deployed container and poke around - similar to `ssh` + - @see [aliases](aliases) `nom-ssh` +- docker cp ✅ + - hot-copy edited file into _running_ deploy (avoid full pipeline to see changes) + - @see [aliases](aliases) `nom-cp` + - hook in VSCode + [sync-rsync](https://marketplace.visualstudio.com/items?itemName=vscode-ext.sync-rsync) + package to 'copy (into container) on save' +- secrets ✅ +- load balancers ✅ +- 2+ instances HPA ✅ +- PV ✅ +- http/2 ✅ +- auto http => https ✅ +- web sockets ✅ +- auto-embed HSTS in https headers, similar to kubernetes ✅ + - eg: `Strict-Transport-Security: max-age=15724800; includeSubdomains` + + +# Constraints +In the past, we've made it so certain jobs are "constrained" to run on specifc 1+ cluster VM. + +Here's how you can do it: +You can manually add this to 1+ VM `/etc/nomad/nomad.hcl` file: +```ini +client { + meta { + "kind" = "tcp-vm" + } +} +``` + +You can add this as a new file named `job.nomad` in the top of a project/repo: +```ini +constraint { + attribute = "${meta.kind}" + operator = "set_contains" + value = "tcp-vm" +} +``` + +Then deploys for this repo will *only* deploy to your specific VMs. diff --git a/aliases b/aliases new file mode 100644 index 0000000..a81920a --- /dev/null +++ b/aliases @@ -0,0 +1,312 @@ +#!/bin/bash + + +# look for NOMAD_ADDR and NOMAD_TOKEN +[ -e $HOME/.config/nomad ] && source $HOME/.config/nomad + + +# If not running interactively, don't setup autocomplete +if [ ! -z "$PS1" ]; then + # nomad/consul autocompletes + if [ "$ZSH_VERSION" = "" ]; then + which nomad >/dev/null && complete -C $(which nomad) nomad + which consul >/dev/null && complete -C $(which consul) consul + else + # https://apple.stackexchange.com/questions/296477/ + ( which compdef 2>&1 |fgrep -q ' not found' ) && autoload -Uz compinit && compinit + + which nomad >/dev/null && autoload -U +X bashcompinit && bashcompinit + which nomad >/dev/null && complete -o nospace -C $(which nomad) nomad + which consul >/dev/null && complete -o nospace -C $(which consul) consul + fi +fi + + +function nom-app() { + # finds the webapp related to given job/CWD and opens it in browser + [ $# -eq 1 ] && JOB=$1 + [ $# -ne 1 ] && JOB=$(nom-job-from-cwd) + + _nom-url + + URL=$(echo "$URL" |head -1) + + [ "$URL" = "" ] && echo "URL not found - is service running? try:\n nomad status $JOB" && return + open "$URL" +} + + +function nom-ssh() { + # simple way to pop in (ssh-like) to a given job + # Usage: [job name, eg: x-thumb] -OR- no args will use CWD to determine job + [ $# -ge 1 ] && JOB=$1 + [ $# -lt 1 ] && JOB=$(nom-job-from-cwd) + [ $# -ge 2 ] && TASK=$2 # for rarer TaskGroup case where 2+ Tasks spec-ed in same Job + [ $# -lt 2 ] && TASK=http + + ALLOC=$(nomad job status $JOB |egrep -m1 '\srun\s' |cut -f1 -d' ') + echo "nomad alloc exec -i -t -task $TASK $ALLOC" + + if [ $# -ge 3 ]; then + shift + shift + nomad alloc exec -i -t -task $TASK $ALLOC "$@" + else + nomad alloc exec -i -t -task $TASK $ALLOC \ + sh -c '([ -e /bin/zsh ] && zsh) || ([ -e /bin/bash ] && bash) || ([ -e /bin/sh ] && sh)' + fi +} + + +function nom-sshn() { + # simple way to pop in (ssh-like) to a given job with 2+ allocations/containers + local N=${1:?"Usage: [container/allocation number, starting with 1]"} + + local ALLOC=$(nomad job status $JOB |egrep '\srun\s' |head -n $N |tail -1 |cut -f1 -d' ') + echo "nomad alloc exec -i -t $ALLOC" + + nomad alloc exec -i -t $ALLOC \ + sh -c '([ -e /bin/zsh ] && zsh) || ([ -e /bin/bash ] && bash) || ([ -e /bin/sh ] && sh)' +} + + +function nom-cp() { + # copies a laptop local file into running deploy (avoids full pipeline just to see changes) + + # first, see if this is vscode sync-rsync + local VSCODE= + [ "$#" -ge 4 ] && ( echo "$@" |fgrep -q .vscode ) && VSCODE=1 + + if [ $VSCODE ]; then + # fish out file name from what VSCode 'sync-rsync' package sends us -- should be 2nd to last arg + local FILE=$(echo "$@" |rev |tr -s ' ' |cut -f2 -d' ' |rev) + # switch dirs to make aliases work + local DIR=$(dirname "$FILE") + cd "$DIR" + local BRANCH=$(git rev-parse --abbrev-ref HEAD) + local JOB=$(nom-job-from-cwd) + local ALLOC=$(nom-job-to-alloc) + local TASK=http + cd - + + else + local FILE=${1:?"Usage: [src file, locally qualified while 'cd'-ed inside a repo]"} + local BRANCH=$(git rev-parse --abbrev-ref HEAD) + local JOB=$(nom-job-from-cwd) + local ALLOC=$(nom-job-to-alloc) + [ $# -ge 2 ] && TASK=$2 # for rarer TaskGroup case where 2+ Tasks spec-ed in sam Job + [ $# -lt 2 ] && TASK=http + fi + + # now split the FILE name into two pieces -- 'the root of the git tree' and 'the rest' + local DIR=$(dirname "$FILE") + local TOP=$(git -C "$DIR" rev-parse --show-toplevel) + local REST=$(echo "$FILE" | perl -pe "s|^$TOP||; s|^/+||;") + + + for var in FILE DIR TOP REST BRANCH JOB ALLOC; do + echo $var="${(P)var}" + done + echo + + if [ $VSCODE ]; then + local MAIN= + [ "$BRANCH" = "main" ] && MAIN=true + [ "$BRANCH" = "master" ] && MAIN=true + + local RSYNC= + [ $MAIN ] && RSYNC=true + [ ! $MAIN ] && [ "$RSYNC_BRANCHES" ] && RSYNC=true + + [ $RSYNC ] && ( set -x; rsync "$@" ) + + # this is a special exception project where we DONT want to ALSO copy file to nomad deploy + [ $MAIN ] && [ "$JOB" = "ia-petabox" ] && [ ! $NOM_CP_PETABOX_MAIN ] && exit 0 + fi + + + if [ "$JOB" = "" -o "$ALLOC" = "" ]; then + # no relevant job & alloc found - nothing to do + echo 'has this branch run a full pipeline and deployed a Review App yet?' + return + fi + + # HinD updated nomad clusters w/ latest nomad seem to *not* get the stdin close properly + # (and thus hang). So timeout/kill after 2s :( tracey 2024/3 ) + set +e + cat "$FILE" | ( set -x; set +e; nomad alloc exec -i -task $TASK "$ALLOC" sh -c "timeout 2 cat >| '$REST'" ) + echo SUCCESS +} + + +function nom-logs() { + # simple way to view logs for a given job + [ $# -eq 1 ] && JOB=$1 + [ $# -ne 1 ] && JOB=$(nom-job-from-cwd) + # NOTE: the 2nd $JOB is useful for when a job has 2+ tasks (eg: `kv` or DB/redis, etc.) + nomad alloc logs -f -job $JOB http +} + + +function nom-logs-err() { + # simple way to view logs for a given job + [ $# -eq 1 ] && JOB=$1 + [ $# -ne 1 ] && JOB=$(nom-job-from-cwd) + nomad alloc logs -stderr -f -job $JOB http +} + + +function nom-status() { + # prints detailed status for a repo's service and deployment + [ $# -eq 1 ] && JOB=$1 + [ $# -ne 1 ] && JOB=$(nom-job-from-cwd) + + line + echo "nomad status $JOB" + line + nomad status $JOB | grep --color=always -iE 'unhealthy|healthy|$' + line + echo 'nomad alloc status -stats $(nom-job-to-alloc '$JOB')' + line + nomad alloc status -stats $(nom-job-to-alloc $JOB) | grep --color=always -iE 'unhealthy|healthy|Job Version.*|Node Name.*|$' + line +} + + +function nom-urls() { + # Lists all current urls for the services deployed to current nomad cluster (eg: webapps) + # Ideally, this is a faster single-shot call. But to avoid requiring either `consul` addr + # and ACL token _in addition_ to `nomad` - we'll just use `nomad` directly instead. + # consul catalog services -tags + for JOB in $(curl -sH "X-Nomad-Token: ${NOMAD_TOKEN?}" ${NOMAD_ADDR?}/v1/jobs \ + | jq -r '.[] | select(.Type=="service") | "\(.Name)"') + do + _nom-url + echo $URL + done |sort +} + + +function _nom-url() { + # logically private helper function + URL=$(curl -sH "X-Nomad-Token: ${NOMAD_TOKEN?}" ${NOMAD_ADDR?}/v1/job/$JOB \ + | jq -r '.TaskGroups[0].Services[0].Tags' \ + | fgrep . |fgrep -v redirect=308 |tr -d '", ' |perl -pe 's/:443//; s=^urlprefix\-=https://=;' + ) +} + + +function nom-resubmit() { + # Retrieves current job spec from nomad cluster and resubmits it to nomad. + # Useful for when a job has exceedded a setup timeout, is (nonideally) marked 'dead', etc. + [ $# -eq 1 ] && JOB=$1 + [ $# -ne 1 ] && JOB=$(nom-job-from-cwd) + + nomad inspect ${JOB?} |tee .$JOB + + # in case we are trying to _move_ an active/OK deploy + nomad stop ${JOB?} + sleep 5 + + curl -XPOST -H "Content-Type: application/json" -H "X-Nomad-Token: $NOMAD_TOKEN" -d @.${JOB?} \ + $NOMAD_ADDR/v1/jobs + + rm -f .$JOB +} + + +function d() { + # show docker running containers and local images + [ "$#" = "0" ] && clear -x + + local SUDO= + [ $(uname) = "Linux" ] && local SUDO=sudo + [ ! -e /usr/bin/docker ] && local docker=podman + + $SUDO $docker ps -a --format "table {{.ID}}\t{{.Image}}\t{{.Status}}\t{{.Names}}\t{{.State}}" | $SUDO cat >| $HOME/.dockps + chmod 666 $HOME/.dockps + for i in STATE running restarting created paused removing exited dead; do + cat $HOME/.dockps |egrep "$i$" |perl -pe 's/'$i'$//' + done + rm -f $HOME/.dockps + + line + $SUDO $docker images +} + +function nom() { + # quick way to get an overview of a nomad server when ssh-ed into it + d + line + nomad server members + line + nomad status + line +} + + +function nom-job-from-cwd() { + # print the nomad job name based on the current project + # parse out repo info, eg: 'ia-petabox' -- ensure clone-over-ssh or clone-over-https work + local GURL TMP GROUP_PROJECT PROJECT BRANCH SLUG JOB + GURL=$(git config --get remote.origin.url) + [[ "$GURL" =~ https:// ]] && TMP=$(echo "$GURL" |cut -f4- -d/) + [[ "$GURL" =~ https:// ]] || TMP=$(echo "$GURL" |rev |cut -f1 -d: |rev) + GROUP_PROJECT=$(echo "$TMP" |perl -pe 's/\.git//' |tr A-Z a-z |tr / -) + + PROJECT=$(git rev-parse --absolute-git-dir |egrep --color -o '.*?.git' |rev |cut -f2 -d/ |rev) + BRANCH=$(git rev-parse --abbrev-ref HEAD) + SLUG=$(echo "$BRANCH" |tr '/_.' '-' |tr A-Z a-z) + JOB=$GROUP_PROJECT + [ "$SLUG" = "main" -o "$SLUG" = "master" -o "$SLUG" = "staging" -o "$SLUG" = "production" ] || JOB="${JOB}-${SLUG}" + echo $(echo "$JOB" |cut -b1-63) +} + + + +function nom-image-from-cwd() { + # print the registry image based on the current project + # parse out repo info, eg: 'ia-petabox' -- ensure clone-over-ssh or clone-over-https work + local GURL GROUP_PROJECT BRANCH SLUG JOB + GURL=$(git config --get remote.origin.url) + [[ "$GURL" =~ https:// ]] && GROUP_PROJECT=$(echo "$GURL" |cut -f4- -d/) + [[ "$GURL" =~ https:// ]] || GROUP_PROJECT=$(echo "$GURL" |rev |cut -f1 -d: |rev) + + BRANCH=$(git rev-parse --abbrev-ref HEAD) + SLUG=$(echo "$BRANCH" |tr '/_.' '-' |tr A-Z a-z) + echo $(echo "registry.archive.org/$GROUP_PROJECT/$SLUG" |cut -b1-63) +} + + + + +function nom-job-to-alloc() { + # prints alloc of a given job (when in high-availability and 2+ allocations, picks one at random) + # Usage: [job name, eg: x-thumb] -OR- no args will use CWD to determine job + [ $# -eq 1 ] && JOB=$1 + [ $# -ne 1 ] && JOB=$(nom-job-from-cwd) + nomad job status $JOB |egrep -m1 '\srun\s' |cut -f1 -d' ' +} + + +function line () { + # horizontal line break + perl -e 'print "_"x100; print "\n\n";' +} + + +function nom-tunnel() { + # Sets up an ssh tunnel in the background to be able to talk to nomad cluster's consul. + [ "$NOMAD_ADDR" = "" ] && echo "Please set NOMAD_ADDR environment variable first" && return + local HOST=$(echo "$NOMAD_ADDR" | sed 's/:4646\/*$//' |sed 's/^https*:\/\///') + ssh -fNA -L 8500:localhost:8500 $HOST +} + + +function web-logs-tail() { + # admin script that can more easily "tail -f" the caddy (JSON) web logs + ( + set -x + tail -f /var/log/caddy/access.log | jq -r '"\(.request.host)\(.request.uri)\t\t\(.request.headers."User-Agent")"' + ) +} diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..5cc1bcc --- /dev/null +++ b/build.sh @@ -0,0 +1,94 @@ +#!/bin/bash -e + +# Build stage script for Auto-DevOps + +# FROM: registry.gitlab.com/internetarchive/auto-build-image/main +# which was +# FROM registry.gitlab.com/gitlab-org/cluster-integration/auto-build-image:v1.14.0 +# +# then pulled the unused heroku/buildpack stuff/clutter + +# Wondering how to do podman-in-podman? Of course we are. Here's a minimal example: +# +# SOCK=$(sudo podman info |grep -F podman.sock |rev |cut -f1 -d ' ' |rev) +# podman run --rm --privileged --net=host --cgroupns=host -v $SOCK:$SOCK registry.gitlab.com/internetarchive/nomad/master zsh -c 'podman --remote ps -a' + +set -o pipefail + +filter_docker_warning() { + grep -E -v "^WARNING! Your password will be stored unencrypted in |^Configure a credential helper to remove this warning. See|^https://docs.docker.com/engine/reference/commandline/login/#credentials-store" || true +} + +docker_login_filtered() { + # $1 - username, $2 - password, $3 - registry + # this filters the stderr of the `podman --remote login`, without merging stdout and stderr together + { echo "$2" | podman --remote login -u "$1" --password-stdin "$3" 2>&1 1>&3 | filter_docker_warning 1>&2; } 3>&1 +} + +gl_write_auto_build_variables_file() { + echo "CI_APPLICATION_TAG=$CI_APPLICATION_TAG@$(podman --remote image inspect --format='{{ index (split (index .RepoDigests 0) "@") 1 }}' "$image_tagged")" > gl-auto-build-variables.env +} + + +if [[ -z "$CI_COMMIT_TAG" ]]; then + export CI_APPLICATION_REPOSITORY=${CI_APPLICATION_REPOSITORY:-$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG} + export CI_APPLICATION_TAG=${CI_APPLICATION_TAG:-$CI_COMMIT_SHA} +else + export CI_APPLICATION_REPOSITORY=${CI_APPLICATION_REPOSITORY:-$CI_REGISTRY_IMAGE} + export CI_APPLICATION_TAG=${CI_APPLICATION_TAG:-$CI_COMMIT_TAG} +fi + +DOCKER_BUILDKIT=1 +image_tagged="$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" +image_latest="$CI_APPLICATION_REPOSITORY:latest" + +if [[ -n "$CI_REGISTRY" && -n "$CI_REGISTRY_USER" ]]; then + echo "Logging in to GitLab Container Registry with CI credentials..." + docker_login_filtered "$CI_REGISTRY_USER" "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" +fi + + +# xxx seccomp for IA git repos +# o/w opening seccomp profile failed: open /etc/containers/seccomp.json: no such file or directory +build_args=( + --cache-from "$CI_APPLICATION_REPOSITORY" + $AUTO_DEVOPS_BUILD_IMAGE_EXTRA_ARGS + --security-opt seccomp=unconfined + --tag "$image_tagged" +) + +if [ "$NOMAD_VAR_SERVERLESS" = "" ]; then + build_args+=(--tag "$image_latest") +fi + +if [[ -n "${DOCKERFILE_PATH}" ]]; then + build_args+=(-f "$DOCKERFILE_PATH") +fi + +if [[ -n "$AUTO_DEVOPS_BUILD_IMAGE_FORWARDED_CI_VARIABLES" ]]; then + build_secret_file_path=/tmp/auto-devops-build-secrets + "$(dirname "$0")"/export-build-secrets > "$build_secret_file_path" # xxx /build/export-build-secrets + build_args+=( + --secret "id=auto-devops-build-secrets,src=$build_secret_file_path" + ) +fi + + +( + set -x + podman --remote buildx build "${build_args[@]}" --progress=plain . 2>&1 +) + +( + set -x + podman --remote push "$image_tagged" +) +if [ "$NOMAD_VAR_SERVERLESS" = "" ]; then + ( + set -x + podman --remote push "$image_latest" + ) +fi + + +gl_write_auto_build_variables_file diff --git a/build.yml b/build.yml new file mode 100644 index 0000000..66e2624 --- /dev/null +++ b/build.yml @@ -0,0 +1,23 @@ +# Tracey 3/2024: +# This was adapted & simplified from: +# https://gitlab.com/gitlab-org/gitlab/-/raw/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml + +build: + stage: build + # If need to rebuild this image while runners are down, `cd` to this directory, then, as root: + # podman login registry.gitlab.com + # podman build --net=host --tag registry.gitlab.com/internetarchive/nomad/master . && sudo podman push registry.gitlab.com/internetarchive/nomad/master + image: registry.gitlab.com/internetarchive/nomad/master + variables: + DOCKER_HOST: 'unix:///run/podman/podman.sock' + DOCKER_TLS_CERTDIR: '' + DOCKER_BUILDKIT: 1 + script: + - /build.sh + artifacts: + reports: + dotenv: gl-auto-build-variables.env + rules: + - if: '$BUILD_DISABLED' + when: never + - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..c430a3d --- /dev/null +++ b/deploy.sh @@ -0,0 +1,346 @@ +#!/bin/bash -e + +function verbose() { + if [ "$NOMAD_VAR_VERBOSE" ]; then + echo "$@"; + fi +} + + +function main() { + if [ "$NOMAD_TOKEN" = test ]; then + # during testing, set any var that isn't set, to an empty string, when the var gets used later + NOMAD_VAR_NO_DEPLOY=${NOMAD_VAR_NO_DEPLOY:-""} + GITHUB_ACTIONS=${GITHUB_ACTIONS:-""} + NOMAD_VAR_HOSTNAMES=${NOMAD_VAR_HOSTNAMES:-""} + CI_REGISTRY_READ_TOKEN=${CI_REGISTRY_READ_TOKEN:-""} + NOMAD_VAR_COUNT=${NOMAD_VAR_COUNT:-""} + NOMAD_SECRETS=${NOMAD_SECRETS:-""} + NOMAD_ADDR=${NOMAD_ADDR:-""} + NOMAD_TOKEN_PROD=${NOMAD_TOKEN_PROD:-""} + NOMAD_TOKEN_STAGING=${NOMAD_TOKEN_STAGING:-""} + NOMAD_TOKEN_EXT=${NOMAD_TOKEN_EXT:-""} + PRIVATE_REPO=${PRIVATE_REPO:-""} + fi + + + # IF someone set this programmatically in their project yml `before_script:` tag, etc., exit + if [ "$NOMAD_VAR_NO_DEPLOY" ]; then exit 0; fi + + if [ "$GITHUB_ACTIONS" ]; then github-setup; fi + + ############################### NOMAD VARS SETUP ############################## + + # auto-convert from pre-2022 var name + if [ "$BASE_DOMAIN" = "" ]; then + BASE_DOMAIN="$KUBE_INGRESS_BASE_DOMAIN" + fi + + MAIN_OR_PROD_OR_STAGING_OR_EXT= + MAIN_OR_PROD_OR_STAGING_OR_EXT_SLUG= + PRODUCTION= + STAGING= + EXT= + if [ "$CI_COMMIT_REF_SLUG" = "main" -o "$CI_COMMIT_REF_SLUG" = "master" ]; then + MAIN_OR_PROD_OR_STAGING_OR_EXT=1 + MAIN_OR_PROD_OR_STAGING_OR_EXT_SLUG=1 + elif [ "$CI_COMMIT_REF_SLUG" = "production" ]; then + PRODUCTION=1 + MAIN_OR_PROD_OR_STAGING_OR_EXT=1 + MAIN_OR_PROD_OR_STAGING_OR_EXT_SLUG=1 + elif [ "$BASE_DOMAIN" = "prod.archive.org" ]; then + # NOTE: this is _very_ unusual -- but it's where a repo can elect to have + # another branch name (not `production`) deploy to production cluster via (typically) various + # gitlab CI/CD variables pegged to that branch name. + PRODUCTION=1 + MAIN_OR_PROD_OR_STAGING_OR_EXT=1 + elif [ "$CI_COMMIT_REF_SLUG" = "staging" ]; then + STAGING=1 + MAIN_OR_PROD_OR_STAGING_OR_EXT=1 + MAIN_OR_PROD_OR_STAGING_OR_EXT_SLUG=1 + elif [ "$CI_COMMIT_REF_SLUG" = "ext" ]; then + EXT=1 + MAIN_OR_PROD_OR_STAGING_OR_EXT=1 + MAIN_OR_PROD_OR_STAGING_OR_EXT_SLUG=1 + fi + + + # some archive.org specific production/staging/ext deployment detection & var updates first + if [[ "$BASE_DOMAIN" == *.archive.org ]]; then + if [ $PRODUCTION ]; then + export BASE_DOMAIN=prod.archive.org + if [[ "$CI_PROJECT_PATH_SLUG" == internetarchive-emularity-* ]]; then + export BASE_DOMAIN=ux-b.archive.org + fi + elif [ $STAGING ]; then + export BASE_DOMAIN=staging.archive.org + elif [ $EXT ]; then + export BASE_DOMAIN=ext.archive.org + fi + + if [ $PRODUCTION ]; then + if [ "$NOMAD_TOKEN_PROD" != "" ]; then + export NOMAD_TOKEN="$NOMAD_TOKEN_PROD" + echo using nomad production token + fi + if [ "$NOMAD_VAR_COUNT" = "" ]; then + export NOMAD_VAR_COUNT=3 + fi + elif [ $STAGING ]; then + if [ "$NOMAD_TOKEN_STAGING" != "" ]; then + export NOMAD_TOKEN="$NOMAD_TOKEN_STAGING" + echo using nomad staging token + fi + elif [ $EXT ]; then + if [ "$NOMAD_TOKEN_EXT" != "" ]; then + export NOMAD_TOKEN="$NOMAD_TOKEN_EXT" + echo using nomad ext token + fi + fi + fi + + export BASE_DOMAIN + + + # Make a nice "slug" that is like [GROUP]-[PROJECT]-[BRANCH], each component also "slugged", + # where "-main", "-master", "-production", "-staging", "-ext" are omitted. + # Respect DNS 63 max chars limit. + export BRANCH_PART="" + if [ ! $MAIN_OR_PROD_OR_STAGING_OR_EXT_SLUG ]; then + export BRANCH_PART="-${CI_COMMIT_REF_SLUG}" + fi + export NOMAD_VAR_SLUG=$(echo "${CI_PROJECT_PATH_SLUG}${BRANCH_PART}" |cut -b1-63) + # make nice (semantic) hostname, based on the slug, eg: + # services-timemachine.x.archive.org + # ia-petabox-webdev-3939-fix-things.x.archive.org + # however, if repo has list of 1+ custom hostnames it wants to use instead for main/master branch + # review app, then use them and log during [deploy] phase the first hostname in the list + export HOSTNAME="${NOMAD_VAR_SLUG}.${BASE_DOMAIN}" + # NOTE: YAML or CI/CD Variable `NOMAD_VAR_HOSTNAMES` is *IGNORED* -- and automatic $HOSTNAME above + # is used for branches not main/master/production/staging/ext + + # make even nicer names for archive.org processing cluster deploys + if [ "$BASE_DOMAIN" = "work.archive.org" ]; then + export HOSTNAME="${CI_PROJECT_NAME}${BRANCH_PART}.${BASE_DOMAIN}" + fi + + if [ "$NOMAD_ADDR" = "" ]; then + export NOMAD_ADDR=https://$BASE_DOMAIN + if [ "$BASE_DOMAIN" = archive.org ]; then + # an archive.org specific adjustment + export NOMAD_ADDR=https://dev.archive.org + fi + fi + + if [ "$NOMAD_VAR_HOSTNAMES" != "" -a "$BASE_DOMAIN" != "" ]; then + # Now auto-append .$BASE_DOMAIN to any hostname that isn't a fully qualified domain name + export NOMAD_VAR_HOSTNAMES=$(deno eval 'const fqdns = JSON.parse(Deno.env.get("NOMAD_VAR_HOSTNAMES")).map((e) => e.includes(".") ? e : e.concat(".").concat(Deno.env.get("BASE_DOMAIN"))); console.log(fqdns)') + fi + + if [ "$MAIN_OR_PROD_OR_STAGING_OR_EXT" -a "$NOMAD_VAR_HOSTNAMES" != "" ]; then + export HOSTNAME=$(echo "$NOMAD_VAR_HOSTNAMES" |cut -f1 -d, |tr -d '[]" ' |tr -d "'") + else + NOMAD_VAR_HOSTNAMES= + + if [ "$PRODUCTION" -o "$STAGING" -o "$EXT" ]; then + export HOSTNAME="${CI_PROJECT_NAME}.$BASE_DOMAIN" + fi + fi + + + if [ "$NOMAD_VAR_HOSTNAMES" = "" ]; then + export NOMAD_VAR_HOSTNAMES='["'$HOSTNAME'"]' + fi + + + if [[ "$NOMAD_ADDR" == *crawl*.archive.org:* ]]; then # nixxx + export NOMAD_VAR_CONSUL_PATH='/usr/local/bin/consul' + fi + + + if [ "$CI_REGISTRY_READ_TOKEN" = "0" ]; then unset CI_REGISTRY_READ_TOKEN; fi + + ############################### NOMAD VARS SETUP ############################## + + + + if [ "$ARG1" = "stop" ]; then + nomad stop $NOMAD_VAR_SLUG + exit 0 + fi + + + + echo using nomad cluster $NOMAD_ADDR + echo deploying to https://$HOSTNAME + + # You can have your own/custom `project.nomad` in the top of your repo - or we'll just use + # this fully parameterized nice generic 'house style' project. + # + # Create project.hcl - including optional insertions that a repo might elect to inject + REPODIR="$(pwd)" + cd /tmp + if [ -e "$REPODIR/project.nomad" ]; then + cp "$REPODIR/project.nomad" project.nomad + else + rm -f project.nomad + wget -q https://gitlab.com/internetarchive/nomad/-/raw/master/project.nomad + fi + + verbose "Replacing variables internal to project.nomad." + + ( + grep -F -B10000 VARS.NOMAD--INSERTS-HERE project.nomad + # if this filename doesnt exist in repo, this line noops + cat "$REPODIR/vars.nomad" 2>/dev/null || echo + grep -F -A10000 VARS.NOMAD--INSERTS-HERE project.nomad + ) >| tmp.nomad + cp tmp.nomad project.nomad + ( + grep -F -B10000 JOB.NOMAD--INSERTS-HERE project.nomad + # if this filename doesnt exist in repo, this line noops + cat "$REPODIR/job.nomad" 2>/dev/null || echo + grep -F -A10000 JOB.NOMAD--INSERTS-HERE project.nomad + ) >| tmp.nomad + cp tmp.nomad project.nomad + ( + grep -F -B10000 GROUP.NOMAD--INSERTS-HERE project.nomad + # if this filename doesnt exist in repo, this line noops + cat "$REPODIR/group.nomad" 2>/dev/null || echo + grep -F -A10000 GROUP.NOMAD--INSERTS-HERE project.nomad + ) >| tmp.nomad + cp tmp.nomad project.nomad + + verbose "project.nomad -> project.hcl" + + cp project.nomad project.hcl + + verbose "NOMAD_VAR_SLUG variable substitution" + # Do the one current substitution nomad v1.0.3 can't do now (apparently a bug) + sed -ix "s/NOMAD_VAR_SLUG/$NOMAD_VAR_SLUG/" project.hcl + + case "$NOMAD_ADDR" in + https://work.archive.org|https://hind.archive.org|https://dev.archive.org|https://ext.archive.org) + # HinD cluster(s) use `podman` driver instead of `docker` + sed -ix 's/driver\s*=\s*"docker"/driver="podman"/' project.hcl # xxx + sed -ix 's/memory_hard_limit/# memory_hard_limit/' project.hcl # xxx + ;; + esac + + verbose "Handling NOMAD_SECRETS." + if [ "$NOMAD_SECRETS" = "" ]; then + # Set NOMAD_SECRETS to JSON encoded key/val hashmap of env vars starting w/ "NOMAD_SECRET_" + # (w/ NOMAD_SECRET_ prefix omitted), then convert to HCL style hashmap string (chars ":" => "=") + echo '{}' >| env.env + ( env | grep -qE ^NOMAD_SECRET_ ) && ( + echo NOMAD_SECRETS=$(deno eval 'console.log(JSON.stringify(Object.fromEntries(Object.entries(Deno.env.toObject()).filter(([k, v]) => k.startsWith("NOMAD_SECRET_")).map(([k ,v]) => [k.replace(/^NOMAD_SECRET_/,""), v]))))' | sed 's/":"/"="/g') >| env.env + ) + else + # this alternate clause allows GitHub Actions to send in repo secrets to us, as a single secret + # variable, as our JSON-like hashmap of keys (secret/env var names) and values + cat >| env.env << EOF +NOMAD_SECRETS=$NOMAD_SECRETS +EOF + fi + + verbose "copy current env vars starting with "CI_" to "NOMAD_VAR_CI_" variants & inject them into shell" + deno eval 'Object.entries(Deno.env.toObject()).map(([k, v]) => console.log("export NOMAD_VAR_"+k+"="+JSON.stringify(v)))' | grep -E '^export NOMAD_VAR_CI_' >| ci.env + source ci.env + rm ci.env + + if [ "$NOMAD_TOKEN" = test ]; then + nomad run -output -var-file=env.env project.hcl >| project.json + exit 0 + fi + + set -x + nomad validate -var-file=env.env project.hcl + nomad plan -var-file=env.env project.hcl 2>&1 |sed 's/\(password[^ \t]*[ \t]*\).*/\1 ... /' |tee plan.log || echo + export INDEX=$(grep -E -o -- '-check-index [0-9]+' plan.log |tr -dc 0-9) + + # some clusters sometimes fail to fetch deployment :( -- so let's retry 5x + for RETRIES in $(seq 1 5); do + set -o pipefail + nomad run -var-file=env.env -check-index $INDEX project.hcl 2>&1 |tee check.log + if [ "$?" = "0" ]; then + if grep -E 'Status[ ]*=[ ]*failed' check.log; then + # for example, unhealthy 5x, unable to roll back, ends up failing + exit 1 + fi + + # This particular fail case output doesnt seem to exit non-zero -- so we have to check for it + # ==> 2023-03-29T17:21:15Z: Error fetching deployment + if ! grep -F 'Error fetching deployment' check.log; then + echo deployed to https://$HOSTNAME + return + fi + fi + + echo retrying.. + sleep 10 + continue + done + exit 1 +} + + +function github-setup() { + # Converts from GitHub env vars to GitLab-like env vars + + # You must add these as Secrets to your repository: + # NOMAD_TOKEN + # NOMAD_TOKEN_PROD (optional) + # NOMAD_TOKEN_STAGING (optional) + # NOMAD_TOKEN_EXT (optional) + + # You may override the defaults via passed-in args from your repository: + # BASE_DOMAIN + # NOMAD_ADDR + # https://github.com/internetarchive/cicd + + + # Example of the (limited) GitHub ENV vars that are avail to us: + # GITHUB_REPOSITORY=internetarchive/dyno + + # (registry host) + export CI_REGISTRY=ghcr.io + + local GITHUB_REPOSITORY_LC=$(echo "${GITHUB_REPOSITORY?}" |tr A-Z a-z) + + # eg: ghcr.io/internetarchive/dyno:main (registry image) + export CI_GITHUB_IMAGE="${CI_REGISTRY?}/${GITHUB_REPOSITORY_LC?}:${GITHUB_REF_NAME?}" + # since the registry image :part uses a _branch name_ and not a commit id (like gitlab), + # we can end up with a stale deploy if we happen to redeploy to the same VM. so force a pull. + export NOMAD_VAR_FORCE_PULL=true + + # eg: dyno (project name) + export CI_PROJECT_NAME=$(basename "${GITHUB_REPOSITORY_LC?}") + + # eg: main (branchname) xxxd slugme + export CI_COMMIT_REF_SLUG="${GITHUB_REF_NAME?}" + + # eg: internetarchive-dyno xxxd better slugification + export CI_PROJECT_PATH_SLUG=$(echo "${GITHUB_REPOSITORY_LC?}" |tr '/.' - |cut -b1-63 | sed 's/[^a-z0-9\-]//g') + + if [ "$PRIVATE_REPO" = "false" ]; then + # turn off `docker login`` before pulling registry image, since it seems like the TOKEN expires + # and makes re-deployment due to containers changing hosts not work.. sometimes? always? + unset CI_REGISTRY_READ_TOKEN + fi + + + # unset any blank vars that come in from GH actions + for i in $(env | grep -E '^NOMAD_VAR_[A-Z0-9_]+=$' |cut -f1 -d=); do + unset $i + done + + # see if we should do nothing + if [ "$NOMAD_VAR_NO_DEPLOY" ]; then exit 0; fi + if [ "${NOMAD_TOKEN}${NOMAD_TOKEN_PROD}${NOMAD_TOKEN_STAGING}${NOMAD_TOKEN_EXT}" = "" ]; then exit 0; fi +} + + +ARG1= +if [ $# -gt 0 ]; then ARG1=$1; fi + +main diff --git a/hello-world.hcl b/hello-world.hcl new file mode 100644 index 0000000..661f948 --- /dev/null +++ b/hello-world.hcl @@ -0,0 +1,57 @@ +# Minimal basic project using only GitLab CI/CD std. variables +# Run like: nomad run hello-world.hcl + +# Variables used below and their defaults if not set externally +variables { + # These all pass through from GitLab [build] phase. + # Some defaults filled in w/ example repo "bai" in group "internetarchive" + # (but all 7 get replaced during normal GitLab CI/CD from CI/CD variables). + CI_REGISTRY = "registry.gitlab.com" # registry hostname + CI_REGISTRY_IMAGE = "registry.gitlab.com/internetarchive/bai" # registry image location + CI_COMMIT_REF_SLUG = "main" # branch name, slugged + CI_COMMIT_SHA = "latest" # repo's commit for current pipline + CI_PROJECT_PATH_SLUG = "internetarchive-bai" # repo and group it is part of, slugged + CI_REGISTRY_USER = "" # set for each pipeline and .. + CI_REGISTRY_PASSWORD = "" # .. allows pull from private registry + + # Switch this, locally edit your /etc/hosts, or otherwise. as is, webapp will appear at: + # https://internetarchive-bai-main.x.archive.org/ + BASE_DOMAIN = "x.archive.org" +} + +job "hello-world" { + datacenters = ["dc1"] + group "group" { + network { + port "http" { + to = 5000 + } + } + service { + tags = ["https://${var.CI_PROJECT_PATH_SLUG}-${var.CI_COMMIT_REF_SLUG}.${var.BASE_DOMAIN}"] + port = "http" + check { + type = "http" + port = "http" + path = "/" + interval = "10s" + timeout = "2s" + } + } + task "web" { + driver = "docker" + + config { + image = "${var.CI_REGISTRY_IMAGE}/${var.CI_COMMIT_REF_SLUG}:${var.CI_COMMIT_SHA}" + + ports = [ "http" ] + + auth { + server_address = "${var.CI_REGISTRY}" + username = "${var.CI_REGISTRY_USER}" + password = "${var.CI_REGISTRY_PASSWORD}" + } + } + } + } +} diff --git a/img/architecture.drawio.svg b/img/architecture.drawio.svg new file mode 100644 index 0000000..51fdfda --- /dev/null +++ b/img/architecture.drawio.svg @@ -0,0 +1,452 @@ + + + + + + + + + +
+
+
+ Hashistack +
+
+
+
+ + Hashistack + +
+
+ + + + + + virtual machine 1               + + + + + + + + + +
+
+
+ Nomad daemon +
+
+
+
+ + Nomad daemon + +
+
+ + + + + +
+
+
+ webapp2 +
+
+
+
+ + webapp2 + +
+
+ + + + + + + + + +
+
+
+ docker +
+  daemon +
+
+
+
+ + docker... + +
+
+ + + + + +
+
+
+ webapp2 +
+
+
+
+ + webapp2 + +
+
+ + + + + + + + + + +
+
+
+ svc +
+ discovery +
+
+
+
+ + svc... + +
+
+ + + + + +
+
+
+ Consul +
+ daemon +
+
+
+
+ + Consul... + +
+
+ + + + + + +
+
+
+ Fabio +
+ loadbalancer +
+
+ (has https certs) +
+
+
+
+ + Fabio... + +
+
+ + + + + +      virtual    machine 2 + + + + + + + + + +
+
+
+ Nomad daemon +
+
+
+
+ + Nomad daemon + +
+
+ + + + + +
+
+
+ webapp2 +
+
+
+
+ + webapp2 + +
+
+ + + + + + + + + +
+
+
+ docker +
+  daemon +
+
+
+
+ + docker... + +
+
+ + + + + +
+
+
+ webapp1 +
+
+
+
+ + webapp1 + +
+
+ + + + + + + + + + +
+
+
+ Consul +
+ daemon +
+
+
+
+ + Consul... + +
+
+ + + + + + + + +
+
+
+ http to +
+ webapp1 +
+
+
+
+ + http to... + +
+
+ + + + + + + + +
+
+
+ http to +
+ webapp2 +
+
+
+
+ + http to... + +
+
+ + + + + + +
+
+
+ httpS to either +
+ webapp1 +
+ webapp2 +
+
+
+
+ + httpS to either... + +
+
+ + + + + + + + +
+
+
+ browser +
+
+
+
+ + browser + +
+
+ + + + + +
+
+
+ service discovery +
+
+
+
+ + service discovery + +
+
+ + + + + + + +
+
+
+ gitlab CI/CD pipeline +
+ or admin +
+
+
+
+ + gitla... + +
+
+ + + + + +
+
+
+ web +
+
+
+
+ + web + +
+
+ + +
+ + + + + Viewer does not support full SVG 1.1 + + + +
\ No newline at end of file diff --git a/img/overview.drawio.svg b/img/overview.drawio.svg new file mode 100644 index 0000000..92c3d84 --- /dev/null +++ b/img/overview.drawio.svg @@ -0,0 +1,180 @@ + + + + + + + + + +
+
+
+ Hashistack +
+
+
+
+ + Hashistack + +
+
+ + + + + + +
+
+
+ browser +
+
+
+
+ + browser + +
+
+ + + + + + +
+
+
+
+ + loadbalancer  + +
+
+
+
+
+ + loadbalancer  + +
+
+ + + + +
+
+
+ http to webapp +
+
+
+
+ + http to webapp + +
+
+ + + + +
+
+
+
+
+ + webapp + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + webapp... + +
+
+ + + + + + +
+
+
+ httpS to webapp +
+
+
+
+ + httpS to webapp + +
+
+ + + + + + +
+
+
+ http daemon +
+
+
+
+ + http daem... + +
+
+ + + + +
+
+
+ DB +
+
+
+
+ + DB + +
+
+ + + + +
+ + + + + Viewer does not support full SVG 1.1 + + + +
\ No newline at end of file diff --git a/img/overview2.drawio.svg b/img/overview2.drawio.svg new file mode 100644 index 0000000..5de17a6 --- /dev/null +++ b/img/overview2.drawio.svg @@ -0,0 +1,299 @@ + + + + + + + + + +
+
+
+ Hashistack +
+
+
+
+ + Hashistack + +
+
+ + + + + + +
+
+
+ browser +
+
+
+
+ + browser + +
+
+ + + + + + +
+
+
+
+ + Fabio + + loadbalancer  +
+
+
+
+
+ + Fabio loadbalancer  + +
+
+ + + + +
+
+
+ http to webapp +
+
+
+
+ + http to webapp + +
+
+ + + + +
+
+
+
+ webapp + + TaskGroup + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + webapp TaskGroup... + +
+
+ + + + + + +
+
+
+ httpS to webapp +
+
+
+
+ + httpS to webapp + +
+
+ + + + + + +
+
+
+ http daemon +
+
+
+
+ + http daem... + +
+
+ + + + +
+
+
+ DB +
+
+
+
+ + DB + +
+
+ + + + + + + + + + + + +
+
+
+ gitlab CI/CD  +
+ or admin +
+
+
+
+ + gitlab CI/CD... + +
+
+ + + + + + + + + +
+
+
+ + Nomad +
+ daemon +
+
+
+
+
+ + Nomad... + +
+
+ + + + + + +
+
+
+ + docker +
+ daemon +
+
+
+
+
+ + docker... + +
+
+ + + + +
+
+
+ + Vault +
+ daemon +
+
+
+
+
+ + Vault... + +
+
+ + + + + + +
+
+
+ + Consul +
+ daemon +
+
+
+
+
+ + Consul... + +
+
+ + +
+ + + + + Viewer does not support full SVG 1.1 + + + +
\ No newline at end of file diff --git a/img/prod.jpg b/img/prod.jpg new file mode 100644 index 0000000000000000000000000000000000000000..55c7cca2a7ea711b660ea1a17fa5df93271b0dbd GIT binary patch literal 27724 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
{?2cF{e=B|DunVgj9)!RP7J%l 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 QSN|Ub6aN=-3i`<300Q+oIsgCw literal 0 HcmV?d00001 diff --git a/img/protect.jpg b/img/protect.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5fc325ca4198baa19cc30ab148b3f9aad7181b08 GIT binary patch literal 27738 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 literal 0 HcmV?d00001 diff --git a/img/secrets.jpg b/img/secrets.jpg new file mode 100644 index 0000000000000000000000000000000000000000..44caab5e8c91780421a7911e59934cb04a446d22 GIT binary patch literal 47183 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�HuVZh=wwg1sEF2 zJ$68oF4%pgo-gqlaL*>KU0`D`6tH9U&eklW?~7y&3E5@gxBzt_wF&>{=@{rb8<*#g z?xKH|`G@{}HA<5F2roo>_ff2;MrmJBfQeWqVN2OE24mAETd_!`+@QOJJ(EG^ox#?J z;D{*cy+}F|#`xTP{n<2~nKRFQk*7n_O<(ogD<1%mDfX5T8bgg&-gAdEn4dtJ3q*ch zFD6{jjG70fnIoKi(`P$a43$rX+c2nM&nld7<%49fYXsYwsUy<2qyR0m2xg$#mvfN5d5IOz3ECOFH3~@vM0tPakkRgA$2if z-9E`cFLFD;0Sfe4K4q`vbc)ukF9Y&<<(X5C?)s&|4Wu7%0T=T*>(psN1syLjI6g^E{b@3m}&3$`lQY2$vte3U3qoAVw#tKl$sgs zt&8YICRZprhDFSNz0SHr5U%;if*q0a4r|S-q17`wOi^9BV6JH;#zsWIpKyGw@~wQp za1zR(ZenQ3PgO)Y{=#CzmCDtx>Vx1N$Cx9WgCgQA(t~z6TovV~b2g1hXA?_fP{pYc z?KMe-C2m#D-e9-Zj~qUEdSqg3CzX1uoDx<7c=mUr%}!w$g*vf!&vU`=r`2vj&Gc@O<5cN2wagZoG-a z^HQ(h#vXv8TabYyt_hSvylcabz=XtA;VAQ9> ziB56+8gnW+n+KN8kdM$%{Z_Wzi$>0qI*u??h~niLOLE+dp%_>-mJOlCiihjf&}Eju zcwMQ`4r7tO=4YKIhg%CMcQ7A`<@fyMSGJiLTCO?CH@rrH{!= zzdqVQBr}pNgNr9eG6D*O(`Vr?CV)gKg)PBVa4q>0@e9pEqR(vH{8RH#7f9geomF}q zZ?6X5kBB>M^hnZ6Ldi+v6&KH4syQ~>KPJIg^u7IJ%zHq1;?NZqybUfPb$PB0UeL<- z=^xVv3dcsODcfrzIxWh+`AIaqPOKaZV~Wu+353xc5OPjr^7&|N9R6Z_BbeP+6=(H! z?hkCTU6cr+o3U%UuIdW@!0}&Euu!I4mL={x^NO|$h`Rt2J z{mMAnQh}`GQIX^m9!@eP%r-FjqD=8Rd#)Acl>(I33JXu8VNvXzN0=m8y{^PI`f;09 zUBgGKxPar7CVgUO6tucJi(J8H!2$Ew_4Wqr<#sk+zF#j)Dso7)31<{%gOK>twSxWQ zu!M53I-<63HVBQMXVQ6?I?>GK+B7APVJS1aFl&KgN}%Ld$kd)iw%JgYB3O{UjoZx3 z_hRUaJB4%Ku5P2SZ&S!Fa^}jFY(>F*E8m_{OV`#*?uD3grf=FXQW^9{58!dHmg|>K zI^eAmZ>C$Vp*nJD=ARd9HXqc$pTo3~V>Ua|)bpj~hB16&w#qgZL>?l@BNB2ZvzX)w z)X(z={oFluE9-joK9|Y_%Fc1biZ+v}+80QIj!8(Iw&3c#=FdO%7If0$OAP!T>8PN! zzchUuIWZT+13)I+6^XmI-%CVUHq?e2Mr$cYr^je8W@kU!@=3$nAp?+;S|*8q8^2?I z>Dtl|ed68Wb<)IPPp$vR`NsIUio*L`!FI7=&6^Yd^k z-Y^VII$P6N21gPmJw3HDMe@_c1{3FCj>aQ zDdiO?P#Hm|+j;EOHMS~oS)oc*KJl$nk@9^5devRDna{DDbC_77K1#LXoY4pKppdXhR zz{@Y-cWJ5z=|EeGeCcJWF;xJdo`h<_dYxuoyRzebzn0 zT(v}RZIpyg>J@5=4y z_k@t91?EIry0LYCaOzzTJsr2>`TLerl~0$(;{}Ujt$qP2{8n*(?Ngi;W&J5`$@Kzs zbaAh40E+j7qvPA?lMSnKX2oJ_pRz(9@9Kh@aEM<$wYm)_*$uF+S?tS5dG({B4%RT9 zQg|4LfvhRVv8*48#_ zz5G=iGx>dMb1Z-v5qD2q)hbhx+_SJaKf%+OgGNjzo4!Q@R&$3;`m3Et%eU#%szH(F z5Nqkq3-a3>tk$TVFW3aDhI}`v{FpvYMMa zs7Ad6V?|a9EbKw^EZFN#gQD>h!6N@FNSnmN;(s~WvaxTKe_shBqtL_^WtDM)#wkdn zP>0>{<;wxuwY3eil@>KMF>(+Yd)ga>f)8ge`S`fyms;SFk<3}og4r{UDxXNw+s(O5j0I7tyG-oTiC zKXR`H>R<6S@nz4^ZO1lq@StDi^z(ItX2y}Ktd^yjBTe~RUK(iFUdxr3jMbIacZ%$V z(3wTLPk+rK)XI+p8M!zYo>;{8s5jY8TEvc_DFa44 zrWEa(OgMn_9gO2DD5#Oql4oZSmq_l-Z34a3x%}>0oYE+X#D5mJOOa#onxrau>;bR} zVzz-<;Gw9F?-HS}h6JmMA;H7n&o5%?Gqm^LxVckKY_`cle3Q7(EccplmMjKGggr7` ze$u5q`}~z(H+d!eEn^z}r_Pjye5a!^fySL?a7;+Qw~5Ui%Xh6L6(4_MXHDDY8F32g#kt^z81NY4S%oVPO^)b}4Hrym#Q|n$9SCZgM z>|-wn?N?8xlrf@txvS}+s&9}(g{Z^k@bzzD6gO%4>I)n?M|%0bxq3TSi2@hnv17_O zz;Oty>GaIBRX)H6kGYM8u8xMe_XwpQXSLha5=SuT@t8~8m~hznIAAKXHOa6cA9hM| zKcv~Rd|$^|FW$+qJ{hFu-#ynZz?ur93_(efvTI;$MkB;*Hd2gD@=HeJ|m^-U_ll*#WRM%3_t&PI`)wv zm;3@~;U599a;uGHwt!>6Q5`SX+=M5+`~o}GQ}wKrW;n}9!bE_WI?EU1SxrJ>Eagqv zWF>ShO>R)l5>KM~};{Jr+xDTFU!ocj4 zZViw%V-7gUjy6dsUn$RYXp=i}Dr|@sG)jfJ7MQV?->%is_`X;v6;W?skLlT{CU>E# zkki_=?u`oNj&i_oRaM6#+eweLQwShJ$O$mUr^QPRim84f01c~cMKU-{rM)yTTBYCB ziy?~gT)DQ);(njMNA0s;&-;mCf97aMV~$;Jeyyj#Dn+zjE1q6k*MqV`37WNVDw)PV zGfrRcp0A9x!39PbZcAAej)_Y@mLwOtxUC8BtCVLSFD(4IrT^4ybZ|E>HMR+};Hn_O z-<8Gfg^GVOh1Msn2<){tH+68gb}i{GYWaP z0spxh&=@>nKH7(HzdG=Cq3QM3D^_z>GeBza^Lp^CkW*fH-^T((^`@vK-3 zH7IC0*EEN&=w8o>*i>Pr&Du&TjYf*vW00{>I9Q0|n*s;T9ou;9ReykUJpRyBX`F0g z7>m!ky60Y12Q*f=fK4AjKf4r#A2%LMV zG8SYJrITWDC+yeB1*LhLd=={36Mo2}F1IDJ^!k1AZJeX$-Ro?W7rAX{N+^?U381bn z%I{Tk+^Nhc^!{->;thoTV?+rJ{i6{(PpvX@hP!^*g6Z_9&-jEYs+^oVM)_i5nfb>z zQ>EWgCk~3<`FoXd`kKonk(Fn5@*kQ+FBdG=3*quUiE4|RB8YKUqx`a<0Y7tS*lgi5 zjFr|qr+dvusJ^#}rNrimx+YbKvDv?C zIbCS=X9B0DNp>2xrSq=I!VK3H=`*;9Z7={Gt`<7y&3qx9F9gT&Sbn#pmGLnC2f+TR z)mohhcS-ZWOP7}HHHVyxeRL1fi7ku?x~C5SK8`AK2dE(F3`Q1FX_cs>|JBddzp*=Y za7rDS!(+>i@_u`NW?#^}Ezv5)1d`YbfUef!2spt)ItS2a=b{2`fu;R^Zr-sN_!XD& zG_u1VcjXx%FJ65ib5|g#YK#K<=j%(WcN^R4n(z4L^Lga=1WK{m^!44jQEsSrbpk(x zXTbJhw}$z=m#fMiJdS8kKZ~miK!Qml#_O}OHUS78|LKJNa7;ybPX8{ye8+nswzqUZ zUS7-6y2a^PcauR@nr6evldn_IXIxEK-6`Jtw9j{SL{koy4l>SdbFkSjk=~c~{YZ&R zZ8Gyh(c&J&6LlZn`|gK_e^(Ihy(nmQK7&$2wV74j9SLLE_&rZsNckKw~u5s%0-F1sxUS_kPOuWAT3r*>IDU z3h&L=!;&|o^VY}T(&&uhb$iE2-mTPSUCyot3rDfMi|?z6s8Z>ZR7NOAjFL+bziyB; zi}P`nB7bImi8-E|EwVyKkRWxisYuMZ`UnVF%!Y58MN!?&^Dd{GN>;U6J`*xyWo5`u zYp8sG01BUUnii_gT`FEzu-rPl!zidT3r7oE`n7sXrOe zQ#x%Gj*w9*8VH`RPsq7g_~B&K!qz$)$)3 zZ_nw7@6XdF7h0PZs}~I$lh(Iw>YF6h}oHyDic-rr?Ba8p&Pd5rJ}it&L>JjcX#M0=ih7v>fFQsKSCIU>sPIzB}fgnq{eXByjwLP zt`X{ywG(w?>nSDNN8-M9|T?8}wbXxfqXOh83F{121H0vYD!6%{&qD(Uu zH47`m_V|QJQra2}DIsd`Fj(RVkef5kdRxc4I9!4&8U>X@BLMf{h*V44UcL<4!s}RB z9Nm#PM_X>1-Kv+NC_qK4dqm4gK;&EyvigohkT?BYQ>N`}GRj8WW4ePIiJJ#TS| z@cgh?J9|d4vmPG-Q=?2~Y4k|Z4o zG)V>Ui;L$`b1M2ga4*}?*47CRvHxsLNs?A~F|v56>=v_~)70J;KDWGV)P8B~fZumr zk;mwzcU8W>1Jr;MhX*Mf=B;KtTPkDUF1U)d{6Flyby!?mvp(2JaJS&vxH|+7!9ob` z8r&LpLa;yx?ykYz>BfQtcZW0@+)2>j)90K|&Uf$0y}vs%&-~_@`TpS9bobhOuf2Ay zTD7X)ddu`dUA9mVKYr0UGG1Ynd8*-y{haI+zQYdt)8tmvjd)1cE)&8yC`Yp;MS0qR zvp|03a-C;lj8Yn}+kkGMqo=aZCotM|)IFOx-oI{HPnmc8QnA@Y0VtCBDHdjd6d*+2 zYK=Ve3u)<&)t4b%+!;a(hc`Dl>fE3{3(sbv`QV>GTAbRuXpI1CyHP6M0JoiP(>{C5 zcc5@mWfm=8y`s$Wykf<0Kw}t7cEYP)XrsukVB+|=_q?BF<6#6jtM ztuXd1a{@X0>(X#3tz4U;(o*!Z4{1+eAa=Sky_dalHsH(hyb_QIK%N4~<`1$z}LN%mR} z%%636q;Ou2DQh=a7|t#rT%^N+=-Ss092vTMiL!7!s7mCmw zeXbAsipRe=HCeARCsdF>B%_{mFE+6FS=u}3VC~~OH z8V6yGEM=XsY};So;xi`P7pG1OUu272G^FgJBxL#t+q893$@1^9IyxkX4ar8Z1+YQ@ zyidm9#NlU8OvrvH?&brqniRCw~8|yhUyJiScMRe z1On-$R`P|kE)W=l_iYPE1!6$>U5*xx|14}N&*+Ny*y`0W=-{y$4@uaEaq5Mhu90*FJ2??3;*un=Z zO>|RysURxl&@sfOgp;1y64{$7F56)}y*6~)@A=PLJ^C~V$xv7Wr?;qa_rd{Z?6Pw= zldz;xMvZm^#Xt^FS7N3J9ruzlM?uN8KDGG|)t?`9n{}(ID~cgnb@7P6k=pL6F5;4i ziojqO9Y1WseQYZm&a7=IGQT_z#Ux~(09mq_RFk-1&7#Q$-;WQo&K8cqMUNA&nWo7_ zpcMgJr!fg%>ni>}ktbedH;_$GgiA9{c4y%=)|hiYz_ehVI%rXL22{y!<1pNXf9(QE z&Eo+8F?^67PF@RDOWTn#HVZ}}TDpWvaFwjqTo^8liM42Yx%tsTN!X7LbHka#cxYww zbfTq9kg&vd5%6Dy`-$|Z?{(N_Z=-_n-oD-%f_SJmWCw-{k;4mBIqHL646l>@ zQ-+Z&eUdny=awB!+U=?z5cS+w^07+5&bo|>*gpE&f#9*ULvj##7XI=xIr>m6Uf` z;vC;V)d(iK9&yi=)B=evxR0@mW$Y>XJmL?dJoco_ZD~>50ZS?Jj6{k7p65++)aKv4 zw#(%{?SAjPtkqd8;3?6TA`P)Xj3qWhgjXQB;Dg(q2Va%Hu}|h!cJ{R@dyixIvP$|s zYk?^dzuC!kaCu?9@sP71;g(Xz(Gqg2wk*0esJ_jDQTqC0Ed+h>LagnXWd?}ANgw)! z+^Hjtw2TSuC!I@xkPu_ssIc2DV&YixBwOUjRyULB2*C6;Z#)3;iU4lsnL$fZCMi-b zCkMyYOy!ay`Is(ANbAc2PDv>Pa~Ib!C7EKQ1i=pubhF!#9F+w38Fmhh9Qv=HmDf;~ zK*W@7;pek;^)ex9C^PyP&*1~N?poiy7B^1Zh+fhbwEAXTzmKzOuk>3ug% z;4wpril>-_dx*zF{2tM7EuZ>+-FpnvCoPC5%>l#KLX)LB5W;E_i*101B-f}(YLw0i zGlnW8Z1;BTjd`a}%wXw+-kI}YY%|KLn}W%e;nK=h{f}IMGGpURa*HTD%4)m`!FtLS zLoEhj^X3MR0MLCV<|9CSyzzdlxj$vGXw`QAf(pyVCxRprx7W%1!i`V8%m5QbUK4OjdmGjiXd zT(N-8zGoi2YEN6HSC$OqL}ce$0-99Wrz+m*K~$n}sd(ixLnXKwspm`BlhU6J#L8Aw zO);avVO=kEZj_sEUWn~8PMr^OK2?dIB5E;i<(A>W&Nt(O+tF4m8Q|cYR%?mst70eA ze2@uOAmR~-m7?aR&dxN=@b@m=%dwjRRaqs|lq_}w;|=8FhEZHT+nGl~&`>w6!q2~0 zSZd@2i4{q4lgVNPY)UA%$71S(v7$CTK+?!u9-LtT-P88=VJ5W*&ec=?{S)k z3(Vr-m_cBvjv1boQJV;2va;TQ=rz(96|hIRlKB4dxAmGi(1rr37A)jL_I5lot1x=X zn{dx*FCEmND&=U0;oSXTZd;N|w-z-7SX+yW8|T||yG7kdXFmEA$AMy_87J=o0MyXM z11MmAWhcF?(b4Ey_L+Hg1uKlEAEn?dSFEFa^BVB#%4-KH1Uqclq&^Dp%R|SqFx?&&g_-JyrE|V}1aS3afUShbcr&j`6O?Phx2nyWK3e&=Gs$4N?k|kG-&*@Z9bA2xZL0d8 zSdgiJ9(GphJW89WedI894t}7hUt{7qNk?$ zHI(;W<3@H35ih4{62~zMm}%%EDs0JsIIktgR`j&*#D&$&-cyzmG!iIxS%|i*e%@GBW7u>_|}8e?>8=o3blEP&!Af zCYOe*rNG@D0BBlC6Z-+JD^=rF&^LSOwxsU~V-;aVF!>*1A^$sg9>U|~AN%hTTMuKX zfU-@;@QTK9-@5~(y|_K?-CPW2H)DaR%lWbXMTRp^&~1ljq*YGqky6XV!z}%-cP-}b zb9F~nS`b?2PTXl^kOb-KrvPE^2AkyWJ*zh&&#Gp@QKr;+ODLOLb#R*-ky-}NEc42j z48@k>%LYN4sPtiSW8YU<5EjWZPhqGRRY_u>J`GZvUB()uD9{EJyByWRnQc{NJ@)DQS zuB+BzgLz%@%8SH~ID)v$Xx##n!>>mJqGs(Nn4fFWT0P=Lee}A3q&Zt%H&A_B*u)uv zqb+n0yr-|FRc>L0nO^IRhKRBJ8i{50KS2Th4xaq~B~WteZ(bV~Vft_!Qf&>ykSAdz zi~+*OzAA``8b)pm>W=MiCtAbJ9TqdKD4es;YZVE0fgy7*gy9l{goH$6q+vt_s%$?* ztKn;SWZ^815v-`PF)825y4n+a=Se4>G{nQ*^#T-s=Sh&+>D{@{a60oiSzL8+&KWEG zFFBlkfde8urngS=0O&4SkH3EZ2%yot8}2yie+0axdSJ=%p2NO@fi~*+1)7Uid1s)^ zI?6mJw?AOq8pw};@cfb1hdckPu1COnsdY)~u~v|GivMcN!%vAjO-hT^tBps%XY5A+ zp`y%Fzb{gv|GaL@pDt4PDdO|D-0VL!mrL`!hix!Uf*%3BYLhanP7}Yp>8BHjHRGR} zt8N=U0^Uj7t~~;NXf&vLF7e_2vyUZb9FKqws{8N$Kg;x(T8~Ll|J?eYU1ujSyuw8M zQ{zANJh!bmdT7dSP>LB5ME7L>d1R&)`48RXXTvLSY{`N|HHl}Udy!HC8=^;XVH?ow z_TIe4zrKm8p|+yGpv5NUX5`>Uf`lTW~-Km0m0wx ztm`u0XnkVxuScldz<2>l@KhW3xrLC@@l)r^!JY3#2DWcv&Ybr`1r~=6SxKCsk|?7K$DLeF*aVc_YEp z9LhjdShwc?dW`D(3Q=b`_%6wm+L(ETsX693B$Phz^XY}WzdZ7U0mLNJmT6m(h!2vH z5eV#-_}Kp`n}42GDvO5tMlTo}k{BY-y%u;L%xLrVZ=W^`C^+LnR1WFg(2T7d3Qa0AgGf~V1@iTPx;v$fm9 zFI0x6FzlktqZMG4f+0BW`VtfD=TgEu{M;>#8s4Pl==dNHHm=Hk#7og=CfMLG{Pi0u zi@3!XZhDIA{Y5y&=lI&^N9K=}FwR4VeD6vJ+9bLGOF1p{=w|94zdhajubwUeRJpKX z96=^bNkQkD~4{BD02){3^n#T%}CnjW_7v78U(YwM#ef!snzpTc1VX1pw zU>;Wy&&oBwoT=X+)`?q#X8!!^F%(LY?=?r5I#N|6kv3#Ty&+6+am}%hB)HAa8Hb?$*4Bz4ouHD2xR4Nk{7As?<;4Di6Rjo$c z-B}<9kl?_dziGu^C|7O8MPbu;ui~hUusFJM_%iJT$PNO<*;EPC4k~l>;WATD)i0Br zxBdy59~oot#czG8{A)GM!~7|mKOI>YH&in*2<4Ktu3_0zc_s<|W!%USHBLQe>I;W~ z*GoH89KY!yfj_OIf3^V1RvqeYwHMw^QbD0rPkTk~=9T?ZRSy zPtZ5{{SzWARhWuM@L#Mk$$=AMPZ^^r(15DMOad-pF$mVyD^NhTa zp*d`Aj{sa(xq)A;%Mlti>6eD$!inQ3PZH=vcZ#RUZ#2FeLJf{@`6h>JzBO^#Q@wR7 z+Td@01mLJeKzn{ENN-y{rx8}n=h#Gj0wXrdV#YXNt1o!+GhQa=T)MRia9V4%Po5zuw!#@iYHOX zp{t*vkDV^KwQu|jS!}p>RGMMWheUhLrW0muaJ1%YsjPC16C+nz`+ZtU{g9rPk^1oF z>_<+@e6-h*qS2PM;$OdYt>({l^-LT)(fBmbAX#+C4Mtpo2D|&RxZkr?HAoV>Su+)N z+N}2&_h1*F-I5;JpBJq^8+Te1Ct+-r{33?bgz@VKnT|5hW&}oO#DttgfI`=KCGiD? zxGpf1H8opa)?Cw_9&i9bi2Gfx!N2HN@+DO0{b$prbB+M6>+Yz2$7;%?6svV5mQzJV zM`1HaD_H;i$5aRC8qTx(V;|20nD&ED52mvDK<|s>(Fx-b86H z=A?L}qlB;=mMQgRA0wkppIF4wDp1ZiRt~nK7%0D)E>t|wCN*%FWX~$FFSNvo>K7@n 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 Bo8$lh literal 0 HcmV?d00001 diff --git a/logo.jpg b/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d8a5351537309d8069f5c2bf3de0daecf3b71c77 GIT binary patch literal 2285 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 literal 0 HcmV?d00001 diff --git a/project.nomad b/project.nomad new file mode 100644 index 0000000..2ff454b --- /dev/null +++ b/project.nomad @@ -0,0 +1,459 @@ +# Variables used below and their defaults if not set externally +variables { + # These all pass through from GitLab [build] phase. + # Some defaults filled in w/ example repo "bai" in group "internetarchive" + # (but all 7 get replaced during normal GitLab CI/CD from CI/CD variables). + CI_REGISTRY = "registry.gitlab.com" # registry hostname + CI_REGISTRY_IMAGE = "registry.gitlab.com/internetarchive/bai" # registry image location + CI_COMMIT_REF_SLUG = "master" # branch name, slugged + CI_COMMIT_SHA = "latest" # repo's commit for current pipline + CI_PROJECT_PATH_SLUG = "internetarchive-bai" # repo and group it is part of, slugged + + # NOTE: if repo is public, you can ignore these next 3 registry related vars + CI_REGISTRY_USER = "" # set for each pipeline and .. + CI_REGISTRY_PASSWORD = "" # .. allows pull from private registry + # optional CI/CD registry read token which allows rerun of deploy phase anytime later + CI_REGISTRY_READ_TOKEN = "" # preferred name + + + # This autogenerates from https://gitlab.com/internetarchive/nomad/-/blob/master/.gitlab-ci.yml + # & normally has "-$CI_COMMIT_REF_SLUG" appended, but is omitted for "main" or "master" branches. + # You should not change this. + SLUG = "internetarchive-bai" + + + # The remaining vars can be optionally set/overriden in a repo via CI/CD variables in repo's + # setting or repo's `.gitlab-ci.yml` file. + # Each CI/CD var name should be prefixed with 'NOMAD_VAR_'. + + # default 300 MB + MEMORY = 300 + # default 100 MHz + CPU = 100 + + # A repo can set this to "tcp" - can help for debugging 1st deploy + CHECK_PROTOCOL = "http" + # What path healthcheck should use and require a 200 status answer for succcess + CHECK_PATH = "/" + # Allow individual, periodic healthchecks this much time to answer with 200 status + CHECK_TIMEOUT = "2s" + # Dont start first healthcheck until container up at least this long (adjust for slow startups) + HEALTH_TIMEOUT = "20s" + + # How many running containers should you deploy? + # https://learn.hashicorp.com/tutorials/nomad/job-rolling-update + COUNT = 1 + + COUNT_CANARIES = 1 + + NETWORK_MODE = "bridge" + + NAMESPACE = "default" + + # only used for github repos + CI_GITHUB_IMAGE = "" + + CONSUL_PATH = "/usr/bin/consul" + + FORCE_PULL = false + + # For jobs with 2+ containers (and tasks) (so we can setup ports properly) + MULTI_CONTAINER = false + + # Persistent Volume - set to a (fully qualified) dest dir inside your container, if you need a PV. + # We suggest "/pv". + PERSISTENT_VOLUME = "" + + /* You can overrride this for type="batch" and "cron-like" jobs (they rerun periodically & exit). + Combine this var override, with a small `job.nomad` in your repo to setup a cron, + with contents in the file like this, to run every hour at 15m past the hour: + type = "batch" + periodic { + cron = "15 * * * * *" + prohibit_overlap = false # must be false cause of kv env vars task + } + */ + IS_BATCH = false + + # There are more variables immediately after this - but they are "lists" or "maps" and need + # special definitions to not have defaults or overrides be treated as strings. +} + +variable "PORTS" { + # You must have at least one key/value pair, with a single value of 'http'. + # Each value is a string that refers to your port later in the project jobspec. + # + # Note: use -1 for your port to tell nomad & docker to *dynamically* assign you a random high port + # then your repo can read the environment variable: NOMAD_PORT_http upon startup to know + # what your main daemon HTTP listener should listen on. + # + # Note: if your port *only* talks TCP directly (or some variant of it, like IRC) and *not* HTTP, + # then make your port number (key) *negative AND less than -1*. + # Don't worry -- we'll use the abs() of it; + # negative numbers makes them easily identifiable and partition-able below ;-) + # + # Note: if you want an extra port to only use HTTP and not HTTPS, add 10000 to your desired + # port number (so for 18989, the public url will be http://... mapped internally to :8989 ). + # + # Examples: + # NOMAD_VAR_PORTS='{ 5000 = "http" }' + # NOMAD_VAR_PORTS='{ -1 = "http" }' + # NOMAD_VAR_PORTS='{ 5000 = "http", 666 = "cool-ness" }' + # NOMAD_VAR_PORTS='{ 8888 = "http", 8012 = "backend", 7777 = "extra-service" }' + # NOMAD_VAR_PORTS='{ 5000 = "http", -7777 = "irc" }' + # NOMAD_VAR_PORTS='{ 5000 = "http", 18989 = "db" }' + type = map(string) + default = { 5000 = "http" } +} + +variable "HOSTNAMES" { + # This autogenerates from https://gitlab.com/internetarchive/nomad/-/blob/master/.gitlab-ci.yml + # but you can override to 1 or more custom hostnames if desired, eg: + # NOMAD_VAR_HOSTNAMES='["www.example.com", "site.example.com"]' + type = list(string) + default = ["group-project-branch-slug.example.com"] +} + +variable "VOLUMES" { + # Pass in a list of [host VM => container] direct pass through of volumes, eg: + # NOMAD_VAR_VOLUMES='["/usr/games:/usr/games:ro"]' + type = list(string) + default = [] +} + +variable "NOMAD_SECRETS" { + # this is automatically populated with NOMAD_SECRET_ env vars by @see .gitlab-ci.yml + type = map(string) + default = {} +} + + +locals { + # Ignore all this. really :) + + # Copy hashmap, but remove map key/val for the main/default port (defaults to 5000). + # Then split hashmap in two: one for HTTP port mappings; one for TCP (only; rare) port mappings. + ports_main = {for k, v in var.PORTS: k => v if v == "http"} + ports_extra_tmp = {for k, v in var.PORTS: k => v if v != "http"} + ports_extra_tmp2 = {for k, v in local.ports_extra_tmp: k => v if k > -2} + ports_extra_https = {for k, v in local.ports_extra_tmp2: k => v if k < 10000} + ports_extra_http = {for k, v in local.ports_extra_tmp: abs(k - 10000) => v if k > 10000} + ports_extra_tcp = {for k, v in local.ports_extra_tmp: abs(k) => v if k < -1} + # 1st docker container configures all ports *unless* MULTI_CONTAINER is true, then just main port + ports_docker = values(var.MULTI_CONTAINER ? local.ports_main : var.PORTS) + + # Now create a hashmap of *all* ports to be used, but abs() any portnumber key < -1 + ports_all = merge(local.ports_main, local.ports_extra_https, local.ports_extra_http, local.ports_extra_tcp, {}) + + # Use CI_GITHUB_IMAGE if set, otherwise use GitLab vars interpolated string + docker_image = var.CI_GITHUB_IMAGE != "" ? var.CI_GITHUB_IMAGE : "${var.CI_REGISTRY_IMAGE}/${var.CI_COMMIT_REF_SLUG}:${var.CI_COMMIT_SHA}" + # " + + # GitLab docker login user/pass timeout rather quickly. If admin set CI_REGISTRY_READ_TOKEN key + # in the group/repo [Settings] [CI/CD] [Variables] - then use a token-based alternative to deploy. + # Effectively, use CI_REGISTRY_READ_TOKEN variant if set; else use CI_REGISTRY_* PAIR + docker_user = var.CI_REGISTRY_READ_TOKEN != "" ? "deploy-token" : var.CI_REGISTRY_USER + docker_pass = [for s in [var.CI_REGISTRY_READ_TOKEN, var.CI_REGISTRY_PASSWORD] : s if s != ""] + # Make [true] (array of length 1) if all docker password vars are "" + docker_no_login = length(local.docker_pass) > 0 ? [] : [true] + + + # If job is using secrets and CI/CD Variables named like "NOMAD_SECRET_*" then set this + # string to a KEY=VAL line per CI/CD variable. If job is not using secrets, set to "". + kv = join("\n", [for k, v in var.NOMAD_SECRETS : join("", concat([k, "='", v, "'"]))]) + + volumes = concat( + var.VOLUMES, + var.PERSISTENT_VOLUME == "" ? [] : ["/pv/${var.CI_PROJECT_PATH_SLUG}:${var.PERSISTENT_VOLUME}"], + ) + + auto_promote = var.COUNT_CANARIES > 0 ? true : false + + # make boolean-like array that can logically omit 2 `dynamic` blocks below for type=batch + service_type = var.IS_BATCH ? [] : ["service"] + + # split the 1st hostname into non-domain and domain parts + host0parts = split(".", var.HOSTNAMES[0]) + host0 = local.host0parts[0] + host0domain = join(".", slice(local.host0parts, 1, length(local.host0parts))) + + legacy = var.CI_PROJECT_PATH_SLUG == "www-dweb-ipfs" ? true : (var.CI_PROJECT_PATH_SLUG == "www-dweb-webtorrent" ? true : false) # xxx + + legacy2 = local.host0domain == "staging.archive.org" || local.host0domain == "prod.archive.org" || var.HOSTNAMES[0] == "polyfill.archive.org" || var.HOSTNAMES[0] == "esm.archive.org" || var.HOSTNAMES[0] == "purl.archive.org" || var.HOSTNAMES[0] == "popcorn.archive.org" # xxx + + tags = local.legacy2 ? merge( + {for portnum, portname in local.ports_extra_https: portname => [ + # If the main deploy hostname is `card.example.com`, and a 2nd port is named `backend`, + # then make its hostname be `card-backend.example.com` + "urlprefix-${local.host0}-${portname}.${local.host0domain}" + ]}, + {for portnum, portname in local.ports_extra_http: portname => [ + "urlprefix-${local.host0}-${portname}.${local.host0domain} proto=http" + ]}, + {for portnum, portname in local.ports_extra_tcp: portname => [ + "urlprefix-:${portnum} proto=tcp" + ]}, + ) : merge( + {for portnum, portname in local.ports_extra_https: portname => [ + # If the main deploy hostname is `card.example.com`, and a 2nd port is named `backend`, + # then make its hostname be `card-backend.example.com` + local.legacy ? "https://${var.HOSTNAMES[0]}:${portnum}" : "https://${local.host0}-${portname}.${local.host0domain}" // xxx + ]}, + {for portnum, portname in local.ports_extra_http: portname => [ + "http://${local.host0}-${portname}.${local.host0domain}" + ]}, + {for portnum, portname in local.ports_extra_tcp: portname => []}, + ) +} + + +# VARS.NOMAD--INSERTS-HERE + + +# NOTE: for main or master branch: NOMAD_VAR_SLUG === CI_PROJECT_PATH_SLUG +job "NOMAD_VAR_SLUG" { + datacenters = ["dc1"] + namespace = "${var.NAMESPACE}" + + dynamic "update" { + for_each = local.service_type + content { + # https://learn.hashicorp.com/tutorials/nomad/job-rolling-update + max_parallel = 1 + # https://learn.hashicorp.com/tutorials/nomad/job-blue-green-and-canary-deployments + canary = var.COUNT_CANARIES + auto_promote = local.auto_promote + min_healthy_time = "30s" + healthy_deadline = "10m" + progress_deadline = "11m" + auto_revert = true + } + } + + dynamic "group" { + for_each = [ "${var.SLUG}" ] + labels = ["${group.value}"] + content { + count = var.COUNT + + restart { + attempts = 3 + delay = "15s" + interval = "30m" + mode = "fail" + } + network { + dynamic "port" { + # port.key == portnumber + # port.value == portname + for_each = local.ports_all + labels = [ "${port.value}" ] + content { + to = port.key + } + } + } + + + # The "service" stanza instructs Nomad to register this task as a service + # in the service discovery engine, which is currently Consul. This will + # make the service addressable after Nomad has placed it on a host and + # port. + # + # For more information and examples on the "service" stanza, please see + # the online documentation at: + # + # https://www.nomadproject.io/docs/job-specification/service.html + # + service { + name = "${var.SLUG}" + task = "http" + + tags = [for HOST in var.HOSTNAMES: local.legacy2 ? "urlprefix-${HOST}" : "https://${HOST}"] + + canary_tags = [for HOST in var.HOSTNAMES: "https://canary-${HOST}"] + + port = "http" + check { + name = "alive" + type = "${var.CHECK_PROTOCOL}" + path = "${var.CHECK_PATH}" + port = "http" + interval = "10s" + timeout = "${var.CHECK_TIMEOUT}" + check_restart { + limit = 3 # auto-restart task when healthcheck fails 3x in a row + + # give container (eg: having issues) custom time amount to stay up for debugging before + # 1st health check (eg: "3600s" value would be 1hr) + grace = "${var.HEALTH_TIMEOUT}" + } + } + } + + dynamic "service" { + for_each = merge(local.ports_extra_https, local.ports_extra_http, local.ports_extra_tcp) + content { + # service.key == portnumber + # service.value == portname + name = "${var.SLUG}--${service.value}" + task = var.MULTI_CONTAINER ? service.value : "http" + # NOTE: Empty tags list if MULTI_CONTAINER (private internal ports like DB) + tags = var.MULTI_CONTAINER ? [] : local.tags[service.value] + + port = "${service.value}" + check { + name = "alive" + type = "tcp" + path = "${var.CHECK_PATH}" + port = "${service.value}" + interval = "10s" + timeout = "${var.CHECK_TIMEOUT}" + } + check_restart { + grace = "${var.HEALTH_TIMEOUT}" + } + } + } + + task "http" { + driver = "docker" + + # UGH - have to copy/paste this next block twice -- first for no docker login needed; + # second for docker login needed (job spec will assemble in just one). + # This is because we can't put dynamic content *inside* the 'config { .. }' stanza. + dynamic "config" { + for_each = local.docker_no_login + content { + image = "${local.docker_image}" + image_pull_timeout = "20m" + network_mode = "${var.NETWORK_MODE}" + ports = local.ports_docker + volumes = local.volumes + force_pull = var.FORCE_PULL + memory_hard_limit = "${var.MEMORY * 10}" # NOTE: not podman driver compatible + } + } + dynamic "config" { + for_each = slice(local.docker_pass, 0, min(1, length(local.docker_pass))) + content { + image = "${local.docker_image}" + image_pull_timeout = "20m" + network_mode = "${var.NETWORK_MODE}" + ports = local.ports_docker + volumes = local.volumes + force_pull = var.FORCE_PULL + memory_hard_limit = "${var.MEMORY * 10}" # NOTE: not podman driver compatible + + auth { + # server_address = "${var.CI_REGISTRY}" + username = local.docker_user + password = "${config.value}" + } + } + } + + resources { + # The MEMORY var now becomes a **soft limit** + # We will 10x that for a **hard limit** + cpu = "${var.CPU}" + memory = "${var.MEMORY}" + memory_max = "${var.MEMORY * 10}" + } + + + dynamic "template" { + # Secrets get stored in consul kv store, with the key [SLUG], when your project has set a + # CI/CD variable like NOMAD_SECRET_[SOMETHING]. + # Setup the nomad job to dynamically pull secrets just before the container starts - + # and insert them into the running container as environment variables. + for_each = slice(keys(var.NOMAD_SECRETS), 0, min(1, length(keys(var.NOMAD_SECRETS)))) + content { + change_mode = "noop" + destination = "secrets/kv.env" + env = true + data = "{{ key \"${var.SLUG}\" }}" + } + } + + template { + # Pass in useful hostname(s), repo & branch info to container's runtime as env vars + change_mode = "noop" + destination = "secrets/ci.env" + env = true + data = <&1 | tee $LOG + set -e + while [ $# -gt 0 ]; do + EXPECT=$1 + shift + grep "$EXPECT" $LOG + done +} + +function tags() { + STR=$(jq -cr '[..|objects|.Tags//empty]' /tmp/project.json) + if [ "$STR" != "$1" ]; then + set +x + echo "services tags: $STR not expected: $1" + exit 1 + fi +} + +function ctags() { + STR=$(jq -cr '[..|objects|.CanaryTags//empty]' /tmp/project.json) + if [ "$STR" != "$1" ]; then + set +x + echo "services canary tags: $STR not expected: $1" + exit 1 + fi +} + +function slug() { + STR=$(jq -cr '.Job.ID' /tmp/project.json) + if [ "$STR" != "$1" ]; then + set +x + echo "slug/job name: $STR not expected: $1" + exit 1 + fi +} + +function prodtest() { + CI_PROJECT_NAME=$(echo "$CI_PROJECT_PATH_SLUG" |cut -f2- -d-) + BASE_DOMAIN=${BASE_DOMAIN:-"prod.archive.org"} # default to prod.archive.org unless caller set it + NOMAD_TOKEN_PROD=test + expects "deploying to https://$CI_HOSTNAME" +} + +# test various deploy scenarios (verify expected hostname and cluster get used) +# NOTE: the CI_ * vars are normally auto-poplated by CI/CD GL (gitlab) yaml setup +# NOTE: the GITHUB_* vars are normally auto-poplated in CI/CD GH Actions by GH (github) +( + banner GL to dev + BASE_DOMAIN=dev.archive.org + CI_PROJECT_NAME=av + CI_COMMIT_REF_SLUG=main + CI_PROJECT_PATH_SLUG=www-$CI_PROJECT_NAME + expects 'nomad cluster https://dev.archive.org' \ + 'deploying to https://www-av.dev.archive.org' + tags '[["https://www-av.dev.archive.org"]]' + ctags '[["https://canary-www-av.dev.archive.org"]]' + slug www-av +) +( + banner GL to dev, custom hostname + BASE_DOMAIN=dev.archive.org + CI_PROJECT_NAME=av + CI_COMMIT_REF_SLUG=main + CI_PROJECT_PATH_SLUG=www-$CI_PROJECT_NAME + NOMAD_VAR_HOSTNAMES='["av"]' + expects 'nomad cluster https://dev.archive.org' \ + 'deploying to https://av.dev.archive.org' + tags '[["https://av.dev.archive.org"]]' + ctags '[["https://canary-av.dev.archive.org"]]' + slug www-av +) +( + echo GL to prod, via alt/unusual branch name, custom hostname + BASE_DOMAIN=prod.archive.org + CI_PROJECT_NAME=av + CI_COMMIT_REF_SLUG=avinfo + CI_PROJECT_PATH_SLUG=www-$CI_PROJECT_NAME + NOMAD_VAR_HOSTNAMES='["avinfo"]' + NOMAD_TOKEN_PROD=test + expects 'nomad cluster https://prod.archive.org' \ + 'deploying to https://avinfo.prod.archive.org' \ + 'using nomad production token' + tags '[["urlprefix-avinfo.prod.archive.org"]]' + ctags '[["https://canary-avinfo.prod.archive.org"]]' + slug www-av-avinfo +) +( + echo GL to prod, via alt/unusual branch name, custom hostname + BASE_DOMAIN=prod.archive.org + CI_PROJECT_NAME=plausible + CI_COMMIT_REF_SLUG=plausible-ait + CI_PROJECT_PATH_SLUG=services-$CI_PROJECT_NAME + NOMAD_VAR_HOSTNAMES='["plausible-ait"]' + NOMAD_TOKEN_PROD=test + expects 'nomad cluster https://prod.archive.org' \ + 'deploying to https://plausible-ait.prod.archive.org' \ + 'using nomad production token' +) +( + echo GL to dev, branch, so custom hostname ignored + banner GL to dev, w/ 2+ custom hostnames + BASE_DOMAIN=dev.archive.org + CI_PROJECT_NAME=av + CI_COMMIT_REF_SLUG=main + CI_PROJECT_PATH_SLUG=www-$CI_PROJECT_NAME + NOMAD_VAR_HOSTNAMES='["av1", "av2.dweb.me", "poohbot.com"]' + expects 'nomad cluster https://dev.archive.org' \ + 'deploying to https://av1.dev.archive.org' + # NOTE: subtle -- with multiple names to single port deploy, we expect a list of 3 hostnames + # applying to *one* service + tags '[["https://av1.dev.archive.org","https://av2.dweb.me","https://poohbot.com"]]' + ctags '[["https://canary-av1.dev.archive.org","https://canary-av2.dweb.me","https://canary-poohbot.com"]]' +) +( + banner GL to dev, branch, so custom hostname ignored + BASE_DOMAIN=dev.archive.org + CI_PROJECT_NAME=av + CI_COMMIT_REF_SLUG=tofu + CI_PROJECT_PATH_SLUG=www-$CI_PROJECT_NAME + NOMAD_VAR_HOSTNAMES='["av"]' + expects 'nomad cluster https://dev.archive.org' \ + 'deploying to https://www-av-tofu.dev.archive.org' + slug www-av-tofu +) +( + banner GL to prod + BASE_DOMAIN=dev.archive.org + CI_PROJECT_NAME=plausible + CI_COMMIT_REF_SLUG=production + CI_PROJECT_PATH_SLUG=services-$CI_PROJECT_NAME + NOMAD_TOKEN_PROD=test + expects 'nomad cluster https://prod.archive.org' \ + 'deploying to https://plausible.prod.archive.org' \ + 'using nomad production token' + tags '[["urlprefix-plausible.prod.archive.org"]]' + ctags '[["https://canary-plausible.prod.archive.org"]]' +) +( + banner GL to ext + BASE_DOMAIN=dev.archive.org + CI_PROJECT_NAME=av + CI_COMMIT_REF_SLUG=ext + CI_PROJECT_PATH_SLUG=www-$CI_PROJECT_NAME + NOMAD_TOKEN_EXT=test + expects 'nomad cluster https://ext.archive.org' \ + 'deploying to https://av.ext.archive.org' \ + 'using nomad ext token' + tags '[["https://av.ext.archive.org"]]' + ctags '[["https://canary-av.ext.archive.org"]]' +) +( + banner GL to prod, custom hostname + BASE_DOMAIN=dev.archive.org + CI_PROJECT_NAME=plausible + CI_COMMIT_REF_SLUG=production + CI_PROJECT_PATH_SLUG=services-$CI_PROJECT_NAME + NOMAD_VAR_HOSTNAMES='["plausible-ait.prod.archive.org"]' + NOMAD_TOKEN_PROD=test + expects 'nomad cluster https://prod.archive.org' \ + 'deploying to https://plausible-ait.prod.archive.org' \ + 'using nomad production token' +) +( + banner GH to dev + GITHUB_ACTIONS=1 + GITHUB_REPOSITORY=internetarchive/emularity-engine + GITHUB_REF_NAME=tofu + BASE_DOMAIN=dev.archive.org + expects 'nomad cluster https://dev.archive.org' \ + 'deploying to https://internetarchive-emularity-engine-tofu.dev.archive.org' +) +( + banner GH to staging + GITHUB_ACTIONS=1 + GITHUB_REPOSITORY=internetarchive/emularity-engine + GITHUB_REF_NAME=staging + BASE_DOMAIN=dev.archive.org + NOMAD_TOKEN_PROD=test + expects 'nomad cluster https://staging.archive.org' \ + 'deploying to https://emularity-engine.staging.archive.org' +) +( + banner GH to production + GITHUB_ACTIONS=1 + GITHUB_REPOSITORY=internetarchive/emularity-engine + GITHUB_REF_NAME=production + BASE_DOMAIN=dev.archive.org + NOMAD_TOKEN_PROD=test + expects 'nomad cluster https://ux-b.archive.org' \ + 'deploying to https://emularity-engine.ux-b.archive.org' \ + 'using nomad production token' +) +( + banner "GL repo using 'main' branch to be like 'production'" + BASE_DOMAIN=prod.archive.org + CI_PROJECT_NAME=offshoot + CI_COMMIT_REF_SLUG=main + CI_PROJECT_PATH_SLUG=www-$CI_PROJECT_NAME + NOMAD_TOKEN_PROD=test + NOMAD_VAR_HOSTNAMES='["offshoot"]' + expects 'nomad cluster https://prod.archive.org' \ + 'deploying to https://offshoot.prod.archive.org' + slug www-offshoot +) +( + banner GL repo using one HTTP-only port and 2+ ports/names, to dev + BASE_DOMAIN=dev.archive.org + CI_PROJECT_NAME=lcp + CI_COMMIT_REF_SLUG=main + CI_PROJECT_PATH_SLUG=services-$CI_PROJECT_NAME + NOMAD_VAR_PORTS='{ 9999 = "http" , 18989 = "lcp", 8990 = "lsd" }' + expects 'nomad cluster https://dev.archive.org' \ + 'deploying to https://services-lcp.dev.archive.org' + # NOTE: subtle -- with multiple ports (one thus one service per port), we expect 3 services + # eacho with its own hostname + tags '[["https://services-lcp.dev.archive.org"],["http://services-lcp-lcp.dev.archive.org"],["https://services-lcp-lsd.dev.archive.org"]]' + ctags '[["https://canary-services-lcp.dev.archive.org"]]' +) +( + banner GL repo using one HTTP-only port and 2+ ports/names, to prod + BASE_DOMAIN=dev.archive.org + CI_PROJECT_NAME=lcp + CI_COMMIT_REF_SLUG=production + CI_PROJECT_PATH_SLUG=services-$CI_PROJECT_NAME + NOMAD_VAR_PORTS='{ 9999 = "http" , 18989 = "lcp", 8990 = "lsd" }' + NOMAD_TOKEN_PROD=test + expects 'nomad cluster https://prod.archive.org' \ + 'deploying to https://lcp.prod.archive.org' \ + 'using nomad production token' + # NOTE: subtle -- with multiple ports (one thus one service per port), we expect 3 services + # eacho with its own hostname + tags '[["urlprefix-lcp.prod.archive.org"],["urlprefix-lcp-lcp.prod.archive.org proto=http"],["urlprefix-lcp-lsd.prod.archive.org"]]' + ctags '[["https://canary-lcp.prod.archive.org"]]' +) +( + banner GL repo using one TCP-only port and 2+ ports/names + BASE_DOMAIN=dev.archive.org + CI_PROJECT_NAME=scribe-c2 + CI_COMMIT_REF_SLUG=main + CI_PROJECT_PATH_SLUG=services-$CI_PROJECT_NAME + NOMAD_VAR_PORTS='{ 9999 = "http" , -7777 = "tcp", 8889 = "reg" }' + expects 'nomad cluster https://dev.archive.org' \ + 'deploying to https://services-scribe-c2.dev.archive.org' + # NOTE: subtle -- with multiple ports (one thus one service per port), we'd normally expect 3 services + # eacho with its own hostname -- but one is TCP so the middle Service gets an *empty* list of tags. + tags '[["https://services-scribe-c2.dev.archive.org"],[],["https://services-scribe-c2-reg.dev.archive.org"]]' + ctags '[["https://canary-services-scribe-c2.dev.archive.org"]]' +) + + +# a bunch of quick, simple production deploy tests validating hostnames +( + CI_PROJECT_PATH_SLUG=services-article-exchange + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=article-exchange.prod.archive.org + prodtest +) +( + CI_PROJECT_PATH_SLUG=services-atlas + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=atlas.prod.archive.org + prodtest +) +( + CI_PROJECT_PATH_SLUG=services-bwhogs + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=bwhogs.prod.archive.org + prodtest +) +( + CI_PROJECT_PATH_SLUG=services-ids-logic + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=ids-logic.prod.archive.org + prodtest +) +( + CI_PROJECT_PATH_SLUG=services-lcp + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=lcp.prod.archive.org + prodtest +) +( + CI_PROJECT_PATH_SLUG=services-microfilmmonitor + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=microfilmmonitor.prod.archive.org + prodtest +) +( + CI_PROJECT_PATH_SLUG=services-oclc-ill + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=oclc-ill.prod.archive.org + prodtest +) +( + CI_PROJECT_PATH_SLUG=services-odyssey + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=odyssey.prod.archive.org + prodtest +) +( + CI_PROJECT_PATH_SLUG=services-opds + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=opds.prod.archive.org + prodtest +) +( + CI_PROJECT_PATH_SLUG=services-plausible + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=plausible.prod.archive.org + prodtest +) +( + CI_PROJECT_PATH_SLUG=services-rapid-slackbot + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=rapid-slackbot.prod.archive.org + prodtest +) +( + CI_PROJECT_PATH_SLUG=services-scribe-serial-helper + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=scribe-serial-helper.prod.archive.org + prodtest +) +( + CI_PROJECT_PATH_SLUG=www-av + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=av.prod.archive.org + prodtest +) +( + CI_PROJECT_PATH_SLUG=www-bookserver + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=bookserver.prod.archive.org + prodtest +) +( + CI_PROJECT_PATH_SLUG=www-iiif + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=iiif.prod.archive.org + prodtest +) +( + CI_PROJECT_PATH_SLUG=www-nginx + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=nginx.prod.archive.org + prodtest +) +( + CI_PROJECT_PATH_SLUG=www-rendertron + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=rendertron.prod.archive.org + prodtest +) + + +# a bunch of quick, _custom HOSTNAMES_, production deploy tests validating hostnames +( + NOMAD_VAR_HOSTNAMES='["popcorn.archive.org"]' + CI_PROJECT_PATH_SLUG=www-popcorn + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=popcorn.archive.org + prodtest +) +( + NOMAD_VAR_HOSTNAMES='["polyfill.archive.org"]' + CI_PROJECT_PATH_SLUG=www-polyfill-io-production + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=polyfill.archive.org + prodtest +) +( + NOMAD_VAR_HOSTNAMES='["purl.archive.org"]' + CI_PROJECT_PATH_SLUG=www-purl + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=purl.archive.org + prodtest +) +( + NOMAD_VAR_HOSTNAMES='["esm.archive.org"]' + CI_PROJECT_PATH_SLUG=www-esm + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=esm.archive.org + prodtest +) +( + NOMAD_VAR_HOSTNAMES='["cantaloupe.prod.archive.org"]' + CI_PROJECT_PATH_SLUG=services-ia-iiif-cantaloupe-experiment + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=cantaloupe.prod.archive.org + prodtest +) +( + NOMAD_VAR_HOSTNAMES='["plausible-ait.prod.archive.org"]' + CI_PROJECT_PATH_SLUG=services-plausible + CI_COMMIT_REF_SLUG=production-ait + CI_HOSTNAME=plausible-ait.prod.archive.org + prodtest +) +( + NOMAD_VAR_HOSTNAMES='["parse_dates"]' + CI_PROJECT_PATH_SLUG=services-parse-dates + CI_COMMIT_REF_SLUG=production + CI_HOSTNAME=parse_dates.prod.archive.org + prodtest +) + +banner SUCCESS diff --git a/vsync b/vsync new file mode 100755 index 0000000..cafd07c --- /dev/null +++ b/vsync @@ -0,0 +1,10 @@ +#!/bin/zsh -e + +# Shell script version of `nom-cp` alias +# Typically used with `sync-rsync` extension to VSCode. + +mydir=${0:a:h} + +source $mydir/aliases + +nom-cp "$@" From 4dcb64d2a791f37c1c173d859002aca114fbbe75 Mon Sep 17 00:00:00 2001 From: Tracey Jaquith Date: Mon, 30 Dec 2024 13:14:58 -0500 Subject: [PATCH 09/10] fixes --- Caddyfile | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Caddyfile b/Caddyfile index e78d078..c7f80a1 100644 --- a/Caddyfile +++ b/Caddyfile @@ -2,7 +2,7 @@ admin off } -:8888 { +:5000 { # We answer all requests this CI/CD yaml file from this repo file_server rewrite * /.gitlab-ci.yml diff --git a/Dockerfile b/Dockerfile index 6f19747..3c8fd2a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,4 +19,4 @@ COPY build.sh deploy.sh / USER deno -CMD ["/usr/bin/caddy", "run"] +CMD ["/usr/sbin/caddy", "run"] From 170901f6488efcbb4bbc9220593a8de317eea803 Mon Sep 17 00:00:00 2001 From: Tracey Jaquith Date: Mon, 30 Dec 2024 13:30:43 -0500 Subject: [PATCH 10/10] smoothing out --- Caddyfile | 2 +- Dockerfile | 2 +- README.md | 8 ++- build.yml | 23 ------- ci.yml | 113 ----------------------------------- .gitlab-ci.yml => gitlab.yml | 0 6 files changed, 9 insertions(+), 139 deletions(-) delete mode 100644 build.yml delete mode 100644 ci.yml rename .gitlab-ci.yml => gitlab.yml (100%) diff --git a/Caddyfile b/Caddyfile index c7f80a1..9b33306 100644 --- a/Caddyfile +++ b/Caddyfile @@ -5,5 +5,5 @@ :5000 { # We answer all requests this CI/CD yaml file from this repo file_server - rewrite * /.gitlab-ci.yml + rewrite * /gitlab.yml } diff --git a/Dockerfile b/Dockerfile index 3c8fd2a..9bf0f5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN mkdir -m777 /usr/local/sbin && \ ln -s /usr/bin/podman /usr/bin/docker WORKDIR /app -COPY .gitlab-ci.yml Caddyfile . +COPY gitlab.yml Caddyfile ./ COPY build.sh deploy.sh / diff --git a/README.md b/README.md index 1cb9f70..2e2ee5e 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,14 @@ Uses: - simply make your project have this simple `.gitlab-ci.yml` in top-level dir: ```yaml include: - - remote: 'https://gitlab.com/internetarchive/nomad/-/raw/master/.gitlab-ci.yml' + - remote: 'https://nomad.archive.org' ``` +*OR* +```yaml +include: + - remote: 'https://raw.githubusercontent.com/internetarchive/nomad/refs/heads/main/gitlab.yml' +``` + - if you want a [test] phase, you can add this to the `.gitlab-ci.yml` file above: ```yaml test: diff --git a/build.yml b/build.yml deleted file mode 100644 index 66e2624..0000000 --- a/build.yml +++ /dev/null @@ -1,23 +0,0 @@ -# Tracey 3/2024: -# This was adapted & simplified from: -# https://gitlab.com/gitlab-org/gitlab/-/raw/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml - -build: - stage: build - # If need to rebuild this image while runners are down, `cd` to this directory, then, as root: - # podman login registry.gitlab.com - # podman build --net=host --tag registry.gitlab.com/internetarchive/nomad/master . && sudo podman push registry.gitlab.com/internetarchive/nomad/master - image: registry.gitlab.com/internetarchive/nomad/master - variables: - DOCKER_HOST: 'unix:///run/podman/podman.sock' - DOCKER_TLS_CERTDIR: '' - DOCKER_BUILDKIT: 1 - script: - - /build.sh - artifacts: - reports: - dotenv: gl-auto-build-variables.env - rules: - - if: '$BUILD_DISABLED' - when: never - - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' diff --git a/ci.yml b/ci.yml deleted file mode 100644 index c2ace58..0000000 --- a/ci.yml +++ /dev/null @@ -1,113 +0,0 @@ -# NOTE: keep in mind this file is _included_ by _other_ repos, and thus the env var names -# are not _always_ related to _this_ repo ;-) - -# A GitLab group (ideally) or project that wants to deploy to a nomad cluster, -# will need to set [Settings] [CI/CD] [Variables] -# NOMAD_ADDR -# NOMAD_TOKEN -# to whatever your Nomad cluster was setup to. - - -# NOTE: very first pipeline, the [build] below will make sure this is created -image: registry.gitlab.com/internetarchive/nomad/master - -stages: - - build - - test - - deploy - - cleanup - -build: - # Tracey 3/2024: - # This was adapted & simplified from: - # https://gitlab.com/gitlab-org/gitlab/-/raw/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml - stage: build - # If need to rebuild this image while runners are down, `cd` to this directory, then, as root: - # podman login registry.gitlab.com - # podman build --net=host --tag registry.gitlab.com/internetarchive/nomad/master . && sudo podman push registry.gitlab.com/internetarchive/nomad/master - image: registry.gitlab.com/internetarchive/nomad/master - variables: - DOCKER_HOST: 'unix:///run/podman/podman.sock' - DOCKER_TLS_CERTDIR: '' - DOCKER_BUILDKIT: 1 - script: - - /build.sh - artifacts: - reports: - dotenv: gl-auto-build-variables.env - rules: - - if: '$BUILD_DISABLED' - when: never - - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' - -test-ourself: - stage: test - image: ${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}:${CI_COMMIT_SHA} - script: - - env -i zsh -euax test/test.sh - rules: - - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - when: never - - if: '$CI_PROJECT_PATH_SLUG == "internetarchive-nomad"' - -deploy: - stage: deploy - script: - # https://gitlab.com/internetarchive/nomad/-/blob/master/deploy.sh - - /deploy.sh - environment: - name: $CI_COMMIT_REF_SLUG - url: https://$HOSTNAME - on_stop: stop_review - rules: - - if: '$NOMAD_VAR_NO_DEPLOY' - when: never - - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - when: never - - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' - -deploy-serverless: - stage: deploy - script: - - | - if [[ -n "$CI_REGISTRY" && -n "$CI_REGISTRY_USER" ]]; then - echo "Logging in to GitLab Container Registry with CI credentials..." - - # this filters stderr of `podman login`, w/o merging stdout & stderr together - set +x - { echo "$CI_REGISTRY_PASSWORD" | podman --remote login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" 2>&1 1>&3 | ( grep -E -v "^WARNING! Your password will be stored unencrypted in |^Configure a credential helper to remove this warning. See|^https://docs.docker.com/engine/reference/commandline/login/#credentials-store" || true ) 1>&2; } 3>&1 - fi - - set -x - image_tagged="$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA" - image_latest="$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:latest" - podman --remote tag $image_tagged $image_latest - podman --remote push $image_latest - rules: - - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - when: never - - if: '$CI_COMMIT_BRANCH && $NOMAD_VAR_SERVERLESS' - - -stop_review: - # See: - # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml - stage: cleanup - variables: - GIT_STRATEGY: none - script: - - /deploy.sh stop - environment: - name: $CI_COMMIT_REF_SLUG - action: stop - dependencies: [] - allow_failure: true - rules: - - if: '$CI_COMMIT_BRANCH == "main"' - when: never - - if: '$CI_COMMIT_BRANCH == "master"' - when: never - - if: '$NOMAD_VAR_NO_DEPLOY' - when: never - - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' - when: manual diff --git a/.gitlab-ci.yml b/gitlab.yml similarity index 100% rename from .gitlab-ci.yml rename to gitlab.yml