403 lines
14 KiB
YAML
403 lines
14 KiB
YAML
---
|
|
- name: Install a verified GuestOps source release
|
|
hosts: guestops
|
|
become: true
|
|
gather_facts: true
|
|
|
|
vars:
|
|
guestops_root: /srv/guestops
|
|
guestops_config_root: /etc/guestops
|
|
guestops_public_hostname: sandbox-guestops.futuresens.co.uk
|
|
guestops_public_origin: "https://{{ guestops_public_hostname }}"
|
|
guestops_release_root: "{{ guestops_root }}/releases/{{ guestops_release_commit }}"
|
|
guestops_staging_archive: "/var/tmp/{{ guestops_archive | basename }}"
|
|
guestops_staging_record: "/var/tmp/{{ guestops_source_record | basename }}"
|
|
guestops_api_image: "guestops-api:{{ guestops_release_commit }}"
|
|
guestops_worker_image: "guestops-worker:{{ guestops_release_commit }}"
|
|
|
|
pre_tasks:
|
|
- name: Validate required release variables
|
|
ansible.builtin.assert:
|
|
that:
|
|
- guestops_release_version is match('^[0-9]+\\.[0-9]+\\.[0-9]+$')
|
|
- guestops_release_commit is match('^[0-9a-f]{40}$')
|
|
- guestops_archive_sha256 is match('^[0-9a-f]{64}$')
|
|
- guestops_archive | length > 0
|
|
- guestops_source_record | length > 0
|
|
- guestops_evidence_directory | length > 0
|
|
- guestops_public_hostname is match('^[A-Za-z0-9.-]+$')
|
|
- guestops_public_hostname == 'sandbox-guestops.futuresens.co.uk'
|
|
fail_msg: Release version, full commit, archive, SHA-256, source record and evidence directory are required.
|
|
|
|
- name: Verify source package on the Ansible controller
|
|
ansible.builtin.command:
|
|
argv:
|
|
- python3
|
|
- "{{ playbook_dir }}/../verify_source_package.py"
|
|
- --archive
|
|
- "{{ guestops_archive }}"
|
|
- --record
|
|
- "{{ guestops_source_record }}"
|
|
- --expected-commit
|
|
- "{{ guestops_release_commit }}"
|
|
- --expected-version
|
|
- "{{ guestops_release_version }}"
|
|
delegate_to: localhost
|
|
become: false
|
|
changed_when: false
|
|
|
|
- name: Check private GuestOps environment exists
|
|
ansible.builtin.stat:
|
|
path: "{{ guestops_config_root }}/guestops.env"
|
|
register: guestops_environment
|
|
|
|
- name: Require a private pre-provisioned environment file
|
|
ansible.builtin.assert:
|
|
that:
|
|
- guestops_environment.stat.exists
|
|
- guestops_environment.stat.isreg
|
|
- guestops_environment.stat.mode == '0600'
|
|
fail_msg: /etc/guestops/guestops.env must already exist with mode 0600.
|
|
|
|
tasks:
|
|
- name: Require supported Debian host
|
|
ansible.builtin.assert:
|
|
that:
|
|
- ansible_facts.system == 'Linux'
|
|
- ansible_facts.distribution == 'Debian'
|
|
- ansible_facts.distribution_major_version | int >= 12
|
|
- ansible_facts.processor_vcpus | default(0) | int >= 4
|
|
- ansible_facts.memtotal_mb | default(0) | int >= 7300
|
|
fail_msg: GuestOps requires Debian 12 or newer with at least 4 CPUs and 7.3 GiB RAM.
|
|
|
|
- name: Enable Docker at boot
|
|
ansible.builtin.service:
|
|
name: docker
|
|
enabled: true
|
|
state: started
|
|
|
|
- name: Enable Nginx at boot
|
|
ansible.builtin.service:
|
|
name: nginx
|
|
enabled: true
|
|
state: started
|
|
|
|
- name: Create release and evidence directories
|
|
ansible.builtin.file:
|
|
path: "{{ item.path }}"
|
|
state: directory
|
|
owner: root
|
|
group: root
|
|
mode: "{{ item.mode }}"
|
|
loop:
|
|
- { path: "{{ guestops_root }}/releases", mode: "0755" }
|
|
- { path: "{{ guestops_release_root }}", mode: "0755" }
|
|
- { path: "{{ guestops_config_root }}/release-records", mode: "0700" }
|
|
|
|
- name: Copy source archive to the host
|
|
ansible.builtin.copy:
|
|
src: "{{ guestops_archive }}"
|
|
dest: "{{ guestops_staging_archive }}"
|
|
owner: root
|
|
group: root
|
|
mode: "0600"
|
|
|
|
- name: Copy source record to the host
|
|
ansible.builtin.copy:
|
|
src: "{{ guestops_source_record }}"
|
|
dest: "{{ guestops_staging_record }}"
|
|
owner: root
|
|
group: root
|
|
mode: "0600"
|
|
|
|
- name: Calculate transferred archive checksum
|
|
ansible.builtin.stat:
|
|
path: "{{ guestops_staging_archive }}"
|
|
checksum_algorithm: sha256
|
|
register: guestops_transferred_archive
|
|
|
|
- name: Reject a changed transferred archive
|
|
ansible.builtin.assert:
|
|
that:
|
|
- guestops_transferred_archive.stat.checksum == guestops_archive_sha256
|
|
fail_msg: Transferred GuestOps archive does not match the approved SHA-256.
|
|
|
|
- name: Extract committed application files
|
|
ansible.builtin.unarchive:
|
|
src: "{{ guestops_staging_archive }}"
|
|
dest: "{{ guestops_release_root }}"
|
|
remote_src: true
|
|
extra_opts:
|
|
- --strip-components=1
|
|
|
|
- name: Verify transferred package and record on the target
|
|
ansible.builtin.command:
|
|
argv:
|
|
- python3
|
|
- deploy/verify_source_package.py
|
|
- --archive
|
|
- "{{ guestops_staging_archive }}"
|
|
- --record
|
|
- "{{ guestops_staging_record }}"
|
|
- --expected-commit
|
|
- "{{ guestops_release_commit }}"
|
|
- --expected-version
|
|
- "{{ guestops_release_version }}"
|
|
- --output
|
|
- "{{ guestops_config_root }}/release-records/{{ guestops_release_commit }}.source-verification.json"
|
|
chdir: "{{ guestops_release_root }}"
|
|
changed_when: false
|
|
|
|
- name: Retain approved source record on the target
|
|
ansible.builtin.copy:
|
|
src: "{{ guestops_staging_record }}"
|
|
dest: "{{ guestops_config_root }}/release-records/{{ guestops_release_commit }}.source.json"
|
|
remote_src: true
|
|
owner: root
|
|
group: root
|
|
mode: "0600"
|
|
|
|
- name: Select release images and safe pilot defaults in the private environment
|
|
ansible.builtin.lineinfile:
|
|
path: "{{ guestops_config_root }}/guestops.env"
|
|
regexp: "^{{ item.name }}="
|
|
line: "{{ item.name }}={{ item.value }}"
|
|
create: false
|
|
mode: "0600"
|
|
loop:
|
|
- { name: GUESTOPS_API_IMAGE, value: "{{ guestops_api_image }}" }
|
|
- { name: GUESTOPS_WORKER_IMAGE, value: "{{ guestops_worker_image }}" }
|
|
- { name: GOOGLE_ENABLE_SENDING, value: "false" }
|
|
- { name: AUTO_REPLY_ENABLE_LIVE, value: "false" }
|
|
no_log: true
|
|
|
|
- name: Copy private deployment environment into the release
|
|
ansible.builtin.copy:
|
|
src: "{{ guestops_config_root }}/guestops.env"
|
|
dest: "{{ guestops_release_root }}/.env"
|
|
remote_src: true
|
|
owner: root
|
|
group: root
|
|
mode: "0600"
|
|
|
|
- name: Build API image from the verified source
|
|
ansible.builtin.command:
|
|
argv:
|
|
- docker
|
|
- build
|
|
- --target
|
|
- api
|
|
- --tag
|
|
- "{{ guestops_api_image }}"
|
|
- .
|
|
chdir: "{{ guestops_release_root }}"
|
|
register: guestops_api_build
|
|
changed_when: true
|
|
|
|
- name: Build worker image from the verified source
|
|
ansible.builtin.command:
|
|
argv:
|
|
- docker
|
|
- build
|
|
- --target
|
|
- worker
|
|
- --tag
|
|
- "{{ guestops_worker_image }}"
|
|
- .
|
|
chdir: "{{ guestops_release_root }}"
|
|
register: guestops_worker_build
|
|
changed_when: true
|
|
|
|
- name: Inspect API image identity
|
|
ansible.builtin.command:
|
|
argv: [docker, image, inspect, --format, "{{ '{{.Id}}' }}", "{{ guestops_api_image }}"]
|
|
register: guestops_api_identity
|
|
changed_when: false
|
|
|
|
- name: Inspect worker image identity
|
|
ansible.builtin.command:
|
|
argv: [docker, image, inspect, --format, "{{ '{{.Id}}' }}", "{{ guestops_worker_image }}"]
|
|
register: guestops_worker_identity
|
|
changed_when: false
|
|
|
|
- name: Create post-build release record
|
|
ansible.builtin.command:
|
|
argv:
|
|
- python3
|
|
- deploy/release_record.py
|
|
- --artifact
|
|
- "{{ guestops_staging_archive }}"
|
|
- --commit
|
|
- "{{ guestops_release_commit }}"
|
|
- --api-image
|
|
- "{{ guestops_api_image }}"
|
|
- --api-id
|
|
- "{{ guestops_api_identity.stdout }}"
|
|
- --worker-image
|
|
- "{{ guestops_worker_image }}"
|
|
- --worker-id
|
|
- "{{ guestops_worker_identity.stdout }}"
|
|
- --output
|
|
- "{{ guestops_config_root }}/release-records/{{ guestops_release_commit }}.json"
|
|
chdir: "{{ guestops_release_root }}"
|
|
|
|
- name: Run offline deployment preflight
|
|
ansible.builtin.command:
|
|
argv:
|
|
- python3
|
|
- deploy/ops.py
|
|
- preflight
|
|
- --offline
|
|
chdir: "{{ guestops_release_root }}"
|
|
changed_when: false
|
|
|
|
- name: Validate Compose without exposing expanded configuration
|
|
ansible.builtin.command:
|
|
argv:
|
|
- docker
|
|
- compose
|
|
- --env-file
|
|
- "{{ guestops_config_root }}/guestops.env"
|
|
- config
|
|
- --quiet
|
|
chdir: "{{ guestops_release_root }}"
|
|
changed_when: false
|
|
|
|
- name: Start the verified release without rebuilding
|
|
ansible.builtin.command:
|
|
argv:
|
|
- docker
|
|
- compose
|
|
- --env-file
|
|
- "{{ guestops_config_root }}/guestops.env"
|
|
- up
|
|
- --detach
|
|
- --no-build
|
|
chdir: "{{ guestops_release_root }}"
|
|
environment:
|
|
GUESTOPS_API_IMAGE: "{{ guestops_api_image }}"
|
|
GUESTOPS_WORKER_IMAGE: "{{ guestops_worker_image }}"
|
|
changed_when: true
|
|
|
|
- name: Wait for loopback readiness
|
|
ansible.builtin.uri:
|
|
url: http://127.0.0.1:8080/health/ready
|
|
method: GET
|
|
status_code: 200
|
|
return_content: false
|
|
register: guestops_readiness
|
|
retries: 30
|
|
delay: 2
|
|
until: guestops_readiness.status == 200
|
|
|
|
- name: Confirm TLS certificate files exist
|
|
ansible.builtin.stat:
|
|
path: "{{ item }}"
|
|
follow: true
|
|
loop:
|
|
- "/etc/letsencrypt/live/{{ guestops_public_hostname }}/fullchain.pem"
|
|
- "/etc/letsencrypt/live/{{ guestops_public_hostname }}/privkey.pem"
|
|
register: guestops_certificates
|
|
|
|
- name: Require the pre-provisioned TLS certificate
|
|
ansible.builtin.assert:
|
|
that:
|
|
- guestops_certificates.results | map(attribute='stat.exists') | min
|
|
- guestops_certificates.results | map(attribute='stat.isreg') | min
|
|
fail_msg: A valid pre-provisioned Let's Encrypt certificate is required before enabling Nginx.
|
|
|
|
- name: Install reviewed GuestOps Nginx site
|
|
ansible.builtin.copy:
|
|
src: "{{ playbook_dir }}/../nginx.conf"
|
|
dest: /etc/nginx/sites-available/guestops.conf
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
register: guestops_nginx_site
|
|
|
|
- name: Enable GuestOps Nginx site
|
|
ansible.builtin.file:
|
|
src: /etc/nginx/sites-available/guestops.conf
|
|
dest: /etc/nginx/sites-enabled/guestops.conf
|
|
state: link
|
|
register: guestops_nginx_enabled
|
|
|
|
- name: Validate Nginx configuration
|
|
ansible.builtin.command:
|
|
argv: [nginx, -t]
|
|
changed_when: false
|
|
|
|
- name: Reload Nginx after reviewed configuration change
|
|
ansible.builtin.service:
|
|
name: nginx
|
|
enabled: true
|
|
state: reloaded
|
|
when: guestops_nginx_site.changed or guestops_nginx_enabled.changed
|
|
|
|
- name: Inspect host TCP listeners
|
|
ansible.builtin.command:
|
|
argv: [ss, -ltnH]
|
|
register: guestops_tcp_listeners
|
|
changed_when: false
|
|
|
|
- name: Reject public API or MongoDB listeners
|
|
ansible.builtin.assert:
|
|
that:
|
|
- guestops_tcp_listeners.stdout_lines | select('search', ':8080(\\s|$)') | reject('search', '127\\.0\\.0\\.1:8080(\\s|$)') | list | length == 0
|
|
- guestops_tcp_listeners.stdout_lines | select('search', ':27017(\\s|$)') | list | length == 0
|
|
fail_msg: API port 8080 must be loopback-only and MongoDB must have no host listener.
|
|
|
|
- name: Verify public HTTP redirects to HTTPS
|
|
ansible.builtin.uri:
|
|
url: "http://{{ guestops_public_hostname }}/health/ready"
|
|
method: GET
|
|
follow_redirects: none
|
|
status_code: [301, 302, 307, 308]
|
|
return_content: false
|
|
delegate_to: localhost
|
|
become: false
|
|
|
|
- name: Verify public HTTPS readiness and certificate trust
|
|
ansible.builtin.uri:
|
|
url: "{{ guestops_public_origin }}/health/ready"
|
|
method: GET
|
|
status_code: 200
|
|
return_content: true
|
|
validate_certs: true
|
|
register: guestops_public_readiness
|
|
retries: 10
|
|
delay: 3
|
|
until:
|
|
- guestops_public_readiness.status == 200
|
|
- guestops_public_readiness.json is defined
|
|
- guestops_public_readiness.json.status == 'ready'
|
|
delegate_to: localhost
|
|
become: false
|
|
|
|
- name: Select the current successful release
|
|
ansible.builtin.file:
|
|
src: "{{ guestops_release_root }}"
|
|
dest: "{{ guestops_root }}/current"
|
|
state: link
|
|
force: true
|
|
|
|
- name: Retain release record on the Ansible controller
|
|
ansible.builtin.fetch:
|
|
src: "{{ guestops_config_root }}/release-records/{{ guestops_release_commit }}.json"
|
|
dest: "{{ guestops_evidence_directory }}/release-record.json"
|
|
flat: true
|
|
|
|
- name: Retain target source verification on the Ansible controller
|
|
ansible.builtin.fetch:
|
|
src: "{{ guestops_config_root }}/release-records/{{ guestops_release_commit }}.source-verification.json"
|
|
dest: "{{ guestops_evidence_directory }}/source-verification.json"
|
|
flat: true
|
|
|
|
- name: Remove transferred staging files after successful deployment
|
|
ansible.builtin.file:
|
|
path: "{{ item }}"
|
|
state: absent
|
|
loop:
|
|
- "{{ guestops_staging_archive }}"
|
|
- "{{ guestops_staging_record }}"
|