Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an Ansible playbook for creating a droplet #37

Merged
merged 1 commit into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 25 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ prerequisites:

* Create a DigitalOcean API token, and pass it to the inventory generator by
setting the `DO_API_TOKEN` environment variable.
* If you are creating a new droplet, and want to configure DNS as well, then
create a CloudFlare API token, and pass it to the Ansible playbook by setting
the `CLOUDFLARE_TOKEN` environment variable.
* Set the vault decryption password of the Ansible vaulted file with our
secrets. This may be done by setting the `ANSIBLE_VAULT_PASSWORD_FILE`
environment variable to point to a file containing the password.
Expand Down Expand Up @@ -99,9 +102,11 @@ Naming
We follow a simplified version of the naming scheme on [this blog
post](https://mnx.io/blog/a-proper-server-naming-scheme/):

* Servers are named `<prefix>.matplotlib.org` in A records.
* Servers get a functional CNAME alias (e.g., `web01.matplotlib.org`).
* matplotlib.org is a CNAME to the functional CNAME of a server.
* Servers are named `<prefix>.matplotlib.org` in A records, pointing to the
IPv4 address of the droplet.
* Servers get a functional CNAME alias (e.g., `web01.matplotlib.org`) pointing
to the hostname `<prefix>.matplotlib.org`.
* matplotlib.org is a CNAME alias of the functional CNAME of a server.

We use [planets in our Solar System](https://namingschemes.com/Solar_System)
for the name prefix. When creating a new server, pick the next one in the list.
Expand All @@ -113,51 +118,34 @@ The summary of the initial setup is:

1. Create the droplet with monitoring and relevant SSH keys.
2. Assign new droplet to the matplotlib.org project and the Web firewall.
3. Grab the SSH host fingerprints.
4. Reboot.
3. Add DNS entries pointing to the server on CloudFlare.
4. Grab the SSH host fingerprints.
5. Reboot.

We currently use a simple $10 droplet from DigitalOcean. You can create one
from the control panel, or using the `doctl` utility. Be sure to enable
monitoring, and add the `website` tag and relevant SSH keys to the droplet. An
example of using `doctl` is the following:
We currently use a simple $12 droplet from DigitalOcean. You can create one
from the control panel, or using the `create.yml` Ansible playbook:

```
doctl compute droplet create \
--image fedora-35-x64 \
--region tor1 \
--size s-1vcpu-2gb \
--ssh-keys <key-id>,<key-id> \
--tag-name website \
--enable-monitoring \
venus.matplotlib.org
ansible-playbook create.yml
```

Note, you will have to use `doctl compute ssh-key list` to get the IDs of the
relevant SSH keys saved on DigitalOcean, and substitute them above. Save the ID
of the new droplet from the output, e.g., in:
This playbook will prompt you for 3 settings:

```
ID Name Public IPv4 Private IPv4 Public IPv6 Memory VCPUs Disk Region Image VPC UUID Status Tags Features Volumes
294098687 mpl.org 2048 1 50 tor1 Fedora 35 x64 new website monitoring,droplet_agent
```

the droplet ID is 294098687.
1. The host name of the droplet, which should follow the naming convention
above.
2. The functional CNAME alias of the droplet.
3. The names of SSH keys to add to the droplet.


You should also assign the new droplet to the `matplotlib.org` project and the
`Web` firewall:
You may also pass these directly to Ansible as:

```
doctl projects list
# Get ID of the matplotlib.org project from the output.
doctl projects resources assign <project-id> --resource=do:droplet:<droplet-id>


doctl compute firewall list
# Get ID of the Web firewall from the output.
doctl compute firewall add-droplets <firewall-id> --droplet-ids <droplet-id>
ansible-playbook create.yml --extra-vars "host=pluto functional=web99 ssh_keys='a b c'"
```

The playbook will create the server, as well as add DNS records on CloudFlare.
Note, you must set `DO_API_TOKEN` and `CLOUDFLARE_TOKEN` in the environment to
access these services.

Then, to ensure you are connecting to the expected server, you should grab the
SSH host keys via the DigitalOcean Droplet Console:

Expand All @@ -181,14 +169,6 @@ Finally, you should reboot the droplet. This is due to a bug in cloud-init on
DigitalOcean, which generates a new machine ID after startup, causing system
logs to be seem invisible.

DNS setup
---------

1. Add an A record for `<prefix>.matplotlib.org` to the IPv4 address of the new
droplet.
2. Add a CNAME record for `webNN.matplotlib.org` pointing to the given
`<prefix.matplotlib.org>`.

Running Ansible
---------------

Expand Down
1 change: 1 addition & 0 deletions collections/requirements.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
collections:
- name: ansible.posix
- name: community.general
version: ">=2.0.0"
- name: community.digitalocean
142 changes: 142 additions & 0 deletions create.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
- hosts: localhost
tasks:
- name: Gather information about DigitalOcean droplets
community.digitalocean.digital_ocean_droplet_info:
register: do_droplets
- name: Gather information about DigitalOcean SSH keys
community.digitalocean.digital_ocean_sshkey_info:
register: do_ssh_keys

- name: Print info on existing droplets
ansible.builtin.debug:
msg: >-
{{ item.name }}:
{{ item.networks.v4 | map(attribute='ip_address') | join(',') }}
loop: "{{ do_droplets.data }}"
loop_control:
label: "{{ item.id }}"

- name: "Enter name for new droplet (subdomain only)"
ansible.builtin.pause:
register: input_name
when: host is not defined

- name: "Enter functional name for new droplet (webNN)"
ansible.builtin.pause:
register: input_functional
when: functional is not defined

- name: Print available SSH public keys
ansible.builtin.debug:
msg: "{{ item.name}} {{ item.fingerprint }}"
loop: "{{ do_ssh_keys.data }}"
loop_control:
label: "{{ item.id }}"

- name: "Enter SSH key names for new droplet (space separated)"
ansible.builtin.pause:
register: input_ssh_keys
when: ssh_keys is not defined

- name: Set droplet facts
ansible.builtin.set_fact:
host: >-
{{
(host if host is defined else input_name.user_input) |
trim
}}
functional: >-
{{
(functional if functional is defined else input_functional.user_input) |
trim
}}
ssh_fingerprints: >-
{{
do_ssh_keys.data |
selectattr(
'name',
'in',
(ssh_keys if ssh_keys is defined
else input_ssh_keys.user_input) | split) |
map(attribute='fingerprint')
}}

- name: Verify droplet configuration
ansible.builtin.assert:
that:
- host in valid_planets
# Must not be an existing name.
- >-
do_droplets.data |
selectattr('name', 'equalto', '{{ host }}.matplotlib.org') |
count == 0
# TODO: Also check that functional name doesn't already exist.
- functional is regex('^web[0-9][0-9]$')
# At least 1 key, and same number as requested.
- ssh_fingerprints | length >= 1
- >-
ssh_fingerprints | length == (
ssh_keys if ssh_keys is defined
else input_ssh_keys.user_input) | split | length

- name: Print configuration
ansible.builtin.debug:
msg: "Creating droplet '{{ host }}' with SSH keys {{ ssh_fingerprints }}"

- name: Please verify the above configuration
ansible.builtin.pause:

- name: Create droplet on DigitalOcean
community.digitalocean.digital_ocean_droplet:
state: present
name: "{{ host }}.matplotlib.org"
firewall:
- Web
image: fedora-39-x64
monitoring: true
project: matplotlib.org
region: tor1
size: s-1vcpu-2gb
ssh_keys: "{{ ssh_fingerprints }}"
tags:
- website
unique_name: true
register: new_droplet

- name: Setup DNS for droplet on CloudFlare
community.general.cloudflare_dns:
state: present
proxied: true
record: "{{ host }}"
type: A
value: >-
{{
new_droplet.data.droplet.networks.v4 |
selectattr('type', 'equalto', 'public') |
map(attribute='ip_address') |
first
}}
zone: matplotlib.org

- name: Setup functional DNS for droplet on CloudFlare
community.general.cloudflare_dns:
state: present
proxied: true
record: "{{ functional }}"
type: CNAME
value: "{{ host }}.matplotlib.org"
zone: matplotlib.org

vars:
# We currently name servers based on planets in the Solar System.
valid_planets:
- mercury
- venus
- earth
- mars
- jupiter
- saturn
- uranus
- neptune
- pluto
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking a political stand?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh, I decided it's a nice short name to use for testing random things.