Friday, March 20, 2026
Multi-cloud Devops tips
Saturday, May 17, 2025
DevOps lessons from Polaris and Iceberg
Tuesday, March 11, 2025
Cloud Maintenance
Although it's often referred to as "infrastructure as code", there is very little code in what most people call DevOps. It's mostly markup. This can cause maintenance issues. There are, however, ways of dealing with this situation.
"I wouldn’t head down the Helm path for that before I took a long look at CDK8s. Helm templates are a nightmare for debugging in my experience. Instead having a real programming language backing up the templates is so much better... It renders the YAML, it does not apply it. I use it with ArgoCD as my deployment mechanism. So I take the resulting YAML and check it into git for ArgoCD to apply to the cluster. Execution of the CDK8s code and check into git is automatic as part of the CI jobs." [Reddit]
Saturday, March 8, 2025
Automated documentation
![]() |
| IntelliJ's version of Postman |
Thursday, September 5, 2024
Architecting Azure
![]() |
| Bursty network logs |
Friday, April 5, 2024
Network Adventures in Azure Databricks
"Resource groups are units of deployment in ARM [Azure Resource Manager].
"They are containers grouping multiple resource instances in a security and management boundary.
"A resource group is uniquely named in a subscription.
"Resources can be provisioned on different Azure regions and yet belong to the same resource group.
"Resource groups provide additional services to all the resources within them. Resource groups provide metadata services, such as tagging, which enables the categorization of resources; the policy-based management of resources; RBAC; the protection of resources from accidental deletion or updates; and more...
"They have a security boundary, and users that don't have access to a resource group cannot access resources contained within it. Every resource instance needs to be part of a resource group; otherwise, it cannot be deployed." [Azure for Architects]
"A VNet is required to host a virtual machine. It provides a secure communication mechanism between Azure resources so that they can connect to each other.
"The VNets provide internal IP addresses to the resources, facilitate access and connectivity to other resources (including virtual machines on the same virtual network), route requests, and provide connectivity to other networks.
"A virtual network is contained within a resource group and is hosted within a region, for example, West Europe. It cannot span multiple regions but can span all datacenters within a region, which means we can span virtual networks across multiple Availability Zones in a region. For connectivity across regions, virtual networks can be connected using VNet-to-VNet connectivity." [Azure for Architects]
"Subnets provide isolation within a virtual network. They can also provide a security boundary. Network security groups (NSGs) can be associated with subnets, thereby restricting or allowing specific access to IP addresses and ports. Application components with separate security and accessibility requirements should be placed within separate subnets." [Azure for Architects]
Friday, June 9, 2023
Modern DevOps Tools
Some tools that I've had too little time to investigate thoroughly.
TestContainers
The free and open source TestContainers offers huge convenience to developers. For instance, you can fire up a very lightweight Postgres container in just a second or two. This ZIO SQL test (AgregatingSpec) ran in just 3.85s on my laptop. In that time, it started a Docker container, populated the Postgres database in it with test data, ran some Scala code against it then tore down the container. The container can last as long as the JVM so all your tests can use it before it detects the JVM is exiting whereupon it will kill the container.
MinIO
If you need to run S3 API compatible storage locally, you can try MinIO. It's written in Go and open source and allows you to have a local Docker container emulating Amazon storage.
DuckDB
This open source, C++ application allows you to run SQL against Parquet files without having to fire up a whole platform. You can even run DBeaver against it.
Crossplane
Crossplane is an open source Go project that "connects your Kubernetes cluster to external, non-Kubernetes resources, and allows platform teams to build custom Kubernetes APIs to consume those resources." [docs]
Scala Native
You can now convert Scala code to stand alone executable binaries using Scala Native [baeldung]. It currently only works with single threaded applications. The output can be converted to WebAssembly...
WebAssembly
Wikipedia describes WebAssembly as "a portable binary-code format and a corresponding text format for executable programs ... for facilitating interactions between such programs and their host environment." It is an "open standard and aims to support any language on any operating system".
Tapir
Is a type-safe, Scala library that documents HTTP endpoints.
GraphQL
GraphQL is a type system, query language, etc accessible through a single endpoint that only returns what is asked of it and no surplus information. It's a spec and there are implementations in a number of languages. The graph bit comes in insofar a "query is a path in the graph, going from the root type to its subtypes until we reach scalar types with no subfields." [Bogdan Nedelcu]
LLVM
LLVM is an open source tool chain written in C++. The 'VM' in LLVM originally stood for Virtual Machine but these days but this is no longer the case. Instead of being a virtual machine, it turns any major language into a common intermediate code that can then be turned to machine code.
GraalVM
GraalVM is an open source JDK and JRE written in Java itself and has its roots in project Maxine. But it's more than that. It offers compilation to native code as well as supporting polyglot code via its Truffle framework, a language-agnostic AST.
Quarkus
Based on GraalVM (above), Quarkus is an open source Java Framework tailored for Kubernetes. Since the JVM code is natively compiled, startup and memory sizes are small.
Spring Boot
Is an "opinionated" Java framework that favours convention-over-configuration and runs Spring apps with the minimum of fuss.
Python/Java Interop
Together, Python and Java both dominate the data engineering landscape. These languages can interoperate via Py4J which uses sockets to allow Python to invoke Java code and Jython which runs Python code wholely inside the JVM. Py4J is used extensively in Spark to allow PySpark devs to talk to Spark JVMs.
Jython, unfortunately, does not support Python 3.
Project Nessie
Nessie is an open source, JVM project that promises to do to big data what Git did to code: versioning, branching etc. It apparently sits nicely on top of Iceberg and DataBricks.
The lakeFS project is a open source, Go project that offers similar functionality.
Cloud native CI/CD
Tekton that is written in GoLang.
Argo is a Python based, Kubernetes native tool. For instance, it handles rolling deployments building on K8's RollingUpdate strategy which does not natively control traffic flow during an update.
CircleCI seems to be mostly closed source.
Pipelines
Interestingly, CI/CD and data pipelines both use directed acycliclic graphs but with very different intent. User Han on Discord eloquently spelled out the architectural distinction:
Specifically the reason is in batch data [Pipeline] processing, we tend to scale things out horizontally by a whole lot, sometimes using GPUs. This is not a common feature supported by CI/CD workflow tools. In summary:Jenkins, CodePipeline, Github Actions, TeamCity, Argo, etc ==> used to build DAGs for CI/CD, tends to have shorter run time, less compute requirement, and fairly linear in dependencies.
Airflow, Dagster, Prefect, Flyte, etc ==> used to build data and/or machine learning pipelines. It tend to have longer run time, larger horizontal scaling needs, and sometimes complex dependencies. Data pipelines also sometimes have certain needs, e.g., backfilling, resume, rerun, parameterization, etc that's not common in CI/CD pipelines
Istio is an open source GoLang project that transparently provides "a uniform way to integrate microservices, manage traffic flow across microservices, enforce policies and aggregate telemetry data."
Saturday, November 12, 2022
(GitHub) Action stations
Here are some notes I made on learning GitHub Actions:
There are some implicit environment variables. For instance, GITHUB_ENV (docs) is a temporary file that can hold environment variables like this:
echo "ENVIRONMENT=develop" >> $GITHUB_ENV
This only appears to take an effect in the next run block.
In addition to these, there are contexts, which are "a way to access information about workflow runs, runner environments, jobs, and steps." For instance github.ref that refers to "the branch or tag ref that triggered the workflow run" (docs) and you use it with something like:
if: endsWith(github.ref, '/develop')
To set up secrets you follow the instructions here. It asks you to go to the Settings tab on GitHub page. If you can't see it, you don't have permission to change them. You can reference these secrests like any other context. For example, to login to AWS:
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: '${{ secrets.AWS_ACCESS_KEY_ID }}'
aws-secret-access-key: '${{ secrets.AWS_SECRET_ACCESS_KEY }}'
aws-region: eu-west-2
Where aws-actions/configure-aws-credentials@v1 (and its ilk) are plugins to facilitate access to third party tools.
Contexts can also reference the output of actions. For example:
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: 'Build, tag, and push image to Amazon ECR'
env:
ECR_REGISTRY: '${{ steps.login-ecr.outputs.registry }}'
Where login-ecr is an arbitrary ID but outputs.registry is part of the action's data structure.
Tuesday, October 11, 2022
CI/CD in the Cloud
I asked on Discord:
Are there any tools to weave all the components together so both developers and the CI/CD tool can easily run the same suite of integration tests? I've done a PoC that takes one application in the pipeline and runs it locally while it talks to an AWS environment I've provisioned with Terraform. I intend to use docker-compose to bring up all apps altogether for integraion testing. The going is slow. How do other people tackle the same problem?
and user hubt gave me some great insights. Here is an abridged version of his/her answers:
Some people move to Kubernetes instead. there are tons of CI/CD tools built on it.
I have used shared dev environments for a long time. Not having individual dev environments has its drawbacks but it also has strengths too.
One advantage is everyone sees the same data and database. At some point you start to run into problems where your bugs are based on specific data. With individual dev environments, these bugs are hard to reproduce for everyone unless there is a way of sharing both code and data. If you have a a super solid consistent data set that you have curated and built for every test then you are miles ahead of the game. but maintaining and updating that data set and test suite is very hard to do.We have a promotion process from shared dev to staging to prod. We run continuous integration tests in dev and staging. People break those on a regular basis.
We don't have developers run them. We just [run] them automatically hourly. Our process works reasonably well. The hourly integration test cadence is driven more by how long the integration test suite takes rather than being strictly hourly.[If somebody deletes a file from the dev environment thinking it wasn't needed] they would have broken dev and we would debug dev or staging to fix things.
Admittedly this probably doesn't scale for 100+ developers. I deal with a team of a few dozen developers so it isn't quite the same as supporting a team of 4 or a team of 100+.
We also have separate unit tests for some things. those are targeted and should be 100% successful. Integration tests can fail for a variety of different reasons unrelated to someone's code. Unit tests should not. So, yes the integration tests are more general.
[Regarding creating the dev environment on every test suite run] I think that only makes sense in very tightly controlled and constrained environments like if you have an application that has no database.
It's not like we are able to recreate our production environment regularly, so in some ways you want to handle things like you do production. Recreating environments per test run would make sense if you are shipping packaged software. But as a cloud product/web service it makes less sense.
Discord user dan_hill2802 also gave his insights here and here:
We have developers able to easily build AWS environments for development and testing and tear down again when done. The pipelines use the same mechanism to build independent environments in AWS, run tests, then tear down
We aim to have applications as cloud agnostic as possible, so a lot of application development is done with docker containers locally. We also use localstack.cloud [open source community edition] for mocking some of the cloud endpoints. However, it's still not quite representative enough so we enable developers to deploy the application and all supporting infrastructure in AWS, they can also attach to their environment for debugging. The "deploy" is a wrap up of a few steps, including Terraform apply, setting up database users for the app, seeding the database with baseline data. The teardown then does the reverse of thatThe individual AWS stacks are mostly for infrastructure engineers working on the Terraform code, where local really isn't an option. We then made it available to the Devs too who were asking about being able to test in AWSThe tools we use (in order of priority)
- Terraform
- Make
- Kitchen
- Concourse CI (but could use any other CI tool)
Number 4 also allows Devs to create AWS environments without any AWS credentials.
- Upon each raised PR, a test environment is built using Terraform inside GitHubActions.
- GitHubActions checks out the code, runs the Alembic DB scripts to prepare the Postgres database, and then runs the integration tests against this DB.
- Postgres is torn down after the tests. This is partly to save money (the DB is unused except during PRs) and partly to ensure nobody messes up the Alembic scripts. That is, we ensure that DB schema can always be built from scratch.
- Created some Python code to create synthetic test data. This also uses pyarrow.parquet to read the schema of a file used in manual tests to pad the synthetic data with columns which are not used as part of the test. Athena will complain if you don't do this.
- Upload the synthetic data to an S3 bucket that Athena can see by registering it with AWS Glue (think Hive metastore). You only have to register the files once and that was done manually since this Athena instance was built manually. Automating this was left as tech debt.
- Now, the integration tests can run SQL queries through Athen with boto3.client(service_name="athena", ...). This data will be the same on every run of this test suite.
Deploying the environment is slow and can take well over 10 minutes.
- It takes approximately 4 minutes to create a Postgres DB in AWS and another 4 minutes to tear it down when done.
- It also takes a few minutes to create a Python environment (that's fresh each time we run the tests) since we need to download and installing all the dependencies.
- Finally, it appears that GitHubActions runs in the US while our DBs are in the EU and this cross-Atlantic traffic is slow (think loading hundreds of megs of baseline data into a DB).
Monday, October 10, 2022
AWS DevOps cheat sheet
Miscellaneous notes that I keep needing to refer to.
Read a file in S3 from the command line
If you want to, say, look at the numbof lines in a file, run:
aws s3 cp s3://BUCKET/PATH - | wc -l
Note the critical hypen.
Avoid Pagination
The --no-cli-pager switch is the daddy here. If I want to list all my RDS databases in a script, I don't want them to be paginated nor truncated, so run:
aws rds describe-db-instances --no-cli-pager
Logging
Run something like this to get the logs:
aws logs tail YOUR_LOG_GROUP_NAME --since 5d
where YOUR_LOG_GROUP_NAME is in your container_definitions/options/awslogs-group of resource "aws_ecs_task_definition" "backend_task" if you're using Terraform.
The 5d is the last 5 days, but it could be, sah 1h (the last hour) or --follow if you want to tail it.
Caveat
Beware that you configure the health check of your services correctly. One of our services was returning an HTTP error 404 for the page the health checker was trying to hit. Everything was fine other than a page was missing. But AWS saw the 404 and kept deciding to kill the service. Oops.
Terraform
This isn't specifically related to AWS but since I use the two together, I'll post here.
Create new workspaces to isolate yourself from breaking other people's work with:
terraform workspace new staging
Bad boys and girls might change the environment by hand. In this case, you have tell Terraform to import it with something like:
terraform import -var-file="vars/production.tfvars" -var="x=y" ... aws_db_subnet_group.my_db my-db
You can see the dependency graph of your infrastructure with:
terraform graph | dot -Tsvg > ~/Pictures/graph.svg
Docker
This has helped me a few times when trying to work out locally why my Docker container does not work when depoyed in ECS. Say your container dies immediately. You can debug (on Ubuntu) the docker daemon itself with:
journalctl -f -u docker.service
Then you can see the IDs of short lived containers and get its logs (see the Docker docs)
Thursday, September 22, 2022
Rage against the Markup
Markup is pervasive in DevOps. But markup is also:
- hard to refactor
- has limited control flow
- not type-safe
- hard to test
Refactoring
I see in our codebase something like this:
- name: Set env to Develop
if: endsWith(github.ref, '/develop')
run: |
echo "ENVIRONMENT=develop" >> $GITHUB_ENV
- name: Set env to Staging
if: endsWith(github.ref, '/main')
run: |
echo "ENVIRONMENT=staging" >> $GITHUB_ENV
- name: Set env to Productions
if: endsWith(github.ref, '/production')
run: |
echo "ENVIRONMENT=production" >> $GITHUB_ENV
Not only is this ugly, it's copy-and-pasted everywhere. I can't refactor it and what's more, there is a...
(Lack) of Control
Imagine I want to create an AWS Postrgres instance with Terraform then provision the DB with a schema using the Python library, Alembic all via GitHub Actions? GHA can call the Terraform file and create a DB, but how do I get the URL of that Postgres instance so I can give it to Alembic for it to connect and create the tables? Weaving different technologies together in a Turing Complete language is easy; with markup: less so. I had to hack some calls to the AWS CLI command and parse the JSON it returned, all in bash.
Type-safety issue #1
An example of a lack of type safety can be found in any Terraform script. We had something like this:resource "aws_ecs_task_definition" "compute_task" {
family = var.task_name
container_definitions = <<DEFINITION
[
{
"name": "${var.task_name}",
"image": "${aws_ecr_repository.docker_container_registry.repository_url}:${var.tag}",
...
Which leads to testing. I made my changes in my GitHub Actions file to use the password in the secret ${{ secrets.DB_PASSWORD-staging }} and ran my mock GHS so:
act -j populate -s DB_PASSWORD-staging=...
![]() |
| Spot the errors |


