Skip to content
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
78 changes: 77 additions & 1 deletion .github/workflows/cloud-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,61 @@ jobs:
- name: Run Terraform lint
run: make terraform-lint-check

config-management-lint:
name: Ansible and Salt lint
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Run Ansible and Salt smoke checks
run: make config-management-smoke

config-management-cloud-smoke:
name: Linode ${{ matrix.method }} Drupal
needs:
- terraform-lint
- config-management-lint
runs-on: ubuntu-24.04
timeout-minutes: 150
if: github.event.pull_request.head.repo.full_name == github.repository
strategy:
fail-fast: false
matrix:
method:
- ansible
- salt
concurrency:
group: cloud-compose-config-management-${{ matrix.method }}-drupal
cancel-in-progress: false
env:
CLOUD_COMPOSE_SMOKE_AUTO_APPROVE: "true"
CLOUD_COMPOSE_SMOKE_DESTROY_TIMEOUT: "1800"
CLOUD_COMPOSE_SMOKE_SWEEP_ORPHANS: "true"
CLOUD_COMPOSE_SMOKE_RUN_ID: ${{ github.run_id }}
LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Install Terraform
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4
with:
terraform_version: 1.14.6
terraform_wrapper: false

- name: Run Linode config-management smoke test
run: make config-management-cloud-smoke METHOD=${{ matrix.method }}

- name: Destroy Linode config-management smoke resources
if: always()
run: ci/config-management-cloud-smoke.sh destroy-${{ matrix.method }}-drupal

smoke:
name: ${{ matrix.name }}
needs: terraform-lint
needs:
- terraform-lint
- config-management-lint
runs-on: ubuntu-24.04
timeout-minutes: 150
if: github.event.pull_request.head.repo.full_name == github.repository
Expand Down Expand Up @@ -108,6 +160,30 @@ jobs:
if: always() && matrix.provider == 'gcp' && (env.GCLOUD_OIDC_POOL == '' || env.GSA == '' || env.GCLOUD_PROJECT == '')
run: echo "Skipping GCP destroy because GCP smoke secrets are not configured and no GCP resources were applied."

config-management-cleanup:
name: Destroy Linode ${{ matrix.method }} Drupal config-management
needs: config-management-cloud-smoke
if: always() && (failure() || cancelled()) && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
method:
- ansible
- salt
concurrency:
group: cloud-compose-config-management-${{ matrix.method }}-drupal
cancel-in-progress: false
env:
CLOUD_COMPOSE_SMOKE_RUN_ID: ${{ github.run_id }}
LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Sweep Linode config-management smoke resources
run: ci/config-management-cloud-smoke.sh sweep-${{ matrix.method }}-drupal

cleanup:
name: Destroy ${{ matrix.name }}
needs: smoke
Expand Down
31 changes: 30 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
.PHONY: lint lint-check terraform-fmt terraform-fmt-check terraform-validate terraform-lint-check shell-lint terraform-docs smoke-test-clouds smoke-test smoke-test-digitalocean-isle smoke-test-linode-wp smoke-test-gcp-wp destroy-smoke destroy-smoke-digitalocean-isle destroy-smoke-linode-wp destroy-smoke-gcp-wp docs docs-docker-build docs-build docs-serve docs-preview docs-clean
.PHONY: lint lint-check shell-lint config-management-smoke
.PHONY: terraform-fmt terraform-fmt-check terraform-validate terraform-lint-check terraform-docs
.PHONY: config-management-cloud-smoke config-management-cloud-smoke-ansible-drupal config-management-cloud-smoke-salt-drupal
.PHONY: destroy-config-management-cloud-smoke destroy-config-management-cloud-smoke-ansible-drupal destroy-config-management-cloud-smoke-salt-drupal
.PHONY: smoke-test-clouds smoke-test smoke-test-digitalocean-isle smoke-test-linode-wp smoke-test-gcp-wp
.PHONY: destroy-smoke destroy-smoke-digitalocean-isle destroy-smoke-linode-wp destroy-smoke-gcp-wp
.PHONY: docs docs-docker-build docs-build docs-serve docs-preview docs-clean

DOCS_IMAGE ?= cloud-compose-docs
DOCS_PORT ?= 8888
Expand All @@ -25,6 +31,29 @@ shell-lint:
-path "./docs/site" -prune -o \
-type f -name "*.sh" -print0 | xargs -0 shellcheck

config-management-smoke:
ci/config-management-smoke.sh

config-management-cloud-smoke:
@test -n "$(METHOD)" || { echo "METHOD is required"; exit 2; }
ci/config-management-cloud-smoke.sh $(METHOD)-drupal

config-management-cloud-smoke-ansible-drupal:
$(MAKE) config-management-cloud-smoke METHOD=ansible

config-management-cloud-smoke-salt-drupal:
$(MAKE) config-management-cloud-smoke METHOD=salt

destroy-config-management-cloud-smoke:
@test -n "$(METHOD)" || { echo "METHOD is required"; exit 2; }
ci/config-management-cloud-smoke.sh destroy-$(METHOD)-drupal

destroy-config-management-cloud-smoke-ansible-drupal:
$(MAKE) destroy-config-management-cloud-smoke METHOD=ansible

destroy-config-management-cloud-smoke-salt-drupal:
$(MAKE) destroy-config-management-cloud-smoke METHOD=salt

terraform-docs:
terraform-docs markdown table --sort-by required --output-file README.md .

Expand Down
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
# cloud-compose

Deploy Docker Compose projects to cloud VMs. The root module targets Google
Cloud; provider modules under `modules/` cover DigitalOcean and Linode.
Deploy Docker Compose projects to VMs. Provider-specific Terraform entrypoints
under `providers/` cover GCP, DigitalOcean, and Linode without loading unused
cloud providers. Existing Debian/Ubuntu hosts can consume the same runtime
contract through the Ansible role or Salt formula.

Template defaults live in `templates/apps.json` and are shared by Terraform,
Ansible, and Salt. The default deployment shape is one app per VM or host; pass
`runtime.compose.projects` when several apps should share the same machine.

<!-- BEGIN_TF_DOCS -->
## Requirements

| Name | Version |
|------|---------|
| ---- | ------- |
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.2.4 |

## Providers
Expand All @@ -17,7 +23,7 @@ No providers.
## Modules

| Name | Source | Version |
|------|--------|---------|
| ---- | ------ | ------- |
| <a name="module_digitalocean"></a> [digitalocean](#module\_digitalocean) | ./modules/digitalocean | n/a |
| <a name="module_gcp"></a> [gcp](#module\_gcp) | ./modules/gcp | n/a |
| <a name="module_linode"></a> [linode](#module\_linode) | ./modules/linode | n/a |
Expand All @@ -29,7 +35,7 @@ No resources.
## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| ---- | ----------- | ---- | ------- | :------: |
| <a name="input_name"></a> [name](#input\_name) | Deployment name. | `string` | n/a | yes |
| <a name="input_cloud_provider"></a> [cloud\_provider](#input\_cloud\_provider) | Cloud provider to deploy to. Supported values are gcp, digitalocean, and linode. | `string` | `"gcp"` | no |
| <a name="input_digitalocean"></a> [digitalocean](#input\_digitalocean) | DigitalOcean infrastructure settings. | <pre>object({<br/> region = optional(string, "tor1")<br/> tags = optional(list(string), ["cloud-compose"])<br/><br/> droplet = optional(object({<br/> size = optional(string, "s-2vcpu-4gb")<br/> image = optional(string, "ubuntu-24-04-x64")<br/> ssh_keys = optional(list(string), [])<br/> vpc_uuid = optional(string, null)<br/> monitoring = optional(bool, true)<br/> ipv6 = optional(bool, true)<br/> backups = optional(bool, false)<br/> }), {})<br/><br/> ssh = optional(object({<br/> cloud_compose_keys = optional(list(string), [])<br/> users = optional(map(list(string)), {})<br/> }), {})<br/><br/> volumes = optional(object({<br/> data_size_gb = optional(number, 50)<br/> docker_volumes_size_gb = optional(number, 100)<br/> }), {})<br/><br/> firewall = optional(object({<br/> enabled = optional(bool, true)<br/> ssh_source_addresses = optional(list(string), ["0.0.0.0/0", "::/0"])<br/> web_source_addresses = optional(list(string), ["0.0.0.0/0", "::/0"])<br/> }), {})<br/> })</pre> | `{}` | no |
Expand All @@ -41,7 +47,7 @@ No resources.
## Outputs

| Name | Description |
|------|-------------|
| ---- | ----------- |
| <a name="output_appGsa"></a> [appGsa](#output\_appGsa) | The Google Service Account the app can use for app-scoped auth. |
| <a name="output_backend"></a> [backend](#output\_backend) | Backend service ID for attaching Cloud Run ingress to an external HTTPS load balancer. |
| <a name="output_cloud_provider"></a> [cloud\_provider](#output\_cloud\_provider) | Selected cloud provider. |
Expand Down
5 changes: 5 additions & 0 deletions ansible.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[defaults]
inventory = ansible/inventory.example.yml
interpreter_python = auto_silent
retry_files_enabled = False
roles_path = ansible/roles
50 changes: 50 additions & 0 deletions ansible/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# cloud-compose Ansible Adapter

This role deploys the cloud-compose runtime onto an existing Debian or Ubuntu
host. Use it when Terraform should not create the VM, disks, firewall, or DNS.

The role consumes the same `templates/apps.json` template defaults used by the
Terraform provider modules.

The packaged runtime currently uses fixed host paths: `/home/cloud-compose`,
`/mnt/disks/data`, and `/mnt/disks/volumes`.

The normal on-prem shape is one app per machine. Put each machine in the
`cloud_compose` inventory group and set that host's template/runtime variables.
For multiple apps on one host, pass the same
`cloud_compose_runtime.compose.projects` shape used by Terraform.

Example inventory:

```yaml
all:
children:
cloud_compose:
hosts:
isle-prod.example.edu:
ansible_user: debian
cloud_compose_name: isle-prod
cloud_compose_template: isle
cloud_compose_runtime:
compose:
ingress:
domain: isle.example.edu
acme_email: admin@example.edu
sitectl:
environment: production
wp-prod.example.edu:
ansible_user: debian
cloud_compose_name: wp-prod
cloud_compose_template: wp
cloud_compose_runtime:
compose:
ingress:
domain: wp.example.edu
acme_email: admin@example.edu
```

Run:

```bash
ansible-playbook ansible/playbooks/site.yml
```
1 change: 1 addition & 0 deletions ansible/group_vars/cloud_compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cloud_compose_provider: onprem
37 changes: 37 additions & 0 deletions ansible/inventory.example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
all:
children:
cloud_compose:
hosts:
isle-prod.example.edu:
ansible_user: debian
cloud_compose_name: isle-prod
cloud_compose_template: isle
cloud_compose_runtime:
compose:
ingress:
domain: isle.example.edu
acme_email: admin@example.edu
sitectl:
environment: production
wp-prod.example.edu:
ansible_user: debian
cloud_compose_name: wp-prod
cloud_compose_template: wp
cloud_compose_runtime:
compose:
ingress:
domain: wp.example.edu
acme_email: admin@example.edu
sitectl:
environment: production
drupal-prod.example.edu:
ansible_user: debian
cloud_compose_name: drupal-prod
cloud_compose_template: drupal
cloud_compose_runtime:
compose:
ingress:
domain: drupal.example.edu
acme_email: admin@example.edu
sitectl:
environment: production
5 changes: 5 additions & 0 deletions ansible/playbooks/site.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- name: Deploy cloud-compose runtime
hosts: cloud_compose
become: true
roles:
- role: cloud_compose
56 changes: 56 additions & 0 deletions ansible/roles/cloud_compose/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
cloud_compose_name: cloud-compose
cloud_compose_provider: onprem
cloud_compose_template: ""

cloud_compose_templates_file: "{{ role_path }}/../../../templates/apps.json"
cloud_compose_rootfs_source: "{{ role_path }}/../../../rootfs"

# The packaged rootfs and systemd units currently hard-code these values.
cloud_compose_user: cloud-compose
cloud_compose_group: cloud-compose
cloud_compose_home: /home/cloud-compose
cloud_compose_data_dir: /mnt/disks/data
cloud_compose_volumes_dir: /mnt/disks/volumes

cloud_compose_docker_compose_version: v5.2.0
cloud_compose_docker_buildx_version: v0.35.0
cloud_compose_sitectl_version: latest

cloud_compose_managed_runtime_enabled: true
cloud_compose_internal_services_enabled: false
cloud_compose_internal_services_auto_update: false
cloud_compose_managed_artifacts: []

cloud_compose_install_packages: true
cloud_compose_reload_systemd: true
cloud_compose_run_bootstrap: true
cloud_compose_force_bootstrap: false
cloud_compose_bootstrap_timeout: 3600
cloud_compose_bootstrap_poll_interval: 15

cloud_compose_extra_env: {}
cloud_compose_runtime: {}

cloud_compose_default_ingress:
letsencrypt: false
bot_mitigation: false
mode: ""
domain: ""
acme_email: ""
trusted_ips: []
max_upload_size: ""
upload_timeout: ""

cloud_compose_default_init:
- 'sitectl config set-context "${SITECTL_CONTEXT_NAME}" --type local --project-dir "${DOCKER_COMPOSE_DIR}" --site "${CLOUD_COMPOSE_INSTANCE_NAME}" --plugin "${SITECTL_PLUGIN}" --environment "${SITECTL_ENVIRONMENT}" --project-name "${CLOUD_COMPOSE_INSTANCE_NAME}" --compose-project-name "${COMPOSE_PROJECT_NAME}" --docker-socket /var/run/docker.sock --env-file .env --default'
cloud_compose_default_up:
- 'sitectl compose --context "${SITECTL_CONTEXT_NAME}" up -d --remove-orphans'
- 'sitectl healthcheck --context "${SITECTL_CONTEXT_NAME}" --persist --timeout "${SITECTL_HEALTHCHECK_TIMEOUT}" --interval "${SITECTL_HEALTHCHECK_INTERVAL}"'
- 'if [ "${SITECTL_ENVIRONMENT}" != "production" ]; then sitectl verify --context "${SITECTL_CONTEXT_NAME}" ${SITECTL_VERIFY_ARGS:-}; fi'
cloud_compose_default_down:
- 'sitectl compose --context "${SITECTL_CONTEXT_NAME}" down'
cloud_compose_default_rollout:
- 'TARGET_REF="${GIT_REF:-${GIT_BRANCH:-${DOCKER_COMPOSE_BRANCH:-main}}}"'
- 'if [ -x ./scripts/rollout.sh ]; then ./scripts/rollout.sh; else sitectl deploy --context "${SITECTL_CONTEXT_NAME}" --branch "$TARGET_REF"; fi'
- 'sitectl healthcheck --context "${SITECTL_CONTEXT_NAME}" --persist --timeout "${SITECTL_HEALTHCHECK_TIMEOUT}" --interval "${SITECTL_HEALTHCHECK_INTERVAL}"'
- 'if [ "${SITECTL_ENVIRONMENT}" != "production" ]; then sitectl verify --context "${SITECTL_CONTEXT_NAME}" ${SITECTL_VERIFY_ARGS:-}; fi'
Loading