Quickstart
Introduction
In this quickstart we will install the caddy service on a host named ivy.
For attributes management we will use the default age based encrypted vaults. If you do not wish to setup age encryption, you can skip the steps marked OPTIONAL-AGE and use MATERIA_ATTRIBUTES='file'.
Installation
Building and installing from source (OPTIONAL)
(OPTIONAL - skip this section if you're using a container or pre-made release)
Clone the repository and run mise build to generate binaries. If you do not wish to install mise you may build it with the normal go tooling: go build -ldflags="-w -s" -o bin/materia ./cmd/materia/.
Assuming you use mise to build, this will have generated binaries for amd64 and arm64 in the format bin/materia-<architecture>.
Copy the binary to the destination host: scp bin/materia-amd64 root@ivy:/usr/local/bin/materia
Verify it works by running materia version.
Building your repository
Like any GitOps tool, Materia needs a source repository that describes the desired state for your nodes. For the sake of this quickstart we're going to assume you already know how to create and push to a Git repository.
Create the base materia repo
Create a git repository on your workstation named whatever you like; we're going to assume it's called materia-repo.
Inside the git repo, create the base directories:
mkdir components attributes
Finally, create a manifest file:
touch MANIFEST.toml
This manifest file will describe what components go on what hosts. We'll get back to it shortly.
Create the caddy component
Components are what materia uses to refer to a service and its associated resources (config files,data files, etc). They are the basic building blocks of a Materia repository and are similar to an ansible role.
For this example we will create a component for the caddy service.
Components are organized by directories under the components/ directory. Create a directory for caddy:
mkdir components/caddy
Create the resources for Caddy
Caddy requires the following resources:
- a Container Quadlet resource to run the caddy container
- Two Volume Quadlet resources for caddy's data and config volumes
- a File resource for caddy's config file
- a Manifest file that describes the component's metadata. All components have a Manifest file, even if it's empty.
The next few steps are done in components/caddy:
cd components/caddy
Create the Caddy container Quadlet resource
Create a file caddy.container.gotmpl for the Caddy container Quadlet: touch caddy.container.gotmpl. We want to template attributes into the file later, so make sure the file ends with .gotmpl to designate it as a templated resource. Materia uses the standard Go Templating engine.
Using your editor of choice, insert the following lines into the file we just created:
[Unit]
Description=Caddy reverse proxy
[Container]
Image=docker.io/caddy:{{ .containerTag }}
ContainerName=caddy
AddCapability=NET_ADMIN
Volume=caddy-data.volume:/data
Volume=caddy-config.volume:/config
Volume={{ m_dataDir "caddy" }}/conf:/etc/caddy:Z
{{ snippet "autoUpdate" "registry" }}
# local web data
{{- if ( exists "localWeb" )}}
Volume={{ .localWeb }}:/srv/www:Z
{{- end }}
PublishPort=443:443
[Service]
ExecReload=podman exec caddy caddy reload --config /etc/caddy/Caddyfile
[Install]
# Start by default on boot
WantedBy=multi-user.target default.target
The text contained in brackets are Go Templating variables, e.g. Image=docker.io/caddy:{{ .containerTag }} would become Image=docker.io/caddy:latest if containerTag is set to latest.
Materia also includes some ease of use functions like exists and m_dataDir that are referred to as macros.
Materia also includes pre-made snippets of text such as "autoUpdate".
Create the data and config Volume resources
Create two files, caddy-config.volume and caddy-data.volume. Since these will not be templated files, they do not need to end in .gotmpl.
touch caddy-config.volume caddy-data.volume.
Both files should have the same content:
[Volume]
Create the config File resource
Now to create a Caddyfile for caddy's config. We are going to create this in an subdirectory that will be bind-mounted into the container; this is to make it easier for Caddy to see when the Caddyfile changes on systemctl reload and to show off how subdirectories will be translated as-is from the repository to the target host.
First create the subdirectory:
mkdir conf
Then create the Caddyfile template:
touch conf/Caddyfile.gotmpl
Insert the following line into your freshly created Caddyfile:
{{ .caddyfile }}
In the real world most config files are simple and can be templated more directly i.e. ConfigOption = {{ .configValue }} but since Caddy's is more complicated and this is a quickstart, we're going to store the entire configuration as an attribute.
Create the Manifest resource
The last thing we need is a manifest file for the component. Create the file in the root of the component (i.e. materia-repo/components/caddy/)
touch MANIFEST.toml
Add the following content:
[Defaults]
containerTag = "latest"
[[Services]]
Service = "caddy.service"
The [Defaults] section is a TOML table describing default attribute values. In this case, the containerTag attribute is set to latest by default.
The [[Services]] section is a TOML array describing what services the component cares about. They can be either a part of the component or installed seperately. In this case, the only service that is defined is the caddy.service. This means that when the component is installed materia will start the caddy.service unit, when the component is removed it will make sure the service is stopped, and if the service is detected as not-running when materia runs it will attempt to start it again.
For more details, such as how to set services to restart when certain files are updated, see the component section of the manifest reference.
Create repository attributes
We've created the caddy component, but some of the resources in it use attributes. It's time to setup our attributes engine and store some attributes.
By default, the age attributes engine will look for *.age files named either vault.age, attributes.age, or the hostname of the node e.g. ivy.age.
We will be setting up two age-based vaults in our repository: one that applies to all hosts (attributes/vault.age) and one that only applies to ivy (attributes/ivy.age). For the age attributes engine, a vault is just an encrypted TOML file.
If you do not have the age cli tool installed, follow the instructions here to install it. While materia has age decryption built in it does not manage attributes vaults on its own and expects you to handle it.
Create the general vault
First we create the vault used by all hosts. Create a file named attributes/vault.toml:
touch attributes/vault.toml
Put the following content in it:
[global]
containerTag = "stable"
[components.caddy]
containerTag = "latest"
Attributes in the [global] section will be available to all hosts and all components.
Attributes in a [components.componentname] section will only be available to the component componentname.
With this file, all components with the containerTag attribute will have the value stable, except for the caddy component which will have latest.
Create the host vault
Next, create a vault that will only apply to components installed on ivy:
touch attributes/ivy.toml
Insert the following content in it:
[components.caddy]
localWeb = "/srv/www"
This means the localWeb attribute on ivy and only ivy will be set to "/srv/www".
Setup age based encryption (OPTIONAL-AGE)
- Create an age key:
age-keygen -o key.txt. - Extract the public key from the generated private key:
age-keygen -y key.txt. Store this somewhere convenient. - Install the key on
ivy; for this guide we'll put it in/etc/materia/key.txt. DO NOT STORE THIS IN YOUR MATERIA REPOSITORY - Encrypt the files to extracted public key:
cd attributes && for file in $(find -name '*.toml'); do age -r <INSERT PUBLIC KEY HERE> -o $(basename $file .toml).age $file; done - Once the files are successfully encrypted, remove the no longer needed raw TOML files:
rm attributes/*.toml.
Create the repository manifest
We have a component and we have attributes to use with the component, now to put it all together in the repository manifest.
Open the materia-repo/MANIFEST.toml file and add the following content:
[Hosts.ivy]
components = ["caddy"]
Here we've defined the ivy host ([hosts.ivy]) and assign the caddy component to it: components = ["caddy"].
Final results
The final repository directory should like this:
materia-repo/
materia-repo/components/
materia-repo/components/caddy
materia-repo/components/caddy/conf
materia-repo/components/caddy/conf/Caddyfile.gotmpl
materia-repo/components/caddy/caddy.container.gotmpl
materia-repo/components/caddy/caddy-config.volume
materia-repo/components/caddy/caddy-data.volume
materia-repo/attributes/
materia-repo/attributes/ivy.age
materia-repo/attributes/vault.age
materia-repo/MANIFEST.toml
Push your git repository and logon to ivy for the next steps.
Validate your repository with materia plan
Materia uses a plan-execute system for managing nodes. We can generate a plan ahead of time for validation purposes.
These steps will assume your git repository is at github.com/user/materia-repo and is public. If you need to use a private repository, look at the git configuration settings
Configure materia's source URL
Materia needs to know where your repository is. This can be done in a config file, but we'll just use an environmental variable
export MATERIA_SOURCE__URL=git://git@github.com:user/materia-repo
Configure materia's attributes engine
Materia needs to know where to find attributes. This can also be done in a config file, but we'll just use environmental varibables again:
export MATERIA_ATTRIBUTES=age
export MATERIA_AGE__KEYFILE=/etc/materia/key.txt
export MATERIA_AGE__BASE_DIR=attributes
Update known_hosts (OPTIONAL)
This is optional if you're using git over HTTP or you've already set this up on your host (i.e. the root user can clone your repository without any input).
Make sure your Git forge is in your known_hosts file for SSH access:
ssh-keyscan github.com >> ~/.ssh/known_hosts
Run materia plan
Use the plan command to see what materia will do when you run it. The output should look something like this:
$ materia plan
Plan:
1. Installing component caddy
2. Templating container resource caddy/caddy.container
2. Installing volume resource caddy/caddy-config.volume
3. Installing volume resource caddy/caddy-data.volume
4. Templating file resource caddy/Caddyfile
3. Reloading systemd units
4. Starting service caddy/caddy.service
$
The output is deterministic and will make sure that your repository is valid. If there's any missing attributes or other information not available to the host it will fail.
Update the host
Finally, use the update command to update ivy to the desired state:
$ materia update
Plan:
1. Installing component caddy
2. Templating container resource caddy/caddy.container
2. Installing volume resource caddy/caddy-config.volume
3. Installing volume resource caddy/caddy-data.volume
4. Templating file resource caddy/Caddyfile
3. Reloading systemd units
4. Starting service caddy/caddy.service
$ ls /etc/containers/systemd/
caddy
$ ls -a /etc/containers/systemd/caddy
. .. caddy.container caddy-config.volume caddy-data.volume .materia_managed
$ ls -a /var/lib/materia/components/caddy
. .. .component_version conf/
$ systemctl is-active caddy.service
active
$