Cloudfront blue green (#263)
This commit is contained in:
@@ -30,6 +30,49 @@ These are the minimum versions of modules required if used:
|
||||
|
||||
Previous versions do not allow for proper interaction with both the software installers and logging s3 buckets.
|
||||
|
||||
## Blue-Green Key Rotation
|
||||
|
||||
This module supports rotating CloudFront signing keys without invalidating existing signed URLs. Instead of a single `public_key` / `private_key` pair, you can provide a map of named keypairs and select which one is active:
|
||||
|
||||
```hcl
|
||||
module "cloudfront-software-installers" {
|
||||
source = "..."
|
||||
customer = "fleet"
|
||||
s3_bucket = module.main.byo-vpc.byo-db.byo-ecs.fleet_s3_software_installers_config.bucket_name
|
||||
|
||||
keypairs = {
|
||||
current = {
|
||||
public_key = data.aws_kms_secrets.cloudfront.plaintext["public_key"]
|
||||
private_key = data.aws_kms_secrets.cloudfront.plaintext["private_key"]
|
||||
}
|
||||
next = {
|
||||
public_key = data.aws_kms_secrets.cloudfront_next.plaintext["public_key"]
|
||||
private_key = data.aws_kms_secrets.cloudfront_next.plaintext["private_key"]
|
||||
}
|
||||
}
|
||||
|
||||
active_keypair_name = "current"
|
||||
}
|
||||
```
|
||||
|
||||
All keypairs in the map are added to the CloudFront key group, so signed URLs created by any retained key remain valid. Only the active keypair populates the Secrets Manager secret that Fleet reads.
|
||||
|
||||
### Rotation procedure
|
||||
|
||||
1. **Upgrade the module** with the existing key only (or via the legacy `public_key` / `private_key` inputs). Confirm Terraform plans only a resource move (`aws_cloudfront_public_key.software_installers[0]` → `["current"]`) with no create/update/delete.
|
||||
2. **Add the new keypair** under any new map name (e.g. `next`), leaving `active_keypair_name = "current"`. Apply and wait for the CloudFront key group update to deploy.
|
||||
3. **Flip the active key** by changing `active_keypair_name` to the new key name and applying. This creates a new Secrets Manager secret version.
|
||||
4. **Redeploy Fleet ECS tasks** so the container receives the updated secret values.
|
||||
5. **Retire the old keypair** after the maximum signed URL lifetime has passed. Remove it from the map and apply. If CloudFront reports the old public key is still in use, wait for propagation and apply again.
|
||||
|
||||
### Legacy inputs
|
||||
|
||||
The `public_key` and `private_key` variables are deprecated but still supported. When `keypairs` is not set, they are normalized into a single `current` keypair internally. Migrate to the `keypairs` map at your convenience.
|
||||
|
||||
### External key groups
|
||||
|
||||
Blue-green key rotation is not supported when `key_group_id` is set. When using an external key group, the module cannot manage the public key resources, so `active_keypair_name` cannot select the correct public key ID. Use `keypairs` only when the module manages the key group (i.e., `key_group_id` is not set).
|
||||
|
||||
## Configuration considerations for other modules
|
||||
|
||||
### tf-mod-root/tf-mod-byo-vpc/tf-mod-byo-db/tf-mod-byo-ecs
|
||||
|
||||
@@ -29,6 +29,49 @@ These are the minimum versions of modules required if used:
|
||||
|
||||
Previous versions do not allow for proper interaction with both the software installers and logging s3 buckets.
|
||||
|
||||
## Blue-Green Key Rotation
|
||||
|
||||
This module supports rotating CloudFront signing keys without invalidating existing signed URLs. Instead of a single `public_key` / `private_key` pair, you can provide a map of named keypairs and select which one is active:
|
||||
|
||||
```hcl
|
||||
module "cloudfront-software-installers" {
|
||||
source = "..."
|
||||
customer = "fleet"
|
||||
s3_bucket = module.main.byo-vpc.byo-db.byo-ecs.fleet_s3_software_installers_config.bucket_name
|
||||
|
||||
keypairs = {
|
||||
current = {
|
||||
public_key = data.aws_kms_secrets.cloudfront.plaintext["public_key"]
|
||||
private_key = data.aws_kms_secrets.cloudfront.plaintext["private_key"]
|
||||
}
|
||||
next = {
|
||||
public_key = data.aws_kms_secrets.cloudfront_next.plaintext["public_key"]
|
||||
private_key = data.aws_kms_secrets.cloudfront_next.plaintext["private_key"]
|
||||
}
|
||||
}
|
||||
|
||||
active_keypair_name = "current"
|
||||
}
|
||||
```
|
||||
|
||||
All keypairs in the map are added to the CloudFront key group, so signed URLs created by any retained key remain valid. Only the active keypair populates the Secrets Manager secret that Fleet reads.
|
||||
|
||||
### Rotation procedure
|
||||
|
||||
1. **Upgrade the module** with the existing key only (or via the legacy `public_key` / `private_key` inputs). Confirm Terraform plans only a resource move (`aws_cloudfront_public_key.software_installers[0]` → `["current"]`) with no create/update/delete.
|
||||
2. **Add the new keypair** under any new map name (e.g. `next`), leaving `active_keypair_name = "current"`. Apply and wait for the CloudFront key group update to deploy.
|
||||
3. **Flip the active key** by changing `active_keypair_name` to the new key name and applying. This creates a new Secrets Manager secret version.
|
||||
4. **Redeploy Fleet ECS tasks** so the container receives the updated secret values.
|
||||
5. **Retire the old keypair** after the maximum signed URL lifetime has passed. Remove it from the map and apply. If CloudFront reports the old public key is still in use, wait for propagation and apply again.
|
||||
|
||||
### Legacy inputs
|
||||
|
||||
The `public_key` and `private_key` variables are deprecated but still supported. When `keypairs` is not set, they are normalized into a single `current` keypair internally. Migrate to the `keypairs` map at your convenience.
|
||||
|
||||
### External key groups
|
||||
|
||||
Blue-green key rotation is not supported when `key_group_id` is set. When using an external key group, the module cannot manage the public key resources, so `active_keypair_name` cannot select the correct public key ID. Use `keypairs` only when the module manages the key group (i.e., `key_group_id` is not set).
|
||||
|
||||
## Configuration considerations for other modules
|
||||
|
||||
### tf-mod-root/tf-mod-byo-vpc/tf-mod-byo-db/tf-mod-byo-ecs
|
||||
@@ -180,19 +223,19 @@ No requirements.
|
||||
## Providers
|
||||
|
||||
| Name | Version |
|
||||
|------|---------|
|
||||
| <a name="provider_aws"></a> [aws](#provider\_aws) | 6.49.0 |
|
||||
| ---- | ------- |
|
||||
| <a name="provider_aws"></a> [aws](#provider\_aws) | 6.54.0 |
|
||||
|
||||
## Modules
|
||||
|
||||
| Name | Source | Version |
|
||||
|------|--------|---------|
|
||||
| ---- | ------ | ------- |
|
||||
| <a name="module_cloudfront_software_installers"></a> [cloudfront\_software\_installers](#module\_cloudfront\_software\_installers) | terraform-aws-modules/cloudfront/aws | 5.2.0 |
|
||||
|
||||
## Resources
|
||||
|
||||
| Name | Type |
|
||||
|------|------|
|
||||
| ---- | ---- |
|
||||
| [aws_cloudfront_key_group.software_installers](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_key_group) | resource |
|
||||
| [aws_cloudfront_public_key.software_installers](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_public_key) | resource |
|
||||
| [aws_iam_policy.software_installers_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
|
||||
@@ -205,21 +248,23 @@ No requirements.
|
||||
## Inputs
|
||||
|
||||
| Name | Description | Type | Default | Required |
|
||||
|------|-------------|------|---------|:--------:|
|
||||
| ---- | ----------- | ---- | ------- | :------: |
|
||||
| <a name="input_active_keypair_name"></a> [active\_keypair\_name](#input\_active\_keypair\_name) | Name of the keypair in `keypairs` (or `"current"` when using legacy inputs) whose keys populate the Secrets Manager secret. All keypairs in the map are added to the CloudFront key group so signed URLs from any retained key remain valid. | `string` | `"current"` | no |
|
||||
| <a name="input_customer"></a> [customer](#input\_customer) | Customer name for the cloudfront instance | `string` | `"fleet"` | no |
|
||||
| <a name="input_enable_logging"></a> [enable\_logging](#input\_enable\_logging) | Enable optional logging to s3 | `bool` | `false` | no |
|
||||
| <a name="input_key_group_id"></a> [key\_group\_id](#input\_key\_group\_id) | Cloudfront key group id | `string` | `null` | no |
|
||||
| <a name="input_keypairs"></a> [keypairs](#input\_keypairs) | Map of named keypairs for blue-green key rotation. Each value must contain `public_key` and `private_key`. When set, `public_key` and `private_key` variables are ignored. | <pre>map(object({<br/> public_key = string<br/> private_key = string<br/> }))</pre> | `null` | no |
|
||||
| <a name="input_logging_s3_bucket"></a> [logging\_s3\_bucket](#input\_logging\_s3\_bucket) | s3 bucket to log to | `string` | `null` | no |
|
||||
| <a name="input_logging_s3_prefix"></a> [logging\_s3\_prefix](#input\_logging\_s3\_prefix) | logging s3 bucket prefix | `string` | `"cloudfront"` | no |
|
||||
| <a name="input_private_key"></a> [private\_key](#input\_private\_key) | Private key used for signed URLs | `string` | n/a | yes |
|
||||
| <a name="input_public_key"></a> [public\_key](#input\_public\_key) | Public key used for signed URLs | `string` | n/a | yes |
|
||||
| <a name="input_private_key"></a> [private\_key](#input\_private\_key) | Private key used for signed URLs. Deprecated: use `keypairs` instead. | `string` | `null` | no |
|
||||
| <a name="input_public_key"></a> [public\_key](#input\_public\_key) | Public key used for signed URLs. Deprecated: use `keypairs` instead. | `string` | `null` | no |
|
||||
| <a name="input_public_key_id"></a> [public\_key\_id](#input\_public\_key\_id) | Cloudfront public key id. Required when passing in a key\_group\_id | `string` | `null` | no |
|
||||
| <a name="input_s3_bucket"></a> [s3\_bucket](#input\_s3\_bucket) | Name of the S3 bucket that Cloudfront will point to | `string` | n/a | yes |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Name | Description |
|
||||
|------|-------------|
|
||||
| ---- | ----------- |
|
||||
| <a name="output_cloudfront_arn"></a> [cloudfront\_arn](#output\_cloudfront\_arn) | n/a |
|
||||
| <a name="output_cloudfront_distribution_arn"></a> [cloudfront\_distribution\_arn](#output\_cloudfront\_distribution\_arn) | n/a |
|
||||
| <a name="output_extra_execution_iam_policies"></a> [extra\_execution\_iam\_policies](#output\_extra\_execution\_iam\_policies) | n/a |
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
locals {
|
||||
keypairs = var.keypairs != null ? var.keypairs : {
|
||||
current = {
|
||||
public_key = var.public_key
|
||||
private_key = var.private_key
|
||||
}
|
||||
}
|
||||
|
||||
keypair_names = var.keypairs != null ? keys(var.keypairs) : ["current"]
|
||||
}
|
||||
|
||||
data "aws_s3_bucket" "software_installers" {
|
||||
bucket = var.s3_bucket
|
||||
}
|
||||
@@ -15,16 +26,22 @@ resource "aws_iam_policy" "software_installers_secret" {
|
||||
}
|
||||
|
||||
resource "aws_cloudfront_public_key" "software_installers" {
|
||||
count = var.key_group_id == null ? 1 : 0
|
||||
comment = "${var.customer} software installers public key"
|
||||
encoded_key = var.public_key
|
||||
name = "${var.customer}-software-installers"
|
||||
for_each = var.key_group_id == null ? toset(nonsensitive(local.keypair_names)) : toset([])
|
||||
|
||||
comment = each.key == "current" ? "${var.customer} software installers public key" : "${var.customer} software installers public key ${each.key}"
|
||||
encoded_key = local.keypairs[each.key].public_key
|
||||
name = each.key == "current" ? "${var.customer}-software-installers" : "${var.customer}-software-installers-${each.key}"
|
||||
}
|
||||
|
||||
moved {
|
||||
from = aws_cloudfront_public_key.software_installers[0]
|
||||
to = aws_cloudfront_public_key.software_installers["current"]
|
||||
}
|
||||
|
||||
resource "aws_cloudfront_key_group" "software_installers" {
|
||||
count = var.key_group_id == null ? 1 : 0
|
||||
comment = "${var.customer} software installers key group"
|
||||
items = [aws_cloudfront_public_key.software_installers[0].id]
|
||||
items = values(aws_cloudfront_public_key.software_installers)[*].id
|
||||
name = "${var.customer}-software-installers-group"
|
||||
}
|
||||
|
||||
@@ -35,10 +52,10 @@ resource "aws_secretsmanager_secret" "software_installers" {
|
||||
resource "aws_secretsmanager_secret_version" "software_installers" {
|
||||
secret_id = aws_secretsmanager_secret.software_installers.id
|
||||
secret_string = jsonencode({
|
||||
FLEET_S3_SOFTWARE_INSTALLERS_CLOUDFRONT_URL_SIGNING_PRIVATE_KEY = var.private_key
|
||||
FLEET_S3_SOFTWARE_INSTALLERS_CLOUDFRONT_URL_SIGNING_PUBLIC_KEY = var.public_key
|
||||
FLEET_S3_SOFTWARE_INSTALLERS_CLOUDFRONT_URL_SIGNING_PRIVATE_KEY = local.keypairs[var.active_keypair_name].private_key
|
||||
FLEET_S3_SOFTWARE_INSTALLERS_CLOUDFRONT_URL_SIGNING_PUBLIC_KEY = local.keypairs[var.active_keypair_name].public_key
|
||||
FLEET_S3_SOFTWARE_INSTALLERS_CLOUDFRONT_URL = "https://${module.cloudfront_software_installers.cloudfront_distribution_domain_name}"
|
||||
FLEET_S3_SOFTWARE_INSTALLERS_CLOUDFRONT_URL_SIGNING_PUBLIC_KEY_ID = var.public_key_id == null ? aws_cloudfront_public_key.software_installers[0].id : var.public_key_id
|
||||
FLEET_S3_SOFTWARE_INSTALLERS_CLOUDFRONT_URL_SIGNING_PUBLIC_KEY_ID = var.public_key_id == null ? aws_cloudfront_public_key.software_installers[var.active_keypair_name].id : var.public_key_id
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -26,14 +26,46 @@ variable "public_key_id" {
|
||||
}
|
||||
}
|
||||
|
||||
variable "private_key" {
|
||||
description = "Private key used for signed URLs"
|
||||
variable "keypairs" {
|
||||
description = "Map of named keypairs for blue-green key rotation. Each value must contain `public_key` and `private_key`. When set, `public_key` and `private_key` variables are ignored."
|
||||
type = map(object({
|
||||
public_key = string
|
||||
private_key = string
|
||||
}))
|
||||
default = null
|
||||
|
||||
validation {
|
||||
condition = var.keypairs != null || (var.public_key != null && var.private_key != null)
|
||||
error_message = "Either keypairs must be set, or both public_key and private_key must be provided."
|
||||
}
|
||||
|
||||
validation {
|
||||
condition = var.keypairs == null || var.key_group_id == null
|
||||
error_message = "keypairs cannot be used with key_group_id. Blue-green key rotation requires the module to manage the CloudFront key group."
|
||||
}
|
||||
}
|
||||
|
||||
variable "active_keypair_name" {
|
||||
description = "Name of the keypair in `keypairs` (or `\"current\"` when using legacy inputs) whose keys populate the Secrets Manager secret. All keypairs in the map are added to the CloudFront key group so signed URLs from any retained key remain valid."
|
||||
type = string
|
||||
default = "current"
|
||||
|
||||
validation {
|
||||
condition = var.keypairs == null ? true : contains(keys(var.keypairs), var.active_keypair_name)
|
||||
error_message = "active_keypair_name must be a key present in the keypairs map."
|
||||
}
|
||||
}
|
||||
|
||||
variable "private_key" {
|
||||
description = "Private key used for signed URLs. Deprecated: use `keypairs` instead."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "public_key" {
|
||||
description = "Public key used for signed URLs"
|
||||
description = "Public key used for signed URLs. Deprecated: use `keypairs` instead."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "s3_bucket" {
|
||||
|
||||
Reference in New Issue
Block a user