Dev::ContainerizedService
Dev::ContainerizedService
This module aims to ease the process of setting up services (such as Postgres) for the purpose of having a local development environment for Raku projects. For example, one might have a Raku web application that uses a database. In order to try out the application locally, a database instance needs to be set up. Ideally this should be effortless and also isolated.
As the name suggests, this module achieves its aims using containers. It
depends on nothing more than Raku and having a functioning docker
installation.
Usage
Getting Started
Let's assume we have a web application that uses a Postgres database and expects
that the DB_CONN_INFO environment variable will be populated with a connection
string.
To make a development environment configuration using this module, we create a
script devenv.raku:
#!/usr/bin/env raku
use Dev::ContainerizedService;
service 'postgres', :tag<13.0>, -> (:$conninfo, *%) {
env 'DB_CONN_INFO', $conninfo;
}The service function specifies the service ID, a Docker image tag, and a block that
should be called when the service is up and running. The env function, located in a
service, specifies an environment variable to be set.
We can then (assuming chmod +x devenv.raku) use the script as follows:
./devenv.raku run raku -Ilib service.rakuThis will:
Pull the Postgres docker container if required
Run the container, setting up a database user/password and binding it to a free port
Run
raku -Ilib service.rakuwith theDB_CONN_INFOenvironment variable set
If using the cro development tool, one could do:
./devenv.raku run cro runAdditional Actions
The service block is run after the container is started (service implementations include readiness checks). As well as - or instead of - specifying environment variables to pass to the process, one can write any Raku code there. For example, one could run database migrations (in the case where it's desired to have them explicitly applied to production, rather than having them applied at application startup time).
Retaining data
By default, any created databases are not persisted once the run command is
completed. To change this, alter the configuration file to specify a project name
(the name of your application) and call store:
#!/usr/bin/env raku
use Dev::ContainerizedService;
project 'my-app';
store;
service 'postgres', :tag<13.0>, -> (:$conninfo, *%) {
env 'DB_CONN_INFO', $conninfo;
}Now when using ./devenv.raku run ..., for services that support it, Docker
volume(s) will be created and the generated password(s) for services will be
saved (in your home directory). These will be reused on subsequent runs.
To clean up this storage, use:
./devenv.raku deleteWhich will remove any created volumes along with saved settings.
Showing produced configuration
When using storage, it is also possible to see the most recently passed service settings for each service by using:
./devenv.raku showThe output looks like this:
postgres
conninfo: host=localhost port=29249 user=test password=xxlkC2MrOv4yJ3vP1V-pVI7 dbname=test
dbname: test
host: localhost
password: xxlkC2MrOv4yJ3vP1V-pVI7
port: 29249
user: testWhen used while run is active, this is handy for obtaining connection string
information in order to connect to the database using tools of your choice.
Tools
Some service specifications also come with a way to run related tools. For
example, the postgres specification can run the psql command line client
(using the version in the container, to be sure of server compatibility),
injecting the correct credentials. Thus:
./devenv.raku tool postgres clientIs sufficient to launch the client to look at the database. Note that this only works when the service is running (so one would run it in one terminal window, and then use the tool subcommand in another).
Multiple stores
Calling:
store;Is equivalent to calling:
store 'default';That is, it specifies the name of a default store. It is possible to have multiple
independent stores, which are crated using the --store argument before the run
subcommand:
./devenv.raku --store=bug42 run cro runTo see the created stores, use:
./devenv.raku storesTo show the produced service configuration for a particular store, use:
./devenv.raku --store=bug42 showTo use a tool against a particular store, use:
./devenv.raku --store=bug42 tool postgres clientTo delete a particular store, rather than the default one, use:
./devenv.raku --store=bug42 deleteMultiple instances of a given service
One can have multiple instances of a given service. When doing this, it is wise
to assign them names (otherwise names like postgres-2 will be generated, and
this will not be too informative in show output):
service 'postgres', :tag<13.0>, :name<pg-products> -> (:$conninfo, *%) {
env 'PRODUCT_DB_CONN_INFO', $conninfo;
}
service 'postgres', :tag<13.0>, :name<pg-billing> -> (:$conninfo, *%) {
env 'BILLING_DB_CONN_INFO', $conninfo;
}These names are used in the tool subcommand:
./devenv.raku -tool pg-billing clientCustom images
Sometimes you might not want to use the default docker images for a service; for
exmaple, if doing Postgres with pgvector you might need an image with that
extension installed instead. In that case, pass in image-name:
service 'postgres', :image-name<pgvector/pgvector>, :tag<0.8.0-pg17>, -> (:$conninfo, :$host, :$port, :$user, :$password, :$dbname) {
...
}Is this magic?
Not really; the Dev::ContainerizedService module exports a MAIN sub, which is
how it gets to provide the program entrypoint.
Available Services
Postgres
Either obtain a connection string:
service 'postgres', :tag<13.0>, -> (:$conninfo, *%) {
env 'DB_CONN_INFO', $conninfo;
}Or the individual parts of the database connection details:
service 'postgres', :tag<13.0>, -> (:$host, :$port, :$user, :$password, :$dbname, *%) {
env 'DB_HOST', $host;
env 'DB_PORT', $port;
env 'DB_USER', $user;
env 'DB_PASS', $password;
env 'DB_NAME', $dbname;
}Postgres supports storage of the database between runs when store is used.
The client tool is available, and runs the psql client:
./devenv.raku tool postgres clientRedis
Obtain the host and port of the started instance:
service 'redis', :tag<7.0>, -> (:$host, :$port) {
env 'REDIS_HOST', $host;
env 'REDIS_PORT', $port;
}Redis is currently always in-memory and will never be stored.
The service I want isn't here!
Fork this repository.
Add a module
Dev::ContainerizedService::Spec::Foo, and in it write a class of the same name that doesDev::ContainerizedService::Spec. See the role's documentation as well as other specs as an example.Add a mapping to the
constant %specsinDev::ContainerizedService.Write a test to make sure it works.
Add an example to the
README.md.Submit a pull request.