6.3 KiB
+++ title = "Terraform workflow using Guix and Emacs" date = "2024-06-05" author = "Peter Tillemans" email = "pti@snamellit.com" [taxonomies] tags = ["emacs", "guix"] categories = ["programming"] +++
Terraform Deployments
Terraform allows infrastructure to be defined to deploy applications and other solutions as code and supports a plethora of on-premise and cloud deployment targets.
It is essentially based on building a graph of dependencies between resources, data and modules using the terraform language.
Due to the nature of the beast these things tend to run in the CI pipelines which makes editing these files frustrating as the edits have to be committed, pushed, runners have to be scheduled and usually the deploy pipeline is not the first job.
So good local tooling is needed to get fast feedback.
Terraform tooling on GUIX
In order to run terraform I need to first package it as it is not available in the GUIX repositories.
(define-public terraform
(package
(name "snam-terraform")
(version "1.8.4")
(source (origin
(method url-fetch)
(uri (string-append "https://releases.hashicorp.com/terraform/" version "/terraform_" version "_linux_amd64.zip"))
(sha256
(base32
"1i181cmzwlrx8d40z1spilcwgnhkzwalrg8822d23sqdmrs7a5hj"))))
(build-system binary-build-system)
(supported-systems '("x86_64-linux"))
(arguments '(
#:install-plan
`(("." ("terraform") "bin/"))
#:phases
(modify-phases %standard-phases
;; this is required because standard unpack expects
;; the archive to contain a directory with everything inside it,
;; while babashka's release .tar.gz only contains the `bb` binary.
(replace 'unpack
(lambda* (#:key inputs #:allow-other-keys)
(system* (which "unzip")
(assoc-ref inputs "source"))
#t)))))
(inputs
`(("libstdc++" ,(make-libstdc++ gcc))
("zlib" ,zlib)))
(native-inputs
`(("unzip" ,unzip)))
(synopsis "A tool to describe and deploy infrastructure as code")
(description
"Terraform allows you to describe your complete infrastructure in the form of code. Even if your servers come from different providers such as AWS or Azure, Terraform helps you build and manage these resources in parallel across providers.")
(home-page "https://hashicorp.com/terraform")
(license #f)))
(define-public snam-terraform-1.6
(package
(inherit terraform)
(version "1.6.6")
(source (origin
(method url-fetch)
(uri (string-append "https://releases.hashicorp.com/terraform/" version "/terraform_" version "_linux_amd64.zip"))
(sha256
(base32
"002g0ypkkfqy5nf989jyk3m1l7l0455hsaq11xfhr5lbv4zqh5yi"))))))
I immediately added support to build older versions because that's what the customer is on and terraform is quite version dependent AFAICT.
Now I can create a manifest for this project. I usually bootstrap them
with guile shell --export-manifes go gopls
or similar and then add
stuff when it comes up.
;; What follows is a "manifest" equivalent to the command line you gave.
;; You can store it in a file that you may then pass to any 'guix' command
;; that accepts a '--manifest' (or '-m') option.
(specifications->manifest
(list "go" "gopls"
"google-cloud-sdk"
"postgresql"
"snam-terraform-1.6" ; from snamellit channel
))
Direnv support
In order to manage my project environment and align it with the CI environment I added the expected variables and use the guix support in the stdlib of direnv. This will create a guix environment configured from the manifest.
use guix
export DB_URL="postgresql://<db_ip>/myproj"
export DB_USER="xyz"
export DB_PASSWORD="secret"
export PGPASSWORD=$DB_PASSWORD
export VAULT_TOKEN="<blablabla>"
export APPTIO_URL=https://acme.tpondemand.com
export APPTIO_TOKEN=<blablabla>
export OPENAI_API_KEY=<blablabla>
export VAULT_ADDR=https://vault.acme.com
export STATE_BUCKET=com-acme-test-myproj-tf-state
export TF_VAR_project_short=myproj
export TF_VAR_project=com-acme-test-${TF_VAR_project_short}
PATH_add ./node_modules/.bin
Emacs support
Direnv Support
Emacs direnv mode will load the configuration from the .envrc file when opening a file in that project. The variables and apps are then available for complition, LSP, shell, etc.
;; enable direnv mode
(direnv-mode)
I just enable it globally because I want that always, not just for terraform.
Terraform Support
Enable some syntax highlighting and more importantly documentation
help. Also set format-on-save
and the indent to 2 spaces
;; configure terraform support
(require 'terraform-mode)
(add-hook 'terraform-mode-hook
(lambda ()
(outline-minor-mode 1)))
(custom-set-variables
'(terraform-indent-level 2)
'(terraform-format-on-save t))
and expose the functionality in similar keybindings as I use for LSP support :
(evil-define-key 'normal terraform-mode-map
(kbd "<leader>c k") #'terraform-open-doc
(kbd "<leader>c f") #'terraform-format
(kbd "<leader>c F") #'terraform-format-buffer
(kbd "<leader>c n") 'flymake-goto-next-error
(kbd "<leader>c p") 'flymake-goto-prev-error)
will now open a browser window with the documentation of the terraform element under the cursor. This does need terraform to be installed though.
Add support to Makefile
In order to save me from remembering the commandlines and because I keep the terraform files in a terraform directory I make some make targets to quickly access them.
tfinit:
terraform -chdir=terraform init -backend-config="bucket=${STATE_BUCKET}"
tfcheck:
terraform -chdir=terraform validate
tfapply:
terraform -chdir=terraform apply
tfplan:
terraform -chdir=terraform plan
tflint:
docker run --rm -v `pwd`/terraform:/data -t ghcr.io/terraform-linters/tflint
Workflow tips
Most of it is essentially hidden in the normal workflow. i.e. opening a terraform file will load it, saving formats it. The compile feature can be used to do file checking and linting.
Magit commit - push triggers the CI pipeline to run the deploy.
It is easy to test things out locally with the Makefile and it reduces the number of steps in the CI script , so less things which can do weird things.