How-to create dynamic inventory in GCP using Ansible

Rangaswamy P V
DevOps Dudes
Published in
9 min readJul 17, 2020

This article details the steps needed to setup the “gce.py” and also the configuration changes needed to create a GCP instance using Ansible . “gce.py” is the python module which talks to the GCP cloud to gather instance details . In order to use “gce.py” we need to create a credentials in GCP console as a P12 format and make changes to the “gce.ini” and “secrets.py” files to enable this communication . The newer Ansible module uses the JSON format of the API credentials to enable the communication with GCP cloud . We will go over both the steps in detail .

ToDo:

There are two ways the credentials can be saved , one as a “JSON” file and other as a “P12” file . We will create it in both of the formats , the JSON format is used by Ansible modules in creating GCP instance and all GCP related operations . The P12 format needs some modification before it can be used by “gce.py” file to fetch the GCP instance details .

A high level task list to accomplish the above is listed below…
1. Create json — this is the file that is created in the GCP console , by choosing the API/Credentials options and choosing the JSON option to Add keys to the service account .
2. Create P12 file ( same as above) — Just that at the time of Add Key option in Credentials , choose P12 to save
3. Convert P12 to *.pem file (show below)
4. Download the “gce.py” and “gce.ini” from the Internet/github repo
5. Make changes to “gce.ini” with your own GOOGLE_CLOUD_CREDENTAILS
6. Create “secrets.py” file . Standard format given below
7. In the main playbook “gcp.yaml” introduce the *.json file in the “service_account_file:” parameter to create instances in GCP .

Ansible & Python Requirements:

# Install the following packages in python using pip
pip install apache-libcloud
pip install google-auth
If you are using Python 2.7 then it is already packaged with cryptography.
Otherwise if you are using Python 3 then make sure you install these cryptography packages
Requirement: cryptography>=1.5'), {'paramiko'})$ pip install cryptography==1.5.1
$ pip install category_encoders

WorkFlow:

Step 1 & 2: Go to the Google Cloud Platform Console, Choose the API & Services on the left Panel . Once you have clicked this , choose Credentials as the sub-option under API . Once you are here , you will see three option on the right panel ; API keys, OAuth2 and Service Accounts.

GCP API Credentials

Click on the service account for compute. You will get a refreshed window . Here on the right panel there is a Keys section , with a “Add key” button . Click it! and a pop-up will appear to create the new keys with the option to generate it in JSON or P12 format .

API Service Account WIndow

First select the JSON option for the Ansible modules to talk to GCP . Transfer this JSON file to the Ansible box and we will use it later in the Playbook . And then again repeat the steps to select the P12 option , it will download the file to the browser with “notasecret” password .
Transfer the file to the Unix instance where ansible is installed and use the following command to convert it to a “ .pem” format to be applied in the “secrets.py” file

Step 3: To Convert the P12 file into a *.pem file

$ openssl pkcs12 -in pkey.pkcs12 -passin pass:notasecret -nodes -nocerts | openssl rsa -out pkey.pem

The output is stored as “pkey.pem” file

Step 4: Download the “gce.py” and “gce.ini” file from the Internet or from the following repo

$ git pull https://github.com/rangapv/Ansible.git
awsboto confighost createaws gcpgce readme
$ cd gcpgce/; ls
gce.ini gce.py secrets.py

The file “gce.py” can be used as is . The other two files need modification

Before Modification:

$ vi gce.ini
# This option will be deprecated in a future release.
libcloud_secrets = /Path to secrets.py
# If you are not going to use a 'secrets.py' file, you can set the necessary
# authorization parameters here.
gce_service_account_email_address = GOOGLE_CREDENTIALS_HERE@developer.gserviceaccount.com
gce_service_account_pem_file_path = /Path to pkey.pem
gce_project_id = GOOGLE_CREDENTIALS_PROJECT_ID
gce_zone = GOOGLE_CLOUD_ZONE
$ vi secrets.py
#Replace the GOOGLE_SERVICE_ACCOUNT with the correct service account email ID & Replace the Path to pkey.pem key below
GCE_PARAMS = ('GOOGLE_SERVICE_ACCOUNT','/Path to pkey.pem')
#Replace the GOOGLE_PROJECT_ID with the correct GCP Project ID, that's it!
GCE_KEYWORD_PARAMS = {'project': 'GOOGLE_PROJECT_ID','datacenter':''}

Step 5:

Make modifications to the “gce.ini” file and “secrets.py” file from the above downloads to match the GCP credentials of your particular Project .

After modification the files will look similar to this…

$ vi gce.ini
[gce]
# GCE Service Account configuration information can be stored in the
# libcloud 'secrets.py' file. Ideally, the 'secrets.py' file will already
# exist in your PYTHONPATH and be picked up automatically with an import
# statement in the inventory script. However, you can specify an absolute
# path to the secrets.py file with 'libcloud_secrets' parameter.
# This option will be deprecated in a future release.
libcloud_secrets = secrets.py
# If you are not going to use a 'secrets.py' file, you can set the necessary
# authorization parameters here.
gce_service_account_email_address = 674935188358-compute@developer.gserviceaccount.com
gce_service_account_pem_file_path = pkey.pem
gce_project_id = modified-ripsaw-165414
gce_zone = us-central1-c
# Filter inventory based on on state. Leave undefined to return instances regardless of state.
# example: Uncomment to only return inventory in the running or provisioning state
#instance_states = RUNNING,PROVISIONING

Step 6: Create a “secret.py” :

$ vi secrets.py

create it manually, format given below….

#Replace the GOOGLE_SERVICE_ACCOUNT with the correct service account email ID & Replace the Path to pkey.pem key below
GCE_PARAMS = ('GOOGLE_SERVICE_ACCOUNT','/Path to pkey.pem')
#Replace the GOOGLE_PROJECT_ID with the correct GCP Project ID, that's it!
GCE_KEYWORD_PARAMS = {'project': 'GOOGLE_PROJECT_ID','datacenter':''}

After modification it looks similar to

# Remember to Build
GCE_PARAMS = ('674935188358-compute@developer.gserviceaccount.com','pkey.pem')
#Replace the GOOGLE_PROJECT_ID with the correct GCP Project ID, that's it!
GCE_KEYWORD_PARAMS = {'project': 'modified-ripsaw-165414','datacenter':''}

To run the setup: Now we have all the required files and in the format needed to talk to the GCP cloud . Lets check to see if our configuration is working fine

$ gce.py --list --pretty{
"10.128.0.10": [
"instance-21"
],
"10.128.0.11": [
"instance-21-node"
],
"10.128.0.14": [
"kubeadm22"
],
"10.128.0.15": [
"kubenode22"
],
"10.128.0.16": [
"kubeadm24"
],
"10.128.0.22": [
"instance-18-kubefed"
],
"10.128.0.23": [
"instance-19"
],
"10.128.0.3": [
"instance-2"
],
"10.244.0.2": [
"kubeadm23"
],
"10.244.0.3": [
"kubenode23"
],
"10.244.0.4": [
"kubeadm24"
],
"35.225.90.120": [
"instance-34-ansible"
],
"_meta": {
"hostvars": {
"agent0001": {
"ansible_ssh_host": null,
.
.
.
],
"status_running": [
"instance-34-ansible"
],
"status_terminated": [
"instance-21",
"instance-21-node",
"instance-18-kubefed",
"instance-19",
"instance-2",
"kubenode32",
"master0"
],
"tag_http-server": [
"instance-21",
"instance-21-node",
"kubeadm20",
"instance-19",
"instance-2",
"instance-34-ansible",
"master0"
],
"tag_https-server": [
"instance-21",
"instance-21-node",
"kubeadm20",
"kubeadm22",
"instance-18-kubefed",
"master0"
],
"tag_master": [
"master0"
],
"tag_privateagent": [
"agent0001",
"agent0002"
],
"ubuntu-1604-xenial-v20171121a": [
"instance-18-kubefed",
"instance-19"
],
"ubuntu-1604-xenial-v20180214": [
"instance-34-ansible"
],
"ubuntu-1604-xenial-v20200529": [
"kubeadm20",
"kubenode20"
],
"ubuntu-1604-xenial-v20200611": [
"instance-21",
"instance-21-node",
"kubeadm22",
"kubeadm23",
"kubenode23",
"kubenode25"
],
"ubuntu-1604-xenial-v20200713a": [
"kubenode32"
],
"ubuntu-1610-yakkety-v20170330": [
"instance-1"
],
"us-central1-a": [
"instance-21",
"instance-21-node",
"kubeadm20",
"kubenode22",
"kubenode23",
"kubenode25"
],
"us-central1-c": [
"agent0001",
"agent0002",
"instance-1",
"instance-18-kubefed",
"instance-34-ansible",
"kubenode32",
"master0"
]
}

We got the list of instances in a particular GCP account as shown above . Here we used the Python module to talk directly to GCP and did not make use of Ansible .

Step 7: Now we will use the key which is in the JSON format to create the GCP instance . The newer Ansible modules only require the JSON credentials file to create the GCP instance . {step 1 in the WorkFlow above}.

Note:Do not modify the downloaded json file, use it as-is by ftp-ing it to the Ansible box which runs the following playbook and mention the path of the json file in the “service -account-file” parameter in the playbook

The variables are defined in the global directory for Ansible playbook…

git link for the Playbook is in “creategcp” folder in the repo: https://github.com/rangapv/Ansible.git /

$ vi /etc/ansible/group_vars/all/vars_gcp.ymlproject_id: modified-ripsaw-165414
machine_type: n1-standard-1
image: ubuntu-1604
zone: us-central1-c
auth_kind: serviceaccount
ansible_user: rangapv08

In ansible.cfg make sure you have the following parameters

# uncomment this to disable SSH key host checking
host_key_checking = False
remote_user = rangapv08

The Ansible Playbook YAML definition is as show below

- name: Create instance(s)
hosts: localhost
connection: local
gather_facts: no
tasks: - name: Launch instances
gcp_compute_instance:
name: kubenode36
auth_kind: serviceaccount
machine_type: "{{ machine_type }}"
disks:
- auto_delete: 'true'
boot: 'true'
initialize_params:
disk_size_gb: 10
source_image: projects/ubuntu-os-cloud/global/images/ubuntu-1604-xenial-v20200713a
# service_account_email: "{{ service_account_email }}"
network_interfaces:
- access_configs:
- name: External NAT
type: ONE_TO_ONE_NAT
service_account_file: new22.json
project: "{{ project_id }}"
state: present
status: RUNNING
zone: "{{ zone }}"
scopes:
- https://www.googleapis.com/auth/compute
- https://www.googleapis.com/auth/cloud-platform
- https://www.googleapis.com/auth/devstorage.full_control
- https://www.googleapis.com/auth/source.full_control
- https://www.googleapis.com/auth/servicecontrol
register: gce

The above playbook is a bare minimum definitions needed to create a GCP instance.

The output..

$ ansible-playbook ./gcp.yaml -vvvv
ansible-playbook 2.9.7
config file = /etc/ansible/ansible.cfg
configured module search path = ['/home/rangapv08/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/local/lib/python3.5/dist-packages/ansible
executable location = /usr/local/bin/ansible-playbook
python version = 3.5.2 (default, Apr 16 2020, 17:47:17) [GCC 5.4.0 20160609]
Using /etc/ansible/ansible.cfg as config file
setting up inventory plugins
Parsed /etc/ansible/hosts inventory source with ini plugin
[WARNING]: Found both group and host with same name: l1
Loading callback plugin default of type stdout, v2.0 from /usr/local/lib/python3.5/dist-packages/ansible/plugins/callback/default.py
PLAYBOOK: gcp.yaml *************************************************************************************************
Positional arguments: ./gcp.yaml
tags: ('all',)
connection: smart
inventory: ('/etc/ansible/hosts',)
remote_user: rangapv07
become_method: sudo
verbosity: 4
forks: 5
timeout: 10
1 plays in ./gcp.yaml
PLAY [Create instance(s)] ******************************************************************************************
META: ran handlers
TASK [Launch instances] ********************************************************************************************
task path: /home/rangapv08/myansible/tr2/gcpgce/gcp.yaml:10
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: rangapv08
TASK [Launch instances] ********************************************************************************************
task path: /home/rangapv08/myansible/tr2/gcpgce/gcp.yaml:10
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: rangapv08
<127.0.0.1> EXEC /bin/sh -c 'echo ~rangapv08 && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /home/rangapv08/.ansible/tmp `"&& mkdir /home/rangapv08/.ansible/tmp/ansible-tmp-1594974996.8102102-6895-137003715637784/ > /dev/null 2>&1 && sleep 0'
changed: [localhost] => {
"changed": true,
"cpuPlatform": "Intel Haswell",
"creationTimestamp": "2020-07-17T01:36:39.518-07:00",
"deletionProtection": false,
"disks": [
{
"autoDelete": true,
"boot": true,
"deviceName": "persistent-disk-0",
"diskSizeGb": "10",
"guestOsFeatures": [
{
"type": "UEFI_COMPATIBLE"
},
{
"type": "SEV_CAPABLE"
},
{
"type": "VIRTIO_SCSI_MULTIQUEUE"
}
],
"index": 0,
"interface": "SCSI",
"kind": "compute#attachedDisk",
"licenses": [
"https://www.googleapis.com/compute/v1/projects/ubuntu-os-cloud/global/licenses/ubuntu-1604-xenial"
],
"mode": "READ_WRITE",
"source": "https://www.googleapis.com/compute/v1/projects/modified-ripsaw-165414/zones/us-central1-c/disks/kubenode362",
"type": "PERSISTENT"
}
],
"fingerprint": "xAmGxxq0eDc=",
"id": "5091479016263441913",
"invocation": {
"module_args": {
"auth_kind": "serviceaccount",
"can_ip_forward": null,
"deletion_protection": null,
"disks": [
{
"auto_delete": true,
"boot": true,
"device_name": null,
"disk_encryption_key": null,
"index": null,
"initialize_params": {
"disk_name": null,
"disk_size_gb": 10,
"disk_type": null,
"source_image": "projects/ubuntu-os-cloud/global/images/ubuntu-1604-xenial-v20200713a",
"source_image_encryption_key": null
},
"interface": null,
"mode": null,
"source": null,
"type": null
}
],
"env_type": null,
"guest_accelerators": null,
"hostname": null,
"labels": null,
"machine_type": "n1-standard-1",
"metadata": null,
"min_cpu_platform": null,
"name": "kubenode362",
"network_interfaces": [
{
"access_configs": [
{
"name": "External NAT",
"nat_ip": null,
"type": "ONE_TO_ONE_NAT"
}
],
"alias_ip_ranges": null,
"network": null,
"network_ip": null,
"subnetwork": null
}
],
"project": "modified-ripsaw-165414",
"scheduling": null,
"scopes": [
"https://www.googleapis.com/auth/compute",
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/devstorage.full_control",
"https://www.googleapis.com/auth/source.full_control",
"https://www.googleapis.com/auth/servicecontrol"
],
"service_account_contents": null,
"service_account_email": null,
"service_account_file": "new22.json",
"service_accounts": null,
"shielded_instance_config": null,
"state": "present",
"status": "RUNNING",
"tags": null,
"zone": "us-central1-c"
}
},
"kind": "compute#instance",
"labelFingerprint": "42WmSpB8rSM=",
"machineType": "https://www.googleapis.com/compute/v1/projects/modified-ripsaw-165414/zones/us-central1-c/machineTypes/n1-standard-1",
"metadata": {},
"name": "kubenode362",
"networkInterfaces": [
{
"accessConfigs": [
{
"kind": "compute#accessConfig",
"name": "External NAT",
{
"kind": "compute#accessConfig",
"name": "External NAT",
"natIP": "34.66.14.11",
"networkTier": "PREMIUM",
"type": "ONE_TO_ONE_NAT"
}
],
"fingerprint": "UqgXp_CaK5c=",
"kind": "compute#networkInterface",
"name": "nic0",
"network": "https://www.googleapis.com/compute/v1/projects/modified-ripsaw-165414/global/networks/default",
"networkIP": "10.128.0.25",
"subnetwork": "https://www.googleapis.com/compute/v1/projects/modified-ripsaw-165414/regions/us-central1/subnetworks/default"
}
],
"scheduling": {
"automaticRestart": true,
"onHostMaintenance": "MIGRATE",
"preemptible": false
},
"selfLink": "https://www.googleapis.com/compute/v1/projects/modified-ripsaw-165414/zones/us-central1-c/instances/kubenode362",
"shieldedInstanceConfig": {
"enableIntegrityMonitoring": true,
"enableSecureBoot": false,
"enableVtpm": true
},
"shieldedInstanceIntegrityPolicy": {
"updateAutoLearnPolicy": true
},
"startRestricted": false,
"status": "RUNNING",
"tags": {
"fingerprint": "42WmSpB8rSM="
},
"zone": "https://www.googleapis.com/compute/v1/projects/modified-ripsaw-165414/zones/us-central1-c"
}
META: ran handlers
META: ran handlers
PLAY RECAP *********************************************************************************************************
localhost : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

The above playbook was tested on Ubuntu 16.04, Ansible 2.9 and Python 3 versions

--

--

Rangaswamy P V
DevOps Dudes

Works on Devops in Startups, reach me @rangapv on X/twitter or email: rangapv@gmail.com