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

bablubablu
11 min read

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.

  1. Go to Compute Engine > VM instances in Google Cloud Console.

  2. Click your VM instance's name for details.

  3. Click "EDIT" at the top.

  4. Scroll to "SSH Keys".

  5. Click "Add item".

  6. Paste the full content of your public key file (.pub) into the box. Include everything from ssh-rsa to USERNAME.

    • View your public key in Cloud Shell: cat ~/.ssh/centos-ssh-key.pub
  7. 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 use centuser for the SSH connection to 192.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 to 192.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 the mariadb-server package is installed and updated using the ansible.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 the ansible.builtin.file module.

    • - name: Start and enable MariaDB service ansible.builtin.systemd: name: mariadb state: started enabled: yes: Manages the mariadb service to ensure it is running and set to start on boot using the ansible.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 the yum module for package management on RedHat-based systems.

  • name: python3-pip: Focuses on installing the python3-pip package.

  • state: present: Ensures python3-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 the pip module to manage Python packages.

    • name: PyMySQL: Installs the PyMySQL library, a Python MySQL client for connecting to MySQL/MariaDB databases.

    • executable: pip3: Ensures installation for Python 3 by using pip3.

    • when: ansible_python_version.startswith('3.'): Installs PyMySQL 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 the mysql_db module from community.mysql for managing MySQL/MariaDB databases.

    • name: "{{ db_name }}": Sets the database name using the db_name variable from the playbook's vars 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 the mysql_user module.

    • name: "{{ db_user }}": Sets the username with the db_user variable.

    • host: "{{ db_host }}": Limits user connections to localhost for security.

    • password: "{{ db_password }}": Sets the user's password from db_password. (Use Ansible Vault for security in production!)

    • priv: "{{ db_name }}.*:ALL": Grants ALL 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 the copy 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 the products table and add initial data.

      • USE {{ db_name }};: Selects the database.

      • CREATE TABLE IF NOT EXISTS products (...): Sets up the products 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 the mysql_db module with a different state.

    • 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 the yum 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 with DirectoryIndex and captures the rest.

    • line: '\1index.php \2': Updates the line to include index.php as the default file.

    • backrefs: yes: Allows using backreferences (\1, \2, etc.) in the line parameter to refer to parts of the matched regexp.

    • notify: Restart httpd: Ansible "handlers" feature. If httpd.conf changes, it "notifies" the Restart 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 the systemd module to manage the Apache service.

    • name: httpd: Specifies the service to manage.

    • state: started: Ensures the httpd service is running.

    • enabled: yes: Ensures the httpd 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 the git package to install.

    • state: present: Confirms Git is installed and ready.

  • - name: Clone the application repository

    • ansible.builtin.git:: Uses Ansible's git 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, often master 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 the copy module with content 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 to apache 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 the systemd module.

      • name: httpd, state: restarted: Ensures Apache restarts if httpd.conf changes, applying new settings.

vm created

🧹 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.

0
Subscribe to my newsletter

Read articles from bablu directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

bablu
bablu