DEV Community

Cover image for Introduction to Taskfile: a Makefile alternative
Sébastien Kurtzemann for Stack Labs

Posted on

Introduction to Taskfile: a Makefile alternative

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
Enter fullscreen mode Exit fullscreen mode

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 .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And if we run task build, it returns

Λ\: task build
task: [build] docker build -t mycontainerimage -f Dockerfile .
[...]
Enter fullscreen mode Exit fullscreen mode

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}}
Enter fullscreen mode Exit fullscreen mode

Now task --list returns

Λ\: task --list
task: Available tasks for this project:
* build:    Build the container image
* enter:    Enter into the built container
Enter fullscreen mode Exit fullscreen mode

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}}
Enter fullscreen mode Exit fullscreen mode

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}}
Enter fullscreen mode Exit fullscreen mode

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"}}
Enter fullscreen mode Exit fullscreen mode

A step further

Another use case I often had, is to create infrastucture resources and then init/provison them.

For this example I will

  1. use Terraform to create infrastructure (a Kubernetes cluster)
  2. 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
Enter fullscreen mode Exit fullscreen mode

Create actions

I will create

  • 3 actions for Terraform operations : init, plan and apply
  • 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}}"
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 and generates 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.

Top comments (0)