Introduction to Taskfile: a Makefile alternative
Tags: automation, make, productivity
Published: 2021-06-11 14:58:30

---
title: Introduction to Taskfile: a Makefile alternative
publish: true
description: Easier and simpler than Makefile
tags: automation, make, productivity

cover_image: https://res.cloudinary.com/skurtzemann/image/upload/v1623195292/blog/dev-to/task-intro/austrian-national-library-EXgfRVFJK4M-unsplash_xqx6cs.jpg
---

`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](https://taskfile.dev/#/) 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](https://taskfile.dev/#/installation) section.

On macOS, simply use Homebrew
```bash
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

```yaml
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` 

```bash
Λ\: task --list
task: Available tasks for this project:
* build: 	Build the container image
```

And if we run `task build`, it returns

```bash
Λ\: 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.

```yaml
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

```bash
Λ\: 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 

```yaml
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

```yaml
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:

```yaml
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
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

```bash
.
├── releases
│   └── helmfile.yaml
└── terraform
    └── main.tf
```

### Create actions

I will create
* 3 actions for Terraform operations : `init`, `plan` and `apply`
* 2 actions for Helm releases : `diff`, `apply` (I use [helmfile](https://github.com/roboll/helmfile) to install Helm release) 


Let's create a naive `Taskfile.yml`

```yaml
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 

```yaml
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`

```bash
Λ\: 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

```yaml
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
```bash
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](https://taskfile.dev/#/usage?id=including-other-taskfiles) others taskfiles.

Let's add another folder: `taskfiles/`, with 2 files in it
* `terraform.yml`
* `helmfile.yml`


```bash
.
├── releases
│   └── helmfile.yaml
├── taskfiles
│   ├── helmfile.yml
│   └── terraform.yml
└── terraform
    └── main.tf
```

Let's see the content of each files

* `releases/terraform.yml`

```yaml
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`

```yaml
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 ?

```yaml
version: "3"

includes:
  tf: ./taskfiles/terraform.yml
  releases: ./taskfiles/helmfile.yml
```

If we run `task --list` we got the same result
```bash
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` 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](https://taskfile.dev/#/) as a `make` alternative, with similar functionalities but I think easier to use.