--- tags: web,day3 robots: noindex, nofollow lang: pt-br --- # 29 Automatizando o Deploy Vamos usar o **Ansible** para criar a automatização do deploy, todos os passos que fizemos nas aulas passadas iremos agora colocar em um **playbook** no Ansible. ## Ansible Ansible é um projeto open-source para automatização de tarefas sequenciais e utiliza o protocolo SSH para se comunicar com um ou mais servidores e executar as **tasks** definidas em um arquivo no formato **yaml**. Além de podermos descrever o passo a passo do deploy em um playbook ainda podemos reaproveitar automatizações criadas por outras pessoas e disponíveis no https://galaxy.ansible.com ### Instalando ```bash pip install ansible ``` ### Play e Playbook **Play** é um conjunto de fatos, variavéis e tarefas (facts, vars, tasks) e um **playbook** é o arquivo no formato **.yaml** onde colocamos um ou mais plays, em cada play descrevemos o estado que desejamos que o servidor esteja assim que o playbook for executado. ### Inventário O playbook é aplicado em todos os **hosts** de um inventário, o inventário é o local onde todos os IPs são agrupados e normalmente colocamos nosso inventário de hosts em um arquivo chamado `inventory` ou passamos os IPs via linha de comando. ## Automatizando o Deploy Vamos trabalhar na pasta `exemplos/day3/ansible` e para isso precisamos de uma VM no seu estado inicial, se você fez um snapshot pode agora salvar o estado atual da VM e então restaurar para o estado inicial. Se não fez o snapshot pode baixar novamente o arquivo .vdi em osboxes.org e iniciar uma nova VM limpa. ### 1 Inventory Criamos o arquivo `inventory` contendo o nosso host. > **NOTA** confirme o endereço de IP com o comando `ip address` dentro da VM. `nano exemplos/day3/ansible/inventory` ```ini [webservers] 192.168.1.101 ``` Caso queira fazer o deploy em mais servidores ao mesmo tempo poderá listar quantos IPS quiser e também pode criar quantos grupos quiser. ### 2 testando o Ansible `exemplos/day3/ansible/django-deploy.yaml` ```yaml --- - hosts: webservers remote_user: osboxes vars: - msg: Hello tasks: - name: print hello debug: msg: "{{ msg }} World" - name: Ping the server ping: ``` Execute com o comando ```bash ansible-playbook -i inventory django-deploy.yaml ``` ``` PLAY [webservers] *************************************************************** TASK [Gathering Facts] ********************************************************** ok: [192.168.1.101] TASK [print hello] ************************************************************** ok: [192.168.1.101] => { "msg": "Hello World" } TASK [Ping the server] ********************************************************** ok: [192.168.1.101] PLAY RECAP ********************************************************************** 192.168.1.101 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ``` > **NOTAS** Substitua o `remote_user` caso seja necessário e se não tiver uma chave SSH configurada verá um erro de autenticação, pode passar `--ask-pass` para o Ansible pedir para você digitar a sua senha. Repare que o Ansible executou 3 **TASKS** a primeira task **gathering facts** é default do próprio ansible, nesta tarefa ele coleta informações a respeito ma máquina remota e depois executa as nossa próprias tasks em sequência. Agora que já temos a resposta do teste podemos alterar o playbook para fazer de fato o deploy. ### 3 Deploy Agora vamos criar o playbook e nele descrever as variáveis e tarefas. > **DICA** se usar o VSCode instale as extensões [YAML](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) e [Ansible](https://marketplace.visualstudio.com/items?itemName=redhat.ansible) Os passos que vamos colocar em cada uma das tasks do playbook são: ![](imgs/deploy_steps.png) 1. Pacotes 2. Virtual Env 3. APP (pastas, permissões, repo) 4. Configurações 5. App Server 6. Web Server `exemplos/day3/ansible/django-deploy.yaml` ```yaml --- - hosts: webservers remote_user: osboxes vars: system_user: osboxes base_path: /app repo_url: 'https://github.com/rochacbruno/python-web-api' repo_branch: day3 repo_path: "{{ base_path }}/python-web-api" app_path: "{{ repo_path }}/exemplos/day2/django" app_name: djblog settings_path: "{{ app_name }}.settings" database_path: "{{ base_path }}/db.sqlite3" venv_path: /home/{{ system_user }}/.venv django_admin: "{{ venv_path }}/bin/django-admin" environment: PYTHONPATH: "{{ app_path }}" DJANGO_SETTINGS_MODULE: "{{ settings_path }}" BLOG_ENV: production tasks: - name: Instalação de pacotes become: true apt: update_cache: true state: present pkg: - python3-dev - python-is-python3 - nginx - git - sqlite3 - python3.10-venv - name: Virtual Env Python pip: virtualenv: "{{ venv_path }}" virtualenv_command: python3.10 -m venv name: pip state: latest - name: Adding user to www-data group become: true user: name: "{{ system_user }}" groups: - www-data append: true - name: Base APP Folder become: true file: path: "{{ base_path }}" state: directory owner: "{{ system_user }}" group: www-data mode: 0775 recurse: true - name: Respository Codebase git: repo: "{{ repo_url }}" dest: "{{ repo_path }}" single_branch: true version: "{{ repo_branch }}" force: true - name: Installing the project pip: virtualenv: "{{ venv_path }}" name: file://{{app_path}} state: present - name: Place the app settings file template: src: blog_settings.toml.j2 dest: "{{ base_path }}/blog_settings.toml" - name: Django - Get the database name command: chdir: "{{ base_path }}" cmd: >- {{ django_admin }} shell -c "from django.conf import settings;print(settings.DATABASES.default.NAME)" register: django_db_name - name: Django - Assert App is correctly set assert: that: django_db_name.stdout == database_path - name: Django - DB Migration command: chdir: "{{ base_path }}" cmd: "{{ django_admin }} migrate --noinput" - name: Django - Check if admin user exists command: chdir: "{{ base_path }}" cmd: >- {{ django_admin }} shell -c "from django.contrib.auth.models import User;print(User.objects.count())" register: django_admin_user_count - name: Django - Create admin user command: chdir: "{{ base_path }}" cmd: >- {{ django_admin }} shell -c "from django.contrib.auth.models import User; User.objects.create_superuser('admin', 'admin@example.com', 'admin')" when: django_admin_user_count.stdout == "0" - name: Django - Collect static files command: chdir: "{{ base_path }}" cmd: "{{ django_admin }} collectstatic --noinput" - name: Gunicorn become: true block: - name: Gunicorn - Install pip: name: gunicorn virtualenv: "{{ venv_path }}" state: present - name: Gunicorn - socket file template: src: templates/gunicorn.socket.j2 dest: /etc/systemd/system/gunicorn.socket - name: Gunicorn - service file template: src: templates/gunicorn.service.j2 dest: /etc/systemd/system/gunicorn.service - name: Gunicorn - Reload systemd systemd: daemon_reload: yes - name: Gunicorn - Start service systemd: state: started enabled: true name: gunicorn.socket - name: NGINX become: true block: - name: NGINX - Copy site config template: src: templates/nginx_blog.j2 dest: "/etc/nginx/sites-available/nginx_blog" - name: NGINX - Enable site config file: src: "/etc/nginx/sites-available/nginx_blog" dest: /etc/nginx/sites-enabled/default state: link - name: NGINX - Reload service systemd: state: restarted name: nginx ``` > **NOTA** como em algumas tasks usamos `become: true` para executar com `sudo` teremos que informar o password e por isso executamos com `--ask-become-pass` ```bash ansible-playbook -i inventory django-deploy.yaml --ask-become-pass ``` ``` BECOME password: PLAY [webservers] *************************************************************** TASK [Gathering Facts] ********************************************************** ok: [192.168.1.101] TASK [Instalação de pacotes] **************************************************** ok: [192.168.1.101] TASK [Virtual Env Python] ******************************************************* ok: [192.168.1.101] TASK [Adding user to www-data group] ******************************************** ok: [192.168.1.101] TASK [Base APP Folder] ********************************************************** changed: [192.168.1.101] TASK [Respository Codebase] ***************************************************** changed: [192.168.1.101] TASK [Installing the project] *************************************************** changed: [192.168.1.101] TASK [Place the app settings file] ********************************************** ok: [192.168.1.101] TASK [Django - Get the database name] ******************************************* changed: [192.168.1.101] TASK [Django - Assert App is correctly set] ************************************* ok: [192.168.1.101] => { "changed": false, "msg": "All assertions passed" } TASK [Django - DB Migration] **************************************************** changed: [192.168.1.101] TASK [Django - Check if admin user exists] ************************************** changed: [192.168.1.101] TASK [Django - Create admin user] *********************************************** changed: [192.168.1.101] TASK [Django - Collect static files] ******************************************** changed: [192.168.1.101] TASK [Gunicorn - Install] ******************************************************* changed: [192.168.1.101] TASK [Gunicorn - socket file] *************************************************** changed: [192.168.1.101] TASK [Gunicorn - service file] ************************************************** changed: [192.168.1.101] TASK [Gunicorn - Reload systemd] ************************************************ ok: [192.168.1.101] TASK [Gunicorn - Start service] ************************************************* changed: [192.168.1.101] TASK [NGINX - Copy site config] ************************************************* changed: [192.168.1.101] TASK [NGINX - Enable site config] *********************************************** changed: [192.168.1.101] TASK [NGINX - Reload service] *************************************************** changed: [192.168.1.101] PLAY RECAP ********************************************************************** 192.168.1.101 : ok=22 changed=15 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ``` ## Templates Na pasta `templates` `blog_settings.toml.j2` ```html [production] allowed_hosts = ["*"] debug = false DATABASES__default__NAME="{{ database_path }}" STATIC_ROOT="{{ base_path }}/static/" STATICFILES_DIR="{{base_path }}/static/" STATIC_URL="/static/" ``` `gunicorn.service.j2` ```ini [Unit] Description=gunicorn daemon Requires=gunicorn.socket After=network.target [Service] Environment=PYTHONPATH={{ app_path }} Environment=DJANGO_SETTINGS_MODULE={{ settings_path }} Environment=BLOG_ENV=production User={{ system_user }} Group=www-data WorkingDirectory={{ base_path }} ExecStart={{ venv_path }}/bin/gunicorn \ --access-logfile - \ --workers 3 \ --bind unix:/run/gunicorn.sock \ {{ app_name }}.wsgi [Install] WantedBy=multi-user.target ``` `gunicorn.socket.j2` ```ini [Unit] Description=gunicorn socket [Socket] ListenStream=/run/gunicorn.sock [Install] WantedBy=sockets.target ``` `nginx_blog.j2` ```nginx server { listen 80 default_server; listen [::]:80 default_server; server_name _; location = /favicon.ico { access_log off; log_not_found off; } location /static/ { autoindex on; alias {{ base_path }}/static/; } location / { include proxy_params; proxy_pass http://unix:/run/gunicorn.sock; } } ```