Terraform is an infrastructure as code tool. The key term here being code. And where do you store your code? In a version control system like GitHub! I recently published a video with the awesome April Edwards on creating your first Terraform repository.
This blog post is a companion to that video. Let’s dive in!
If you’re seasoned developer using version control, then I don’t need to convince you of its benefits. But if you’re like me and coming from a sysadmin background, then you might not be as familiar with version control software or its benefits. Here are some reasons why you should use version control:
With all that in mind, how do you get started with version control for your Terraform code? First we need to go over some terminology and tools.
The most popular source control software in the world is git. It’s leveraged by GitHub, GitLab, Bitbucket, and many other platforms. There are other source control systems like Subversion, but I don’t know anything about those, so we’ll focus on using git.
Git tracks your code through a repository. Your code is checked into a local repository managed by git. The repository tracks the various versions of code through commit IDs and branches. To add a file to git, you first stage the file, then commit it to the repository. The commit process creates a new commit ID and stores the file in the repository.
There’s a LOT more to it than just that. But for our purposes, the important thing is to understand the process of staging files and committing them to the repository. Just saving a file to the directory being used by git doesn’t mean it’s being tracked by git.
In addition to your local repository, you can also configure one or more remote repositories. This allows you to push your local code to a remote location, collaborate on the code with others, and pull down updates they have made.
While you’ll certainly want to commit and track your Terraform files, like main.tf
or variables.tf
, there are some files that you don’t want to commit. For instance your local state file (if you’re using local state) or the .terraform
directory that holds the provider plugins and modules used by your code. You can tell git to ignore these files by creating a .gitignore
file in your repository.
The .gitignore
file is a special file that tells git what files to exclude from commits. You can specify files by name, or use wildcards to exclude entire directories or file extensions.
Now that we have some background in version control and git, it’s time to create a repository!
If you’ve been using Terraform for a while now, there’s a good chance you have a bunch of directories with your code in them. Maybe you’ve been sharing the code by zipping it up and emailing it, or copying it to a shared drive. Now it’s time to try a better way by storing that code in a git repository. Let’s see how you can start by creating a local repository for your existing code.
In my example, we have a directory called taco-app
that has the following file tree:
taco-app$ tree -a -L 1
.
├── .terraform
├── .terraform.lock.hcl
├── main.tf
├── outputs.tf
├── terraform.tf
├── terraform.tfstate
├── terraform.tfvars
└── variables.tf
This is the code that we want to store in a repository. First we’ll run a git command to create the repository in the taco-app
directory. In the same way that you initialize a Terraform configuration with terraform init
, you start your git journey by running git init
.
taco-app$ git init -b main
Initialized empty Git repository in .../taco-app/.git/
A new repository needs a default branch for git to track. We can use the -b
flag to set the default branch name for our repository. In this case, we used main
as the branch name.
The command created a hidden folder called .git
, which will store information about the files being tracked, commit ids, and configuration of our git client for this repository.
taco-app$ tree -L 1 .git
.git
├── HEAD
├── branches
├── config
├── description
├── hooks
├── info
├── objects
└── refs
Now we can add our files to the repository. Before we do that, I’m going to set up a .gitignore
file to exclude the .terraform
directory and the terraform.tfstate
file. We don’t want to commit these files to the repository. You can find a good .gitignore
file for Terraform on GitHub.
The command to stage our files is git add
followed by the files we want to add. Rather than laboriously typing out all the files and directories, instead we can use the .
to add all files and subfolders of the current working directory.
taco-app$ git add .
You won’t see any output from this command if everything has gone well. If we want to see what files have been staged, we can run git status
:
taco-app$ git status
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: .gitignore
new file: .terraform.lock.hcl
new file: main.tf
new file: outputs.tf
new file: terraform.tf
new file: variables.tf
With our files staged, now we can commit them to source control using the git commit
command. This command requires that you provide a message about the commit. The message can be anything you like, but it should be descriptive of the changes being introduced by your commit.
taco-app$ git commit -m "First commit"
[main (root-commit) 827b4dd] First commit
6 files changed, 58 insertions(+)
create mode 100644 .gitignore
create mode 100644 .terraform.lock.hcl
create mode 100644 main.tf
create mode 100644 outputs.tf
create mode 100644 terraform.tf
create mode 100644 variables.tf
Our files are now part of the repository! But right now they’re only stored on our local machine. If we want to collaborate with others, or have a backup of our code, we need to push it to a remote repository.
To create a remote repository on GitHub, you’ll need to have an account. If you don’t have one, go to GitHub and sign up.
You can create a new repository using the web interface or the GitHub command line tool. Since we’re doing everything else with the command line, we might as well use it to create our remote repository too. If you don’t have the GitHub CLI tool installed, you can download it from the GitHub CLI website.
If you’ve never used the tool before, you’ll need to login into GitHub using the command gh auth login
. The wizard will walk you through authenticating with GitHub and procuring a token for the CLI to use.
We can create the repository using the gh repo create
command. Since we already have the local repository, we can let GitHub know that it should use our local repository by specifying the --source
flag. We also want to make the repository public, so we’ll use the --public
flag. Finally, we want to add a remote entry to our local repository, so we’ll use the --remote
flag to specify the name of the remote repository entry.
taco-app$ gh repo create --public --source=. --remote=upstream
✓ Created repository ned1313/taco-app on GitHub
✓ Added remote https://github.com/ned1313/taco-app.git
Awesome! We’ve successfully created a local repository for our Terraform code and a remote repository on GitHub. Now we need to push our local code to the remote repository.
Our local repository now knows about the remote repository on GitHub. We can view the details of the remote repository by running the git remote show
command with the name of the remote entry:
taco-app$ git remote show upstream
* remote upstream
Fetch URL: https://github.com/ned1313/taco-app.git
Push URL: https://github.com/ned1313/taco-app.git
HEAD branch: (unknown)
See that HEAD branch: (unknown)
line? That’s because we haven’t pushed our local code to the remote repository yet. We can do that using the git push
command. The -u
flag tells git to set the main branch on the remote repository called upstream
to track our local main
branch.
taco-app$ git push -u upstream main
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 16 threads
Compressing objects: 100% (7/7), done.
Writing objects: 100% (9/9), 1.79 KiB | 37.00 KiB/s, done.
Total 9 (delta 1), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (1/1), done.
To https://github.com/ned1313/taco-app.git
* [new branch] main -> main
Branch 'main' set up to track remote branch 'main' from 'upstream'.
This also means that in the future, when we run git push
, git will know to push to the remote branch we’ve set in the upstream entry.
Now if you check the info for the remote repository, you’ll see that the HEAD
branch is set to main
:
taco-app$ git remote show upstream
* remote upstream
Fetch URL: https://github.com/ned1313/taco-app.git
Push URL: https://github.com/ned1313/taco-app.git
HEAD branch: main
Remote branch:
main tracked
Local branch configured for 'git pull':
main merges with remote main
Local ref configured for 'git push':
main pushes to main (up to date)
Awesome! We’ve taken our Terraform code, put it in a local repository, and pushed the code up to GitHub. Now we can collaborate with others, have a backup of our code, and automate testing and deployment of our code on check in.
Using version control for your Terraform code is a great first step. But there’s a lot more you can do with version control. You can create branches to work on new features or bug fixes. You can create pull requests to review and approve changes before they’re merged and applied. You can even automate testing and deployment of your code on check in.
In a future blog post and video, I’ll dig into how you can use source control to publish your own Terraform modules!
Resourcely Guardrails and Blueprints
November 15, 2024
Deploying Azure Landing Zones with Terraform
November 12, 2024
October 18, 2024
What's New in the AzureRM Provider Version 4?
August 27, 2024