Makefile
are often used to build assets from source code.
You can also use them to automate common and repetitive tasks. Building containers or perform some Terraform operations are some examples.
But, when comes the need to create some documentation about available targets or add new tasks, it can become more difficult.
Few years ago, I discovered Task who describe itself as:
Task is a task runner / build tool that aims to be simpler and easier to use than, for example, GNU Make.
In this post, I will introduce this tool and go throught the most common uses I had with it.
Installing task
Several methods are available in the official installation section.
On macOS, simply use Homebrew
brew install go-task/tap/go-task
Create a task to build a container
One use case I got very often, is to build containers from a Dockerfile
.
The 3 most common operations I did are
- building the container image
- pushing the built image to a Registry
- log into an instance of the container image (for some tests, etc.)
Define the build
task
For this, we just need a basic Taskfile.yml
with the below content
version: "3"
tasks:
build:
desc: Build the container image
cmds:
- docker build -t mycontainerimage -f Dockerfile .
Under the tasks
parameter, we declare a task named build
with 2 parameters desc
and cmds
.
Then, we just run task --list
Λ\: task --list
task: Available tasks for this project:
* build: Build the container image
And if we run task build
, it returns
Λ\: task build
task: [build] docker build -t mycontainerimage -f Dockerfile .
[...]
This is pretty simple
-
desc
is the task description -
cmds
are commands you want to run
Add more operations
Sometimes before pushing the built container, you need to log into an instance to test some stuff... So, how can we add this operation?
But wait... the container image name will be the same between build
and enter
tasks... Let's introduces some variables.
version: "3"
vars:
CONTAINER_IMAGE: mycontainerimage
tasks:
build:
desc: Build the container image
cmds:
- docker build -t {{.CONTAINER_IMAGE}} -f Dockerfile .
enter:
desc: Enter into the built container
cmds:
- docker run -it --rm --entrypoint=sh {{.CONTAINER_IMAGE}}
Now task --list
returns
Λ\: task --list
task: Available tasks for this project:
* build: Build the container image
* enter: Enter into the built container
And we can easily add, a new task, for the push operation
version: "3"
vars:
CONTAINER_IMAGE: mycontainerimage
tasks:
build:
desc: Build the container image
cmds:
- docker build -t {{.CONTAINER_IMAGE}} -f Dockerfile .
enter:
desc: Enter into the built container
cmds:
- docker run -it --rm --entrypoint=sh {{.CONTAINER_IMAGE}}
push:
desc: Push built image
cmds:
- docker push {{.CONTAINER_IMAGE}}
Variables
Variables can also be assigned dynamically and be nested.
For example, if we want to set image tag to the current git commit
version: "3"
vars:
CONTAINER_IMAGE_NAME: mycontainerimage
CONTAINER_IMAGE_TAG: {sh: git-rev parse HEAD}
CONTAINER_IMAGE: "{{.CONTAINER_IMAGE_NAME}}:{{.CONTAINER_IMAGE_TAG}}"
tasks:
build:
desc: Build the container image
cmds:
- docker build -t {{.CONTAINER_IMAGE}} -f Dockerfile .
enter:
desc: Enter into the built container
cmds:
- docker run -it --rm --entrypoint=sh {{.CONTAINER_IMAGE}}
push:
desc: Push built image
cmds:
- docker push {{.CONTAINER_IMAGE}}
Because Task
is built in Golang
, some Go template functions can be used to defined variables or commands:
vars:
CURRENT_TIME: {{now | date "2006-01-02"}}
A step further
Another use case I often had, is to create infrastucture resources and then init/provison them.
For this example I will
- use Terraform to create infrastructure (a Kubernetes cluster)
- then install
ingress-nginx
Helm package
By design, I will put
- Terraform files into a
terraform/
folder - Helm releases definition into a
releases/
folder
Here the structure, no matter the files content
.
├── releases
│ └── helmfile.yaml
└── terraform
└── main.tf
Create actions
I will create
- 3 actions for Terraform operations :
init
,plan
andapply
- 2 actions for Helm releases :
diff
,apply
(I use helmfile to install Helm release)
Let's create a naive Taskfile.yml
version: "3"
vars:
TFPLAN: .tfplan
tasks:
init:
dir: terraform/
desc: Init terraform
cmds:
- terraform init
plan:
dir: terraform/
desc: Show terraform resources creation
cmds:
- terraform plan -out={{.TFPLAN}}
apply:
dir: terraform/
desc: Apply resources creation
cmds:
- terraform apply "{{.TFPLAN}}"
diff:
dir: releases/
desc: Show releases diff
cmds:
- helmfile diff
apply:
dir: releases/
desc: Apply releases
cmds:
- helmfile apply
We introduce a new parameter dir
, which specified where to run cmds
.
But we have a problem, 2 tasks have the same name
! So just add a prefix
version: "3"
vars:
TFPLAN: .tfplan
tasks:
init:
dir: terraform/
desc: Init terraform
cmds:
- terraform init
plan:
dir: terraform/
desc: Show terraform resources creation
cmds:
- terraform plan -out={{.TFPLAN}}
terraform-apply:
dir: terraform/
desc: Apply resources creation
cmds:
- terraform apply "{{.TFPLAN}}"
diff:
dir: releases/
desc: Show releases diff
cmds:
- helmfile diff
releases-apply:
dir: releases/
desc: Apply releases
cmds:
- helmfile apply
Then try task --list
Λ\: task --list
task: Available tasks for this project:
* diff: Show releases diff
* init: Init terraform
* plan: Show terraform resources creation
* releases-apply: Apply releases
* terraform-apply: Apply resources creation
It's working, but not good enough to understand which actions goes with each group.
A better usage will be to add a namespace
version: "3"
vars:
TFPLAN: .tfplan
tasks:
terraform:init:
dir: terraform/
desc: Init terraform
cmds:
- terraform init
terraform:plan:
dir: terraform/
desc: Show terraform resources creation
cmds:
- terraform plan -out={{.TFPLAN}}
terraform:apply:
dir: terraform/
desc: Apply resources creation
cmds:
- terraform apply "{{.TFPLAN}}"
releases:diff:
dir: releases/
desc: Show releases diff
cmds:
- helmfile diff
releases:apply:
dir: releases/
desc: Apply releases
cmds:
- helmfile apply
And task --list
returns
task: Available tasks for this project:
* releases:apply: Apply releases
* releases:diff: Show releases diff
* terraform:apply: Apply resources creation
* terraform:init: Init terraform
* terraform:plan: Show terraform resources creation
But over time, this Taskfile.yml
will become huge!
A better approach
A feature I really like in Task is including others taskfiles.
Let's add another folder: taskfiles/
, with 2 files in it
terraform.yml
-
helmfile.yml
.
├── releases
│ └── helmfile.yaml
├── taskfiles
│ ├── helmfile.yml
│ └── terraform.yml
└── terraform
└── main.tf
Let's see the content of each files
-
releases/terraform.yml
version: "3"
vars:
TFPLAN: .tfplan
tasks:
init:
dir: terraform/
desc: Init terraform
cmds:
- terraform init
plan:
dir: terraform/
desc: Show terraform resources creation
cmds:
- terraform plan -out={{.TFPLAN}}
apply:
dir: terraform/
desc: Apply resources creation
cmds:
- terraform apply "{{.TFPLAN}}"
-
releases/helmfile.yml
version: "3"
tasks:
diff:
dir: releases/
desc: Show releases diff
cmds:
- helmfile diff
apply:
dir: releases/
desc: Apply releases
cmds:
- helmfile apply
As we can see, it looks very similar, but without namespaces.
What is now the main Taskfile.yml
content ?
version: "3"
includes:
tf: ./taskfiles/terraform.yml
releases: ./taskfiles/helmfile.yml
If we run task --list
we got the same result
task: Available tasks for this project:
* releases:apply: Apply releases
* releases:diff: Show releases diff
* tf:apply: Apply resources creation
* tf:init: Init terraform
* tf:plan: Show terraform resources creation
As you can see, includes
- make the "main"
Taskfile.yml
easier to read - give the ability to define namespaces independently of included files names
- offer better tasks organization with dedicated files
And more...
Others functionalities are also available
- prevent unnecessary work using
sources
andgenerates
parameters - dependency management with
deps
- watching files changes to run tasks again
- etc.
Conclusion
Launching repetitive commands every day is painful, Makefile
are great to avoid this.
In this article, we discovered Task as a make
alternative, with similar functionalities but I think easier to use.