This post will show you how to utilize Ansible to manage your servers in Google Cloud Platform.
In this post we will:
- Install Ansible
- Configure access to GCP
- Create three instances
- Create a dynamic inventory
- Run a playbook against those instances
- Remove everything we created
Let’s go through these one by one.
Install Ansible
First you need to install Ansible on what is called the control machine. When you get started with Ansible this might be your laptop, or a server you share with your team. But as you progress it is recommended to use Ansible Automation Platform as a centralised place to run and manage all your automation.
To install Ansible on RHEL:
sudo dnf install ansible-core
To install Ansible on macOS, assuming you have Homebrew:
brew install ansible
For other operating systems see the documentation.
Install Ansible Collections
After installing Ansible we also need to install content collections which are needed to give Ansible the required integration with Google Cloud Platform.
ansible-galaxy collection install jugasit.gcp
This will install the collection gcp from the namespace jugasit. It contains roles for managing resources in GCP.
Connect to GCP
In order for Ansible to authenticate against GCP you need to create a Service Account. When doing so you will get a JSON file. In the examples below we assume it was saved as gcp_credentials.json in the working directory. If you saved it somewhere else, adjust the examples accordingly.
Create servers
Create a playbook named create_servers.yml with the following content:
- name: Create resources in GCP
hosts: localhost
connection: local
vars_files:
- vars/gcp.yml
roles:
- name: jugasit.gcp.addresses
tags: addresses
- name: jugasit.gcp.networks
tags: networks
- name: jugasit.gcp.firewalls
tags: firewalls
- name: jugasit.gcp.instances
tags: instances
Let’s break this down. This contains a single play called Create resources in GCP which we run against localhost. We load variables from a file called vars/gcp.yml (which we will create soon) and then we run a list of roles from the collection we just installed. These roles are tagged so we can customise our run and run only selected roles as we prefer. For example:
# do not create firewall rules
ansible-playbook create_servers.yml --skip-tags firewalls
# only create instances
ansible-playbook create_servers.yml --tags instances
# only create addresses and networks
ansible-playbook create_servers.yml --tags addresses,networks
Next step is to create the variables file. It is good practice to keep variables inside the folder vars. Now we have only a single file here but you may want to split your variables into several files to keep the files from growing too big. Having them in the same folder keeps things organised.
Create the file vars/gcp.yml with the following content:
# the project ID where resources are managed
gcp_project: playground-123456
# the default region and zone for placing resources
gcp_region: europe-north1
gcp_zone: europe-north1-a
# how to authenticate against gcp
gcp_auth_kind: serviceaccount
gcp_service_account_file: ./gcp_credentials.json
# for SSH we use the current user (via environment variable)
# and the public key in the users home directory.
gcp_ssh_user: "{{ lookup('env', 'USER') }}"
gcp_ssh_key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
# list of networks to create
gcp_networks:
- name: lab-net
description: Lab network for testing Ansible.
mtu: 1460
# list of external IP addresses to create
gcp_addresses:
- name: lab-address-001
- name: lab-address-002
- name: lab-address-003
# list of instances to create
gcp_instances:
- name: lab-instance-001
machine_type: e2-standard-4
labels:
aap_gateway:
aap_controller:
aap_eda:
disks:
- auto_delete: true
boot: true
type: PERSISTENT
initialize_params:
source_image: projects/rhel-cloud/global/images/rhel-9-v20250611
disk_size_gb: 50
network_interfaces:
- network:
selfLink: global/networks/lab-net
access_configs:
- name: External NAT
nat_ip:
selfLink: global/addresses/lab-address-001
type: ONE_TO_ONE_NAT
metadata:
ssh-keys: "{{ gcp_ssh_user }}:{{ gcp_ssh_key }}"
- name: lab-instance-002
machine_type: e2-standard-2
labels:
aap_database:
disks:
- auto_delete: true
boot: true
type: PERSISTENT
initialize_params:
source_image: projects/rhel-cloud/global/images/rhel-9-v20250611
disk_size_gb: 50
network_interfaces:
- network:
selfLink: global/networks/lab-net
access_configs:
- name: External NAT
nat_ip:
selfLink: global/addresses/lab-address-002
type: ONE_TO_ONE_NAT
metadata:
ssh-keys: "{{ gcp_ssh_user }}:{{ gcp_ssh_key }}"
- name: lab-instance-003
machine_type: e2-standard-2
labels:
aap_hub:
disks:
- auto_delete: true
boot: true
type: PERSISTENT
initialize_params:
source_image: projects/rhel-cloud/global/images/rhel-9-v20250611
disk_size_gb: 50
network_interfaces:
- network:
selfLink: global/networks/lab-net
access_configs:
- name: External NAT
nat_ip:
selfLink: global/addresses/lab-address-003
type: ONE_TO_ONE_NAT
metadata:
ssh-keys: "{{ gcp_ssh_user }}:{{ gcp_ssh_key }}"
# list of firewall rules to create
gcp_firewalls:
# allow ssh
- name: lab-firewall-001
network:
selfLink: global/networks/lab-net
source_ranges: ["0.0.0.0/0"]
allowed:
- ip_protocol: icmp
- ip_protocol: tcp
ports:
- 22
There a lot going on here. This is a definition of everything we want to create in GCP, and also some configuration on how to create it.
The variables define three instances using three external IPs, and they are all placed on the same network which we also create here. Lastly we create a firewall rule for the network allowing SSH and ping from anywhere to our servers.
As you noticed the playbook is fairly simple and the complexity lies in our variables. This is a good practice when using Ansible at scale as it lets you think of your environment as a state, instead of the result of a bunch of operations.
Besides, these variable are defined in a YAML file in this example, but they could come from other sources such as a CMDB.
Now let’s run this playbook:
$ ansible-playbook create_servers.yml PLAY [Create resources in GCP] ************************************************* TASK [Gathering Facts] ********************************************************* ok: [localhost] TASK [jugasit.gcp.addresses : Configure GCP addresses] ************************* changed: [localhost] => (item=lab-address-001) changed: [localhost] => (item=lab-address-002) changed: [localhost] => (item=lab-address-003) TASK [jugasit.gcp.networks : Configure GCP networks] *************************** changed: [localhost] => (item=lab-net) TASK [jugasit.gcp.firewalls : Configure GCP firewalls] ************************* changed: [localhost] => (item=lab-firewall-001) TASK [jugasit.gcp.instances : Configure GCP instances] ************************* changed: [localhost] => (item=lab-instance-001) changed: [localhost] => (item=lab-instance-002) changed: [localhost] => (item=lab-instance-003) TASK [jugasit.gcp.instances : Wait for GCP instance] *************************** ok: [localhost] => (item=lab-instance-001) ok: [localhost] => (item=lab-instance-002) ok: [localhost] => (item=lab-instance-003) PLAY RECAP ********************************************************************* localhost : ok=6 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
After this we have three servers which we can do something with.
Managing the servers
Creating servers in GCP is neat, but even better would be if we could create a new playbook to run against these servers. Let’s create one called update_servers.yml with the following content:
- name: Update servers
hosts: all
become: true
vars:
ansible_ssh_common_args: '-o StrictHostKeyChecking=no'
tasks:
- name: Update all packages
ansible.builtin.package:
name: '*'
state: latest
Now, to run this we need an inventory with the servers. Instead of creating one manually, let’s use the dynamic inventory plugin for GCP. Create an inventory file called inventory.gcp.yml with the following content:
plugin: google.cloud.gcp_compute
projects:
- playground-123456
auth_kind: serviceaccount
service_account_file: ./gcp_credentials.json
groups:
automationgateway: "'aap_gateway' in labels"
automationcontroller: "'aap_controller' in labels"
automationeda: "'aap_eda' in labels"
automationhub: "'aap_hub' in labels"
database: "'aap_database' in labels"
This tells Ansible to use the gcp_compute inventory plug from the google.cloud collection (installed as a dependency when we installed jugasit.gcp earlier).
We tell Ansible which project to fetch servers from and how to authenticate.
Lastly we tell Ansible to put the servers into groups if they have certain labels.
To test the inventory run the following command:
$ ansible-inventory -i inventory.gcp.yml --graph
@all:
|--@ungrouped:
|--@automationgateway:
| |--34.88.55.195
|--@automationcontroller:
| |--34.88.55.195
|--@automationeda:
| |--34.88.55.195
|--@database:
| |--35.228.226.185
|--@automationhub:
| |--34.88.46.132
Everything looks good, so let’s run our playbook:
ansible-playbook update_servers.yml -i inventory.gcp.yml PLAY [Update servers] ********************************************************** TASK [Gathering Facts] ********************************************************* ok: [34.88.46.132] ok: [35.228.226.185] ok: [34.88.55.195] TASK [Update all packages] ***************************************************** ok: [34.88.55.195] ok: [35.228.226.185] ok: [34.88.46.132] PLAY RECAP ********************************************************************* 34.88.46.132 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 34.88.55.195 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 35.228.226.185 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Cleaning up
Being able to create servers is fine, but we should also remove stuff. Especially in a lab like this as we don’t want any resources to linger and cost money.
Create a new playbook called remove_servers.yml with the following content:
- name: Remove resources in GCP
hosts: localhost
connection: local
vars_files:
- vars/gcp.yml
vars:
gcp_state: absent
roles:
- jugasit.gcp.instances
- jugasit.gcp.addresses
- jugasit.gcp.firewalls
- jugasit.gcp.networks
This looks almost identical to the playbook for creating stuff but there are two differences:
- The roles are in a different order
- We tell the roles that all resources should be absent instead of present.
Let’s run this playbook:
$ ansible-playbook remove_servers.yml PLAY [Remove resources in GCP] ************************************************* TASK [Gathering Facts] ********************************************************* ok: [localhost] TASK [jugasit.gcp.instances : Configure GCP instances] ************************* changed: [localhost] => (item=lab-instance-001) changed: [localhost] => (item=lab-instance-002) changed: [localhost] => (item=lab-instance-003) TASK [jugasit.gcp.instances : Wait for GCP instance] *************************** skipping: [localhost] => (item=lab-instance-001) skipping: [localhost] => (item=lab-instance-002) skipping: [localhost] => (item=lab-instance-003) skipping: [localhost] TASK [jugasit.gcp.addresses : Configure GCP addresses] ************************* changed: [localhost] => (item=lab-address-001) changed: [localhost] => (item=lab-address-002) changed: [localhost] => (item=lab-address-003) TASK [jugasit.gcp.firewalls : Configure GCP firewalls] ************************* changed: [localhost] => (item=lab-firewall-001) TASK [jugasit.gcp.networks : Configure GCP networks] *************************** changed: [localhost] => (item=lab-net) PLAY RECAP ********************************************************************* localhost : ok=5 changed=4 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
Conclusion
That wraps up this guide. I hope you learned something from it and is eager to expand on this to automate even more stuff in Google Cloud Platform.
If you need any help with your automation endeavours do not hesitate to contact me.



Leave a Reply