Terraform 101
Manually starting microservices as we did last week is tedious, especially when managing multiple microservices. Thankfully, Terraform comes to our rescue.
Need to switch from GCP to AWS but dread learning the new UI to manage your infrastructure? Or migrate from your local development environment to a hosted provider? Terraform is your new best friend.
The theme of this series is to keep all development local (and free) while you bootstrap your MVP, so our discussion of Terraform will be limited to deploying to a local Kubernetes cluster just like we did manually last week.
Terraform was created by HashiCorp, so it uses the HashiCorp Configuration Language. Let's take a look at it before we dive into this week's code.
HCL Files
HCL files for Kubernetes are essentially YAML files with a different syntax. Note the similarities in the following Namespace declarations:
resource "kubernetes_namespace" "flixtube" {
metadata {
name = "flixtube"
}
}apiVersion: v1
kind: Namespace
metadata:
name: flixtube
Every kind: in YAML maps to a kubernetes_[kind] in HCL, making translations fairly easy.
Round 1: Providers and Namespaces. (source)
Terraform communicates with third-party vendors like AWS and GCP using plugins called Providers. These Providers define resources that are used for configuration, so let's take a quick look at our providers.tf file.
terraform {
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.0"
}
}
}
provider "kubernetes" {
config_path = "~/.kube/config"
}
The Kubernetes Provider defines several resources for use while defining our cluster. The highest-level resource for us is a Namespace, which allows us to partition different services by anything we choose. (e.g., dev/test/prod)
Our example:
resource "kubernetes_namespace" "flixtube" {
metadata {
name = "flixtube"
}
}
We can now run terraform init, then terraform apply to build our infrastructure. There's nothing really to see here yet, but minikube kubectl -- get namespaces will prove that our namespace was successfully created.
Yawning at this point is perfectly okay. All we have proven thus far is that we can successfully use Terraform to build the most basic of infrastructures. A quick terraform destroy will tear the existing infrastructure down, allowing us to build and destroy it as many times as it takes to get comfortable.
Round 2: Container Registry. (source)
Kubernetes can easily pull Docker images from outside registries like Docker Hub, but having your containers cached locally to a container registry is more convenient.
Creating a local container registry is a simple minikube addons enable registry on the command line. Terraform makes it just a bit more difficult, requiring a null_resource along with a local-exec provisioner that defines the command to run.
The minikube addons enable registry command requires either a -p command line flag to specify which profile to use OR a MINIKUBE_PROFILE environment variable specifying the same. We'll stick with the latter for simplicity's sake by defining an environment in the provisioner section.
We give the null_resource the name of enable_registry to make the logging clear in the following container-registry.tf file.
resource "null_resource" "enable_registry" {
provisioner "local-exec" {
command = "minikube addons enable registry"
environment = {
MINIKUBE_PROFILE = "flixtube"
}
}
}This doesn't do too much locally, but it does more closely mirror our intended production environment.
Warning
The following error means your minikube cluster hasn't started:
Error running command 'minikube addons enable registry': exit status 10.
Ensure you have run minikube start if you see that error.
Round 3: Variables. (source)
The variables.tf file gives us a centralized location to add, and change, our important variables.
variable "app_name" {
}
Notice that we don't assign a value in the variable declaration. The lack of assignment tells Terraform to ask us for the value interactively. (We can also assign it from the command line with, e.g., terraform apply -var app_name=flixtube.)
We can then reference that variable in container-registry.tf:
resource "null_resource" "enable_registry" {
provisioner "local-exec" {
command = "minikube addons enable registry"
environment = {
MINIKUBE_PROFILE = var.app_name
}
}
}
One terraform init and terraform apply later, you will have another functioning Kubernetes cluster.
terraform destroy and we will move on to the final step.
Round 4: Push image to a minikube registry. (source)
Our goal in this series is to learn to mimic developing a microservice application AND deploy it to a production-like environment for free locally.
Terraform is immensely helpful in this regard, but we are still using our local Docker images. Can we fix that?
Of course!
To do so, we need to configure Docker to use the minikube container registry, push the image to the registry, tell the Kubernetes Deployment to use the registry's image, expose the Service on localhost, and test it via the browser.
Terraform is already configured to use our local registry after we run terraform apply, so the (almost) final steps are the following straightforward Docker commands:
- Build the image:
docker build -t video-streaming:1 . - Tag the image:
docker tag video-streaming:1 localhost:5000/video-streaming:1 - Push the image to the registry:
docker push localhost:5000/video-streaming:1
The following steps are a bit more interactive:
-
Update
deploy.ymlto use the registry's image.apiVersion: apps/v1 kind: Deployment metadata: name: video-streaming spec: replicas: 1 selector: matchLabels: app: video-streaming template: metadata: labels: app: video-streaming spec: containers: - name: video-streaming image: localhost:5000/video-streaming:1 imagePullPolicy: IfNotPresent env: - name: PORT value: "4000" --- apiVersion: v1 kind: Service metadata: name: video-streaming spec: selector: app: video-streaming type: LoadBalancer ports: - protocol: TCP port: 80 targetPort: 4000 -
Create a tunnel to expose your service:
minikube tunnellike last week.Your output, after entering your password, will look something lke this:
Status: machine: flixtube pid: 425582 route: 10.96.0.0/12 -> 192.168.49.2 minikube: Running services: [video-streaming] errors: minikube: no errors router: no errors loadbalancer emulator: no errors -
Get the service's IP:
minikube kubectl -- get services.Again, your output:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3d23h video-streaming LoadBalancer 10.110.40.191 10.110.40.191 80:30387/TCP 3d17h - Access the service via
video-streaming's EXTERNAL-IP/video.
Conclusion
While Terraform is an optional chapter in Bootstrapping Microservices, I definitely recommend reading it at some point.
Manually deploying multiple microservice with minikube kubectl quickly becomes an impediment to rapid development. And anything that hinders rapid development should be avoided.
This series (and the book it's derived from) has focused more on learning Kubernetes and Docker than best coding practices so far. That will be addressed next week with the introduction of pre-commit hooks with act, which will prevent us from pushing failing code to our remote repository.