Skip to content

Add support for multi-node Data Guard deployments #279

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jul 17, 2025
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
25 changes: 24 additions & 1 deletion docs/terraform.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ This setup supports the deployment of the following configurations:
This approach is particularly suitable for deploying and configuring:

- Oracle databases on RHEL or Oracle Linux
- High-availability configurations for database and application clusters
- Two-node Oracle Data Guard deployments, where:

The user provides:

* zone1 and subnetwork1 for the primary node
* zone2 and subnetwork2 for the standby node

Only 2-node Data Guard setups are currently supported.

---

Expand All @@ -43,6 +50,22 @@ This infrastructure is modular and customizable, allowing you to tailor it to sp

---

## Instance naming convention
For single-instance deployments, the VM will be named using the pattern:
"<instance_name>-1".
Example: If instance_name = "oracle-db", the resulting VM will be oracle-db-1.

For multi-node Oracle Data Guard deployments:

* Primary node: "<instance_name>-1"
* Standby node: "<instance_name>-2"

Example: If instance_name = "oracle-db", the primary VM will be oracle-db-1 and the standby VM will be oracle-db-2.


---


## Pre-requisites

To use this Terraform and Ansible integration, ensure you have the following tools installed:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,3 @@
PATH: "{{ oracle_home }}/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin"
no_log: true
tags: workload-agent

- name: Copy workload-agent's configuration file to the database VM
template:
src: "configuration.json.j2"
dest: "/etc/google-cloud-workload-agent/configuration.json"
owner: root
group: root
mode: u=rw,go=r
notify: Restart workload-agent
tags: workload-agent
18 changes: 16 additions & 2 deletions roles/workload-agent/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,23 @@
file: install.yml
tags: workload-agent

- name: Configure Google Cloud Agent for Compute Workloads to collect Oracle metrics and send them to Google Cloud Monitoring
- name: Create a database user for the Google Cloud Agent for Compute Workloads to collect Oracle metrics
include_tasks:
file: metric_collection.yml
file: create_db_user.yml
# Run for single-instance setup or for the primary in multi-node Data Guard setup.
# For standby setup, install-oracle.sh generates the [primary] group in the inventory file.
when:
- oracle_metrics_secret | length > 0
- "groups['primary'] is not defined"

- name: Copy workload-agent's configuration file to the database VM
template:
src: "configuration.json.j2"
dest: "/etc/google-cloud-workload-agent/configuration.json"
owner: root
group: root
mode: u=rw,go=r
when:
- oracle_metrics_secret | length > 0
notify: Restart workload-agent
tags: workload-agent
197 changes: 127 additions & 70 deletions terraform/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ locals {
fs_disks = [
{
auto_delete = true
boot = false
device_name = "oracle_home"
disk_size_gb = var.oracle_home_disk.size_gb
disk_type = var.oracle_home_disk.type
Expand All @@ -26,23 +25,20 @@ locals {
asm_disks = [
{
auto_delete = true
boot = false
device_name = "data"
disk_size_gb = var.data_disk.size_gb
disk_type = var.data_disk.type
disk_labels = { diskgroup = "data", purpose = "asm" }
},
{
auto_delete = true
boot = false
device_name = "reco"
disk_size_gb = var.reco_disk.size_gb
disk_type = var.reco_disk.type
disk_labels = { diskgroup = "reco", purpose = "asm" }
},
{
auto_delete = true
boot = false
device_name = "swap"
disk_size_gb = var.swap_disk_size_gb
disk_type = var.swap_disk_type
Expand Down Expand Up @@ -79,70 +75,149 @@ locals {
additional_disks = concat(local.fs_disks, local.asm_disks)

project_id = var.project_id

is_multi_instance = (
var.zone1 != "" && var.zone2 != "" && var.subnetwork1 != "" && var.subnetwork2 != ""
)

instances = local.is_multi_instance ? {
"${var.instance_name}-1" = {
zone = var.zone1
subnetwork = var.subnetwork1
role = "primary"
}
"${var.instance_name}-2" = {
zone = var.zone2
subnetwork = var.subnetwork2
role = "standby"
}
} : {
"${var.instance_name}-1" = {
zone = var.zone1
subnetwork = var.subnetwork1
role = "primary"
}
}
}

module "instance_template" {
source = "terraform-google-modules/vm/google//modules/instance_template"
version = "~> 13.0"

name_prefix = format("%s-template", var.instance_name)
region = var.region
project_id = local.project_id
network = coalesce(var.network, var.subnetwork)
subnetwork = coalesce(var.subnetwork, var.network)
subnetwork_project = local.project_id
service_account = {
email = var.vm_service_account
scopes = ["https://www.googleapis.com/auth/cloud-platform"]
data "google_compute_image" "os_image" {
family = var.source_image_family
project = var.source_image_project
}

resource "time_static" "template_suffix" {}

locals {
template_suffix = formatdate("YYYYMMDDhhmmss", time_static.template_suffix.rfc3339)
}

resource "google_compute_instance_template" "default" {
name = "${var.instance_name}-${local.template_suffix}"
project = var.project_id
machine_type = var.machine_type

network_interface {
# gets overridden during instance creation
subnetwork = var.subnetwork1
}
disk {
boot = true
auto_delete = true
source_image = data.google_compute_image.os_image.self_link
disk_type = var.boot_disk_type
disk_size_gb = var.boot_disk_size_gb
}

machine_type = var.machine_type
source_image_family = var.source_image_family
source_image_project = var.source_image_project
disk_size_gb = var.boot_disk_size_gb
disk_type = var.boot_disk_type
auto_delete = true
dynamic "disk" {
for_each = local.additional_disks
content {
boot = false
auto_delete = disk.value.auto_delete
device_name = disk.value.device_name
disk_size_gb = disk.value.disk_size_gb
disk_type = disk.value.disk_type
labels = disk.value.disk_labels
}
}

service_account {
email = var.vm_service_account
scopes = ["cloud-platform"]
}

metadata = {
metadata_startup_script = var.metadata_startup_script
enable-oslogin = "TRUE"
}

additional_disks = local.additional_disks

tags = var.network_tags
}

module "compute_instance" {
source = "terraform-google-modules/vm/google//modules/compute_instance"
version = "~> 13.0"

region = var.region
zone = var.zone
network = coalesce(var.network, var.subnetwork)
subnetwork = coalesce(var.subnetwork, var.network)
subnetwork_project = local.project_id
hostname = var.instance_name
instance_template = module.instance_template.self_link
deletion_protection = false

access_config = var.assign_public_ip ? [{
nat_ip = null
network_tier = "PREMIUM"
}] : []
resource "google_compute_instance_from_template" "database_vm" {
for_each = local.instances

name = each.key
zone = each.value.zone
project = var.project_id
source_instance_template = google_compute_instance_template.default.self_link

network_interface {
subnetwork = each.value.subnetwork

dynamic "access_config" {
for_each = var.assign_public_ip ? [1] : []
content {}
}
}

}

resource "random_id" "suffix" {
byte_length = 4
}

locals {
database_vm_nodes = [
for vm in google_compute_instance_from_template.database_vm : {
name = vm.name
zone = vm.zone
ip = vm.network_interface[0].network_ip
role = local.instances[vm.name].role
}
]
}

locals {
common_flags = join(" ", compact([
length(local.asm_disk_config) > 0 ? "--ora-asm-disks-json '${jsonencode(local.asm_disk_config)}'" : "",
length(local.data_mounts_config) > 0 ? "--ora-data-mounts-json '${jsonencode(local.data_mounts_config)}'" : "",
"--swap-blk-device /dev/disk/by-id/google-swap",
var.ora_swlib_bucket != "" ? "--ora-swlib-bucket ${var.ora_swlib_bucket}" : "",
var.ora_version != "" ? "--ora-version ${var.ora_version}" : "",
var.ora_backup_dest != "" ? "--backup-dest ${var.ora_backup_dest}" : "",
var.ora_db_name != "" ? "--ora-db-name ${var.ora_db_name}" : "",
var.ora_db_container != "" ? "--ora-db-container ${var.ora_db_container}" : "",
var.ntp_pref != "" ? "--ntp-pref ${var.ntp_pref}" : "",
var.ora_release != "" ? "--ora-release ${var.ora_release}" : "",
var.ora_edition != "" ? "--ora-edition ${var.ora_edition}" : "",
var.ora_listener_port != "" ? "--ora-listener-port ${var.ora_listener_port}" : "",
var.ora_redo_log_size != "" ? "--ora-redo-log-size ${var.ora_redo_log_size}" : "",
var.db_password_secret != "" ? "--db-password-secret ${var.db_password_secret}" : "",
var.oracle_metrics_secret != "" ? "--oracle-metrics-secret ${var.oracle_metrics_secret}" : "",
var.install_workload_agent ? "--install-workload-agent" : "",
var.skip_database_config ? "--skip-database-config" : "",
var.ora_pga_target_mb != "" ? "--ora-pga-target-mb ${var.ora_pga_target_mb}" : "",
var.ora_sga_target_mb != "" ? "--ora-sga-target-mb ${var.ora_pga_target_mb}": "",
var.data_guard_protection_mode != "" ? "--data-guard-protection-mode '${var.data_guard_protection_mode}'": ""
]))
}

resource "google_compute_instance" "control_node" {
project = var.project_id
name = "${var.control_node_name_prefix}-${random_id.suffix.hex}"
machine_type = var.control_node_machine_type
zone = var.zone

zone = var.zone1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing spaces

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I marked this as resolved too soon. I just wanted to add some visual separation between the scheduling {} block and the basic parameter settings.

scheduling {
max_run_duration {
seconds = 604800
Expand All @@ -157,8 +232,7 @@ resource "google_compute_instance" "control_node" {
}

network_interface {
network = coalesce(var.network, var.subnetwork)
subnetwork = coalesce(var.subnetwork, var.network)
subnetwork = var.subnetwork1
subnetwork_project = local.project_id

dynamic "access_config" {
Expand All @@ -174,41 +248,24 @@ resource "google_compute_instance" "control_node" {

metadata_startup_script = templatefile("${path.module}/scripts/setup.sh.tpl", {
gcs_source = var.gcs_source
instance_name = module.compute_instance.instances_details[0].name
instance_zone = module.compute_instance.instances_details[0].zone
ip_addr = module.compute_instance.instances_details[0].network_interface[0].network_ip
asm_disk_config = jsonencode(local.asm_disk_config)
data_mounts_config = jsonencode(local.data_mounts_config)
swap_blk_device = "/dev/disk/by-id/google-swap"
ora_swlib_bucket = var.ora_swlib_bucket
ora_version = var.ora_version
ora_backup_dest = var.ora_backup_dest
ora_db_name = var.ora_db_name
ora_db_container = var.ora_db_container
ntp_pref = var.ntp_pref
ora_release = var.ora_release
ora_edition = var.ora_edition
ora_listener_port = var.ora_listener_port
ora_redo_log_size = var.ora_redo_log_size
db_password_secret = var.db_password_secret
install_workload_agent = var.install_workload_agent
oracle_metrics_secret = var.oracle_metrics_secret
skip_database_config = var.skip_database_config
ora_pga_target_mb = var.ora_pga_target_mb
ora_sga_target_mb = var.ora_sga_target_mb
database_vm_nodes_json = jsonencode(local.database_vm_nodes)
common_flags = local.common_flags
deployment_name = var.deployment_name
data_guard_protection_mode = var.data_guard_protection_mode
})

metadata = {
enable-oslogin = "TRUE"
}

depends_on = [module.compute_instance]
depends_on = [google_compute_instance_from_template.database_vm]
}

output "control_node_log_url" {
description = "Logs Explorer URL with Oracle Toolkit output"
value = "https://console.cloud.google.com/logs/query;query=resource.labels.instance_id%3D${urlencode(google_compute_instance.control_node.instance_id)};duration=P30D?project=${urlencode(var.project_id)}"
}

output "database_vm_names" {
description = "Names of the created database VMs from instance templates"
value = [for vm in google_compute_instance_from_template.database_vm : vm.name]
}
Loading