Back to Blog

Comparing 2 approaches of managing cloud resources for applications: Terraform vs Kubernetes

Tech

Overview

Terraform

Terraform is an open-source tool for managing infrastructure as code. It provides HCL language and CLI tools to provision and manage infrastructure resources. Terraform also has many cloud and API providers such as AWS, GCP, Azure, Kubernetes, Docker, etc. You can see a full list here. To track changes of resources and discover what will be changed if you apply your code, Terraform stores information about managed resources in state. State is a JSON representation of managed resources, and information about them. With Terraform you can store your state in different formats such as local file, AWS S3, GCP Storage Bucket, Azure Blobstorage, etc.

Pros:

  • Cloud agnostic. Flexibility to choose which cloud provider to work with.
  • Extensible. To allow Terraform to work with your custom API, you can create a custom provider and write a logic on how terraform should interact with your resources.
  • Multi-platform. You can run Terraform CLI tool in Linux, Mac OS, FreeBSD, OpenBSD and Solaris platforms.
  • Predictable. Provides a way to check infrastructure changes before applying the changes.
  • Reusable. Provides a way to publish and reuse your code via modules. One module can have dependencies on another module.
  • Has dependency graph. Allows you to specify which resources are dependent on each other and which are not, and thus can be executed in parallel.
  • Big community. At the moment of writing Terraform has 31k stars on Github.
  • Agentless. You don’t need to run any agent or anything.

Cons:

  • No rollbacks. There is no auto rollback feature.
  • New language. You need to learn a new language syntax.
  • Implicit dependencies don’t work well. Sometimes errors occur when you haven’t specified explicit dependencies.

Kubernetes / Helm

Kubernetes is an open-source system for automating deployment, scaling, and management of containerized applications. It provides an API that allows you to create various resources that can be configured via the YAML language. Configurations of resources are stored in etcd storage. You can use default resources to manage containers and communication between them, or you can define your own custom resource definitions and manage them via operators and controllers.

To apply changes in Kubernetes you can use raw manifest files, in which you can specify all necessary properties and deploy them into the Kubernetes cluster. The problem is when you want to reuse and version these resources in the manifest. This is where Helm can help you. Helm is a simple template tool for Kubernetes manifests which has an ability to package your configuration for specific components into artifacts, called Charts. With Charts you can set a version for the Chart, store it in a repository called Chartmuseum, and reuse Charts for deploying a component with values. Simply, it provides templating and package management for your Kubernetes manifests with some additional features. To track managed resources it uses the state approach which is used by Terraform, but for each deployment it creates another state which is also called Release. Having one Release for each deployment allows Helm to do rollbacks in case something goes wrong.

Pros:

  • Templating. It allows parameterization of raw manifest files.
  • Reusable. Provides package management functionality via Chartmuseum and packaging helm charts. One helm chart can be dependent on another.
  • Provides stages of deployment. Has a lifecycle hooks feature that allows you to specify what resources should be created before or after installation/upgrade.
  • Extensible. CLI tool is extensible via plugins and you can support your custom resources via extending Kuberetes API through CRDs and operators.
  • Has rollbacks. Provides a way to rollback your application to the previous state in case a problem occurs.
  • Agentless. You don’t need to run any agent or anything starting from v3.
  • Predictable. Provides a way to check what Helm will do when you deploy changes.
  • Big community. At the moment of writing Kubernetes has 21.1k stars on Github.

Cons:

  • Weak tracking of actual state. Tracks actual state of resources through last deployed release instead of API calls.
  • No dependency management. Resources can be installed only all at once. There is no dependency graph. So you can’t specify an order for deployed resources.

Describing environment

Our application environment will consists of these components:

  • Wordpress application.
  • AWS RDS.

Hands on

To create Kubernetes cluster (EKS) in both cases we will use terraform. You can find the Source code here: …

Terraform approach

First, let’s configure our input variables in a variable.tf file. In our case we need to set region for AWS and EKS remote state to configure Kubernetes and Helm providers:

variable.tf

    variable "region" {
    type    = string
    default = "eu-central-1"
    }


    variable "eks_remote_state_bucket" {
    type = string
    }


    variable "eks_remote_state_key" {
    type = string
    }
                    

Then let’s create a vars folder in which we will set variables and backend configuration:

vars/default.tfvars

    eks_remote_state_bucket = "artcher-infrastructure"
    eks_remote_state_key = "eks/terraform.tfstate"
                    

vars/default.tfbackend

    bucket = "artcher-infrastructure"
    key    = "application/terraform.tfstate"
    region = "eu-central-1"
                    

In the next step we need to define providers in main.tf and data resources for remote state in data.tf:

data.tf

    data "terraform_remote_state" "eks" {
        backend = "s3"

        config = {
            bucket = var.eks_remote_state_bucket
            key    = var.eks_remote_state_key
            region = var.region
            }
    }

    data "aws_eks_cluster" "cluster" {
        name = data.terraform_remote_state.eks.outputs.cluster_id
    }

    data "aws_eks_cluster_auth" "cluster" {
        name = data.terraform_remote_state.eks.outputs.cluster_id
    }

                    

main.tf

    terraform {
        required_providers {
            aws = {
            source  = "hashicorp/aws"
            version = "~> 3.0"
            }
            kubernetes = {
            source = "hashicorp/kubernetes"
            version = "2.8.0"
            }
            helm = {
            source = "hashicorp/helm"
            version = "2.4.1"
            }
        }


        backend "s3" {
        }
    }

    provider "aws" {
        region = var.region
    }

    provider "kubernetes" {
        host = data.aws_eks_cluster.cluster.endpoint
        token = data.aws_eks_cluster_auth.cluster.token
        insecure = true
    }

    provider "helm" {
        kubernetes {
            host = data.aws_eks_cluster.cluster.endpoint
            token = data.aws_eks_cluster_auth.cluster.token
            insecure = true
        }
    }

    resource "kubernetes_namespace" "main" {
        metadata {
            name = "dev"
        }
    }
                    

Let’s define application namespace in main.tf file:

main.tf

    resource "kubernetes_namespace" "main" {
        metadata {
            name = "dev"
        }
    }
                    

Next step is defining an RDS instance for our Wordpress installation in db.tf file. Good practice is to allow Terraform to generate a password for future usage, so we will use random_password resource to generate password for our database. Also, because VPC creation is usually done by Terraform, we can simply refer to VPC id, VPC cidr block and subnet ids via terraform_remote_state resource.

db.tf

    locals {
        db_username = "wordpress"
        db_password = random_password.db_password.result
        db_name = "wordpress"
    }


    resource "aws_db_subnet_group" "main" {
        name       = "wordpress"
        subnet_ids = data.terraform_remote_state.eks.outputs.vpc_subnet_ids
    }


    resource "aws_security_group" "allow_mysql" {
        name        = "allow_mysql"
        description = "Allow MySQL inbound traffic"
        vpc_id      = data.terraform_remote_state.eks.outputs.vpc_id


        ingress {
            description      = "MYSQL from VPC"
            from_port        = 3306
            to_port          = 3306
            protocol         = "tcp"
            cidr_blocks      = [data.terraform_remote_state.eks.outputs.vpc_cidr_block]
        }


        egress {
            from_port        = 0
            to_port          = 0
            protocol         = "-1"
            cidr_blocks      = ["0.0.0.0/0"]
            ipv6_cidr_blocks = ["::/0"]
        }
    }


    resource "aws_db_instance" "main" {
        allocated_storage    = 10
        engine               = "mysql"
        engine_version       = "5.7"
        instance_class       = "db.t3.micro"
        name                 = local.db_name
        username             = local.db_username
        password             = local.db_password
        skip_final_snapshot  = true
        db_subnet_group_name = aws_db_subnet_group.main.name
        vpc_security_group_ids = [aws_security_group.allow_mysql.id]
    }


    resource "random_password" "db_password" {
        length           = 16
        special          = false
    }
                    

Deployment AWS Elasticache via Terraform is straightforward. Let’s define it in cache.tf file:

cache.tf

    resource "aws_elasticache_cluster" "main" {
        cluster_id           = "wordpress"
        engine               = "memcached"
        node_type            = "cache.t3.micro"
        num_cache_nodes      = 1
        parameter_group_name = "default.memcached1.6"
        port                 = 11211
        subnet_group_name    = aws_elasticache_subnet_group.main.name
    }

    resource "aws_elasticache_subnet_group" "main" {
        name       = "wordpress"
        subnet_ids = data.terraform_remote_state.eks.outputs.vpc_subnet_ids
    }
                    

And finally, let’s define Wordpress deployment in wordpress.tf file. Address of the site will be wordpress.<yourdomain>. All parameters that depend on other Terraform resources we can take from properties of these resources. Wordpress admin password will be generated via Terraform as well.

wordpress.tf

    locals {
        wordpress_values = <<EOF
        wordpressUsername: "admin"
        wordpressPassword: "${random_password.wordpress_password.result}"
        ingress:
        enabled: true
        hostname: wordpress.${data.terraform_remote_state.eks.outputs.dns_domain}
        annotations:
            kubernetes.io/ingress.class: "nginx"
        service:
        type: ClusterIP
        mariadb:
        enabled: false
        externalDatabase:
        host: ${aws_db_instance.main.address}
        database: ${aws_db_instance.main.name}
        user: ${local.db_username}
        password: ${local.db_password}
        EOF
    }


    resource "random_password" "wordpress_password" {
        length           = 16
        special          = true
        override_special = "_%@"
    }


    resource "helm_release" "wordpress" {
        name       = "wordpress"
        namespace  = kubernetes_namespace.main.metadata[0].name
        chart      = "wordpress"
        repository = "https://charts.bitnami.com/bitnami"


        values = [
            local.wordpress_values
        ]


        wait = true
    }
                    

Let’s define outputs:

outputs.tf

    output "wordpress_password" {
        value = random_password.wordpress_password.result
    }
                    

Next step will be initialization of terraform script and applying changes:

    $ terraform init -backend-config ./vars/default.tfbackend
    $ terraform apply -var-file ./vars/default.tfvars
    > yes
                    

Wait until script is complete and find a password of a wordpress in outputs array of this command:

    $ terraform show -json
                    

Check result via browser.

Kubernetes / Helm approach

If we want to use Kubernetes to manage the database of Wordpress, we need to install AWS RDS controller in our EKS cluster. Let’s download the chart of rds-controller and put it in infra layer of terraform EKS provisioning:

    $ helm pull oci://public.ecr.aws/aws-controllers-k8s/rds-chart --version v0.0.17
    $ tar -xf rds-chart-v0.0.17.tgz
    $ mv rds-chart terraform/src/infra/charts/rds-chart
                    

First, we need to add IAM permissions to add permissions for cluster to manage RDS instances. For educational purposes, let’s simply modify and deploy policy in terraform/src/eks/iam.tf:

    resource "aws_iam_policy" "nodes_dns_policy" {
        name = "additional.nodes.eks"

        policy = <<EOF
        {
        "Version": "2012-10-17",
        "Statement": [
        {
            "Effect": "Allow",
            "Action": [
            "route53:ChangeResourceRecordSets"
            ],
            "Resource": [
            "arn:aws:route53:::hostedzone/${aws_route53_zone.primary.zone_id}"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
            "route53:ListHostedZones",
            "route53:ListResourceRecordSets"
            ],
            "Resource": [
            "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": "rds:*",
            "Resource": "*"
        }
        ]
        }
        EOF
    }

    resource "aws_iam_role_policy_attachment" "nodes_dns_policy" {
        for_each   = module.eks.eks_managed_node_groups
        policy_arn = aws_iam_policy.nodes_dns_policy.arn
        role       = each.value.iam_role_name
    }
                    

After that let’s add file controllers.tf in infra layer and deploy configuration of rds-chart deployment:

locals {
  rds_values = <<EOF
aws:
  region: eu-central-1
EOF
}

resource "kubernetes_namespace" "controllers" {
  metadata {
    name = "controllers"
  }
}

resource "helm_release" "rds_controller" {
  name       = "rds-controller"
  namespace  = kubernetes_namespace.controllers.metadata[0].name
  chart      = "./charts/rds-chart"
  values     = [local.rds_values]

  wait = true
}
                    

After terraform script is deployed, let’s check that controller is up and running:

    $ aws eks update-kubeconfig –-name eks # Fetch kubeconfig
    $ kubectl get po -n controllers
    NAME                                        READY   STATUS    RESTARTS   AGE
    rds-controller-rds-chart-6965c55d8f-cv596   1/1     Running   0          3h3
                    

When EKS cluster is ready, we can start writing a helm chart for our RDS instance.

    $ helm create db
    $ cd db
    $ find ./templates ! -name '_helpers.tpl' -type f -exec rm -f {} +
    $ echo > values.yaml
                    

We need to define DBInstance resource in templates/dbinstance.yaml:

templates/dbinstance.yaml

apiVersion: rds.services.k8s.aws/v1alpha1
kind: DBInstance
metadata:
  name: {{ include "db.fullname" . }}
spec:
  allocatedStorage: {{ .Values.db.allocatedStorage}}
  engine: {{ .Values.db.engine }}
  engineVersion: {{ .Values.db.engineVersion | quote }}
  dbInstanceClass: {{ .Values.db.instanceClass }}
  dbInstanceIdentifier: {{ .Values.db.instanceIdentifier }}
  dbName: {{ .Values.db.name }}
  masterUsername: {{ .Values.db.masterUsername }}
  masterUserPassword:
    name: {{ include "db.fullname" . }}-creds
    key: password
    namespace: {{ .Release.Namespace }}
  dbSubnetGroupName: {{ .Values.dbSubnetGroup.name }}
  vpcSecurityGroupIDs:
    {{- range $.Values.db.vpcSecurityGroupIDs }}
    - {{ . | quote }}
    {{- end }}
                    

Also, we need a DBSubnetGroup resource for DBInstance. It will be in templates/dbsubnetgroup.yaml:

templates/dbsubnetgroup.yaml

apiVersion: rds.services.k8s.aws/v1alpha1
kind: DBSubnetGroup
metadata:
  name: {{ include "db.fullname" . }}
spec:
  name: {{ .Values.dbSubnetGroup.name }}
  description: {{ .Values.dbSubnetGroup.description }}
  subnetIDs:
    {{- range $.Values.dbSubnetGroup.subnetIDs }}
    - {{ . | quote }}
    {{- end }}
                    

Then, let’s create a secret for our RDS instance password in templates/secret-db-creds.yaml. We will need to specify this password explicitly, because Helm doesn’t have a way to generate passwords and keep them persistent between upgrades:

templates/secret-db-creds.yaml

apiVersion: v1
kind: Secret
type: Opaque
metadata:
  name: {{ include "db.fullname" . }}-creds
data:
  password: {{ .Values.db.masterUserPassword | b64enc | quote }}
                    

And finally, we need to create values.yaml file for default values:

values.yaml

db:
  name: ""
  allocatedStorage: 20
  engine: mysql
  engineVersion: "5.7"
  instanceClass: ""
  instanceIdentifier: ""
  masterUsername: ""
  masterUserPassword: ""
  vpcSecurityGroupIDs: []


dbSubnetGroup:
  name: ""
  description: ""
  subnetIDs: []
                    

Unfortunately, we cannot deploy db and wordpress charts simultaneously, because we will know about DBInstance endpoint only after it’s created and ready, we don’t have a way in Helm and Kubernetes to use status properties of existing resources as input for new resources. So we need to deploy the db helm chart first, wait until the AWS RDS instance is created, fetch the endpoint from resource status properties and then use it to deploy the wordpress helm chart. Let’s create values for db chart:

db-values.yaml

db:
  name: wordpress
  instanceClass: db.t3.micro
  instanceIdentifier: wordpress
  masterUsername: wordpress
  masterUserPassword: mMZTJqGJRcfQvM5K
  vpcSecurityGroupIDs:
    - sg-0e9cfcd062b3ca0d5


dbSubnetGroup:
  name: wordpress
  description: "Wordpress Subnet Group"
  subnetIDs:
    - subnet-099d349fc9781289c
    - subnet-08dc04b820f394868
                    

Deploy helm chart:

    $ kubectl create ns dev
    $ helm upgrade --install --namespace dev -f db-values.yaml --wait rds ./charts/db
                    

When deployment is finished you can see in status properties of dbinstance resource values like these:

    ...
    Endpoint:
        Address:                            wordpress.cscrfrhxk6c6.eu-central-1.rds.amazonaws.com
        Hosted Zone ID:                     Z1RLNUO7B9Q6NB
        Port:                               3306
    ...
                    

Copy endpoint address and use it in your values for wordpress chart:

wordpress-values.yaml

externalDatabase:
  database: wordpress
  host: wordpress.cscrfrhxk6c6.eu-central-1.rds.amazonaws.com
  password: mMZTJqGJRcfQvM5K
  user: wordpress
service:
  type: ClusterIP
ingress:
  annotations:
    kubernetes.io/ingress.class: nginx
  enabled: true
  hostname: wordpress.ds-pet.xyz
mariadb:
  enabled: false
wordpressPassword: nHLHm%yQWGRYK2x1
wordpressUsername: admin
                    

Deploy wp chart:

    helm upgrade --install --n amespace dev -f wordpress-values.yaml --wait wordpress bitnami/wordpress --version 13.0.12
                    

When deployment is finished, check result in a browser.

Terraform approach

Pros:

  • No dependencies needed except Terraform;
  • No need to install additional agents;
  • Most cloud resources are supported;
  • Provides a way to specify dependency graph and ordering of deployment;
  • Supports password generation;
  • Can refer to terraform resource properties which is located in another state without explicit specification.

Cons:

  • Need to know HCL language;
  • Modifications take longer than Helm upgrade;
  • Two artifacts for application: terraform module + helm chart.

Kubernetes / Helm approach

Pros:

  • Everything is packaged in one artifact: Helm chart;
  • Modifications are faster than in Terraform approach.

Cons:

  • Need to know Go template;
  • Doesn’t support password generation;
  • No way to refer on properties of already deployed resources;
  • --wait is not working with CRDs;
  • There is a separate controller for each service type;
  • Some properties may not exist in controller resources;
  • Not so many cloud resources are supported. Example in AWS;
  • No way to specify dependency graph and ordering of deployment;
  • Some of properties needs to be explicitly set if they are related to resources that deployed in Terraform.

Summary

A comparison of these 2 approaches shows us that the Kubernetes / Helm approach can be used in small or medium organizations that require only basic resources, such as EC2, S3, etc. So, it will be easy to provision artifacts with all dependencies that are declared in the same manner. You can use it in projects like eCommerce, news portal, telegram bots, etc. On the other hand, the Terraform approach allows you to provision more resources and allows you to build a much more complex system. It can be used in projects of all sizes. With this approach you can easily cover requirements for Healthcare, Industrial, Big Data, IoT and other areas.

From our perspective, using the Terraform approach to manage cloud resources for applications is more efficient and comfortable than managing cloud resources in Kubernetes. But it’s ok to use the Kubernetes / Helm approach in applications that operate with simple cloud resources such as S3 buckets, EC2 instances.

Test drive the Kaa IoT platform and boost your Internet of Things endeavors

Related Stories

KaaIoT and ELSYS Join Forces for Smart Building Solutions

KaaIoT and ELSYS join forces to bring robust smart building solutions with real-time climate monitoring.

Promoting Smart Building Solutions: Roomsys & Kaa Partnership

Exciting news! KaaIoT has teamed up with Roomsys to promote smart building management IoT solutions!

Simple Industrial IoT Solutions: KaaIoT & Sensative Partnership

The KaaIoT and Sensative partnership tackles a common challenge: water & oil leaks.

Remote Asset Tracking Solutions: KaaIoT & Seeed Partnership

Seeed offers a wide range of IoT hardware products. This article explores how KaaIoT and Seeed are joining...