From bc71da905feb7b19b9182e04549ca1a6f9013d0f Mon Sep 17 00:00:00 2001 From: KenF Date: Sat, 17 May 2025 22:10:34 +0800 Subject: [PATCH] NEW: Initial commit Terraform modules for AWS Zonal Shift demo --- .gitignore | 17 +++ README.md | 18 +++ main.tf | 190 +++++++++++++++++++++++++ modules/iam-role-v2/LICENSE | 12 ++ modules/iam-role-v2/README.md | 56 ++++++++ modules/iam-role-v2/main.tf | 47 ++++++ modules/iam-role-v2/outputs.tf | 19 +++ modules/iam-role-v2/provider.tf | 9 ++ modules/iam-role-v2/variables.tf | 39 +++++ modules/security_group/README.md | 43 ++++++ modules/security_group/example/main.tf | 32 +++++ modules/security_group/main.tf | 39 +++++ modules/security_group/outputs.tf | 3 + modules/security_group/variables.tf | 20 +++ outputs.tf | 11 ++ provider.tf | 25 ++++ terraform.tfvars | 4 + userdata.sh | 4 + variables.tf | 4 + 19 files changed, 592 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 main.tf create mode 100644 modules/iam-role-v2/LICENSE create mode 100644 modules/iam-role-v2/README.md create mode 100644 modules/iam-role-v2/main.tf create mode 100644 modules/iam-role-v2/outputs.tf create mode 100644 modules/iam-role-v2/provider.tf create mode 100644 modules/iam-role-v2/variables.tf create mode 100644 modules/security_group/README.md create mode 100644 modules/security_group/example/main.tf create mode 100644 modules/security_group/main.tf create mode 100644 modules/security_group/outputs.tf create mode 100644 modules/security_group/variables.tf create mode 100644 outputs.tf create mode 100644 provider.tf create mode 100644 terraform.tfvars create mode 100644 userdata.sh create mode 100644 variables.tf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ea01ad --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d46196 --- /dev/null +++ b/README.md @@ -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 \ + --away-from \ + --expires-in \ + --comment AzFailoverTest + +aws arc-zonal-shift cancel-zonal-shift --zonal-shift-id +``` diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..2d26558 --- /dev/null +++ b/main.tf @@ -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" + } + } +} \ No newline at end of file diff --git a/modules/iam-role-v2/LICENSE b/modules/iam-role-v2/LICENSE new file mode 100644 index 0000000..b64d22a --- /dev/null +++ b/modules/iam-role-v2/LICENSE @@ -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. \ No newline at end of file diff --git a/modules/iam-role-v2/README.md b/modules/iam-role-v2/README.md new file mode 100644 index 0000000..c2ac1a5 --- /dev/null +++ b/modules/iam-role-v2/README.md @@ -0,0 +1,56 @@ + +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 |
map(
object(
{
description = string
policy = string
}
)
)
| `{}` | 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. \ No newline at end of file diff --git a/modules/iam-role-v2/main.tf b/modules/iam-role-v2/main.tf new file mode 100644 index 0000000..615b7f6 --- /dev/null +++ b/modules/iam-role-v2/main.tf @@ -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 +} \ No newline at end of file diff --git a/modules/iam-role-v2/outputs.tf b/modules/iam-role-v2/outputs.tf new file mode 100644 index 0000000..b6d0aa4 --- /dev/null +++ b/modules/iam-role-v2/outputs.tf @@ -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 +} \ No newline at end of file diff --git a/modules/iam-role-v2/provider.tf b/modules/iam-role-v2/provider.tf new file mode 100644 index 0000000..bc0239a --- /dev/null +++ b/modules/iam-role-v2/provider.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.3.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.4.0" + } + } +} \ No newline at end of file diff --git a/modules/iam-role-v2/variables.tf b/modules/iam-role-v2/variables.tf new file mode 100644 index 0000000..a3203fb --- /dev/null +++ b/modules/iam-role-v2/variables.tf @@ -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 +} \ No newline at end of file diff --git a/modules/security_group/README.md b/modules/security_group/README.md new file mode 100644 index 0000000..10d28eb --- /dev/null +++ b/modules/security_group/README.md @@ -0,0 +1,43 @@ + +## 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. \ No newline at end of file diff --git a/modules/security_group/example/main.tf b/modules/security_group/example/main.tf new file mode 100644 index 0000000..7751628 --- /dev/null +++ b/modules/security_group/example/main.tf @@ -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" + } + } +} \ No newline at end of file diff --git a/modules/security_group/main.tf b/modules/security_group/main.tf new file mode 100644 index 0000000..6fb358e --- /dev/null +++ b/modules/security_group/main.tf @@ -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] +} diff --git a/modules/security_group/outputs.tf b/modules/security_group/outputs.tf new file mode 100644 index 0000000..5a963bf --- /dev/null +++ b/modules/security_group/outputs.tf @@ -0,0 +1,3 @@ +output id { + value = aws_security_group.sg.id +} \ No newline at end of file diff --git a/modules/security_group/variables.tf b/modules/security_group/variables.tf new file mode 100644 index 0000000..86e2584 --- /dev/null +++ b/modules/security_group/variables.tf @@ -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) +} \ No newline at end of file diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..3753ad3 --- /dev/null +++ b/outputs.tf @@ -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) +} \ No newline at end of file diff --git a/provider.tf b/provider.tf new file mode 100644 index 0000000..07a171c --- /dev/null +++ b/provider.tf @@ -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() +} diff --git a/terraform.tfvars b/terraform.tfvars new file mode 100644 index 0000000..20c9448 --- /dev/null +++ b/terraform.tfvars @@ -0,0 +1,4 @@ +aws-region = "ap-northeast-1" +environment = "Lab" +project = "Demo" +application = "ZonalShift" diff --git a/userdata.sh b/userdata.sh new file mode 100644 index 0000000..ae453a6 --- /dev/null +++ b/userdata.sh @@ -0,0 +1,4 @@ +#!/bin/bash +dnf -q -y install nginx +systemctl enable --now nginx +ec2-metadata -z > /usr/share/nginx/html/index.html \ No newline at end of file diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..6bf984d --- /dev/null +++ b/variables.tf @@ -0,0 +1,4 @@ +variable "aws-region" {} +variable "environment" {} +variable "project" {} +variable "application" {}