Introduction to Cloud Development Kit for Terraform

Introduction to Cloud Development Kit for Terraform

Cloud Development Kit for Terrafrom or CDKTF as it is commonly referred to is a tool kit that allows you to leverage the advantages of Terrafrom from within a high-level programming language such as TypeScript, Python, Java, C#, or Go. CDKTF automatically extracts the schema from Terraform providers and modules to generate the necessary classes for your application. We can use CDKTF with every provider that is avaiable on the Terrafrom registry.

In this post, we will look at an example of using CDKTF with Python to provision resources on a Palo Alto Networks Panorama. I chose a Palo Alto Networks Panorama as the target here because I am from a network engineering background.

Installation

Hashicorp have well documented steps on how you can install CDKTF and can be found here.

Getting Started

Initialise a new Project

Once we have successfully installed CDKTF and verified our installation, we begin by initialising a cdktf project with an appropriate template. Here, we are using Python as our template.

cdktf init --template=python --local --project-name cdktf-post --project-description A-simple-project

Initialise a new CDKTF project

Once the initialiation has been completed, you will notice that several new files and folders are created. The main.py file is where we will be writing our code.

Add a provider

Next, we need to add the provider that we will be using in our code. In our case it would be the PaloAltoNetworks/panos provider.

cdktf provider add PaloAltoNetworks/panos

Add a provider

Once the provider has been added you would again notice a new folder called imports being created. This folder has sub-folders containing Python Classes that we can import and use in our script. If you observe closely, the sub-folders directly correpond to resources in the PaloAltoNetworks/panos provider.

Edit the code

We will now edit the code to import the provider and create a network object on Panorama by editing the class MyStack in our main.py file. Refer to the in-line comments in the snippet below for explanation.

#!/usr/bin/env python
from constructs import Construct
from cdktf import App, TerraformStack
# Import the PanosProvider class from the imports folder that was created when we added the provider.
from imports.panos.provider import PanosProvider
# Import the panorama_address_object class from the imports folder that was created when we added the provider.
from imports.panos.panorama_address_object import PanoramaAddressObject

class MyStack(TerraformStack):
    def __init__(self, scope: Construct, id: str):
        super().__init__(scope, id)

        # Initialise the provider
        PanosProvider(self, 'panos', hostname="192.168.1.180", 
                    username="admin", 
                    password="Password123")
        
        """
        HCL Equivalent of the resource we are trying to create:
        
        resource "panos_panorama_address_object" "o2" {
            name = "ntp2"
            value = "192.168.1.1"
            type = "ip-netmask"
        }
        """
        PanoramaAddressObject(self, "o2", name="ntp2", value="192.168.1.1", type="ip-netmask")

app = App()
MyStack(app, "cdktf-post")

app.synth()

main.py

Initialise and Generate Plan

Once we have updated the code, we initialise terraform and generate the plan by executing the cdktf diff command.

cdktf diff

⠴  Synthesizing
[2022-11-15T12:00:57.844] [ERROR] default - /opt/homebrew/Cellar/pipenv/2022.9.24/libexec/lib/python3.11/site-packages/pipenv/vendor/attr/_make.py:876: RuntimeWarning: Running interpreter doesn't sufficiently support code object introspection.  Some features like bare super() or accessing __class__ will not work with slotted classes.
cdktf-post  Initializing the backend...
cdktf-post
            Successfully configured the backend "local"! Terraform will automatically
            use this backend unless the backend configuration changes.
cdktf-post  Initializing provider plugins...
            - Finding paloaltonetworks/panos versions matching "1.11.0"...
cdktf-post  - Using paloaltonetworks/panos v1.11.0 from the shared cache directory
cdktf-post  Terraform has created a lock file .terraform.lock.hcl to record the provider
            selections it made above. Include this file in your version control repository
            so that Terraform can guarantee to make the same selections by default when
            you run "terraform init" in the future.
cdktf-post  ╷
            │ Warning: Incomplete lock file information for providers
            │
            │ Due to your customized provider installation methods, Terraform was forced
            │ to calculate lock file checksums locally for the following providers:
            │   - paloaltonetworks/panos
            │
            │ The current .terraform.lock.hcl file only includes checksums for
            │ darwin_arm64, so Terraform running on another platform will fail to install
            │ these providers.
            │
            │ To calculate additional checksums for another platform, run:
            │   terraform providers lock -platform=linux_amd64
            │ (where linux_amd64 is the platform to generate)
            ╵

            Terraform has been successfully initialized!

            You may now begin working with Terraform. Try running "terraform plan" to see
            any changes that are required for your infrastructure. All Terraform commands
            should now work.

            If you ever set or change modules or backend configuration for Terraform,
            rerun this command to reinitialize your working directory. If you forget, other
            commands will detect it and remind you to do so if necessary.
cdktf-post  Terraform used the selected providers to generate the following execution
            plan. Resource actions are indicated with the following symbols:
              + create

            Terraform will perform the following actions:
cdktf-post    # panos_panorama_address_object.o2 (o2) will be created
              + resource "panos_panorama_address_object" "o2" {
                  + device_group = "shared"
                  + id           = (known after apply)
                  + name         = "ntp2"
                  + type         = "ip-netmask"
                  + value        = "192.168.1.1"
                  + vsys         = "vsys1"
                }

            Plan: 1 to add, 0 to change, 0 to destroy.

            ─────────────────────────────────────────────────────────────────────────────

            Saved the plan to: plan

            To perform exactly these actions, run the following command to apply:
                terraform apply "plan"

Plan

On closer observation of the command output, it is similar to the output of terrafrom plan command and it provides us a summary of the changes it will make.

Deploying the changes

To create the resource on Panorama we execute the cdktf deploy command. This will result in the network object being created on Panorama.

cdktf deploy

⠼  Synthesizing
[2022-11-15T12:01:18.045] [ERROR] default - /opt/homebrew/Cellar/pipenv/2022.9.24/libexec/lib/python3.11/site-packages/pipenv/vendor/attr/_make.py:876: RuntimeWarning: Running interpreter doesn't sufficiently support code object introspection.  Some features like bare super() or accessing __class__ will not work with slotted classes.
cdktf-post  Initializing the backend...
cdktf-post  Initializing provider plugins...
            - Reusing previous version of paloaltonetworks/panos from the dependency lock file
cdktf-post  - Using previously-installed paloaltonetworks/panos v1.11.0
cdktf-post  Terraform has been successfully initialized!

            You may now begin working with Terraform. Try running "terraform plan" to see
            any changes that are required for your infrastructure. All Terraform commands
            should now work.

            If you ever set or change modules or backend configuration for Terraform,
            rerun this command to reinitialize your working directory. If you forget, other
            commands will detect it and remind you to do so if necessary.
cdktf-post  Terraform used the selected providers to generate the following execution
            plan. Resource actions are indicated with the following symbols:
              + create

            Terraform will perform the following actions:
cdktf-post    # panos_panorama_address_object.o2 (o2) will be created
              + resource "panos_panorama_address_object" "o2" {
                  + device_group = "shared"
                  + id           = (known after apply)
                  + name         = "ntp2"
                  + type         = "ip-netmask"
                  + value        = "192.168.1.1"
                  + vsys         = "vsys1"
                }

            Plan: 1 to add, 0 to change, 0 to destroy.

            ─────────────────────────────────────────────────────────────────────────────

            Saved the plan to: plan

            To perform exactly these actions, run the following command to apply:
                terraform apply "plan"
cdktf-post  panos_panorama_address_object.o2 (o2): Creating...
cdktf-post  panos_panorama_address_object.o2 (o2): Creation complete after 1s [id=shared:ntp2]
cdktf-post
            Apply complete! Resources: 1 added, 0 changed, 0 destroyed.


No outputs found.

Apply

Bonus - Looping to create multiple objects

One of the hardest concepts to comprehend about Terraform for me was loops. CDKTF enables us to use the looping concepts from the high-level programming of our choice to create resouces. The below example shows how we can use python for loop to create multiple objects.

#!/usr/bin/env python
from constructs import Construct
from cdktf import App, TerraformStack
# Import the PanosProvider class from the imports folder that was created when we added the provider.
from imports.panos.provider import PanosProvider
# Import the panorama_address_object class from the imports folder that was created when we added the provider.
from imports.panos.panorama_address_object import PanoramaAddressObject

class MyStack(TerraformStack):
    def __init__(self, scope: Construct, id: str):
        super().__init__(scope, id)

        # define resources here
        PanosProvider(self, 'panos', hostname="192.168.1.180", 
                    username="admin", 
                    password="Password123")

        objects = [
            {"name": "test1", "ip": "1.1.1.1/32", "type": "ip-netmask"},
            {"name": "test2", "ip": "google.com", "type": "fqdn"},
            {"name": "test3", "ip": "1.1.1.20-1.1.1.30", "type": "ip-range"},
        ]

        for object in objects:
            PanoramaAddressObject(self, object["name"], name=object["name"], value=object["ip"], type=object["type"])

app = App()
MyStack(app, "cdktf-post")

app.synth()

Looping over data