Cloud-init for DHIS2

Cloud init

clould init is a technology used by cloud provider to setup instance

the config below is the “user data” that will customize an Ubuntu image to a DHIS2 image

https://cloud-init.io/

what it does

  • install tomcat 9
  • install postgresql 14
  • create 2 databases dhis2_dev and dhis2 and add the postgis extension
  • setup a daily backup of the dhis2 database but keep 1 per day for 14 day, 1 per week for 60 day, 1 per month for 1 year then 1 per year
  • generate a SSL certificate with lets ’ Encrypt (certbot), the DNS record must be set within 5 min after the first boot on the instance
  • configure Tomcat and postgres memory based on availability (~45/50)
  • create 2 dhis config folder in /home/dhis/config and /home/dhis/config-dev
  • configure the tomcat context so https://PROJ_NAME.DOMAIN leads to prod and https://PROJ_NAME.DOMAIN/dev to dev server

limitations

  • both dhis2 instances use the same postgres and tomcat server (on purpose to not “block” resource for dev)
  • no backup of dev
  • if certbot fails then one must generate the cert manually (without going through nginx which won’t start because of the missing certs)

Utilization

update the content of

  • /etc/environment in the write_files section, especially the PROJ_NAME, DOMAIN, MAILADDRESS, DHIS_DB_PW
  • replace in the user/ssh-authorized-keys section with you RSA key, if you remove the ssh-authorized-keys section you must add a password and set ssh_pwauth: yes

other

on my version I have also a command line like

  • su - dhis -c “ssh-import-id-gh delcroip”

To add my key from github, I removed it to avoid getting access to server of people forgetting to remove that line after a copy paste …

user-data

#cloud-config
hostname: mydomain
timezone: UTC
write_files:
  - path: /etc/environment
    permissions: '0644'
    content: |
      PROJ_NAME=subdomain
      DOMAIN=domain
      MAILADDRESS=YourAdress
      DHIS_VERSION=2.39.0.1
      POSTGRES_VERSION=14
      DHIS_DB_PW=your pass
      DHIS_DB_USER=dhis
      DHIS_DB_NAME=dhis2
      DHIS_DB_NAME_DEV=dhis2_dev
      DHIS_HOME=/home/dhis/config
      DHIS_HOME_DEV=/home/dhis/config-dev
      CATALINA_HOME=/usr/share/tomcat9
      CATALINA_BASE=/var/lib/tomcat9
      BACKUP_SCRIPT=/opt/database_backup.sh
      BACKUP_DIR=/var/backups/dhis2
      TOMCAT_CONTEXT=/var/lib/tomcat9/conf/Catalina/localhost
  - path: /etc/cron.d/dhis
    owner: root:root
    content: |
        0 7 * * 0 /bin/bash /home/dhis/clean_pg_backups.sh
        0 1 * * 0 certbot renew --post-hook "systemctl reload nginx"
        0 1 * * * DATE_TIME=`date +%F-%H%M%S`; BACKUPFILENAME="${BACKUP_DIR}/backup-${PROJ_NAME}-v${DHIS_VERSION}-${DATE_TIME}.sql.gz"; pg_dump -T aggregated* -T analytics* -T completeness* -T _* -O $DHIS_DB_NAME | gzip > $BACKUPFILENAME
  - path: /var/set-env.sh
    owner: root
    permissions: '0777'
    content: |
      #!/bin/bash
      export MEMORY=$(( $(awk '/MemTotal:/ { print $2 }' /proc/meminfo)/(1024)  ))
      export JAVA_MAX=$(( $(awk '/MemTotal:/ { print $2 }' /proc/meminfo) *2 / (1024 *5)))
      export JAVA_MIN=$(( $(awk '/MemTotal:/ { print $2 }' /proc/meminfo) / (1024 * 4) ))
      export JAVA_OPTS="-Djava.security.egd=file:/dev/./urandom -Xmx${JAVA_MAX}m -Xms${JAVA_MIN}m"
      export PSQL_MEM_WORK=$(( $(awk '/MemTotal:/ { print $2 }' /proc/meminfo) / (1024 * 4) ))
      export PSQL_MEM_CACHE=$(( $(awk '/MemTotal:/ { print $2 }' /proc/meminfo) / (1024 * 8) ))
      export PSQL_MEM_BUFF=$(( $(awk '/MemTotal:/ { print $2 }' /proc/meminfo) / (1024 * 8) ))
  - path: /tmp/set-dhis2.sh
    permissions: '0777'
    content: |
        #!/bin/bash
        su - postgres -c "psql -c \"CREATE ROLE $D2_DB_USER WITH LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE PASSWORD '$D2_DB_PW';\" -q"
        su - postgres -c "psql -U postgres -c 'CREATE DATABASE $D2_DB_NAME OWNER $D2_DB_USER;'"
        su - postgres -c "psql -U postgres -d $D2_DB_NAME -c 'CREATE EXTENSION IF NOT EXISTS postgis;'"
        mkdir -p $D2_HOME; envsubst < /tmp/dhis.conf.tpl > $D2_HOME/dhis.conf ; chown dhis:tomcat $D2_HOME -R; chmod g+w $D2_HOME -R
  - path: /tmp/context.xml.tpl
    permissions: '0644'
    content: |
        <Context>
          <Environment name="dhis2-home" value="$D2_HOME"
                 type="java.lang.String" override="false"/>
        </Context>

  - path: /home/dhis/clean_pg_backups.sh
    permissions: '0744'
    content: | 
        #!/bin/bash

        for file in $BACKUP_DIR/*
        do
            path=$BACKUP_DIR$(basename $file)
            created_date='date -r "$path"'

            year=$( date -r "$path"  '+%Y')
            month=$(date -r "$path" +"%B")
            month_day=$( date -r "$path" +"%d")
            week_day=$(date -r "$path" +"%u")
            year_day=$(date -r "$path"  +"%j")

            data_diff=$(( ( $(date +"%s") - $(date -r "$path" +"%s")) / (60*60*24) ))
            # keep 2 weeks of logs everyday
            if [ "$data_diff" -gt 14 ] ;then
                if [ "$data_diff" -lt 60 ] ;then
                    if [ "$week_day" -ne 1 ] ; then
                        rm $file -f
                    fi
                else
                    if [ "$data_diff" -lt 360 ];then
                        if [ "$month_day" -ne 1 ] ; then
                            rm $file -f
                        fi
                    else
                        if [ "$year_day" -ne 1 ] ; then
                            rm $file -f
                        fi
                    fi
                fi
            fi
        done
  - path: /tmp/pg_conf/postgresql.conf.tpl
    permissions: '0644'
    content: |
        # dhis2 tuning https://postgresqlco.nf/doc/en
        synchronous_commit = off
        max_connections = 200
        # Total RAM * 0.20
        shared_buffers = ${PSQL_MEM_BUFF}MB
        # Total RAM * 0.25
        effective_cache_size = ${PSQL_MEM_CACHE}MB
        # Total RAM * 0.5
        work_mem = ${PSQL_MEM_WORK}MB
        #Total RAM * 0.05
        maintenance_work_mem = 256MB
        # WRITE-AHEAD LOG
        wal_writer_delay = 10000ms
        wal_buffers = 16MB
        checkpoint_completion_target = 0.8
        default_statistics_target = 100
  - path: /tmp/pg_conf/pg_ident.conf
    permissions: '0644'
    content: | 
      # MAPNAME      SYSTEM-USERNAME    PG-USERNAME
      local_users    postgres           postgres
      local_users    root               postgres
      local_users    root               dhis
  - path: /tmp/pg_conf/pg_hba.conf
    permissions: '0644'    ssh-authorized-keys:
    content: |
      # Allow root to login as postgres or dhis DB user without the need of entering a password.
      local   all             all                                     peer map=local_users
      host    all             all             127.0.0.1/32            md5
      host    all             all             ::1/128                 md5
  - path: /tmp/dhis.conf.tpl
    permissions: '0644'
    content: |
        connection.dialect = org.hibernate.dialect.PostgreSQLDialect
        connection.driver_class = org.postgresql.Driver
        connection.url = jdbc:postgresql:$D2_DB_NAME
        connection.username = $D2_DB_USER
        connection.password = $D2_DB_PW
        connection.schema = update
  - path: /tmp/nginx.conf.tpl
    permissions: '0644'
    content: |
        user www-data;
        worker_processes auto;
        pid /run/nginx.pid;
        events {
          worker_connections 1024;
        }
        http {
          proxy_cache_path  /var/cache/nginx  levels=1:2  keys_zone=dhis:250m  inactive=1d;
          root          /var/lib/tomcat9/webapps/ROOT;
          include       mime.types;
          default_type  application/octet-stream;
          gzip on; # Enables compression, incl Web API content-types
          gzip_types
            "application/json;charset=utf-8" application/json
            "application/javascript;charset=utf-8" application/javascript text/javascript
            "application/xml;charset=utf-8" application/xml text/xml
            "text/css;charset=utf-8" text/css
            "text/plain;charset=utf-8" text/plain;
          server {
            listen 80 default_server;
            listen [::]:80 default_server;
            # Redirect all HTTP requests to HTTPS with a 301 Moved Permanently response.
            return 301 https://$host$request_uri;
            # enable Let's Encrypt Renewal
            location ~ /.well-known {
              allow all;
            }
          }
          # HTTPS server
          server {
            server_name ${PROJ_NAME}.${DOMAIN};
            listen      443 ssl;
            listen [::]:443 ssl;
            # File Max Upload size
            client_max_body_size 10M;
            ssl_certificate      /etc/letsencrypt/live/${PROJ_NAME}.${DOMAIN}/fullchain.pem;
            ssl_certificate_key  /etc/letsencrypt/live/${PROJ_NAME}.${DOMAIN}/privkey.pem;
            ssl_session_cache    shared:SSL:20m;
            ssl_session_timeout  10m;
            ssl_protocols              TLSv1 TLSv1.1 TLSv1.2;
            ssl_ciphers                RC4:HIGH:!aNULL:!MD5;
            ssl_prefer_server_ciphers  on;
            ssl_dhparam /etc/ssl/certs/dhparam.pem;
            #add_header  X-Frame-Options DENY;
            # Disallow Search Engine Crawling
            location = /robots.txt {
              add_header  Content-Type  text/plain;
              return 200 "User-agent: *\nDisallow: /\n";
            }
            # Proxy pass to servlet container
            location / {
              proxy_pass                http://localhost:8080/;
              proxy_redirect            off;
              proxy_set_header          Host             $host;
              proxy_set_header          X-Real-IP          $remote_addr;
              proxy_set_header          X-Forwarded-For    $proxy_add_x_forwarded_for;
              proxy_set_header          X-Forwarded-Proto  https;
              proxy_buffer_size         128k;
              proxy_buffers             8 128k;
              proxy_busy_buffers_size   256k;
              proxy_cookie_path         ~*^/(.*) "/$1; SameSite=Lax";
              proxy_cache               dhis;
            }
          }
        }
#--------------------
users:
  - name: dhis
    groups: tomcat
    shell: /bin/bash
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
    ssh-authorized-keys:
      - <YOURKEY>
locale: en_US.UTF-8
manage_etc_hosts: true
packages:
  - openjdk-11-jre
  - software-properties-common
  - unzip
  - nginx
  - fail2ban
  - htop
  - tomcat9
  - python3-certbot-nginx
  - certbot
  - postgresql-14
  - postgresql-14-postgis-3 
  - postgresql-client
package_update: true
package_upgrade: true
ssh_pwauth: no
disable_root: true
package_reboot_if_required: true
runcmd:
  - set -a; . /etc/environment; set +a; source /var/set-env.sh
  #- sed -i "/#\$nrconf{restart} = 'i';/s/.*/\$nrconf{restart} = 'a';/" /etc/needrestart/needrestart.conf
  #- apt-get install -y  postgresql-14 postgresql-14-postgis-3 postgresql-client
  - MAIN_DHIS_VERSION=$(echo $DHIS_VERSION | cut -c1-4); wget  https://releases.dhis2.org/${MAIN_DHIS_VERSION}/dhis2-stable-${DHIS_VERSION}.war -O $CATALINA_BASE/webapps/ROOT.war
  - cp $CATALINA_BASE/webapps/ROOT.war $CATALINA_BASE/webapps/dev.war
  - sed -i 's/Connector port="8080"/& relaxedQueryChars = "[]|{}^\&#x5c;\&#x60;\&quot;\&lt;\&gt;"/' /etc/tomcat9/server.xml
  # MAin DB
  - D2_HOME=$DHIS_HOME D2_DB_NAME=$DHIS_DB_NAME D2_DB_USER=$DHIS_DB_USER D2_DB_PW=$DHIS_DB_PW /tmp/set-dhis2.sh
  - D2_HOME=$DHIS_HOME envsubst < /tmp/context.xml.tpl > $TOMCAT_CONTEXT/ROOT.xml && chown tomcat:tomcat $TOMCAT_CONTEXT/ROOT.xml
  # dev db
  - D2_HOME=$DHIS_HOME_DEV D2_DB_NAME=$DHIS_DB_NAME_DEV D2_DB_USER=$DHIS_DB_USER D2_DB_PW=$DHIS_DB_PW /tmp/set-dhis2.sh
  - D2_HOME=$DHIS_HOME_DEV envsubst < /tmp/context.xml.tpl > $TOMCAT_CONTEXT/dev.xml && chown tomcat:tomcat $TOMCAT_CONTEXT/dev.xml
  #cetbot
  - sleep 360; certbot --nginx --email $MAILADDRESS -d "${PROJ_NAME}.${DOMAIN}" --non-interactive --agree-tos
  # configure postgres
  - if [ ! -f /etc/ssl/certs/dhparam.pem ]; then  openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048; fi
  - su - postgres -c "source /var/set-env.sh; envsubst < /tmp/pg_conf/postgresql.conf.tpl >> /etc/postgresql/$POSTGRES_VERSION/main/postgresql.conf"
  - su - postgres -c "cp /tmp/pg_conf/pg_* /etc/postgresql/$POSTGRES_VERSION/main"
  #config and restart nginx
  - envsubst '${DOMAIN} ${PROJ_NAME}'  < /tmp/nginx.conf.tpl > /etc/nginx/nginx.conf && chown www-data:www-data /etc/nginx/nginx.conf
  - rm -rf $CATALINA_BASE/webapps/ROOT && service nginx reload


2 Likes

Thank you so much for sharing with the community! Would you please add more description to what it is and why it’s useful? Where are the instances deployed?

Thanks!

is it better ?

1 Like

Lots of useful information! Thanks a lot for the update! :pray: