NEW: Initial commit

Terraform modules for AWS Zonal Shift demo
This commit is contained in:
KenF
2025-05-17 22:10:34 +08:00
commit bc71da905f
19 changed files with 592 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
*.tfstate.backup
*.backup
*.tfstate
*.tfstate.lock
**/*.tfstate
**/*.backup
.terraform/
.DS_Store
*.iml
.idea
.terraform.lock.hcl
*.log
examples/
experimental/
headdesk-aws/
vsphere-yige/
anz-sandbox/
+18
View File
@@ -0,0 +1,18 @@
# ZonelShiftLab
Deploy VPC, Subnet, Ec2, NLB for testing Zonal Shift
## Description
When zonel shift is initiated, the paused AZ will stop responding after 1-2 minutes
After cancelling a zonal shift, wait for DNS TTL to expire and the resumed AZ should start responding.
To initiate / cancel zonal shift:
```bash
aws arc-zonal-shift start-zonal-shift \
--resource-identifier <RESOURCE_ARN> \
--away-from <AZ_ID> \
--expires-in <DURATION eg 30m> \
--comment AzFailoverTest
aws arc-zonal-shift cancel-zonal-shift --zonal-shift-id <SHIFT_ID>
```
+190
View File
@@ -0,0 +1,190 @@
# main.tf
data "aws_availability_zones" "available" {}
locals {
azs = slice(data.aws_availability_zones.available.names, 0, 2)
vpc_cidr = "10.0.0.0/16"
}
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.21.0"
name = "${var.environment}-${var.project}-${var.application}-vpc01"
cidr = local.vpc_cidr
azs = local.azs
private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k)]
public_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 4)]
# private_subnet_names = ["vpc01-private1", "vpc01-private2"]
manage_default_network_acl = false
manage_default_route_table = false
manage_default_security_group = false
enable_dns_hostnames = true
enable_dns_support = true
enable_nat_gateway = true
single_nat_gateway = true
enable_flow_log = false
}
data "aws_ami" "al2023" {
most_recent = true
name_regex = "^al2023-ami-2023.*kernel-6.12-arm64"
owners = ["amazon"]
}
module "ec2" {
source = "terraform-aws-modules/ec2-instance/aws"
count = length(local.azs)
name = "${var.environment}-${var.project}-${var.application}-web${count.index + 1}"
instance_type = "t4g.nano"
ami = data.aws_ami.al2023.id
launch_template = {
id = aws_launch_template.this.id
version = "$Latest"
}
vpc_security_group_ids = [module.sg.id]
subnet_id = module.vpc.private_subnets[count.index]
}
module "sg" {
source = "modules/security_group"
name = "WebServers"
description = "SG of Web servers"
vpc-id = module.vpc.vpc_id
ingress = {
r1 = "tcp,80,80,0.0.0.0/0,Public web access"
}
egress = {
r1 = "-1,-1,-1,0.0.0.0/0,Allow outbound traffic"
}
}
module "Ec2InstanceProfile" {
source = "modules/iam-role-v2"
role-name = "${var.environment}-${var.project}-${var.application}-role"
description = "Ec2 instance role"
create-instance-profile = true
trusted-entity = "ec2.amazonaws.com"
}
resource "aws_iam_role_policy_attachment" "this" {
role = module.Ec2InstanceProfile.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_launch_template" "this" {
name = "${var.environment}-${var.project}-${var.application}-web"
description = "Al2023 spot with nginx"
block_device_mappings {
device_name = "/dev/sda1"
ebs {
volume_size = 10
}
}
credit_specification {
cpu_credits = "standard"
}
ebs_optimized = true
iam_instance_profile {
name = module.Ec2InstanceProfile.profile-name[0]
}
image_id = data.aws_ami.al2023.id
instance_type = "t4g.nano"
instance_initiated_shutdown_behavior = "terminate"
instance_market_options {
market_type = "spot"
}
metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
http_put_response_hop_limit = 1
instance_metadata_tags = "enabled"
}
vpc_security_group_ids = [module.sg.id]
user_data = filebase64("userdata.sh")
}
resource "aws_eip" "eip" {
count = length(local.azs)
domain = "vpc"
}
module "nlb" {
source = "terraform-aws-modules/alb/aws"
version = "9.16.0"
name = "${var.environment}-${var.project}-${var.application}-nlb01"
load_balancer_type = "network"
vpc_id = module.vpc.vpc_id
create_security_group = false
enable_deletion_protection = false
subnet_mapping = [for i, eip in aws_eip.eip :
{
allocation_id = eip.id
subnet_id = module.vpc.public_subnets[i]
}
]
enable_cross_zone_load_balancing = true
enable_zonal_shift = true
listeners = {
tcp80 = {
port = 80
protocol = "TCP"
forward = {
target_group_key = "tcp80"
}
}
}
target_groups = {
tcp80 = {
name_prefix = "tcp80-"
protocol = "TCP"
port = 80
target_type = "instance"
target_id = module.ec2[0].id
deregistration_delay = 60
preserve_client_ip = true
target_health_state = {
# must be disabled for zonal shift
# https://docs.aws.amazon.com/elasticloadbalancing/latest/network/enable-zonal-shift.html
enable_unhealthy_connection_termination = false
}
health_check = {
enabled = true
healthy_threshold = 2
unhealthy_threshold = 2
interval = 10
protocol = "TCP"
timeout = 3
}
}
}
additional_target_group_attachments = {
tcp80-2 = {
target_group_key = "tcp80"
target_type = "instance"
target_id = module.ec2[1].id
port = "80"
}
}
}
+12
View File
@@ -0,0 +1,12 @@
BSD Zero Clause License
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
+56
View File
@@ -0,0 +1,56 @@
<!-- This readme file is generated with terraform-docs -->
Inline policy for IAM role is not supported by this module. Use managed policies instead.
When trusted-entity is provided as an AWS service name (e.g ec2.amazonaws.com), the assume role
policy will be generated. Otherwise, the trusted-entity variable is assumed to be a json-encoded
policy. Assume role policy will be set with the json-encoded string. See examples.
## Requirements
| Name | Version |
|------|---------|
| terraform | >= 1.3.0 |
| aws | >= 5.4.0 |
## Providers
| Name | Version |
|------|---------|
| aws | >= 5.4.0 |
## Modules
No modules.
## Resources
| Name | Type |
|------|------|
| [aws_iam_instance_profile.ip](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_instance_profile) | resource |
| [aws_iam_policy.p](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
| [aws_iam_role.r](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
| [aws_iam_role_policy_attachment.pa](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
## Inputs
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| create-instance-profile | Determines whether instance profile will be created | `bool` | `false` | no |
| description | Description of IAM role | `string` | n/a | yes |
| path | Path of IAM role. Defaults to /Customer/ | `string` | `"/Customer/"` | no |
| policies | Map of policies to be created and attached | <pre>map(<br> object(<br> {<br> description = string<br> policy = string<br> }<br> )<br> )</pre> | `{}` | no |
| role-name | Name of IAM role | `string` | n/a | yes |
| trusted-entity | AWS service allowed to assume this role or a full assume role policy | `string` | n/a | yes |
## Outputs
| Name | Description |
|------|-------------|
| instance-profile-arn | ARN of IAM instance profile |
| name | Name of IAM role |
| profile-name | Name of IAM instance profile |
| role-arn | IAM role ARN |
---
## Authorship
This module was developed by KF.
+47
View File
@@ -0,0 +1,47 @@
# Assume role policy can be provided as-is, or built using the trusted-entity variable
locals {
assume-role-policy = endswith(var.trusted-entity, ".com") ? jsonencode(
{
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Principal" : {
"Service" : [
var.trusted-entity
]
},
"Action" : "sts:AssumeRole"
}
]
}
) : var.trusted-entity
}
resource "aws_iam_instance_profile" "ip" {
count = var.create-instance-profile ? 1 : 0
name = "${var.role-name}-profile"
role = aws_iam_role.r.name
path = var.path
}
resource "aws_iam_role" "r" {
name = var.role-name
description = var.description
assume_role_policy = local.assume-role-policy
force_detach_policies = true
path = var.path
}
resource "aws_iam_policy" "p" {
for_each = var.policies
description = each.value.description
name = each.key
policy = each.value.policy
}
resource "aws_iam_role_policy_attachment" "pa" {
for_each = aws_iam_policy.p
role = aws_iam_role.r.name
policy_arn = each.value.arn
}
+19
View File
@@ -0,0 +1,19 @@
output "profile-name" {
description = "Name of IAM instance profile"
value = aws_iam_instance_profile.ip[*].name
}
output "role-arn" {
description = "IAM role ARN"
value = aws_iam_role.r.arn
}
output "name" {
description = "Name of IAM role"
value = aws_iam_role.r.name
}
output "instance-profile-arn" {
description = "ARN of IAM instance profile"
value = aws_iam_instance_profile.ip.*.arn
}
+9
View File
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 1.3.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.4.0"
}
}
}
+39
View File
@@ -0,0 +1,39 @@
variable "create-instance-profile" {
description = "Determines whether instance profile will be created"
type = bool
default = false
}
variable "description" {
description = "Description of IAM role"
type = string
}
variable "policies" {
description = "Map of policies to be created and attached"
type = map(
object(
{
description = string
policy = string
}
)
)
default = {}
}
variable "role-name" {
description = "Name of IAM role"
type = string
}
variable "path" {
description = "Path of IAM role. Defaults to /Customer/"
type = string
default = "/Customer/"
}
variable "trusted-entity" {
description = "AWS service allowed to assume this role or a full assume role policy"
type = string
}
+43
View File
@@ -0,0 +1,43 @@
<!-- This readme file is generated with terraform-docs -->
## Requirements
No requirements.
## Providers
| Name | Version |
|------|---------|
| aws | n/a |
## Modules
No modules.
## Resources
| Name | Type |
|------|------|
| [aws_security_group.sg](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource |
| [aws_vpc_security_group_egress_rule.egress-rules](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource |
| [aws_vpc_security_group_ingress_rule.ingress-rules](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource |
| [aws_default_tags.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/default_tags) | data source |
## Inputs
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| description | Description of SG | `string` | n/a | yes |
| egress | Map of string where each string is a comma-separated Egress SG rule. For example r1 = "-1,-1,-1,0.0.0.0/0,Allow All" | `map(string)` | n/a | yes |
| ingress | Map of string where each string is a comma-separated Ingress SG rule. For example r1 = "-1,-1,-1,0.0.0.0/0,Allow All" | `map(string)` | n/a | yes |
| name | Name of SG | `string` | n/a | yes |
| vpc-id | ID of VPC | `string` | n/a | yes |
## Outputs
| Name | Description |
|------|-------------|
| id | n/a |
---
## Authorship
This module was developed by xpk.
+32
View File
@@ -0,0 +1,32 @@
module "example-sg" {
source = "../"
name = "bastion-sg"
description = "SG of EC2 bastion instances"
vpc-id = "vpc-12345678"
ingress = {
r1 = "tcp,4750,4750,1.2.3.4/32,Patch Management Tool"
r2 = "tcp,22,22,1.2.3.4/32,Patch Management Tool"
r3 = "tcp,52311,52311,${aws_ec2_managed_prefix_list.example.id},BigFix server to client"
}
egress = {
r1 = "-1,-1,-1,0.0.0.0/0,Allow Ingress from all"
}
}
resource "aws_ec2_managed_prefix_list" "example" {
name = "Omprem subnets"
address_family = "IPv4"
max_entries = 5
dynamic "entry" {
for_each = toset([
"192.168.99.0/24",
"192.168.100.0/24"
])
content {
cidr = entry.value
description = "Onprem management subnets"
}
}
}
+39
View File
@@ -0,0 +1,39 @@
data "aws_default_tags" "this" {
lifecycle {
postcondition {
condition = length(self.tags) >= 1
error_message = "Validation failed: Provider default_tags not set."
}
}
}
resource "aws_security_group" "sg" {
name = var.name
description = var.description
vpc_id = var.vpc-id
tags = { Name = var.name }
}
resource "aws_vpc_security_group_ingress_rule" "ingress-rules" {
for_each = var.ingress
security_group_id = aws_security_group.sg.id
ip_protocol = split(",", each.value)[0]
from_port = split(",", each.value)[1]
to_port = split(",", each.value)[2]
cidr_ipv4 = substr(split(",", each.value)[3], 2, 1) != "-" ? split(",", each.value)[3] : null
referenced_security_group_id = substr(split(",", each.value)[3], 0, 2) == "sg" ? split(",", each.value)[3] : null
prefix_list_id = substr(split(",", each.value)[3], 0, 2) == "pl" ? split(",", each.value)[3] : null
description = split(",", each.value)[4]
}
resource "aws_vpc_security_group_egress_rule" "egress-rules" {
for_each = var.egress
security_group_id = aws_security_group.sg.id
ip_protocol = split(",", each.value)[0]
from_port = split(",", each.value)[1]
to_port = split(",", each.value)[2]
cidr_ipv4 = substr(split(",", each.value)[3], 2, 1) != "-" ? split(",", each.value)[3] : null
referenced_security_group_id = substr(split(",", each.value)[3], 0, 2) == "sg" ? split(",", each.value)[3] : null
prefix_list_id = substr(split(",", each.value)[3], 0, 2) == "pl" ? split(",", each.value)[3] : null
description = split(",", each.value)[4]
}
+3
View File
@@ -0,0 +1,3 @@
output id {
value = aws_security_group.sg.id
}
+20
View File
@@ -0,0 +1,20 @@
variable "name" {
description = "Name of SG"
type = string
}
variable "description" {
description = "Description of SG"
type = string
}
variable "vpc-id" {
description = "ID of VPC"
type = string
}
variable "ingress" {
description = "Map of string where each string is a comma-separated Ingress SG rule. For example r1 = \"-1,-1,-1,0.0.0.0/0,Allow All\""
type = map(string)
}
variable "egress" {
description = "Map of string where each string is a comma-separated Egress SG rule. For example r1 = \"-1,-1,-1,0.0.0.0/0,Allow All\""
type = map(string)
}
+11
View File
@@ -0,0 +1,11 @@
output "NlbDns" {
value = module.nlb.dns_name
}
output "NlbArn" {
value = module.nlb.arn
}
output AzId {
value = slice(data.aws_availability_zones.available.zone_ids, 0, 2)
}
+25
View File
@@ -0,0 +1,25 @@
provider "aws" {
region = var.aws-region
default_tags {
tags = {
Environment = var.environment
Project = var.project
Application = var.application
TerraformDir = "${reverse(split("/", path.cwd))[1]}/${reverse(split("/", path.cwd))[0]}"
}
}
}
terraform {
required_version = ">= 1.3"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}
output "last-updated" {
value = timestamp()
}
+4
View File
@@ -0,0 +1,4 @@
aws-region = "ap-northeast-1"
environment = "Lab"
project = "Demo"
application = "ZonalShift"
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
dnf -q -y install nginx
systemctl enable --now nginx
ec2-metadata -z > /usr/share/nginx/html/index.html
+4
View File
@@ -0,0 +1,4 @@
variable "aws-region" {}
variable "environment" {}
variable "project" {}
variable "application" {}