diff --git a/.gitignore b/.gitignore index b8a0db71..f4fde537 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,24 @@ Brewfile.lock.json # Ignore schematics json /update/*.json + +### Go ### +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum diff --git a/README.md b/README.md index 08b90723..94e78dfc 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ This module creates virtual server instances (VSI) across multiple subnets with * [Submodules](./modules) * [fscloud](./modules/fscloud) * [Examples](./examples) + * [Basic example using a Snapshot Consistency Group for volumes](./examples/snapshot) * [Complete Example using a placement group, attaching a load balancer, creating secondary interface, and adding additional data volumes](./examples/complete) * [End to end basic example](./examples/basic) * [Financial Services Cloud profile example](./examples/fscloud) @@ -58,6 +59,20 @@ module.vsi["test-vsi"].ibm_is_volume.volume["test-vsi-3-two"] --- +### Reserved IP addresses + +By setting the `manage_reserved_ips` to true, this Terraform module will manage VPC reserved IPs for all VSI instances. In the case where the VSI instances would need to be recreated by this module (such as rolling back to a snapshot volume), the new instances will retain the same reserved IP from their previous deployment. + +--- + +### Static boot volume names + +The default boot volume names created for VSI instances are four-word random names, which are regenerated if the VSI is recreated. If you set the `use_static_boot_volume_name` to true, the boot volume name for each VSI will not be random and will have a name that will be used again when recreated. This static name is of the format `hostname-boot`. If the VSI is recreated by Terraform for any reason, the exact same boot volume name will be used for the new instance. + +Example of static boot volume name: "my-prefix-0a2b-001-boot" + +--- + ### Floating IP addresses By using the `enable_floating_ip`, a floating IP address is assigned to each VSI created by this module. This floating IP address is displayed in the output, if provisioned. @@ -70,6 +85,24 @@ This module creates any number of application load balancers to balance traffic --- +### Storage Volume Snapshot support + +This module supports volume snapshots for both VSI boot volumes and attached block storage volumes. This feature can be used in either of the following scenarios: +1. Create new VSI instances using existing volume snapshots. +2. Roll back currently deployed VSI instances to existing volume snapshots. NOTE: if the boot volume is restored from a snapshot, all VSI instances will be recreated, and will retain most or all of their previous configuration (see note about [Reserved IP addresses above](#reserved-ip-addresses)) + +There are three methods you can use to specify volume snapshots for your deployment: +1. Specify individual Snapshot Ids using the `boot_volume_snapshot_id` and `block_storage_volumes.snapshot_id` input variables +2. Specify a Snapshot Consistency Group Id using the `snapshot_consistency_group_id` input variable (see explanation below) +3. A combination of specific Snapshot Ids and Consistency Group Ids, with specific Snapshot Ids taking precedence over Consistency Group Id snapshots, useful in situations where you may want to override one or more of the Consistency Group snapshots + +Snapshot Consistency Group logic explained: +If a `snapshot_consistency_group_id` is passed into this module, the snapshots belonging to that group will be queried for their "service_tags" that were applied at group creation. These tags will contain an index that identifies the snapshot within the group as belonging to either the boot volume of the instance (index 0), or one of the attached block storage volumes (index 1..n). These indexes are used to match up each group snapshot with the boot volume of the instance (which is always index 0), as well as any additional required volumes from the `block_storage_volumes` input variable, using the order of the input variable against the tag index (first `block_storage_volume` in input array = index 1, second = index 2, and so on). If there is a mismatch of group snapshots to the required storage specified in module inputs, then any of the extra snapshots or volumes will simply be ignored in the matching logic. + +NOTE: Snapshot and Consistency Group creation are not a part of this module and should be handled elsewhere. + +--- + ### Usage ```terraform @@ -121,7 +154,7 @@ You need the following permissions to run this module. | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.3.0 | -| [ibm](#requirement\_ibm) | >= 1.59.0, < 2.0.0 | +| [ibm](#requirement\_ibm) | >= 1.65.0, < 2.0.0 | | [time](#requirement\_time) | >= 0.9.1, < 1.0.0 | ### Modules @@ -143,8 +176,11 @@ No modules. | [ibm_is_lb_pool_member.nlb_pool_members](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/is_lb_pool_member) | resource | | [ibm_is_security_group.security_group](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/is_security_group) | resource | | [ibm_is_security_group_rule.security_group_rules](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/is_security_group_rule) | resource | +| [ibm_is_subnet_reserved_ip.vsi_ip](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/is_subnet_reserved_ip) | resource | | [ibm_is_volume.volume](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/is_volume) | resource | | [time_sleep.wait_for_authorization_policy](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep) | resource | +| [ibm_is_snapshot.snapshots_from_group](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/data-sources/is_snapshot) | data source | +| [ibm_is_snapshot_consistency_group.snapshot_group](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/data-sources/is_snapshot_consistency_group) | data source | | [ibm_is_vpc.vpc](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/data-sources/is_vpc) | data source | ### Inputs @@ -153,8 +189,9 @@ No modules. |------|-------------|------|---------|:--------:| | [access\_tags](#input\_access\_tags) | A list of access tags to apply to the VSI resources created by the module. For more information, see https://cloud.ibm.com/docs/account?topic=account-access-tags-tutorial. | `list(string)` | `[]` | no | | [allow\_ip\_spoofing](#input\_allow\_ip\_spoofing) | Allow IP spoofing on the primary network interface | `bool` | `false` | no | -| [block\_storage\_volumes](#input\_block\_storage\_volumes) | List describing the block storage volumes that will be attached to each vsi |
list(| `[]` | no | +| [block\_storage\_volumes](#input\_block\_storage\_volumes) | List describing the block storage volumes that will be attached to each vsi |
object({
name = string
profile = string
capacity = optional(number)
iops = optional(number)
encryption_key = optional(string)
resource_group_id = optional(string)
})
)
list(| `[]` | no | | [boot\_volume\_encryption\_key](#input\_boot\_volume\_encryption\_key) | CRN of boot volume encryption key | `string` | `null` | no | +| [boot\_volume\_snapshot\_id](#input\_boot\_volume\_snapshot\_id) | The snapshot id of the volume to be used for creating boot volume attachment (if specified, the `image_id` parameter will not be used) | `string` | `null` | no | | [create\_security\_group](#input\_create\_security\_group) | Create security group for VSI. If this is passed as false, the default will be used | `bool` | n/a | yes | | [enable\_floating\_ip](#input\_enable\_floating\_ip) | Create a floating IP for each virtual server created | `bool` | `false` | no | | [existing\_kms\_instance\_guid](#input\_existing\_kms\_instance\_guid) | The GUID of the Hyper Protect Crypto Services instance in which the key specified in var.boot\_volume\_encryption\_key is coming from. | `string` | `null` | no | @@ -162,6 +199,7 @@ No modules. | [kms\_encryption\_enabled](#input\_kms\_encryption\_enabled) | Set this to true to control the encryption keys used to encrypt the data that for the block storage volumes for VPC. If set to false, the data is encrypted by using randomly generated keys. For more info on encrypting block storage volumes, see https://cloud.ibm.com/docs/vpc?topic=vpc-creating-instances-byok | `bool` | `false` | no | | [load\_balancers](#input\_load\_balancers) | Load balancers to add to VSI |
object({
name = string
profile = string
capacity = optional(number)
iops = optional(number)
encryption_key = optional(string)
resource_group_id = optional(string)
snapshot_id = optional(string) # set if you would like to base volume on a snapshot
})
)
list(| `[]` | no | | [machine\_type](#input\_machine\_type) | VSI machine type. Run 'ibmcloud is instance-profiles' to get a list of regional profiles | `string` | n/a | yes | +| [manage\_reserved\_ips](#input\_manage\_reserved\_ips) | Set to `true` if you want this terraform module to manage the reserved IP addresses that are assigned to VSI instances. If this option is enabled, when any VSI is recreated it should retain its original IP. | `bool` | `false` | no | | [placement\_group\_id](#input\_placement\_group\_id) | Unique Identifier of the Placement Group for restricting the placement of the instance, default behaviour is placement on any host | `string` | `null` | no | | [prefix](#input\_prefix) | The IBM Cloud platform API key needed to deploy IAM enabled resources | `string` | n/a | yes | | [resource\_group\_id](#input\_resource\_group\_id) | ID of resource group to create VSI and block storage volumes. If you wish to create the block storage volumes in a different resource group, you can optionally set that directly in the 'block\_storage\_volumes' variable. | `string` | n/a | yes | @@ -173,9 +211,11 @@ No modules. | [security\_group](#input\_security\_group) | Security group created for VSI |
object({
name = string
type = string
listener_port = optional(number)
listener_port_max = optional(number)
listener_port_min = optional(number)
listener_protocol = string
connection_limit = optional(number)
idle_connection_timeout = optional(number)
algorithm = string
protocol = string
health_delay = number
health_retries = number
health_timeout = number
health_type = string
pool_member_port = string
profile = optional(string)
accept_proxy_protocol = optional(bool)
subnet_id_to_provision_nlb = optional(string) # Required for Network Load Balancer. If no value is provided, the first one from the VPC subnet list will be selected.
dns = optional(
object({
instance_crn = string
zone_id = string
})
)
security_group = optional(
object({
name = string
rules = list(
object({
name = string
direction = string
source = string
tcp = optional(
object({
port_max = number
port_min = number
})
)
udp = optional(
object({
port_max = number
port_min = number
})
)
icmp = optional(
object({
type = number
code = number
})
)
})
)
})
)
})
)
object({| `null` | no | | [security\_group\_ids](#input\_security\_group\_ids) | IDs of additional security groups to be added to VSI deployment primary interface. A VSI interface can have a maximum of 5 security groups. | `list(string)` | `[]` | no | | [skip\_iam\_authorization\_policy](#input\_skip\_iam\_authorization\_policy) | Set to true to skip the creation of an IAM authorization policy that permits all Storage Blocks to read the encryption key from the KMS instance. If set to false, pass in a value for the KMS instance in the existing\_kms\_instance\_guid variable. In addition, no policy is created if var.kms\_encryption\_enabled is set to false. | `bool` | `false` | no | +| [snapshot\_consistency\_group\_id](#input\_snapshot\_consistency\_group\_id) | The snapshot consistency group Id. If supplied, the group will be queried for snapshots that are matched with both boot volume and attached (attached are matched based on name suffix). You can override specific snapshot Ids by setting the appropriate input variables as well. | `string` | `null` | no | | [ssh\_key\_ids](#input\_ssh\_key\_ids) | ssh key ids to use in creating vsi | `list(string)` | n/a | yes | | [subnets](#input\_subnets) | A list of subnet IDs where VSI will be deployed |
name = string
rules = list(
object({
name = string
direction = string
source = string
tcp = optional(
object({
port_max = number
port_min = number
})
)
udp = optional(
object({
port_max = number
port_min = number
})
)
icmp = optional(
object({
type = number
code = number
})
)
})
)
})
list(| n/a | yes | | [tags](#input\_tags) | List of tags to apply to resources created by this module. | `list(string)` | `[]` | no | +| [use\_static\_boot\_volume\_name](#input\_use\_static\_boot\_volume\_name) | Sets the boot volume name for each VSI to a static name in the format `{hostname}_boot`, instead of a random name. Set this to `true` to have a consistent boot volume name even when VSIs are recreated. | `bool` | `false` | no | | [user\_data](#input\_user\_data) | User data to initialize VSI deployment | `string` | n/a | yes | | [vpc\_id](#input\_vpc\_id) | ID of VPC | `string` | n/a | yes | | [vsi\_per\_subnet](#input\_vsi\_per\_subnet) | Number of VSI instances for each subnet | `number` | n/a | yes | @@ -184,6 +224,8 @@ No modules. | Name | Description | |------|-------------| +| [consistency\_group\_boot\_snapshot\_id](#output\_consistency\_group\_boot\_snapshot\_id) | The Snapshot Id used for the VSI boot volume, determined from an optionally supplied consistency group | +| [consistency\_group\_storage\_snapshot\_ids](#output\_consistency\_group\_storage\_snapshot\_ids) | Map of attached storage volumes requested, and the Snapshot Ids that will be used, determined from an optionally supplied consistency group, and mapped | | [fip\_list](#output\_fip\_list) | A list of VSI with name, id, zone, and primary ipv4 address, and floating IP. This list only contains instances with a floating IP attached. | | [ids](#output\_ids) | The IDs of the VSI | | [lb\_hostnames](#output\_lb\_hostnames) | Hostnames for the Load Balancer created | diff --git a/examples/basic/version.tf b/examples/basic/version.tf index 9a5086f5..3a566ce2 100644 --- a/examples/basic/version.tf +++ b/examples/basic/version.tf @@ -3,7 +3,7 @@ terraform { required_providers { ibm = { source = "IBM-Cloud/ibm" - version = "= 1.59.0" + version = "= 1.65.0" } tls = { source = "hashicorp/tls" diff --git a/examples/complete/version.tf b/examples/complete/version.tf index ad1abc9d..1f77a7ee 100644 --- a/examples/complete/version.tf +++ b/examples/complete/version.tf @@ -3,7 +3,7 @@ terraform { required_providers { ibm = { source = "IBM-Cloud/ibm" - version = ">= 1.54.0" + version = ">= 1.65.0" } tls = { source = "hashicorp/tls" diff --git a/examples/fscloud/version.tf b/examples/fscloud/version.tf index ad1abc9d..1f77a7ee 100644 --- a/examples/fscloud/version.tf +++ b/examples/fscloud/version.tf @@ -3,7 +3,7 @@ terraform { required_providers { ibm = { source = "IBM-Cloud/ibm" - version = ">= 1.54.0" + version = ">= 1.65.0" } tls = { source = "hashicorp/tls" diff --git a/examples/snapshot/README.md b/examples/snapshot/README.md new file mode 100644 index 00000000..227604ab --- /dev/null +++ b/examples/snapshot/README.md @@ -0,0 +1,11 @@ +# Basic example using a Snapshot Consistency Group for volumes + +An end-to-end basic example that will provision the following, using previously created snapshots for storage volumes: + +- A new resource group if one is not passed in. +- A new public SSH key if one is not passed in. +- A new VPC with 3 subnets +- A VSI in each subnet +- Two additional block storage attached to each VSI +- Reserved and Floating IPs managed by Terraform for each VSI +- Boot volume and additional storage volumes will be based on snapshots from consistency group, if ID is supplied diff --git a/examples/snapshot/main.tf b/examples/snapshot/main.tf new file mode 100644 index 00000000..fa3e1cb9 --- /dev/null +++ b/examples/snapshot/main.tf @@ -0,0 +1,91 @@ +############################################################################## +# Locals +############################################################################## + +locals { + ssh_key_id = var.ssh_key != null ? data.ibm_is_ssh_key.existing_ssh_key[0].id : resource.ibm_is_ssh_key.ssh_key[0].id +} + +############################################################################## +# Resource Group +############################################################################## + +module "resource_group" { + source = "terraform-ibm-modules/resource-group/ibm" + version = "1.1.5" + # if an existing resource group is not set (null) create a new one using prefix + resource_group_name = var.resource_group == null ? "${var.prefix}-resource-group" : null + existing_resource_group_name = var.resource_group +} + +############################################################################## +# Create new SSH key +############################################################################## + +resource "tls_private_key" "tls_key" { + count = var.ssh_key != null ? 0 : 1 + algorithm = "RSA" + rsa_bits = 4096 +} + +resource "ibm_is_ssh_key" "ssh_key" { + count = var.ssh_key != null ? 0 : 1 + name = "${var.prefix}-ssh-key" + public_key = resource.tls_private_key.tls_key[0].public_key_openssh +} + +data "ibm_is_ssh_key" "existing_ssh_key" { + count = var.ssh_key != null ? 1 : 0 + name = var.ssh_key +} + +############################################################################# +# Provision VPC +############################################################################# + +module "slz_vpc" { + source = "terraform-ibm-modules/landing-zone-vpc/ibm" + version = "7.18.1" + resource_group_id = module.resource_group.resource_group_id + region = var.region + prefix = var.prefix + tags = var.resource_tags + name = "vpc" +} + +############################################################################# +# Provision VSI +############################################################################# + +module "slz_vsi" { + source = "../../" + resource_group_id = module.resource_group.resource_group_id + image_id = var.image_id + create_security_group = false + tags = var.resource_tags + access_tags = var.access_tags + subnets = module.slz_vpc.subnet_zone_list + vpc_id = module.slz_vpc.vpc_id + prefix = var.prefix + machine_type = var.machine_type + vsi_per_subnet = 1 + ssh_key_ids = [local.ssh_key_id] + user_data = null + manage_reserved_ips = true + enable_floating_ip = true + use_static_boot_volume_name = true + block_storage_volumes = [ + { + name = "vsi-block-1" + profile = "general-purpose" + # snapshot_id =
object({
name = string
id = string
zone = string
cidr = optional(string)
})
)
list(| `[]` | no | +| [block\_storage\_volumes](#input\_block\_storage\_volumes) | List describing the block storage volumes that will be attached to each vsi |
object({
name = string
profile = string
capacity = optional(number)
iops = optional(number)
encryption_key = optional(string)
})
)
list(| `[]` | no | | [boot\_volume\_encryption\_key](#input\_boot\_volume\_encryption\_key) | CRN of boot volume encryption key | `string` | n/a | yes | +| [boot\_volume\_snapshot\_id](#input\_boot\_volume\_snapshot\_id) | The snapshot id of the volume to be used for creating boot volume attachment (if specified, the `image_id` parameter will not be used) | `string` | `null` | no | | [create\_security\_group](#input\_create\_security\_group) | Create security group for VSI. If this is passed as false, the default will be used | `bool` | n/a | yes | | [enable\_floating\_ip](#input\_enable\_floating\_ip) | Create a floating IP for each virtual server created | `bool` | `false` | no | | [existing\_kms\_instance\_guid](#input\_existing\_kms\_instance\_guid) | The GUID of the Hyper Protect Crypto Services or Key Protect instance in which the key specified in var.kms\_key\_crn and var.backup\_encryption\_key\_crn is coming from. Required only if var.skip\_iam\_authorization\_policy is set to false. | `string` | `null` | no | | [image\_id](#input\_image\_id) | Image ID used for VSI. Run 'ibmcloud is images' to find available images in a region | `string` | n/a | yes | | [load\_balancers](#input\_load\_balancers) | Load balancers to add to VSI |
object({
name = string
profile = string
capacity = optional(number)
iops = optional(number)
encryption_key = optional(string)
snapshot_id = optional(string) # set if you would like to base volume on a snapshot
})
)
list(| `[]` | no | | [machine\_type](#input\_machine\_type) | VSI machine type. Run 'ibmcloud is instance-profiles' to get a list of regional profiles | `string` | n/a | yes | +| [manage\_reserved\_ips](#input\_manage\_reserved\_ips) | Set to `true` if you want this terraform module to manage the reserved IP addresses that are assigned to VSI instances. If this option is enabled, when any VSI is recreated it should retain its original IP. | `bool` | `false` | no | | [prefix](#input\_prefix) | The prefix that you would like to append to your resources | `string` | n/a | yes | | [resource\_group\_id](#input\_resource\_group\_id) | ID of resource group to create VSI and block storage volumes. If you wish to create the block storage volumes in a different resource group, you can optionally set that directly in the 'block\_storage\_volumes' variable. | `string` | n/a | yes | | [security\_group](#input\_security\_group) | Security group created for VSI |
object({
name = string
type = string
listener_port = number
listener_protocol = string
connection_limit = number
idle_connection_timeout = optional(number)
algorithm = string
protocol = string
health_delay = number
health_retries = number
health_timeout = number
health_type = string
pool_member_port = string
profile = optional(string)
dns = optional(
object({
instance_crn = string
zone_id = string
})
)
security_group = optional(
object({
name = string
rules = list(
object({
name = string
direction = string
source = string
tcp = optional(
object({
port_max = number
port_min = number
})
)
udp = optional(
object({
port_max = number
port_min = number
})
)
icmp = optional(
object({
type = number
code = number
})
)
})
)
})
)
})
)
object({| n/a | yes | | [security\_group\_ids](#input\_security\_group\_ids) | IDs of additional security groups to be added to VSI deployment primary interface. A VSI interface can have a maximum of 5 security groups. | `list(string)` | `[]` | no | | [skip\_iam\_authorization\_policy](#input\_skip\_iam\_authorization\_policy) | Set to true to skip the creation of an IAM authorization policy that permits all Storage Blocks to read the encryption key from the KMS instance. If set to false, pass in a value for the KMS instance in the existing\_kms\_instance\_guid variable. | `bool` | `false` | no | +| [snapshot\_consistency\_group\_id](#input\_snapshot\_consistency\_group\_id) | The snapshot consistency group Id. If supplied, the group will be queried for snapshots that are matched with both boot volume and attached (attached are matched based on name suffix). You can override specific snapshot Ids by setting the appropriate input variables as well. | `string` | `null` | no | | [ssh\_key\_ids](#input\_ssh\_key\_ids) | ssh key ids to use in creating vsi | `list(string)` | n/a | yes | | [subnets](#input\_subnets) | A list of subnet IDs where VSI will be deployed |
name = string
rules = list(
object({
name = string
direction = string
source = string
tcp = optional(
object({
port_max = number
port_min = number
})
)
udp = optional(
object({
port_max = number
port_min = number
})
)
icmp = optional(
object({
type = number
code = number
})
)
})
)
})
list(| n/a | yes | | [tags](#input\_tags) | List of tags to apply to resources created by this module. | `list(string)` | `[]` | no | +| [use\_static\_boot\_volume\_name](#input\_use\_static\_boot\_volume\_name) | Sets the boot volume name for each VSI to a static name in the format `{hostname}_boot`, instead of a random name. Set this to `true` to have a consistent boot volume name even when VSIs are recreated. | `bool` | `false` | no | | [user\_data](#input\_user\_data) | User data to initialize VSI deployment | `string` | n/a | yes | | [vpc\_id](#input\_vpc\_id) | ID of VPC | `string` | n/a | yes | | [vsi\_per\_subnet](#input\_vsi\_per\_subnet) | Number of VSI instances for each subnet | `number` | n/a | yes | diff --git a/modules/fscloud/main.tf b/modules/fscloud/main.tf index a43868bf..c8d40778 100644 --- a/modules/fscloud/main.tf +++ b/modules/fscloud/main.tf @@ -14,6 +14,8 @@ module "fscloud_vsi" { skip_iam_authorization_policy = var.skip_iam_authorization_policy boot_volume_encryption_key = var.boot_volume_encryption_key kms_encryption_enabled = true + manage_reserved_ips = var.manage_reserved_ips + use_static_boot_volume_name = var.use_static_boot_volume_name enable_floating_ip = var.enable_floating_ip allow_ip_spoofing = var.allow_ip_spoofing create_security_group = var.create_security_group @@ -22,4 +24,6 @@ module "fscloud_vsi" { block_storage_volumes = var.block_storage_volumes load_balancers = var.load_balancers access_tags = var.access_tags + snapshot_consistency_group_id = var.snapshot_consistency_group_id + boot_volume_snapshot_id = var.boot_volume_snapshot_id } diff --git a/modules/fscloud/variables.tf b/modules/fscloud/variables.tf index c3ff92f1..dd49230d 100644 --- a/modules/fscloud/variables.tf +++ b/modules/fscloud/variables.tf @@ -79,6 +79,18 @@ variable "boot_volume_encryption_key" { type = string } +variable "manage_reserved_ips" { + description = "Set to `true` if you want this terraform module to manage the reserved IP addresses that are assigned to VSI instances. If this option is enabled, when any VSI is recreated it should retain its original IP." + type = bool + default = false +} + +variable "use_static_boot_volume_name" { + description = "Sets the boot volume name for each VSI to a static name in the format `{hostname}_boot`, instead of a random name. Set this to `true` to have a consistent boot volume name even when VSIs are recreated." + type = bool + default = false +} + variable "enable_floating_ip" { description = "Create a floating IP for each virtual server created" type = bool @@ -143,6 +155,7 @@ variable "block_storage_volumes" { capacity = optional(number) iops = optional(number) encryption_key = optional(string) + snapshot_id = optional(string) # set if you would like to base volume on a snapshot }) ) default = [] @@ -226,3 +239,19 @@ variable "access_tags" { } ############################################################################## +# Snapshot Restore Variables +############################################################################## + +variable "boot_volume_snapshot_id" { + description = "The snapshot id of the volume to be used for creating boot volume attachment (if specified, the `image_id` parameter will not be used)" + type = string + default = null +} + +variable "snapshot_consistency_group_id" { + description = "The snapshot consistency group Id. If supplied, the group will be queried for snapshots that are matched with both boot volume and attached (attached are matched based on name suffix). You can override specific snapshot Ids by setting the appropriate input variables as well." + type = string + default = null +} + +############################################################################## diff --git a/outputs.tf b/outputs.tf index 5750508c..2fddb4eb 100644 --- a/outputs.tf +++ b/outputs.tf @@ -29,6 +29,7 @@ output "list" { floating_ip_id = var.enable_floating_ip ? ibm_is_floating_ip.vsi_fip[vsi_key].id : null floating_ip_crn = var.enable_floating_ip ? ibm_is_floating_ip.vsi_fip[vsi_key].crn : null vpc_id = var.vpc_id + snapshot_id = one(virtual_server.boot_volume[*].snapshot) } ] } @@ -74,3 +75,19 @@ output "lb_security_groups" { } ############################################################################## + +############################################################################## +# Consistency Group Outputs +############################################################################## + +output "consistency_group_boot_snapshot_id" { + description = "The Snapshot Id used for the VSI boot volume, determined from an optionally supplied consistency group" + value = local.consistency_group_boot_snapshot_id +} + +output "consistency_group_storage_snapshot_ids" { + description = "Map of attached storage volumes requested, and the Snapshot Ids that will be used, determined from an optionally supplied consistency group, and mapped " + value = local.consistency_group_snapshot_to_volume_map +} + +############################################################################## diff --git a/snapshot.tf b/snapshot.tf new file mode 100644 index 00000000..2d190d54 --- /dev/null +++ b/snapshot.tf @@ -0,0 +1,35 @@ +############################################################################## +# Snapshot Consistency Group +# +# If a consistency group id is supplied, determine volume snapshots to use +# by looking at details/service_tags of the snapshots that belong to the group, +# and map those snapshots to their appropriate volumes. +# +# NOTE: any snapshot_id that is specifically set in any input variables will +# take precedence over the consistency group snapshots +############################################################################## + +data "ibm_is_snapshot_consistency_group" "snapshot_group" { + count = var.snapshot_consistency_group_id != null ? 1 : 0 + identifier = var.snapshot_consistency_group_id +} + +data "ibm_is_snapshot" "snapshots_from_group" { + for_each = local.consistency_group_available_snapshots_map + identifier = each.value.id +} + +locals { + # this map with snapshot names as keys is only used to get snapshot details from data block (consistency_group snapshots element does not contain all detail) + consistency_group_available_snapshots_map = length(data.ibm_is_snapshot_consistency_group.snapshot_group) > 0 ? { for snap in data.ibm_is_snapshot_consistency_group.snapshot_group[0].snapshots : snap.name => snap } : {} + + # find the bootable snapshot by service tag, looking always for index 0, if it doesn't exist then no snapshot for boot volume (null) + consistency_group_boot_snapshots = [for snap in data.ibm_is_snapshot.snapshots_from_group : snap if contains(snap.service_tags, "is.instance:attachment_index_0") && snap.bootable] + consistency_group_boot_snapshot_id = one(local.consistency_group_boot_snapshots[*].identifier) + + # loop through desired additional block volumes, and see if snapshot in group exists by looking at service tag and index, starting at _1 + consistency_group_snapshot_to_volume_map = { + for idx, volume in var.block_storage_volumes : + volume.name => one([for snap in data.ibm_is_snapshot.snapshots_from_group : snap.identifier if contains(snap.service_tags, format("%s%s", "is.instance:attachment_index_", tostring(idx + 1)))]) + } +} diff --git a/storage.tf b/storage.tf index 46240aca..1c890d8a 100644 --- a/storage.tf +++ b/storage.tf @@ -17,11 +17,13 @@ locals { vol_name = "${var.prefix}-${substr(var.subnets[subnet].id, -4, 4)}-${format("%03d", count + 1)}-${volume.name}" zone = var.subnets[subnet].zone profile = volume.profile - capacity = volume.capacity + capacity = (volume.snapshot_id == null) ? volume.capacity : null vsi_name = "${var.subnets[subnet].name}-${count}" - iops = volume.iops - encryption_key = var.kms_encryption_enabled ? var.boot_volume_encryption_key : volume.encryption_key + iops = (volume.snapshot_id == null) ? volume.iops : null + encryption_key = (volume.snapshot_id == null) ? (var.kms_encryption_enabled ? var.boot_volume_encryption_key : volume.encryption_key) : null resource_group = volume.resource_group_id != null ? volume.resource_group_id : var.resource_group_id + # check for snapshot in this order: supplied directly in variable -> part of consistency group -> null (no snapshot) + snapshot_id = try(coalesce(volume.snapshot_id, lookup(local.consistency_group_snapshot_to_volume_map, volume.name, null)), null) } ] ] @@ -41,16 +43,17 @@ locals { ############################################################################## resource "ibm_is_volume" "volume" { - for_each = local.volume_map - name = each.value.vol_name - profile = each.value.profile - zone = each.value.zone - iops = each.value.iops - capacity = each.value.capacity - encryption_key = each.value.encryption_key - resource_group = each.value.resource_group - tags = var.tags - access_tags = var.access_tags + for_each = local.volume_map + name = each.value.vol_name + profile = each.value.profile + zone = each.value.zone + iops = each.value.iops + capacity = each.value.capacity + encryption_key = each.value.encryption_key + resource_group = each.value.resource_group + tags = var.tags + access_tags = var.access_tags + source_snapshot = each.value.snapshot_id } ############################################################################## diff --git a/tests/pr_test.go b/tests/pr_test.go index cc24d4b9..0b9a4b01 100644 --- a/tests/pr_test.go +++ b/tests/pr_test.go @@ -6,6 +6,7 @@ import ( "os" "testing" + "github.com/gruntwork-io/terratest/modules/terraform" "github.com/stretchr/testify/assert" "github.com/terraform-ibm-modules/ibmcloud-terratest-wrapper/common" "github.com/terraform-ibm-modules/ibmcloud-terratest-wrapper/testhelper" @@ -14,6 +15,7 @@ import ( const basicExampleTerraformDir = "examples/basic" const completeExampleTerraformDir = "examples/complete" const fsCloudExampleTerraformDir = "examples/fscloud" +const snapshotExampleTerraformDir = "examples/snapshot" const resourceGroup = "geretain-test-resources" const region = "us-south" @@ -115,3 +117,60 @@ func TestRunFSCloudExample(t *testing.T) { assert.Nil(t, err, "This should not have errored.") assert.NotNil(t, output, "Expected some output") } + +func TestRunExistingSnapshotGroupExample(t *testing.T) { + t.Parallel() + + snapGroupId := permanentResources["snapshot_group_au_syd_group_id"] + + options := testhelper.TestOptionsDefaultWithVars(&testhelper.TestOptions{ + Testing: t, + TerraformDir: snapshotExampleTerraformDir, + Prefix: "slz-vsi-snap", + ResourceGroup: resourceGroup, + Region: "au-syd", // hardcode due to image requirement + TerraformVars: map[string]interface{}{ + "access_tags": permanentResources["accessTags"], + "snapshot_consistency_group_id": snapGroupId, + "image_id": "r014-0606d617-b866-4ae8-9588-84935b13ff55", // for au-syd region + }, + }) + + // Add a post-apply verfication + options.PostApplyHook = verifyVolumeSnapshots + + output, err := options.RunTestConsistency() + assert.Nil(t, err, "This should not have errored.") + assert.NotNil(t, output, "Expected some output") + +} + +func verifyVolumeSnapshots(options *testhelper.TestOptions) error { + + if assert.Equal(options.Testing, "examples/snapshot", snapshotExampleTerraformDir) { + options.Testing.Logf("DEBUG: value of global pr_test variable is: %s", snapshotExampleTerraformDir) + } + + snapBootId := permanentResources["snapshot_group_au_syd_boot_id"] + snapVol1Id := permanentResources["snapshot_group_au_syd_vol1_id"] + snapVol2Id := permanentResources["snapshot_group_au_syd_vol2_id"] + + options.Testing.Log("====== START VERIFY OF SNAPSHOTS ========") + + // get ouput of last apply + outputs, outputErr := terraform.OutputAllE(options.Testing, options.TerraformOptions) + + if assert.NoErrorf(options.Testing, outputErr, "error getting last terraform apply outputs: %s", outputErr) { + // first, verify the outputs for snapshot IDs were correctly used from group + assert.Equal(options.Testing, snapBootId, outputs["slz_vsi"].(map[string]interface{})["consistency_group_boot_snapshot_id"]) + // check to make sure that TWO attachment snapshots were configured from group + if assert.Equal(options.Testing, 2, len(outputs["slz_vsi"].(map[string]interface{})["consistency_group_storage_snapshot_ids"].(map[string]interface{}))) { + assert.Equal(options.Testing, snapVol1Id, outputs["slz_vsi"].(map[string]interface{})["consistency_group_storage_snapshot_ids"].(map[string]interface{})["vsi-block-1"].(string)) + assert.Equal(options.Testing, snapVol2Id, outputs["slz_vsi"].(map[string]interface{})["consistency_group_storage_snapshot_ids"].(map[string]interface{})["vsi-block-2"].(string)) + } + } + + options.Testing.Log("====== END VERIFY OF SNAPSHOTS ========") + + return nil +} diff --git a/variables.tf b/variables.tf index 4333aff0..8210b3fb 100644 --- a/variables.tf +++ b/variables.tf @@ -104,6 +104,18 @@ variable "existing_kms_instance_guid" { default = null } +variable "manage_reserved_ips" { + description = "Set to `true` if you want this terraform module to manage the reserved IP addresses that are assigned to VSI instances. If this option is enabled, when any VSI is recreated it should retain its original IP." + type = bool + default = false +} + +variable "use_static_boot_volume_name" { + description = "Sets the boot volume name for each VSI to a static name in the format `{hostname}_boot`, instead of a random name. Set this to `true` to have a consistent boot volume name even when VSIs are recreated." + type = bool + default = false +} + variable "enable_floating_ip" { description = "Create a floating IP for each virtual server created" type = bool @@ -219,6 +231,7 @@ variable "block_storage_volumes" { iops = optional(number) encryption_key = optional(string) resource_group_id = optional(string) + snapshot_id = optional(string) # set if you would like to base volume on a snapshot }) ) default = [] @@ -451,3 +464,21 @@ variable "secondary_allow_ip_spoofing" { } ############################################################################## + +############################################################################## +# Snapshot Restore Variables +############################################################################## + +variable "boot_volume_snapshot_id" { + description = "The snapshot id of the volume to be used for creating boot volume attachment (if specified, the `image_id` parameter will not be used)" + type = string + default = null +} + +variable "snapshot_consistency_group_id" { + description = "The snapshot consistency group Id. If supplied, the group will be queried for snapshots that are matched with both boot volume and attached (attached are matched based on name suffix). You can override specific snapshot Ids by setting the appropriate input variables as well." + type = string + default = null +} + +############################################################################## diff --git a/version.tf b/version.tf index 90de73fb..2b33c136 100644 --- a/version.tf +++ b/version.tf @@ -4,7 +4,7 @@ terraform { # Use "greater than or equal to" range in modules ibm = { source = "IBM-Cloud/ibm" - version = ">= 1.59.0, < 2.0.0" + version = ">= 1.65.0, < 2.0.0" } time = { source = "hashicorp/time"
object({
name = string
id = string
zone = string
cidr = string
})
)