We can manage, update, restart, and scale each Docker container separately. That’s the beauty of containerization. Each container should have a single responsibility. PHP, Java, NodeJS — each of those does one obvious thing. Creating containers that include multiple services is considered an anti-pattern. The Single Responsibility Principle doesn’t apply only to our codebase.
But does that mean we must never create such anti-pattern containers just because they violate SRP? I believe there’s one good reason to break that rule, and I’ll show you in this article.
The problem
I built a Software Architecture Platform on top of the Symfony framework and wanted to integrate Structurizr in my system. My platform is delivered as a SaaS, while Structurizr is an open-source solution. So, I wanted to combine both to allow users to have a single place to manage the Software Architecture, and their C4 Model made using the Structurizr DSL.
I needed two main things for the integration:
- Structurizr Docker image — to have the ability to render diagrams
- Structurizr CLI — to validate the DSL
Docker image is a piece of cake. Pull, mount, and ready to use. But the Structurizr CLI was a little problematic because it’s written in Java. I could use a Java container, but the CLI is used in the terminal. The connection between PHP and Java container would have to be done using at least SSH which would require a lot of configuration in both containers (PHP as a client, Java as a server), and also requires a lot of code inside the application itself, to connect with SSH server and execute the CLI command.
I wondered — isn’t there a faster way to handle all that?
Can I Install Java Directly in a PHP Container?
Instead of an SSH connection, I figured I’d just install Java inside the PHP container. That would allow me to execute the Structurizr CLI just using the shell, in the same container. I could simply use the Symfony Process component as an abstraction. This solution would allow me to save a lot of time!
But there’s a catch! Java and PHP are now connected. I cannot scale, update, manage, and restart those independently. But, should I care?
Let’s think about what I needed from each component:
- Do I need scalability? This is a new SaaS platform, and I don’t have tens of thousands of users. I don’t need to scale yet.
- Do I need to update separately? I would, but this is not a critical service. It only executes one CLI command. There are no business-critical operations here.
- Do I need manageability? It’s nice to have, but Java is only used for a single CLI command. No web server, no database connection — just a simple local CLI command. I don’t need to manage it at a large scale. If it fails, the Structurizr integration module will fail as well, and I have logs inside the PHP container.
- Do I need independent restart? Executing this CLI command is strictly connected to the PHP module. Java is treated like a local service that is available only for that one container—PHP. When PHP is down, Java is useless. So, no — I don’t need such independence.
As you can see, there is no reason to avoid installing Java in PHP. But there is a good reason to install it — the time and cost! I can save a lot of time. Instead of configuring all those things, I can just execute a shell command directly from PHP. And it works like a charm!
How Does It Work In Real Practice?
Pistacy.io — that’s the name of the Platform — uses php:8.3-fpm
image as a base for the PHP container. The image is built using a Dockerfile because I need to install custom extensions and libs. Somewhere inside it, there’s just one line that installs Java:
FROM php:8.3-fpm AS base
# ...
RUN apt-get install -y default-jre
# ...
\ In the codebase, I need to execute the CLI command in the shell to validate the Structurizr DSL syntax, and finally transform it into JSON. As mentioned above, Pistacy.io uses the Symfony Process component to do that.
The solution looks as follows:
$process = new Process([
$this->projectDir . '/services/structurizr-cli/structurizr.sh',
'export',
'-workspace', $workingDirectory . '/workspace.dsl',
'-format', 'json',
]);
$process->run();
if (!$process->isSuccessful()) {
// Parse response error and decide what to do in case of error
}
\ The structurizr.sh
is an official Structurizr CLI release from https://github.com/structurizr/cli. It is installed during deployment and mounted inside the Docker image to be available for the PHP process.
That’s all! No additional services, no configuration, no SSH connection. Just old-school shell execution. I saved a lot of time, and it was worth it!
When Is Using an Anti-Pattern a Good Idea?
Remember, such an approach is an anti-pattern. In some circumstances, it is worth using:
- Your second-class service (Java) depends on the first-class service (PHP). In Pistacy.io, if PHP isn’t working, Java is useless because there’s nothing to call it.
- You don’t need to scale, or both services scale at the same level. In Pistacy.io, there’s currently no need to scale independently.
- You don’t need to manage/restart/update the second-class service. In Pistacy.io, Java relies entirely on PHP — it doesn’t function without it. Plus, it’s just a single CLI command being executed.
- The operation performed by the second-class service isn’t business-critical. In Pistacy.io, validating the Structurizr DSL is just one feature among many — and not a high-priority one.
But you need to consider the potential problems:
-
Potential failures in the second-class service can break the entire container — and this is the most important consideration here. For example, Java running inside a PHP container can bring down the whole PHP service. I need to be prepared for that with proper monitoring and a recovery process in place.
\
-
Pushing too many features that rely on the second-class service — without eventually migrating it to a separate Docker container — can lead to significant technical debt. Pistacy.io currently uses only one CLI command in Java. But if we ever need multiple commands, or if Java starts handling more complex tasks beyond simple CLI execution, it should be moved to its own container.
Other considerations:
-
Logs management: The second-class service may have its own log directories or files. You’ll need to account for those to properly debug issues.
-
Increased attack surface: More software means more potential security gaps.
-
Larger image size: This can negatively affect CI/CD pipeline execution times, as more data needs to be fetched for the environment.
\
Final thoughts
This solution works like a charm in my system, but that doesn’t mean it will work for yours. Always weigh the pros and cons to avoid building something fragile or piling up unnecessary tech debt.
If you're curious to see this solution in action, I recommend visiting https://pistacy.io and importing the popular Big Bank PLC workspace, which you can find here: https://github.com/structurizr/examples/blob/main/dsl/big-bank-plc/workspace.dsl.
When you save the DSL, Pistacy executes the Structurizr CLI (Java) to validate it, and the Structurizr On-Premises instance (Node) to render the diagrams in the background — all within a single Docker container, using Symfony Process.
If you’re looking for a solution to manage software architecture documentation, Pistacy.io lets you create C4 Models, vote on ADRs and RFCs, define architectural drivers, and much more.
\
Tidak ada komentar:
Posting Komentar