From 0eb5fda30b1178af9ffbe39da66ab8b62243bf3e Mon Sep 17 00:00:00 2001 From: Robert Fairburn <8029478+rfairburn@users.noreply.github.com> Date: Mon, 13 Oct 2025 03:46:29 -0500 Subject: [PATCH] Initial fleet ec2-based setup --- .../ansible/roles/fleet/handlers/main.yml | 9 ++ .../byo-db/ansible/roles/fleet/tasks/main.yml | 79 ++++++++++ .../roles/fleet/templates/fleet.service.j2 | 16 ++ .../roles/fleet/templates/fleet_env.j2 | 3 + .../ansible/roles/nginx/handlers/main.yml | 5 + .../byo-db/ansible/roles/nginx/tasks/main.yml | 46 ++++++ .../roles/nginx/templates/fleet.conf.j2 | 22 +++ ec2/byo-vpc/byo-db/ansible/site.yml | 9 ++ ec2/byo-vpc/byo-db/main.tf | 140 ++++++++++++++++++ ec2/byo-vpc/byo-db/outputs.tf | 20 +++ .../byo-db/templates/cloud-init.yaml.tftpl | 21 +++ ec2/byo-vpc/byo-db/variables.tf | 115 ++++++++++++++ 12 files changed, 485 insertions(+) create mode 100644 ec2/byo-vpc/byo-db/ansible/roles/fleet/handlers/main.yml create mode 100644 ec2/byo-vpc/byo-db/ansible/roles/fleet/tasks/main.yml create mode 100644 ec2/byo-vpc/byo-db/ansible/roles/fleet/templates/fleet.service.j2 create mode 100644 ec2/byo-vpc/byo-db/ansible/roles/fleet/templates/fleet_env.j2 create mode 100644 ec2/byo-vpc/byo-db/ansible/roles/nginx/handlers/main.yml create mode 100644 ec2/byo-vpc/byo-db/ansible/roles/nginx/tasks/main.yml create mode 100644 ec2/byo-vpc/byo-db/ansible/roles/nginx/templates/fleet.conf.j2 create mode 100644 ec2/byo-vpc/byo-db/ansible/site.yml create mode 100644 ec2/byo-vpc/byo-db/main.tf create mode 100644 ec2/byo-vpc/byo-db/outputs.tf create mode 100644 ec2/byo-vpc/byo-db/templates/cloud-init.yaml.tftpl create mode 100644 ec2/byo-vpc/byo-db/variables.tf diff --git a/ec2/byo-vpc/byo-db/ansible/roles/fleet/handlers/main.yml b/ec2/byo-vpc/byo-db/ansible/roles/fleet/handlers/main.yml new file mode 100644 index 0000000..facff66 --- /dev/null +++ b/ec2/byo-vpc/byo-db/ansible/roles/fleet/handlers/main.yml @@ -0,0 +1,9 @@ +--- +- name: Reload systemd + ansible.builtin.systemd: + daemon_reload: true + +- name: Restart Fleet + ansible.builtin.systemd: + name: "{{ fleet_service_name }}" + state: restarted diff --git a/ec2/byo-vpc/byo-db/ansible/roles/fleet/tasks/main.yml b/ec2/byo-vpc/byo-db/ansible/roles/fleet/tasks/main.yml new file mode 100644 index 0000000..bd55771 --- /dev/null +++ b/ec2/byo-vpc/byo-db/ansible/roles/fleet/tasks/main.yml @@ -0,0 +1,79 @@ +--- +- name: Ensure supporting packages are installed + ansible.builtin.package: + name: + - tar + - gzip + state: present + +- name: Ensure Fleet service user exists + ansible.builtin.user: + name: "{{ fleet_service_user }}" + system: true + shell: /sbin/nologin + create_home: false + +- name: Create Fleet directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ fleet_service_user }}" + group: "{{ fleet_service_user }}" + mode: "0755" + loop: + - /opt/fleet + - /etc/fleet + +- name: Download Fleet release archive + ansible.builtin.get_url: + url: "{{ fleet_download_url }}" + dest: "{{ fleet_archive_path }}" + mode: "0644" + force: true + register: fleet_archive + +- name: Extract Fleet binaries + ansible.builtin.unarchive: + src: "{{ fleet_archive_path }}" + dest: "{{ fleet_extract_dir }}" + remote_src: true + owner: "{{ fleet_service_user }}" + group: "{{ fleet_service_user }}" + mode: "0755" + extra_opts: + - --strip-components=1 + when: fleet_archive is changed + +- name: Ensure Fleet binary is executable + ansible.builtin.file: + path: "{{ fleet_binary_path }}" + owner: "{{ fleet_service_user }}" + group: "{{ fleet_service_user }}" + mode: "0755" + +- name: Render Fleet environment file + ansible.builtin.template: + src: fleet_env.j2 + dest: "{{ fleet_env_file }}" + owner: "root" + group: "root" + mode: "0600" + notify: + - Restart Fleet + +- name: Install Fleet systemd unit + ansible.builtin.template: + src: fleet.service.j2 + dest: "/etc/systemd/system/{{ fleet_service_name }}.service" + owner: "root" + group: "root" + mode: "0644" + notify: + - Reload systemd + - Restart Fleet + +- name: Enable and start Fleet + ansible.builtin.systemd: + name: "{{ fleet_service_name }}" + enabled: true + state: started diff --git a/ec2/byo-vpc/byo-db/ansible/roles/fleet/templates/fleet.service.j2 b/ec2/byo-vpc/byo-db/ansible/roles/fleet/templates/fleet.service.j2 new file mode 100644 index 0000000..c5f5454 --- /dev/null +++ b/ec2/byo-vpc/byo-db/ansible/roles/fleet/templates/fleet.service.j2 @@ -0,0 +1,16 @@ +[Unit] +Description=Fleet device management server +After=network.target + +[Service] +Type=simple +User={{ fleet_service_user }} +Group={{ fleet_service_user }} +EnvironmentFile={{ fleet_env_file }} +ExecStart={{ fleet_binary_path }} serve +Restart=on-failure +RestartSec=5 +LimitNOFILE=999999 + +[Install] +WantedBy=multi-user.target diff --git a/ec2/byo-vpc/byo-db/ansible/roles/fleet/templates/fleet_env.j2 b/ec2/byo-vpc/byo-db/ansible/roles/fleet/templates/fleet_env.j2 new file mode 100644 index 0000000..be5612c --- /dev/null +++ b/ec2/byo-vpc/byo-db/ansible/roles/fleet/templates/fleet_env.j2 @@ -0,0 +1,3 @@ +{% for item in fleet_env_map | dictsort %} +{{ item.0 }}={{ item.1 | tojson }} +{% endfor %} diff --git a/ec2/byo-vpc/byo-db/ansible/roles/nginx/handlers/main.yml b/ec2/byo-vpc/byo-db/ansible/roles/nginx/handlers/main.yml new file mode 100644 index 0000000..0f31453 --- /dev/null +++ b/ec2/byo-vpc/byo-db/ansible/roles/nginx/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: Reload nginx + ansible.builtin.service: + name: nginx + state: reloaded diff --git a/ec2/byo-vpc/byo-db/ansible/roles/nginx/tasks/main.yml b/ec2/byo-vpc/byo-db/ansible/roles/nginx/tasks/main.yml new file mode 100644 index 0000000..a3cec91 --- /dev/null +++ b/ec2/byo-vpc/byo-db/ansible/roles/nginx/tasks/main.yml @@ -0,0 +1,46 @@ +--- +- name: Enable EPEL repository + ansible.builtin.package: + name: epel-release + state: present + +- name: Install nginx and certbot packages + ansible.builtin.package: + name: + - nginx + - certbot + - python3-certbot-nginx + state: present + +- name: Ensure nginx configuration directory exists + ansible.builtin.file: + path: /etc/nginx/conf.d + state: directory + owner: root + group: root + mode: "0755" + +- name: Deploy Fleet reverse proxy configuration + ansible.builtin.template: + src: fleet.conf.j2 + dest: /etc/nginx/conf.d/fleet.conf + owner: root + group: root + mode: "0644" + notify: + - Reload nginx + +- name: Ensure nginx enabled and running + ansible.builtin.service: + name: nginx + state: started + enabled: true + +- name: Request TLS certificates with certbot + ansible.builtin.command: > + certbot --nginx --non-interactive --agree-tos --redirect + --email {{ tls_email }} + {% for domain in tls_domains %}-d {{ domain }} {% endfor %} + args: + creates: "/etc/letsencrypt/live/{{ tls_domains[0] }}/fullchain.pem" + when: tls_domains | length > 0 diff --git a/ec2/byo-vpc/byo-db/ansible/roles/nginx/templates/fleet.conf.j2 b/ec2/byo-vpc/byo-db/ansible/roles/nginx/templates/fleet.conf.j2 new file mode 100644 index 0000000..9fb3ccd --- /dev/null +++ b/ec2/byo-vpc/byo-db/ansible/roles/nginx/templates/fleet.conf.j2 @@ -0,0 +1,22 @@ +upstream fleet_app { + server 127.0.0.1:8080; +} + +server { + listen 80; + server_name {{ tls_domains | join(" ") }}; + + location / { + proxy_pass http://fleet_app; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_set_header Connection ""; + } + + location /.well-known/acme-challenge/ { + root /usr/share/nginx/html; + } +} diff --git a/ec2/byo-vpc/byo-db/ansible/site.yml b/ec2/byo-vpc/byo-db/ansible/site.yml new file mode 100644 index 0000000..16b8da8 --- /dev/null +++ b/ec2/byo-vpc/byo-db/ansible/site.yml @@ -0,0 +1,9 @@ +--- +- name: Configure Fleet host + hosts: localhost + become: true + connection: local + gather_facts: false + roles: + - fleet + - nginx diff --git a/ec2/byo-vpc/byo-db/main.tf b/ec2/byo-vpc/byo-db/main.tf new file mode 100644 index 0000000..b80932f --- /dev/null +++ b/ec2/byo-vpc/byo-db/main.tf @@ -0,0 +1,140 @@ +data "aws_ami" "rhel" { + most_recent = true + owners = ["309956199498"] # Red Hat, Inc. + + filter { + name = "name" + values = ["RHEL-9.*_HVM-*-x86_64-*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + filter { + name = "root-device-type" + values = ["ebs"] + } +} + +locals { + instance_configuration = merge({ + type = "t3a.large" + key_name = null + iam_instance_profile = null + volume_size = 50 + volume_type = "gp3" + volume_iops = null + volume_throughput = null + delete_on_termination = true + }, var.instance_configuration) + + extra_environment = { + for pair in var.fleet_config.extra_environment_variables : + pair.key => pair.value + } + + fleet_env_map = merge( + { + FLEET_SERVER_PRIVATE_KEY = random_password.fleet_server_private_key.result + }, + local.extra_environment, + ) + + fleet_download_url = "https://github.com/fleetdm/fleet/releases/download/fleet-${var.fleet_config.fleet_version}/fleet_${var.fleet_config.fleet_version}_linux.tar.gz" + + ansible_extra_vars = jsonencode({ + fleet_download_url = local.fleet_download_url + fleet_archive_path = "/tmp/fleet.tar.gz" + fleet_extract_dir = "/opt/fleet" + fleet_binary_path = "/opt/fleet/fleet" + fleet_env_file = "/etc/fleet/fleet_env" + fleet_env_map = local.fleet_env_map + fleet_service_user = var.fleet_config.service_user + fleet_service_name = var.name + tls_domains = var.fleet_config.tls.domains + tls_email = var.fleet_config.tls.email + }) + + ansible_repo_url = var.ansible_source.repo_url + ansible_repo_ref = var.ansible_source.ref + ansible_repo_path = "/opt/fleet-terraform" + ansible_sparse_path = "ec2/byo-vpc/byo-db/ansible" + ansible_playbook = "ec2/byo-vpc/byo-db/ansible/site.yml" + + cloud_init = templatefile("${path.module}/templates/cloud-init.yaml.tftpl", { + ansible_extra_vars = local.ansible_extra_vars + ansible_repo_url = local.ansible_repo_url + ansible_repo_ref = local.ansible_repo_ref + ansible_repo_path = local.ansible_repo_path + ansible_sparse_path = local.ansible_sparse_path + ansible_playbook = local.ansible_playbook + }) + + security_group_ids = length(var.security_group_ids) == 0 ? [aws_security_group.fleet[0].id] : var.security_group_ids +} + +resource "random_password" "fleet_server_private_key" { + length = 32 + special = true + override_special = "!@#$%^&*()-_=+[]{}" +} + +resource "aws_security_group" "fleet" { + count = length(var.security_group_ids) == 0 ? 1 : 0 + name_prefix = "${var.name}-fleet-" + description = "Security group for Fleet EC2 instance" + vpc_id = var.vpc_id + + egress { + description = "Allow all egress" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + ipv6_cidr_blocks = ["::/0"] + } + + dynamic "ingress" { + for_each = var.ingress_rules + content { + description = lookup(ingress.value, "description", null) + from_port = ingress.value.from_port + to_port = ingress.value.to_port + protocol = ingress.value.protocol + cidr_blocks = lookup(ingress.value, "cidr_blocks", []) + ipv6_cidr_blocks = lookup(ingress.value, "ipv6_cidr_blocks", []) + security_groups = lookup(ingress.value, "security_groups", []) + prefix_list_ids = lookup(ingress.value, "prefix_list_ids", []) + } + } +} + +resource "aws_instance" "fleet" { + ami = data.aws_ami.rhel.id + instance_type = local.instance_configuration.type + subnet_id = var.subnet_id + vpc_security_group_ids = local.security_group_ids + associate_public_ip_address = var.associate_public_ip_address + iam_instance_profile = local.instance_configuration.iam_instance_profile + key_name = local.instance_configuration.key_name + user_data = local.cloud_init + user_data_replace_on_change = true + + root_block_device { + volume_size = local.instance_configuration.volume_size + volume_type = local.instance_configuration.volume_type + iops = local.instance_configuration.volume_iops + throughput = local.instance_configuration.volume_throughput + delete_on_termination = local.instance_configuration.delete_on_termination + encrypted = true + } + + tags = merge( + { + Name = "${var.name}-fleet" + }, + var.tags, + ) +} diff --git a/ec2/byo-vpc/byo-db/outputs.tf b/ec2/byo-vpc/byo-db/outputs.tf new file mode 100644 index 0000000..c53dc93 --- /dev/null +++ b/ec2/byo-vpc/byo-db/outputs.tf @@ -0,0 +1,20 @@ +output "instance_id" { + description = "Identifier of the Fleet EC2 instance." + value = aws_instance.fleet.id +} + +output "instance_public_ip" { + description = "Public IPv4 address assigned to the Fleet EC2 instance." + value = aws_instance.fleet.public_ip +} + +output "security_group_ids" { + description = "Security groups attached to the Fleet EC2 instance." + value = local.security_group_ids +} + +output "fleet_server_private_key" { + description = "Generated Fleet server private key." + value = random_password.fleet_server_private_key.result + sensitive = true +} diff --git a/ec2/byo-vpc/byo-db/templates/cloud-init.yaml.tftpl b/ec2/byo-vpc/byo-db/templates/cloud-init.yaml.tftpl new file mode 100644 index 0000000..7ba2208 --- /dev/null +++ b/ec2/byo-vpc/byo-db/templates/cloud-init.yaml.tftpl @@ -0,0 +1,21 @@ +#cloud-config +package_update: true +package_upgrade: true +packages: + - python3 + - python3-pip + - git + - ansible-core + +runcmd: + - | + #!/bin/bash + set -euo pipefail + export HOME=/root + repo_dir="${ansible_repo_path}" + git clone --depth 1 --filter=blob:none --sparse --branch "${ansible_repo_ref}" "${ansible_repo_url}" "${ansible_repo_path}" + git -C "${repo_dir}" sparse-checkout set "${ansible_sparse_path}" + cat >/tmp/fleet-extra-vars.json <<'JSON' +${ansible_extra_vars} +JSON + ansible-playbook "${repo_dir}/${ansible_playbook}" --inventory localhost, --extra-vars @/tmp/fleet-extra-vars.json diff --git a/ec2/byo-vpc/byo-db/variables.tf b/ec2/byo-vpc/byo-db/variables.tf new file mode 100644 index 0000000..06c96d6 --- /dev/null +++ b/ec2/byo-vpc/byo-db/variables.tf @@ -0,0 +1,115 @@ +variable "name" { + type = string + description = "Base name to use for created resources." + default = "fleet" +} + +variable "vpc_id" { + type = string + description = "Identifier of the VPC that hosts the Fleet instance." +} + +variable "subnet_id" { + type = string + description = "Subnet where the Fleet instance will be launched." +} + +variable "associate_public_ip_address" { + type = bool + description = "Whether to associate a public IP address to the Fleet instance." + default = true +} + +variable "security_group_ids" { + type = list(string) + description = "Existing security group IDs to attach to the Fleet instance. If empty, a security group will be created." + default = [] +} + +variable "ingress_rules" { + description = < 0 + error_message = "Provide at least one domain in fleet_config.tls.domains for certificate issuance." + } +} + +variable "ansible_source" { + description = "Location of the Fleet Terraform repository to pull Ansible content from." + type = object({ + repo_url = string + ref = string + }) + default = { + repo_url = "https://github.com/fleetdm/fleet-terraform.git" + ref = "main" + } +} + +variable "tags" { + type = map(string) + description = "Additional tags to apply to created resources." + default = {} +}