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: ...
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
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"
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"
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;
...
}
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
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
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"
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
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
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
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
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"
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
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
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
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)