Tag: Terraform

  • Infrastructure as Code (IaC) Security Best Practices

    Infrastructure as Code (IaC) Security Best Practices

    Infrastructure as Code (IaC) has revolutionized how we provision and manage infrastructure, enabling speed, repeatability, and scalability. However, as with any code, IaC introduces new security considerations. Misconfigurations, exposed secrets, and lack of visibility can lead to significant risks in production environments.

    This guide explores actionable best practices for securing your Terraform, Ansible, and other IaC pipelines-helping you build robust, compliant, and resilient infrastructure. Sample code and how-to notes included!


    1. Treat IaC Like Application Code

    Just as you would with application code, store your IaC in version control systems (e.g., Git). This enables:

    • Change tracking: Who changed what, when, and why.
    • Peer reviews: Enforce code reviews and approvals before merging.
    • Rollback: Revert to previous known-good states if issues arise.

    Tip:
    Use branch protection rules and require pull request reviews for all changes.


    2. Secrets Management

    Never hard-code secrets or sensitive data (API keys, passwords, certificates) in your IaC files. Instead:

    • Use secret management tools like HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault.
    • Integrate secrets into your pipelines at runtime, not in code.
    • Leverage environment variables or encrypted files for sensitive values.

    Sample: Fetching Secrets from Vault in Terraform

    provider "vault" {
      address = "https://vault.example.com"
    }
    
    resource "vault_generic_secret" "example" {
      path = "secret/data/myapp"
    }
    
    output "db_password" {
      value = vault_generic_secret.example.data["password"]
    }

    How-to:

    • Replace https://vault.example.com with your Vault server address.
    • The vault_generic_secret resource fetches secrets dynamically.
    • Output blocks can reference secrets securely-never hardcode them!

    Sample: Encrypting Secrets in Ansible with ansible-vault

    - hosts: all
      vars_files:
        - secrets.yml
      tasks:
        - name: Use secret password
          debug:
            msg: "The password is {{ secret_password }}"

    How-to:

    • Create and encrypt the secrets file: ansible-vault create secrets.yml
    • Reference the encrypted file in your playbook under vars_files.
    • Access secrets in tasks using Jinja2 templating, e.g., {{ secret_password }}.

    3. Automated Security Scanning

    Integrate security tools into your CI/CD pipeline to catch misconfigurations and vulnerabilities early:

    Sample: Terraform Security Scanning with tfsec in GitHub Actions

    name: Terraform Security Scan
    
    on: [push]
    
    jobs:
      tfsec:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v2
          - name: Run tfsec
            uses: aquasecurity/tfsec-action@v1
            with:
              tfsec_version: 'latest'

    How-to:

    • Add this workflow to your .github/workflows directory.
    • The scan will fail the build if critical security issues are found, helping you catch problems early.

    4. Principle of Least Privilege

    Ensure that your IaC tools, pipelines, and the resources they provision follow the principle of least privilege:

    • Limit IAM permissions for automation accounts.
    • Avoid using overly broad roles or root accounts.
    • Regularly audit permissions and remove unnecessary access.

    5. Policy as Code

    Define and enforce security and compliance policies programmatically:

    • Use tools like Open Policy Agent (OPA), Sentinel (for Terraform Cloud), or Conftest.
    • Enforce rules such as “no public S3 buckets,” “encryption enabled,” or “no hardcoded credentials.”

    Example:
    OPA policies can block deployments that violate security standards before they reach production.


    6. Continuous Monitoring and Drift Detection

    Even after deployment, infrastructure can drift from the desired state:

    • Use tools like Terraform Cloud, AWS Config, or Driftctl to detect and remediate drift.
    • Set up alerts for unauthorized changes and automate remediation where possible.

    7. Regular Reviews and Updates

    IaC modules and dependencies evolve-so should your security practices:

    • Schedule regular reviews of your IaC codebase and third-party modules.
    • Update modules to patch vulnerabilities and leverage new security features.
    • Document your security practices and train your team.

    Conclusion

    Securing Infrastructure as Code is not a one-time task, but a continuous process woven into your development and deployment lifecycle. By treating IaC like application code, managing secrets securely, automating scanning, enforcing least privilege, and using policy as code, you can drastically reduce risk and build trust in your automation.

  • Using Ansible to run Terraform plans

    Yes, your eyes are not deceiving you 🙂 Usually, when the words Ansible and Terraform appear in the same sentence is for the opposite scenario. Many articles out there provide examples of using Terraform to deploy infrastructure followed by running Ansible to configure it using the local-exec provisioner. This is a good thing because, if you need some post-provisioning configuration, it is better to rely on another declarative and idempotent tool like Ansible, rather than invoking shell scripts.

    But in this post we are not going to do any of that. What we are going to do is the other way around. We are going to have an Ansible playbook that runs your Terraform plan.

    This article is a follow up from a series of posts to introduce infrastructure admins and engineers to Terraform and to explain how to use it to manage on-prem physical infrastructure in a datacenter. You can find access the full series here.

    Reasons to do it

    But why? Well, if you have an physical infrastructure background and are running a traditional datacenter more than likely you have been playing with Ansible. Chances are you started with Ansible running in the command line and hard-coding your infrastructure’s credentials inside the playbook. Then you evolved into using an external credentials file and encrypting it using “ansible-vault”. Eventually you might have deployed Ansible AWX or even RedHat AAP. At this point, you are enjoying RBAC (who can run what playbook and on what infrastructure) and amazing (there is no other way to put it) credential management.

    Then, you talk to your developers over coffee and they tell you about Terraform. But when you get familiar with it, you discover that unless you pay for a SaaS offering you are stuck in the “command-line with hard-coded credentials” scenario. Wouldn’t it be great if you could use AWX to run Terraform? You could enjoy RBAC and credential management and all the other goodness that it provides. This is possible thanks to Ansible’s Terraform module.

    First steps

    Let’s say we have a very basic Terraform project to create a storage group in Dell PowerMax. The “main.tf” shown below starts the PowerMax provider and then creates a storage group “terraform_sg”. I don’t think it is strictly necessary, but if you are interested in learning more about provisioning storage in Dell PowerMax you can check out this previous post. For simplicity we haven’t created or referenced any variables. All the information is contained into a single file, the “main.tf” shown below.

    terraform {
      required_providers {
        powermax = {
          source = "dell/powermax"
        }
      }
    }
    
    provider "powermax" {
      username      = "smc"
      password      = "TFp2ssw0rd"
      endpoint      = "https://10.1.2.3:8443"
      serial_number = "000123456789"
      pmax_version  = 100
      insecure      = true
    }
    
    resource "powermax_storagegroup" "tf_sg" {
      name          = "terraform_sg"
      srp_id         = "SRP_1"
    }

    In the same machine we have both Terraform and Ansible installed. In order to run the above Terraform plan with Ansible, we need at a minimum a playbook that looks like follows. It needs to include two mandatory parameters: “project_path” and “state”. The first parameter specifies the directory that contains the plan, ie the “main.tf” and typically any other files like “variables.tf”. My Ansible playbook is called “runtf.yml”.

    ---
    - hosts: localhost
      gather_facts: no
    
      vars:
        project_dir: "/root/powermax" # folder that contains main.tf, ...
    
      tasks:
        - name: Run terraform
          community.general.terraform:
            project_path: '{{ project_dir }}'
            state: present
          register: output
    
        - debug:
            var: output

    As you can see I have registered the output of the terraform task and then created a second task to print it out on the terminal so that we can get more details. All is left now is to run it using the “ansible-playbook” command

    root@alb-terraform:~# ansible-playbook runtf.yml
    
    PLAY [localhost] ***************************************************************************************
    
    TASK [Run terraform] ***************************************************************************************
    changed: [localhost]
    
    TASK [debug] ***************************************************************************************
    ok: [localhost] => {
        "output": {
            "changed": true,
            "command": "/usr/bin/terraform apply -no-color -input=false -auto-approve -lock=true /tmp/tmpjaojemdl.tfplan",
            "failed": false,
            "outputs": {},
            "state": "present",
            "stderr": "",
            "stderr_lines": [],
            "stdout": "powermax_storagegroup.tf_sg: Creating...\npowermax_storagegroup.tf_sg: Creation complete after 0s [id=terraform_sg]\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n",
            "stdout_lines": [
                "powermax_storagegroup.tf_sg: Creating...",
                "powermax_storagegroup.tf_sg: Creation complete after 0s [id=terraform_sg]",
                "",
                "Apply complete! Resources: 1 added, 0 changed, 0 destroyed."
            ],
            "workspace": "default"
        }
    }
    
    PLAY RECAP ***************************************************************************************
    localhost  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    ignored=0

    Ansible run Terraform, and Terraform managed to make the changes so as expected, Ansible reports the task as “changed”. In the “debug” task, notice the “command” parameter. This is how Ansible is invoking Terraform. This invocation includes the “-auto-approve” flag so that Terraform doesn’t look for confirmation. The “stdout” and stdout_lines” parameters show the output that has been captured from Terraform about the creation of the different resources .

    A very important feature of both Ansible and Terraform is idempotency, ie the ability to run the same code repeatedly without making any damage. Let’s run it again and observe what happens.

    root@alb-terraform:~# ansible-playbook runtf.yml
    
    PLAY [localhost] ***************************************************************************************
    
    TASK [Run terraform] ***************************************************************************************
    ok: [localhost]
    
    TASK [debug] ***************************************************************************************
    ok: [localhost] => {
        "output": {
            "changed": false,
            "command": "/usr/bin/terraform apply -no-color -input=false -auto-approve -lock=true /tmp/tmp718kkwbo.tfplan",
            "failed": false,
            "outputs": {},
            "state": "present",
            "stderr": "",
            "stderr_lines": [],
            "stdout": "powermax_storagegroup.tf_sg: Refreshing state... [id=terraform_sg]\n\nNo changes. Your infrastructure matches the configuration.\n\nTerraform has compared your real infrastructure against your configuration\nand found no differences, so no changes are needed.\n",
            "stdout_lines": [
                "powermax_storagegroup.tf_sg: Refreshing state... [id=terraform_sg]",
                "",
                "No changes. Your infrastructure matches the configuration.",
                "",
                "Terraform has compared your real infrastructure against your configuration",
                "and found no differences, so no changes are needed."
            ],
            "workspace": "default"
        }
    }
    
    PLAY RECAP ***************************************************************************************
    localhost : ok=2    changed=0    unreachable=0    failed=0    skipped=0     ignored=0
    

    As expected, Ansible is reporting “ok”, which means the current state matched the desired state and therefore no changes were made. It has also captured the relevant from Terraform stating the same thing.

    Using variables

    This is a good start but ideally we would like to have the ability to make the plan more flexible by using variables for information such as the storage group name in this case. We can modify the “main.tf” as follows. Notice how the “default” value of the variable is commented out. It doesn’t matter if it is commented or not because when we invoke Terraform with external variables we overwrite the default value defined in the variable blocks.

    terraform {
      required_providers {
        powermax = {
          source = "dell/powermax"
        }
      }
    }
    
    provider "powermax" {
      username      = "smc"
      password      = var.password
      endpoint      = "https://10.1.2.3:8443"
      serial_number = "000123456789"
      pmax_version  = 100
      insecure      = true
    }
    
    variable "sg_name" {
     description = "Name of volume to create"
     type        = string
     #default     = "terraform_sg"
    }
    
    variable "password" {
      type        = string
      description = "Stores the password of Unisphere."
      #default = ""
    }
    
    resource "powermax_storagegroup" "tf_sg" {
      name          = var.sg_name
      srp_id         = "SRP_1"
    }

    Additionally, notice how in the “provider” block we have also referenced another variable for the “password”. For security reasons we might want to have, sensitive information like that, stored in a separate file and protect it. In this case we haven’t even declared a default value because we can leverage ansible-vault or AWX to encrypt it and to pass it to Terraform at run time.

    Now, the corresponding Ansible playbook needs to use the “variables” parameter. This parameter essentially converts each of the variables into a “-var” flag when invoking Terraform.

    ---
    - hosts: localhost
      gather_facts: no
    
      vars:
        project_dir: "/root/powerstore" # folder that contains main.tf, ...
    
      tasks:
        - name: Run terraform
          community.general.terraform:
            project_path: '{{ project_dir }}'
            state: present
            variables:
              sg_name: "{{ vol_name }}"
              password: "{{ password }}"
          register: output
    
        - debug:
            var: output

    Now, at runtime we are going to select a different storage group name. The storage group is created as expected and the relevant Terraform output is captured.

    root:~# ansible-playbook runtf.yml --extra-vars "sg_name=tf-sg2 password=TFp2ssw0rd"
    
    PLAY [localhost] ***************************************************************************************
    
    TASK [Run terraform] ***************************************************************************************
    changed: [localhost]
    
    TASK [debug] ***************************************************************************************
    ok: [localhost] => {
        "output": {
            "changed": true,
            "command": "/usr/bin/terraform apply -no-color -input=false -auto-approve -lock=true /tmp/tmpimdwzdu1.tfplan",
            "failed": false,
            "outputs": {},
            "state": "present",
            "stderr": "",
            "stderr_lines": [],
            "stdout": "powermax_storagegroup.tf_sg: Creating...\npowermax_storagegroup.tf_sg: Creation complete after 5s [id=vol1]\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n",
            "stdout_lines": [
                "powermax_storagegroup.tf_sg: Creating...",
                "powermax_storagegroup.tf_sg: Creation complete after 5s [id=vol1]",
                "",
                "Apply complete! Resources: 1 added, 0 changed, 0 destroyed."
            ],
            "workspace": "default"
        }
    }
    
    PLAY RECAP ***************************************************************************************
    localhost  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    ignored=0

    Terraform plan

    We went straight to “terraform apply” but most Terraform workflows also include running the plan command first. This is done with Ansible by running the playbook in “check mode”. Check mode invokes “terraform plan” for us.

    root:~# ansible-playbook runtf.yml --check --extra-vars "sg_name=tf-sg3 password=TFpass"
    
    PLAY [localhost] ***************************************************************************************
    
    TASK [Run terraform] ***************************************************************************************
    ok: [localhost]
    
    TASK [debug] ***************************************************************************************
    ok: [localhost] => {
        "output": {
            "changed": false,
            "command": "/usr/bin/terraform apply -no-color -input=false -auto-approve -lock=true /tmp/tmp6f2o83k5.tfplan",
            "failed": false,
            "outputs": {},
            "state": "present",
            "stderr": "",
            "stderr_lines": [],
            "stdout": "powermax_storagegroup.tf_sg: Refreshing state... [id=tf-sg2]\n\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n  ~ update in-place\n\nTerraform will perform the following actions:\n\n  # powermax_storagegroup.tf_sg will be updated in-place\n  ~ resource \"powermax_storagegroup\" \"tf_sg\" {\n      ~ cap_gb                   = 0 -> (known after apply)\n      + child_storage_group      = (known after apply)\n      ~ compression              = true -> (known after apply)\n      + compression_ratio        = (known after apply)\n      ~ compression_ratio_to_one = 0 -> (known after apply)\n      + device_emulation         = (known after apply)\n      ~ id                       = \"tf-sg2\" -> (known after apply)\n      + maskingview              = (known after apply)\n      ~ name                     = \"tf-sg2\" -> \"tf-sg3\"\n      ~ num_of_child_sgs         = 0 -> (known after apply)\n      ~ num_of_masking_views     = 0 -> (known after apply)\n      ~ num_of_parent_sgs        = 0 -> (known after apply)\n      + num_of_snapshot_policies = (known after apply)\n      ~ num_of_snapshots         = 0 -> (known after apply)\n      ~ num_of_vols              = 0 -> (known after apply)\n      + parent_storage_group     = (known after apply)\n      + service_level            = (known after apply)\n      ~ slo                      = \"NONE\" -> (known after apply)\n      ~ slo_compliance           = \"NONE\" -> (known after apply)\n      + snapshot_policies        = (known after apply)\n      + tags                     = (known after apply)\n      ~ type                     = \"Standalone\" -> (known after apply)\n      ~ unprotected              = true -> (known after apply)\n      ~ unreducible_data_gb      = 0 -> (known after apply)\n      + uuid                     = (known after apply)\n      ~ volume_ids               = [] -> (known after apply)\n      + vp_saved_percent         = (known after apply)\n      + workload                 = (known after apply)\n        # (2 unchanged attributes hidden)\n    }\n\nPlan: 0 to add, 1 to change, 0 to destroy.\n\n─────────────────────────────────────────────────────────────────────────────\n\nSaved the plan to: /tmp/tmp6f2o83k5.tfplan\n\nTo perform exactly these actions, run the following command to apply:\n    terraform apply \"/tmp/tmp6f2o83k5.tfplan\"\n",
            "stdout_lines": [
                "powermax_storagegroup.tf_sg: Refreshing state... [id=tf-sg2]",
                "",
                "Terraform used the selected providers to generate the following execution",
                "plan. Resource actions are indicated with the following symbols:",
                "  ~ update in-place",
                "",
                "Terraform will perform the following actions:",
                "",
                "  # powermax_storagegroup.tf_sg will be updated in-place",
                "  ~ resource \"powermax_storagegroup\" \"tf_sg\" {",
                "      ~ cap_gb                   = 0 -> (known after apply)",
                "      + child_storage_group      = (known after apply)",
                "      ~ compression              = true -> (known after apply)",
                "      + compression_ratio        = (known after apply)",
                "      ~ compression_ratio_to_one = 0 -> (known after apply)",
                "      + device_emulation         = (known after apply)",
                "      ~ id                       = \"tf-sg2\" -> (known after apply)",
                "      + maskingview              = (known after apply)",
                "      ~ name                     = \"tf-sg2\" -> \"tf-sg3\"",
                "      ~ num_of_child_sgs         = 0 -> (known after apply)",
                "      ~ num_of_masking_views     = 0 -> (known after apply)",
                "      ~ num_of_parent_sgs        = 0 -> (known after apply)",
                "      + num_of_snapshot_policies = (known after apply)",
                "      ~ num_of_snapshots         = 0 -> (known after apply)",
                "      ~ num_of_vols              = 0 -> (known after apply)",
                "      + parent_storage_group     = (known after apply)",
                "      + service_level            = (known after apply)",
                "      ~ slo                      = \"NONE\" -> (known after apply)",
                "      ~ slo_compliance           = \"NONE\" -> (known after apply)",
                "      + snapshot_policies        = (known after apply)",
                "      + tags                     = (known after apply)",
                "      ~ type                     = \"Standalone\" -> (known after apply)",
                "      ~ unprotected              = true -> (known after apply)",
                "      ~ unreducible_data_gb      = 0 -> (known after apply)",
                "      + uuid                     = (known after apply)",
                "      ~ volume_ids               = [] -> (known after apply)",
                "      + vp_saved_percent         = (known after apply)",
                "      + workload                 = (known after apply)",
                "        # (2 unchanged attributes hidden)",
                "    }",
                "",
                "Plan: 0 to add, 1 to change, 0 to destroy.",
                "",
                "─────────────────────────────────────────────────────────────────────────────",
                "",
                "Saved the plan to: /tmp/tmp6f2o83k5.tfplan",
                "",
                "To perform exactly these actions, run the following command to apply:",
                "    terraform apply \"/tmp/tmp6f2o83k5.tfplan\""
            ],
            "workspace": "default"
        }
    }
    
    PLAY RECAP ***************************************************************************************
    localhost   : ok=2    changed=0    unreachable=0    failed=0    skipped=0    ignored=0

    No changes are made and the Ansible module has captured the details of the changes that will be made if the plan is applied.

    Terraform destroy

    Finally we need to learn how to use “terraform destroy”. You might have guessed it by now. The native way of destroying something in Ansible is by setting the state to “absent”.

    ---
    - hosts: localhost
      gather_facts: no
    
      vars:
        project_dir: "/root/powermax" # dir that contains main.tf, ...
    
      tasks:
        - name: Run terraform
          community.general.terraform:
            project_path: '{{ project_dir }}'
            state: absent
            variables:
              sg_name: "{{ sg_name }}"
              password: "{{ password }}"
          register: output
    
        - debug:
            var: output

    Let’s run it and see what it does.

    root:~# ansible-playbook runtf.yml --extra-vars "sg_name=tf-sg2 password=TFp2ssw0rd"
    
    PLAY [localhost] ***************************************************************************************
    
    TASK [Run terraform] ***************************************************************************************
    changed: [localhost]
    
    TASK [debug] ***************************************************************************************
    ok: [localhost] => {
        "output": {
            "changed": true,
            "command": "/usr/bin/terraform destroy -no-color -auto-approve -lock=true -var sg_name=tf-sg2 -var password=smc",
            "failed": false,
            "outputs": {},
            "state": "absent",
            "stderr": "",
            "stderr_lines": [],
            "stdout": "powermax_storagegroup.tf_sg: Refreshing state... [id=tf-sg2]\n\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n  - destroy\n\nTerraform will perform the following actions:\n\n  # powermax_storagegroup.tf_sg will be destroyed\n  - resource \"powermax_storagegroup\" \"tf_sg\" {\n      - cap_gb                   = 0 -> null\n      - compression              = true -> null\n      - compression_ratio_to_one = 0 -> null\n      - host_io_limit            = {} -> null\n      - id                       = \"tf-sg2\" -> null\n      - name                     = \"tf-sg2\" -> null\n      - num_of_child_sgs         = 0 -> null\n      - num_of_masking_views     = 0 -> null\n      - num_of_parent_sgs        = 0 -> null\n      - num_of_snapshots         = 0 -> null\n      - num_of_vols              = 0 -> null\n      - slo                      = \"NONE\" -> null\n      - slo_compliance           = \"NONE\" -> null\n      - srp_id                   = \"SRP_1\" -> null\n      - type                     = \"Standalone\" -> null\n      - unprotected              = true -> null\n      - unreducible_data_gb      = 0 -> null\n      - volume_ids               = [] -> null\n    }\n\nPlan: 0 to add, 0 to change, 1 to destroy.\npowermax_storagegroup.tf_sg: Destroying... [id=tf-sg2]\npowermax_storagegroup.tf_sg: Destruction complete after 0s\n\nDestroy complete! Resources: 1 destroyed.\n",
            "stdout_lines": [
                "powermax_storagegroup.tf_sg: Refreshing state... [id=tf-sg2]",
                "",
                "Terraform used the selected providers to generate the following execution",
                "plan. Resource actions are indicated with the following symbols:",
                "  - destroy",
                "",
                "Terraform will perform the following actions:",
                "",
                "  # powermax_storagegroup.tf_sg will be destroyed",
                "  - resource \"powermax_storagegroup\" \"tf_sg\" {",
                "      - cap_gb                   = 0 -> null",
                "      - compression              = true -> null",
                "      - compression_ratio_to_one = 0 -> null",
                "      - host_io_limit            = {} -> null",
                "      - id                       = \"tf-sg2\" -> null",
                "      - name                     = \"tf-sg2\" -> null",
                "      - num_of_child_sgs         = 0 -> null",
                "      - num_of_masking_views     = 0 -> null",
                "      - num_of_parent_sgs        = 0 -> null",
                "      - num_of_snapshots         = 0 -> null",
                "      - num_of_vols              = 0 -> null",
                "      - slo                      = \"NONE\" -> null",
                "      - slo_compliance           = \"NONE\" -> null",
                "      - srp_id                   = \"SRP_1\" -> null",
                "      - type                     = \"Standalone\" -> null",
                "      - unprotected              = true -> null",
                "      - unreducible_data_gb      = 0 -> null",
                "      - volume_ids               = [] -> null",
                "    }",
                "",
                "Plan: 0 to add, 0 to change, 1 to destroy.",
                "powermax_storagegroup.tf_sg: Destroying... [id=tf-sg2]",
                "powermax_storagegroup.tf_sg: Destruction complete after 0s",
                "",
                "Destroy complete! Resources: 1 destroyed."
            ],
            "workspace": "default"
        }
    }
    
    PLAY RECAP ***************************************************************************************
    localhost  : ok=2    changed=1    unreachable=0    failed=0    skipped=0     ignored=0

    The storage group resource has been destroyed and that shows in Ansible as a task with a status of “changed”. The “stdout” and “stdout_lines” parameters provide a record of what was destroyed.

    In summary, in this article we have seen how to use the terraform Ansible module to plan, apply and destroy Terraform projects. In doing that, organizations can leverage a tool like AWX or RedHat AAP to provide RBAC, strong credential management and a powerful upstream REST API and integrate with other tools like ITSM etc. AWX and AAP can also became a polyglot automation platform capable of running the two most popular automation tools out there to satisfy both traditional infrastructure teams and developers.

  • ServiceNow catalog request with Postman

    In this blog post we will explore how to order items from a ServiceNow catalog using the ServiceNow REST API. There are a few examples out there that use “REST API Explorer” inside ServiceNow. I will focus here on how to do it from an external tool. In particular I am going to provide instructions for Postman. This post is the stepping stone for this follow up post where I try to solve an interesting use case of providing multi-cloud with governance through ServiceNow. You can get the Postman collection I am using from this GitHub repo.

    This is the latest addition to a series of popular ServiceNow related posts:

    There is also a follow up post that uses what I am covering here to allow developers to provision multi-cloud IT resources while observing any governance that might have been implemented in ServiceNow.

    In the post about creating Incidents we covered the “REST API Explorer” tool in more detail. Please refer to that one if you are new to it. In this post we will use the “Service Catalog API“. In this screenshot you can see it is a very extensive API. It provides 47 API calls as of time of this writing.

    Postman

    Now let’s move to Postman. Feel free to use any other REST API developer tool you are used to. If you want to follow along you can retrieve my Postman collection from this GitHub repo. The Postman collection provides 3 folders. In this post we will use the “Catalog Request” folder. The other 2 folders contain the API calls that were used in 2 previous posts in this series mentioned above.

    Requesting items from catalogs can be done in two ways that mimic the behavior of the user in the GUI:

    • order now
    • add to cart and submit the cart

    Add to Cart and Submit order

    This is a 2-step process but it might be more convenient if you are planning to order multiple items in the same request.

    • add an item to the cart
    • submit the cart

    Adding an item to the cart is done with the following API call. Please replace {instance} with your particular ServiceNow instance.

    There are 3 things you need to include:

    • sys_id“. This is the “sys_id” of the item itself. As you can see it needs to be included in the URL
    • sysparm_quantity“. This is the quantity of this item you want to add to the cart. It is included in the “Body” parameter and it is required
    • variables“. This is also in the “Body” and needs to include all the information that you would normally provide through the GUI when creating the request. In my case this catalog entry allows me to request the installation of an application on a virtual machine. At a minimum you will need to include all variables that are defined as “mandatory” in your catalog. In my example below I am going to request the installation of an application on an existing virtual machine. You can see I am requesting the installation of Apache web server on a VM called “albtest3_01” which lives in my “sin” datacenter

    If all goes well, you should get a status code of 200 and the details of the cart will be provided in the response.

    You can find out the “sys_id” of the catalog item by browsing to the item itself in your catalog. It will be included in the URL itself as seen here

    Alternatively you can use other calls in the “Service Catalog API” to get it programmatically. One example could be executing this call and looking for the “name” of the item you want:

    You can also find out programmatically what variables you need to include with the following API call. It returns information about every parameter the specific item defined by “sys_id”. Use the “name” fields as the keys in your “Body” parameter. Also pay attention to whether variables show as “mandatory: true“.

    Notice how the response is providing even the valid “choices” for a variable that has been configured as a “select box”.

    At this point you have an item in the cart. You could potentially add other items to the cart but if you need things to be ordered in a certain sequence order 1 item at a time and orchestrate them all in your Infrastructure as Code tool of choice. The final step is to “submit the order”. This is done with the following API call

    This API call is also part of the Postman collection you downloaded. As you can see in the screenshot below, even though this is a POST call, it doesn’t require a “Body” payload. If successful you should get a status code of 200 and the “Body” of the response will contain the “requests number”.

    Order now

    This is the most straight forward process as it consists of a single REST API call. It uses the “Buy Item (POST)” call. This is the call as shown in REST API Explorer.

    This call needs the same payload that we used to add an item to the cart, ie the quantity and the specific variables that are typically provided in the form in the GUI.

    As you can see, this API call returned a request number instead of cart details because the request was created in a single step. In the following screenshot we can see the request in the “Requests” application in the GUI.

    And in the details of the “Request Item” we can see that the information we specified in the body of the API call was used to create the request.

    Next steps

    Postman allows you to generate code in your language of choice, ex: Python, Golang, etc. You can use this to include what we have covered into a larger script that runs additional tasks.

    Alternatively, you might want to use configuration management or provisioning tools like Ansible or Terraform to order items from the ServiceNow catalog.