├── handlers └── main.yml ├── vars └── main.yml ├── tasks ├── action-migrate.yml ├── pip.yml ├── action-run-cmd.yml ├── action-deploy.yml ├── main.yml ├── postgres-facts.yml ├── asdf.yml ├── common.yml ├── create-swap-file.yml ├── action-remove-app.yml ├── deployer-user.yml ├── app-facts.yml ├── action-setup.yml ├── nginx.yml ├── postgres.yml ├── release.yml ├── monit.yml ├── frontend.yml └── project.yml ├── templates ├── nginx.monit.j2 ├── default-prod.secret.exs.j2 ├── app.monit.j2 └── app.nginx.j2 ├── docs ├── logs.md ├── actions.md ├── hot-code-reloading.md ├── prod-secret-file.md └── configuration.md ├── meta └── main.yml ├── defaults └── main.yml ├── elixir-stack.sh ├── README.md └── library └── monit.py /handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # No handlers yet 3 | -------------------------------------------------------------------------------- /vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # vars file for elixir-stack 3 | -------------------------------------------------------------------------------- /tasks/action-migrate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: migrate database 4 | command: bash -lc "mix ecto.migrate" chdir="{{ project_path }}" 5 | remote_user: "{{ deployer }}" 6 | environment: 7 | MIX_ENV: "{{ mix_env }}" 8 | -------------------------------------------------------------------------------- /tasks/pip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "download pip installer script" 3 | get_url: url="https://bootstrap.pypa.io/get-pip.py" dest="/tmp/get-pip.py" mode=0777 4 | 5 | 6 | - name: "install pip" 7 | command: "python /tmp/get-pip.py" 8 | -------------------------------------------------------------------------------- /templates/nginx.monit.j2: -------------------------------------------------------------------------------- 1 | check process nginx with pidfile /var/run/nginx.pid 2 | start program = "/etc/init.d/nginx start" 3 | stop program = "/etc/init.d/nginx stop" 4 | {% for alert_email in nginx_alert_emails %} 5 | alert {{ alert_email }} 6 | {% endfor %} 7 | -------------------------------------------------------------------------------- /tasks/action-run-cmd.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: run command 4 | command: bash -lc "{{ cmd }}" chdir="{{ project_path }}" 5 | remote_user: "{{ deployer }}" 6 | environment: 7 | MIX_ENV: "{{ mix_env }}" 8 | register: cmd_output 9 | 10 | - debug: msg="{{ cmd_output.stdout_lines }}" 11 | -------------------------------------------------------------------------------- /docs/logs.md: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | As of now there's no log rotation. Feel free to send a PR (please test it also). 4 | 5 | The logs are available at 6 | 7 | * app log - `/home/deployer/projects//rel//log` 8 | * Nginx access log - `/var/log/nginx/access.log` 9 | * Nginx error log - `/var/log/nginx/error.log` 10 | -------------------------------------------------------------------------------- /tasks/action-deploy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - when: setup_postgres == True 3 | include: postgres-facts.yml 4 | 5 | 6 | - include: project.yml 7 | 8 | 9 | - when: build_frontend == True 10 | include: frontend.yml 11 | 12 | 13 | - include: release.yml 14 | 15 | 16 | - name: start nginx using monit 17 | monit: name="nginx" state=started 18 | -------------------------------------------------------------------------------- /tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - when: action == "remove-app" 3 | include: action-remove-app.yml 4 | 5 | - when: action == "setup" 6 | include: action-setup.yml 7 | 8 | - when: action == "deploy" 9 | include: action-deploy.yml 10 | 11 | - when: action == "migrate" 12 | include: action-migrate.yml 13 | 14 | - when: action == "run-cmd" 15 | include: action-run-cmd.yml 16 | -------------------------------------------------------------------------------- /templates/default-prod.secret.exs.j2: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :{{app_name}}, {{ app_endpoint_module }}, 4 | secret_key_base: "{{ secret_key_base }}" 5 | 6 | 7 | config :{{ app_name }}, {{ app_repo_module }}, 8 | adapter: Ecto.Adapters.Postgres, 9 | username: "{{ database_user }}", 10 | password: "{{ database_password }}", 11 | database: "{{ database_name }}", 12 | size: 20 13 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | author: Akash Manohar 4 | description: Tool to setup servers for Elixir & Phoenix framework apps 5 | license: MIT 6 | min_ansible_version: 1.9 7 | 8 | platforms: 9 | - name: Ubuntu 10 | versions: 11 | - all 12 | 13 | categories: 14 | - cloud 15 | - development 16 | - monitoring 17 | - system 18 | - web 19 | 20 | dependencies: [] 21 | -------------------------------------------------------------------------------- /tasks/postgres-facts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - when: "database_name is not defined" 3 | name: "calculate database name" 4 | set_fact: "database_name={{ app_name }}_prod" 5 | 6 | 7 | - name: "create or get postgres password" 8 | set_fact: database_password="{{ lookup('password', '~/credentials/' + deployer + '_postgres length=15') }}" 9 | 10 | 11 | - name: "set database user" 12 | set_fact: database_user="{{ deployer }}" 13 | -------------------------------------------------------------------------------- /templates/app.monit.j2: -------------------------------------------------------------------------------- 1 | check process {{app_name}} MATCHING "{{app_name}}/releases" 2 | start program = "/bin/su - {{ deployer }} -c 'PORT={{ app_port }} {{project_path}}/rel/{{app_name}}/bin/{{app_name}} start'" 3 | stop program = "/bin/su - {{ deployer }} -c 'PORT={{ app_port }} {{project_path}}/rel/{{app_name}}/bin/{{app_name}} stop'" 4 | {% for alert_email in app_alert_emails %} 5 | alert {{ alert_email }} 6 | {% endfor %} 7 | -------------------------------------------------------------------------------- /tasks/asdf.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "install asdf" 3 | git: repo="https://github.com/HashNuke/asdf.git" dest="~/.asdf" update=yes 4 | remote_user: "{{deployer}}" 5 | 6 | 7 | - name: "source asdf in bashrc" 8 | lineinfile: dest="~/.bash_profile" create=yes line="source ~/.asdf/asdf.sh" 9 | remote_user: "{{deployer}}" 10 | 11 | 12 | - name: "add asdf plugins" 13 | command: "bash -lc 'asdf plugin-add {{item}} https://github.com/HashNuke/asdf-{{item}}.git'" 14 | with_items: 15 | - nodejs 16 | - erlang 17 | - elixir 18 | remote_user: "{{deployer}}" 19 | -------------------------------------------------------------------------------- /tasks/common.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "install system packages" 3 | apt: name="{{ item }}" update_cache=yes state=present 4 | with_items: 5 | - gcc 6 | - g++ 7 | - curl 8 | - wget 9 | - unzip 10 | - git 11 | - python-dev 12 | - python-apt 13 | - make 14 | - automake 15 | - autoconf 16 | - libreadline-dev 17 | - libncurses-dev 18 | - libssl-dev 19 | - libyaml-dev 20 | - libxslt-dev 21 | - libffi-dev 22 | - libtool 23 | - unixodbc-dev 24 | 25 | - name: should have credentials dir 26 | file: path=~/credentials state=directory 27 | -------------------------------------------------------------------------------- /templates/app.nginx.j2: -------------------------------------------------------------------------------- 1 | upstream {{app_name}} { 2 | server localhost:{{app_port}}; 3 | } 4 | 5 | 6 | server { 7 | root {{ project_path }}/priv/static; 8 | listen 80; 9 | {% if domains | length != 0 %} 10 | server_name {% for domain in domains %} {{ domain }}{% endfor %}; 11 | {% endif %} 12 | 13 | 14 | location / { 15 | proxy_pass http://{{ app_name }}; 16 | proxy_set_header Host $host; 17 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 18 | 19 | proxy_http_version 1.1; 20 | proxy_set_header Upgrade $http_upgrade; 21 | proxy_set_header Connection "upgrade"; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /tasks/create-swap-file.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: create file 3 | file: path=/swap state=touch mode=0600 4 | 5 | - name: write blocks 6 | command: "dd if=/dev/zero of=/swap bs=1024 count={{ swap_size }}" 7 | 8 | - name: setup swap area in /swap 9 | command: "mkswap /swap" 10 | 11 | - name: swapon 12 | command: "swapon /swap" 13 | 14 | - name: append swap info to fstab 15 | lineinfile: dest=/etc/fstab line="/swap none swap sw 0 0" 16 | 17 | - name: set swappiness in /proc/sys/vm/swappiness 18 | command: bash -lc "echo 10 | sudo tee /proc/sys/vm/swappiness" 19 | 20 | - name: set swappiness in /etc/sysctl.conf 21 | command: bash -lc "echo vm.swappiness = 10 | sudo tee -a /etc/sysctl.conf" 22 | -------------------------------------------------------------------------------- /tasks/action-remove-app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: stop app 3 | monit: name="{{ app_name }}" state=stopped 4 | ignore_errors: True 5 | 6 | 7 | - name: delete project dir 8 | file: path="{{ project_path }}" state=absent 9 | ignore_errors: True 10 | 11 | 12 | - name: remove nginx config for app 13 | file: path="{{ item }}" state=absent 14 | with_items: 15 | - "/etc/nginx/sites-available/{{ app_name }}.nginx" 16 | - "/etc/nginx/sites-enabled/{{ app_name }}.nginx" 17 | ignore_errors: True 18 | 19 | 20 | - name: reload nginx 21 | command: "nginx -s reload" 22 | 23 | 24 | - name: remove monit config for app 25 | file: path="/etc/monit/conf.d/{{ app_name }}.monit" state=absent 26 | ignore_errors: True 27 | 28 | 29 | - name: reload monit 30 | service: name=monit state=reloaded 31 | -------------------------------------------------------------------------------- /tasks/deployer-user.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "create deployer user" 3 | user: name="{{deployer}}" shell=/bin/bash 4 | 5 | 6 | - name: "read authorized keys from root user" 7 | command: "cat ~/.ssh/authorized_keys" 8 | register: "root_authorized_keys" 9 | 10 | 11 | - name: "create .ssh dir for deployer" 12 | file: path="/home/{{ deployer }}/.ssh" state=directory 13 | 14 | 15 | - name: "copy authorized keys to deployer user" 16 | shell: "echo '{{root_authorized_keys.stdout}}' > /home/{{deployer}}/.ssh/authorized_keys" 17 | 18 | 19 | - name: "chown the authorized_keys file" 20 | file: path="/home/{{deployer}}/.ssh" recurse=yes mode=0700 owner="{{ deployer }}" 21 | 22 | 23 | - name: "ensure projects directory" 24 | file: path="~/projects" state=directory 25 | remote_user: "{{ deployer }}" 26 | -------------------------------------------------------------------------------- /tasks/app-facts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "generate secret key base" 3 | command: "openssl rand -base64 {{ secret_key_base_length }}" 4 | register: openssl_random_string 5 | 6 | 7 | - name: set secret_key_base 8 | set_fact: secret_key_base="{{ openssl_random_string.stdout }}" 9 | 10 | 11 | - name: detect endpoint module 12 | command: "grep -m 1 -o '[[:alnum:]]*.Endpoint'" {{ project_path }}/config/dev.exs 13 | register: grep_endpoint_module 14 | 15 | 16 | - name: set app_endpoint_module fact 17 | set_fact: app_endpoint_module="{{ grep_endpoint_module.stdout }}" 18 | 19 | 20 | - name: detect repo module 21 | command: grep -m 1 -o '[[:alnum:]]*.Repo' {{ project_path }}/config/dev.exs 22 | register: grep_repo_module 23 | 24 | 25 | - name: set app_repo_module fact 26 | set_fact: app_repo_module="{{ grep_repo_module.stdout }}" 27 | -------------------------------------------------------------------------------- /tasks/action-setup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - include: common.yml 3 | 4 | - name: "check for swap file path" 5 | stat: path="/swap" 6 | register: swap_info 7 | 8 | 9 | - when: create_swap_file == True and swap_info.stat.exists == False 10 | include: create-swap-file.yml 11 | 12 | 13 | - include: pip.yml 14 | - include: deployer-user.yml 15 | - include: asdf.yml 16 | 17 | 18 | - when: setup_postgres == True 19 | include: postgres-facts.yml 20 | 21 | 22 | - when: setup_postgres == True 23 | include: postgres.yml 24 | 25 | 26 | - include: project.yml 27 | 28 | 29 | - when: build_frontend == True 30 | include: frontend.yml 31 | 32 | 33 | - include: nginx.yml 34 | - include: monit.yml 35 | - include: release.yml 36 | 37 | 38 | - name: start nginx using monit 39 | monit: name="nginx" state=started 40 | 41 | 42 | - name: reload nginx config 43 | service: name=nginx state=reloaded 44 | -------------------------------------------------------------------------------- /tasks/nginx.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: add nginx pkg repo 3 | apt_repository: repo="ppa:nginx/stable" 4 | 5 | 6 | - name: install nginx 7 | apt: name=nginx update_cache=yes state=present 8 | 9 | 10 | - name: "disable nginx and don't start service on reboot" 11 | service: name=nginx enabled=no state=stopped 12 | 13 | 14 | - name: remove nginx default configs 15 | file: name="{{ item }}" state=absent 16 | with_items: 17 | - /etc/nginx/sites-available/default 18 | - /etc/nginx/sites-enabled/default 19 | 20 | 21 | - name: add nginx config for elixir app 22 | template: 23 | src: app.nginx.j2 24 | dest: "/etc/nginx/sites-available/{{ app_name }}.nginx" 25 | 26 | 27 | - name: enable elixir app to be served by nginx 28 | file: 29 | src: "/etc/nginx/sites-available/{{ app_name }}.nginx" 30 | dest: "/etc/nginx/sites-enabled/{{ app_name }}.nginx" 31 | state: link 32 | -------------------------------------------------------------------------------- /tasks/postgres.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "add postgres repository" 3 | apt_repository: repo="deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main" 4 | 5 | 6 | - name: "add postgres repository key" 7 | apt_key: url="https://www.postgresql.org/media/keys/ACCC4CF8.asc" 8 | 9 | 10 | - name: "install postgres & libpq-dev" 11 | apt: name="{{ item }}" update_cache=yes state=present 12 | with_items: 13 | - postgresql-9.4 14 | - libpq-dev 15 | 16 | 17 | - name: install psycopg2 python module 18 | pip: name=psycopg2 19 | 20 | 21 | - name: create postgres user for deployer 22 | postgresql_user: 23 | name: "{{ deployer }}" 24 | password: "{{ database_password }}" 25 | role_attr_flags: CREATEDB,SUPERUSER 26 | sudo: yes 27 | sudo_user: postgres 28 | 29 | 30 | - name: "create database" 31 | postgresql_db: name="{{ database_name }}" encoding="UTF-8" 32 | sudo: yes 33 | sudo_user: postgres 34 | -------------------------------------------------------------------------------- /defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | git_ref: "master" 3 | 4 | projects_dir: "/home/{{ deployer }}/projects" 5 | project_path: "{{ projects_dir }}/{{ app_name }}" 6 | 7 | deploy_type: "restart" 8 | 9 | mix_env: "prod" 10 | setup_postgres: True 11 | 12 | build_frontend: True 13 | frontend_dir: "" 14 | frontend_build_command: "$(npm bin)/brunch build --production" 15 | post_frontend_build: "mix phoenix.digest" 16 | 17 | deployer: deployer 18 | 19 | domains: [] 20 | app_alert_emails: [] 21 | nginx_alert_emails: [] 22 | 23 | create_swap_file: True 24 | 25 | # swap size in 1kb blocks 26 | # we'll set the default at around 2gb 27 | # 2048mb * 1024kb == 2097152 28 | # which is approximately 2097 thousand ( == 2097k) 29 | swap_size: 2097K 30 | 31 | npm_config_jobs: 1 32 | npm_config_install_production: False 33 | secret_key_base_length: 64 34 | 35 | enable_mail_alerts: False 36 | smtp_port: 587 37 | smtp_use_tls: False 38 | 39 | # PRIVATE 40 | smtp_tls_option: "" 41 | npm_install_options: "" 42 | -------------------------------------------------------------------------------- /docs/actions.md: -------------------------------------------------------------------------------- 1 | # Actions 2 | 3 | Below are a list of stuff you can do with the provided playbooks. All commands are meant to be run in your project's root directory. 4 | 5 | 6 | #### Setup server 7 | 8 | ``` 9 | $ ansible-playbook playbooks/setup.yml 10 | ``` 11 | 12 | The app is also deployed for the first time. So when this command completes, you should have your app running. 13 | 14 | #### Deploy 15 | 16 | ``` 17 | $ ansible-playbook playbooks/deploy.yml 18 | ``` 19 | 20 | #### Migrate database 21 | 22 | ``` 23 | $ ansible-playbook playbooks/migrate.yml 24 | ``` 25 | 26 | The ecto.migrate task is run as the deployer user, with the MIX_ENV specified in your configuration in playbooks/vars/main.yml. 27 | 28 | #### Remove app from the server 29 | 30 | ``` 31 | $ ansible-playbook playbooks/remove-app.yml 32 | ``` 33 | 34 | #### Run command in the project's directory on the server 35 | 36 | ``` 37 | $ ansible-playbook playbooks/run-cmd.yml -e "cmd='foo bar'" 38 | ``` 39 | 40 | The command is run as the deployer user, with the MIX_ENV specified in your configuration in playbooks/vars/main.yml. 41 | -------------------------------------------------------------------------------- /tasks/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - when: deploy_type == "restart" 3 | name: stop app 4 | monit: name="{{ app_name }}" state=stopped 5 | 6 | 7 | - when: deploy_type == "restart" 8 | name: delete release 9 | command: bash -lc "mix release.clean" chdir="{{ project_path }}" 10 | remote_user: "{{ deployer }}" 11 | 12 | 13 | - name: "compile and release" 14 | command: bash -lc 'SERVER=1 mix do compile, release' chdir="{{ project_path }}" 15 | remote_user: "{{ deployer }}" 16 | environment: 17 | MIX_ENV: "{{ mix_env }}" 18 | PORT: "{{ app_port }}" 19 | 20 | 21 | - when: deploy_type == "restart" 22 | name: start app 23 | monit: name="{{ app_name }}" state=started 24 | 25 | 26 | - when: deploy_type == "upgrade" 27 | name: get app version 28 | command: bash -lc "mix run -e 'IO.puts Mix.Project.config[:version]'" chdir="{{ project_path }}" 29 | remote_user: "{{ deployer }}" 30 | register: app_version 31 | 32 | 33 | - when: deploy_type == "upgrade" 34 | name: set upgrade command 35 | set_fact: upgrade_command='rel/{{ app_name }}/bin/{{ app_name }} upgrade "{{ app_version.stdout }}"' 36 | 37 | 38 | - when: deploy_type == "upgrade" 39 | name: upgrade app 40 | command: bash -lc "{{ upgrade_command }}" chdir="{{ project_path }}" 41 | remote_user: "{{ deployer }}" 42 | environment: 43 | MIX_ENV: "{{ mix_env }}" 44 | PORT: "{{ app_port }}" 45 | -------------------------------------------------------------------------------- /docs/hot-code-reloading.md: -------------------------------------------------------------------------------- 1 | # Hot code-reloading 2 | 3 | By default, apps are restarted when new versions are deployed. This is to make it easy for people to deploy apps quickly with quick setup. 4 | 5 | ### To enable 6 | 7 | * Set the `deploy_type` variable to `upgrade` in your project's `playbooks/vars/main.yml` file 8 | * Everytime you deploy, update the app's version in the project's `mix.exs` 9 | 10 | 11 | ### Automatic versioning using git 12 | 13 | For hot code-reloading, the app's version needs to be updated in `mix.exs` for every deploy. That can get repeatitive, since hobby projects are updated & deployed frequently. We've got a workaround for that. We'll use automatic versioning based on git commit SHAs. Technical details are explained in [this blog post](TODO). 14 | 15 | * Change the following in `mix.exs` 16 | 17 | ```elixir 18 | def project do 19 | [app: :hello_phoenix, 20 | version: "1.4.1", 21 | elixir: "~> 1.0", 22 | ... 23 | ``` 24 | 25 | to look like the following 26 | 27 | ```elixir 28 | def project do 29 | {result, _exit_code} = System.cmd("git", ["rev-parse", "HEAD"]) 30 | 31 | # We'll truncate the commit SHA to 7 chars. Feel free to change 32 | git_sha = String.slice(result, 0, 7) 33 | 34 | [app: :hello_phoenix, 35 | version: "1.4.1-#{git_sha}", 36 | elixir: "~> 1.0", 37 | ... 38 | ``` 39 | 40 | That just changes the `version` to use the git commit SHA. 41 | -------------------------------------------------------------------------------- /docs/prod-secret-file.md: -------------------------------------------------------------------------------- 1 | # prod.secret.exs 2 | 3 | This file is auto-generated when the app is setup. The following template is used. 4 | 5 | ```elixir 6 | use Mix.Config 7 | 8 | config {{app_name}}, {{ app_endpoint }}, 9 | secret_key_base: "{{ secret_key_base }}" 10 | 11 | 12 | config {{ app_name }}, {{ app_repo_module }}, 13 | adapter: Ecto.Adapters.Postgres, 14 | username: "{{ database_user }}", 15 | password: "{{ database_password }}", 16 | database: "{{ database_name }}", 17 | size: 20 18 | ``` 19 | 20 | **You can over-ride this default** template by placing your own template as `playbooks/templates/prod.secret.exs.j2` in your project. This file uses [Jinga2](http://jinja.pocoo.org) templating engine that Ansible uses. You can quickly identify that variables are surrounded with `{{` and `}}`. That all you need to know to work with this file. 21 | 22 | The following variables are made available (calculated approximately): 23 | 24 | * `app_name` - This is the OTP app name that you set in `playbooks/vars/main.yml` 25 | * `app_endpoint_module` - The endpoint module to serve 26 | * `secret_key_base` - A secret key base is generated on the server 27 | * `app_repo_module` - The repo module the app uses 28 | * `database_user` - Database user (same as the deployer variable) 29 | * `database_password` - auto-generated and stored on the server 30 | * `database_name` - calculated based on the mix_env. Format is `_` 31 | -------------------------------------------------------------------------------- /tasks/monit.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: install monit 3 | apt: name=monit update_cache=yes 4 | 5 | 6 | - name: allow localhost access in monit config 7 | lineinfile: dest=/etc/monit/monitrc line="{{ item }}" 8 | with_items: 9 | - "set httpd port 2812 and" 10 | - "use address localhost" 11 | - "allow localhost" 12 | 13 | 14 | - when: enable_mail_alerts == True and smtp_use_tls == True 15 | name: set tls_option 16 | set_fact: smtp_tls_option="using tlsv1" 17 | 18 | 19 | - when: enable_mail_alerts == True and smtp_use_tls != True 20 | name: set tls_option 21 | set_fact: smtp_tls_option="" 22 | 23 | 24 | - when: enable_mail_alerts == True 25 | name: set mail server for notifications in monit 26 | lineinfile: dest=/etc/monit/monitrc line="{{ item }}" 27 | with_items: 28 | - "set mailserver {{ smtp_host }} port {{ smtp_port }} username {{ smtp_user }} password {{ smtp_password }} {{ smtp_tls_option }} with timeout 30 seconds" 29 | 30 | 31 | - name: start monit & mark to be started on system reboots 32 | service: name=monit state=started enabled=yes 33 | 34 | 35 | - name: add monit config for elixir app 36 | template: 37 | src: app.monit.j2 38 | dest: "/etc/monit/conf.d/{{ app_name }}.monit" 39 | 40 | 41 | - name: add monit config for nginx 42 | template: 43 | src: nginx.monit.j2 44 | dest: "/etc/monit/conf.d/nginx.monit" 45 | 46 | - name: reload monit 47 | service: name=monit state=reloaded 48 | -------------------------------------------------------------------------------- /tasks/frontend.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "set npm jobs config" 3 | command: bash -lc "npm config set jobs {{ npm_config_jobs }}" 4 | remote_user: "{{ deployer }}" 5 | args: 6 | chdir: "{{ project_path }}/{{ frontend_dir }}" 7 | 8 | 9 | - when: npm_config_install_production == True 10 | name: calculate npm install options 11 | set_fact: npm_install_options="--production" 12 | 13 | 14 | - name: "fetch npm dependencies" 15 | command: bash -lc "npm install {{npm_install_options}}" chdir="{{ project_path }}/{{ frontend_dir }}" 16 | remote_user: "{{ deployer }}" 17 | async: 1800 18 | 19 | 20 | - name: check for bower.json 21 | stat: path="{{ project_path }}/{{frontend_dir}}/bower.json" 22 | register: bower_json_file 23 | 24 | 25 | - when: bower_json_file.stat.exists == True 26 | name: install bower dependencies 27 | command: bash -lc "$(npm bin)/bower install" 28 | remote_user: "{{ deployer }}" 29 | args: 30 | chdir: "{{ project_path }}/{{ frontend_dir }}" 31 | 32 | 33 | - name: "build frontend assets" 34 | command: bash -lc "{{ frontend_build_command }}" 35 | remote_user: "{{ deployer }}" 36 | args: 37 | chdir: "{{ project_path }}/{{ frontend_dir }}" 38 | 39 | 40 | - when: "post_frontend_build != False" 41 | name: "post-frontend-build command" 42 | command: 'bash -lc "{{ post_frontend_build }}"' 43 | remote_user: "{{ deployer }}" 44 | args: 45 | chdir: "{{ project_path }}" 46 | environment: 47 | MIX_ENV: "{{ mix_env }}" 48 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration options 2 | 3 | Variables are set in the `playbooks/vars/main.yml` file in your project. A few variables are mandatory and others have convenient defaults. Refer to the list of variables below for details. 4 | 5 | ## Required config 6 | 7 | ##### app_name 8 | 9 | Name of your OTP app. This is the name of your project in `mix.exs` (the value of the `app` key in the `project` function). 10 | 11 | * Example values: `foo`, `bar` 12 | 13 | Let's say your app's mix.exs looks like this: 14 | 15 | ```elixir 16 | defmodule Firebrick.Mixfile do 17 | use Mix.Project 18 | 19 | def project do 20 | [app: :firebrick, 21 | version: "0.0.2", 22 | ... 23 | ``` 24 | 25 | `firebrick` is your app name here. 26 | 27 | > Do not use the hostname of your server as the app's name. Monit will have conflicts and error out. 28 | 29 | ##### repo_url 30 | 31 | Git url of your project 32 | 33 | * Example values: 34 | * `https://example.com/foo/bar.git` 35 | * `foo@example.com:bar.git` 36 | 37 | ##### app_port 38 | 39 | Must be set to an integer. Port must not be used by any apps already on the same server. 40 | 41 | > Suggestion: Start somewhere at 3001 and keep incrementing it for every app you deploy. Easier to keep track. 42 | 43 | ##### domains 44 | 45 | The domains/subdomains to use for your project. By default your project is accessible from the IP address. Using projects without domains isn't recommended since Nginx will only serve the first project for an IP address. If you intend to host multiple projects on a server, use domain or subdomain names. 46 | 47 | * Default: `[]` 48 | * Example values: 49 | * `["example.com"]` 50 | * `["example.com", "foo.example.com", "bar.example.com"]` 51 | 52 | 53 | ## Other options 54 | 55 | There is a tonne of other options. All of them along with explanation can be found in the [defaults/vars.yml](https://github.com/HashNuke/ansible-elixir-stack/blob/master/defaults/vars.yml) file 56 | -------------------------------------------------------------------------------- /elixir-stack.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | app_name=$(grep -m 1 -oh 'app: :[[:alnum:]_]*' mix.exs | sed 's/app:\ ://') 4 | git_repo_url=$(git config --get remote.origin.url) 5 | mkdir -p playbooks/vars playbooks/templates 6 | 7 | cat > playbooks/setup.yml < playbooks/deploy.yml < playbooks/migrate.yml < playbooks/remove-app.yml < inventory < ansible.cfg < playbooks/vars/main.yml <> config/config.exs < .tool-versions < To deploy to Heroku, use the [Heroku Elixir buildpack](https://github.com/HashNuke/heroku-buildpack-elixir) instead. 19 | 20 | ## Install 21 | 22 | ```sh 23 | $ pip install ansible 24 | $ ansible-galaxy install HashNuke.elixir-stack 25 | 26 | # assuming your SSH key is called `id_rsa` 27 | # run this everytime you start your computer 28 | $ ssh-add ~/.ssh/id_rsa 29 | ``` 30 | 31 | > If the above commands fail, try with `sudo`. 32 | > For Mac OS X, Ansible is also available on homebrew. 33 | 34 | ## Setup your project 35 | 36 | 1.) Add [exrm](https://github.com/bitwalker/exrm) as your project's dependency in mix.exs 37 | 38 | ```elixir 39 | defp deps do 40 | [{:exrm, "~> 0.18.1"}] 41 | end 42 | ``` 43 | 44 | 2.) In your project dir, run following command: 45 | 46 | ```sh 47 | $ curl -L http://git.io/ansible-elixir-stack.sh | bash 48 | ``` 49 | 50 | **FOLLOW INSTRUCTIONS OF ABOVE COMMAND** 51 | 52 | > Checkout the [documentation on configuration options](docs/configuration.md) 53 | 54 | ## Deploy your project 55 | 56 | Assuming you have root SSH access to the server 57 | 58 | ##### To deploy the first time 59 | 60 | ```sh 61 | $ ansible-playbook playbooks/setup.yml 62 | ``` 63 | 64 | ##### To update your project 65 | 66 | ```sh 67 | $ ansible-playbook playbooks/deploy.yml 68 | ``` 69 | 70 | > By default the application is restarted on each deploy. [Read how to enable hot code-reloading](docs/hot-code-reloading.md). 71 | 72 | ## FAQ 73 | 74 | * **Is this only meant for small $5 servers?** 75 | Should fit servers of any size. In that case you could also increase the swap and npm 76 | 77 | * **How to have different set of servers for staging and production?** 78 | Use the `inventory` file as a template and maintain different inventory files for staging and production. Let's say your staging inventory file is called `staging.inventory`, then you could do `ansible-playbook setup.yml -i staging.inventory` (and similar for deploy). Notice the `-i` switch. 79 | *B/w if you are going this way, you probably should learn Ansible or hire someone who knows it* 80 | 81 | 82 | ## Misc 83 | 84 | * [ansible-galaxy guide](http://docs.ansible.com/galaxy.html#installing-roles) 85 | -------------------------------------------------------------------------------- /library/monit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # (c) 2013, Darryl Stoflet 5 | # 6 | # This file is part of Ansible 7 | # 8 | # Ansible is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # Ansible is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with Ansible. If not, see . 20 | # 21 | 22 | DOCUMENTATION = ''' 23 | --- 24 | module: monit 25 | short_description: Manage the state of a program monitored via Monit 26 | description: 27 | - Manage the state of a program monitored via I(Monit) 28 | version_added: "1.2" 29 | options: 30 | name: 31 | description: 32 | - The name of the I(monit) program/process to manage 33 | required: true 34 | default: null 35 | state: 36 | description: 37 | - The state of service 38 | required: true 39 | default: null 40 | choices: [ "present", "started", "stopped", "restarted", "monitored", "unmonitored", "reloaded" ] 41 | requirements: [ ] 42 | author: "Darryl Stoflet (@dstoflet)" 43 | ''' 44 | 45 | EXAMPLES = ''' 46 | # Manage the state of program "httpd" to be in "started" state. 47 | - monit: name=httpd state=started 48 | ''' 49 | 50 | def main(): 51 | arg_spec = dict( 52 | name=dict(required=True), 53 | state=dict(required=True, choices=['present', 'started', 'restarted', 'stopped', 'monitored', 'unmonitored', 'reloaded']) 54 | ) 55 | 56 | module = AnsibleModule(argument_spec=arg_spec, supports_check_mode=True) 57 | 58 | name = module.params['name'] 59 | state = module.params['state'] 60 | 61 | MONIT = module.get_bin_path('monit', True) 62 | 63 | if state == 'reloaded': 64 | if module.check_mode: 65 | module.exit_json(changed=True) 66 | rc, out, err = module.run_command('%s reload' % MONIT) 67 | if rc != 0: 68 | module.fail_json(msg='monit reload failed', stdout=out, stderr=err) 69 | module.exit_json(changed=True, name=name, state=state) 70 | 71 | def status(): 72 | """Return the status of the process in monit, or the empty string if not present.""" 73 | rc, out, err = module.run_command('%s summary' % MONIT, check_rc=True) 74 | for line in out.split('\n'): 75 | # Sample output lines: 76 | # Process 'name' Running 77 | # Process 'name' Running - restart pending 78 | parts = line.split() 79 | if len(parts) > 2 and parts[0].lower() == 'process' and parts[1] == "'%s'" % name: 80 | return ' '.join(parts[2:]).lower() 81 | else: 82 | return '' 83 | 84 | def run_command(command): 85 | """Runs a monit command, and returns the new status.""" 86 | module.run_command('%s %s %s' % (MONIT, command, name), check_rc=True) 87 | return status() 88 | 89 | process_status = status() 90 | present = process_status != '' 91 | 92 | if not present and not state == 'present': 93 | module.fail_json(msg='%s process not presently configured with monit' % name, name=name, state=state) 94 | 95 | if state == 'present': 96 | if not present: 97 | if module.check_mode: 98 | module.exit_json(changed=True) 99 | status = run_command('reload') 100 | if status == '': 101 | module.fail_json(msg='%s process not configured with monit' % name, name=name, state=state) 102 | else: 103 | module.exit_json(changed=True, name=name, state=state) 104 | module.exit_json(changed=False, name=name, state=state) 105 | 106 | running = 'running' in process_status 107 | 108 | if running and state in ['started', 'monitored']: 109 | module.exit_json(changed=False, name=name, state=state) 110 | 111 | if running and state == 'stopped': 112 | if module.check_mode: 113 | module.exit_json(changed=True) 114 | status = run_command('stop') 115 | if status in ['not monitored'] or 'stop pending' in status: 116 | module.exit_json(changed=True, name=name, state=state) 117 | module.fail_json(msg='%s process not stopped' % name, status=status) 118 | 119 | if running and state == 'unmonitored': 120 | if module.check_mode: 121 | module.exit_json(changed=True) 122 | status = run_command('unmonitor') 123 | if status in ['not monitored'] or 'unmonitor pending' in status: 124 | module.exit_json(changed=True, name=name, state=state) 125 | module.fail_json(msg='%s process not unmonitored' % name, status=status) 126 | 127 | elif state == 'restarted': 128 | if module.check_mode: 129 | module.exit_json(changed=True) 130 | status = run_command('restart') 131 | if status in ['initializing', 'running'] or 'restart pending' in status: 132 | module.exit_json(changed=True, name=name, state=state) 133 | module.fail_json(msg='%s process not restarted' % name, status=status) 134 | 135 | elif not running and state == 'started': 136 | if module.check_mode: 137 | module.exit_json(changed=True) 138 | status = run_command('start') 139 | if status in ['initializing', 'running'] or 'start pending' in status: 140 | module.exit_json(changed=True, name=name, state=state) 141 | module.fail_json(msg='%s process not started' % name, status=status) 142 | 143 | elif not running and state == 'monitored': 144 | if module.check_mode: 145 | module.exit_json(changed=True) 146 | status = run_command('monitor') 147 | if status not in ['not monitored']: 148 | module.exit_json(changed=True, name=name, state=state) 149 | module.fail_json(msg='%s process not monitored' % name, status=status) 150 | 151 | module.exit_json(changed=False, name=name, state=state) 152 | 153 | # import module snippets 154 | from ansible.module_utils.basic import * 155 | 156 | main() 157 | --------------------------------------------------------------------------------