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 (0)