Check the chart's actual DAGSTER_PG_PASSWORD injection in the tightly coupled preflight instead of DAGSTER_POSTGRES_*, which the Simpl chart never sets; the old check warned about a misconfiguration on a correctly configured run pod. Evidence reports the variable name only, never its value. Record the platform k8s_job_executor run as cluster-verified in the guide and readiness checklist, add the Dagster UI screenshots covering job list, graph, code-configured tags and a successful run. Changelog: fixed
300 lines
23 KiB
Markdown
300 lines
23 KiB
Markdown
# Distributed Execution: Readiness Checklist
|
||
|
||
Pre-run validation for a workflow's execution target. Companion to the
|
||
[user guide](distributed-execution-guide.md); this document covers AC3.
|
||
|
||
Run these checks **before the first execution** of a workflow, and again after any
|
||
change to the run launcher, the executor, the target namespace or the code
|
||
location image.
|
||
|
||
> **Both Kubernetes paths are cluster-verified.** L4–L10 were cleared on
|
||
> sandbox-cat-dat on 2026-08-31. The tightly coupled `k8s_job_executor` path
|
||
> reached `RUN_SUCCESS` through the platform launcher on 2026-09-01. Checks
|
||
> L1–L3 and L11, which exercise the payload contract, message parsing and image
|
||
> isolation, are also verified locally. See [section 2.1](#21-sandbox-state-2026-08-31).
|
||
|
||
---
|
||
|
||
## 1. Common checks — both setups
|
||
|
||
| # | Check | How to verify | Expected evidence |
|
||
|---|---|---|---|
|
||
| C1 | Code location loads | Dagster UI → **Deployment** → **Code locations** | Location `distributed-execution` shows status *Loaded*, with a recent load timestamp and no error banner |
|
||
| C2 | Jobs are registered | Dagster UI → **Jobs** | The jobs listed in the guide's section 5.5 appear under the code location |
|
||
| C3 | Image tag matches the intended release | `kubectl -n dagster get deploy -l dagster/code-location=distributed-execution -o jsonpath='{.items[*].spec.template.spec.containers[*].image}'` | Tag equals the version in `pipeline.variables.sh`; never `latest` |
|
||
| C4 | Image architecture matches the nodes | `docker manifest inspect <image>` | Includes `linux/amd64`; a manifest with only `linux/arm64` produces `no match for platform` at pull time |
|
||
| C5 | Run launcher type is as intended | `kubectl -n <namespace> get cm dagster-instance -o yaml` | `run_launcher` block shows `K8sRunLauncher`. Note that `job_namespace` here is only a default — a code location's `DAGSTER_CONTAINER_CONTEXT` overrides it, so read that too before concluding where run pods land |
|
||
| C6 | Target namespace exists and is schedulable | `kubectl get ns <namespace>` and `kubectl -n <namespace> get resourcequota` | Namespace is `Active`; remaining quota exceeds the job's aggregate requests. Also confirm the launcher's `instance_config_map`, `postgres_password_secret` and any PVC volumes exist **in the namespace run pods actually use** — these references do not cross namespaces (see section 2.1) |
|
||
|
||
## 2. Tightly coupled checks
|
||
|
||
| # | Check | How to verify | Expected evidence |
|
||
|---|---|---|---|
|
||
| T1 | Run pod reaches the metadata database | Launch `tightly_coupled_in_process_job` | Run reaches `SUCCESS`; `report_execution_target` metadata lists `DAGSTER_PG_PASSWORD` under `env_vars_present` without exposing its value, and `env_vars_missing` is empty. Successful step startup is the connectivity proof because the step loads run state from Postgres before executing the op |
|
||
| T2 | Vault injection works | Same run; inspect the run pod | `kubectl -n dagster describe pod <run-pod>` shows the `vault-env` init container completed; no `vault:` literal remains in the process environment |
|
||
| T3 | Object storage is reachable | Same run, if the workflow uses S3 | No `EndpointConnectionError` in run logs; `S3_ENDPOINT_URL` present in `env_vars_present` |
|
||
| T4 | Multiprocess fan-out actually fans out | Launch `tightly_coupled_local_job` | Run succeeds; `summarise_results` metadata shows one entry per unit in `contributing_workers` and a single entry in `contributing_hosts` — separate processes, same machine |
|
||
| T5 | RBAC permits step Jobs | `kubectl -n dagster auth can-i create jobs --as=system:serviceaccount:dagster:dagster-dev` | Returns `yes`; required only for `k8s_job_executor` |
|
||
| T6 | Step pods are actually created | Launch `tightly_coupled_k8s_job`, then `kubectl -n dagster get jobs -l dagster/run-id=<run-id>` | One Job per mapped unit; `contributing_hosts` now shows one entry **per unit**, not one |
|
||
| T7 | Step pod egress is permitted | Same run | Steps do not hang in `STARTING`; run logs contain no connection timeouts to port 5432 |
|
||
| T8 | Failure surfaces as a pod failure | Force a step failure in a scratch namespace | `failPodOnRunFailure: true` is set, and the step pod reports `Failed` rather than `Completed` |
|
||
|
||
### 2.1 Sandbox state, 2026-08-31
|
||
|
||
The T checks were assessed against sandbox-cat-dat by reading the live
|
||
`dagster-instance` ConfigMap in `dataprovider01` and by running T1, T4, T6 and T7
|
||
from pods there.
|
||
|
||
What the configuration shows:
|
||
|
||
| # | Finding |
|
||
|---|---|
|
||
| C5 | `run_launcher` is `K8sRunLauncher`; `job_namespace: dagster` is the default, overridden per code location to `dataprovider01` |
|
||
| T2 | Vault injection is configured — `pod_template_spec_metadata` carries the banzaicloud annotations with role `sandbox-cat-dat-role` |
|
||
| T3 | `S3_ENDPOINT_URL` is `https://s3.sandbox-cat-dat.simpl-europe.eu`, with access keys injected from Vault |
|
||
| T5 | Passes. `dagster-role`, bound to `dagster-svc-account`, grants `batch/jobs` with create, delete, get, list, patch, update and watch |
|
||
| T8 | `fail_pod_on_run_failure: true` is set |
|
||
|
||
**T1 and T4 were executed on 2026-08-31** from a pod in `dataprovider01`
|
||
(`yaml/sandbox/probe-tightly-coupled-sandbox.yaml`), using a local SQLite
|
||
instance rather than the platform's Postgres.
|
||
|
||
| # | Result |
|
||
|---|---|
|
||
| T1 | Reachability **passes** — `check_tcp_reachable('pg-cluster.common01.svc.cluster.local', 5432)` returned `passed: True`. The env-var half of the check does not hold; see below |
|
||
| T4 | **Passes.** `tightly_coupled_local_job` reached `RUN_SUCCESS` with `contributing_hosts: ['distexec-tc-probe-hbdfl']` — one entry — and four distinct PIDs in `contributing_workers`. Separate processes, same machine, exactly as the row predicts |
|
||
|
||
`tightly_coupled_in_process_job` also succeeded, reporting a single worker.
|
||
|
||
### T1's env-var check and platform contract
|
||
|
||
The run emitted:
|
||
|
||
> Orchestration runtime env vars not visible to this process:
|
||
> `DAGSTER_POSTGRES_HOST`, `DAGSTER_POSTGRES_USER`, `DAGSTER_POSTGRES_DB`.
|
||
> Expected for a loosely coupled target; a misconfiguration for a tightly coupled one.
|
||
|
||
That warning was a false positive on a **correctly configured** platform run
|
||
pod. The chart does not set those variables: it injects `DAGSTER_PG_PASSWORD`
|
||
from `dagster-postgresql-secret` and stores host, user and database in
|
||
`dagster.yaml`. The reference check now tests for `DAGSTER_PG_PASSWORD` and
|
||
reports only its name, never its value. Database reachability is evidenced by
|
||
successful step startup: a tightly coupled step must load its run state from
|
||
the configured Dagster instance before `report_execution_target` can execute.
|
||
|
||
### T6 and T7, executed 2026-08-31
|
||
|
||
Running them needed three things the earlier attempt lacked: dagster pinned to
|
||
1.12.8 to match the control plane, so writing to the live `dataprovider01_dagster`
|
||
could not trigger a migration; step outputs moved onto the RWX
|
||
`dagster-shared-pvc`, because the default I/O manager writes them under
|
||
`$DAGSTER_HOME/storage` and each step pod otherwise gets its own empty volume;
|
||
and `automount_service_account_token: true` in `step_k8s_config`, because
|
||
`dagster-svc-account` disables it by default. The probe is
|
||
`yaml/sandbox/probe-k8s-executor-sandbox.yaml`; the run is
|
||
`d05c736a-8ec7-4e59-8cb0-9cb238db40dc`.
|
||
|
||
| # | Result |
|
||
|---|---|
|
||
| T6 | **Passes.** Six Jobs labelled `dagster/job=tightly_coupled_k8s_job` were created, one per step, each in its own pod |
|
||
| T7 | **Passes.** No step hung in `STARTING` and no step reported a connection timeout to port 5432. Every step pod opened the platform Postgres itself — the outputs and inputs below were all recorded through it |
|
||
|
||
Four steps executed and succeeded: `generate_work_units` (four dynamic outputs),
|
||
`report_execution_target`, and `process_work_unit[unit_0]` and `[unit_1]`, each
|
||
loading its input from the shared volume that a different pod wrote.
|
||
|
||
**The run then failed, and `summarise_results` never started.** The cause is a
|
||
property of the probe, not of the code or the platform:
|
||
|
||
> `Detected run worker status UNKNOWN: DagsterK8sUnrecoverableAPIError … jobs.batch`
|
||
> `"dagster-run-d05c736a-…" is forbidden: User "system:serviceaccount:dataprovider01:`
|
||
> `dagster-svc-account" cannot get resource "jobs/status" … in the namespace "dagster"`
|
||
|
||
Run monitoring polls for a run worker Job named `dagster-run-<run-id>`. The probe
|
||
starts the run with `dagster job execute` from a hand-rolled Job, so no such
|
||
object exists; and with no launcher-supplied container context the monitor looked
|
||
in the launcher's default namespace, `dagster`, where `dagster-svc-account` holds
|
||
no rights. The 403 turned *absent* into *unknown*, and the monitor failed the run
|
||
at its first poll. `process_work_unit[unit_2]` and `[unit_3]` had their pods
|
||
started already; both logged `Skipping step execution … since the run is in status
|
||
DagsterRunStatus.FAILURE` and exited 0 — which is why all six Jobs read `Complete`
|
||
while only four steps ran.
|
||
|
||
A run submitted the normal way, through the webserver to `K8sRunLauncher`, gets a
|
||
`dagster-run-<run-id>` Job in `dataprovider01` and is not exposed to this. Closing
|
||
T6/T7 end to end, including `summarise_results`, needs the code location
|
||
registered on the platform Dagster.
|
||
|
||
### T6 and T7 closed end to end, 2026-09-01
|
||
|
||
With the code location registered on the platform Dagster,
|
||
`tightly_coupled_k8s_job` was launched from the webserver and reached
|
||
`RUN_SUCCESS` as run `1d8cb167-9fb8-4a34-a036-789eac381e13`. `K8sRunLauncher`
|
||
created `dagster-run-1d8cb167-…` in `dataprovider01`, `K8sStepHandler` created one
|
||
`dagster-step-…` Job per step, and every step — including `summarise_results` —
|
||
completed. No hand-rolled probe, no run-monitoring 403.
|
||
|
||
**The last thing in the way was where step outputs are written.** Two earlier
|
||
attempts failed on
|
||
`FileNotFoundError: /tmp/dagster/storage/<run-id>/generate_work_units/result/unit_0`,
|
||
with every step pod using its own container filesystem. Mounting
|
||
`dagster-shared-pvc` into the step pods was necessary but not sufficient: setting
|
||
`DAGSTER_HOME` through `step_k8s_config` does not take effect, because the Dagster
|
||
chart already injects `DAGSTER_HOME=/tmp/dagster` and the appended second entry
|
||
does not displace the first. The fix is to bypass `DAGSTER_HOME` and pin the I/O
|
||
manager instead — `fs_io_manager.configured({"base_dir": SHARED_IO_BASE_DIR})` on
|
||
the job, with `SHARED_IO_BASE_DIR` under the mounted PVC. The successful run logs
|
||
confirm it:
|
||
|
||
> Handled output "result" using IO manager "io_manager" —
|
||
> `/dagster/shared/distributed-execution/storage/1d8cb167-…/generate_work_units/result/unit_0`
|
||
|
||
The platform run exposed the false-positive `DAGSTER_POSTGRES_*` warning
|
||
described under T1 above. The reference now checks the chart's actual
|
||
`DAGSTER_PG_PASSWORD` injection instead.
|
||
|
||
> Note: `report_execution_target` reported `namespace: <not-in-kubernetes>` on a
|
||
> step pod that plainly was in Kubernetes. The check reads the service account
|
||
> namespace file, which is absent when the token is not automounted. It is a
|
||
> property of the probe, not of the run.
|
||
|
||
**C6 passes, contrary to what `job_namespace` alone suggests.** The launcher's
|
||
`job_namespace: dagster` is only a default. Each code location ships a
|
||
`DAGSTER_CONTAINER_CONTEXT` environment variable — generated by the chart — whose
|
||
`k8s.namespace` is `dataprovider01`, and the code location's context is merged
|
||
*over* the launcher's, so it wins (`dagster_k8s/container_context.py`: `namespace
|
||
= other.namespace if other.namespace else self.namespace`). Run pods therefore
|
||
land in `dataprovider01`, where `dagster-instance`, `dagster-postgresql-secret`
|
||
and `dagster-shared-pvc` all exist.
|
||
|
||
Both deployed code locations carry it, so both would launch correctly:
|
||
|
||
| Code location | `DAGSTER_CONTAINER_CONTEXT` `k8s.namespace` |
|
||
|---|---|
|
||
| `template-code-location` | `dataprovider01` |
|
||
| `semaphore-ui` | `dataprovider01` |
|
||
|
||
The successful run confirms that the code-location container context places run
|
||
and step pods in `dataprovider01` with the required namespace-local dependencies.
|
||
|
||
The residual risk is narrower than a broken namespace, but real: a code location
|
||
that reaches the launcher *without* a container context namespace inherits
|
||
`dagster` and would fail on three missing objects. The chart always sets it, so
|
||
this bites only hand-rolled pods — the reason the C6 row asks you to check the
|
||
launcher's namespace-local dependencies rather than just the namespace.
|
||
|
||
> An earlier revision of this section claimed C6 failed and that this explained
|
||
> the absence of runs. That was wrong: it read `job_namespace` without accounting
|
||
> for the container-context override.
|
||
|
||
## 3. Loosely coupled checks
|
||
|
||
| # | Check | How to verify | Expected evidence |
|
||
|---|---|---|---|
|
||
| L1 | Payload contract is intact | `uv run pytest tests/test_loosely_coupled.py` | `test_payload_does_not_import_dagster` passes — the payload imports `dagster_pipes` only |
|
||
| L2 | Pipes round trip works | Launch `loosely_coupled_subprocess_job` | Run reaches `SUCCESS`; run logs contain the payload's `External payload started on …` line, proving `pipes.log` crossed the channel |
|
||
| L3 | Silence is treated as failure | Same test module | `test_silent_message_path_is_treated_as_failure` passes — an empty message list raises rather than yielding an empty result |
|
||
| L4 | Payload image is pullable by the target cluster | `kubectl -n <payload-ns> run pull-probe --image=<payload-image> --restart=Never --command -- true` | Pod reaches `Completed`; no `ImagePullBackOff`. Both Gitea images pull anonymously — a bare registry `GET` returns 401, but that is the start of the Docker token handshake, not a refusal |
|
||
| L5 | Dispatcher can create Jobs in the payload namespace | `kubectl -n <payload-ns> auth can-i create jobs --as=system:serviceaccount:dagster:dagster-dev` | Returns `yes` |
|
||
| L6 | Dispatcher can read pod logs — **the message channel** | `kubectl -n <payload-ns> auth can-i get pods/log --as=system:serviceaccount:dagster:dagster-dev` | Returns `yes`. A `no` here breaks reporting *without* failing the workload |
|
||
| L7 | Payload Job is actually created | Launch `loosely_coupled_k8s_job`, then `kubectl -n <payload-ns> get jobs -l app.kubernetes.io/name=distributed-execution-payload` | One Job per dispatch, labelled `dagster/execution-target=loosely-coupled` |
|
||
| L8 | Payload has **no** orchestration connectivity | Same run; inspect run logs | No `Payload could see orchestration runtime credentials` warning. This is a positive check — absence of errors is not sufficient |
|
||
| L9 | Work ran off-platform | Same run | `summarise_results` metadata shows `contributing_hosts` containing the payload pod names, not the run worker's hostname |
|
||
| L10 | One workload dispatched per unit | Same run, or `uv run pytest -k dispatched_to_its_own` locally | `contributing_workers` has one entry per unit; the local test asserts four distinct external workers |
|
||
| L11 | Payload **image** carries no orchestration dependency | `docker run --rm <payload-image> python -c "import importlib.util; print(importlib.util.find_spec('dagster') is not None)"` | Prints `False`. L1 proves the *source* does not import `dagster`; this proves the shipped image does not contain it either |
|
||
|
||
Checks L4–L9 require a cluster. L1–L3, L10 and L11 run on a laptop and should
|
||
gate every change to the payload or the dispatching op.
|
||
|
||
`yaml/loosely-coupled/probe-pipes-k8s.yaml` clears L4–L9 in a single run from a
|
||
throwaway namespace, without deploying a code location or a Dagster control
|
||
plane. Prefer it over assembling the cluster checks by hand: the RBAC it grants
|
||
is exactly the set L5 and L6 ask about, so a failure localises immediately.
|
||
|
||
**L4–L10 were cleared on sandbox-cat-dat on 2026-08-31** using the sandbox
|
||
variant `yaml/sandbox/probe-pipes-k8s-sandbox.yaml`. Run
|
||
`cff9b348-bfc3-4ac1-ab51-a94892b8e3a0` reached `RUN_SUCCESS` in `dataprovider01`:
|
||
four payload Jobs, four distinct payload pod hostnames in `contributing_hosts`,
|
||
and the payload's `External payload started on …` lines in the dispatcher's log,
|
||
which is the pod log stream doing its job as the message channel. No credentials
|
||
were needed anywhere.
|
||
|
||
One trap the run exposed. The platform's `dagster-svc-account` sets
|
||
`automountServiceAccountToken: false`, and the Dagster chart overrides it to
|
||
`true` on every pod it manages. A hand-written pod that does not is a plausible
|
||
future failure: the pipes client selects in-cluster authentication correctly and
|
||
then fails on `Service token file does not exist`, which points at Kubernetes
|
||
rather than at the omission.
|
||
|
||
---
|
||
|
||
## 4. Common misconfiguration symptoms
|
||
|
||
### 4.1 Both setups
|
||
|
||
| Symptom | Likely cause | Correction |
|
||
|---|---|---|
|
||
| Code location stuck in *Loading*, then errors | Entry point path in `codeServerArgs` does not match the image layout | Confirm `--python-file` matches `workspace.yaml`; both must be `src/distributed_execution/repository.py` |
|
||
| `ImagePullBackOff` with `no match for platform` | Image published for a single non-matching architecture | Rebuild multi-arch with `docker buildx`, and pin a version tag rather than `latest` |
|
||
| Run stays in `QUEUED` indefinitely | Run coordinator concurrency limit reached, or no schedulable node | Check `max_concurrent_runs` and tag concurrency limits; check node capacity and resource quota |
|
||
| Run fails immediately with a serialisation error | Code location image and Dagster control-plane versions diverge | Align the `dagster` version in `pyproject.toml` with the chart's version and rebuild |
|
||
|
||
### 4.2 Tightly coupled
|
||
|
||
| Symptom | Likely cause | Correction |
|
||
|---|---|---|
|
||
| Run pod starts, then fails with a connection timeout to port 5432 | Execution namespace NetworkPolicy does not permit egress to Postgres | Add an egress rule for the metadata database, or move run pods to an already-approved namespace via `jobNamespace` |
|
||
| `env_vars_missing` is non-empty in `report_execution_target` metadata | Env vars are set on the code location deployment but not on the run pod | Add them under `runLauncher.config.k8sRunLauncher.runK8sConfig.containerConfig.env` — code location env is **not** inherited by run pods |
|
||
| A literal `vault:...` string appears as a value at runtime | Vault mutating webhook did not process the pod | Verify the `vault.security.banzaicloud.io/*` annotations are on the **run pod** template, not only the code location pod |
|
||
| Steps hang in `STARTING` with `k8s_job_executor` | Service account lacks Job create/watch permission | Apply `yaml/tightly-coupled/rbac-step-executor.yaml` and confirm with `kubectl auth can-i` |
|
||
| `FileNotFoundError` loading a step input from `/tmp/dagster/storage/...` | Step pods each use their own container filesystem for I/O manager output | Mount an RWX volume into step pods via `step_k8s_config` **and** pin `fs_io_manager`'s `base_dir` under it. Setting `DAGSTER_HOME` in `step_k8s_config` does not work — the chart's `DAGSTER_HOME=/tmp/dagster` is injected first and wins |
|
||
| `contributing_hosts` shows one host when `k8s_job_executor` is configured | Run tags or Launchpad config overrode the executor, or the image predates the change | Confirm the code location reloaded after the image bump; check the run's *Config* tab for an `execution:` override |
|
||
| Step pods `OOMKilled` under fan-out | Per-step memory limit applied per pod, aggregate exceeded quota | Raise `step_k8s_config` limits or lower step concurrency; the two multiply |
|
||
| Postgres refuses connections once fan-out grows | Each step pod is an independent DB client | Reduce step concurrency, raise the Postgres connection limit, or move the fan-out step to a loosely coupled target |
|
||
|
||
### 4.3 Loosely coupled
|
||
|
||
| Symptom | Likely cause | Correction |
|
||
|---|---|---|
|
||
| Op fails with `No pipes messages received from the external payload` | The message path is broken, not the workload | Work through L6 then L4. The payload very likely ran and succeeded; only its reporting was lost |
|
||
| Payload pod `Completed`, but Dagster shows no payload log lines | A log shipper is intercepting or truncating stdout | Exclude the payload namespace from the shipper, or switch to an object-storage message reader |
|
||
| Op hangs until `pod_wait_timeout` (default 24 h) | Payload Job never scheduled — quota, node selector or image pull | Check `kubectl -n <payload-ns> describe job <name>`; lower `pod_wait_timeout` so the failure surfaces quickly |
|
||
| `403 Forbidden` creating the Job | Dispatcher service account lacks Job create permission | Apply `yaml/loosely-coupled/rbac-pipes-dispatch.yaml` in the **payload** namespace |
|
||
| Warning: `Payload could see orchestration runtime credentials` | Payload pod inherited run-pod env or a Vault annotation | Remove the inherited env; the payload should receive only what `extras` and explicit `env` pass it |
|
||
| Payload exits non-zero but the run reports success | Exit status not being checked, or messages read before failure | Confirm the dispatching op returns through `_result_from_pipes`; do not swallow `PipesClientCompletedInvocation` errors |
|
||
| Payload receives no `units` | `extras` key mismatch between dispatcher and `pipes.get_extra()` | Both sides must use the same key; a typo yields a `KeyError` inside the payload |
|
||
| Run cancelled in the UI, payload pod keeps running | Cancellation is not propagated to dispatched workloads automatically | `delete_pod_on_completion` handles the normal path; for cancellation, verify orphaned Jobs and add a cleanup sensor |
|
||
|
||
> **Partly observed.** The pipes rows were exercised on sandbox-cat-dat on
|
||
> 2026-08-31. The tightly coupled `k8s_job_executor` rows were closed on
|
||
> 2026-09-01 by run `1d8cb167-9fb8-4a34-a036-789eac381e13`; the remaining rows in
|
||
> the table above still follow from the dagster-k8s API rather than from
|
||
> observation.
|
||
>
|
||
> One symptom the cluster run added, absent from the table above: a service
|
||
> account with `automountServiceAccountToken: false` — which the platform's
|
||
> `dagster-svc-account` uses — makes the pipes client fail with
|
||
> `ConfigException: Service token file does not exist`. It reads as a Kubernetes
|
||
> fault; the fix is `automountServiceAccountToken: true` on the pod.
|
||
|
||
---
|
||
|
||
## 5. Evidence retention
|
||
|
||
For each workflow's first execution, attach to the workflow's repository or
|
||
change record:
|
||
|
||
1. The run ID and its final status.
|
||
2. The `report_execution_target` output metadata block (pod identity, namespace,
|
||
env var presence).
|
||
3. The `summarise_results` metadata block (`contributing_hosts` and
|
||
`contributing_workers`), which proves which execution target was actually
|
||
used and that the fan-out reached it.
|
||
4. For `k8s_job_executor`, the output of `kubectl get jobs -l dagster/run-id=<run-id>`.
|
||
5. For the loosely coupled target, the payload image digest and the run log line
|
||
emitted by `pipes.log` — together they prove which payload version ran and
|
||
that the message channel was open.
|
||
|
||
Items 2 and 3 together are sufficient to demonstrate that the configured
|
||
execution target is the one that ran — which is the point of the checklist.
|