Package your software
This document walks you through the process of packaging your software for Antithesis so that you can run your first test.
1. Containerize your software
Antithesis runs your software using Linux container images, similarly to how Docker and Kubernetes do it. So the first step is to build a container image that includes your software and any of its dependencies. If you operate a microservices architecture, you should provide a separate container for each of your services, though it’s fine if they share intermediate layers.
If you or your customers already deploy your software via containers, there’s a decent chance your existing images will work out of the box. However there are a few common gotchas that you should be aware of:
- Within the Antithesis environment, your various containerized services will have network access to each other, but not to the broader internet. If your services perform network calls to the public web on startup, for example to fetch a software dependency or data file, you should move this into the container build process instead.
For example, DON’T do this:
FROM docker.io/ubuntu:latest
COPY my_app /opt/my_app
CMD curl --fail https://example.com/data > /opt/data && /opt/my_app /opt/data
Do this instead:
FROM docker.io/ubuntu:latest
COPY my_app /opt/my_app
RUN curl --fail https://example.com/data > /opt/data
CMD /opt/my_app /opt/data
-
If you have chosen to instrument your software, then depending on the language your software is written in, you may need to install additional runtime dependencies into your container.
You may also need to add “symbol files” created by the instrumentor or by your build toolchain to a special location in the software container image.
It’s fine not to worry about instrumentation for your very first run. Please see the language-specific instructions in the instrumentation section of our documentation for more.
-
Antithesis runs your software on x86-64 CPUs. Ensure that your software is compiled for this architecture by building containers with the following option:
docker build --platform linux/amd64
.
2. Containerize your dependencies
Antithesis runs your system in a hermetic simulation environment. Everything your system depends upon must also be deployed into that environment, or suitably mocked.
- If your system depends on other first-party services that you ordinarily control and operate, for example a local database node, it’s generally easiest to just put those in additional containers and run them in the Antithesis simulation as well. In the next step, we will see how to use container orchestration technologies to enable your containers to discover each other.
- If your system depends on third-party services that you cannot run yourself (such as an API controlled by another company), then it’s easiest either to disable this part of your system’s functionality, or to build a local mock that emulates its behavior. This mock can run alongside your software in the same container, or it can run in a separate container and make itself available over the network. The latter option enables Antithesis to perform fault injection on the connection between your software and the third-party service.
- For some very popular third-party services, Antithesis provides sophisticated local mocks or emulators, which you can just use with no additional effort. Moreover we transparently emulate a number of AWS APIs and services using Localstack. So if you are already using Localstack in your integration tests, it should be straightforward to lift and shift to Antithesis.
3. Compose your first test
A test template is the code that makes your software do something. See Compose your first test for guidance on writing one. Once it’s written, your test template must also be built into a container image. Your test template will run in a separate container from the services you’re testing, so that we can fault inject the connections between them.
If your test template makes use of the Antithesis SDK, you may need to install additional runtime dependencies into this container as well. See our SDK documentation for more information.
4. Create a configuration directory
Now you’re going to create a directory in our local filesystem, called config
, into which we will put the following items:
- Any configuration files expected by the containers: license files, settings, or other resources.
- Empty directories for any mountpoints of volumes that will be shared into the containers. You should create each of these under the common prefix
volumes/
- A container orchestration file (see next step).
There are two main reasons to use external volume mounts for your containers:
- Storing data that needs to be durable even if the container is restarted (for example database files).
- Storing logs that you want to have captured by Antithesis. In general, Antithesis will capture everything written to the container’s stdout and stderr streams, but if your service writes its logs to files instead, then these should be written to a volume that is mapped out of the container, so that Antithesis can still capture them.
Here are the contents of our example configuration directory:
$ ls -a config/
.
..
docker-compose.yaml
license
volumes
$ ls -a config/volumes
.
..
database
workload-logs
5. Set up container orchestration
Antithesis supports a few different orchestration technologies, but the one that’s easiest to set up is Docker Compose. This involves creating a file called docker-compose.yaml
that lists each of the services you want to have running, the container image that service should be started from, any external volumes that should be mounted into that image, and other options. We support most of the options available in a Compose file.
Here’s an example:
version: '3.0'
services:
application1:
container_name: application1
hostname: application1
image: mycompany/app:antithesis-tag
networks:
antithesis-net:
ipv4_address: 10.20.20.1
application2:
container_name: application2
hostname: application2
image: mycompany/app:antithesis-tag
networks:
antithesis-net:
ipv4_address: 10.20.20.2
database:
container_name: database
hostname: database
image: docker.io/mysql:latest
volumes:
- ./volumes/database:/usr/bin/database/data
networks:
antithesis-net:
ipv4_address: 10.20.20.3
workload:
container_name: workload
hostname: workload
image: mycompany/workload:antithesis-tag
volumes:
- ./volumes/workload-logs:/usr/bin/workload/logs
networks:
antithesis-net:
ipv4_address: 10.20.20.128
networks:
antithesis-net:
driver: bridge
ipam:
config:
- subnet: 10.20.20.0/24
A few items to note in the above example:
- Two application services are being run from the same underlying container image. In general, Antithesis is most useful for testing high-availability configurations of your service. For example, we can try things like the following: we can cause one of the application servers to fail while the other remains up and see if the workload/client is able to reconnect successfully.
- Some of the container images are coming from your internal container registry, and others (the MySQL dependency) are coming from a public Docker Hub repository. We support either option.
- Two of the services have requested external volume mounts. Data stored in these directories will be mapped out of the container filesystem, and will outlive the container’s own lifetime. The volume declarations are referring to the empty directories created in the configuration directory (see above). Since we are going to use the configuration directory as our working directory, these relative paths will resolve correctly.
- The example explicitly defines a network and allocates static IPs to each of the services running on that network; both of these choices are optional.
- In addition to network address, the services in the above example can reach each other by hostname. Our container orchestration will automatically generate suitable DNS entries from the names of each of the services, and inject them into the environment of every other service.
Contact us if you need help setting up suitable container orchestration.
6. Validating what you have so far
If you’ve followed the steps above, you should now be able to test your entire setup locally before deploying it to the Antithesis environment. Running docker-compose up
from the root of your configuration directory should produce a running system with a connected workload.
However, we want to test that this all still works without any access to the internet, because Antithesis does not provide external internet access. The easiest way to do this is to first enter into a network namespace before running the command:
$ pwd
/home/user/config
$ unshare -n
[2] $ docker-compose up
...
[Lots of output]
...
If everything still comes up and works, your services all find each other, and your workload begins running, all when run without access to the internet, then we’re nearly there!
7. Building a configuration image
Now that everything works locally when run from within your configuration directory, the only thing left is to ship the contents of your configuration directory to Antithesis as well. Since you’re already going to be sending us a bunch of container images, the easiest thing to do is to create one more container image to hold the contents of the configuration directory, and send us that as well. We call this the “configuration image”.
A Dockerfile that copies the configuration files and adds the directories into a scratch image is generally sufficient:
FROM scratch
COPY config/docker-compose.yaml /docker-compose.yaml
ADD config/license /license
ADD config/volumes/database /volumes/database
ADD config/volumes/workload-logs /volumes/workload-logs
8. Push your containers
Antithesis has configured a container registry for you. If your usage tier qualifies you for enhanced tenant isolation, then this is literally run on separate hardware from every other tenant, so that even a bug in our systems or configuration would not be sufficient to enable other users to read your container data. As part of your Antithesis onboarding, we sent you a credential file $TENANT_NAME.key.json
.
To authenticate to your container registry, run the following command:
$ cat $TENANT_NAME.key.json | docker login -u _json_key https://us-central1-docker.pkg.dev --password-stdin
Now you are locally authenticated to the registry, and can run all other docker commands as normal.
The registry you should push to is located at: us-central1-docker.pkg.dev/molten-verve-216720/$TENANT_NAME-repository/
If you have a local image named app
, you can tag and push it as follows:
$ docker tag app us-central1-docker.pkg.dev/molten-verve-216720/$TENANT_NAME-repository/app:antithesis-latest
$ docker push us-central1-docker.pkg.dev/molten-verve-216720/$TENANT_NAME-repository/app:antithesis-latest
Here, antithesis-latest
is just an example of a tag that you could use. If you have multiple Git branches, test configurations, or environments that you want to test, you should pick a different tag for each of them. However, it’s important that every container image within a given branch, test configuration, or environment use the same tag. When Antithesis begins running your test, it will pull all of your containers with a given tag.
9. Wrapping up
That’s it! You now have basic integration with Antithesis. From here, we recommend running some tests and iterating with us on the results. When you’re satisfied by the setup, you should configure your CI system to automatically build and push the above container images every night.
Contact us if you have questions.