Azure entry install -- sanctioned first bring-up on AKS
Audience: an operator standing up an entry-shape MemQL instance on
Azure. Issue: memql#4204.
Placeholders. Every Azure name below is the operator's, and this page
names none of them -- the same rule the hostnames already follow with
$MEMQL_DOMAIN. Substitute your own throughout: <aks-name> (the
cluster), <cluster-rg> (its resource group), <backup-rg> (the backup
resource group), <registry-rg> (the resource group holding the ACR and
Key Vault), <instance-repo> (the private repository that will hold this
instance's definition), and <instance> (the overlay directory inside
it).
First bring-up is Argo in <aks-name> reconciling
deploy/k8s/overlays/cloud-entry. It is not an imperative deploy. It is not
aks-deploy.sh (that script is gone). Those are digest rolls against an
already-installed cluster. This page is the sanctioned entry-install
path.
MemQL ships one installation shape. A second environment is a second
instance -- own AKS, own Argo, own domain -- not a staging/prod split
inside one cluster (epic memql#3943). overlays/cloud stays on
cnpg-db/presets/top and mesh 2; it is what
deploy/argocd/apps/memql.yaml points at. Do not apply
overlays/cloud to <cluster-rg>. Do not add a second Application
next to deploy/argocd/apps/memql.yaml.
cloud-entry is the same base and the same CNPG shape, with the
numbers an entry / client install actually wants:
overlays/cloud
overlays/cloud-entry
Database
cnpg-db/presets/top
cnpg-db/presets/entry (1 instance, 32+32 GiB)
Mesh replicas
2
1
MCP
2
replicas 0
mcp is first-class replicas 0, with no pin at all -- not an all-zeros
MCP digest, which was a fail-closed placeholder for a running pin.
The committed default in the overlay is memql.localhost. No file
under deploy/ names a real domain (memql#3593). An install patches
MEMQL_DOMAIN and the Ingress / certificate hosts through the ArgoCD
Application's spec.source.kustomize.patches. .localhost is
unroutable externally, so a cluster reconciled before its domain is
set fails visibly instead of half-working.
The company apex site is not the cluster domain. Do not point the
company apex at AKS. The cluster domain is the install's
MEMQL_DOMAIN (the value lives on the install, not in this repo):
Role
Host
Notes
identity
identity.$MEMQL_DOMAIN
sign-in; first-run wizard at /setup
api
api.$MEMQL_DOMAIN
gRPC + HTTP edge
sites (os)
os.$MEMQL_DOMAIN
the platform's own site; its own exact rule and SAN (memql#4224)
sites (wildcard)
*.$MEMQL_DOMAIN
every other site; certified by memql-wildcard-tls over DNS-01 (memql#4347)
sites (apex)
$MEMQL_DOMAIN
same edge Service
mcp
mcp.$MEMQL_DOMAIN
exists in the generator; leave it dark
The MCP host is part of the closed role set. The overlay already sets
mcp replicas to 0. Do not publish it, do not send clients there.
Point DNS at the cluster ingress, then wait for the certificate:
Name
Type
Value
$MEMQL_DOMAIN
A
cluster ingress IP
*.$MEMQL_DOMAIN
A
same cluster ingress IP
The wildcard DNS record is for ROUTING: it is what lets *.$MEMQL_DOMAIN
reach the edge for every site. It is a separate thing from the wildcard
CERTIFICATE below -- both are needed, and the certificate additionally needs
the zone to be an Azure DNS zone this cluster's identity may write to,
because that is how the DNS-01 challenge is answered.
TWO certificates, split by which ACME challenge can issue each half
(memql#4224, memql#4347):
Certificate
Issuer
Names
memql-front-door-tls
letsencrypt-prod (HTTP-01)
api., identity., mcp., os., the apex
memql-wildcard-tls
letsencrypt-dns01 (DNS-01, Azure DNS)
*.$MEMQL_DOMAIN, the apex
Do not add a *.$MEMQL_DOMAIN dnsName to memql-front-door-tls. ACME
cannot serve an HTTP-01 challenge for a wildcard, and one wildcard name
fails the WHOLE order: the Certificate sits Pending, the Secret is never
written, and every host serves ingress-nginx's self-signed default
("Kubernetes Ingress Controller Fake Certificate"; Safari: "This
Connection Is Not Private"). That is how the first entry-shape bring-up
went, and the hand-edit to exact names that followed is now the generated
shape. The wildcard belongs on memql-wildcard-tls and nowhere else.
tls.hosts must name hosts the certificate that entry points at can
cover. ingress-nginx verifies the certificate against each host an
Ingress lists under tls and falls back to the default for a host that
certificate does not name -- and it builds a certificate-bearing server
block per RULE host, never per tls host. So edge-front-door carries
TWO tls entries: the apex against memql-front-door-tls, and
*.$MEMQL_DOMAIN against memql-wildcard-tls. A platform site with no exact
rule of its own still reproduces the fake-certificate failure for
os.$MEMQL_DOMAIN with both certificates Ready; the generator emits
os-front-door (an exact rule to the same edge Service) for exactly
that reason. The cluster-side os-front-door Ingress that was
hand-made as the ops workaround has the same name, so Argo adopts and
overwrites it on the next sync.
The wildcard certificate is what makes a hosted site live over TLS with
no operator step. Before it, a customer site hostname routed by the
wildcard had no certificate until somebody added a Certificate and an
exact-host Ingress for it -- one object pair per site. Now *.$MEMQL_DOMAIN
already names it. A hostname OUTSIDE $MEMQL_DOMAIN -- a customer's own
apex, a second domain -- is still that object pair
(site-hosting.md), and so is anything
more than one label deep (shop.eu.$MEMQL_DOMAIN).
Do these BEFORE the first sync. Skipping them is not fatal -- the wildcard
Certificate simply never becomes Ready and hosted sites fall back to the
controller's default, which is where they were before memql#4347 -- but the
role hosts and sign-in are unaffected either way, which is the whole reason
the two certificates are separate.
shell
export ZONE_GROUP="<dns-zone-rg>"# the resource group holding the DNS zone
export IDENTITY_GROUP="<cluster-rg>"# where the managed identity is created
export CLUSTER_RG="<cluster-rg>"
export CLUSTER="<aks-name>"
export SUBSCRIPTION_ID="$(az account show --query id -o tsv)"
# 1. The DNS zone for the domain, delegated at the registrar. `az network dns
# zone show` must succeed before anything below is worth running.
az network dns zone create -g "$ZONE_GROUP" -n "$MEMQL_DOMAIN"
az network dns zone show -g "$ZONE_GROUP" -n "$MEMQL_DOMAIN" --query nameServers -o tsv
# ...point the registrar at those name servers, then wait for the delegation
# to resolve. `dig NS $MEMQL_DOMAIN` must answer with them.
# 2. A user-assigned managed identity, and DNS Zone Contributor ON THE ZONE.
# Scope it to the zone, never to the subscription: this identity's only job
# is writing _acme-challenge TXT records under one name.
az identity create -g "$IDENTITY_GROUP" -n id-certmanager-memql
# 4. The four values the ClusterIssuer needs, for the Argo patch list below.
# The webhook wiring itself (the SA client-id annotation and the pod label)
# is already in git -- see the note under this block -- so there is nothing
# to patch by hand here; $DNS_CLIENT_ID is the value you substitute into
# deploy/cert-manager/install/kustomization.yaml.
echo"hostedZoneName = $MEMQL_DOMAIN"
echo"resourceGroupName = $ZONE_GROUP"
echo"subscriptionID = $SUBSCRIPTION_ID"
echo"clientID = $DNS_CLIENT_ID"
INFO: the webhook wiring is committed, not hand-patched. The
workload-identity webhook keys off two things the upstream cert-manager
manifest does not carry: azure.workload.identity/client-id on the
cert-manager ServiceAccount and azure.workload.identity/use: "true" on the
controller's pod template. Both are patches: entries in
deploy/cert-manager/install/kustomization.yaml, so ArgoCD owns them and a
cert-manager bump cannot silently drop them. The annotation ships as the
placeholder REPLACE-WITH-CERT-MANAGER-IDENTITY-CLIENT-ID; substitute
$DNS_CLIENT_ID from step 2 and commit it, the same way the cloud overlays
carry the database identity's client id.
Both values are read ONLY by that webhook, so this one shape serves every
deploy target: on a cluster without the webhook -- every local k3d cluster,
which reaches the same path through scripts/k3d/up.sh -- the label matches
no admission rule and the annotation is a string nothing dereferences.
Verify after a sync: kubectl -n cert-manager get deploy cert-manager -o jsonpath='{.spec.template.metadata.labels}' shows
azure.workload.identity/use, and kubectl -n cert-manager get sa cert-manager -o jsonpath='{.metadata.annotations}' shows a real client id
rather than the placeholder.
Then, after the first sync:
shell
kubectl -n memql get certificate memql-front-door-tls memql-wildcard-tls
# both must reach READY True. A wildcard stuck at False is almost always the
cmd/frontdoorhosts writes overlays/cloud-entry/front-door.generated.yaml
with the committed default. Install-time patches repoint every host
the generator emitted, plus the memql-domain ConfigMap:
Certificate memql-front-door-tlsspec.dnsNames → api.$MEMQL_DOMAIN,
identity.$MEMQL_DOMAIN, mcp.$MEMQL_DOMAIN, os.$MEMQL_DOMAIN,
$MEMQL_DOMAIN -- five entries in that order (/spec/dnsNames/0 ..
/spec/dnsNames/4), never a wildcard
Ingress api-front-door and api-front-door-grpc → api.$MEMQL_DOMAIN
(/spec/rules/0/host and /spec/tls/0/hosts/0)
Ingress identity-front-door → identity.$MEMQL_DOMAIN (same two pointers)
Ingress mcp-front-door → mcp.$MEMQL_DOMAIN (same two pointers; dark,
still patch the name so it cannot drift)
Ingress os-front-door → os.$MEMQL_DOMAIN (same two pointers)
Ingress edge-front-door → /spec/rules/0/host = *.$MEMQL_DOMAIN,
/spec/rules/1/host = $MEMQL_DOMAIN, /spec/tls/0/hosts/0 =
$MEMQL_DOMAIN (the apex, on memql-front-door-tls) and
/spec/tls/1/hosts/0 = *.$MEMQL_DOMAIN (the wildcard, on
memql-wildcard-tls) -- TWO tls entries, one per certificate
Certificate memql-wildcard-tlsspec.dnsNames → *.$MEMQL_DOMAIN,
$MEMQL_DOMAIN -- two entries in that order (/spec/dnsNames/0,
/spec/dnsNames/1); the apex is on it as well as the wildcard because
*.<domain> matches exactly one label and the apex has none
ClusterIssuer letsencrypt-dns01 → the four values step 4 above printed:
/spec/acme/solvers/0/dns01/azureDNS/hostedZoneName = $MEMQL_DOMAIN,
.../resourceGroupName, .../subscriptionID,
.../managedIdentity/clientID, plus /spec/acme/email
ConfigMap memql-domain key MEMQL_DOMAIN → the install domain
WARNING: the pointer set changed twice -- memql#4224, then memql#4347.
A patch written against the ORIGINAL shape (two dnsNames on
memql-front-door-tls, a wildcard under the edge Ingress's
/spec/tls/0/hosts) does not fail loudly everywhere: the two dnsNames
replaces still succeed and put the wildcard back into the HTTP-01 order,
which is the Pending Certificate again. A patch written against the
memql#4224 shape fails differently and more quietly: it simply has no
/spec/tls/1/hosts/0 entry, so edge-front-door keeps the committed
*.memql.localhost under tls -- a tls host the real wildcard certificate
does not name, which is the memql#4224 symptom at the one host that change
was about. Rewrite the Application patch to the list above before syncing.
The same derivation feeds every side: cmd/frontdoorhosts composes
the Ingress hosts and SANs, component/envregistry/domain.go composes
the issuer, CORS origins and redirect URIs, and the engine's
SeedMaterializer composes the OS site row's hostname from
MEMQL_DOMAIN -- all through component/frontdoor. Two copies of that
rule would be two copies that can disagree.
make frontdoor writes both instance overlays. Do not copy
overlays/cloud/front-door.generated.yaml into cloud-entry --
kustomize refuses a sibling path (load restrictor).
Cluster <aks-name> in <cluster-rg> (AKS Free, entry node
pools). Backups go to <backup-rg>; the ACR and Key Vault in
<registry-rg> stay where they are.
Point that cluster's Argo at znasllc-io/memql, path
deploy/k8s/overlays/cloud-entry, and the host-patches above.
Do not retarget deploy/argocd/apps/memql.yaml.
Seed secrets the way a cloud install already does (External Secrets
Key Vault). Seed memql-domain with the install's MEMQL_DOMAIN.
Wait for DNS, then wait for BOTH certificates to become Ready:
kubectl -n memql get certificate memql-front-door-tls memql-wildcard-tls.
memql-front-door-tls must show READY True with the five exact
dnsNames and no wildcard; memql-wildcard-tls must show READY True
with *.$MEMQL_DOMAIN and the apex. Then prove the SNI match memql#4224
was about, on every exact host:
Every line must name Let's Encrypt as the issuer. Kubernetes Ingress Controller Fake Certificate on any host means that host has no exact
rule or is not in the certificate -- re-check the patch list above. Run
the same probe against an arbitrary site name to prove the wildcard:
Let's Encrypt with subject *.$MEMQL_DOMAIN means the DNS-01 half is
live; the fake certificate there means the wildcard Certificate is not
Ready or /spec/tls/1/hosts/0 was never patched.
5. Open https://identity.$MEMQL_DOMAIN/setup. The domain field
prefills from MEMQL_DOMAIN (memql#4216).
6. Verify:
That script establishes dns / tls / grpc / precedence for api. and
identity. only. mcp. is inconclusive by design (it does not serve
/healthz on the front-door port). The OS shell and the apex are served by
the edge by design -- the shell's exact rule and the apex rule point at
the same Service the wildcard does -- so there is no exact-host
precedence to prove for them.
Roll an entry instance the way every other instance rolls: bump the digests in
deploy/k8s/overlays/cloud-entry and merge -- ArgoCD reconciles. Rollback
is git revert on that overlay.
cloud-entry, never cloud: they are two instances of the one installation
shape, with their own domains and their own databases, and pointing a roll at
the wrong overlay deploys to the wrong cluster.
The imperative memql-cockpit deploy path this section used to give was
removed in memql#4550 along with the Cockpit's TUI. It defaulted to
overlays/cloud, which is exactly the mistake above -- an entry instance had
to remember to override it. See
deploy-bundle-runbook.md.
No overlays/cloud. No top. No extra monitoring addon.
#AI vendors: federate, because there is no key to seed
memql-secrets used to carry a vendor API key per AI vendor at bring-up. It no
longer does, and there is no env name for one: both vendors are reached by
workload identity federation, where the pod presents the token Kubernetes
projects for it and the vendor returns a bearer that lives at most an hour. No
long-lived vendor credential exists in the cluster at any point.
Until you do this, the cluster has no AI providers, which is a working
cluster with ai reported as unconfigured rather than a broken one. Everything
that needs no model works; nothing falls back to anything.
The manifests are already in place -- deploy/k8s/base gives every engine
Deployment the memql-engine ServiceAccount, BOTH projected identity tokens
(anthropic-identity and openai-identity, one audience each) and both empty
federation ConfigMaps -- so each cutover is console work plus a few ids in the
overlay.
Six values join this instance's per-cluster install values, alongside the
domain, the DB identity client id and the mail tenant ids:
They are bound to THIS cluster's OIDC issuer. A re-created cluster gets a new
issuer, which invalidates both -- record them with the rest of the per-cluster
values, and expect to redo the console steps if the cluster is rebuilt.
The two sets are independent: a cluster may federate one vendor and not the
other. Within a vendor it is all or nothing, and a partial set REFUSES BOOT
rather than degrading.
#Mail: the Graph sender lives on the mailbox tenant
Magic links, invitations and admin notifications leave the identity
service through integrations/email's Microsoft Graph sender, and on
the original instance Graph remains its bootstrap transport (memql#4218).
Organization campaign sending can now use ACS through the separate
cluster email configuration.
That Settings flow is not yet part of infrastructure provisioning and must not
be mistaken for an automatic replacement for bootstrap mail.
The lesson from the first bring-up (memql#4226): AKS and the
Pay-As-You-Go subscription sit on one Entra tenant; the sender mailbox
(noreply@<domain>) and the public mail domain live on a DIFFERENT
Microsoft 365 tenant. Graph resolves /users/<sender> in the tenant the
token was issued for, so on the AKS tenant that user is a 404 (or a
guest object with no mailbox), whatever permissions the app holds there.
Everything the sender needs therefore lives on the MAILBOX tenant:
Identify the tenant that hosts the sender mailbox with the two
commands above. Do not assume it is the subscription's tenant.
On that tenant,
GET /v1.0/users/noreply@<domain>?$select=id,userType,assignedLicenses
returns a Member with assignedLicenses non-empty. Empty
licenses, or MailboxNotEnabledForRESTAPI on the first send, means
no Exchange mailbox -- license it or pick another sender before
wiring anything.
Create a new id-memql-mail app registration for THIS instance --
never reuse another instance's. Two reasons, and the second one has
already cost somebody a secret: the blast radius of a leaked client
secret is one instance rather than all of them, and adding a secret to
a shared registration is one flag away from destroying the others (see
the --append warning below).
Grant Mail.Send (Application) with admin consent -- on the mailbox
tenant only.
Apply an ApplicationAccessPolicy, and do not treat it as optional.
Until you do, this app can send as ANY mailbox in the tenant. See the
section below for the invocation; it is Exchange Online PowerShell and
is not reachable from az, which is exactly why it gets skipped.
Do NOT create the mail app on the AKS tenant. Do NOT recreate
noreply@<domain> on the AKS directory: a same-named object on the
wrong tenant sends every later diagnosis down the wrong path.
Seed MEMQL_EMAIL_AZURE_TENANT_ID (the mailbox tenant id),
MEMQL_EMAIL_AZURE_CLIENT_ID, MEMQL_EMAIL_AZURE_CLIENT_SECRET and
MEMQL_EMAIL_SENDER through Key Vault + External Secrets like every
other secret on the install.
Send one magic link; the identity log shows email: using Microsoft Graph sender at boot and sendMail answering 202. A 404 on
/users/<sender> is the tenant; a 401 is the credential.
The Entra display name for the permission is literally "Send mail as any
user", and that is what it grants. An app registration holding it can send
as every mailbox in the tenant -- the CEO's included -- not only as the sender
address you configured. Nothing in the Azure portal, in az, or in this
install path narrows it for you, so an instance stood up without the step
below has a tenant-wide send capability sitting in a Key Vault.
Narrowing it is Exchange Online PowerShell. There is no az equivalent and no
portal blade, which is the whole reason the step goes missing:
# want: Granted, then Denied. Allow up to ~30 minutes for replication before
# reading a Granted-everywhere result as a failed policy.
Microsoft also offers RBAC for Applications (New-ManagementRoleAssignment -App ... -Role "Application Mail.Send" -CustomResourceScope ...) as a newer,
more granular way to say the same thing. It is likewise Exchange Online
PowerShell. Prefer it if your tenant supports it; the requirement this
checklist is enforcing is that the app is scoped by ONE of them, not which.
az ad app credential reset without --append DELETES every existing
secret on the app registration. On the bring-up this was found on, the
secret it destroyed was labelled memql-keep-it. Any automation that
provisions mail credentials must pass --append:
shell
az ad app credential reset --id <client-id> --append --years 2
A one-flag difference between additive and destructive, on a command whose
name says "reset" and whose default is "replace all", is the second and
independent reason to give every instance its own app registration: on a
registration nothing else uses, the wrong flag costs you nothing.
An entry instance's definition is not in git today. This section is the operator's
record of where it lives, what the target shape is, and the exact switch --
from the read-only research pass on memql#4210 (the instance repo) and
memql#4205 (twice-daily entry deploys). Nothing below has been
executed: creating the repo, the ESO credential and vault, the AppProject and
repo-credential writes, and the Argo source switch are owner-gated.
The hand-made ArgoCD Application memql in this instance's own Argo.
Source https://github.com/znasllc-io/memql.git at a pinned main SHA
(rolled forward SHA by SHA, one engine pin PR at a time), path
deploy/k8s/overlays/cloud-entry, manual sync. The engine repo
deliberately carries no Application for this cluster:
deploy/argocd/apps/memql.yaml points at overlays/cloud, which
reconciles nothing live, and
TestCloudStaysOnTopAndTheInClusterAppIsUnchanged fails the build if it
is retargeted or a second Application appears.
Every install-time value lives ONLY in that Application's
spec.source.kustomize.patches: the host patches listed under
Argo host-patch, the memql-domain ConfigMap's
MEMQL_DOMAIN, the CNPG serviceAccountTemplate workload-identity
client id (REPLACE-WITH-DB-IDENTITY-CLIENT-ID in the overlay), and the
backup ObjectStoredestinationPath (REPLACE-WITH-STORAGE-ACCOUNT in
the overlay's https://<account>.blob.core.windows.net/<container>/ value;
the storage account is an install value -- capture it, do not reconstruct
it). The account belongs in the HOST: barman reads a bare container host
as an emulator and then requires a connection string that workload identity
does not supply, archiving nothing while the Cluster reports Ready
(memql#4460).
Out of band -- no engine manifest defines them: the cluster-scoped
ClusterIssuer letsencrypt-prod (HTTP-01) plus letsencrypt-dns01
(DNS-01, which unlike the first one IS in git -- overlays/cloud-entry
ships it), and the hand-seeded
memql-secrets (the Graph mail credentials included). Until memql#4224
is deployed, one cluster-only workaround sits beside them -- the
os-front-door Ingress; with that fix in the engine tag the instance
composes, it becomes generated content and the cluster copy is
overwritten on sync.
Pins today are a human loop: tag vX.Y.Z on main, dispatch
build-engine-images.yml with version=X.Y.Z (it pushes the same image
to ACR and to ghcr.io/znasllc-io/memql-<node>:X.Y.Z), edit
overlays/cloud-entry/kustomization.yaml, land the PR through the merge
queue, retarget the Application to the new main SHA, sync.
dispatch-engine-images-on-release.yml fires only on a PUBLISHED GitHub
Release, and tags are not published as releases, so it never fires today.
An instance repo that holds the cluster's definition, not the cluster's
code (<instance-repo>, private): an overlay, the pins, and an ArgoCD
app-of-apps. It is NOT a product repo stamped from the memql-project
template: the template stamps a product (DSL bundle, a client surface, a
bff-<product> head, its own public entry and its own letsencrypt-prod
ClusterIssuer), and its Makefile and CI fail by design with no
dsl/<domain> and no clients/<name>/. An engine-only entry instance
runs the plain engine plus the OS shell.
The mechanism exists and is verified (kustomize v5.8.1, 54 documents
rendered): compose the engine's entry overlay as a remote kustomize
resource and override the install values on top.
images: # the six engine digests (identity bff agent planner workbench edge)
kustomize shallow-clones the public engine repo at the ref and resolves
cloud-entry's ../../base and ../../components/* inside that clone;
the engine's ArgoCD bootstrap already raises the repo-server exec
timeout to 300s for this repo's size. The Application loses its
spec.source.kustomize block entirely: the patches live in git, under
review, like everything else.
Digests come from GHCR, with no Azure credential.ghcr.io/znasllc-io/memql-<node>:<version> digests are byte-identical
to the ACR pins on main (identity / bff / edge 0.19.6 checked), so a
pin workflow resolves them anonymously.
What stays in the engine:overlays/cloud-entry remains the
canonical entry overlay -- cmd/frontdoorhosts, make frontdoor and
the overlay gates all reference it -- and its images: become reference
defaults that may lag what the instance runs; the live cluster no longer
depends on them. overlays/cloud is untouched and is never
auto-applied.
Who owns the pins is the owner's decision. The recommendation is the
shape above (the instance repo owns them). The alternatives were: the
engine keeps them, so every entry deploy stays an engine PR through
the merge queue; or the instance repo vendors a copy of base and
cloud-entry, rejected because a CVE then means patching every fork.
deploy/external-secrets/ as committed cannot authenticate from this
cluster: its SecretStore uses a workload-identity federated credential
issued for ANOTHER cluster's OIDC issuer, and the vault it names holds that
retired cluster's values. That is why memql-secrets is hand-seeded here.
The wireExternalSecrets install step renders the SecretStore and its
workload-identity ServiceAccount from THIS instance's vault and client id
rather than applying the committed literals, so a new install does not
inherit that problem
(azure-instance-bringup.md).
base ships no ExternalSecret objects of its own. The two it used to
ship were described here as expected to stay unhealthy and part of the
OutOfSync / Degraded noise -- which was the defect, because an operator
who learns that Degraded is normal will not see a real degradation.
Nothing about this instance's health is expected to be red.
Any ExternalSecret that does run here carries
argocd.argoproj.io/compare-options: IgnoreExtraneous (memql#4489).
External Secrets copies an ExternalSecret's annotations onto the Secret it
writes, and Argo's tracking label rides along -- so without it Argo claims
a Secret that exists in no repository and reports it OutOfSync forever,
with nothing to sync and nothing to fix. If you add an ExternalSecret to
an instance repo, it needs the same annotation; the engine's are gated by
deploy/k8s/overlays/externalsecrets_test.go.
Do not "fix" it by repairing the federated credential or pointing the
SecretStore at the old vault. Its entries are another cluster's master
key, operator key and DSN; syncing them over memql-secrets breaks auth
and database access on the live cluster. Secrets stay hand-seeded until
this instance has its own vault holding its own values -- an owner
decision, not part of the switch.
The diff must be empty apart from the items deliberately being moved into
git (the ClusterIssuer, and the memql#4224 workaround until the engine tag
carries it).
Switch (every line is a cluster write; each is reversible; the order
matters):
The repo Secret's url must match the Application's repoURL byte for
byte, or Argo reports a ComparisonError; the AppProject sourceRepos
entry must exist first, or the Application is refused as "repo not
permitted in project" (harmless, but it stops reconciling).
kubectl replace the Application IN PLACE under the same name --
never delete and recreate it. It carries
resources-finalizer.argocd.argoproj.io, so deleting it cascade-deletes
the mesh AND the CNPG cluster, PVCs included. replace drops the old
spec.source.kustomize block wholesale; with the render
content-identical, the sync costs no pod churn.
Manual sync with prune OFF until argocd app diff memql is empty. A
prune sync from a render that omits anything the old render had deletes
it. Only after a green sync: automated: {selfHeal: true} with prune
still off until a second clean sync, then apply root.yaml so the
app-of-apps manages itself.
Rollback, at any step: restore the captured manifest. The render is
content-identical, so the sync is a no-op.
.github/workflows/pin-entry.yml in the instance repo, schedule: cron '0 */12 * * *' plus workflow_dispatch, with the default policy "latest
engine release TAG" -- a no-op until a human cuts a tag, so "twice daily"
means "within 12 hours of a cut":
git ls-remote --tags https://github.com/znasllc-io/memql.git -> the
newest vX.Y.Z.
For each of the six pinned nodes,
docker buildx imagetools inspect ghcr.io/znasllc-io/memql-<node>:X.Y.Z
-> digest. Abort if any node lacks the tag (the images are not built
yet; never half-pin).
Rewrite ?ref=vX.Y.Z and the six digest: lines in
deploy/k8s/overlays/<instance>/kustomization.yaml; render. A render
failure -- typically a patch target the new engine tag renamed -- means
do not commit; an unchanged render is a no-op.
One-file commit: a PR with auto-merge, or direct to main -- the
owner's call. With automated sync on the Application, Argo rolls within
its refresh interval.
Post-roll gate (a workflow_run after the merge, or a second cron 30
minutes later): /healthz on identity., api. and os.,
verify-frontdoor.sh --hosts api.$MEMQL_DOMAIN,identity.$MEMQL_DOMAIN,
and, once the SDK exposes it, ServerHello engine_version == X.Y.Z.
Failure opens an issue; rollback is git revert of the pin commit plus
a sync.
"Never auto-apply top" is structural, not a policy: the workflow edits one
file in the instance repo and has no path to overlays/cloud. The
engine-side process fix is the owner's: publish a GitHub Release for each
tag, so dispatch-engine-images-on-release.yml builds images without a
manual dispatch.