diff --git a/README.md b/README.md index 7369e99..e7f60a4 100644 --- a/README.md +++ b/README.md @@ -7,4 +7,5 @@ This repository contains examples of deploying the [Particular Service Platform] - [Deploying to Azure Container Apps using Bicep](/azure-container-apps/) - [Running locally with Docker Compose](/docker-compose/) - [Deploying to a Kubernetes cluster using helm](/helm/) +- [Running locally with Terraform](/terraform/) - [Deploying to Unraid OS](/unraid-templates/). These templates are also available directly within Unraid via its Community Apps section. diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 0000000..f15a277 --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1,25 @@ +# Terraform state files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which might contain sensitive data +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +# Terraform directory +.terraform/ +.terraform.lock.hcl diff --git a/terraform/README.md b/terraform/README.md new file mode 100644 index 0000000..4a3eb71 --- /dev/null +++ b/terraform/README.md @@ -0,0 +1,150 @@ +# Running with Terraform + +This [Terraform](https://www.terraform.io/) configuration provides an example of spinning up [ServiceControl](https://docs.particular.net/servicecontrol/) and [ServicePulse](https://docs.particular.net/servicepulse/) in a local development environment, using a container-hosted [RabbitMQ](https://docs.particular.net/transports/rabbitmq/) broker as the message transport. + +Running ServiceControl and ServicePulse locally in containers provides a way to use and test Service Platform features during local development on any platform, without needing to install Windows services. + +## Prerequisites + +1. [Docker](https://docs.docker.com/get-docker/) installed and running +2. [Terraform](https://www.terraform.io/downloads) (version 1.0 or later) installed + +## Usage + +### Initialize Terraform + +Before running Terraform for the first time, initialize the working directory to download the required provider plugins: + +```shell +terraform init +``` + +### Configure variables (optional) + +You can customize the deployment by creating a `terraform.tfvars` file. An example file is provided: + +```shell +cp terraform.tfvars.example terraform.tfvars +# Edit terraform.tfvars with your preferred settings +``` + +Key variables you might want to customize: +- `servicecontrol_tag`: Version tag for ServiceControl images (default: `latest`) +- `servicepulse_tag`: Version tag for ServicePulse image (default: `latest`) +- `transport_type`: Message transport type (default: `RabbitMQ.QuorumConventionalRouting`) +- `connection_string`: Transport connection string +- `particular_license`: Your Particular Software license (optional, uses trial if not provided) + +### Deploy the infrastructure + +To see what Terraform will create: + +```shell +terraform plan +``` + +To deploy the containers: + +```shell +terraform apply +``` + +Terraform will show you the planned changes and ask for confirmation. Type `yes` to proceed. + +### Access the services + +Once deployed, the following services will be available: + +* [ServicePulse](https://docs.particular.net/servicepulse/) at http://localhost:9090 +* [ServiceInsight](https://docs.particular.net/serviceinsight/) can connect to http://localhost:33333/api +* RabbitMQ Management UI at http://localhost:15672 (credentials: `guest`/`guest`) + +To see all output URLs, run: + +```shell +terraform output +``` + +### Destroy the infrastructure + +To stop and remove all containers and resources: + +```shell +terraform destroy +``` + +Type `yes` when prompted to confirm the destruction. + +## Implementation details + +* The ports for all services are exposed to localhost: + * `33333`: ServiceControl API + * `44444`: Audit API + * `33633`: Monitoring API + * `8080`: RavenDB backend + * `9090`: ServicePulse UI + * `5672`: RabbitMQ AMQP + * `15672`: RabbitMQ Management UI +* All containers are connected via a dedicated Docker network (`service-platform-network`) +* Persistent Docker volumes are created for: + * RabbitMQ data + * RavenDB configuration + * RavenDB data +* One instance of the [`servicecontrol-ravendb` container](https://docs.particular.net/servicecontrol/ravendb/containers) is used for both the [`servicecontrol`](https://docs.particular.net/servicecontrol/servicecontrol-instances/deployment/containers) and [`servicecontrol-audit`](https://docs.particular.net/servicecontrol/audit-instances/deployment/containers) containers. + * _A single database container should not be shared between multiple ServiceControl instances in production scenarios._ +* The configuration uses the [Terraform Docker provider](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs) to manage containers locally + +## Advanced configuration + +### Using a different transport + +To use a different message transport, modify the `transport_type` and `connection_string` variables in your `terraform.tfvars` file. See the [ServiceControl Transports documentation](https://docs.particular.net/servicecontrol/transports) for supported transport types and connection string formats. + +### Remote Docker daemon + +To deploy to a remote Docker daemon, set the `docker_host` variable: + +```hcl +docker_host = "tcp://remote-host:2376" +``` + +### Custom port mappings + +All external ports can be customized via variables. For example, to change the ServicePulse port: + +```hcl +servicepulse_port = 8080 +``` + +## Troubleshooting + +### Viewing container logs + +To view logs for a specific container: + +```shell +docker logs servicecontrol +docker logs servicecontrol-audit +docker logs servicecontrol-monitoring +docker logs servicepulse +docker logs rabbitmq +docker logs servicecontrol-db +``` + +### Inspecting Terraform state + +To see the current state of resources: + +```shell +terraform show +``` + +### Force recreation of a container + +If a container is misbehaving, you can force Terraform to recreate it: + +```shell +terraform apply -replace=docker_container.servicecontrol +``` + +Replace `servicecontrol` with any other container name as needed. diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..d48547d --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,248 @@ +terraform { + required_version = ">= 1.0" + required_providers { + docker = { + source = "kreuzwerker/docker" + version = "~> 3.0" + } + } +} + +provider "docker" { + host = var.docker_host +} + +# Create a Docker network for the service platform +resource "docker_network" "service_platform" { + name = "service-platform-network" +} + +# Create volumes for persistent data +resource "docker_volume" "rabbitmq_data" { + name = "service-platform-rabbitmq-data" +} + +resource "docker_volume" "raven_config" { + name = "service-platform-raven-config" +} + +resource "docker_volume" "raven_data" { + name = "service-platform-raven-data" +} + +# RabbitMQ container +resource "docker_image" "rabbitmq" { + name = "rabbitmq:3-management" +} + +resource "docker_container" "rabbitmq" { + name = "rabbitmq" + image = docker_image.rabbitmq.image_id + + networks_advanced { + name = docker_network.service_platform.name + } + + ports { + internal = 5672 + external = var.rabbitmq_port + } + + ports { + internal = 15672 + external = var.rabbitmq_management_port + } + + volumes { + volume_name = docker_volume.rabbitmq_data.name + container_path = "/var/lib/rabbitmq" + } + + restart = "unless-stopped" + + healthcheck { + test = ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"] + interval = "30s" + timeout = "10s" + retries = 3 + } +} + +# ServiceControl RavenDB container +resource "docker_image" "servicecontrol_ravendb" { + name = "particular/servicecontrol-ravendb:${var.servicecontrol_tag}" +} + +resource "docker_container" "servicecontrol_db" { + name = "servicecontrol-db" + image = docker_image.servicecontrol_ravendb.image_id + + networks_advanced { + name = docker_network.service_platform.name + } + + ports { + internal = 8080 + external = var.ravendb_port + } + + volumes { + volume_name = docker_volume.raven_config.name + container_path = "/var/lib/ravendb/config" + } + + volumes { + volume_name = docker_volume.raven_data.name + container_path = "/var/lib/ravendb/data" + } + + restart = "unless-stopped" + + # RavenDB container includes a built-in healthcheck script + healthcheck { + test = ["CMD-SHELL", "/usr/lib/ravendb/scripts/healthcheck.sh"] + interval = "30s" + timeout = "10s" + retries = 3 + } +} + +# ServiceControl (Error instance) container +resource "docker_image" "servicecontrol" { + name = "particular/servicecontrol:${var.servicecontrol_tag}" +} + +resource "docker_container" "servicecontrol" { + name = "servicecontrol" + image = docker_image.servicecontrol.image_id + + networks_advanced { + name = docker_network.service_platform.name + } + + ports { + internal = 33333 + external = var.servicecontrol_port + } + + # Note: Environment variables use internal container ports (33333, 44444, 33633, 8080) + # which are fixed by the container images, not the external mapped ports. + env = [ + "RAVENDB_CONNECTIONSTRING=http://servicecontrol-db:8080", + "REMOTEINSTANCES=[{\"api_uri\":\"http://servicecontrol-audit:44444/api\"}]", + "TRANSPORTTYPE=${var.transport_type}", + "CONNECTIONSTRING=${var.connection_string}", + "PARTICULARSOFTWARE_LICENSE=${var.particular_license}" + ] + + command = ["--setup-and-run"] + + restart = "unless-stopped" + + depends_on = [ + docker_container.servicecontrol_db, + docker_container.rabbitmq + ] +} + +# ServiceControl Audit container +resource "docker_image" "servicecontrol_audit" { + name = "particular/servicecontrol-audit:${var.servicecontrol_tag}" +} + +resource "docker_container" "servicecontrol_audit" { + name = "servicecontrol-audit" + image = docker_image.servicecontrol_audit.image_id + + networks_advanced { + name = docker_network.service_platform.name + } + + ports { + internal = 44444 + external = var.servicecontrol_audit_port + } + + env = [ + "RAVENDB_CONNECTIONSTRING=http://servicecontrol-db:8080", + "SERVICECONTROLQUEUEADDRESS=Particular.ServiceControl", + "TRANSPORTTYPE=${var.transport_type}", + "CONNECTIONSTRING=${var.connection_string}", + "PARTICULARSOFTWARE_LICENSE=${var.particular_license}" + ] + + command = ["--setup-and-run"] + + restart = "unless-stopped" + + depends_on = [ + docker_container.servicecontrol_db, + docker_container.rabbitmq + ] +} + +# ServiceControl Monitoring container +resource "docker_image" "servicecontrol_monitoring" { + name = "particular/servicecontrol-monitoring:${var.servicecontrol_tag}" +} + +resource "docker_container" "servicecontrol_monitoring" { + name = "servicecontrol-monitoring" + image = docker_image.servicecontrol_monitoring.image_id + + networks_advanced { + name = docker_network.service_platform.name + } + + ports { + internal = 33633 + external = var.servicecontrol_monitoring_port + } + + env = [ + "TRANSPORTTYPE=${var.transport_type}", + "CONNECTIONSTRING=${var.connection_string}", + "PARTICULARSOFTWARE_LICENSE=${var.particular_license}" + ] + + command = ["--setup-and-run"] + + restart = "unless-stopped" + + depends_on = [ + docker_container.rabbitmq + ] +} + +# ServicePulse container +resource "docker_image" "servicepulse" { + name = "particular/servicepulse:${var.servicepulse_tag}" +} + +resource "docker_container" "servicepulse" { + name = "servicepulse" + image = docker_image.servicepulse.image_id + + networks_advanced { + name = docker_network.service_platform.name + } + + ports { + internal = 9090 + external = var.servicepulse_port + } + + # Note: URLs use internal container ports (33333, 33633), not external mapped ports + env = [ + "SERVICECONTROL_URL=http://servicecontrol:33333", + "MONITORING_URL=http://servicecontrol-monitoring:33633" + ] + + restart = "unless-stopped" + + depends_on = [ + docker_container.servicecontrol, + docker_container.servicecontrol_monitoring, + docker_container.rabbitmq + ] +} diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000..2663d8d --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,34 @@ +output "servicepulse_url" { + description = "URL for ServicePulse web interface" + value = "http://localhost:${var.servicepulse_port}" +} + +output "servicecontrol_api_url" { + description = "URL for ServiceControl API (for use with ServiceInsight)" + value = "http://localhost:${var.servicecontrol_port}/api" +} + +output "servicecontrol_audit_api_url" { + description = "URL for ServiceControl Audit API" + value = "http://localhost:${var.servicecontrol_audit_port}/api" +} + +output "servicecontrol_monitoring_url" { + description = "URL for ServiceControl Monitoring API" + value = "http://localhost:${var.servicecontrol_monitoring_port}" +} + +output "ravendb_url" { + description = "URL for RavenDB management interface" + value = "http://localhost:${var.ravendb_port}" +} + +output "rabbitmq_management_url" { + description = "URL for RabbitMQ Management UI (credentials: guest/guest)" + value = "http://localhost:${var.rabbitmq_management_port}" +} + +output "network_name" { + description = "Name of the Docker network created for the service platform" + value = docker_network.service_platform.name +} diff --git a/terraform/terraform.tfvars.example b/terraform/terraform.tfvars.example new file mode 100644 index 0000000..247ee2b --- /dev/null +++ b/terraform/terraform.tfvars.example @@ -0,0 +1,30 @@ +# Example Terraform variables file +# Copy this file to terraform.tfvars and customize as needed + +# Docker host (defaults to local Docker daemon) +# docker_host = "unix:///var/run/docker.sock" + +# Image tags +servicecontrol_tag = "latest" +servicepulse_tag = "latest" + +# Transport configuration +transport_type = "RabbitMQ.QuorumConventionalRouting" +connection_string = "host=rabbitmq;username=guest;password=guest" + +# Optional: Particular Software license +# particular_license = <<-EOT +# +# +# ... +# +# EOT + +# Port mappings (defaults shown) +# servicecontrol_port = 33333 +# servicecontrol_audit_port = 44444 +# servicecontrol_monitoring_port = 33633 +# servicepulse_port = 9090 +# ravendb_port = 8080 +# rabbitmq_port = 5672 +# rabbitmq_management_port = 15672 diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..dc4293e --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,86 @@ +variable "docker_host" { + description = "Docker daemon host (e.g., unix:///var/run/docker.sock for local, or tcp://host:2376 for remote)" + type = string + default = "unix:///var/run/docker.sock" +} + +variable "servicecontrol_tag" { + description = "Docker image tag for ServiceControl components" + type = string + default = "latest" +} + +variable "servicepulse_tag" { + description = "Docker image tag for ServicePulse" + type = string + default = "latest" +} + +variable "transport_type" { + description = "The message transport type (e.g., RabbitMQ.QuorumConventionalRouting)" + type = string + default = "RabbitMQ.QuorumConventionalRouting" +} + +variable "connection_string" { + description = "Connection string for the message transport" + type = string + default = "host=rabbitmq;username=guest;password=guest" +} + +variable "particular_license" { + description = <<-EOT + Particular Software license content (optional, will use trial if not provided). + Should contain the full XML content of the license file. + Example format: + + + ... + + EOT + type = string + default = "" + sensitive = true +} + +variable "servicecontrol_port" { + description = "External port for ServiceControl API" + type = number + default = 33333 +} + +variable "servicecontrol_audit_port" { + description = "External port for ServiceControl Audit API" + type = number + default = 44444 +} + +variable "servicecontrol_monitoring_port" { + description = "External port for ServiceControl Monitoring API" + type = number + default = 33633 +} + +variable "servicepulse_port" { + description = "External port for ServicePulse UI" + type = number + default = 9090 +} + +variable "ravendb_port" { + description = "External port for RavenDB" + type = number + default = 8080 +} + +variable "rabbitmq_port" { + description = "External port for RabbitMQ AMQP" + type = number + default = 5672 +} + +variable "rabbitmq_management_port" { + description = "External port for RabbitMQ Management UI" + type = number + default = 15672 +}