Skip to main content

Build Block

The build references the sources and can have provisioners that will create the artifact, for example an image, and post-processors that will manipulate and resolve what to do with the artifact.

Sources

The build needs at least one source block defined. This block can be referenced multiple times within the build to generate several different builders and the provisioner commands will be executed within each of the selected builders.

Knowing this, it is good practice to declare in the source blocks values that do not change and declare the changes within the reference to the source in the build as in the example I will show.

The example below illustrates the null type source which is not really a builder, it just sets up an SSH connection and runs the provisioners. It can be used to debug provisioners without incurring high wait times. It does not create any type of image or artifact.

Null

A good practice is to define the name in the build to appear in the output.


# Communicator is mandatory can be ssh winrm or none in case of null
# Depending on the source, ssh must be defined for example; in the case of AWS and in the case of Azure ssh is the default since all machines have the azure user.
variable "pass" {
description = "My password"
type = string
# using function to get my exported variable
default = env("MY_PASS")
}

source "null" "first-example" {
communicator = "none"
}

# Can have multiple sources of the same type, but never with the same name
source "null" "second-example" {
ssh_host= "127.0.0.1"
ssh_username = "david"
ssh_password = var.pass
# The argument below is necessary but I will set it there in the build block
# If this argument is set here it is not possible to set it there.
# ssh_password = "xxxxxx"
}

build {
name = "MyNullBuild"
# using the first builder and adding the name variable
source "null.first-example" {
name = "consul"
}
# using the first builder and adding an extra variable
source "null.first-example" {
name = "nomad"
}
source "null.second-example" {
name = "vault"
}

sources = ["null.first-example","null.second-example"]

# It is possible to reference name and type of sources as follows
# It is not possible to reference other variables like othername using source.othername
provisioner "shell-local" {
inline = ["echo ${source.name} and ${source.type} TEST PROVISIONER"
]
}

post-processor "shell-local" {
inline = ["echo ${source.name} OR ${source.type} TEST POST-PROCESSOR"]
}
}

To execute the build below, it is necessary to enable ssh on your machine to allow password authentication and start the sshd service. Check the project and do your tests in the example by running the command inside the folder.

~/gitlab/personal/study-packer/examples/build main ⇡3 !3 ?1
❯ packer build .
MyNullBuild.null.first-example: output will be in this color.
MyNullBuild.null.second-example: output will be in this color.
MyNullBuild.null.consul: output will be in this color.
MyNullBuild.null.nomad: output will be in this color.
MyNullBuild.null.vault: output will be in this color.

==> MyNullBuild.null.vault: Using SSH communicator to connect: 127.0.0.1
==> MyNullBuild.null.second-example: Using SSH communicator to connect: 127.0.0.1
==> MyNullBuild.null.vault: Waiting for SSH to become available...
==> MyNullBuild.null.second-example: Waiting for SSH to become available...
==> MyNullBuild.null.nomad: Running local shell script: /tmp/packer-shell4018821226
==> MyNullBuild.null.consul: Running local shell script: /tmp/packer-shell24449155
==> MyNullBuild.null.first-example: Running local shell script: /tmp/packer-shell590834333
MyNullBuild.null.nomad: nomad and null TEST PROVISIONER
MyNullBuild.null.consul: consul and null TEST PROVISIONER
==> MyNullBuild.null.nomad: Running post-processor: (type shell-local)
==> MyNullBuild.null.consul: Running post-processor: (type shell-local)
MyNullBuild.null.first-example: first-example and null TEST PROVISIONER
==> MyNullBuild.null.first-example: Running post-processor: (type shell-local)
==> MyNullBuild.null.nomad (shell-local): Running local shell script: /tmp/packer-shell661443636
==> MyNullBuild.null.consul (shell-local): Running local shell script: /tmp/packer-shell3268706950
==> MyNullBuild.null.first-example (shell-local): Running local shell script: /tmp/packer-shell3568713064
MyNullBuild.null.nomad (shell-local): nomad OR null TEST POST-PROCESSOR
Build 'MyNullBuild.null.nomad' finished after 12 milliseconds 418 microseconds.
MyNullBuild.null.consul (shell-local): consul OR null TEST POST-PROCESSOR
Build 'MyNullBuild.null.consul' finished after 12 milliseconds 632 microseconds.
MyNullBuild.null.first-example (shell-local): first-example OR null TEST POST-PROCESSOR
Build 'MyNullBuild.null.first-example' finished after 14 milliseconds 121 microseconds.
==> MyNullBuild.null.vault: Connected to SSH!
==> MyNullBuild.null.second-example: Connected to SSH!
==> MyNullBuild.null.vault: Running local shell script: /tmp/packer-shell1580315204
==> MyNullBuild.null.second-example: Running local shell script: /tmp/packer-shell2396870170
MyNullBuild.null.vault: vault and null TEST PROVISIONER
==> MyNullBuild.null.vault: Running post-processor: (type shell-local)
MyNullBuild.null.second-example: second-example and null TEST PROVISIONER
==> MyNullBuild.null.second-example: Running post-processor: (type shell-local)
==> MyNullBuild.null.vault (shell-local): Running local shell script: /tmp/packer-shell1814621561
==> MyNullBuild.null.second-example (shell-local): Running local shell script: /tmp/packer-shell4080843186
MyNullBuild.null.vault (shell-local): vault OR null TEST POST-PROCESSOR
Build 'MyNullBuild.null.vault' finished after 165 milliseconds 197 microseconds.
MyNullBuild.null.second-example (shell-local): second-example OR null TEST POST-PROCESSOR
Build 'MyNullBuild.null.second-example' finished after 167 milliseconds 229 microseconds.

==> Wait completed after 167 milliseconds 336 microseconds

==> Builds finished. The artifacts of successful builds are:
--> MyNullBuild.null.nomad: Did not export anything. This is the null builder
--> MyNullBuild.null.nomad: Did not export anything. This is the null builder
--> MyNullBuild.null.consul: Did not export anything. This is the null builder
--> MyNullBuild.null.consul: Did not export anything. This is the null builder
--> MyNullBuild.null.first-example: Did not export anything. This is the null builder
--> MyNullBuild.null.first-example: Did not export anything. This is the null builder
--> MyNullBuild.null.vault: Did not export anything. This is the null builder
--> MyNullBuild.null.vault: Did not export anything. This is the null builder
--> MyNullBuild.null.second-example: Did not export anything. This is the null builder
--> MyNullBuild.null.second-example: Did not export anything. This is the null builder
❯ packer build .
roles.null.first-example: output will be in this color.
roles.null.second-example: output will be in this color.
roles.null.consul: output will be in this color.
roles.null.nomad: output will be in this color.
roles.null.vault: output will be in this color.

==> roles.null.second-example: Running local shell script: /tmp/packer-shell2204105492
==> roles.null.nomad: Running local shell script: /tmp/packer-shell3063727616
==> roles.null.consul: Running local shell script: /tmp/packer-shell2232404743
==> roles.null.vault: Running local shell script: /tmp/packer-shell607664062
==> roles.null.first-example: Running local shell script: /tmp/packer-shell3215393418
roles.null.consul: consul and null
roles.null.nomad: nomad and null
roles.null.second-example: second-example and null
roles.null.first-example: first-example and null
Build 'roles.null.nomad' finished after 52 milliseconds 323 microseconds.
Build 'roles.null.second-example' finished after 53 milliseconds 750 microseconds.
Build 'roles.null.consul' finished after 53 milliseconds 860 microseconds.
Build 'roles.null.first-example' finished after 54 milliseconds 339 microseconds.
roles.null.vault: vault and null
Build 'roles.null.vault' finished after 65 milliseconds 939 microseconds.

==> Wait completed after 66 milliseconds 290 microseconds

==> Builds finished. The artifacts of successful builds are:
--> roles.null.nomad: Did not export anything. This is the null builder
--> roles.null.second-example: Did not export anything. This is the null builder
--> roles.null.consul: Did not export anything. This is the null builder
--> roles.null.first-example: Did not export anything. This is the null builder
--> roles.null.vault: Did not export anything. This is the null builder

For the build to access the machine there are two ways, either declare to have ssh in the case of Linux or winrm in the case of Windows. Usually ssh is used. If there is no defined communicator, most build steps will not work.

Plugins extend the types of sources and provisioners.

Provisioners

Provisioners work on top of the instance made available by the sources and after boot install and configure the machine image after boot.

All provisioners have common parameters

  • pause_before(duration) - Sleeps for a period before execution.

  • max_retries(int) - Maximum times the provisioner will retry in case of failure. Default is zero (0).

  • only(array of string) - Execute the provisioner only for the provisioners listed by name.

  • override(object) - Override the builder with different settings for a specific builder

Native Provisioners

  • file: This provisioner is used to upload a file into the machine made available by the sources. It is only possible to upload to a directory that the user used in the source to make ssh has permission. A good practice is to do it inside /tmp and then with another provisioner manipulate the files.

    build {
    provisioner "file" {
    # The paths to the local files or directories to upload to the machine
    sources = ["app.tar.gz"]
    # The path where the file will be uploaded to the machine
    destination = "/tmp/app.tar.gz"
    # If set download will pull file from vm into local machine. Default is upload.
    direction = "upload"
    }
    }

    download direction is used many times to pull something in the post processor.

  • shell: This provisioner will execute shell scripts. It is the main provisioner. It is with it that you manipulate the files that were loaded in the vm. A good practice in my opinion is to always reference scripts in files instead of showing command by command. This avoids code pollution and organizes things better. That's why in the base project there is a folder just for scripts. One of the advantages is always defining the shell that is used when you have the script. Attention to execute_command which is an attribute of this provisioner.

    • execute_command(string) - The command to be used to execute the script. By default, it is chmod +x {{ .Path }}; {{ .Vars }} {{ .Path }}. If the user defines a file where environment variables are, the default command will be chmod +x {{.Path}}; . {{.EnvVarFile}} && {{.Path}} there are three extra variables available:
      • Path is the path to be executed
      • Vars is the list of environment_vars if defined
      • EnvVarFile is the path to the file that contains env vars, if use_env_var_file is true
      build {
    provisioner "shell" {
    environment_vars = [
    "MY_TEST=david",
    "MY_TEST2=puziol"
    ]
    # Example of how to execute with sudo
    # "echo 'packer' | sudo -S sh -c '{{ .Vars }} {{ .Path }}'"
    # "echo 'packer' | sudo -S env {{ .Vars }} {{ .Path }}"

    execute_command = "{{ .Vars }} /bin/bash '{{ .Path }}'"
    scripts = [
    "./scripts/script1.sh",
    "./scripts/script2.sh",
    "./scripts/script3.sh"
    ]
    }
    }
  • shell-local: will execute a script on the machine on which Packer is running. The parameters are the same as the shell provisioner. Widely used during post-processor to execute commands.

Worth mentioning that there are some default envs.

  • PACKER_BUILD_NAME: used to differentiate the name between builds otherwise all will use the same build name.
  • PACKER_BUILDER_TYPE: defines the type of builder that was used to create the machine that will execute the script.
  • PACKER_HTTP_ADDR: Only used if the builder provides an http server for file transfer

NO USE TALKING ABOUT EVERYTHING THAT EXISTS, YOU NEED TO KNOW IT EXISTS, AND WHEN YOU NEED TO USE IT YOU STUDY. THESE ARE THE MAIN ONES.

How to forward your local ssh agent to the machine?

provisioner "shell" {
inline = [
"sudo apt-get install -y git",
"ssh-keyscan github.com >> ~/.ssh/known_hosts",
"git clone [email protected]:exampleorg/myprivaterepo.git"
]
}

However, using the git plugin would make things easier and ensure greater security.

Post-processors

It is the last thing to be executed, after the builders and provisioners. They are optional and used to resolve what to do with the artifacts.

  • Upload
  • Repackage
  • Checksum
  • Manifests
  • Upload to an s3 bucket
  • etc