Setting Up E-commerce on Google Compute Engine: A Guide to Automation with Terraform and Ansible


In our last blog, we walked you through setting up a virtual machine in Google Cloud with Terraform. Now, let's take it a step further! In this article, we'll show you how to automate the installation of a web server and a database on that VM using Ansible.
We'll use Cloud Shell to manage our VM with Ansible. First, install Ansible in the Cloud Shell environment using pip
, as it's simple and efficient. If you're curious about the operating system, run cat /etc/os-release
. But for installing Ansible, just proceed with pip
.
Once Ansible is installed, set up secure communication between your Cloud Shell (Ansible control node) and your Google Compute Engine VM (target host) by creating an SSH key pair. Run this command in Cloud Shell:
ssh-keygen -t rsa -f ~/.ssh/KEY_FILENAME -C USERNAME
Replace KEY_FILENAME
with your desired key file name and USERNAME
with your SSH access username for the VM.
For example, ssh-keygen -t rsa -f ~/.ssh/centos-ssh-key -C centuser
creates:
A private key file named
centos-ssh-key
A public key file named
centos-ssh-key.pub
Important: Keep the private key safe. In Cloud Shell, it's secure in your home directory, but always handle private keys with care. The public key will be added to your VM for Ansible authentication.
Great! With your SSH key pair ready, link the public key to your Compute Engine VM for Ansible access via SSH.
Go to Compute Engine > VM instances in Google Cloud Console.
Click your VM instance's name for details.
Click "EDIT" at the top.
Scroll to "SSH Keys".
Click "Add item".
Paste the full content of your public key file (
.pub
) into the box. Include everything fromssh-rsa
toUSERNAME
.- View your public key in Cloud Shell:
cat ~/.ssh/centos-ssh-key.pub
- View your public key in Cloud Shell:
Click "SAVE" to update your VM.
Your VM is now ready for SSH connections from Cloud Shell using your key pair.
To work with our virtual machine using Ansible, you'll need two important files: an inventory
file to list our VM and its connection details, and a configuration
file to set up how Ansible will behave and manage the interaction.
Continuing from our earlier chat about the two key files needed for Ansible, here's an example of what part of your Ansible inventory file might look like:
[servers]
192.10.10.1 ansible_user=centuser ansible_ssh_private_key_file=~/.ssh/centos-ssh-key
This snippet sets up a host under the [servers]
group. Here's what it means:
[servers]
: A group header in Ansible. It lets you organize hosts into groups, so you can run commands or playbooks on specific groups instead of individually. This makes automation scalable and easier to manage. For example, you might have groups like[web_servers]
,[database_servers]
, or[development_environments]
.192.10.10.1
: The IP address of your VM for Ansible to connect to. A hostname can also be used if resolvable in your network.ansible_user=centuser
: This variable tells Ansible to usecentuser
for the SSH connection to192.10.10.1
, instead of the default username.ansible_ssh_private_key_file=~/.ssh/centos-ssh-key
: This tells Ansible where to find the SSH private key for connecting to the VM securely. Ansible uses the key at~/.ssh/centos-ssh-key
on your control machine to connect to192.10.10.1
.
These entries in your inventory file provide Ansible with the address, login user, and SSH key needed to connect to your VM. This setup is crucial for automation tasks with Ansible.
Understanding the E-commerce Playbook:
Database Configuration
Now that we've covered our Ansible inventory file, let's explore the core of our automation: the Ansible playbook. We'll use a configuration file named ecom.yaml
, which details the steps Ansible will take to deploy our e-commerce application.
- name: Deploy E-commerce Application
hosts: all
become: yes
vars:
db_name: ecomdb
db_user: ecomuser
db_password: "ecompassword"
db_host: localhost
project_root: /var/www/html
repo_url: https://github.com/kodekloudhub/learning-app-ecommerce.git
tasks:
#--------------------------------------------------
# MariaDB Setup
#--------------------------------------------------
- name: Install MariaDB server
ansible.builtin.yum:
name: mariadb-server
state: present
- name: Ensure MariaDB configuration directory exists
ansible.builtin.file:
path: /etc/my.cnf.d
state: directory
mode: '0755'
- name: Start and enable MariaDB service
ansible.builtin.systemd:
name: mariadb
state: started
enabled: yes
Let's break down this playbook:
- name: Deploy E-commerce Application
: This is the name of our playbook, describing its purpose.hosts: all
: This specifies that the playbook will run on all hosts in our inventory.become: yes
: This tells Ansible to use elevated privileges for tasks that need root permissions.vars:
: This section sets up variables for use throughout the playbook, making it more reusable and easier to maintain.db_name: ecomdb
,db_user: ecomuser
,db_password: "ecompassword"
: These variables define the MariaDB database credentials. Use Ansible Vault to encrypt sensitive data like passwords.db_host:
localhost
: Indicates the database runs on the same server as the application.project_root: /var/www/html
: Specifies where the e-commerce application files will be stored.repo_url:
https://github.com/kodekloudhub/learning-app-ecommerce.git
: Contains the URL of the Git repository with the application's source code.
tasks:
: This section outlines the automation steps. Tasks run sequentially on target hosts, grouped for clarity.- name: Install MariaDB server
ansible.builtin.yum:
name: mariadb-server
state: present
: Ensures themariadb-server
package is installed and updated using theansible.builtin.yum
module.- name: Ensure MariaDB configuration directory exists
ansible.builtin.file:
path: /etc/my.cnf.d
state: directory
mode: '0755'
: Ensures the/etc/my.cnf.d
directory exists with correct permissions using theansible.builtin.file
module.- name: Start and enable MariaDB service
ansible.builtin.systemd:
name: mariadb
state: started
enabled: yes
: Manages themariadb
service to ensure it is running and set to start on boot using theansible.builtin.systemd
module.
- name: Ensure python3-pip is installed on CentOS 9
ansible.builtin.yum:
name: python3-pip
state: present
update_cache: yes
when:
- ansible_os_family == "RedHat"
- ansible_distribution_major_version == "9"
- name: Ensure PyMySQL is installed for Python 3
ansible.builtin.pip:
name: PyMySQL
executable: pip3
when: ansible_python_version.startswith('3.')
- name: Ensure python3-pip is installed on CentOS 9
ansible.builtin.yum:
: Uses theyum
module for package management on RedHat-based systems.name: python3-pip
: Focuses on installing thepython3-pip
package.state: present
: Ensurespython3-pip
is installed.update_cache: yes
: Refreshes the local package cache for the latest package info.when:
: Runs the task only under specific conditions.- ansible_os_family == "RedHat"
: Checks if the OS is part of the "RedHat" family.- ansible_distribution_major_version == "9"
: Ensures the task runs only on version 9 systems.
- name: Ensure PyMySQL is installed for Python 3
ansible.builtin.pip:
: Uses thepip
module to manage Python packages.name: PyMySQL
: Installs thePyMySQL
library, a Python MySQL client for connecting to MySQL/MariaDB databases.executable: pip3
: Ensures installation for Python 3 by usingpip3
.when: ansible_python_version.startswith('3.')
: InstallsPyMySQL
only if Python 3 is the default version on the host.
After installing MariaDB and Python dependencies, our ecom.yaml
playbook sets up the e-commerce database by creating it, adding a user, and populating it with initial data. This prepares the database backend for our application.
- name: Create the application database
community.mysql.mysql_db:
name: "{{ db_name }}"
state: present
login_unix_socket: /var/lib/mysql/mysql.sock
- name: Create the application database user
community.mysql.mysql_user:
name: "{{ db_user }}"
host: "{{ db_host }}" # Typically 'localhost' for local DB access
password: "{{ db_password }}"
priv: "{{ db_name }}.*:ALL" # Grant privileges only on the application's database
state: present
login_unix_socket: /var/lib/mysql/mysql.sock # Must match the socket MariaDB is using
- name: Create database load script
ansible.builtin.copy:
dest: "/tmp/db-load-script.sql"
content: |
USE {{ db_name }};
CREATE TABLE IF NOT EXISTS products (
id mediumint(8) unsigned NOT NULL auto_increment,
Name varchar(255) default NULL,
Price varchar(255) default NULL,
ImageUrl varchar(255) default NULL,
PRIMARY KEY (id)
) AUTO_INCREMENT=1;
INSERT INTO products (Name,Price,ImageUrl) VALUES
("Laptop","100","c-1.png"),
("Drone","200","c-2.png"),
("VR","300","c-3.png"),
("Tablet","50","c-5.png"),
("Watch","90","c-6.png"),
("Phone Covers","20","c-7.png"),
("Phone","80","c-8.png"),
("Laptop","150","c-4.png")
ON DUPLICATE KEY UPDATE Name=VALUES(Name), Price=VALUES(Price), ImageUrl=VALUES(ImageUrl);
mode: '0644'
- name: Load data into the database
community.mysql.mysql_db:
name: "{{ db_name }}"
state: import
target: "/tmp/db-load-script.sql"
login_unix_socket: /var/lib/mysql/mysql.sock
- name: Create the application database
community.mysql.mysql_db:
: Uses themysql_db
module fromcommunity.mysql
for managing MySQL/MariaDB databases.name: "{{ db_name }}"
: Sets the database name using thedb_name
variable from the playbook'svars
section.state: present
: Ensures the database exists; creates it if not, maintaining idempotency.login_unix_socket: /var/lib/mysql/mysql.sock
: Connects to the MariaDB server using a local Unix domain socket for security and efficiency.
-name: Create the application database user
community.mysql.mysql_user:
: Manages MySQL/MariaDB users using themysql_user
module.name: "{{ db_user }}"
: Sets the username with thedb_user
variable.host: "{{ db_host }}"
: Limits user connections tolocalhost
for security.password: "{{ db_password }}"
: Sets the user's password fromdb_password
. (Use Ansible Vault for security in production!)priv: "{{ db_name }}.*:ALL"
: GrantsALL
privileges only on{{ db_name }}
tables, following the least privilege principle.state: present
: Ensures the user exists with the specified privileges.login_unix_socket: /var/lib/mysql/mysql.sock
: Uses the Unix socket for authentication.
- name: Create database load script
ansible.builtin.copy:
: Uses thecopy
module to create a file on the remote server.dest: "/tmp/db-load-script.sql"
: Saves the temporary SQL script here.content: |
: Allows multi-line content in the playbook. Writes SQL commands to create theproducts
table and add initial data.USE {{ db_name }};
: Selects the database.CREATE TABLE IF NOT EXISTS products (...)
: Sets up theproducts
table. Ensures it isn't recreated if it exists.INSERT INTO products (Name, Price, ImageUrl) VALUES (...) ON DUPLICATE KEY UPDATE ...
: Adds sample data. Updates existing records if a product with the same primary key exists.
mode: '0644'
: Sets file permissions for the SQL script.
- name: Load data into the database
community.mysql.mysql_db:
: Uses themysql_db
module with a differentstate
.name: "{{ db_name }}"
: Specifies the database for data loading.state: import
: Instructs to import an SQL file into the specified database.target: "/tmp/db-load-script.sql"
: Points to the SQL script created earlier.login_unix_socket: /var/lib/mysql/mysql.sock
: Maintains a consistent connection with the Unix socket.
Apache and PHP Setup
After installing MariaDB and Python dependencies, our ecom.yaml
playbook sets up the e-commerce database. It creates the database, sets up a user, and adds initial data, preparing the backend for our application.
# Apache and PHP Setup
#--------------------------------------------------
- name: Install Apache (httpd), PHP, and PHP MySQL connector
ansible.builtin.yum:
name:
- httpd
- php
- php-mysqlnd # Provides mysql, mysqli, pdo_mysql extensions
state: present
- name: Configure Apache to serve index.php by default
ansible.builtin.lineinfile:
path: /etc/httpd/conf/httpd.conf
regexp: '^(DirectoryIndex\s+)(.*)$'
line: '\1index.php \2' # Adds index.php to the list of DirectoryIndex files
backrefs: yes
notify: Restart httpd
- name: Start and enable Apache (httpd) service
ansible.builtin.systemd:
name: httpd
state: started
enabled: yes
- name: Install Apache (httpd), PHP, and PHP MySQL connector
ansible.builtin.yum:
: Uses theyum
module for package management.name:
: Lists packages to install.- httpd
: Apache HTTP Server package.- php
: Main PHP interpreter.- php-mysqlnd
: Native driver for MySQL/MariaDB in PHP, includes necessary extensions for database connection.
state: present
: Ensures packages are installed and updated.- name: Configure Apache to serve index.php by default
ansible.builtin.lineinfile:
: Ensures a specific line is added, removed, or replaced in a file.path: /etc/httpd/conf/httpd.conf
: Points to the main Apache config file.regexp: '^(DirectoryIndex\s+)(.*)$'
: Searches for lines starting withDirectoryIndex
and captures the rest.line: '\1index.php \2'
: Updates the line to includeindex.php
as the default file.backrefs: yes
: Allows using backreferences (\1
,\2
, etc.) in theline
parameter to refer to parts of the matchedregexp
.notify: Restart httpd
: Ansible "handlers" feature. Ifhttpd.conf
changes, it "notifies" theRestart httpd
handler, which runs once at the end of a playbook, ensuring efficient service restarts only when needed.
- name: Start and enable Apache (httpd) service
ansible.builtin.systemd:
: Uses thesystemd
module to manage the Apache service.name: httpd
: Specifies the service to manage.state: started
: Ensures thehttpd
service is running.enabled: yes
: Ensures thehttpd
service starts on boot.
Application Deployment and Final Touches
With MariaDB and the Apache/PHP environment set up, the final step in our ecom.yaml
playbook is deploying the e-commerce application code and ensuring everything runs smoothly. This completes our infrastructure setup, bringing the application to life.
# Application Deployment
#--------------------------------------------------
- name: Install Git
ansible.builtin.yum:
name: git
state: present
- name: Clone the application repository
ansible.builtin.git:
repo: "{{ repo_url }}"
dest: "{{ project_root }}"
version: master
force: yes # Overwrites local changes if any
- name: Create .env file with database credentials
ansible.builtin.copy:
dest: "{{ project_root }}/.env"
content: |
DB_HOST={{ db_host }}
DB_USER={{ db_user }}
DB_PASSWORD={{ db_password }}
DB_NAME={{ db_name }}
owner: apache
group: apache
mode: '0640' # Secure permissions for file with credentials
handlers:
- name: Restart httpd
ansible.builtin.systemd:
name: httpd
state: restarted
- name: Install Git
ansible.builtin.yum:
: Ensures Git is installed on the server.name: git
: Specifies thegit
package to install.state: present
: Confirms Git is installed and ready.
- name: Clone the application repository
ansible.builtin.git:
: Uses Ansible'sgit
module for managing Git repositories.repo: "{{ repo_url }}"
: Specifies the Git repository URL to clone.dest: "{{ project_root }}"
: Indicates where to clone the repository on the server.version: master
: Specifies the branch or tag to check out, oftenmaster
for development.force: yes
: This is a key setting. If the destination folder (project_root
) already has files in it,force: yes
will make sure the repository gets updated or re-cloned, replacing any local changes or uncommitted files. Be careful using this in production, where there might be manual changes. In a clean, automated setup, it guarantees a fresh copy.
- name: Create .env file with database credentials
ansible.builtin.copy:
: Uses thecopy
module withcontent
for dynamic file creation.dest: "{{ project_root }}/.env"
: Sets the.env
file location in the app's root directory.content: |
: Contains dynamic database connection details:DB_HOST={{ db_host }}
DB_USER={{ db_user }}
DB_PASSWORD={{ db_password }}
DB_NAME={{ db_name }}
owner: apache
,group: apache
: Sets the.env
file owner and group toapache
so the web server can read it.mode: '0640'
: Grants read/write to the owner and read to the group, keeping the file secure.handlers:
: Defines tasks that run only when notified, ensuring efficiency.- name: Restart httpd
: Restarts the Apache service using thesystemd
module.name: httpd
,state: restarted
: Ensures Apache restarts ifhttpd.conf
changes, applying new settings.
🧹 Cleanup and What's Next
After experimenting with your e-commerce setup, make sure to destroy your infrastructure using terraform destroy
to avoid unexpected Google Cloud charges.
This guide is a good start for automated deployments. Stay tuned for our next blog post on advanced security practices for your cloud infrastructure.
Subscribe to my newsletter
Read articles from bablu directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
