From e272779cd07bfcaf37f2cf4b57231ec49d0a8285 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 19 Dec 2023 17:36:29 -0500 Subject: [PATCH] Add an Ansible playbook for creating a droplet --- README.md | 70 ++++++----------- collections/requirements.yml | 1 + create.yml | 142 +++++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 45 deletions(-) create mode 100644 create.yml diff --git a/README.md b/README.md index ca91e52..1c9e883 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 `.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 `.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 `.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. @@ -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 , \ - --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 --resource=do:droplet: - - -doctl compute firewall list -# Get ID of the Web firewall from the output. -doctl compute firewall add-droplets --droplet-ids +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: @@ -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 `.matplotlib.org` to the IPv4 address of the new - droplet. -2. Add a CNAME record for `webNN.matplotlib.org` pointing to the given - ``. - Running Ansible --------------- diff --git a/collections/requirements.yml b/collections/requirements.yml index cb8953c..4a795f3 100644 --- a/collections/requirements.yml +++ b/collections/requirements.yml @@ -2,4 +2,5 @@ collections: - name: ansible.posix - name: community.general + version: ">=2.0.0" - name: community.digitalocean diff --git a/create.yml b/create.yml new file mode 100644 index 0000000..f23dbdd --- /dev/null +++ b/create.yml @@ -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