A comprehensive guide to building a secure, scalable CI/CD pipeline for any PHP-based application, including WordPress, Drupal, and custom solutions
In today’s fast-paced web development world, automating deployment processes isn’t just a luxury — it’s a necessity. Whether you’re managing a popular CMS like WordPress, a robust e-commerce platform, or a custom PHP application, the challenges of consistent, secure, and efficient deployments remain the same. This guide will walk you through creating a powerful, flexible automation solution that can handle any PHP-based web application with ease.
We’ll use SuiteCRM, an open-source customer relationship management system, as our primary example. However, the principles and techniques we’ll cover are universally applicable to any PHP application. From setting up a CI/CD pipeline with GitHub Actions to provisioning infrastructure with AWS CloudFormation, and configuring servers with Ansible, we’ll explore a comprehensive DevOps approach that will revolutionize your deployment process.
Introduction
This tutorial provides a comprehensive guide to automating the deployment of SuiteCRM using GitHub Actions, AWS CloudFormation, and Ansible. We'll cover the entire process, including all necessary code for GitHub Actions workflows and Ansible roles.
But we won’t stop at just automation. We’ll delve into crucial aspects like securely managing secrets and variables, ensuring that your deployment pipeline is not only efficient but also adheres to the highest security standards. Whether you’re a seasoned DevOps engineer or a PHP developer looking to streamline your workflow, this guide will equip you with the tools and knowledge to master modern deployment strategies.
Ready to transform your PHP application deployments from time-consuming manual processes to streamlined, automated workflows? Let’s dive in and explore the world of DevOps mastery!
Let’s understand how our process looks like from the figures below
Figure 1: Overview of our automated PHP application deployment pipeline, from code push to server configuration.
Figure 2: Detailed view of the AWS infrastructure created by our CloudFormation template, including VPC, subnets, and security groups.
Figure 3: Flowchart of our GitHub Actions workflow, showing the steps from initial setup to deployment of infrastructure and application.
Why Automate SuiteCRM Deployment?
Automating SuiteCRM deployment offers numerous benefits:
- Consistency across deployments
- Rapid deployment and updates
- Scalability for growing businesses
- Version control for infrastructure and configuration
- Reduced human error
- Cost-effectiveness in the long run
- Improved security through standardized processes
- Easier application of updates and patches
Overview of the Automation Stack
Our automation stack consists of:
- GitHub Actions for CI/CD pipeline
- AWS CloudFormation for infrastructure as code
- Ansible for configuration management and application deployment
Step-by-Step Tutorial
1. Setting Up the GitHub Repository
Create a GitHub repository with the following structure:
.
├── .github
│ └── workflows
│ ├── main.yaml
│ ├── infra.yaml
│ └── crm.yaml
├── infrastructure
│ ├── prod.yaml
│ └── common
│ ├── vpc
│ │ ├── vpc.yaml
│ │ └── eip.yaml
│ └── ec2
│ └── ec2.yaml
└── crm
└── ansible
├── ansible.cfg
├── main.yml
└── roles
├── apache2
├── php
├── sql
└── suitecrm
2. Setting Up AWS IAM Roles
Create two IAM roles in AWS:
- A role for GitHub Actions with permissions to manage CloudFormation, EC2, S3, and Route53.
- A role for CloudFormation with permissions to create and manage AWS resources.
3. Configuring GitHub Secrets and Variables
In your GitHub repository settings, add the following:
Secrets:
-
AWS_ROLE_TO_ASSUME
: ARN of the GitHub Actions IAM role -
CLOUDFORMATION_ROLE_ARN
: ARN of the CloudFormation IAM role -
CRM_KEY
: SSH key for EC2 access
Variables:
-
PROD_STACK_NAME
: Name for your CloudFormation stack -
PROD_CFN_S3_BUCKET
: S3 bucket for CloudFormation templates -
CHANGE_SET_NAME
: Name for CloudFormation change sets -
HOSTED_ZONE_ID
: Your Route 53 hosted zone ID -
CRM_DomainName
: Domain for your SuiteCRM installation
4. Setting Up GitHub Actions Workflows
Create the following workflow files in the .github/workflows/
directory:
main.yaml
name: Main Class File
on:
workflow_call:
inputs:
aws-region:
required: false
type: string
default: 'us-east-1'
role-session-name:
required: false
type: string
default: 'GitHubActions'
secrets:
AWS_ROLE_TO_ASSUME:
required: true
outputs:
cache-hit:
description: "Whether the cache was hit"
value: ${{ jobs.setup.outputs.cache-hit }}
jobs:
setup:
runs-on: ubuntu-latest
outputs:
cache-hit: ${{ steps.cache.outputs.cache-hit }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Cache AWS configuration and custom scripts
id: cache
uses: actions/cache@v3
with:
path: |
~/.aws
~/.local/bin
key: ${{ runner.os }}-aws-config-${{ hashFiles('**/package-lock.json', '**/requirements.txt') }}
- name: Configure AWS Credentials
if: steps.cache.outputs.cache-hit != 'true'
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
aws-region: ${{ inputs.aws-region }}
role-session-name: ${{ inputs.role-session-name }}
- name: Install aws cli and other packages
if: steps.cache.outputs.cache-hit != 'true'
run: |
sudo apt-get update
sudo apt-get install -y unzip zip curl awscli net-tools ansible
- name: Save environment variables
if: steps.cache.outputs.cache-hit != 'true'
run: |
env | grep -E "AWS_|ANSIBLE_" > $GITHUB_ENV
infra.yaml
name: Deploy or Update SuiteCRM
permissions:
id-token: write
contents: read
on:
push:
branches:
- main
paths:
- 'infrastructure/**'
jobs:
setup:
uses: ./.github/workflows/main.yaml
secrets:
AWS_ROLE_TO_ASSUME: ${{ secrets.AWS_ROLE_TO_ASSUME }}
with:
aws-region: us-east-1
role-session-name: 'InfraDeployment'
deploy:
needs: setup
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
aws-region: us-east-1
role-session-name: "SuiteCRM"
- name: Package the CloudFormation template
run: |
aws cloudformation package --debug \
--template-file prod.yaml \
--output-template-file ${{ vars.PROD_STACK_NAME }}-packaged.template \
--s3-bucket ${{ vars.PROD_CFN_S3_BUCKET }} \
--s3-prefix ${{ vars.PROD_STACK_NAME }}
working-directory: infrastructure
- name: Deploy the CloudFormation stack
run: |
MY_IP=$(curl -s https://ifconfig.me)
MY_IP_CIDR="${MY_IP}/32"
aws cloudformation deploy \
--s3-bucket ${{ vars.PROD_CFN_S3_BUCKET }} \
--s3-prefix ${{ vars.PROD_STACK_NAME }} \
--template-file ${{ vars.PROD_STACK_NAME }}-packaged.template \
--stack-name ${{ vars.PROD_STACK_NAME }} \
--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \
--role-arn ${{ secrets.CLOUDFORMATION_ROLE_ARN }} \
--parameter-overrides \
MyIp=$MY_IP_CIDR \
HostedZoneId=${{ vars.HOSTED_ZONE_ID }} \
CRMDomainName=${{ vars.CRM_DomainName }} \
--no-fail-on-empty-changeset
working-directory: infrastructure
crm.yaml
name: Configure SuiteCRM
permissions:
id-token: write
contents: read
on:
push:
branches:
- main
paths:
- 'crm/**'
jobs:
setup:
uses: ./.github/workflows/main.yaml
secrets:
AWS_ROLE_TO_ASSUME: ${{ secrets.AWS_ROLE_TO_ASSUME }}
with:
aws-region: us-east-1
role-session-name: 'InfraDeployment'
deploy:
needs: setup
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
aws-region: us-east-1
role-session-name: "SuiteCRM"
- name: Get CloudFormation Outputs
id: cfn_outputs
run: |
OUTPUTS=$(aws cloudformation describe-stacks --stack-name ${{ vars.PROD_STACK_NAME }} --query "Stacks[0].Outputs" --output json)
echo "Outputs: $OUTPUTS"
echo "::set-output name=elastic_ip::$(echo $OUTPUTS | jq -r '.[] | select(.OutputKey == "ElasticIp") | .OutputValue')"
echo "::set-output name=domain_name::$(echo $OUTPUTS | jq -r '.[] | select(.OutputKey == "Route53DomainName") | .OutputValue')"
- name: Create Ansible Inventory File
run: |
echo "[crm]" > inventory.ini
echo "${{ steps.cfn_outputs.outputs.elastic_ip }} ansible_user=ubuntu ansible_ssh_private_key_file=id_rsa" >> inventory.ini
working-directory: crm/ansible
- name: Get ssh keys
env:
CRM_KEY: ${{ secrets.CRM_KEY }}
run: |
echo "$CRM_KEY" > id_rsa
chmod 400 id_rsa
working-directory: crm/ansible
- name: Run Ansible Playbook
run: |
ansible-galaxy install mahdi22.mariadb_install
ansible-playbook -i inventory.ini main.yml --extra-vars "domain_name=${{ steps.cfn_outputs.outputs.domain_name }}"
working-directory: crm/ansible
5. Creating Ansible Roles
Create the following Ansible roles in the crm/ansible/roles/
directory:
apache2/tasks/main.yml
---
# tasks file for apache2
- name: Update the Cache
apt:
update_cache: yes
- name: Install Apache2
apt: name=apache2 state=present
- name: Start Apache2
service: name=apache2 state=started enabled=yes
php/tasks/main.yml
---
# tasks file for php
- name: Add PHP PPA repository
apt_repository:
repo: ppa:ondrej/php
state: present
- name: Update the Cache
apt:
update_cache: yes
- name: Install PHP Packages
apt:
name: "{{ item }}"
state: present
loop: "{{ packages }}"
tags: packages
- name: Set upload_max_filesize to 100M
lineinfile:
path: /etc/php/{{ php_version }}/apache2/php.ini
regexp: '^upload_max_filesize ='
line: 'upload_max_filesize = 100M'
notify: Restart Apache
- name: Set post_max_size to 100M
lineinfile:
path: /etc/php/{{ php_version }}/apache2/php.ini
regexp: '^post_max_size ='
line: 'post_max_size = 100M'
notify: Restart Apache
- name: Set memory_limit to 500M
lineinfile:
path: /etc/php/{{ php_version }}/apache2/php.ini
regexp: '^memory_limit ='
line: 'memory_limit = 500M'
notify: Restart Apache
php/vars/main.yml
---
packages:
- php8.1
- php8.1-cli
- php8.1-common
- php8.1-imap
- php8.1-redis
- php8.1-snmp
- php8.1-xml
- php8.1-zip
- php8.1-mbstring
- php8.1-curl
- libapache2-mod-php8.1
- php8.1-gd
- php8.1-intl
- php8.1-mysql
- php8.1-ldap
- php8.1-soap
php_version: "8.1"
sql/tasks/main.yml
---
- name: Install MariaDB packages
apt:
name:
- mariadb-server
- python3-pymysql
state: present
update_cache: yes
- name: Start MariaDB service
service:
name: mariadb
state: started
enabled: yes
- name: Copy MariaDB configuration file
template:
src: mariadb.cnf.j2
dest: /etc/mysql/mariadb.conf.d/50-server.cnf
notify: Restart MariaDB
- name: Check if .my.cnf file exists
stat:
path: /root/.my.cnf
register: mycnf_file
- name: Attempt to access MariaDB as root
command: mysql -u root -e "SELECT 1"
register: mysql_access
ignore_errors: yes
changed_when: false
when: not mycnf_file.stat.exists
become: true
- name: Reset MariaDB root password (Ubuntu)
block:
- name: Stop MariaDB
service:
name: mariadb
state: stopped
- name: Start MariaDB with skip-grant-tables
shell: mysqld_safe --skip-grant-tables &
async: 45
poll: 0
become: true
- name: Wait for MariaDB to start
wait_for:
port: 3306
- name: Update root password
shell: |
mysql -e "FLUSH PRIVILEGES; ALTER USER 'root'@'localhost' IDENTIFIED BY '{{ mariadb_root_password }}'; FLUSH PRIVILEGES;"
become: true
- name: Stop MariaDB
shell: mysqladmin -u root -p'{{ mariadb_root_password }}' shutdown
become: true
- name: Start MariaDB normally
service:
name: mariadb
state: started
when:
- mysql_access is failed
- not mycnf_file.stat.exists
- name: Create .my.cnf file with root credentials
template:
src: my.cnf.j2
dest: /root/.my.cnf
mode: '0600'
when: not mycnf_file.stat.exists
become: true
- name: Create MariaDB database
mysql_db:
name: "{{ mariadb_db }}"
state: present
login_unix_socket: /var/run/mysqld/mysqld.sock
become: true
- name: Create MariaDB user
mysql_user:
name: "{{ mariadb_user }}"
password: "{{ mariadb_password }}"
priv: "{{ mariadb_db }}.*:ALL"
host: 'localhost'
state: present
login_unix_socket: /var/run/mysqld/mysqld.sock
become: true
suitecrm/tasks/main.yml
---
# tasks file for suitecrm
- name: Install unzip
apt:
name: unzip
state: present
- name: Download SuiteCRM
get_url:
url: https://suitecrm.com/download/147/suite86/564202/suitecrm-8-6-2.zip
dest: /tmp/suitecrm.zip
- name: Unzip SuiteCRM
unarchive:
src: /tmp/suitecrm.zip
dest: /var/www/html
remote_src: yes
mode: '0755'
become: true
- name: Set ownership of /var/www/html to www-data
file:
path: /var/www/html
state: directory
recurse: yes
owner: www-data
group: www-data
become: true
- name: Create SuiteCRM Apache config file
template:
src: suitecrm.conf.j2
dest: /etc/apache2/sites-available/suitecrm.conf
- name: Enable SuiteCRM site
command: a2ensite suitecrm.conf
become: true
- name: Install Certbot and python3-certbot-apache
apt:
name:
- certbot
- python3-certbot-apache
state: present
- name: Obtain SSL certificate using Certbot for SuiteCRM
command: certbot --apache -d {{ domain_name }} --non-interactive --agree-tos --email {{ email }}
become: true
- name: Reload Apache to apply SSL certificate
service:
name: apache2
state: reloaded
become: true
- name: Set permissions for directories to 755
find:
paths: /var/www/html
recurse: yes
file_type: directory
register: directories
- name: Apply 755 permissions to directories
file:
path: /var/www/html
mode: '0755'
become: true
- name: Reload Apache with SSL configuration
service:
name: apache2
state: reloaded
become: true
suitecrm/templates/suitecrm.conf.j2
<VirtualHost *:80>
ServerName {{ domain_name }}
DocumentRoot /var/www/html/public
<Directory /var/www/html/public>
AllowOverride All
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
suitecrm/vars/main.yml
---
# vars file for suitecrm
domain_name: "crm.thebudgetbuys.com"
email: "rijalnitesh78@gmail.com"
6. Additional Configuration Files
ansible.cfg
Create this file in the crm/ansible/
directory:
[defaults]
host_key_checking = False
crm/ansible/main.yml
This is the main Ansible playbook that orchestrates the roles:
---
- hosts: crm
become: true
roles:
- role: sql
- role: apache2
- role: php
- role: suitecrm
roles/sql/defaults/main.yml
---
# defaults file for sql
mariadb_root_password: "suitecrm"
mariadb_db: "suitecrm"
mariadb_user: "suitecrm"
mariadb_password: "suitecrm"
roles/sql/vars/main.yml
---
# vars file for sql
mariadb_root_password: suitecrm
mariadb_database: suitecrm
mariadb_user: suitecrm
mariadb_password: suitecrm
ansible_python_interpreter: /usr/bin/python3
roles/sql/handlers/main.yml
---
- name: Restart MariaDB
service:
name: mariadb
state: restarted
roles/sql/templates/mariadb.cnf.j2
[mysqld]
datadir=/var/lib/mysql
socket=/var/run/mysqld/mysqld.sock
bind-address=0.0.0.0
port=3306
# InnoDB settings
innodb_buffer_pool_size=1G
innodb_log_file_size=256M
innodb_flush_log_at_trx_commit=1
innodb_flush_method=O_DIRECT
# MyISAM settings
key_buffer_size=256M
# Query cache
query_cache_size=0
query_cache_type=0
# Logging
general_log_file=/var/log/mysql/mysql.log
log_error=/var/log/mysql/error.log
# Character set
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci
[mysql]
default-character-set=utf8mb4
[client]
default-character-set=utf8mb4
roles/sql/templates/my.cnf.j2
[client]
user=root
password={{ mariadb_root_password }}
7. Deployment Process
With all these files in place, the automated deployment process works as follows:
-
When changes are pushed to the
infrastructure/
directory, theinfra.yaml
workflow is triggered.- This workflow deploys or updates the AWS infrastructure using CloudFormation.
-
When changes are pushed to the
crm/
directory, thecrm.yaml
workflow is triggered.- This workflow runs the Ansible playbook, which:
- Installs and configures MariaDB
- Installs and configures Apache2
- Installs and configures PHP
- Downloads and sets up SuiteCRM
- Configures SSL using Certbot
- This workflow runs the Ansible playbook, which:
After the playbook runs successfully, SuiteCRM should be accessible at the specified domain (https://crm.thebudgetbuys.com in this case).
Benefits of This Approach
- Infrastructure as Code (IaC): All AWS resources are defined in CloudFormation templates, allowing for version control and easy replication.
- Configuration as Code: Ansible roles define the exact configuration of the server and application, ensuring consistency across deployments.
- Automated Testing: You can easily add testing steps in the GitHub Actions workflows to ensure quality before deployment.
- Easy Rollbacks: If an issue occurs, you can quickly roll back to a previous version of either the infrastructure or the application configuration.
- Scalability: This setup can be easily adapted to deploy multiple environments or multiple instances of SuiteCRM.
- Audit Trail: All changes to infrastructure and configuration are tracked in Git, providing a clear audit trail.
- Reduced Downtime: Automated deployments are faster and can be scheduled during off-peak hours to minimize disruption.
- Knowledge Sharing: New team members can quickly understand the entire setup by reviewing the code in the repository.
Conclusion
This comprehensive setup provides a fully automated deployment pipeline for SuiteCRM. By leveraging GitHub Actions, AWS CloudFormation, and Ansible, we've created a robust, scalable, and maintainable solution that can significantly streamline the deployment and management of SuiteCRM instances.
Remember to regularly update your dependencies, review security best practices, and continuously improve your automation scripts to keep your SuiteCRM deployment efficient and secure.