Changeset - b70cbdc05748
[Not reviewed]
0 1 0
Branko Majic (branko) - 15 months ago 2024-09-09 15:34:34
branko@majic.rs
MAR-218: Update the get_url invocation to use the new checksum attribute.
1 file changed with 1 insertions and 1 deletions:
0 comments (0 inline, 0 general)
docs/usage.rst
Show inline comments
 
@@ -1368,385 +1368,385 @@ server.
 
     mysql
 

	
 
   Of course, no database has been created for either of the web applications,
 
   but we will get to that one later (there is a dedicated ``database`` role
 
   which can be combined with web app roles for this purpose).
 

	
 

	
 
Deploying a PHP web application (Nextcloud)
 
-----------------------------------------------
 

	
 
We have some basic infrastructure up and running on our web server, so
 
now we can move on to setting-up a PHP web application on it. As
 
mentioned before, we will roll-out *Nextcloud*.
 

	
 
For this we will create a local role in our site to take care of
 
it. This role will in turn utilise two roles coming from *Majic
 
Ansible Roles* that will make our life (a little) easier.
 

	
 
To make the example a bit simpler, no parameters will be introduced
 
for this role (not even the password for database) - we'll hard-code
 
everything.
 

	
 
Before we start, here is a couple of useful pointers regarding the
 
``php_website`` role:
 

	
 
* The role is designed to execute every application via dedicated user
 
  and group. The user/group name is automatically derived from the
 
  FQDN of website, for example ``web-nextcloud_example_com``.
 
* While running the application, application user's umask is set to
 
  ``0007`` (letting the administrator user be able to manage any files
 
  created while the application is running).
 
* An administrative user is created as well, and this user should be
 
  used when running maintenance and installation commands. Similar to
 
  application user, the name is also derived from the FQDN of website,
 
  for example ``admin-nextcloud_example_com``. Administrative user
 
  does not have a dedicated group, and instead belongs to same group
 
  as the application user.
 
* PHP applications are executed via FastCGI, using *PHP-FPM*.
 
* If you ever need to set some additional PHP FPM settings, this can
 
  easily be done via the ``additional_fpm_config`` role
 
  parameter. This particular example does not set any, though.
 
* Incoming request headers can be set/overridden using the
 
  ``http_header_overrides`` parameter. This can be useful for
 
  manipulating headers in specifics ways, such as disabling
 
  compression etc. on the application side.
 
* Mails delivered to local admin/application users are forwarded to
 
  ``root`` account (configurable via ``website_mail_recipients`` role
 
  parameter.
 
* If you ever find yourself mixing-up test and production websites,
 
  have a look at ``environment_indicator`` role parameter. It lets you
 
  insert small strip with environment information at bottom of each
 
  HTML page served by the web server. If the strip gets in the way, it
 
  can easily be collapsed using the arrows on the left side.
 
* Static content (non-PHP) is served directly by *Nginx*.
 
* Each web application gets distinct sub-directory under ``/var/www``,
 
  named after the FQDN. All sub-directories created under there are
 
  created with ``02750`` permissions, with ownership set to admin
 
  user, and group set to the application's group. In other words, all
 
  directories will have ``SGID`` bit set, allowing you to create
 
  files/directories that will have their group automatically set to
 
  the group of the parent directory.
 
* Files are served (both by *Nginx* and *PHP-FPM*) from sub-directory
 
  called ``htdocs`` (located in website directory). For example
 
  ``/var/www/nextcloud.example.com/htdocs/``. Normally, this can be a
 
  symlink to some other sub-directory within the website directory
 
  (useful for having multiple versions for easier downgrades etc).
 
* Combination of admin user membership in application group, ``SGID``
 
  permission, and the way ownership of sub-directories is set-up
 
  usually means that the administrator will be capable of managing
 
  application files, and application can be granted write permissions
 
  to a *minimum* of necessary files.
 

	
 
  .. warning::
 
     Just keep in mind that some file-management commands, like
 
     ``mv``, do *not* respect the ``SGID`` bit. In fact, I would
 
     recommend using ``cp`` when you deploy new files to the directory
 
     instead (don't simply move them from your home directory).
 

	
 
1. Start-off with creating the necessary directories for the new role::
 

	
 
     mkdir -p ~/mysite/roles/nextcloud/{tasks,meta,files}/
 

	
 
2. Let's set-up role dependencies, reusing some common roles to make
 
   our life easier.
 

	
 
   :file:`~/mysite/roles/nextcloud/meta/main.yml`
 
   ::
 

	
 
      ---
 

	
 
      dependencies:
 

	
 
        # Role helps us set-up Nginx virtual host for serving our app.
 
        - role: php_website
 

	
 
          # Name that will be bound to specific virtual host definition.
 
          fqdn: nextcloud.example.com
 

	
 
          # TLS key and certificate to use for the virtual host.
 
          https_tls_certificate: "{{ lookup('file', '~/mysite/tls/nextcloud.example.com_https.pem') }}"
 
          https_tls_key: "{{ lookup('file', '~/mysite/tls/nextcloud.example.com_https.key') }}"
 

	
 
          # Additional packages required for deploying and running Nextcloud.
 
          packages:
 
            - php-gd
 
            - php-json
 
            - php-mysql
 
            - php-curl
 
            - php-intl
 
            - php-mbstring
 
            - php-imagick
 
            - php-ldap
 
            - php-xml
 
            - php-zip
 
            - php-gmp
 
            - python3-pexpect
 
            - php-apcu
 
            - php-bcmath
 

	
 
          # Set-up URL rewrites for well-known URIs (see https://en.wikipedia.org/wiki/Well-known_URIs).
 
          rewrites:
 
            - '^/\.well-known/carddav /remote.php/dav/ permanent'
 
            - '^/\.well-known/caldav /remote.php/dav/ permanent'
 
            - '^/remote/(.*) /remote.php last'
 

	
 
          # Prevent specific files from ever being served by the web server (for security reasons etc).
 
          deny_files_regex:
 
            - '^/(build|tests|config|lib|3rdparty|templates|data)/'
 
            - '^/(?:\.|autotest|occ|issue|indie|db_|console)'
 

	
 
          # Custom regex defining what files shouled be processed via PHP
 
          # interpreter.
 
          php_file_regex: \.php(?:$|/)
 

	
 
          # Not necessarily needed, but in case you have a policy on uid/gid
 
          # usage, this is useful. Take note that the uid value is also used
 
          # for the application group (gid == uid).
 
          uid: 2000
 
          admin_uid: 3000
 

	
 
        # Role that sets up a new dedicated database for our web
 
        # application.
 
        - role: database
 

	
 
          # This is both the database name, _and_ name of the database user
 
          # that will be granted full privileges on the database.
 
          db_name: nextcloud
 

	
 
          # Password for user used for accessing the database. Take note
 
          # that the user can only login from localhost.
 
          db_password: nextcloud
 

	
 

	
 

	
 
3. Now for my favourite part again - creating private keys and
 
   certificates!  Why?  Because the ``php_website`` role requires a
 
   private key/certificate pair to be deployed. So... Moving on:
 

	
 
   1. Create new template for ``certtool``:
 

	
 
      :file:`~/mysite/tls/nextcloud.example.com_https.cfg`
 
      ::
 

	
 
         organization = "Example Inc."
 
         country = SE
 
         cn = "Example Inc. Cloud Service"
 
         expiration_days = 365
 
         dns_name = "nextcloud.example.com"
 
         tls_www_server
 
         signing_key
 
         encryption_key
 

	
 
   2. Create the keys and certificates for the application::
 

	
 
        certtool --sec-param normal --generate-privkey --outfile ~/mysite/tls/nextcloud.example.com_https.key
 
        certtool --generate-certificate --load-ca-privkey ~/mysite/tls/ca.key --load-ca-certificate ~/mysite/tls/ca.pem --template ~/mysite/tls/nextcloud.example.com_https.cfg --load-privkey ~/mysite/tls/nextcloud.example.com_https.key --outfile ~/mysite/tls/nextcloud.example.com_https.pem
 

	
 
4. Time to get our hands a bit more dirty... Up until now we didn't
 
   have to write custom tasks, but that ends now.
 

	
 
   :file:`~/mysite/roles/nextcloud/tasks/main.yml`
 
   ::
 

	
 
      ---
 

	
 
      # Deployment
 
      # ==========
 

	
 
      - name: Download the application archive
 
        ansible.builtin.get_url:
 
          url: "https://download.nextcloud.com/server/releases/nextcloud-29.0.4.tar.bz2"
 
          dest: "/var/www/nextcloud.example.com/nextcloud-29.0.4.tar.gz"
 
          sha256sum: "19c469e264b31ee80400f8396460854546569e88db4c15fc0854e192f96027eb"
 
          checksum: "sha256:19c469e264b31ee80400f8396460854546569e88db4c15fc0854e192f96027eb"
 
        become: yes
 
        become_user: admin-nextcloud_example_com
 

	
 
      - name: Unpack the application archive
 
        ansible.builtin.unarchive:
 
          src: "/var/www/nextcloud.example.com/nextcloud-29.0.4.tar.gz"
 
          dest: "/var/www/nextcloud.example.com/"
 
          copy: no
 
          creates: "/var/www/nextcloud.example.com/nextcloud"
 
        become: yes
 
        become_user: admin-nextcloud_example_com
 

	
 
      # Majic Ansible Roles currently only support utf8 encoding.
 
      - name: Disable opportunistic use of utf8mb4 on fresh installs
 
        ansible.builtin.lineinfile:
 
          dest: "/var/www/nextcloud.example.com/nextcloud/lib/private/Setup/MySQL.php"
 
          line: "{{ '\t\t\t' }}$this->config->setValue('mysql.utf8mb4', true);"
 
          state: absent
 

	
 
      - name: Allow application user to install and update applications
 
        ansible.builtin.file:
 
          path: "/var/www/nextcloud.example.com/nextcloud/apps"
 
          mode: g+w
 

	
 
      - name: Allow CLI tool to be run by the user and group
 
        ansible.builtin.file:
 
          path: "/var/www/nextcloud.example.com/nextcloud/occ"
 
          mode: u+x,g+x
 

	
 
      - name: Create directory for storing data
 
        ansible.builtin.file:
 
          path: "/var/www/nextcloud.example.com/data"
 
          state: directory
 
          mode: 02770
 
          owner: "admin-nextcloud_example_com"
 
          group: "web-nextcloud_example_com"
 

	
 
      - name: Create directory for storing configuration files
 
        ansible.builtin.file:
 
          path: "/var/www/nextcloud.example.com/nextcloud/config"
 
          state: directory
 
          mode: 02750
 
          owner: "admin-nextcloud_example_com"
 
          group: "web-nextcloud_example_com"
 

	
 
      - name: Create an empty log file if it does not exist
 
        ansible.builtin.copy:
 
          content: ""
 
          dest: "/var/www/nextcloud.example.com/data/nextcloud.log"
 
          force: no
 

	
 
      - name: Set-up log file permissions
 
        ansible.builtin.file:
 
          path: "/var/www/nextcloud.example.com/data/nextcloud.log"
 
          owner: "admin-nextcloud_example_com"
 
          group: "web-nextcloud_example_com"
 
          mode: 0660
 

	
 
      - name: Symlink the default path used by the web server for finding application files
 
        ansible.builtin.file:
 
          src: "/var/www/nextcloud.example.com/nextcloud"
 
          dest: "/var/www/nextcloud.example.com/htdocs"
 
          state: link
 
          owner: "admin-nextcloud_example_com"
 
          group: "web-nextcloud_example_com"
 
        notify:
 
          - Restart PHP-FPM
 

	
 

	
 
      # Installation
 
      # ============
 

	
 
      - name: Get application installation status
 
        ansible.builtin.command: "/var/www/nextcloud.example.com/nextcloud/occ status"
 
        become: yes
 
        become_user: "admin-nextcloud_example_com"
 
        register: nextcloud_status
 
        changed_when: False
 
        failed_when: False
 

	
 
      - name: Check if application is installed
 
        ansible.builtin.set_fact:
 
          nextcloud_installed: "{{ 'Nextcloud is not installed' not in nextcloud_status.stderr }}"
 

	
 
      - name: Deploy installation script
 
        ansible.builtin.copy:
 
          src: "install_nextcloud.py"
 
          dest: "/var/www/nextcloud.example.com/install_nextcloud.py"
 
          owner: "admin-nextcloud_example_com"
 
          group: "web-nextcloud_example_com"
 
          mode: 0700
 
        when: "not nextcloud_installed"
 

	
 
      - name: Install application
 
        ansible.builtin.command: "/var/www/nextcloud.example.com/install_nextcloud.py"
 
        become: yes
 
        become_user: "admin-nextcloud_example_com"
 
        when: "not nextcloud_installed"
 

	
 
      - name: Remove installation script
 
        ansible.builtin.file:
 
          path: "/var/www/nextcloud.example.com/install_nextcloud.py"
 
          state: absent
 

	
 
      - name: Fix data file permissions for application user/group
 
        ansible.builtin.file:
 
          path: "/var/www/nextcloud.example.com/data"
 
          mode: g+w
 
          recurse: yes
 
          follow: no
 

	
 
      - name: Deploy local configuration overrides
 
        ansible.builtin.copy:
 
          src: "local.config.php"
 
          dest: "/var/www/nextcloud.example.com/nextcloud/config/local.config.php"
 
          owner: "admin-nextcloud_example_com"
 
          group: "web-nextcloud_example_com"
 
          mode: 0640
 

	
 
5. Set-up files that are deployed by the role.
 

	
 
   :file:`~/mysite/roles/nextcloud/files/local.config.php`
 
   ::
 

	
 
      <?php
 
      $CONFIG = array (
 
        'config_is_read_only' => true,
 
        'instanceid' => 'suqw2cvca8sp',
 
        'trusted_domains' =>
 
          array (
 
            0 => 'nextcloud.example.com',
 
          ),
 
      );
 

	
 
   :file:`~/mysite/roles/nextcloud/files/install_nextcloud.py`
 
   ::
 

	
 
      #!/usr/bin/env python3
 

	
 
      import pexpect
 

	
 
      # Spawn the process.
 
      install_process = pexpect.spawnu('/var/www/nextcloud.example.com/nextcloud/occ',
 
                                       args = [ 'maintenance:install',
 
                                                '--database', 'mysql',
 
                                                '--database-name', 'nextcloud',
 
                                                '--database-user', 'nextcloud',
 
                                                '--database-host', 'localhost',
 
                                                '--database-port', '3306',
 
                                                '--admin-user', 'admin',
 
                                                '--data-dir', '/var/www/nextcloud.example.com/data'])
 

	
 
      # If we get EOF, we probably already installed application, and ran
 
      # into error at the end since no patterns matched.
 
      try:
 
          # Provide database password.
 
          install_process.expect(u'What is the password to access the database with user.*\?', timeout=10)
 
          install_process.sendline(u'nextcloud')
 

	
 
          # Provide administrator password.
 
          install_process.expect(u'What is the password you like to use for the admin account.*\?', timeout=10)
 
          install_process.sendline(u'admin')
 

	
 
          # Wait for application to finish.
 
          install_process.expect(pexpect.EOF, timeout=120)
 

	
 
      except pexpect.EOF as e:
 
          pass
 

	
 
      # Print command output. Has to be done prior to final wait for
 
      # pexpect.EOF.
 
      print(install_process.before.encode('utf-8'))
 

	
 
      # Close application. Additional wait for pexpect.EOF prevents the
 
      # process from getting killed prematurely in case it exits
 
      # immediatelly (due to wrong command line arguments etc). Some
 
      # background information can be found at (although it is a very old
 
      # post):
 
      #
 
      # https://www.heikkitoivonen.net/blog/2009/01/28/pexpect-and-inconsistent-exit-status/
 
      #
 
      install_process.expect(pexpect.EOF)
 
      install_process.close()
 

	
 
      # Return same exit code like child process.
 
      exit(install_process.exitstatus)
 

	
 
6. And... Let's add the new role to our web server.
 

	
 
   :file:`~/mysite/playbooks/web.yml`
 
   ::
 

	
0 comments (0 inline, 0 general)