Skip to content
Mar 31 / Greg

ServiceNow to Terraform via Event-Driven Ansible

This demo shows the dream. A user goes to SNOW’s service catalog, clicks order on a VM, and the Ansible Automation Platform(AAP) calls Terraform to provision. AAP will then configure the server, add it to monitoring, complete any tickets…essentially anything you want to happen. Again, I can’t stress enough how insanely functional this is for your users…as far as they are concerned everything just works!

Simple Data Flow


It starts with the SNOW service catalog item being ordered.
It then securely passes all of the service catalog item data(brings it over as variables) to EDA.
EDA then, via a rulebook, passes those variables over to controller as extra-vars.
AAP then clones my terraform repo and provisions the infrastructure!

Demo

SNOW and EDA Configuration

This process looks like the following:

I went into great detail here in another blog post. I detail configuring both ServiceNow and connecting it to EDA in that blog post, so reference that for full config info and a video!

Playbook

All of my playbooks can be found here in my public repo.
I’ve got the Terraform playbook in there right now, which I’ve got posted below(be sure to check the source for any updates):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
---
- name: Deploy VMware infrastructure using Terraform from EDA
  hosts: ee-builder
  gather_facts: no
  vars:
    snow_eda_directory: /root/snow-eda
    terraform_directory: "{{ snow_eda_directory }}/vmware-terraform"
    terraform_variables_file: "{{ terraform_directory }}/terraform.tfvars"
 
    # test data
    catalog_item: "{{ ansible_eda.event.payload.catalog_item | default('GregSowell VMWare VM Terraform AAP')}}"
    cpus: "{{ ansible_eda.event.payload.cpus | default('4') }}"
    dc_name: "{{ ansible_eda.event.payload.dc_name | default('MNS') }}"
    default_gateway: "{{ ansible_eda.event.payload.default_gateway | default('10.0.50.1') }}"
    event: "{{ ansible_eda.event.payload.event | default('SERVICE_CATALOG') }}"
    hd_size: "{{ ansible_eda.event.payload.hd_size | default('40') }}"
    ip_address: "{{ ansible_eda.event.payload.ip_address | default('10.0.50.77') }}"
    os_type: "{{ ansible_eda.event.payload.os_type | default('Rocky9') }}"
    ram_size: "{{ ansible_eda.event.payload.ram_size | default('4096') }}"
    request_name: "{{ ansible_eda.event.payload.request_name | default('1111111111') }}"
    requester: "{{ ansible_eda.event.payload.requester | default('[email protected]') }}"
    subnet_mask: "{{ ansible_eda.event.payload.subnet_mask | default('255.255.255.0') }}"
    vm_folder: "{{ ansible_eda.event.payload.vm_folder | default('/Greg/redhat/terraform') }}"
    vm_name: "{{ ansible_eda.event.payload.vm_name | default('NewVM1') }}"
 
    remove_vm: false
 
    # Git settings
    backup_repo: [email protected]:gregsowell/snow-eda
    git_name: Git Backup
    git_email: [email protected]
 
  tasks:
    - name: Create the ssh key file based on the supplied cred
      become: true
      run_once: true
      ansible.builtin.copy:
        dest: "/tmp/id_rsa"
        content: "{{ cert_key }}"
        mode: '0600'
 
    - name: Check if snow-eda directory exists
      stat:
        path: "{{ snow_eda_directory }}"
      register: snow_eda_dir   
 
    - name: Clone git repo if snow-eda directory does not exist
      when: not snow_eda_dir.stat.exists
      # ansible.builtin.shell: git clone https://github.com/gregsowell/snow-eda.git
      ansible.builtin.shell: "git config --global core.sshCommand 'ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i /tmp/id_rsa'; git clone {{ backup_repo }}" 
      args:
        chdir: /root
      environment:
        GIT_COMMITTER_NAME: "{{ git_name | default(omit) }}"
        GIT_COMMITTER_EMAIL: "{{ git_email | default(omit) }}"
        GIT_AUTHOR_NAME: "{{ git_name | default(omit) }}"
        GIT_AUTHOR_EMAIL: "{{ git_email | default(omit) }}"
 
    - name: Git pull if snow-eda directory exists
      when: snow_eda_dir.stat.exists
      ansible.builtin.shell: "git config --global core.sshCommand 'ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i /tmp/id_rsa'; git -C {{ snow_eda_directory }} pull"
 
    - name: Remove existing VM entry block (reposition safeguard)
      blockinfile:
        path: "{{ terraform_variables_file }}"
        marker: "####{mark}-{{ vm_name }}"
        marker_begin: "begin"
        marker_end: "end"
        state: absent
 
    - name: Insert or update VM entry in terraform.tfvars
      when: remove_vm | bool == false
      blockinfile:
        path: "{{ terraform_variables_file }}"
        insertafter: '^\s*####start-vms-here\s*$'
        marker: "####{mark}-{{ vm_name }}"
        marker_begin: "begin"
        marker_end: "end"
        block: |
            {{ vm_name }} = {
              template  = "/{{ dc_name }}/vm/Templates/{{ os_type }}"
              network   = "{{ network | default('Greg') }}"
              cpu       = {{ cpus | int }}
              memory_mb = {{ ram_size | int }}
              disk_gb   = {{ hd_size | int }}
              notes     = "{{ request_name }}"
            }
 
    - name: Resolve vSphere credentials for Terraform
      no_log: true
      set_fact:
        tf_vsphere_server: "{{ vsphere_server | default(lookup('env', 'TF_VAR_vsphere_server'), true) | default(lookup('env', 'VSPHERE_SERVER'), true) }}"
        tf_vsphere_user: "{{ vsphere_user | default(lookup('env', 'TF_VAR_vsphere_user'), true) | default(lookup('env', 'VSPHERE_USER'), true) }}"
        tf_vsphere_password: "{{ vsphere_password | default(lookup('env', 'TF_VAR_vsphere_password'), true) | default(lookup('env', 'VSPHERE_PASSWORD'), true) }}"
 
    - name: Ensure required vSphere variables are provided
      assert:
        that:
          - tf_vsphere_server is defined
          - tf_vsphere_server | length > 0
          - tf_vsphere_user is defined
          - tf_vsphere_user | length > 0
          - tf_vsphere_password is defined
          - tf_vsphere_password | length > 0
        fail_msg: "Missing required vSphere credentials. Provide extra vars (vsphere_server, vsphere_user, vsphere_password) or environment vars (TF_VAR_vsphere_*, or VSPHERE_*)."
 
    - name: Initialize Terraform
      command:
        argv:
          - terraform
          - init
          - -no-color
          - -input=false
      args:
        chdir: "{{ terraform_directory }}"
      environment:
        TF_VAR_vsphere_server: "{{ tf_vsphere_server }}"
        TF_VAR_vsphere_user: "{{ tf_vsphere_user }}"
        TF_VAR_vsphere_password: "{{ tf_vsphere_password }}"
 
    - name: Plan Terraform deployment
      command:
        argv:
          - terraform
          - plan
          - -no-color
          - -input=false
          - -out=tfplan
      args:
        chdir: "{{ terraform_directory }}"
      environment:
        TF_VAR_vsphere_server: "{{ tf_vsphere_server }}"
        TF_VAR_vsphere_user: "{{ tf_vsphere_user }}"
        TF_VAR_vsphere_password: "{{ tf_vsphere_password }}"
      register: terraform_plan
      async: 1800
      poll: 5
 
    - name: Display Terraform plan output
      debug:
        var: terraform_plan.stdout
 
    - name: Apply Terraform deployment
      command:
        argv:
          - terraform
          - apply
          - -no-color
          - -input=false
          - -auto-approve
          - tfplan
      args:
        chdir: "{{ terraform_directory }}"
      environment:
        TF_VAR_vsphere_server: "{{ tf_vsphere_server }}"
        TF_VAR_vsphere_user: "{{ tf_vsphere_user }}"
        TF_VAR_vsphere_password: "{{ tf_vsphere_password }}"
      async: 1800
      poll: 5
 
    - name: push the repo back with no tags
      ansible.builtin.shell: "git add *; git commit -m '{{ vm_name }}-Add'; git config --global core.sshCommand 'ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i /tmp/id_rsa'; git push"
      args:
        chdir: "{{ terraform_directory }}"
      environment:
        GIT_COMMITTER_NAME: "{{ git_name | default(omit) }}"
        GIT_COMMITTER_EMAIL: "{{ git_email | default(omit) }}"
        GIT_AUTHOR_NAME: "{{ git_name | default(omit) }}"
        GIT_AUTHOR_EMAIL: "{{ git_email | default(omit) }}"
      # delegate_to: localhost
      changed_when: git_return.stderr != "Everything up-to-date"
      run_once: true
      # connection: local
      register: git_return
      # become: True
 
    - name: Remove temporary SSH key file "/tmp/id_rsa"
      ansible.builtin.file:
        path: /tmp/id_rsa
        state: absent
      run_once: true
      # delegate_to: localhost
      # become: true

For this quick and dirty demo I’m just connecting to a worker machine. If I spent a bit more time I could get it all working purely from an EE, but for the sake of getting it done quick I have a VM that does the work.

If you check the “vars” section you’ll notice I have all of the variables setup so they are ready to accept the variables passed over from EDA.

Have a look at this overview:

1. You can see that I start by cloning the repository that has all of my terraform files(including state files).
2. I then use the “blockinfile” module to drop in the new VM configuration in my tfvars file. I add a pre and post marker of “####ServerName-begin” and “####ServerName-end”. This makes it easy to review the file as well as have automation go back and remove any infrastructure that should be deprovisioned.
3. I then supply terraform with the init, plan, and last apply commands.
4. Then at the end it will push the file updates(including the state file) back to my git repository for safe keeping.

Keeping the files in the repo give me version control on everything too. An big upgrade to the process could be to use Terraform enterprise to do the provisioning, and in that case I would do very similar things, but instead of having to run the terraform commands directly I would be throwing API commands at TE.

Conclusion

I hope you could see just how easy the process becomes once you use EDA to tie everything together. Keep in mind that ANY tool you have in your environment can call AAP to perform the same process.

If you have any questions or comments, I’d love to hear them.

Good luck, and happy automating!

Leave a Comment

 

*