Adding a new service
1. Give it a home
Create the directory at the repository root. Not under infra/services/, which is closed
(CHANGE-5). Name it what you want the image
called.
Write go.mod with a lyceum.technology/<name> module path and the shared library
(GO-3):
module lyceum.technology/<name>
go 1.26
require lyceum.technology/daedalus v0.0.0
replace lyceum.technology/daedalus => ../daedalus_go
Binary under cmd/<binary>/main.go, everything else under internal/. Full layout in
Go style guidelines.
2. Wire it into the build
Add mise.toml:
[task_config]
includes = [
"{{vars.repo_root}}/mise/includes/go.toml",
"{{vars.repo_root}}/mise/includes/go-service-image.toml",
]
[env]
DOCKER_PROJECT_DIR = "<dir>"
DOCKER_IMAGE_NAME = "<image-name>"
DOCKER_BINARY = "<binary>"
For Python:
- Include
python.tomlandpython-service-image.toml. - Declare
DOCKER_SERVICE_DIR,DOCKER_PACKAGEandDOCKER_IMAGE_NAMEinstead ofDOCKER_PROJECT_DIRandDOCKER_BINARY.DOCKER_SERVICE_DIRis separate, becauseDOCKER_PROJECT_DIRis the Go selector. - Add the project to
[tool.uv.workspace]in the rootpyproject.toml(PYTHON-1).
Then list the directory in [monorepo].config_roots in the root mise.toml. Do not skip this.
An unlisted project is matched by no wildcard: nothing lints it, tests it or builds it, and nothing
warns you (BUILD-6).
Check it worked:
mise tasks --all | grep <dir>
mise run //<dir>:build
mise run //<dir>:lint
You do not write tasks. If you think you need one, read
BUILD-1 first. Shared logic goes in mise/scripts/, and a
project must never redefine a task the includes already provide.
3. Satisfy the service contract
From service rules:
- Import
lyceum.technology/daedalus/version. Log build info at startup and register thebuild_infogauge (SERVICE-1). A binary that does not link the version package ignores the linker flags without saying so. - Expose
/metrics, even with nothing else to serve (SERVICE-2). Usedaedalus/metrics.MustNewRegistry. - Read config from the environment or a mount (BUILD-22).
- Handle
SIGTERM(SERVICE-3). Keep no state on local disk (SERVICE-4). - Take auth, messaging, logging and metrics from
daedalus(SERVICE-5, SERVICE-6). - Name the owning team (SERVICE-10).
Once an image exists, check build_info carries a real version. 0.0.0/unknown means the build
arguments never reached the binary (SERVICE-1).
No Dockerfile either. The shared one for your language builds from the identity in step 2 (BUILD-16).
4. Give the image a repository
One repository per image (BUILD-24). Add the hyphenated
image name to image_repositories in
infra/terraform/_modules/gcp/platform_artifact_registries/variables.tf. Add a
keep_most_recent_overrides entry there if the default retention is wrong.
Grant pull to the runtime service accounts in the runtime_pull_bindings input in
infra/terraform/platform/artifact_registries/terragrunt.hcl. The module default is always
overridden by that input.
The runtime service account MUST exist before you grant it. A binding to a missing account fails the apply.
Apply the unit against platform-staging.
5. Push the image
There is nothing to add. The service-image include from step 2 makes it a service with an image.
build-service-images.yml builds every such service with //<service_dir>:push-image, and
docker-env.bash derives region, repository, Dockerfile, tag and build arguments from your
mise.toml.
The workflow is dispatched by hand, so no image appears until someone runs it. See Trying a branch on staging.
The same build runs locally:
DOCKER_GCP_PROJECT=platform-staging-506908 mise run //<dir>:build-image
6. Deploy it
Add a module under infra/terraform/_modules/gcp/<name> holding the Cloud Run service. Add a unit
under infra/terraform/gcp/<name> that sources it, modelled on infra/terraform/gcp/croesus.
Take the image as an image_ref_* variable so each environment pins its own. Export the pin as
TF_VAR_image_ref_<service> in infra/terraform/envs/<env>.env. Without the export the unit
reads an empty string and applies an empty image reference.
Pin an immutable version or digest, never a floating tag
(BUILD-15). The platform registries push no :latest, so
such a reference resolves to nothing. Promotion between environments is a commit changing that pin
(ENV-5). CI applies, from a merged commit
(INFRA-1).
7. CI
There is nothing to add. The sweep, check-lint-and-test.yml, lints and tests every directory in
config_roots (MERGE-1).
DO NOT add a workflow for your service (BUILD-25). If your service needs a check the workflows lack, then add it for every service of its kind.
8. Document it
- A
README.mdsaying what the service is, linking to the wiki (DOC-7). - A wiki page and its
naventry (DOC-8). - Update any page this change makes wrong, in the same pull request (DOC-6).
Checklist
[ ] Directory at the repository root, not in a closed one
[ ] go.mod / pyproject entry, daedalus wired up
[ ] mise.toml with the shared includes and this service's identity
[ ] Listed in [monorepo].config_roots
[ ] Version package linked, startup log and build_info gauge
[ ] /metrics endpoint
[ ] Config from environment or mount, SIGTERM handled, no local state
[ ] Owning team named
[ ] Repository in image_repositories, pull bindings granted, unit applied
[ ] Terraform module and unit, unit takes an image_ref_* variable
[ ] TF_VAR_image_ref_<service> exported in envs/<env>.env
[ ] Deploy config pins a version or digest
[ ] README, wiki page, nav entry
Related
- Engineering rules
- Mise tasks
- Container images: why the pipeline is shaped this way
- Bootstrapping platform images