Skip to content

Health check on container instances - #2570

Open
cptanalatriste wants to merge 26 commits into
alan-turing-institute:developfrom
cptanalatriste:2568-sre-healthchecks
Open

Health check on container instances#2570
cptanalatriste wants to merge 26 commits into
alan-turing-institute:developfrom
cptanalatriste:2568-sre-healthchecks

Conversation

@cptanalatriste

@cptanalatriste cptanalatriste commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

✅ Checklist

  • You have given your pull request a meaningful title (e.g. Enable foobar integration rather than 515 foobar).
  • You are targeting the appropriate branch. If you're not certain which one this is, it should be develop.
  • Your branch is up-to-date with the target branch (it probably was when you started, but it may have changed since then).

🚦 Depends on

N/A

⤴️ Summary

  • We have added a new CLI command: dsh sre healthcheck (see sre.py).
  • We are also extending the AzureContainerInstane type, so it can now return all the containers in the container group (see azure_container_instance.py). We'll use this method in assertions (see healthcheck_utils.py).
  • The health check logic is built using pytest (see sre_healtchcheck_runner.py). To that end, we have developed a plugin to provide tests with information of the stack, the subscription, and the SRE config file (see healthcheck_plugin.py).
  • This first round of tests just check that every SRE container is in running state (see test_container_instance.py and pyproject.toml). We need to add some logic to handle Nexus containers, since it's not deployed in T0/T1.
  • We need to modify the Pulumi code of all the container instance resources, to expose the container group name and resource group (see infrastructure/programs/sre/*.py). We use this information to gather container group information.

🌂 Related issues

Contributes towards #2568

🔬 Tests

Tested on a Tier 2 and a Tier 0 SREs (below):

image

@github-actions

github-actions Bot commented Feb 27, 2026

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  data_safe_haven/commands
  sre.py 280-316
  data_safe_haven/external/interface
  azure_container_instance.py 92-93, 98-107
  data_safe_haven/healthcheck
  __init__.py
  check_container_instance.py 14-50, 58-70
  healthcheck_plugin.py 17, 21, 25
  healthcheck_utils.py 27-48, 56-66
  sre_healthcheck_runner.py 28-52
  data_safe_haven/infrastructure/programs
  declarative_sre.py 525-532
  data_safe_haven/infrastructure/programs/sre
  apt_proxy_server.py 206-211
  clamav_mirror.py 180-185
  gitea_server.py 421-426
  hedgedoc_server.py 351-356
  identity.py 255-260
  remote_desktop.py 424
  data_safe_haven/provisioning
  sre_provisioning_manager.py 51-63
Project Total  

This report was generated by python-coverage-comment-action

@cptanalatriste
cptanalatriste marked this pull request as ready for review March 3, 2026 13:36
@cptanalatriste
cptanalatriste requested a review from a team as a code owner March 3, 2026 13:36

@JimMadge JimMadge left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, interesting 🤔.

Where were you seeing this going in the medium/long term?

Some things feel a bit odd,

  • using pytest for these checks
  • A CLI for this rather than a dashboard

Those things feel a bit more reasonable when we think about scaling this to other tests though. That said, we are probably only using 1% of pytest and it might be simpler to write test class of our own.

When talking about containers, it would be good to add a health check too, on top of checking the containers are running. I'm not sure if all of the containers we use have an appropriate HEALTHCHECK defined, but we could add them. Looks like in ACI this is called liveness probe.

@cptanalatriste

Copy link
Copy Markdown
Contributor Author

Hi @JimMadge , thanks for your comments. Regarding your questions:

  1. In the short term, I see the healthcheck feature as quick holistic quality check on deployed SRE. For example, the tests contained in this PR would detect if an automatic PR broke a container (like Updating Nexus to v3.88.0 breaks the container #2557 ). Or if we're facing an already known container-related issue, like OAuth client secret starting with dash breaks identity container #2502. I envision this tool will be really helpful for the admin teams working with production TREs (maybe @helen-d-little and @llewelld can comment).
  2. This PR is meant to be just a basic scaffolding setup, and further tests/checks are coming. On the medium term, I would like to port most of the tests in the security checklist to the healtcheck CLI. That would also speed-up DSH releases.
  3. On the long term, as discussed before, the logic built for this feature can serve as basis for a CI/CD pipeline that deploys automatically to a Development TRE. It can even, for example, run periodically and populate a dashboard.
  4. I also felt weird using pytest outside a testing context, but it had so many convenient features that made working on this PR very easy. To mention a few: 1) Using plugins to deliver testing dependencies, 2) Parametrised tests, 3) Configurable output and reporting. I'll argue building those features is not trivial, and I don't see a strong argument for not using a mature library that already offers them.
  5. It would definitely be great to implement liveness probes to our container instances! We would need to check into each image documentation to see what's already on offer, or maybe setup a custom one ourselves. However, at the moment the container state gaves me all the signal I need for the issues I'm aware of (see item 1.). Happy to add this to backlog, and work on it later.

Hopefully this makes sense. Please let me know if you have further questions.

@JimMadge

JimMadge commented Mar 9, 2026

Copy link
Copy Markdown
Member

I think the longer term plan makes sense, but we need to do more now to plan what that will look like.

Container status/healthchecks feels like a sensible place to start. Running pulumi refresh in check mode also feels like an obvious starting point.

I feel pytest is the wrong library to use here. It is really focused on code testing, which isn't what we are doing. Parametrising test functions and formatting the output shouldn't be very difficult, and I think going with pytest we are likely to run in to problems in the future with the way it handles exceptions and reports errors.

I think what we really want is a function with returns a test result (or even just a boolean) and then format sensible output from that.

@cptanalatriste

Copy link
Copy Markdown
Contributor Author

@JimMadge , I'm still not entirely clear on the concrete technical problems we would encounter in a hypothetical future by using pytest (or if they're unavoidable), and I feel what you're suggesting we'll require re-writing/re-implementing mature features already available and battle-tested.

However, this feature is not been progressed for two weeks and I'm feeling some urgency for getting closer to the medium/long term plan. Also, as you mentioned, the pytest features I'm using at the moment are not that complicated. So, I'll prepare a pytest-free version of this PR shortly. I'll let you know when it's ready.

@cptanalatriste

Copy link
Copy Markdown
Contributor Author

@JimMadge here's a pytest-free version of this PR. Still working as expected:
image

@JimMadge JimMadge left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is looking good. I'm finding the structure quite complex and maybe over engineered though.

Couldn't we just have a HealthCheck ABC, and sublcass it as we need, maybe like ContainerHealthCheck(HeathCheck) PackageContainerHealthCheck(ContainerHealthCheck). Which is, more or less what is here, but I don't like the implicit way typing.Protocol works.

We could also make factories, but I think classes make sense because we are likely to want helper methods as well as the run method.

For executing, I have used decorators before to add functions to a list. So you could have something like,

health_check_instances = []

class MyCheck(HealthCheck):
    def run(self, ...):
        ...

@health_check_instance
MyCheck(case1)

@health_check_instance
MyCheck(case2)


for instances in health_check_instances:
    result = instance()
    handle_result(result)

How does that sound to you?

container_group_name=self.container_group_name,
)

return typing.cast(list[Container], container_group.containers)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this because container_group.containers isn't actually a list[Container]?

@@ -0,0 +1,70 @@
from attrs import define

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to use this over dataclasses.dataclass?
attrs isn't an explicit dependency, and we are already using dataclasses elsewhere.

Comment on lines -54 to +61
self.software_repository_params: dict[str, str] | None = sre_stack.output(
"software_repositories"
)
self.software_repository_params: dict[str, str] | None = None

try:
self.software_repository_params = sre_stack.output("software_repositories")
except KeyError:
self.logger.info(
"There's no software repositories information in the current SRE"
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a bug fix, can we put it in a different PR?
We shouldn't use exception handling for flow control if we can avoid it. Hopefully we can do a if "software_repositories" in ... here.

Comment on lines +8 to +25
@define
class SREHeathCheckPlugin:

_sre_project_manager: SREProjectManager
_subscription_name: AzureSubscriptionName
_sre_config: SREConfig

@property
def project_manager(self) -> SREProjectManager:
return self._sre_project_manager

@property
def subscription_name(self) -> AzureSubscriptionName:
return self._subscription_name

@property
def sre_config(self) -> SREConfig:
return self._sre_config

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this class for? It is just aliasing three variables.

Comment on lines +14 to +16
class HealthCheckTest(Protocol):

def check(self, plugin: SREHeathCheckPlugin) -> str: ...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the implicit way classes are assigned. I think it would be better to make an ABC for health checks

Comment on lines +10 to +11
class HealthCheckError(Exception):
pass

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We collect our exceptions in the exceptions module.



@define
class SREHealthCheckRunner:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a benefit to this being a class? Feels like it is just a function.

Comment on lines +50 to +52
self._logger.info(f"\u2705 {success_message}")
except HealthCheckError as error:
self._logger.info(f"\u274c {error.args[0]}")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rich has support for emoji codes that are more human readable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants