├── .ansible-lint ├── .github └── workflows │ └── molecule.yml ├── .gitignore ├── .yamllint ├── LICENSE ├── README.md ├── Vagrantfile ├── ansible.cfg ├── dbservers.yml ├── development ├── docker └── Dockerfile ├── group_vars ├── development │ └── vars.yml └── vagrant │ └── vars.yml ├── molecule └── default │ ├── INSTALL.rst │ ├── converge.yml │ ├── molecule.yml │ └── tests │ └── test_default.py ├── requirements-dev.txt ├── requirements.txt ├── roles ├── avahi │ └── tasks │ │ └── main.yml ├── base │ ├── defaults │ │ └── main.yml │ └── tasks │ │ ├── create_swap_file.yml │ │ └── main.yml ├── celery │ ├── handlers │ │ └── main.yml │ ├── tasks │ │ ├── copy_scripts.yml │ │ ├── main.yml │ │ └── setup_supervisor.yml │ ├── templates │ │ ├── celery_beat_start.j2 │ │ ├── celery_start.j2 │ │ ├── supervisor_celery.conf.j2 │ │ └── supervisor_celery_beat.conf.j2 │ └── vars │ │ └── main.yml ├── certbot │ ├── defaults │ │ └── main.yml │ └── tasks │ │ └── main.yml ├── db │ ├── handlers │ │ └── main.yml │ └── tasks │ │ └── main.yml ├── memcached │ ├── handlers │ │ └── main.yml │ ├── tasks │ │ └── main.yml │ ├── templates │ │ └── memcached.conf.j2 │ └── vars │ │ └── main.yml ├── nginx │ ├── defaults │ │ └── main.yml │ ├── handlers │ │ └── main.yml │ ├── tasks │ │ └── main.yml │ └── templates │ │ └── django_default_project.j2 ├── rabbitmq │ ├── handlers │ │ └── main.yml │ ├── tasks │ │ ├── main.yml │ │ ├── setup_users.yml │ │ └── setup_vhosts.yml │ └── vars │ │ └── main.yml ├── security │ ├── defaults │ │ └── main.yml │ ├── files │ │ └── apt_periodic │ ├── handlers │ │ └── main.yml │ ├── tasks │ │ ├── create_non_root_sudo_user.yml │ │ ├── force_ssh_authentication.yml │ │ ├── main.yml │ │ ├── perform_aptitude_dist_upgrade.yml │ │ ├── setup_fail2ban.yml │ │ ├── setup_unattended_upgrades.yml │ │ └── setup_uncomplicated_firewall.yml │ └── vars │ │ └── main.yml └── web │ ├── defaults │ └── main.yml │ ├── handlers │ └── main.yml │ ├── tasks │ ├── create_users_and_groups.yml │ ├── install_additional_packages.yml │ ├── main.yml │ ├── set_file_permissions.yml │ ├── setup_django_app.yml │ ├── setup_git_repo.yml │ ├── setup_supervisor.yml │ └── setup_virtualenv.yml │ └── templates │ ├── gunicorn_start.j2 │ ├── maintenance_off.html │ ├── supervisor_config.j2 │ └── virtualenv_postactivate.j2 ├── security.yml ├── site.yml ├── vagrant.yml └── webservers.yml /.ansible-lint: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | skip_list: 4 | - ANSIBLE0006 5 | - ANSIBLE0012 6 | # TODO: Remove free-form from the repo. 7 | - no-free-form 8 | - name[missing] 9 | # TODO: Fix this. 10 | - name[casing] 11 | # TODO: Test removing this from each task and see if there is a workaround. 12 | - no-changed-when 13 | -------------------------------------------------------------------------------- /.github/workflows/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Molecule Test 3 | on: 4 | pull_request: 5 | schedule: 6 | - cron: '30 5 * * 0' 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.ref }} 9 | cancel-in-progress: true 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: true 15 | max-parallel: 4 16 | matrix: 17 | python-version: ["3.12", "3.13"] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | cache: pip 26 | - name: Install dependencies 27 | run: | 28 | python3 -m pip install --upgrade pip 29 | python3 -m pip install -r requirements-dev.txt 30 | - name: Test with molecule 31 | run: | 32 | molecule test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # PyCharm IDE 2 | .idea 3 | 4 | # Vagrant 5 | .vagrant 6 | *.log 7 | vagrant_ansible_inventory_default 8 | 9 | # Ansible 10 | *.retry 11 | 12 | # Python 13 | *.pyc 14 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | extends: default 4 | 5 | rules: 6 | braces: 7 | max-spaces-inside: 1 8 | level: error 9 | brackets: 10 | max-spaces-inside: 1 11 | level: error 12 | line-length: disable 13 | # NOTE(retr0h): Templates no longer fail this lint rule. 14 | # Uncomment if running old Molecule templates. 15 | # truthy: disable 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jonathan Calazan and individual contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible Django Stack 2 | 3 | ![Build Status](https://github.com/jcalazan/ansible-django-stack/actions/workflows/molecule.yml/badge.svg?branch=master) 4 | 5 | This is a complete Ansible playbook that will deploy a Django application to a 6 | server. It includes roles for setting up a PostgreSQL database, Nginx web 7 | server, Gunicorn application server, and Celery task queue. 8 | 9 | ## Requirements 10 | 11 | Ansible Playbook designed for environments running a Django app. 12 | It can install and configure these applications that are commonly used in 13 | production Django deployments: 14 | 15 | - Nginx 16 | - Gunicorn 17 | - PostgreSQL 18 | - Supervisor 19 | - Virtualenv 20 | - Memcached 21 | - Celery 22 | - RabbitMQ 23 | 24 | Default settings are stored in `roles/role_name/defaults/main.yml`. 25 | Environment-specific settings are in the `group_vars` directory. 26 | 27 | A `certbot` role is also included for automatically generating and renewing 28 | trusted SSL certificates with [Let's Encrypt][lets-encrypt]. 29 | 30 | **Tested with OS:** Ubuntu 24.04 LTS (64-bit), Ubuntu 22.04 LTS (64-bit). 31 | 32 | **Tested with Cloud Providers:** [Digital Ocean][digital-ocean], [AWS][aws], [Rackspace][rackspace] 33 | 34 | ## Python Requirements 35 | 36 | This project requires **Python 3.12 or 3.13**. Earlier versions of Python are no longer supported. 37 | 38 | ## Getting Started 39 | 40 | A quick way to get started is with Vagrant. 41 | 42 | ### Requirements 43 | 44 | - [Ansible][ansible-installation_guide] 45 | - [Vagrant][vagrant-downloads] 46 | - [VirtualBox][virtual-box_downloads] or [Docker][docker-get_started] 47 | 48 | It's recommended to use the version of Ansible specified in `requirements.txt`, 49 | although any version greater than Ansible 2.7 will work with this repository. 50 | When choosing an Ansible version, consider: 51 | 52 | - Ansible only issues security fixes for the [last three major releases][ansible-release_cycle]. 53 | - The included version of `molecule` has requirements on the Ansible version 54 | (currently, Molecule requires Ansible 2.5 or later and the 2.23 release 55 | will require Ansible 2.7 or greater) 56 | 57 | Ansible has been configured to use Python 3 inside the remote machine when 58 | provisioning it. In Ubuntu 16.04 LTS, compatible Ansible versions are not 59 | in the main package repositories, but can be installed from the Ansible PPA by 60 | running these commands: 61 | 62 | ``` 63 | sudo add-apt-repository ppa:ansible/ansible 64 | sudo apt-get update 65 | ``` 66 | 67 | ### Configuring your application 68 | 69 | The main settings to change are in the `group_vars/[environment_name]/vars.yml` 70 | file, where you can configure the location of your Git project, the project 71 | name, and the application name which will be used throughout the Ansible 72 | configuration. 73 | 74 | Note that the default values in the playbooks assume that your project 75 | structure looks something like this: 76 | 77 | ``` 78 | myproject 79 | ├── manage.py 80 | ├── myapp 81 | │ ├── apps 82 | │ │ └── __init__.py 83 | │ ├── __init__.py 84 | │ ├── settings 85 | │ │ ├── base.py 86 | │ │ ├── __init__.py 87 | │ │ ├── local.py 88 | │ │ └── production.py 89 | │ ├── templates 90 | │ │ ├── 403.html 91 | │ │ ├── 404.html 92 | │ │ ├── 500.html 93 | │ │ └── base.html 94 | │ ├── urls.py 95 | │ └── wsgi.py 96 | ├── README.md 97 | └── requirements.txt 98 | ``` 99 | 100 | The main things to note are the locations of the `manage.py` and `wsgi.py` 101 | files. If your project's structure is a little different, you may need to 102 | change the values in these 2 files: 103 | 104 | - `roles/web/tasks/setup_django_app.yml` 105 | - `roles/web/templates/gunicorn_start.j2` 106 | 107 | Also, if your app needs additional system packages installed, you can add them 108 | in `roles/web/tasks/install_additional_packages.yml`. 109 | 110 | ### Creating the machine 111 | 112 | Type this command from the project root directory: 113 | 114 | ``` 115 | vagrant up 116 | ``` 117 | 118 | (To use Docker instead of VirtualBox, add the flag `--provider=docker` to the 119 | command above. Note that extra configuration may be required first on your host 120 | for Docker to run systemd in a container.) 121 | 122 | Wait a few minutes for the magic to happen. Access the app by going to this 123 | URL: [https://my-cool-app.local](https://my-cool-app.local) 124 | 125 | Yup, exactly, you just provisioned a completely new server and deployed an 126 | entire Django stack in 5 minutes with _two words_ :). 127 | 128 | ### Additional vagrant commands 129 | 130 | **SSH to the box** 131 | 132 | ``` 133 | vagrant ssh 134 | ``` 135 | 136 | **Re-provision the box to apply the changes you made to the Ansible configuration** 137 | 138 | ``` 139 | vagrant provision 140 | ``` 141 | 142 | **Reboot the box** 143 | 144 | ``` 145 | vagrant reload 146 | ``` 147 | 148 | **Shutdown the box** 149 | 150 | ``` 151 | vagrant halt 152 | ``` 153 | 154 | ## Pulling from a private git repository using SSH agent forwarding 155 | 156 | If your code is in a private repository you must use an SSH connection along 157 | with a key so ansible can checkout the code. HTTPS connections and SSH 158 | connections with a username and password do not work because ansible cannot 159 | deal with interactive logins. 160 | 161 | Using SSH agent forwarding we can get the authentication request from the 162 | repository sent to the machine where the playbook is run. That keeps the 163 | private key to the repository as safe as possible. To set this up you need to: 164 | 165 | - Add a public key to the server hosting your repo. 166 | - Make sure ssh-agent is running on your local machine 167 | - Add the public key to ssh-agent 168 | 169 | [Connecting to GitHub with SSH](https://help.github.com/articles/connecting-to-github-with-ssh/) 170 | has all the information on key generation, adding keys to the server, setting up ssh-agent and 171 | troubleshooting any problems. 172 | 173 | Your server SSH configuration should work out-of-the-box. The "Server-Side 174 | Configuration Options" section in [SSH Essentials: Working with SSH Servers, Clients, and 175 | Keys](https://www.digitalocean.com/community/tutorials/ssh-essentials-working-with-ssh-servers-clients-and-keys) 176 | has good advice on locking down who can access the server over SSH 177 | connections. 178 | 179 | ### Getting the playbook to use agent forwarding 180 | 181 | The first thing you need to do is set the ssh_agent_forwarding flag in 182 | env_vars/base.yml to `true`: 183 | ``` 184 | ssh_forward_agent: true 185 | ``` 186 | 187 | This flag is used when configuring sudoers so that any user you become on the 188 | remote server will also use the same socket connection when requesting to 189 | unlock keys. 190 | 191 | To enable SSH agent forwarding on the Vagrant box, change the following flag in 192 | VagrantFile and set it to `true`: 193 | ``` 194 | config.ssh.forward_agent = true 195 | ``` 196 | 197 | When running a playbook to provision a server, you enable SSH agent forwarding 198 | using the `--ssh-extra-args` option on the command line: 199 | ``` 200 | ansible-playbook --ssh-extra-args=-A -i production site.yml 201 | ``` 202 | 203 | This is a little bit clunky but it does not restrict you from setting other 204 | SSH options if you need to. 205 | 206 | ## Security 207 | 208 | *NOTE: Do not run the Security role without understanding what it does. 209 | Improper configuration could lock you out of your machine.* 210 | 211 | **Security role tasks** 212 | 213 | The security module performs several basic server hardening tasks. Inspired by 214 | [this blog post][securing-ubuntu]: 215 | 216 | - Updates `apt` 217 | - Performs `aptitude safe-upgrade` 218 | - Adds a user specified by the `server_user` variable, found in `roles/base/defaults/main.yml` 219 | - Adds authorized key for the new user 220 | - Installs sudo and adds the new user to sudoers with the password specified by 221 | the `server_user_password` variable found in `roles/security/defaults/main.yml` 222 | - Installs and configures various security packages: 223 | - [Unattended upgrades](https://help.ubuntu.com/lts/serverguide/automatic-updates.html) 224 | - [Uncomplicated Firewall](https://wiki.ubuntu.com/UncomplicatedFirewall) 225 | - [Fail2ban](http://www.fail2ban.org/) (NOTE: Fail2ban is disabled by default 226 | as it is the most likely to lock you out of your server. Handle with care!) 227 | - Restricts connection to the server to SSH and http(s) ports 228 | - Limits `su` access to the sudo group 229 | - Disallows password authentication (be careful!) 230 | - Disallows root SSH access (you will only SSH to your machine as your new user 231 | and use a password for `sudo` access) 232 | - Restricts SSH access to the new user specified by the `server_user` variable 233 | - Deletes the `root` password 234 | 235 | **Security role configuration** 236 | 237 | - Change the `server_user` from `root` to something else in `roles/base/defaults/main.yml` 238 | - Change the sudo password in `group_vars/[environment_name]/vars.yml` 239 | - Change variables in `./roles/security/vars/` per your desired configuration 240 | by overriding them in `group_vars/[environment_name]/vars.yml` 241 | 242 | **Running the Security role** 243 | 244 | - The security role can be run by running `security.yml` via: 245 | 246 | ``` 247 | ansible-playbook -i development security.yml 248 | ``` 249 | 250 | ## Running the Ansible Playbook to provision servers 251 | 252 | **NOTE:** to enable the Security module you can use the steps above prior to 253 | following the steps below. 254 | 255 | Create an inventory file for the environment, for example: 256 | 257 | ``` 258 | # development 259 | 260 | [webservers] 261 | webserver1.example.com 262 | webserver2.example.com 263 | 264 | [dbservers] 265 | dbserver1.example.com 266 | ``` 267 | 268 | Next, create a playbook for the server type. See 269 | [webservers.yml](webservers.yml) for an example. 270 | 271 | Run the playbook: 272 | 273 | ``` 274 | ansible-playbook -i development webservers.yml [-K] 275 | ``` 276 | 277 | You can also provision an entire site by combining multiple playbooks. For 278 | example, I created a playbook called `site.yml` that includes both the 279 | `webservers.yml` and `dbservers.yml` playbook. 280 | 281 | A few notes here: 282 | 283 | - The `dbservers.yml` playbook will only provision servers in the `[dbservers]` 284 | section of the inventory file. 285 | - The `webservers.yml` playbook will only provision servers in the 286 | `[webservers]` section of the inventory file. 287 | - The `-K` flag is for adding the sudo password you created for a new sudoer in 288 | the Security role (if applicable) 289 | 290 | You can then provision the entire site with this command: 291 | 292 | ``` 293 | ansible-playbook -i development site.yml [-K] 294 | ``` 295 | 296 | If you're testing with vagrant, you can use this command: 297 | 298 | ``` 299 | ansible-playbook -i vagrant_ansible_inventory_default --private-key=~/.vagrant.d/insecure_private_key vagrant.yml [-K] 300 | ``` 301 | 302 | ## Using Ansible for Django Deployments 303 | 304 | When doing deployments, you can simply use the `--tags` option to only run 305 | those tasks with these tags. 306 | 307 | For example, you can add the tag `deploy` to certain tasks that you want to 308 | execute as part of your deployment process and then run this command: 309 | 310 | ``` 311 | ansible-playbook -i stage webservers.yml --tags="deploy" 312 | ``` 313 | 314 | This repo already has `deploy` tags specified for tasks that are likely needed 315 | to run during deployment in most Django environments. 316 | 317 | ## Advanced Options 318 | 319 | ### Changing the Ubuntu release 320 | 321 | The [Vagrantfile](Vagrantfile) uses the Ubuntu 22.04 LTS Vagrant box for a 322 | 64-bit PC that is published by Canonical in HashiCorp Atlas. To use Ubuntu 323 | 24.04 LTS instead, change the `config.vm.box` setting to `ubuntu/noble64`. 324 | 325 | ### Changing the Python version used by your application 326 | 327 | This project supports Python 3.12 and 3.13. Python 3.12 is used by default in the `virtualenv`. 328 | To use Python 3.13 instead, override the `virtualenv_python_version` variable and set it to `python3.13`. 329 | 330 | It is possible to install other versions of Python from an 331 | [unofficial PPA by Felix Krull (see disclaimer)][deadsnakes]. 332 | To use this PPA, override the `enable_deadsnakes_ppa` variable and set it to 333 | `yes`. Then the `virtualenv_python_version` variable can be set to the name of 334 | a Python package from this PPA, such as `python3.12` or `python3.13`. 335 | 336 | **Note:** Only Python 3.12 and 3.13 are officially supported and tested. 337 | 338 | ### Changing the Python version used by Ansible 339 | 340 | To use Python 2 as the interpreter for Ansible, override the 341 | `ansible_python_interpreter` variable and set it to `/usr/bin/python`. This 342 | allows a machine without Python 3 to be provisioned. 343 | 344 | ### Creating a swap file 345 | 346 | By default, the playbook won't create a swap file. To create/enable swap, 347 | simply change the values in [roles/base/defaults/main.yml](roles/base/defaults/main.yml). 348 | 349 | You can also override these values in the main playbook, for example: 350 | 351 | ``` 352 | --- 353 | 354 | ... 355 | 356 | roles: 357 | - { role: base, create_swap_file: true, swap_file_size_kb: 1024 } 358 | - db 359 | - rabbitmq 360 | - web 361 | - celery 362 | ``` 363 | 364 | This will create and mount a 1GB swap. Note that block size is 1024, so the 365 | size of the swap file will be 1024 x `swap_file_size_kb`. 366 | 367 | ### Automatically generating and renewing Let's Encrypt SSL certificates with the certbot client 368 | 369 | A `certbot` role has been added to automatically install the `certbot` client 370 | and generate a Let's Encrypt SSL certificate. 371 | 372 | **Requirements:** 373 | 374 | - A DNS "A" or "CNAME" record must exist for the host to issue the certificate to. 375 | - The `--standalone` option is being used, so port 80 or 443 must not be in use 376 | (the playbook will automatically check if Nginx is installed and will stop 377 | and start the service automatically). 378 | 379 | In `roles/nginx/defaults.main.yml`, you're going to want to override the 380 | `nginx_use_letsencrypt` variable and set it to yes/true to reference the Let's 381 | Encrypt certificate and key in the Nginx template. 382 | 383 | In `roles/certbot/defaults/main.yml`, you may want to override the 384 | `certbot_admin_email` variable. 385 | 386 | A cron job to automatically renew the certificate will run daily. Note that if 387 | a certificate is due for renewal (expiring in less than 30 days), Nginx will be 388 | stopped before the certificate can be renewed and then started again once 389 | renewal is finished. Otherwise, nothing will happen so it's safe to leave it 390 | running daily. 391 | 392 | ### Maintenance mode 393 | 394 | The playbook contains a maintenance page option. 395 | [`roles/web/templates/maintenance_off.html`](roles/web/templates/maintenance_off.html) 396 | is the provided maintenance template. To activate the maintenance mode, you can rename 397 | the template to 398 | `maintenance_on.html`, in order for `nginx` to serve it. This can be done manually. Alternately, 399 | you can include in the playbook a step activating the maintenance page (using the renaming 400 | process) while the site requires downtime. Then switch back to running mode in 401 | the playbook when operations requiring downtime are completed. 402 | 403 | ## Useful Links 404 | 405 | - [Ansible - Getting Started][ansible-installation_guide] 406 | - [Ansible - Best Practices][ansible-best_practices] 407 | - [Setting up Django with Nginx, Gunicorn, virtualenv, supervisor and PostgreSQL][michal-ansible_guide] 408 | - [How to deploy encrypted copies of your SSL keys and other files with Ansible and OpenSSL][deploy-encrypted-copies] 409 | - [Using SSH agent forwarding - GitHub developer guide](https://developer.github.com/guides/using-ssh-agent-forwarding/) 410 | - Jeff Geerling has some incredible resources for learning Ansible and his scripts and blog post have been instrumental in building this repo: 411 | - His [YouTube Channel](https://www.youtube.com/channel/UCR-DXc1voovS8nhAvccRZhg) has a video series on learning Ansible. 412 | - The book [Ansible for DevOps](https://www.ansiblefordevops.com) is an excellent resource for learning Ansible best practices. 413 | 414 | ## Contributing 415 | 416 | Contributions are welcome! Please make sure any PR passes the test suite. 417 | 418 | ### Running the test suite locally: 419 | 420 | The test suite uses a Docker container - make sure Docker is installed and 421 | configured before running the following commands: 422 | 423 | ``` 424 | pip install -r requirements-dev.txt 425 | molecule test 426 | ``` 427 | 428 | [aws]: https://aws.amazon.com 429 | [digital-ocean]: https://www.digitalocean.com/?refcode=5aa134a379d7 430 | [rackspace]: http://www.rackspace.com/ 431 | 432 | [ansible-installation_guide]: https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html 433 | [ansible-best_practices]: https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html 434 | [ansible-release_cycle]: https://docs.ansible.com/ansible/latest/reference_appendices/release_and_maintenance.html 435 | [docker-get_started]: https://www.docker.com/get-started 436 | [lets-encrypt]: https://letsencrypt.org/ 437 | [vagrant-downloads]: https://www.vagrantup.com/downloads.html 438 | [virtual-box_downloads]: https://www.virtualbox.org/wiki/Downloads 439 | 440 | [securing-ubuntu]: https://web.archive.org/web/20190629150318/https://www.codelitt.com/blog/my-first-10-minutes-on-a-server-primer-for-securing-ubuntu/ 441 | [deadsnakes]: https://launchpad.net/~fkrull/+archive/ubuntu/deadsnakes/?field.series_filter=xenial 442 | [michal-ansible_guide]: http://michal.karzynski.pl/blog/2013/06/09/django-nginx-gunicorn-virtualenv-supervisor/ 443 | [deploy-encrypted-copies]: https://www.calazan.com/how-to-deploy-encrypted-copies-of-your-ssl-keys-and-other-files-with-ansible-and-openssl/ 444 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! 5 | VAGRANTFILE_API_VERSION = "2" 6 | 7 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 8 | config.vm.box = "ubuntu/jammy64" 9 | config.ssh.forward_agent = false 10 | config.vm.define "my-cool-app.local", primary: true do |app| 11 | app.vm.hostname = "my-cool-app" 12 | app.vm.network "private_network", type: "dhcp" 13 | end 14 | 15 | config.vm.provider "virtualbox" do |vb| 16 | vb.customize ["modifyvm", :id, "--name", "django_default_project", "--memory", "1024"] 17 | end 18 | 19 | config.vm.provider "docker" do |d, override| 20 | override.vm.box = nil 21 | 22 | d.name = "django_default_project" 23 | d.build_dir = "docker" 24 | d.create_args = ["--publish-all", "--security-opt=seccomp:unconfined", 25 | "--tmpfs=/run", "--tmpfs=/run/lock", "--tmpfs=/tmp", 26 | "--volume=/sys/fs/cgroup:/sys/fs/cgroup:ro"] 27 | d.has_ssh = true 28 | end 29 | 30 | # For local development, uncommenting and editing the line below will enable 31 | # a folder in the host machine containing your local git repo to be synced to 32 | # the guest machine. Ensure the Ansible playbook variable "setup_git_repo" is 33 | # set to "no" (in group_vars/vagrant/vars.yml) when enabling this. 34 | #config.vm.synced_folder "../../../django_default_project", "/webapps/django_default_project/django_default_project" 35 | 36 | # Ansible provisioner. 37 | config.vm.provision "ansible" do |ansible| 38 | ansible.playbook = "vagrant.yml" 39 | ansible.host_key_checking = false 40 | ansible.verbose = "vv" 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | host_key_checking=false 3 | -------------------------------------------------------------------------------- /dbservers.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Provision application db server 4 | hosts: dbservers 5 | become: true 6 | become_user: root 7 | remote_user: "{{ server_user }}" 8 | vars: 9 | update_apt_cache: true 10 | 11 | module_defaults: 12 | ansible.builtin.apt: 13 | force_apt_get: true 14 | 15 | roles: 16 | - base 17 | - db 18 | -------------------------------------------------------------------------------- /development: -------------------------------------------------------------------------------- 1 | [webservers] 2 | dev.example.com nginx_use_letsencrypt=true 3 | 4 | [dbservers] 5 | dev.example.com 6 | 7 | [development:children] 8 | webservers 9 | dbservers 10 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Ubuntu 22.04 base image from the Docker repository 2 | FROM ubuntu:jammy 3 | 4 | # Allow processes to detect that they are being run in a container 5 | ENV container oci 6 | 7 | # Install packages for sudo and OpenSSH Server 8 | RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y sudo openssh-server 9 | 10 | # Add a "vagrant" user, and disable password-based login 11 | RUN adduser --disabled-password --gecos "" vagrant 12 | 13 | # Allow the "vagrant" user to login via SSH using the insecure keypair 14 | RUN su -c "URL=https://github.com/hashicorp/vagrant/raw/master/keys/%s.pub ssh-import-id vagrant" vagrant 15 | 16 | # Grant password-less sudo privileges to the "vagrant" user 17 | RUN echo "vagrant ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/vagrant 18 | 19 | # Start the OpenSSH Server when the container is run 20 | RUN systemctl enable ssh 21 | 22 | # Indicate the port number of the listening socket for SSH connections 23 | EXPOSE 22 24 | 25 | # Execute the init system (systemd) when the container is run 26 | ENTRYPOINT ["/sbin/init"] 27 | 28 | # Send the init process SIGRTMIN+3 when stopping the container 29 | STOPSIGNAL SIGRTMIN+3 30 | -------------------------------------------------------------------------------- /group_vars/development/vars.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | git_repo: https://github.com/YPCrumble/django-default-project 4 | 5 | project_name: django_default_project 6 | application_name: django_default_project 7 | 8 | # Note that this PPA doesn't guarantee timely updates in case of security issues. 9 | # Simply remove these two vars below if you prefer to use the official PPA and 10 | # default Python version that came with your Linux distro. 11 | # 12 | # More info here: https://launchpad.net/~fkrull/+archive/ubuntu/deadsnakes 13 | enable_deadsnakes_ppa: true 14 | virtualenv_python_version: python3.12 15 | 16 | 17 | # Git settings. 18 | setup_git_repo: true 19 | git_branch: development 20 | 21 | 22 | # Security settings. 23 | sudo_user_password: $6$rounds=656000$ca2RWJgtEqDVpOp9$0S0N3GHjOIO1PwRZ0vDyr0Z5Pi8ZcEa8.r.T.Wsx.O8RZlpTV1w0BLoEWwDb.zTkJOmP1Re.zBfQsviZaP89m0 24 | 25 | 26 | # Database settings. 27 | db_user: "{{ application_name }}" 28 | db_name: "{{ application_name }}" 29 | db_password: password 30 | 31 | 32 | # Gunicorn settings. For the number of workers, a good rule to follow is 33 | # 2 x number of CPUs + 1 34 | gunicorn_num_workers: 3 35 | 36 | # Setting this to 1 will restart the Gunicorn process each time 37 | # you make a request, basically reloading the code. Very handy 38 | # when developing. Set to 0 for unlimited requests (default). 39 | gunicorn_max_requests: 0 40 | gunicorn_timeout_seconds: 300 41 | 42 | 43 | # RabbitMQ settings. 44 | rabbitmq_server_name: "{{ inventory_hostname }}" 45 | 46 | rabbitmq_admin_user: admin 47 | rabbitmq_admin_password: password 48 | 49 | rabbitmq_application_vhost: "{{ application_name }}" 50 | rabbitmq_application_user: "{{ application_name }}" 51 | rabbitmq_application_password: password 52 | 53 | 54 | # Celery settings. 55 | celery_num_workers: 2 56 | use_celery_beat: false 57 | 58 | 59 | # Application settings. 60 | django_settings_file: "{{ application_name }}.settings.development" 61 | django_secret_key: "akr2icmg1n8%z^3fe3c+)5d0(t^cy-2_25rrl35a7@!scna^1#" 62 | 63 | broker_url: "amqp://{{ rabbitmq_application_user }}:{{ rabbitmq_application_password }}@localhost/{{ rabbitmq_application_vhost }}" 64 | 65 | requirements_file: "{{ project_path }}/requirements.txt" 66 | 67 | run_django_db_migrations: true 68 | run_django_collectstatic: true 69 | 70 | 71 | # Nginx settings. 72 | ssl_crt: | 73 | -----BEGIN CERTIFICATE----- 74 | MIIDOjCCAiKgAwIBAgIBADANBgkqhkiG9w0BAQsFADAaMRgwFgYDVQQDDA9kZXYu 75 | ZXhhbXBsZS5jb20wIhgPMTk3MDAxMDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVow 76 | GjEYMBYGA1UEAwwPZGV2LmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOC 77 | AQ8AMIIBCgKCAQEAuTyvpgw5iC7vHGr9cpCa1yVW3rTc/81PWqMPL3lKmV4IzHd7 78 | +50QKOFCQE6nfhYS+jVM/3dk8DQgaaTdo1BVF9kT/p1SQE2aE4AFHfPKXP1M+MFB 79 | oqK6uujejns4sItZg5yj6QTyTBNOHkaXeCObYpAnp+HokqT5Nmrr/uzPZc7jNZ41 80 | ehM2mL3nJ7Zgl40nQj4UqFLsXmlG4g6DKEKilRIpJClLdRm6lydgdVi2HApE4adk 81 | DOcIXXY687Y/LFoPFrgpztWv/O0/oJguV7LHttjS9b6LzjF8k7rWqCT4muHy1fYc 82 | JXLIOaUrWEru9FQegppwIo6tQjoZuy09lLOztQIDAQABo4GGMIGDMAkGA1UdEwQC 83 | MAAwHQYDVR0OBBYEFG2JDPGRCvuRmxMNQcewxwamyehtMB8GA1UdIwQYMBaAFG2J 84 | DPGRCvuRmxMNQcewxwamyehtMBoGA1UdEQQTMBGCD2Rldi5leGFtcGxlLmNvbTAa 85 | BgNVHRIEEzARgg9kZXYuZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBAIe2 86 | obG3657lBKtQEvRVnhJC8utlNIAo0U3Ys6+jmU87LEijVMVwhZreleLC2jI4DeLe 87 | JDf2uenifKQbhMCoWBJvoPxP5QjwpOaKvxuI/+xhSG4pQfBV9kb6mHsYhykY0sX1 88 | //JgCoumWwLbLQnB2tXV32Dqm+HUWWXqLS/aenNx0HWJwfFCLHPTTPYRn9ESy+oh 89 | 7frGtzjIFEx/OV2yQwBXmMxQjhUa82/Od99vEmiLgC4LLqXVtadNnumENMCRbw1P 90 | 99Z7EbZ9F206VVc8aSCLNhphPAct0wFYTQ59tFGFj637SsrI6LhP/wKXOJS5WLSG 91 | sK13YvMF3uL9YD7JNGI= 92 | -----END CERTIFICATE----- 93 | 94 | # This key is for demo purposes only. If you're committing your keys 95 | # to your git repo, you'd probably want it encrypted. You can use 96 | # ansible-vault to do this. 97 | ssl_key: | 98 | -----BEGIN PRIVATE KEY----- 99 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC5PK+mDDmILu8c 100 | av1ykJrXJVbetNz/zU9aow8veUqZXgjMd3v7nRAo4UJATqd+FhL6NUz/d2TwNCBp 101 | pN2jUFUX2RP+nVJATZoTgAUd88pc/Uz4wUGiorq66N6Oeziwi1mDnKPpBPJME04e 102 | Rpd4I5tikCen4eiSpPk2auv+7M9lzuM1njV6EzaYvecntmCXjSdCPhSoUuxeaUbi 103 | DoMoQqKVEikkKUt1GbqXJ2B1WLYcCkThp2QM5whddjrztj8sWg8WuCnO1a/87T+g 104 | mC5Xsse22NL1vovOMXyTutaoJPia4fLV9hwlcsg5pStYSu70VB6CmnAijq1COhm7 105 | LT2Us7O1AgMBAAECggEAHjQU9+A6aUgt2NZhKRMHDFmcRof7GQKjE8ZOrZD7ZvJ8 106 | QMqivq4nemLwaIfqq5Zx1bZnLaiMHtaBCnjFYuGwXkkZB4UjajS9ELzpGK8tqefr 107 | awwn5ZrfE6bw0w6oebDfEaSy3UXfNCRZsnoULJSxu2qB7M/bGj4oHIVmoZR/ZLwG 108 | LRItoK0wB2Bok5bmF9mAfW9EkoCOwkQP6uEynJ7z1f03teGJhSWu8xfjJv7XkLxG 109 | vIFbSSeGKZdEHK5fz5nHyr8RCkF5DNPagrs5gz0o1clDeG3VcQhCUgE1y8Ly8iSR 110 | LvDhk6KcfTscvdvxXNKPYPtqpFZfnJTt4qVM5rGaAQKBgQDd94EiefLVPxei/gqB 111 | cfhmtsDAdeQRqdJedg/PE4KDzlAqzOY/Bd4klVASwA0yxXepqf2ZRT1vhMkjFmSM 112 | kproc+gzKFQxxUSmJ//Jl2csaR3+UTAiBiv3b5BuCptG9WaHxRwx/jwHyslANrHP 113 | D+33ybLTOjlMbktrwIkqR1JzwQKBgQDVo31EYOQ7fDt4DYLRVHMoKJpErbwDH9jt 114 | hqfloKrV+UfrAWtn4mMcrCb6LKWTiv5T52gPbV/sSM5+7U97W3goffJShCPKobd3 115 | 6vhn2rowPjQd3IkZEygUuw4xTOHWUl1MB6Z9zyUd3hM1/wlO1CiHtaCpnEVgX66b 116 | yEF7IVXs9QKBgBIrscmVvBhS6udv7oI8Rz55VXwr6ni7szoCZjbofPW3TP7D+VFN 117 | dKsAAicWy73NRoeAH/+NGINplmGl8qNDWSUQYADYG1Rbtsv3WEwzdcG/9TGdidgv 118 | Myg1XNh1S9LaQgN5Ul6RVm643hLAp3uw7SUswNPj307vdIMkptXsMsbBAoGAdfuI 119 | 9ZdQ0+0i5oUHptUtl5L8x0rvFwaihWKlHHJjhjHZ3tX02/UxaSdFi0nW0ymilPGq 120 | DUMJA3Od3ojuKSD1td8AUUO6hHBU4zv3nVs1Eel4XLlrWVaz/ubiyqU732GzNobP 121 | EpGwXNNE5sAHAuq1y2Sp6qFryvJseonYZ8icLHUCgYBHSWGgjhOA7KyZdcTdmpu+ 122 | fGAwN2Coj2l+PKvQEJkX96IOboHDkd7N8mwyfjb30FcD1uVkEwjp55PpHmYph8xD 123 | GhN+yDgmf5OjDia0mFhv1nA/Nh16rQZd2NV7qYx0npPabOfud/8imdgOFr78HCjS 124 | MoqV6aeRY/7uwLiviaIUJg== 125 | -----END PRIVATE KEY----- 126 | -------------------------------------------------------------------------------- /group_vars/vagrant/vars.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | git_repo: https://github.com/YPCrumble/django-default-project 4 | 5 | project_name: django_default_project 6 | application_name: django_default_project 7 | 8 | # Note that this PPA doesn't guarantee timely updates in case of security issues. 9 | # Simply remove these two vars below if you prefer to use the official PPA and 10 | # default Python version that came with your Linux distro. 11 | # 12 | # More info here: https://launchpad.net/~fkrull/+archive/ubuntu/deadsnakes 13 | enable_deadsnakes_ppa: true 14 | virtualenv_python_version: python3.12 15 | 16 | 17 | # Git settings. 18 | setup_git_repo: true 19 | git_branch: main 20 | 21 | 22 | # Security settings. 23 | sudo_user_password: $6$rounds=656000$ca2RWJgtEqDVpOp9$0S0N3GHjOIO1PwRZ0vDyr0Z5Pi8ZcEa8.r.T.Wsx.O8RZlpTV1w0BLoEWwDb.zTkJOmP1Re.zBfQsviZaP89m0 24 | 25 | 26 | # Database settings. 27 | db_user: "{{ application_name }}" 28 | db_name: "{{ application_name }}" 29 | db_password: password 30 | 31 | 32 | # Gunicorn settings. For the number of workers, a good rule to follow is 33 | # 2 x number of CPUs + 1 34 | gunicorn_num_workers: 3 35 | 36 | # Setting this to 1 will restart the Gunicorn process each time 37 | # you make a request, basically reloading the code. Very handy 38 | # when developing. Set to 0 for unlimited requests (default). 39 | gunicorn_max_requests: 0 40 | gunicorn_timeout_seconds: 300 41 | 42 | 43 | # RabbitMQ settings. 44 | rabbitmq_server_name: "{{ inventory_hostname }}" 45 | 46 | rabbitmq_admin_user: admin 47 | rabbitmq_admin_password: password 48 | 49 | rabbitmq_application_vhost: "{{ application_name }}" 50 | rabbitmq_application_user: "{{ application_name }}" 51 | rabbitmq_application_password: password 52 | 53 | 54 | # Celery settings. 55 | celery_num_workers: 2 56 | use_celery_beat: false 57 | 58 | 59 | # Application settings. 60 | django_settings_file: "{{ application_name }}.settings.local" 61 | django_secret_key: "akr2icmg1n8%z^3fe3c+)5d0(t^cy-2_25rrl35a7@!scna^1#" 62 | 63 | broker_url: "amqp://{{ rabbitmq_application_user }}:{{ rabbitmq_application_password }}@localhost/{{ rabbitmq_application_vhost }}" 64 | 65 | requirements_file: "{{ project_path }}/requirements.txt" 66 | 67 | run_django_db_migrations: true 68 | run_django_collectstatic: true 69 | 70 | 71 | # Nginx settings. 72 | ssl_crt: | 73 | -----BEGIN CERTIFICATE----- 74 | MIIDQjCCAiqgAwIBAgIBADANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFteS1j 75 | b29sLWFwcC5sb2NhbDAiGA8xOTcwMDEwMTAwMDAwMFoYDzk5OTkxMjMxMjM1OTU5 76 | WjAcMRowGAYDVQQDDBFteS1jb29sLWFwcC5sb2NhbDCCASIwDQYJKoZIhvcNAQEB 77 | BQADggEPADCCAQoCggEBALk8r6YMOYgu7xxq/XKQmtclVt603P/NT1qjDy95Sple 78 | CMx3e/udECjhQkBOp34WEvo1TP93ZPA0IGmk3aNQVRfZE/6dUkBNmhOABR3zylz9 79 | TPjBQaKiurro3o57OLCLWYOco+kE8kwTTh5Gl3gjm2KQJ6fh6JKk+TZq6/7sz2XO 80 | 4zWeNXoTNpi95ye2YJeNJ0I+FKhS7F5pRuIOgyhCopUSKSQpS3UZupcnYHVYthwK 81 | ROGnZAznCF12OvO2PyxaDxa4Kc7Vr/ztP6CYLleyx7bY0vW+i84xfJO61qgk+Jrh 82 | 8tX2HCVyyDmlK1hK7vRUHoKacCKOrUI6GbstPZSzs7UCAwEAAaOBijCBhzAJBgNV 83 | HRMEAjAAMB0GA1UdDgQWBBRtiQzxkQr7kZsTDUHHsMcGpsnobTAfBgNVHSMEGDAW 84 | gBRtiQzxkQr7kZsTDUHHsMcGpsnobTAcBgNVHREEFTATghFteS1jb29sLWFwcC5s 85 | b2NhbDAcBgNVHRIEFTATghFteS1jb29sLWFwcC5sb2NhbDANBgkqhkiG9w0BAQsF 86 | AAOCAQEADEysIopo/TFj9zL5FpDLr/M71QPu7ZG10ncyIDp8wA8BxwLB2v2qcnfn 87 | c4LoAa5Nw9A+WTSJtlEOMBWQCTIALx5dZSfYTcb/p/LIoprmrWx4L0C3TvRH3Vpx 88 | INRxt2ynucNqNLKjfmvaZj5Ry1pJw8PE6AD7mWd1fpKUv3ci1cGY9hoCxMMIs8LJ 89 | 403V27BGsiK3bsUZuU+MWbcwfef8EqxFCjCvpko2qvrnJZ0AP5eDlDjdFgEeFy0c 90 | kRKJS02KMbypFWVFMC9ePAWizLMmhj4NHFLzSP2l8WQ4F3A4Ja5QoxTUuIUUibnC 91 | A/Bp6E0Njdy8AmCss8vS4GWYxTO4aA== 92 | -----END CERTIFICATE----- 93 | 94 | # This key is for demo purposes only. If you're committing your keys 95 | # to your git repo, you'd probably want it encrypted. You can use 96 | # ansible-vault to do this. 97 | ssl_key: | 98 | -----BEGIN PRIVATE KEY----- 99 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC5PK+mDDmILu8c 100 | av1ykJrXJVbetNz/zU9aow8veUqZXgjMd3v7nRAo4UJATqd+FhL6NUz/d2TwNCBp 101 | pN2jUFUX2RP+nVJATZoTgAUd88pc/Uz4wUGiorq66N6Oeziwi1mDnKPpBPJME04e 102 | Rpd4I5tikCen4eiSpPk2auv+7M9lzuM1njV6EzaYvecntmCXjSdCPhSoUuxeaUbi 103 | DoMoQqKVEikkKUt1GbqXJ2B1WLYcCkThp2QM5whddjrztj8sWg8WuCnO1a/87T+g 104 | mC5Xsse22NL1vovOMXyTutaoJPia4fLV9hwlcsg5pStYSu70VB6CmnAijq1COhm7 105 | LT2Us7O1AgMBAAECggEAHjQU9+A6aUgt2NZhKRMHDFmcRof7GQKjE8ZOrZD7ZvJ8 106 | QMqivq4nemLwaIfqq5Zx1bZnLaiMHtaBCnjFYuGwXkkZB4UjajS9ELzpGK8tqefr 107 | awwn5ZrfE6bw0w6oebDfEaSy3UXfNCRZsnoULJSxu2qB7M/bGj4oHIVmoZR/ZLwG 108 | LRItoK0wB2Bok5bmF9mAfW9EkoCOwkQP6uEynJ7z1f03teGJhSWu8xfjJv7XkLxG 109 | vIFbSSeGKZdEHK5fz5nHyr8RCkF5DNPagrs5gz0o1clDeG3VcQhCUgE1y8Ly8iSR 110 | LvDhk6KcfTscvdvxXNKPYPtqpFZfnJTt4qVM5rGaAQKBgQDd94EiefLVPxei/gqB 111 | cfhmtsDAdeQRqdJedg/PE4KDzlAqzOY/Bd4klVASwA0yxXepqf2ZRT1vhMkjFmSM 112 | kproc+gzKFQxxUSmJ//Jl2csaR3+UTAiBiv3b5BuCptG9WaHxRwx/jwHyslANrHP 113 | D+33ybLTOjlMbktrwIkqR1JzwQKBgQDVo31EYOQ7fDt4DYLRVHMoKJpErbwDH9jt 114 | hqfloKrV+UfrAWtn4mMcrCb6LKWTiv5T52gPbV/sSM5+7U97W3goffJShCPKobd3 115 | 6vhn2rowPjQd3IkZEygUuw4xTOHWUl1MB6Z9zyUd3hM1/wlO1CiHtaCpnEVgX66b 116 | yEF7IVXs9QKBgBIrscmVvBhS6udv7oI8Rz55VXwr6ni7szoCZjbofPW3TP7D+VFN 117 | dKsAAicWy73NRoeAH/+NGINplmGl8qNDWSUQYADYG1Rbtsv3WEwzdcG/9TGdidgv 118 | Myg1XNh1S9LaQgN5Ul6RVm643hLAp3uw7SUswNPj307vdIMkptXsMsbBAoGAdfuI 119 | 9ZdQ0+0i5oUHptUtl5L8x0rvFwaihWKlHHJjhjHZ3tX02/UxaSdFi0nW0ymilPGq 120 | DUMJA3Od3ojuKSD1td8AUUO6hHBU4zv3nVs1Eel4XLlrWVaz/ubiyqU732GzNobP 121 | EpGwXNNE5sAHAuq1y2Sp6qFryvJseonYZ8icLHUCgYBHSWGgjhOA7KyZdcTdmpu+ 122 | fGAwN2Coj2l+PKvQEJkX96IOboHDkd7N8mwyfjb30FcD1uVkEwjp55PpHmYph8xD 123 | GhN+yDgmf5OjDia0mFhv1nA/Nh16rQZd2NV7qYx0npPabOfud/8imdgOFr78HCjS 124 | MoqV6aeRY/7uwLiviaIUJg== 125 | -----END PRIVATE KEY----- 126 | -------------------------------------------------------------------------------- /molecule/default/INSTALL.rst: -------------------------------------------------------------------------------- 1 | ******* 2 | Docker driver installation guide 3 | ******* 4 | 5 | Requirements 6 | ============ 7 | 8 | * General molecule dependencies (see https://molecule.readthedocs.io/en/latest/installation.html) 9 | * Docker Engine 10 | * docker-py 11 | * docker 12 | 13 | Install 14 | ======= 15 | 16 | (Running `pip install -r requirements-dev.txt` will cover this) 17 | 18 | .. code-block:: bash 19 | 20 | $ sudo pip install docker 21 | -------------------------------------------------------------------------------- /molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # See https://github.com/metacloud/molecule/issues/843#issuecomment-304710797 3 | # and https://github.com/metacloud/molecule/blob/v2/test/scenarios/driver/ec2/molecule/default/playbook.yml#L1-L13 4 | - name: Converge 5 | hosts: all 6 | gather_facts: false 7 | tasks: 8 | - name: Install Python3 for Ansible 9 | ansible.builtin.raw: test -e /usr/bin/python3 || (apt -y update && apt install -y python3-minimal) 10 | become: true 11 | changed_when: false 12 | 13 | - name: Use molecule to test all roles in the playbook. 14 | hosts: all 15 | vars: 16 | update_apt_cache: true 17 | force_ssh_authentication: false 18 | 19 | tasks: 20 | - name: Install Python3 21 | ansible.builtin.raw: apt-get install python3-minimal 22 | changed_when: false 23 | 24 | module_defaults: 25 | ansible.builtin.apt: 26 | force_apt_get: true 27 | 28 | roles: 29 | - security 30 | - base 31 | - db 32 | - rabbitmq 33 | - web 34 | - celery 35 | - memcached 36 | - nginx 37 | -------------------------------------------------------------------------------- /molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: docker 6 | lint: | 7 | set -e 8 | yamllint . 9 | ansible-lint 10 | flake8 11 | platforms: 12 | - name: instance-jammy 13 | groups: 14 | - vagrant 15 | image: ubuntu 16 | image_version: jammy 17 | privileged: true 18 | - name: instance-noble 19 | groups: 20 | - vagrant 21 | image: ubuntu 22 | image_version: noble 23 | privileged: true 24 | provisioner: 25 | name: ansible 26 | env: 27 | ANSIBLE_ROLES_PATH: ../../roles/ 28 | inventory: 29 | links: 30 | group_vars: ../../group_vars/ 31 | verifier: 32 | name: ansible 33 | -------------------------------------------------------------------------------- /molecule/default/tests/test_default.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import testinfra.utils.ansible_runner 4 | 5 | testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( 6 | os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all') 7 | 8 | 9 | def test_hosts_file(host): 10 | f = host.file('/etc/hosts') 11 | 12 | assert f.exists 13 | assert f.user == 'root' 14 | assert f.group == 'root' 15 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | 3 | ansible-lint==6.21.1 4 | flake8==6.1.0 5 | molecule==25.4.0 6 | molecule-plugins[docker] 7 | yamllint==1.32.0 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ansible==9.13.0 2 | -------------------------------------------------------------------------------- /roles/avahi/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install the Avahi mDNS/DNS-SD daemon 4 | ansible.builtin.apt: 5 | name: avahi-daemon 6 | update_cache: "{{ update_apt_cache }}" 7 | state: present 8 | tags: packages 9 | 10 | - name: Ensure the Avahi mDNS/DNS-SD daemon is running 11 | ansible.builtin.service: 12 | name: avahi-daemon 13 | state: started 14 | enabled: true 15 | -------------------------------------------------------------------------------- /roles/base/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | ansible_python_interpreter: /usr/bin/python3 4 | 5 | server_user: root 6 | 7 | base_python_package: "{{ ansible_python_interpreter | default('/usr/bin/python') | basename }}" 8 | 9 | create_swap_file: false 10 | swap_file_path: /swapfile 11 | swap_file_size_kb: 512 12 | -------------------------------------------------------------------------------- /roles/base/tasks/create_swap_file.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create swap file 3 | ansible.builtin.command: dd if=/dev/zero of={{ swap_file_path }} bs=1024 count={{ swap_file_size_kb }}k 4 | creates="{{ swap_file_path }}" 5 | tags: swap.file.create 6 | 7 | - name: Change swap file permissions 8 | ansible.builtin.file: 9 | path: "{{ swap_file_path }}" 10 | owner: root 11 | group: root 12 | mode: "0600" 13 | tags: swap.file.permissions 14 | 15 | - name: Check swap file type 16 | ansible.builtin.command: file {{ swap_file_path }} 17 | register: swapfile 18 | tags: swap.file.mkswap 19 | changed_when: false 20 | 21 | - name: Make swap file 22 | ansible.builtin.command: "mkswap {{ swap_file_path }}" 23 | when: swapfile.stdout.find('swap file') == -1 24 | tags: swap.file.mkswap 25 | 26 | - name: Write swap entry in fstab 27 | ansible.posix.mount: 28 | name: none 29 | src: "{{ swap_file_path }}" 30 | fstype: swap 31 | opts: sw 32 | passno: 0 33 | dump: 0 34 | state: present 35 | tags: swap.fstab 36 | 37 | - name: Mount swap 38 | ansible.builtin.command: "swapon {{ swap_file_path }}" 39 | when: ansible_swaptotal_mb < 1 40 | tags: swap.file.swapon 41 | -------------------------------------------------------------------------------- /roles/base/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - ansible.builtin.import_tasks: create_swap_file.yml 4 | when: create_swap_file 5 | tags: swap 6 | 7 | - name: Install security updates 8 | ansible.builtin.apt: default_release={{ ansible_distribution_release }}-security 9 | update_cache={{ update_apt_cache }} 10 | upgrade=dist 11 | tags: 12 | - packages 13 | - skip_ansible_lint 14 | 15 | - name: Install base packages 16 | ansible.builtin.apt: 17 | update_cache: "{{ update_apt_cache }}" 18 | state: present 19 | name: 20 | - locales 21 | - build-essential 22 | - acl 23 | - htop 24 | - git 25 | - "{{ base_python_package }}-pip" 26 | - "i{{ base_python_package }}" 27 | - supervisor 28 | tags: 29 | - packages 30 | - packages.security 31 | -------------------------------------------------------------------------------- /roles/celery/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: restart {{ celery_application_name }} 4 | community.general.supervisorctl: name={{ celery_application_name }} state=restarted 5 | 6 | - name: Restart {{ celery_beat_application_name }} 7 | community.general.supervisorctl: name={{ celery_beat_application_name }} state=restarted 8 | when: use_celery_beat is defined 9 | -------------------------------------------------------------------------------- /roles/celery/tasks/copy_scripts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Create the folder for the celery scripts 4 | ansible.builtin.file: path={{ celery_scripts_dir }} 5 | owner={{ celery_user }} 6 | group={{ celery_group }} 7 | mode=0774 8 | state=directory 9 | 10 | - name: Create the script file for {{ celery_application_name }} 11 | ansible.builtin.template: src={{ celery_template_file }} 12 | dest={{ celery_scripts_dir }}/{{ celery_application_name }}_start 13 | owner={{ celery_user }} 14 | group={{ celery_group }} 15 | mode=0755 16 | 17 | - name: Create the script file for {{ celery_beat_application_name }} 18 | ansible.builtin.template: src={{ celery_beat_template_file }} 19 | dest={{ celery_scripts_dir }}/{{ celery_beat_application_name }}_start 20 | owner={{ celery_user }} 21 | group={{ celery_group }} 22 | mode=0755 23 | -------------------------------------------------------------------------------- /roles/celery/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - ansible.builtin.import_tasks: copy_scripts.yml 4 | tags: celery 5 | 6 | - ansible.builtin.import_tasks: setup_supervisor.yml 7 | tags: celery 8 | -------------------------------------------------------------------------------- /roles/celery/tasks/setup_supervisor.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Ensure the Supervisor service is running 4 | ansible.builtin.service: 5 | name: supervisor 6 | state: started 7 | enabled: true 8 | # TODO: This is likely due to a bug in Ansible. 9 | # Remove this line in the future. 10 | # See https://github.com/ansible/ansible/issues/75005 11 | use: sysvinit 12 | 13 | - name: Create the Supervisor config file for {{ celery_application_name }} 14 | ansible.builtin.template: src=supervisor_{{ celery_application_name }}.conf.j2 15 | dest=/etc/supervisor/conf.d/{{ celery_application_name }}.conf 16 | mode=0644 17 | 18 | - name: Create the log directory for {{ celery_application_name }} 19 | ansible.builtin.file: 20 | path: "{{ celery_log_dir }}" 21 | owner: "{{ celery_user }}" 22 | group: "{{ celery_group }}" 23 | state: directory 24 | mode: "0664" 25 | changed_when: false 26 | 27 | - name: Check for an existing celery logfile 28 | ansible.builtin.stat: 29 | path: "{{ celery_log_file }}" 30 | register: p 31 | 32 | - name: Create (or retain) the log file for {{ celery_application_name }} 33 | # Removing until https://github.com/ansible/ansible/issues/45530 gets resolved. 34 | # ansible.builtin.copy: content="" 35 | # dest={{ celery_log_file }} 36 | # owner={{ celery_user }} 37 | # group={{ celery_group }} 38 | # force=no 39 | ansible.builtin.file: 40 | path: "{{ celery_log_file }}" 41 | owner: "{{ celery_user }}" 42 | group: "{{ celery_group }}" 43 | state: '{{ "file" if p.stat.exists else "touch" }}' 44 | mode: "0644" 45 | 46 | - name: Re-read the Supervisor config files 47 | community.general.supervisorctl: name={{ celery_application_name }} state=present 48 | 49 | - name: Create the Supervisor config file for {{ celery_beat_application_name }} 50 | ansible.builtin.template: src=supervisor_{{ celery_beat_application_name }}.conf.j2 51 | dest=/etc/supervisor/conf.d/{{ celery_beat_application_name }}.conf 52 | mode=0644 53 | 54 | - name: Check for an existing celery_beat logfile 55 | ansible.builtin.stat: 56 | path: "{{ celery_beat_log_file }}" 57 | register: b 58 | 59 | - name: Create (or retain) the log file for {{ celery_beat_application_name }} 60 | # Removing until https://github.com/ansible/ansible/issues/45530 gets resolved. 61 | # ansible.builtin.copy: content="" 62 | # dest={{ celery_beat_log_file }} 63 | # owner={{ celery_user }} 64 | # group={{ celery_group }} 65 | # force=no 66 | ansible.builtin.file: 67 | path: "{{ celery_beat_log_file }}" 68 | owner: "{{ celery_user }}" 69 | group: "{{ celery_group }}" 70 | state: '{{ "file" if b.stat.exists else "touch" }}' 71 | mode: "0644" 72 | 73 | - name: Re-read the Supervisor celery_beat config files 74 | community.general.supervisorctl: name={{ celery_beat_application_name }} state=present 75 | -------------------------------------------------------------------------------- /roles/celery/templates/celery_beat_start.j2: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DJANGODIR={{ project_path }} 4 | 5 | # Activate the virtual environment. 6 | cd $DJANGODIR 7 | . {{ virtualenv_path }}/bin/activate 8 | . {{ virtualenv_path }}/bin/postactivate 9 | 10 | # require to install djang_celery_beat package: https://github.com/celery/django-celery-beat 11 | exec celery -A {{ application_name }} beat -l info -S django 12 | -------------------------------------------------------------------------------- /roles/celery/templates/celery_start.j2: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DJANGODIR={{ project_path }} 4 | 5 | # Activate the virtual environment. 6 | cd $DJANGODIR 7 | . {{ virtualenv_path }}/bin/activate 8 | . {{ virtualenv_path }}/bin/postactivate 9 | 10 | # Programs meant to be run under supervisor should not daemonize themselves 11 | # (do not use --daemon). 12 | exec celery -A {{ application_name }} worker -E -l INFO --concurrency={{ celery_num_workers }} 13 | -------------------------------------------------------------------------------- /roles/celery/templates/supervisor_celery.conf.j2: -------------------------------------------------------------------------------- 1 | [program:{{ celery_application_name }}] 2 | command={{ celery_scripts_dir }}/{{ celery_application_name }}_start 3 | 4 | autostart=true 5 | autorestart=true 6 | 7 | user={{ celery_user }} 8 | 9 | stdout_logfile={{ celery_log_file }} 10 | redirect_stderr = true -------------------------------------------------------------------------------- /roles/celery/templates/supervisor_celery_beat.conf.j2: -------------------------------------------------------------------------------- 1 | [program:{{ celery_beat_application_name }}] 2 | command={{ celery_scripts_dir }}/{{ celery_beat_application_name }}_start 3 | 4 | autostart=true 5 | autorestart=true 6 | 7 | user={{ celery_user }} 8 | 9 | stdout_logfile={{ celery_beat_log_file }} 10 | redirect_stderr = true 11 | -------------------------------------------------------------------------------- /roles/celery/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | server_root_dir: /webapps 4 | 5 | virtualenv_path: "/webapps/{{ application_name }}" 6 | project_path: "{{ virtualenv_path }}/{{ project_name }}" 7 | 8 | celery_user: "{{ application_name }}" 9 | celery_group: webapps 10 | 11 | celery_application_name: celery 12 | celery_scripts_dir: "{{ virtualenv_path }}/scripts/celery" 13 | celery_template_file: "{{ celery_application_name }}_start.j2" 14 | 15 | celery_beat_application_name: celery_beat 16 | celery_beat_template_file: "{{ celery_beat_application_name }}_start.j2" 17 | 18 | celery_log_dir: "{{ virtualenv_path }}/logs" 19 | celery_log_file: "{{ celery_log_dir }}/{{ celery_application_name }}.log" 20 | celery_beat_log_file: "{{ celery_log_dir }}/{{ celery_beat_application_name }}.log" 21 | -------------------------------------------------------------------------------- /roles/certbot/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | certbot_auto_renew: true 4 | certbot_admin_email: admin@example.com 5 | certbot_script: /usr/bin/certbot 6 | certbot_output_dir: "/etc/letsencrypt/live/{{ inventory_hostname }}" 7 | -------------------------------------------------------------------------------- /roles/certbot/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install Certbot 4 | ansible.builtin.apt: 5 | update_cache: "{{ update_apt_cache }}" 6 | state: present 7 | name: 8 | - certbot 9 | - python3-certbot-nginx 10 | tags: packages 11 | 12 | - name: Check if Nginx exists 13 | ansible.builtin.stat: path=/etc/init.d/nginx 14 | register: nginx_status 15 | 16 | - name: Ensure Nginx is stopped 17 | ansible.builtin.service: name=nginx state=stopped 18 | when: nginx_status.stat.exists 19 | 20 | - name: Install certbot and generate cert 21 | ansible.builtin.command: "{{ certbot_script }} certonly --noninteractive --agree-tos --standalone --email {{ certbot_admin_email }} -d {{ inventory_hostname }}" 22 | args: 23 | creates: "{{ certbot_output_dir }}" 24 | 25 | - name: Ensure Nginx is started 26 | ansible.builtin.service: name=nginx state=started 27 | when: nginx_status.stat.exists 28 | 29 | - name: Ensure a cron job to auto-renew the cert exists 30 | ansible.builtin.cron: name="daily auto renew cert" 31 | special_time=daily 32 | job="{{ certbot_script }} renew --standalone --no-self-upgrade --pre-hook \"service nginx stop\" --post-hook \"service nginx start\" --quiet" 33 | state=present 34 | when: certbot_auto_renew 35 | -------------------------------------------------------------------------------- /roles/db/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Restart postgresql 4 | ansible.builtin.service: name=postgresql state=restarted enabled=yes 5 | -------------------------------------------------------------------------------- /roles/db/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Add the PostgreSQL repository key to the apt trusted keys 4 | ansible.builtin.apt_key: url=https://www.postgresql.org/media/keys/ACCC4CF8.asc 5 | state=present 6 | 7 | - name: Add the PostgreSQL repository to the apt sources list 8 | ansible.builtin.apt_repository: repo='deb http://apt.postgresql.org/pub/repos/apt/ {{ ansible_distribution_release }}-pgdg main' 9 | update_cache={{ update_apt_cache }} 10 | state=present 11 | 12 | - name: Ensure locale is available 13 | community.general.locale_gen: name=en_US.UTF-8 14 | 15 | - name: Install PostgreSQL 16 | ansible.builtin.apt: 17 | update_cache: "{{ update_apt_cache }}" 18 | state: present 19 | name: 20 | - postgresql 21 | - postgresql-contrib 22 | - "{{ base_python_package }}-psycopg2" 23 | tags: packages 24 | 25 | - name: Ensure the PostgreSQL service is running 26 | ansible.builtin.service: 27 | name: postgresql 28 | state: started 29 | enabled: true 30 | # TODO: This is likely due to a bug in Ansible. 31 | # Remove this line in the future. 32 | # See https://github.com/ansible/ansible/issues/75005 33 | use: sysvinit 34 | 35 | - name: Ensure database is created 36 | become: true 37 | become_user: postgres 38 | community.postgresql.postgresql_db: 39 | name: "{{ db_name }}" 40 | encoding: UTF-8 41 | lc_collate: en_US.UTF-8 42 | lc_ctype: en_US.UTF-8 43 | template: template0 44 | state: present 45 | 46 | - name: Ensure user has access to the database 47 | become: true 48 | become_user: postgres 49 | community.postgresql.postgresql_user: db={{ db_name }} 50 | name={{ db_user }} 51 | password={{ db_password }} 52 | encrypted=yes 53 | state=present 54 | 55 | # If objs is omitted for type "database", it defaults to the database 56 | # to which the connection is established 57 | - name: Grant database privileges to the user. 58 | become: true 59 | become_user: postgres 60 | community.postgresql.postgresql_privs: 61 | db: "{{ db_name }}" 62 | privs: ALL 63 | type: database 64 | role: "{{ db_user }}" 65 | 66 | - name: Grant schema public to the user. 67 | become: true 68 | become_user: postgres 69 | community.postgresql.postgresql_privs: 70 | db: "{{ db_name }}" 71 | privs: ALL 72 | type: schema 73 | objs: public 74 | role: "{{ db_user }}" 75 | 76 | - name: Ensure user does not have unnecessary privileges 77 | become: true 78 | become_user: postgres 79 | community.postgresql.postgresql_user: name={{ db_user }} 80 | role_attr_flags=NOSUPERUSER,NOCREATEDB 81 | state=present 82 | -------------------------------------------------------------------------------- /roles/memcached/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: restart memcached 4 | ansible.builtin.service: name=memcached state=restarted enabled=yes 5 | -------------------------------------------------------------------------------- /roles/memcached/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install Memcached 4 | ansible.builtin.apt: 5 | name: memcached 6 | update_cache: "{{ update_apt_cache }}" 7 | state: present 8 | tags: packages 9 | 10 | - name: Create the Memcached configuration file 11 | ansible.builtin.template: 12 | src: memcached.conf.j2 13 | dest: /etc/memcached.conf 14 | mode: "0644" 15 | backup: true 16 | notify: 17 | - restart memcached 18 | 19 | - name: Ensure the Memcached service is running 20 | ansible.builtin.service: 21 | name: memcached 22 | state: started 23 | enabled: true 24 | # TODO: This is likely due to a bug in Ansible. 25 | # Remove this line in the future. 26 | # See https://github.com/ansible/ansible/issues/75005 27 | use: sysvinit 28 | -------------------------------------------------------------------------------- /roles/memcached/templates/memcached.conf.j2: -------------------------------------------------------------------------------- 1 | # 2003 - Jay Bonci 2 | # This configuration file is read by the start-memcached script provided as 3 | # part of the Debian GNU/Linux distribution. 4 | 5 | # Run memcached as a daemon. This command is implied, and is not needed for the 6 | # daemon to run. See the README.Debian that comes with this package for more 7 | # information. 8 | -d 9 | 10 | # Log memcached's output to /var/log/memcached 11 | logfile /var/log/memcached.log 12 | 13 | # Be verbose 14 | # -v 15 | 16 | # Be even more verbose (print client commands as well) 17 | # -vv 18 | 19 | # Start with a cap of 64 megs of memory. It's reasonable, and the daemon default 20 | # Note that the daemon will grow to this size, but does not start out holding this much 21 | # memory 22 | -m {{ memcached_max_memory_mb }} 23 | 24 | # Default connection port is 11211 25 | -p {{ memcached_port }} 26 | 27 | # Run the daemon as root. The start-memcached will default to running as root if no 28 | # -u command is present in this config file 29 | -u {{ memcached_user }} 30 | 31 | # Specify which IP address to listen on. The default is to listen on all IP addresses 32 | # This parameter is one of the only security measures that memcached has, so make sure 33 | # it's listening on a firewalled interface. 34 | -l {{ memcached_listen }} 35 | 36 | # Limit the number of simultaneous incoming connections. The daemon default is 1024 37 | -c {{ memcached_max_connections }} 38 | 39 | # Lock down all paged memory. Consult with the README and homepage before you do this 40 | # -k 41 | 42 | # Return error when memory is exhausted (rather than removing items) 43 | # -M 44 | 45 | # Maximize core file limit 46 | # -r -------------------------------------------------------------------------------- /roles/memcached/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | memcached_listen: 127.0.0.1 4 | memcached_port: 11211 5 | memcached_user: nobody 6 | memcached_max_memory_mb: 64 7 | memcached_max_connections: 1024 8 | -------------------------------------------------------------------------------- /roles/nginx/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | nginx_server_name: "{{ inventory_hostname }}" 4 | nginx_ssl_dest_dir: /etc/ssl 5 | nginx_strong_dh_group: true # Strongly recomended in production. See weakdh.org. 6 | nginx_use_letsencrypt: false 7 | 8 | # Only used when nginx_use_letsencrypt is set to yes/true. The 'certbot' role will automatically generate these files. 9 | letsencrypt_dir: "/etc/letsencrypt/live/{{ inventory_hostname }}" 10 | letsencrypt_cert_filename: fullchain.pem 11 | letsencrypt_privkey_filename: privkey.pem 12 | -------------------------------------------------------------------------------- /roles/nginx/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Restart nginx 3 | ansible.builtin.service: name=nginx state=restarted enabled=yes 4 | 5 | - name: Reload nginx 6 | ansible.builtin.service: name=nginx state=reloaded 7 | -------------------------------------------------------------------------------- /roles/nginx/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install Nginx 4 | ansible.builtin.apt: name=nginx update_cache={{ update_apt_cache }} state=present 5 | tags: packages 6 | 7 | - name: Copy the SSL certificate to the remote server 8 | ansible.builtin.copy: 9 | content={{ ssl_crt }} 10 | dest={{ nginx_ssl_dest_dir }}/{{ application_name }}.crt 11 | mode="0644" 12 | notify: Restart nginx 13 | when: not nginx_use_letsencrypt 14 | 15 | - name: Copy the SSL private key to the remote server 16 | ansible.builtin.copy: 17 | content={{ ssl_key }} 18 | dest={{ nginx_ssl_dest_dir }}/{{ application_name }}.key 19 | mode="0644" 20 | notify: Restart nginx 21 | when: not nginx_use_letsencrypt 22 | 23 | - name: Ensure that a strong Diffie-Hellman group is used 24 | ansible.builtin.command: openssl dhparam -out /etc/ssl/certs/dhparams.pem 2048 creates=/etc/ssl/certs/dhparams.pem 25 | when: nginx_strong_dh_group is defined and nginx_strong_dh_group 26 | 27 | - name: Create the Nginx configuration file 28 | ansible.builtin.template: src={{ application_name }}.j2 29 | dest=/etc/nginx/sites-available/{{ application_name }} 30 | backup=yes 31 | mode=0644 32 | notify: Reload nginx 33 | 34 | - name: Ensure that the default site is disabled 35 | ansible.builtin.file: path=/etc/nginx/sites-enabled/default state=absent 36 | notify: Reload nginx 37 | 38 | - name: Ensure that the application site is enabled 39 | ansible.builtin.file: src=/etc/nginx/sites-available/{{ application_name }} 40 | dest=/etc/nginx/sites-enabled/{{ application_name }} 41 | state=link 42 | notify: Reload nginx 43 | 44 | - name: Ensure Nginx service is started 45 | ansible.builtin.service: 46 | name: nginx 47 | state: started 48 | enabled: true 49 | # TODO: This is likely due to a bug in Ansible. 50 | # Remove this line in the future. 51 | # See https://github.com/ansible/ansible/issues/75005 52 | use: sysvinit 53 | -------------------------------------------------------------------------------- /roles/nginx/templates/django_default_project.j2: -------------------------------------------------------------------------------- 1 | upstream {{ application_name }}_wsgi_server { 2 | # fail_timeout=0 means we always retry an upstream even if it failed 3 | # to return a good HTTP response (in case the Unicorn master nukes a 4 | # single worker for timing out). 5 | 6 | server unix:{{ virtualenv_path }}/run/gunicorn.sock fail_timeout=0; 7 | } 8 | 9 | server { 10 | listen 80; 11 | server_name {{ nginx_server_name }}; 12 | server_tokens off; 13 | 14 | # Terminate the request immediately if a request uses the IP address. 15 | # This stops Invalid HTTP_HOST header exceptions being raised by Django. 16 | 17 | if ($host !~* ^({{ nginx_server_name }})$ ) { 18 | return 444; 19 | } 20 | 21 | return 301 https://$server_name$request_uri; 22 | } 23 | 24 | server { 25 | listen 443 ssl; 26 | server_name {{ nginx_server_name }}; 27 | server_tokens off; 28 | {% if nginx_use_letsencrypt %} 29 | ssl_certificate {{ letsencrypt_dir }}/{{ letsencrypt_cert_filename }}; 30 | ssl_certificate_key {{ letsencrypt_dir }}/{{ letsencrypt_privkey_filename }}; 31 | {% else %} 32 | ssl_certificate {{ nginx_ssl_dest_dir }}/{{ application_name }}.crt; 33 | ssl_certificate_key {{ nginx_ssl_dest_dir }}/{{ application_name }}.key; 34 | {% endif %} 35 | ssl_protocols TLSv1.2; 36 | ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; 37 | ssl_prefer_server_ciphers on; 38 | {% if nginx_strong_dh_group %} 39 | ssl_dhparam /etc/ssl/certs/dhparams.pem; 40 | {% endif %} 41 | 42 | # Terminate the request immediately if a request uses the IP address. 43 | # This stops Invalid HTTP_HOST header exceptions being raised by Django. 44 | 45 | if ($host !~* ^({{ nginx_server_name }})$ ) { 46 | return 444; 47 | } 48 | 49 | # Prevent MIME type sniffing for security 50 | add_header X-Content-Type-Options "nosniff"; 51 | 52 | # Enable XSS Protection in case user's browser has disabled it 53 | add_header X-XSS-Protection "1; mode=block"; 54 | 55 | 56 | # ----------- Recommended headers (without default settings) ----------- # 57 | # It is recommended that all web applications set these headers. # 58 | # However, this template does not prescribe any defaults. # 59 | 60 | #### Content-Security-Policy #### 61 | # Recommended security-conscious defaults: 62 | # --------------------- -------------------------------------------------- 63 | # default-src https: By default, all content must be loaded over HTTPS 64 | # form-action 'self' Disallow form submission to external URLs 65 | # frame-ancestors 'none' Disable loading the site in a frame (similar to `X-Frame-Options: DENY) 66 | # 67 | # If all content is self-hosted (no JavaScript, CSS, fonts, etc. loaded from CDNs), 68 | # it's wise to use your CSP to prevent content loading from other sources: 69 | # script-src: 'self'; style-src: 'self'; font-src: 'self'; media-src: 'self'; object-src: 'self' 70 | # 71 | # For more information (including additional directives not defined here), see: 72 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 73 | # 74 | # Alternatively, set Content-Security-Policy-Report-Only to report violations, but not block content loading 75 | # add_header Content-Security-Policy "default-src https:; form-action 'self'; frame-ancestors 'none'"; 76 | 77 | #### Referrer-Policy #### 78 | # Recommended reading on the security concerns behind a default referrer policy: 79 | # https://developer.mozilla.org/en-US/docs/Web/Security/Referer_header:_privacy_and_security_concerns 80 | # Don't leak referring URL when following external links (protects user privacy by reducing tracking opportunities) 81 | # Setting "no-referrer" is the most privacy-conscious choice. 82 | # However, some frameworks (e.g. Django) rely on same-origin referrer information for CSRF protection 83 | #add_header Referrer-Policy "same-origin"; 84 | 85 | #### Feature-Policy #### 86 | # This experimental header enumerates exactly which browser features your application will and will not use. 87 | # Feature policies apply to embedded content and can thus help protect your users from malicious third parties. 88 | # Note that there is currently no way to deny all features by default. 89 | # The below policy disables all known features at time of writing. Consult MDN for an up-to-date feature list: 90 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy 91 | #add_header Feature-Policy "accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; camera 'none'; encrypted-media 'none'; fullscreen 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'none'; speaker 'none'; usb 'none'; vr 'none';"; 92 | 93 | # ---------------------- End recommended headers ---------------------- # 94 | 95 | 96 | client_max_body_size 4G; 97 | 98 | location /static/ { 99 | alias {{ nginx_static_dir }}; 100 | } 101 | 102 | location /media/ { 103 | alias {{ nginx_media_dir }}; 104 | } 105 | 106 | location / { 107 | if (-f {{ virtualenv_path }}/maintenance_on.html) { 108 | return 503; 109 | } 110 | 111 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 112 | proxy_set_header X-Forwarded-Proto https; 113 | proxy_set_header Host $host; 114 | proxy_redirect off; 115 | 116 | # Try to serve static files from nginx, no point in making an 117 | # *application* server like Unicorn/Rainbows! serve static files. 118 | if (!-f $request_filename) { 119 | proxy_pass http://{{ application_name }}_wsgi_server; 120 | break; 121 | } 122 | } 123 | 124 | # Error pages 125 | error_page 500 502 504 /500.html; 126 | location = /500.html { 127 | root {{ project_path }}/{{ application_name }}/templates/; 128 | } 129 | 130 | error_page 503 /maintenance_on.html; 131 | location = /maintenance_on.html { 132 | root {{ virtualenv_path }}/; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /roles/rabbitmq/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: restart rabbitmq-server 4 | ansible.builtin.service: 5 | name: rabbitmq-server 6 | state: restarted 7 | -------------------------------------------------------------------------------- /roles/rabbitmq/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install RabbitMQ server 4 | ansible.builtin.apt: 5 | update_cache: "{{ update_apt_cache }}" 6 | state: present 7 | name: 8 | - rabbitmq-server 9 | tags: 10 | - packages 11 | 12 | - name: Make sure rabbitmq-server is enabled and running 13 | ansible.builtin.service: 14 | name: rabbitmq-server 15 | state: started 16 | enabled: true 17 | # TODO: This is likely due to a bug in Ansible. 18 | # Remove this line in the future. 19 | # See https://github.com/ansible/ansible/issues/75005 20 | use: sysvinit 21 | 22 | - name: Enable the RabbitMQ Management Console 23 | community.rabbitmq.rabbitmq_plugin: names=rabbitmq_management state=enabled 24 | notify: restart rabbitmq-server 25 | 26 | - ansible.builtin.import_tasks: setup_vhosts.yml 27 | 28 | - ansible.builtin.import_tasks: setup_users.yml 29 | -------------------------------------------------------------------------------- /roles/rabbitmq/tasks/setup_users.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Create default admin user 4 | community.rabbitmq.rabbitmq_user: 5 | user: "{{ rabbitmq_admin_user }}" 6 | password: "{{ rabbitmq_admin_password }}" 7 | vhost: / 8 | tags: administrator 9 | state: present 10 | 11 | - name: Create application user 12 | community.rabbitmq.rabbitmq_user: 13 | user: "{{ rabbitmq_application_user }}" 14 | password: "{{ rabbitmq_application_password }}" 15 | vhost: "{{ rabbitmq_application_vhost }}" 16 | configure_priv: .* 17 | read_priv: .* 18 | write_priv: .* 19 | state: present 20 | 21 | - name: Ensure the default 'guest' user doesn't exist 22 | community.rabbitmq.rabbitmq_user: 23 | user: "guest" 24 | state: absent 25 | -------------------------------------------------------------------------------- /roles/rabbitmq/tasks/setup_vhosts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Create a vhost for the application 4 | community.rabbitmq.rabbitmq_vhost: name={{ rabbitmq_application_vhost }} state=present 5 | -------------------------------------------------------------------------------- /roles/rabbitmq/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | rabbitmq_server_name: "{{ inventory_hostname }}" 4 | 5 | rabbitmq_admin_user: admin 6 | rabbitmq_admin_password: password 7 | 8 | rabbitmq_application_vhost: "{{ application_name }}" 9 | rabbitmq_application_user: "{{ application_name }}" 10 | rabbitmq_application_password: password 11 | -------------------------------------------------------------------------------- /roles/security/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # You can use the following Python script to adjust this value. 4 | # pip install passlib 5 | # python -c "from passlib.hash import sha512_crypt; import getpass; print sha512_crypt.encrypt(getpass.getpass())" 6 | server_user_password: "{{ sudo_user_password }}" 7 | 8 | perform_aptitude_dist_upgrade: true 9 | 10 | force_ssh_authentication: true 11 | 12 | enable_unattended_upgrades: true 13 | 14 | enable_ufw: true 15 | 16 | enable_fail2ban: false 17 | -------------------------------------------------------------------------------- /roles/security/files/apt_periodic: -------------------------------------------------------------------------------- 1 | APT::Periodic::Update-Package-Lists "1"; 2 | APT::Periodic::Download-Upgradeable-Packages "1"; 3 | APT::Periodic::AutocleanInterval "7"; 4 | APT::Periodic::Unattended-Upgrade "1"; 5 | -------------------------------------------------------------------------------- /roles/security/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart ssh 3 | ansible.builtin.service: name=ssh state=restarted 4 | 5 | - name: restart fail2ban 6 | ansible.builtin.service: name=fail2ban state=restarted 7 | -------------------------------------------------------------------------------- /roles/security/tasks/create_non_root_sudo_user.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Add user 4 | ansible.builtin.user: name="{{ server_user }}" shell="{{ shell }}" password="{{ server_user_password }}" 5 | 6 | - name: Install sudo 7 | ansible.builtin.apt: name=sudo update_cache={{ update_apt_cache }} state=present cache_valid_time=86400 8 | 9 | - name: Add user to sudoers 10 | ansible.builtin.lineinfile: dest=/etc/sudoers 11 | regexp="{{ server_user }} ALL" 12 | line="{{ server_user }} ALL=(ALL) ALL" 13 | state=present 14 | 15 | - name: Limit su access to sudo group 16 | ansible.builtin.command: dpkg-statoverride --update --add root sudo 4750 /bin/su 17 | register: limit_su_res 18 | failed_when: limit_su_res.rc != 0 and ("already exists" not in limit_su_res.stderr) 19 | changed_when: limit_su_res.rc == 0 20 | 21 | - name: Disallow root SSH access 22 | ansible.builtin.lineinfile: dest=/etc/ssh/sshd_config 23 | regexp="^PermitRootLogin" 24 | line="PermitRootLogin no" 25 | state=present 26 | notify: restart ssh 27 | 28 | - name: Delete root password 29 | action: shell passwd -d root 30 | tags: skip_ansible_lint 31 | changed_when: false 32 | -------------------------------------------------------------------------------- /roles/security/tasks/force_ssh_authentication.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Add authorized_keys for the user 4 | ansible.posix.authorized_key: 5 | user: "{{ server_user }}" 6 | key: "{{ lookup('file', item) }}" 7 | with_items: 8 | - "{{ user_public_keys }}" 9 | 10 | - name: Disallow password authentication 11 | ansible.builtin.lineinfile: dest=/etc/ssh/sshd_config 12 | regexp="^PasswordAuthentication" 13 | line="PasswordAuthentication no" 14 | state=present 15 | notify: restart ssh 16 | 17 | - name: Allow ssh only for primary user 18 | ansible.builtin.lineinfile: dest=/etc/ssh/sshd_config 19 | regexp="^AllowUsers" 20 | line="AllowUsers {{ server_user }}" 21 | state=present 22 | notify: restart ssh 23 | -------------------------------------------------------------------------------- /roles/security/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Perform aptitude dist-upgrade 4 | ansible.builtin.import_tasks: perform_aptitude_dist_upgrade.yml 5 | when: perform_aptitude_dist_upgrade is defined and perform_aptitude_dist_upgrade 6 | 7 | - name: Create non-root sudo user 8 | ansible.builtin.import_tasks: create_non_root_sudo_user.yml 9 | when: server_user != "root" 10 | 11 | - name: Force SSH Authentication 12 | ansible.builtin.import_tasks: force_ssh_authentication.yml 13 | when: force_ssh_authentication is defined and force_ssh_authentication 14 | 15 | - name: Setup Unattended Upgrades 16 | ansible.builtin.import_tasks: setup_unattended_upgrades.yml 17 | when: enable_unattended_upgrades is defined and enable_unattended_upgrades 18 | 19 | - name: Setup uncomplicated firewall 20 | ansible.builtin.import_tasks: setup_uncomplicated_firewall.yml 21 | when: enable_ufw is defined and enable_ufw 22 | # TODO: Re-enable this test when we figure out how to integrate ipv6 support with GitHub Actions. 23 | tags: molecule-notest 24 | 25 | - name: Setup Fail2Ban 26 | ansible.builtin.import_tasks: setup_fail2ban.yml 27 | when: enable_fail2ban is defined and enable_fail2ban 28 | -------------------------------------------------------------------------------- /roles/security/tasks/perform_aptitude_dist_upgrade.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Update APT packages cache 4 | ansible.builtin.apt: update_cache={{ update_apt_cache }} cache_valid_time=86400 5 | 6 | - name: Perform aptitude dist-upgrade 7 | ansible.builtin.apt: upgrade=dist 8 | -------------------------------------------------------------------------------- /roles/security/tasks/setup_fail2ban.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install fail2ban 4 | ansible.builtin.apt: update_cache={{ update_apt_cache }} state=present pkg=fail2ban 5 | 6 | - name: Set up fail2ban 7 | ansible.builtin.command: cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local 8 | creates=/etc/fail2ban/jail.local 9 | notify: restart fail2ban 10 | -------------------------------------------------------------------------------- /roles/security/tasks/setup_unattended_upgrades.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install Unattended Upgrades 4 | ansible.builtin.apt: update_cache={{ update_apt_cache }} state=present pkg=unattended-upgrades 5 | 6 | - name: Set up unattended upgrades 7 | ansible.builtin.copy: 8 | src=apt_periodic 9 | dest=/etc/apt/apt.conf.d/10periodic 10 | mode="0644" 11 | 12 | - name: Automatically remove unused dependencies 13 | ansible.builtin.lineinfile: dest=/etc/apt/apt.conf.d/50unattended-upgrades 14 | regexp="Unattended-Upgrade::Remove-Unused-Dependencies" 15 | line="Unattended-Upgrade::Remove-Unused-Dependencies \"true\";" 16 | state=present 17 | create=yes 18 | mode="0644" 19 | -------------------------------------------------------------------------------- /roles/security/tasks/setup_uncomplicated_firewall.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install Uncomplicated Firewall 4 | ansible.builtin.apt: update_cache={{ update_apt_cache }} state=present pkg=ufw 5 | 6 | # Allow only ssh and http(s) ports 7 | - name: Allow ssh and http(s) connections 8 | community.general.ufw: rule=allow port={{ item }} 9 | with_items: 10 | - "{{ ufw_allowed_ports }}" 11 | 12 | - name: Enable ufw/firewall 13 | community.general.ufw: state=enabled policy=deny 14 | -------------------------------------------------------------------------------- /roles/security/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | user_public_keys: 4 | - ~/.ssh/id_rsa.pub 5 | 6 | ufw_allowed_ports: 7 | - '22' 8 | - '80' 9 | - '443' 10 | 11 | shell: "/bin/bash" 12 | -------------------------------------------------------------------------------- /roles/web/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # Virtualenv settings. 4 | enable_deadsnakes_ppa: false 5 | recreate_virtualenv: false 6 | virtualenv_python_version: "{{ base_python_package }}" 7 | 8 | 9 | # Application settings. 10 | virtualenv_path: "/webapps/{{ application_name }}" 11 | project_path: "{{ virtualenv_path }}/{{ project_name }}" 12 | application_log_dir: "{{ virtualenv_path }}/logs" 13 | application_log_file: "{{ application_log_dir }}/gunicorn_supervisor.log" 14 | requirements_file: "{{ project_path }}/requirements.txt" 15 | pip_use_upgrade_flag: false 16 | 17 | 18 | # Gunicorn settings. 19 | gunicorn_user: "{{ application_name }}" 20 | gunicorn_group: webapps 21 | 22 | 23 | # Nginx settings. 24 | nginx_static_dir: "{{ virtualenv_path }}/static/" 25 | nginx_media_dir: "{{ virtualenv_path }}/media/" 26 | 27 | 28 | # Django environment variables. 29 | django_environment: 30 | DJANGO_SETTINGS_MODULE: "{{ django_settings_file }}" 31 | DJANGO_SECRET_KEY: "{{ django_secret_key }}" 32 | MEDIA_ROOT: "{{ nginx_media_dir }}" 33 | STATIC_ROOT: "{{ nginx_static_dir }}" 34 | DATABASE_NAME: "{{ db_name }}" 35 | DATABASE_USER: "{{ db_user }}" 36 | DATABASE_PASSWORD: "{{ db_password }}" 37 | BROKER_URL: "{{ broker_url }}" 38 | -------------------------------------------------------------------------------- /roles/web/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Restart application 4 | community.general.supervisorctl: name={{ application_name }} state=restarted 5 | -------------------------------------------------------------------------------- /roles/web/tasks/create_users_and_groups.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Create the application user 4 | ansible.builtin.user: name={{ gunicorn_user }} state=present 5 | 6 | - name: Create the application group 7 | ansible.builtin.group: name={{ gunicorn_group }} system=yes state=present 8 | 9 | - name: Add the application user to the application group 10 | ansible.builtin.user: name={{ gunicorn_user }} group={{ gunicorn_group }} state=present 11 | -------------------------------------------------------------------------------- /roles/web/tasks/install_additional_packages.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Add deadsnakes team New Python Versions PPA to the apt sources list 4 | ansible.builtin.apt_repository: repo='ppa:deadsnakes/ppa' 5 | update_cache={{ update_apt_cache }} 6 | state=present 7 | when: enable_deadsnakes_ppa 8 | 9 | - name: Install additional packages 10 | ansible.builtin.apt: 11 | update_cache: "{{ update_apt_cache }}" 12 | state: present 13 | name: 14 | - libcurl4-gnutls-dev 15 | - gnutls-dev 16 | - libpq-dev 17 | - "{{ virtualenv_python_version + '-dev' }}" 18 | -------------------------------------------------------------------------------- /roles/web/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - ansible.builtin.import_tasks: install_additional_packages.yml 4 | tags: packages 5 | 6 | - ansible.builtin.import_tasks: create_users_and_groups.yml 7 | 8 | - ansible.builtin.import_tasks: set_file_permissions.yml 9 | tags: deploy 10 | 11 | - ansible.builtin.import_tasks: setup_virtualenv.yml 12 | tags: virtualenv 13 | 14 | - ansible.builtin.import_tasks: setup_git_repo.yml 15 | tags: deploy 16 | 17 | - ansible.builtin.import_tasks: setup_django_app.yml 18 | tags: deploy 19 | 20 | - ansible.builtin.import_tasks: setup_supervisor.yml 21 | tags: supervisor 22 | -------------------------------------------------------------------------------- /roles/web/tasks/set_file_permissions.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Ensure that the application file permissions are set properly 4 | ansible.builtin.file: path={{ virtualenv_path }} 5 | recurse=yes 6 | owner={{ gunicorn_user }} 7 | group={{ gunicorn_group }} 8 | state=directory 9 | changed_when: false 10 | -------------------------------------------------------------------------------- /roles/web/tasks/setup_django_app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install packages required by the Django app inside virtualenv 4 | ansible.builtin.pip: 5 | virtualenv: "{{ virtualenv_path }}" 6 | requirements: "{{ requirements_file }}" 7 | extra_args: "{{ pip_use_upgrade_flag | ternary('--upgrade', omit) }}" 8 | notify: Restart application 9 | 10 | - name: Run the Django syncdb command 11 | community.general.django_manage: 12 | command: syncdb 13 | app_path: "{{ project_path }}" 14 | virtualenv: "{{ virtualenv_path }}" 15 | settings: "{{ django_settings_file }}" 16 | environment: "{{ django_environment }}" 17 | when: run_django_syncdb is defined and run_django_syncdb 18 | tags: django.syncdb 19 | 20 | - name: Run Django database migrations 21 | community.general.django_manage: 22 | command: migrate 23 | app_path: "{{ project_path }}" 24 | virtualenv: "{{ virtualenv_path }}" 25 | settings: "{{ django_settings_file }}" 26 | environment: "{{ django_environment }}" 27 | when: run_django_db_migrations is defined and run_django_db_migrations 28 | tags: django.migrate 29 | 30 | - name: Run Django collectstatic 31 | community.general.django_manage: 32 | command: collectstatic 33 | app_path: "{{ project_path }}" 34 | virtualenv: "{{ virtualenv_path }}" 35 | settings: "{{ django_settings_file }}" 36 | environment: "{{ django_environment }}" 37 | when: run_django_collectstatic is defined and run_django_collectstatic 38 | notify: Restart application 39 | tags: django.collectstatic 40 | -------------------------------------------------------------------------------- /roles/web/tasks/setup_git_repo.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # Make sure the sudoers file preserves the ability to use ssh forwarding. 4 | # That way we don't need to store a private key on the server to get 5 | # access to the git repository. Don't forget to add the key used by the 6 | # git repository to your ssh-agent using ssh-add on the machine where you 7 | # run the playbooks. 8 | # 9 | # https://stackoverflow.com/questions/24124140/ssh-agent-forwarding-with-ansible 10 | 11 | - name: Add ssh agent line to sudoers 12 | ansible.builtin.lineinfile: 13 | dest: /etc/sudoers 14 | state: present 15 | regexp: SSH_AUTH_SOCK 16 | line: Defaults env_keep += "SSH_AUTH_SOCK" 17 | when: ssh_forward_agent is defined and ssh_forward_agent 18 | tags: git 19 | 20 | - name: Setup the Git repo 21 | # Specifying user to prevent Git directory ownership warnings, via this SO question: 22 | # https://stackoverflow.com/a/73356197/2532070 23 | # The question says this does not work but it does seem to work. 24 | become_user: "{{ gunicorn_user }}" 25 | become: true 26 | ansible.builtin.git: 27 | repo: "{{ git_repo }}" 28 | version: "{{ git_branch }}" 29 | dest: "{{ project_path }}" 30 | accept_hostkey: true 31 | when: setup_git_repo is defined and setup_git_repo 32 | notify: Restart application 33 | tags: git 34 | 35 | - name: Delete all .pyc files 36 | ansible.builtin.command: find . -name '*.pyc' -delete 37 | args: 38 | chdir: "{{ project_path }}" 39 | tags: git 40 | changed_when: false 41 | -------------------------------------------------------------------------------- /roles/web/tasks/setup_supervisor.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Ensure the Supervisor service is running 4 | ansible.builtin.service: 5 | name: supervisor 6 | state: started 7 | enabled: true 8 | # TODO: This is likely due to a bug in Ansible. 9 | # Remove this line in the future. 10 | # See https://github.com/ansible/ansible/issues/75005 11 | use: sysvinit 12 | 13 | - name: Create the Supervisor config file 14 | ansible.builtin.template: src=supervisor_config.j2 15 | dest=/etc/supervisor/conf.d/{{ application_name }}.conf 16 | backup=yes 17 | mode="0644" 18 | 19 | - name: Re-read the Supervisor config files 20 | community.general.supervisorctl: name={{ application_name }} state=present 21 | -------------------------------------------------------------------------------- /roles/web/tasks/setup_virtualenv.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install virtualenv 4 | ansible.builtin.pip: 5 | name: virtualenv 6 | version: 20.24.6 7 | extra_args: --break-system-packages 8 | tags: packages 9 | 10 | - name: Check if Supervisor exists 11 | ansible.builtin.stat: path=/etc/init.d/supervisor 12 | register: supervisor_status 13 | when: recreate_virtualenv 14 | 15 | - name: Ensure all processes managed by Supervisor are stopped if exists 16 | community.general.supervisorctl: 17 | name: all 18 | state: stopped 19 | when: recreate_virtualenv and supervisor_status.stat.exists 20 | 21 | - name: Ensure no existing virtualenv exists 22 | ansible.builtin.file: 23 | state: absent 24 | path: "{{ virtualenv_path }}/" 25 | when: recreate_virtualenv 26 | 27 | - name: Create the virtualenv 28 | ansible.builtin.command: virtualenv -p {{ virtualenv_python_version }} {{ virtualenv_path }} 29 | args: 30 | creates: "{{ virtualenv_path }}/bin/activate" 31 | tags: 32 | - packages 33 | 34 | - name: Upgrade pip 35 | ansible.builtin.pip: 36 | virtualenv: "{{ virtualenv_path }}" 37 | name: pip 38 | state: latest 39 | tags: 40 | - packages 41 | 42 | - name: Ensure gunicorn and pycurl are installed in the virtualenv 43 | ansible.builtin.pip: 44 | virtualenv: "{{ virtualenv_path }}" 45 | name: 46 | - gunicorn 47 | - pycurl 48 | 49 | - name: Create the Gunicorn script file 50 | ansible.builtin.template: src=gunicorn_start.j2 51 | dest={{ virtualenv_path }}/bin/gunicorn_start 52 | owner={{ gunicorn_user }} 53 | group={{ gunicorn_group }} 54 | mode=0755 55 | backup=yes 56 | tags: deploy 57 | 58 | - name: Create the application log folder 59 | ansible.builtin.file: 60 | path: "{{ application_log_dir }}" 61 | owner: "{{ gunicorn_user }}" 62 | group: "{{ gunicorn_group }}" 63 | mode: "0664" 64 | state: directory 65 | changed_when: false 66 | 67 | - name: Check for an existing application logfile 68 | ansible.builtin.stat: 69 | path: "{{ application_log_file }}" 70 | register: p 71 | 72 | - name: Create (or retain) the application log file 73 | # Removing until https://github.com/ansible/ansible/issues/45530 gets resolved. 74 | # ansible.builtin.copy: content="" 75 | # dest={{ application_log_file }} 76 | # owner={{ gunicorn_user }} 77 | # group={{ gunicorn_group }} 78 | # mode=0664 79 | # force=no 80 | ansible.builtin.file: 81 | path: "{{ application_log_file }}" 82 | owner: "{{ gunicorn_user }}" 83 | group: "{{ gunicorn_group }}" 84 | mode: 0664 85 | state: '{{ "file" if p.stat.exists else "touch" }}' 86 | 87 | - name: Create the virtualenv postactivate script to set environment variables 88 | ansible.builtin.template: src=virtualenv_postactivate.j2 89 | dest={{ virtualenv_path }}/bin/postactivate 90 | owner={{ gunicorn_user }} 91 | group={{ gunicorn_group }} 92 | mode=0640 93 | backup=yes 94 | notify: Restart application 95 | tags: deploy 96 | 97 | - name: Create the maintenance page 98 | ansible.builtin.template: src=maintenance_off.html 99 | dest={{ virtualenv_path }}/maintenance_off.html 100 | mode=0664 101 | -------------------------------------------------------------------------------- /roles/web/templates/gunicorn_start.j2: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | NAME="{{ application_name }}" 4 | DJANGODIR="{{ project_path }}" 5 | SOCKFILE={{ virtualenv_path }}/run/gunicorn.sock 6 | USER={{ gunicorn_user }} 7 | GROUP={{ gunicorn_group }} 8 | NUM_WORKERS={{ gunicorn_num_workers }} 9 | 10 | # Set this to 0 for unlimited requests. During development, you might want to 11 | # set this to 1 to automatically restart the process on each request (i.e. your 12 | # code will be reloaded on every request). 13 | MAX_REQUESTS={{ gunicorn_max_requests }} 14 | 15 | echo "Starting $NAME as `whoami`" 16 | 17 | # Activate the virtual environment. 18 | cd $DJANGODIR 19 | . {{ virtualenv_path }}/bin/activate 20 | 21 | # Set additional environment variables. 22 | . {{ virtualenv_path }}/bin/postactivate 23 | 24 | # Create the run directory if it doesn't exist. 25 | RUNDIR=$(dirname $SOCKFILE) 26 | test -d $RUNDIR || mkdir -p $RUNDIR 27 | 28 | # Programs meant to be run under supervisor should not daemonize themselves 29 | # (do not use --daemon). 30 | exec gunicorn \ 31 | --name $NAME \ 32 | --workers $NUM_WORKERS \ 33 | --max-requests $MAX_REQUESTS \ 34 | --timeout {{ gunicorn_timeout_seconds|default(30) }} \ 35 | --user $USER --group $GROUP \ 36 | --log-level debug \ 37 | --bind unix:$SOCKFILE \ 38 | {{ application_name }}.wsgi -------------------------------------------------------------------------------- /roles/web/templates/maintenance_off.html: -------------------------------------------------------------------------------- 1 | 2 | Site Maintenance 3 | 11 | 12 |
13 |

We’ll be back soon!

14 |
15 |

Sorry for the inconvenience but we’re performing some maintenance at the moment. If you need to you can always contact us, otherwise we’ll be back online shortly!

16 |

— The Team

17 |
18 |
19 | -------------------------------------------------------------------------------- /roles/web/templates/supervisor_config.j2: -------------------------------------------------------------------------------- 1 | [program:{{ application_name }}] 2 | command = {{ virtualenv_path }}/bin/gunicorn_start 3 | user = {{ gunicorn_user }} 4 | stdout_logfile = {{ application_log_file }} 5 | redirect_stderr = true -------------------------------------------------------------------------------- /roles/web/templates/virtualenv_postactivate.j2: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | {% for variable_name, value in django_environment.items() %} 4 | export {{ variable_name }}={{ value | quote }} 5 | {% endfor %} 6 | -------------------------------------------------------------------------------- /security.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Initial configuration for application server 4 | hosts: all 5 | become: true 6 | become_user: root 7 | remote_user: root 8 | vars: 9 | update_apt_cache: true 10 | vars_files: 11 | - roles/base/defaults/main.yml 12 | module_defaults: 13 | ansible.builtin.apt: 14 | force_apt_get: true 15 | 16 | roles: 17 | - security 18 | -------------------------------------------------------------------------------- /site.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - ansible.builtin.import_playbook: dbservers.yml 4 | 5 | - ansible.builtin.import_playbook: webservers.yml 6 | -------------------------------------------------------------------------------- /vagrant.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Create application virtual machine via vagrant 4 | hosts: all 5 | become: true 6 | become_user: root 7 | remote_user: vagrant 8 | vars: 9 | update_apt_cache: true 10 | module_defaults: 11 | ansible.builtin.apt: 12 | force_apt_get: true 13 | 14 | roles: 15 | - base 16 | - avahi 17 | - db 18 | - rabbitmq 19 | - web 20 | - celery 21 | - memcached 22 | - nginx 23 | -------------------------------------------------------------------------------- /webservers.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Provision application web server 4 | hosts: webservers 5 | become: true 6 | become_user: root 7 | remote_user: "{{ server_user }}" 8 | vars: 9 | update_apt_cache: true 10 | module_defaults: 11 | ansible.builtin.apt: 12 | force_apt_get: true 13 | 14 | roles: 15 | - base 16 | - certbot 17 | - rabbitmq 18 | - web 19 | - celery 20 | - memcached 21 | - nginx 22 | --------------------------------------------------------------------------------