traefik is a great tool... with a lousy configuration file format. TOML's redundancy and verbosity combined with traefik's deeply nested data structures makes it really hard to write routes manually. What's more, route files have to be placed in a global, common directory, rather than being versioned with their projects.
fik is a tool that solves both of these problems, by allowing traefik .toml route files to be generated from shell scripts, markdown files, YAML, or JSON, using simple directives like this in a configuration file:
backend foo "http://bar:80"
match something "Host:something.com"
match another1 "Host:another.com;PathPrefix:/foo"
backend bar "http://baz:80"
match demo-frontend-options
pass-host
priority 1000
must-have "Host:www.example.com,example.com"
must-have "PathPrefix:/foo,/bar"Or using the equivalent YAML:
backends:
foo: { servers: { foo: { url: "http://bar:80" } } }
bar: { servers: { foo: { url: "http://baz:80" } } }
frontends:
another1:
backend: foo
routes: { route001: { rule: "Host:another.com;PathPrefix:/foo" } }
demo-frontend-options:
backend: bar
passHostHeader: true
priority: 1000
routes:
route001: { rule: "Host:www.example.com,example.com" }
route002: { rule: "PathPrefix:/foo,/bar" }
something:
backend: foo
routes: { route001: { rule: "Host:something.com" } }By placing the above shell script in a .fik file, or the above YAML in a fik.yml file (or one or both as shell and yaml blocks in a fik.md file!), you can then use fik up to write the route specifications to a uniquely-named .toml file in an appropriate global directory (/etc/traefik/routes by default).
Your projects aren't limited to a single YAML block or even configuration file, though: shell or .md files can include others, and .md files can contain multiple YAML or shell blocks. You can even generate your routes programmatically!
However you generate them, you can use fik toml or fik json to see the resulting configuration, fik diff and fik watch to compare them against the last published routes, and fik down to remove all the routes from Traefik (without deleting your configuration).
- Installation and Requirements
- Basic Use
- Traefik Configuration
- Command-Line Interface
- Routing Directives
fik is a bash script that requires bash 4.4 or better to run, along with the following minimum requirements:
- jq 1.5 (if you're on linux, your distro probably has a package for it)
- pytoml installed for the default Python interpreter (via
pip install pytoml, or your Linux distro'spytomlpackage)
To install fik itself, simply copy the binary to a directory on your PATH. (If you have basher, you can do this with basher install bashup/fik.)
You may also want some additional tools, depending on what fik features you plan to use:
- If you are using
.ymlfiles or YAML blocks, you will need at least one of the following:- a
yaml2jsoncommand (like this one) on yourPATH, - PyYAML installed for the default Python interpreter (e.g. via
pip install PyYAML), OR - yaml2json.php on your
PATH
- a
- If you're using
fik watch, you need atputcommand (typically installed with ncurses) - Installing
colordiffand/ordiff-highlightwill enable colorized diffs - Installing
pygmentizewill enable colorized TOML output
To use fik, simply create one or more of the following files:
fik.json-- route information in JSON formatfik.yml-- route information in YAML format.fik-- route information expressed as shell commands, plus optional configuration or extension functionsfik.md-- a jqmd-style literate devops file, containingshell,json, and/oryamlblocks
When run, fik searches upward from the current directory until it finds one of the above file(s), then executes in that directory (the "project"), loading any of those files that exist in the listed order, combining and overwriting any parts of the configuration that are defined by earlier files.
Shell code in .fik and fik.md both have access to the full jqmd and mdsh APIs, as well as the routing directives supplied by fik. Shell code can also add new fik subcommands by defining shell functions named fik.X, where X is the name of the desired subcommand. (So the fik.foo function would be run if you invoked fik foo on the command line.)
fik also runs shell code found in /etc/traefik/fikrc and $HOME/.config/fikrc (if they exist), reading them before searching for the project directory. You can use these files to change the default traefik routes directory, or to add new commands or custom directives just as in a .fik or fik.md file.
By default, fik writes fik-{project UUID}.toml files to /etc/traefik/routes, under the assumption that Traefik is watching for .toml route files there.
If you need to change this default directory, you can do so by setting FIK_ROUTES in any shell code read by fik. That is, either the global /etc/traefik/fikrc and $HOME/.config/fikrc, or a project-specific .fik or fik.md. (Note: exporting FIK_ROUTES in the shell environment has no effect; FIK_ROUTES can only be meaningfully set from within a fik configuration file.)
Whatever you set FIK_ROUTES to (or even if you don't set it at all), you will need to make sure Traefik is configured to read routes from there, or from one of its parent directories. The default FIK_ROUTES setting assumes you've configured Traefik's file provider like this:
[file]
directory = "/etc/traefik/routes/"
watch = trueIf you are running Traefik in a docker container and fik on the host, FIK_ROUTES should be the host's path to the directory, while the Traefik setting should reflect the container's path.
Each fik project has its own, automatically-generated .toml routing file under $FIK_ROUTES. This file's name must be unique to each project, to prevent one project's routes from overwriting another's.
To avoid the need for manually assigning unique keys, fik automatically generates a random UUID and saves it in a .fik-project file in the project directory. You must make sure this file always remains alongside the configuration files (e.g. if you move them to a new directory). Otherwise, you will end up with a new key and a duplicate fik-*.toml route file.
Similarly, you must not copy the .fik-project to other directories, or they will share the same route file and overwrite each other! (In most cases, you will also want to avoid checking this file into revision control, so that different checkouts of the same repository will have their own unique IDs and thus won't overwrite each other.)
If for some reason you are intentionally changing a project's key, you should remove its existing route file with fik down before the change, then create the new route file with fik up after the change. Alternately, if you need to leave the routes in effect during the change, you can:
- Run
fik filenameto get the project's old.tomlfilename - Do whatever you're going to do that affects the key (e.g. removing or editing
.fik-project, or moving the config files to a different directory without it) - Run
fik filenameagain, to get the project's new.tomlfilename - Rename the file given by step 1, to the filename given by step 3
You can then resume using fik up, fik diff, etc. with the new project key.
fik offers the following subcommands:
fik up-- publish the current calculated routes to a .toml file for Traefik to read.fik down-- remove the .toml file generated byfik up.fik toml-- output the current calculated routes in TOML format to the console, paging if longer than one screenful, and with colorization ifpygmentizeis installed.fik json[jq-opts] -- output the current calculated routes in jq-colorized JSON to the console, paging if longer than one screenful. jq-opts are passed along tojq, so you can e.g.fik json '.frontends.foo'to get the JSON for thefoofrontend.fik diff-- compare the current .toml file contents (in sorted JSON form) with the current generated routes (i.e. whatfik jsonwould output), as a paged, unified diff, with colorizing ifcolordiff,diff-highlight, and/orpygmentizeare available.fik watch-- output the first screenful offik diffevery 2 seconds.fik key-- output the current "project key" (see TOML Filenames, below, for more info).fik filename-- output the absolute path of the .toml file thatfik upwill create andfik downwill remove.
Note: commands' output is only paged or colorized if sent to a tty, or if FIK_ISATTY=1 is in the environment. (FIK_ISATTY=0 can be set to disable coloring and paging even if the output is being sent to a tty.)
The pager can be set using FIK_PAGER, which defaults to less -FRX. Colorizing can also be controlled with these environment variables:
FIK_TOML_COLOR-- colorizer for TOML output, defaults topygmentize -f 256 -O style=igor -l tomlif thepygmentizecommand is availableFIK_COLORDIFF-- main colorizer fordiffoutput, defaults tocolordiffif thecolordiffcommand is availableFIK_DIFF_HIGHLIGHT-- line-diff highlighter fordiffoutput, defaults todiff-highlightif thediff-highlightcommand is available
The following directives can be used to define routes from shell code in .fik or from shell blocks in fik.md:
-
backendname [urls...] -- set the current backend to name, optionally adding any given urls to the backend'sserverslist (equivalent to callingserveron each url). -
serverurl -- add url to theserversfor the current backend. No duplicate-checking is done: keys are assigned sequentially asurl001,url002, etc. -
matchname [rules...] -- set the current frontend to name, optionally adding any given rules to the frontend'srouteslist (equivalent to callingmust-haveon each rule). The frontend'sbackendis set to the current backend -
pass-host[bool] -- set the current frontend'spassHostHeaderto bool, which defaults totrueif not given. (If given, bool must betrueorfalse.) -
must-haverule -- add rule to theroutesfor the current frontend. No duplicate-checking is done: keys are assigned sequentially asrule001,rule002, etc. -
prioritypriority -- set the current frontend'spriorityto priority -
redirectregex replacement [permanent] -- set up a redirect pattern for the current front-end; permanent must be eithertrueorfalse, and defaults tofalseif not given. -
request-headername value -- add a custom request header named name, with a value of value. -
response-headername value -- add a custom response header named name, with a value of value. -
require-ssl[redirect [temporary]] -- enable or disable automatic SSL redirect for the current front-end. Arguments must betrueorfalse; if no arguments are given, redirection is on and permanent. If redirect isfalse, redirection is disabled. If only one argument is given, it's used for both temporary and permanent, sossl-redirect trueenables SSL redirection and makes it temporary. -
tlscert key [entrypoints...] -- add a certificate/private-key pair to zero or more entrypoints. If no entrypoints are given, the certificate is added to all entry points. cert and key can be either filenames or the actual PEM data; if they're filenames, they must be readable by Traefik.(Note: since Traefik does not currently support watching changes to certificate and key files, it's usually better to include the actual certificate data, e.g. using
tls "$(</my/cert.pem)" "$(</my/key.pem)". Then, the certs can be updated by runningfik up, perhaps via adehydratedhook or a cron job.)
fik supports using dehydrated certificates for Traefik dynamic TLS, using the following directives:
-
hydratedomain-or-alias [entrypoints...] -- load the dehyrated certificate named domain-or-alias from$CERTDIR, adding it to the specified entrypoints. If no entrypoints are given, the certificate is added to all entry points. If$CERTDIRis empty,dehydrated-configis run to locate it. -
hydrate-all[entrypoints...] --hydrateeach certificate listed in$DOMAINS_TXT, adding them to the specified entrypoints. If no entrypoints are given, all certificates are added to all entry points.(Note: aliases are supported, so if a line reads e.g.
some-domain.com *.some-domain.com >some-domain-com, the certificate will be looked for under$CERTDIR/some-domain-com/. If$DOMAINS_TXTis empty,dehydrated-configis run to locate it.) -
dehydrated-config[config-file] -- use the specified config-file to identify the$CERTDIRand$DOMAINS_TXTthat will be used by any subsequenthydrateorhydrate-allcommands. If config-file sets aCONFIG_D, any$CONFIG_D/*.shfiles are loaded, too.If no config-file is given, a dehydrated config file is searched for using the same algorithm as that used by dehydrated. (That is, by looking for a
configfile in/etc/dehydrated,/usr/local/etc/dehydrated,$PWD, and finally the directory where thedehydratedcommand is installed.)
Assuming your dehydrated configuration is in a standard location, you can trivially add all your dehydrated certificates to all Traefik TLS entrypoints using just the hydrate-all directive. For example, you could create an /etc/dehydrated/.fik file containing just hydrate-all, and then have your dehydrated exit_hook run cd "$BASEDIR" && fik up, to automatically refresh Traefik's certificate list when certificates are added or renewed.
These directives do not cover all possible configuration settings, such as rate limits, healthchecks, etc. You can configure these directly using yaml or json blocks in fik.md (or in fik.yml or fik.json), or by using jq filter expressions (via jqmd's API functions).
fik provides two convenience functions for applying jq filter expressions to the current backend or frontend:
backend-setexpr [@]name[=value]... -- apply the jq filter string expr to the current backend, optionally setting the named jq variables from shell variables or valuesfrontend-setexpr [@]name[=value]... -- apply the jq filter string expr to the current frontend, optionally setting the named jq variables from shell variables or values
For example, the directive priority 100 is exactly equivalent to frontend-set '.priority=100' . This means you can do things like backend-set '.healthcheck.interval="10s"' to set a 10 second healthcheck interval on the current back end.
(It's important to note that filter expression strings should be single-quoted, with any string values inside them double-quoted. This is because jq string values are JSON strings (which must be double-quoted), and shell strings must be single-quoted to avoid interpolation.)
Both backend-set and frontend-set are wrappers around jqmd's APPLY function, which lets you convert shell variables and values into JSON strings or values and pass them into a jq expression. So you could write something like: backend-set '.healthcheck.interval=$i' i="$INTERVAL" to automatically JSON-encode the contents of the $INTERVAL shell variable and make it available as the jq variable $i for the duration of the filter's execution.
(For more on how APPLY works, see the jqmd functions documentation.)
fik uses the bashup/events library to let you extend its directives with event handlers. For example, if you wanted to have every frontend pass a host header, use a priority of 1000, and require SSL, you could use the shell code:
event on "frontend" pass-host
event on "frontend" priority 1000
event on "frontend" require-sslEvery match directive run after these commands will invoke the pass-host, priority 1000, and require-ssl commands before adding the given rules (if any). You can then individually override these settings on a per-frontend basis, or stop them from applying automatically by using e.g. event off "frontend" pass-host to remove an individual event handler.
(Note: if you add handlers that add rules, routes, or anything else that's accumulated rather than set, make sure you event off the old handler before you define a new one, so you don't end up with both things being added when you only want the second one. It's not as important for things like priority, since all that will happen if you add a new handler is that the priority will be set twice, but it's still a good idea.)
The currently available events and their arguments are:
event emit "backend"name [urls...] -- emitted after abackenddirective creates or selects a backend, but before any URLs are added.event emit "url"key url -- emitted when a URL is added to a backend, either via thebackenddirective or aserverdirective. The key is the automatically-generated key under.backends[$fik_backend].servers, and the url is the URL being added.event emit "frontend"name [urls...] -- emitted after amatchdirective creates or selects a frontend and sets its backend, but before any routing rules are added.event emit "rule"key rule -- emitted when a rule is added to a frontend, either via thematchdirective or amust-havedirective. The key is the automatically-generated key under.frontends[$fik_frontend].routes, and the rule is the rule being added.
When any of the above events run, the name of the current backend is in $fik_backend in both shell and jq variables. When the frontend and rule events are run, the current frontend name is in $fik_frontend in both shell and jq variables.
Please see the bashup/events documentation for more information on adding and removing event listeners or handling event arguments.
If you are using a fik.md file, you can define your own directives using YAML blocks and interpolation. For example, if your .fik md contains this:
```yaml @func health-interval i="$1"
backends:
\($fik_backend):
healthcheck:
interval: \($i)
```Then at any point below that block, you will be able to call e.g. health-interval "10s" to set the current backend's healthcheck interval. (Your blocks can interpolate $fik_backend and $fik_frontend to get the name of the current back or front end.) In addition, since health-interval is now a directive in its own right, you can add something like:
```shell
event on "backend" health-interval "30s"
```to set a default health-interval for every subsequent backend.
Custom directives can be placed in a separate .md file, which can then be loaded using mdsh-run from any script block or file. (So if you have global common directives, you can put them in a global .md file and then mdsh-run that file from your $HOME/.config/fikrc or /etc/traefik/fikrc, to make them available to all projects.)
For more on the process of creating shell functions like these from markdown blocks, see the jqmd documentation on reusable code blocks.
Currently, Traefik (at least through version 1.7.0-rc5) doesn't log the correct front-end name when a backend is shared between multiple front-ends. You can work around this issue by defining unique backends for each frontend, using the unique-backend and/or unique-backends directives.
The unique-backend [frontend] directive flags the named frontend as needing a unique backend. (If no frontend is given, the current frontend is assumed.)
When configuration is complete, frontends flagged with unique-backend will have unique backends created for them, whose names will be of the form "backend: frontend", where backend is the backend previously associated with frontend. (The settings for the new backend will be copied from the old one.)
To avoid having to invoke unique-backend after every match, you can use unique-backends on, which is equivalent to event on "frontend" unique-backend. (That is, unique-backend will be called for every new match.) unique-backends off turns this behavior off again.
Note: this feature is strictly a workaround until the underlying Traefik issue is fixed. Be sure to fik diff your project's routes and review the effects it has before you fik up with this directive in use. (Not that it isn't always a good idea to fik diff before you fik up!)