diff --git a/container-vm/cloud-init.yaml.tftpl b/container-vm/cloud-init.yaml.tftpl new file mode 100644 index 0000000..520034d --- /dev/null +++ b/container-vm/cloud-init.yaml.tftpl @@ -0,0 +1,71 @@ +#cloud-config +# This cloud-init configuration defines systemd services to run Docker containers +# on Container-Optimized OS (COS) with full observability support. + +write_files: +%{ for name, container in containers ~} + - path: /etc/systemd/system/${name}.service + permissions: 0644 + owner: root + content: | + [Unit] + Description=Containerized application: ${name} + # Wait for the Docker socket to be ready + Wants=docker.socket + After=docker.service + + [Service] + # Crucial for persistence: ensure the service restarts on exit. + Restart=${container.restart_policy} + RestartSec=5s + + # Stop and remove any pre-existing container with the same name. + ExecStartPre=/usr/bin/docker stop ${name} || true + ExecStartPre=/usr/bin/docker rm ${name} || true + + # Pull the image before starting. + ExecStartPre=/usr/bin/docker pull ${container.image} + + # The main execution command: docker run. + # When logging is enabled, use the gcplogs driver to send container logs to Cloud Logging. + ExecStart=/usr/bin/docker run \ + --name ${name} \ +%{ if enable_logging ~} + --log-driver=gcplogs \ + --log-opt gcp-project=${project_id} \ + --log-opt gcp-log-cmd=true \ + --log-opt gcp-meta-zone=true \ + --log-opt gcp-meta-name=true \ + --log-opt labels=container_name \ + --label container_name=${name} \ +%{ endif ~} +%{ for e in container.env ~} + -e ${e.name}="${e.value}" \ +%{ endfor ~} + ${container.image} \ +%{ if container.command != "" ~} + "${container.command}" \ +%{ endif ~} +%{ for arg in container.args ~} + # Escape single quotes: end quoted string, add escaped quote, resume quoted string + '${replace(arg, "'", "'\\''")}' \ +%{ endfor ~} + + + # ExecStop is called when the service is stopped manually. + ExecStop=/usr/bin/docker stop ${name} + + [Install] + WantedBy=multi-user.target + +%{ endfor ~} + +runcmd: + # Reload the systemd configuration to recognize the new service files. + - systemctl daemon-reload +%{ for name, container in containers ~} + # Enable and start ${name} service. + - systemctl enable ${name}.service + - systemctl start ${name}.service +%{ endfor ~} + diff --git a/container-vm/main.tf b/container-vm/main.tf new file mode 100644 index 0000000..92bad48 --- /dev/null +++ b/container-vm/main.tf @@ -0,0 +1,64 @@ +# Fetch the latest source image based on the input variables (defaulting to COS) +data "google_compute_image" "source_image" { + family = var.source_image_family + project = var.source_image_project +} + +# Render the cloud-init YAML template using dynamic module inputs +locals { + rendered_cloud_init = templatefile("${path.module}/cloud-init.yaml.tftpl", { + containers = var.containers, + enable_logging = var.enable_logging, + project_id = var.project_id, + }) +} + +# The Instance Template resource itself +resource "google_compute_instance_template" "container_vm_template" { + project = var.project_id + name_prefix = "systemd-container-tmpl-" + description = "Runs a container using Docker, managed by a systemd unit via cloud-init." + machine_type = var.machine_type + region = var.region + labels = var.labels + + # Disk configuration using the fetched OS image + disk { + source_image = data.google_compute_image.source_image.self_link + auto_delete = true + boot = true + } + + # Network configuration (private VM - no external IP) + network_interface { + network = var.network + subnetwork = var.subnetwork + } + + # Service Account configuration (needed for Google Cloud API access, including image pulls) + service_account { + email = var.service_account_email + scopes = ["cloud-platform"] + } + + # Metadata for cloud-init and observability + metadata = { + user-data = local.rendered_cloud_init + google-logging-enabled = tostring(var.enable_logging) + google-monitoring-enabled = tostring(var.enable_monitoring) + cos-metrics-enabled = tostring(var.enable_cos_metrics) + } + + scheduling { + automatic_restart = true + on_host_maintenance = "MIGRATE" + } + + # Enable shielded VM features for security + shielded_instance_config { + enable_secure_boot = true + enable_vtpm = true + enable_integrity_monitoring = true + } +} + diff --git a/container-vm/outputs.tf b/container-vm/outputs.tf new file mode 100644 index 0000000..7cadf9f --- /dev/null +++ b/container-vm/outputs.tf @@ -0,0 +1,10 @@ +output "instance_template_self_link" { + description = "The self-link of the created instance template. Use this to deploy VMs or MIGs." + value = google_compute_instance_template.container_vm_template.self_link +} + +output "instance_template_name" { + description = "The name of the created instance template." + value = google_compute_instance_template.container_vm_template.name +} + diff --git a/container-vm/variables.tf b/container-vm/variables.tf new file mode 100644 index 0000000..327bd72 --- /dev/null +++ b/container-vm/variables.tf @@ -0,0 +1,94 @@ +variable "project_id" { + description = "The GCP project ID." + type = string +} + +variable "region" { + description = "The region where the compute template is created." + type = string + default = "us-central1" +} + +variable "machine_type" { + description = "The machine type for the VM instance template." + type = string + default = "e2-medium" +} + +variable "containers" { + description = "A map of containers to run on the VM. Each key is the container name, and the value specifies the container configuration." + type = map(object({ + image = string + command = optional(string, "") + args = optional(list(string), []) + env = optional(list(object({ + name = string + value = string + })), []) + restart_policy = optional(string, "always") + })) + + validation { + condition = length(var.containers) > 0 + error_message = "At least one container must be specified." + } + + validation { + condition = alltrue([for name, c in var.containers : can(regex("^.+@sha256:[a-f0-9]{64}$", c.image))]) + error_message = "All container images must be referenced by digest (e.g., 'gcr.io/my-project/my-app@sha256:<64 hex chars>')." + } +} + +variable "service_account_email" { + description = "The email of the service account to use for the VM. Must be created outside this module." + type = string +} + +# Defaulting to COS, which has Docker/containerd pre-installed. +variable "source_image_family" { + description = "The OS image family to use. COS is recommended." + type = string + default = "cos-stable" +} + +variable "source_image_project" { + description = "The project hosting the source image." + type = string + default = "cos-cloud" +} + +variable "network" { + description = "The VPC network to attach the VM to (e.g., 'projects/PROJECT/global/networks/NETWORK' or 'NETWORK')." + type = string +} + +variable "subnetwork" { + description = "The subnetwork to attach the VM to (e.g., 'projects/PROJECT/regions/REGION/subnetworks/SUBNET' or 'SUBNET')." + type = string +} + +# Observability variables +variable "enable_logging" { + description = "Enable Cloud Logging for the VM and containers. When true, container logs are sent to Cloud Logging using the gcplogs driver." + type = bool + default = true +} + +variable "enable_monitoring" { + description = "Enable Cloud Monitoring for the VM. When true, VM metrics are collected and sent to Cloud Monitoring." + type = bool + default = true +} + +variable "enable_cos_metrics" { + description = "Enable Container-Optimized OS metrics collection for additional container and system metrics." + type = bool + default = true +} + +variable "labels" { + description = "Labels to apply to the instance template for organization and filtering in Cloud Monitoring and Logging." + type = map(string) + default = {} +} +