DEV Community

Cover image for We are deploying a PHP project on the prod using Ansible
Denis
Denis

Posted on

We are deploying a PHP project on the prod using Ansible

In this article, we will get to know you at a basic level with Ansible, and deploy a project using it on a PHP server.

Getting to know Ansible

What kind of miracle tool is this?

Ansible is a tool for every YAML champion, with which you can deploy applications, configure configurations, and automate tasks via ssh.

You may have heard of it along with the phrase "Infrastructure as Code (IaC)", because most of the infrastructure is set up using it.


Basic concepts:

Playbook is yaml a file that contains a set of tasks for their sequential execution

--- - name: Update web servers hosts: webservers tasks: ... - name: Update db servers hosts: databases tasks: ... 
Enter fullscreen mode Exit fullscreen mode

The example shows the scenario of updating the web server and database

In each scenario, we describe:

  • name - script name
  • hosts - the name of the host/hosts from the inventory file
  • tasks - a set of tasks, you can specify instructions directly or connect individual files with the task itself

For a detailed introduction, go to link

Task is yaml a file with a certain set of commands that is responsible for its area (installing packages, configuring a web server, etc.)

--- - name: Update web servers hosts: webservers tasks: - name: Ensure apache is at the latest version ansible.builtin.yum: name: httpd state: latest - name: Write the apache config file ansible.builtin.template: src: /srv/httpd.j2 dest: /etc/httpd.conf - name: Update db servers hosts: databases tasks: - name: Ensure postgresql is at the latest version ansible.builtin.yum: name: postgresql state: latest - name: Ensure that postgresql is started ansible.builtin.service: name: postgresql state: started 
Enter fullscreen mode Exit fullscreen mode

Here we are already clearly looking at completing tasks in the playbook

In the problem, we can describe:

  • name - task name
  • ansible.builtin. - the module and its parameters

The module in the task facilitates the execution of operations by preparing a script inside the module, and the interaction takes place by passing arguments to the module.

For more information, go to link

Vars is yaml a file that contains a set of variables that we can use in task and template files

--- repo_url: "https://github.com/deniskorbakov/laravel-12-frankenphp-docker.git" path_to_remote_directory: "/var/www/laravel" 
Enter fullscreen mode Exit fullscreen mode

In the file, we describe the variables that we want to use in our task and template files.

- hosts: app_servers vars: app_path: "{{ path_to_remote_directory }}/22" 
Enter fullscreen mode Exit fullscreen mode

To use variables, we open and close the birds, and in them we specify the name of our variable.

For more information, go to link

Template is j2 a file that we can reuse in task files, for example, to copy configuration files with prepared variables

server { listen 80; listen [::]:80; server_name {{ domain }}; server_tokens off; root {{ path_to_remote_directory }}/public; ... } 
Enter fullscreen mode Exit fullscreen mode

This file contains variables that will be determined during the execution of the playbook, thereby allowing flexible configuration of various files.

For more information, go to link

Inventory is ini a file that contains a list of hosts that we can manage via Ansible

[web] host1 host2 ansible_port=222 # defined inline, interpreted as an integer [web:vars] http_port=8080 # all members of 'web' will inherit these myvar=23 # defined in a :vars section, interpreted as a string 
Enter fullscreen mode Exit fullscreen mode

Using these files, we describe our hosts, which in the future we will be able to specify in the playbook.

For more information, go to link


Conclusion on Ansible:

With these concepts, we can describe task scenarios in playbooks, add reusable variables and template files, and be able to perform tasks on multiple hosts.

About the PHP project

I took my own template for the project.:
https://github.com/deniskorbakov/laravel-12-frankenphp-docker

This template contains frankenphp, docker-compose environment, web sockets via centrifugo, Open Api Doc and ready authorization.

It already describes in advance the playbook for deployment on the prod

Writing a Playbook

Description of what needs to be done:

On the prod, we will need to install the necessary packages, configure nginx to proxy our project, issue certificates for the domain, deploy and configure the project itself.

Setting up inventory:

[webservers] 144.124.249.213 [all:vars] ansible_connection=ssh  ansible_user=root 
Enter fullscreen mode Exit fullscreen mode

We fill in ssh access for the server on which we will deploy the project

We specify variables:

--- repo_url: "https://github.com/deniskorbakov/laravel-12-frankenphp-docker.git" path_to_remote_directory: "/var/www/laravel" domain: "v543323.hosted-by-vdsina.com" url: "https://{{ domain }}" os_environment: - key: APP_URL value: "{{ url }}" - key: APP_ENV value: "production" - key: APP_DEBUG value: "false" - key: OCTANE_HTTPS value: "true" 
Enter fullscreen mode Exit fullscreen mode

We fill in the following variables:

  • repo_url - the url of our project in github
  • path_to_remote_directory - the path where our project will be located on the server
  • domain - specify which is linked to our server
  • url - generated independently from domain
  • os_environment - fill in the variables for env, which we will replace with the product

Creating a Playbook:

- name: Expand the environment hosts: webservers vars_files: - ../vars/default.yml tasks: - name: Init Packages ansible.builtin.include_tasks: ../tasks/packages/init.yml - name: Setup Docker ansible.builtin.include_tasks: ../tasks/docker/setup.yml - name: Clone Project ansible.builtin.include_tasks: ../tasks/sync/copy.yml - name: Init App ansible.builtin.include_tasks: ../tasks/app/init.yml - name: Configure Nginx ansible.builtin.include_tasks: ../tasks/system/nginx.yml - name: Produce Certificates ansible.builtin.include_tasks: ../tasks/system/cert.yml - name: Rebuild App ansible.builtin.include_tasks: ../tasks/app/rebuild.yml 
Enter fullscreen mode Exit fullscreen mode

Here we specify the alias of our hosts from inventory.ini to hosts, add a file with variables and specify the tasks to be performed in turn at startup

We describe Tasks:

Next, let's look at each task in order.

Init Packages

--- - name: Install required packages ansible.builtin.apt: name: - apt-transport-https - ca-certificates - curl - software-properties-common - gnupg - make - git - nginx - socat - certbot - python3-certbot-nginx state: present update_cache: yes 
Enter fullscreen mode Exit fullscreen mode

Here we install all the packages we need to work with.

Setup Docker

--- - name: Add Docker GPG key ansible.builtin.apt_key: url: https://download.docker.com/linux/ubuntu/gpg state: present - name: Add Docker repository ansible.builtin.apt_repository: repo: deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable state: present filename: docker - name: Install Docker ansible.builtin.apt: name: - docker-ce - docker-ce-cli - containerd.io state: present update_cache: yes - name: Start and enable Docker service ansible.builtin.service: name: docker state: started enabled: yes - name: Add user to docker group ansible.builtin.user: name: "{{ ansible_user | default('ansible') }}" groups: docker append: yes - name: Install Docker Compose ansible.builtin.get_url: url: https://github.com/docker/compose/releases/download/v2.29.2/docker-compose-linux-x86_64 dest: /usr/local/bin/docker-compose mode: '0755' - name: Create symbolic link for Docker Compose ansible.builtin.file: src: /usr/local/bin/docker-compose dest: /usr/bin/docker-compose state: link - name: Verify Docker Compose installation ansible.builtin.command: docker-compose --version register: docker_compose_version changed_when: false 
Enter fullscreen mode Exit fullscreen mode

In this task, we install docker and docker-compose for further work.

Clone Project

--- - name: Check if directory exists ansible.builtin.stat: path: "{{ path_to_remote_directory }}" register: project_dir_stat - name: Create project dir ansible.builtin.file: path: "{{ path_to_remote_directory }}" state: directory mode: 0755 when: not project_dir_stat.stat.exists - name: Git clone block: - name: Clone repository ansible.builtin.git: repo: "{{ repo_url }}" dest: "{{ path_to_remote_directory }}" version: "{{ branch | default('main') }}" register: clone_result retries: 3 delay: 5 until: clone_result is succeeded when: not project_dir_stat.stat.exists 
Enter fullscreen mode Exit fullscreen mode

Here we create a directory for the project, if it has not yet been created, and clone our project, which we specified in the variables file.

Init App

--- - name: Create Storage Public Dir ansible.builtin.file: path: "{{ path_to_remote_directory }}/storage/app/public" state: directory mode: 0755 - name: Copy env.example ansible.builtin.copy: src: "{{ path_to_remote_directory }}/.env.example" dest: "{{ path_to_remote_directory }}/.env" remote_src: yes - name: Set vars in ENV lineinfile: path: "{{ path_to_remote_directory }}/.env" state: present regexp: "^{{ item.key }}=" line: "{{ item.key }}={{ item.value}}" with_items: "{{ os_environment }}" become: yes - name: Init Project ansible.builtin.command: cmd: make init-prod chdir: "{{ path_to_remote_directory }}" register: command_result failed_when: "'FAILED' in command_result.stderr" 
Enter fullscreen mode Exit fullscreen mode

Here we create a public directory, copy env.example to env and replace certain variables that we explicitly specify in the file with variables for ansible and run make init-prod to initialize the project on the prod

Configure Nginx

--- - name: Delete default dir ansible.builtin.file: state: absent path: /var/www/html - name: Copy config ansible.builtin.template: src: ../templates/nginx_conf.j2 dest: "/etc/nginx/sites-enabled/{{ domain }}" - name: Reload Nginx ansible.builtin.systemd: state: reloaded name: nginx 
Enter fullscreen mode Exit fullscreen mode

In this task, delete the default directory, copy the config for nginx, and restart the nginx process.

Produce Certificates

--- - name: Obtain SSL certificate with certbot ansible.builtin.command: | certbot \  --force-renewal \  --nginx \  --noninteractive \  --agree-tos \  --cert-name {{ domain }} \  -d {{ domain }} \  -m test@gmail.com \  --verbose  args: creates: "/etc/letsencrypt/live/{{ domain }}/cert.pem" become: yes register: certbot_result 
Enter fullscreen mode Exit fullscreen mode

Here we already issue certificates for our domain through certbot

Rebuild App

--- - name: Pause for 2 min ansible.builtin.pause: minutes: 2 - name: Restart app ansible.builtin.command: cmd: make restart chdir: "{{ path_to_remote_directory }}" become: yes register: restart_result - name: Pause for 2 min ansible.builtin.pause: minutes: 2 - name: Update project ansible.builtin.command: cmd: make update-project chdir: "{{ path_to_remote_directory }}" become: yes register: update_result 
Enter fullscreen mode Exit fullscreen mode

In this task, we are restarting our containers and updating these projects so that everything works for sure!

The result of the work done:

Now you and I have written your first playbook and got acquainted with the wonderful tool Ansible for YAML champions

I'm waiting for comments under the post about what can be improved or how you would write this playbook ;)

Bottom line

Today, we learned a little about Ansible, studied its basic concepts, wrote a Playbook with you, and deployed the project on the prod

Thank you for reading this article.

Top comments (1)

Collapse
 
gallowaydeveloper profile image
Galloway Developer

Nice walkthrough, but it feels more like “manual steps in YAML” than fully idempotent IaC. For example, using command/make and pause reduces repeatability and observability. Would be great to see handlers, proper when conditions, and more use of first-class Docker modules.