This commit is contained in:
notplants 2023-04-03 14:50:10 +05:30
parent e8446e5fc5
commit 9859e40a3f
18 changed files with 335 additions and 151 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
devops/secret_files/*
.idea*
*.pyc
hello_webapp.ini
devops/hosts
secret.json

152
README.md
View File

@ -1,151 +1 @@
# Ansible Flask Template
With this template I can quickly deploy a new webapp to the amazon cloud which logs error messages to slack, has database connectivity, and is configured by Ansible (allowing for idempotent server configuration — no need to remember server state).
I find this very useful for getting small applications up and running quickly. It is also useful for starting a new project to know from the onset that 'deployment' is already taken care of so I can focus on the actual project.
In the past without Ansible I would sometimes end up creating monolithic apps which share infrastructure to perform multiple functions — I used to do this because "solving deployment again" was an intimidating task and so I would tack more functionality onto existing infrastructure to save time.
Because this template allows me to quickly start projects with deployment already taken care of, it encourages me to make new projects completely independent with their own github repository and deployment server, leading to fully independent micro services that are more robust.
In a sense this template is like my own Heroku, but in addition to the convenience of quickly bringing up new machines I can extend and customize it, as well as save money by bringing up AWS micro instances for free.
## Steps To Deploy
### 1. Edit devops/vars.yaml
This file contains the parameters which control Ansible.
- **app_name**: unimportant, but is used for naming of some files (probably should just keep it as hello_webapp)
- **repo_url**: change this to the url of your github repository (e.g. git@github.com:mhfowler/alembic_flask_ansible_ec2_template.git)
- **repo_remote**: git remote to use on server (e.g. origin)
- **repo_branch**: git branch to use on server (e.g. master)
- **src_dir**: the path to where the webapp will be stored on server (probably shouldn't change)
- **log_dir**: the path to where logs will be written on the server (probably shouldn't change)
- **aws_key_name**: the name of the AWS ssh key which Ansible will use for authentication (must already exist in amazon)
- **aws_security_group**: the security group which the spawned server will belong to (security group must already exist)
- **aws_instance_name**: the tag which the spawned server will be given — this is important for identifying your new server in the AWS console (e.g. aws_default)
- **aws_key_location**: the path on your local computer to the SSH private key associated with aws_key_name listed above — this file must already exist on your computer
- **aws_subnet**: the aws subnet which the spawned server will belong to (this subnet must already exist in your amazon)
- **prod_url**: this attribute is not used. I included it because when I add cron jobs via ansible I often make use of this (e.g. http://test.com/)
### 2. Create devops/secret_files/secret.json
All the files contained within devops/secret_files are ignored from git because they contain passwords and other secret information.
The Ansible recipe copies the files in secret_files to the server (to the same relative location) during the deploy task, so that these files can be referenced relatively and expected to exist both locally and on the server.
devops/secret_files/secret.json must contain valid json with a number of keys which are used throughout the application.
My secret.json looks something like this:
```_
{
"SLACKBOT_TOKEN": "XXXX",
"SLACK_CHANNEL_ID": "XXXXXX"
"AWS_ACCESS_KEY_ID": "XXXX",
"AWS_SECRET_ACCESS_KEY": "XXXXX",
"TEST_DB_CONNECTION": "XXXXX",
}
```
SLACKBOT_TOKEN is for authenticating with slack (see hello_utilities/slack_helper.py https://api.slack.com/)
SLACK_CHANNEL_ID is the slack channel id which slack_helper logs to by default.
AWS_ACCESS_KEY_ID and AWS_SECRET_ACESS_KEY are your AWS authentication credentials.
TEST_DB_CONNECTION is a string for authenticating with the database. See get_db_url() in _hello_settings.py for how to configure this to be different for different environments.
### 3. Generate SSH Deploy Keys
Run `devops/generate_deploy_ssh_keys.sh`
As a location for the key to be written, choose, secret_files/deploy_rsa — this will create two files, one in devops/secret_files/deploy_rsa and one in devops/secret_files/deploy_rsa.pub
In the Deploy Keys tab of the settings page of your github repository, copy and paste deploy_rsa.pub as your deploy key.
These deploy keys will be copied to the server as part of the Ansible deployment and will be used to pull the github repository.
### 4. Initialize Alembic
Whatever database you have configured to connect to via the string TEST_DB_CONNECTION in secret.json must already exist.
This project uses alembic to manage schema migrations.
To initialize your database with the correct tables run: `alembic upgrade head`
In the future to auto-generate new migrations run: `alembic revision --autogenerate -m "initial tables"`
Then inspect the migration in alembic/versions/ and then run: `alembic upgrade head` to run the migration.
### 5. Spawn A Server
`./spawn_server.sh`
https://open.spotify.com/track/1S8FBwS475qrBhJhWtqeiP
If everything is correct, Ansible will spawn a new micro instance in the amazon cloud and then will deploy your repository to the created instance and configure it to have a live nginx server sering your flask web app.
After the spawn_server.yml stage of spawn_server.sh a new line will be added to devops/hosts which contains the IP address of your new server. devops/hosts should look something like
```
[webservers]
52.87.226.172 ansible_ssh_user=ubuntu ansible_ssh_private_key_file=<local__path_to_your_aws_private_ssh_key>
```
This IP address will also be printed out on the last line of the Ansible log to std.out.
### 6. Test
In your web browser visit the IP address of your newly created instance &mdash; it should say 'hello hello'.
Visit /slack/ you should receive a slack message.
Visit /error/ this test page will force a 500 error which should log a message to slack with the error (to test that error logging is working).
Visit /test_db/ everytime you refresh this page a new random value should appear (as a new value is logged to the database)
## Deploying New Code
Whenever you want to deploy new code:
```
git push origin master
./deploy.sh;
```
deploy.sh will deploy your new code to the server as well as do some configuration steps which often need to happen again after changes (e.g. copy secret.json, install python requirements etc.)
Conventionally setup_server.sh contains configurations tasks which only need to be run once (for the server intialization) and spawn_server.sh is only run when you want to bring up a new machine.
## Troubleshooting
The first step of spawn_server.sh is to spawn a new server. If that succeeds, then there will be a new line in devops/hosts (you can also confirm this machine's existence in the AWS EC2 console).
To troubleshot spawn_server.sh confirm that a new machine has been created, and then you can try individually running setup_server.sh and deploy.sh to isolate problems (these steps are also attempted when you run spawn_server.sh).
## Design Ideas
#### bash scripts as buttons
I think of bash scripts in my repository as buttons which I am adding to my IDE. Right click and then click Run to use them.
#### secret.json
Any keys and passwords which can't be saved in Git should be stored in devops/secret_files/secret.json.
Ansible will make sure this file is in the same relative location on the server as it is locally, and every run of deploy.sh will copy this file over to the server. Throughout your code you can access the contents of secret.json by importing SECRETS_DICT from hello_settings.py_
#### hello_settings.py
hello_settings.py is the genesis point from which all else flows. hello_settings.py should not import from any other files.
#### hello_utilities
I try to isolate functionality into small files which can function independently. Each of these files goes in hello_utilities. I use the \__main\__ method at the bottom of these files to write 'tests' which can be verified by running the files.
#### idempotent server configuration
ansible has been revolutionary for my personal projects because it allows me to say what my code is as well as what environment my code requires.
Because ansible playbooks are idempotent, if your recipes are written well, you no longer need to remember server state, you just need to know that when you re-run ansible it will make your server what it is supposed to be.
# Spawn BBB Droplet

2
deploy.sh Executable file
View File

@ -0,0 +1,2 @@
#!/usr/bin/env bash
ansible-playbook -i devops/hosts devops/deploy.yml

64
devops/deploy.yml Normal file
View File

@ -0,0 +1,64 @@
---
- hosts: webservers
user: ubuntu
sudo: True
tasks:
- include_vars: vars.yaml
- name: ensure log directory
action: file dest={{log_dir}} state=directory
- name: deploy code from repository
git: repo={{repo_url}} dest={{src_dir}} remote={{repo_remote}} version={{repo_branch}} accept_hostkey=yes
notify:
- restart nginx
- restart webapp
- name: install python requirements
action: pip requirements={{src_dir}}/requirements.txt state=present
- name: copy hellow_webapp.ini
action: template src=templates/hello_webapp.ini dest={{src_dir}}/hello_webapp.ini
- name: create nginx site config
action: template src=templates/nginx_site.conf dest=/etc/nginx/sites-available/{{app_name}}.conf
notify:
- restart nginx
- name: link nginx config
action: file src=/etc/nginx/sites-available/{{app_name}}.conf dest=/etc/nginx/sites-enabled/{{app_name}}.conf state=link
- name: create upstart script for webapp
action: template src=templates/hello_webapp.conf dest=/etc/init/hello_webapp.conf
- name: ensure secrets directory
action: file dest={{src_dir}}/devops/secret_files state=directory
- name: Copy secret.json file
copy: src=secret_files/secret.json dest={{src_dir}}/devops/secret_files/secret.json
- name: make src_dir writeable by webgroup
action: file path={{src_dir}} mode=u=rwX,g=rwX,o=X recurse=yes group=webgroup
- name: make log_dir writeable by webgroup
action: file path={{log_dir}} mode=u=rwX,g=rwX,o=X recurse=yes group=webgroup
# - name: crontab to check alerts
# cron: name="check alerts" minute="*" job="curl {{prod_url}}/get_all_tix/"
- name: restart server and webapp
command: /bin/true
notify:
- restart nginx
- restart webapp
handlers:
- name: restart nginx
action: service name=nginx state=restarted
- name: restart webapp
action: service name={{app_name}} state=restarted

View File

@ -0,0 +1,2 @@
#!/usr/bin/env bash
ssh-keygen -t rsa -b 4096 -C "maxhfowler@gmail.com"

85
devops/setup_server.yml Normal file
View File

@ -0,0 +1,85 @@
---
- hosts: webservers
user: ubuntu
sudo: True
tasks:
- include_vars: vars.yaml
- name: add nginx ppa
action: apt_repository repo=ppa:nginx/stable state=present
- name: install common packages needed for python application development
action: apt pkg=$item state=installed
with_items:
- libpq-dev
- libmysqlclient-dev
- libxml2-dev
- libjpeg62
- libjpeg62-dev
- libfreetype6
- libfreetype6-dev
- zlib1g-dev
- mysql-client
- python-dev
- python-setuptools
- python-imaging
- python-mysqldb
- python-psycopg2
- git-core
- nginx
- name: install pip
action: easy_install name=pip
- name: install virtualenv and uwsgi
action: pip name={{item.name}} version={{item.version}}
with_items:
- { name: 'virtualenv', version: '14.0.6' }
- { name: 'uwsgi', version: '2.0.12' }
- name: symlink imaging library files
action: file src=/usr/lib/x86_64-linux-gnu/libfreetype.so dest=/usr/lib/libfreetype.so state=link
- name: symlink imaging library files
action: file src=/usr/lib/x86_64-linux-gnu/libz.so dest=/usr/lib/libz.so state=link
- name: symlink imaging library files
action: file src=/usr/lib/x86_64-linux-gnu/libjpeg.so.62 dest=/usr/lib/x86_64-linux-gnu/libjpeg.so state=link
- name: symlink imaging library files
action: file src=/usr/lib/x86_64-linux-gnu/libjpeg.so dest=/usr/lib/libjpeg.so state=link
- name: remove default nginx site
action: file path=/etc/nginx/sites-enabled/default state=absent
- name: write nginx.conf
action: template src=templates/nginx.conf dest=/etc/nginx/nginx.conf
- name: create webgroup if it doesn't exist
group: name=webgroup state=present
tags:
- debug
- name: ensure wsgi-user belongs to webgroup
user: name=wsgi-user groups=webgroup append=yes
tags:
- debug
- name: ensure wsgi-user belongs to webgroup
user: name=www-data groups=webgroup append=yes
tags:
- debug
- name: ensure ubuntu belongs to webgroup
user: name=ubuntu groups=webgroup append=yes
tags:
- debug
- name: copy over ssh keys for deploy purposes
action: copy src={{item.from}} dest={{item.to}} mode={{item.mode}}
with_items:
- { from: 'secret_files/deploy_rsa.pub', to: '/root/.ssh/id_rsa.pub', mode: '0644' }
- { from: 'secret_files/deploy_rsa', to: '/root/.ssh/id_rsa', mode: '0600' }

9
devops/spawn_droplet.py Normal file
View File

@ -0,0 +1,9 @@
import os
from hello_settings import SECRETS_DICT, PROJECT_PATH
if __name__ == '__main__':
os.environ['DO_API_TOKEN'] = SECRETS_DICT['DO_API_TOKEN']
spawn_droplet_yml = os.path.join(PROJECT_PATH, 'devops/spawn_droplet.yml')
os.system('ansible-playbook {}'.format(spawn_droplet_yml))

46
devops/spawn_droplet.yml Normal file
View File

@ -0,0 +1,46 @@
---
- hosts: localhost
gather_facts: false
connection: local
vars:
oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
# Create a droplet with ssh key
# The ssh key id can be passed as argument at the creation of a droplet (see ssh_key_ids).
# Several keys can be added to ssh_key_ids as id1,id2,id3
# The keys are used to connect as root to the droplet.
tasks:
# - name: Create SSH key
# community.digitalocean.digital_ocean_sshkey:
# oauth_token: "{{ oauth_token }}"
# name: spawnkey
# ssh_pub_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDOsnpwZF3FI1wlYwCM0JmO1X4N13JUspUuLUVs3BEcec1X1pa+qEgq3B3C/sPzbSCk0uXTFVpgIzyOqDpyY6BvwV2pebSucHDt26h5W3Z8vAmbpjB4ylGeg+D8K5PJBzI6FH1Ln8vVtQ5gCm0qE/i3jTfedG9E0QkkkFmfJRlUyo6YGJcH3EMiiCwfhfEbW4Ys2Q3Wd7p/kxefWTxQFghmz3Na8WLSPlflgRtex426JD4lIJuUCxleklqfgSM7Y0op6f4UVX1j5OgBn24Mjmj0VDLkKuHIR/ic39/ptl1E+vZ/c/27Lq1upsBf7pkhngvpvUE5HKO9MbsFaKM2jvhB email@mfowler.info"
# state: present
# register: spawnkey
#
# - name: Create SSH key
# ansible.builtin.debug:
# msg: |
# Key ID is "{{ spawnkey.data.ssh_key.id }}"
- name: Create a new Droplet
community.digitalocean.digital_ocean_droplet:
oauth_token: "{{ oauth_token }}"
state: present
name: mydroplet
unique_name: true
size: s-1vcpu-1gb
region: sfo3
image: ubuntu-20-04-x64
wait_timeout: 500
ssh_keys:
- "30840574"
register: my_droplet
- name: Show Droplet info
ansible.builtin.debug:
msg: |
Droplet ID is {{ my_droplet.data.droplet.id }}
First Public IPv4 is {{ (my_droplet.data.droplet.networks.v4 | selectattr('type', 'equalto', 'public')).0.ip_address | default('<none>', true) }}
First Private IPv4 is {{ (my_droplet.data.droplet.networks.v4 | selectattr('type', 'equalto', 'private')).0.ip_address | default('<none>', true) }}

40
devops/spawn_server.yml Normal file
View File

@ -0,0 +1,40 @@
---
- hosts: localhost
connection: local
gather_facts: False
tasks:
- include_vars: vars.yaml
- name: Provision a new instance
ec2:
key_name: "{{aws_key_name}}"
instance_type: t2.micro
image: ami-fce3c696
wait: yes
group: "{{aws_security_group}}"
count: 1
vpc_subnet_id: "{{aws_subnet}}"
assign_public_ip: yes
region: us-east-1
instance_tags:
Name: "{{aws_instance_name}}"
register: ec2
- name: Add the newly created EC2 instance(s) to the local host group (located inside the directory)
local_action: lineinfile
dest="./hosts"
regexp={{ item.public_ip }}
insertafter="[webservers]" line="{{ item.public_ip }} ansible_ssh_user=ubuntu ansible_ssh_private_key_file={{aws_key_location}}"
with_items: ec2.instances
- name: Wait for SSH to come up
wait_for: host={{ item.public_dns_name }} port=22 delay=60 timeout=320 state=started
with_items: ec2.instances
- name: add ec2 hosts to known hosts
local_action: command ssh -o StrictHostKeyChecking=no ubuntu@{{ item.public_ip }} -i {{aws_key_location}}
with_items: ec2.instances

View File

@ -0,0 +1,10 @@
description "uWSGI server instance configured to serve hello_webapp"
start on runlevel [2345]
stop on runlevel [!2345]
setuid wsgi-user
setgid webgroup
chdir {{src_dir}}
exec uwsgi --ini hello_webapp.ini

View File

@ -0,0 +1,31 @@
user www-data webgroup;
worker_processes 1;
worker_rlimit_nofile 8192;
events {
worker_connections 3000;
}
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

View File

@ -0,0 +1,10 @@
server {
listen 80;
server_name ec2-52-90-110-188.compute-1.amazonaws.com;
location / {
include uwsgi_params;
uwsgi_pass unix:{{src_dir}}/{{app_name}}.sock;
}
}

1
devops/vars.yaml Normal file
View File

@ -0,0 +1 @@
app_name: hello_webapp

14
hello_settings.py Normal file
View File

@ -0,0 +1,14 @@
import os, json
# project path
PROJECT_PATH = os.path.abspath(os.path.dirname(__file__))
print('PROJECT_PATH: {}'.format(PROJECT_PATH))
# secrets dict
SECRETS_PATH = os.path.join(PROJECT_PATH, 'secret.json')
SECRETS_DICT = json.loads(open(SECRETS_PATH, "r").read())
# temporary settings below

1
requirements.txt Normal file
View File

@ -0,0 +1 @@

3
requirements.yml Normal file
View File

@ -0,0 +1,3 @@
---
collections:
- name: community.digitalocean

2
setup_server.sh Executable file
View File

@ -0,0 +1,2 @@
#!/usr/bin/env bash
ansible-playbook -i devops/hosts devops/setup_server.yml

8
spawn_server.sh Normal file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
SCRIPTPATH=$( cd $(dirname $0) ; pwd -P )
export PYTHONPATH=$SCRIPTPATH:$PYTHONPATH
cd $SCRIPTPATH/devops
python spawn_droplet.py
sleep 5
#ansible-playbook -i hosts setup_server.yml
#ansible-playbook -i hosts deploy.yml