diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 093121e0..8a010fdd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/antonbabenko/pre-commit-terraform - rev: v1.62.3 + rev: v1.64.0 hooks: - id: terraform_fmt - id: terraform_validate diff --git a/README.md b/README.md index dfc711e5..7ac6bc16 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## Features 1. **Cross-account access.** Define IAM roles using `iam_assumable_role` or `iam_assumable_roles` submodules in "resource AWS accounts (prod, staging, dev)" and IAM groups and users using `iam-group-with-assumable-roles-policy` submodule in "IAM AWS Account" to setup access controls between accounts. See [iam-group-with-assumable-roles-policy example](https://github.com/terraform-aws-modules/terraform-aws-iam/tree/master/examples/iam-group-with-assumable-roles-policy) for more details. -1. **Individual IAM resources (users, roles, policies).** See usage snippets and [examples](https://github.com/terraform-aws-modules/terraform-aws-iam#examples) listed below. +2. **Individual IAM resources (users, roles, policies).** See usage snippets and [examples](https://github.com/terraform-aws-modules/terraform-aws-iam#examples) listed below. ## Usage @@ -134,63 +134,31 @@ module "iam_assumable_roles_with_saml" { } ``` -`iam-user`: +`iam-eks-role`: ```hcl -module "iam_user" { - source = "terraform-aws-modules/iam/aws//modules/iam-user" - version = "~> 4" - - name = "vasya.pupkin" - force_destroy = true - - pgp_key = "keybase:test" - - password_reset_required = false -} -``` +module "iam_eks_role" { + source = "terraform-aws-modules/iam/aws//modules/iam-eks-role" + version = "~> 4" -`iam-policy`: + role_name = "my-app" -```hcl -module "iam_policy" { - source = "terraform-aws-modules/iam/aws//modules/iam-policy" - version = "~> 4" + cluster_service_accounts = { + "cluster1" = ["default:my-app"] + "cluster2" = [ + "default:my-app", + "canary:my-app", + ] + } - name = "example" - path = "/" - description = "My example policy" + tags = { + Name = "eks-role" + } - policy = < +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 0.13.1 | +| [aws](#requirement\_aws) | >= 3.0 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [cluster\_autoscaler\_irsa\_role](#module\_cluster\_autoscaler\_irsa\_role) | ../../modules/iam-role-for-service-accounts-eks | n/a | +| [disabled](#module\_disabled) | ../../modules/iam-role-for-service-accounts-eks | n/a | +| [ebs\_csi\_irsa\_role](#module\_ebs\_csi\_irsa\_role) | ../../modules/iam-role-for-service-accounts-eks | n/a | +| [eks](#module\_eks) | terraform-aws-modules/eks/aws | ~> 18.6 | +| [external\_dns\_irsa\_role](#module\_external\_dns\_irsa\_role) | ../../modules/iam-role-for-service-accounts-eks | n/a | +| [irsa\_role](#module\_irsa\_role) | ../../modules/iam-role-for-service-accounts-eks | n/a | +| [karpenter\_controller\_irsa\_role](#module\_karpenter\_controller\_irsa\_role) | ../../modules/iam-role-for-service-accounts-eks | n/a | +| [node\_termination\_handler\_irsa\_role](#module\_node\_termination\_handler\_irsa\_role) | ../../modules/iam-role-for-service-accounts-eks | n/a | +| [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | ~> 3.0 | +| [vpc\_cni\_ipv4\_irsa\_role](#module\_vpc\_cni\_ipv4\_irsa\_role) | ../../modules/iam-role-for-service-accounts-eks | n/a | +| [vpc\_cni\_ipv6\_irsa\_role](#module\_vpc\_cni\_ipv6\_irsa\_role) | ../../modules/iam-role-for-service-accounts-eks | n/a | + +## Resources + +No resources. + +## Inputs + +No inputs. + +## Outputs + +| Name | Description | +|------|-------------| +| [iam\_role\_arn](#output\_iam\_role\_arn) | ARN of IAM role | +| [iam\_role\_name](#output\_iam\_role\_name) | Name of IAM role | +| [iam\_role\_path](#output\_iam\_role\_path) | Path of IAM role | +| [iam\_role\_unique\_id](#output\_iam\_role\_unique\_id) | Unique ID of IAM role | + diff --git a/examples/iam-role-for-service-accounts-eks/main.tf b/examples/iam-role-for-service-accounts-eks/main.tf new file mode 100644 index 00000000..3cb20de9 --- /dev/null +++ b/examples/iam-role-for-service-accounts-eks/main.tf @@ -0,0 +1,216 @@ +provider "aws" { + region = local.region +} + +locals { + name = "ex-iam-eks-role" + cluster_version = "1.21" + region = "eu-west-1" + + tags = { + Example = local.name + GithubRepo = "terraform-aws-iam" + GithubOrg = "terraform-aws-modules" + } +} + +################################################################################ +# IRSA Roles +################################################################################ + +module "disabled" { + source = "../../modules/iam-role-for-service-accounts-eks" + + create_role = false +} + +module "irsa_role" { + source = "../../modules/iam-role-for-service-accounts-eks" + + role_name = local.name + + oidc_providers = { + one = { + provider_arn = module.eks.oidc_provider_arn + namespace_service_accounts = ["default:my-app", "canary:my-app"] + } + two = { + provider_arn = module.eks.oidc_provider_arn + namespace_service_accounts = ["default:blue", "canary:blue"] + } + } + + role_policy_arns = [ + "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" + ] + + tags = local.tags +} + +module "cluster_autoscaler_irsa_role" { + source = "../../modules/iam-role-for-service-accounts-eks" + + role_name = "cluster-autoscaler" + attach_cluster_autoscaler_policy = true + cluster_autoscaler_cluster_ids = [module.eks.cluster_id] + + oidc_providers = { + ex = { + provider_arn = module.eks.oidc_provider_arn + namespace_service_accounts = ["default:my-app", "canary:my-app"] + } + } + + tags = local.tags +} + +module "external_dns_irsa_role" { + source = "../../modules/iam-role-for-service-accounts-eks" + + role_name = "external-dns" + attach_external_dns_policy = true + external_dns_hosted_zone_arns = ["arn:aws:route53:::hostedzone/IClearlyMadeThisUp"] + + oidc_providers = { + ex = { + provider_arn = module.eks.oidc_provider_arn + namespace_service_accounts = ["default:my-app", "canary:my-app"] + } + } + + tags = local.tags +} + +module "ebs_csi_irsa_role" { + source = "../../modules/iam-role-for-service-accounts-eks" + + role_name = "ebs_csi" + attach_ebs_csi_policy = true + + oidc_providers = { + ex = { + provider_arn = module.eks.oidc_provider_arn + namespace_service_accounts = ["default:my-app", "canary:my-app"] + } + } + + tags = local.tags +} + +module "vpc_cni_ipv4_irsa_role" { + source = "../../modules/iam-role-for-service-accounts-eks" + + role_name = "vpc_cni_ipv4" + attach_vpc_cni_policy = true + vpc_cni_enable_ipv4 = true + + oidc_providers = { + ex = { + provider_arn = module.eks.oidc_provider_arn + namespace_service_accounts = ["default:my-app", "canary:my-app"] + } + } + + tags = local.tags +} + +module "vpc_cni_ipv6_irsa_role" { + source = "../../modules/iam-role-for-service-accounts-eks" + + role_name = "vpc_cni_ipv6" + attach_vpc_cni_policy = true + vpc_cni_enable_ipv6 = true + + oidc_providers = { + ex = { + provider_arn = module.eks.oidc_provider_arn + namespace_service_accounts = ["default:my-app", "canary:my-app"] + } + } + + tags = local.tags +} + +module "node_termination_handler_irsa_role" { + source = "../../modules/iam-role-for-service-accounts-eks" + + role_name = "node_termination_handler" + attach_node_termination_handler_policy = true + + oidc_providers = { + ex = { + provider_arn = module.eks.oidc_provider_arn + namespace_service_accounts = ["default:my-app", "canary:my-app"] + } + } + + tags = local.tags +} + +module "karpenter_controller_irsa_role" { + source = "../../modules/iam-role-for-service-accounts-eks" + + role_name = "karpenter_controller" + attach_karpenter_controller_policy = true + + karpenter_controller_cluster_ids = [module.eks.cluster_id] + karpenter_controller_node_iam_role_arns = [module.eks.eks_managed_node_groups["default"].iam_role_arn] + + oidc_providers = { + ex = { + provider_arn = module.eks.oidc_provider_arn + namespace_service_accounts = ["default:my-app", "canary:my-app"] + } + } + + tags = local.tags +} + +################################################################################ +# Supporting Resources +################################################################################ + +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "~> 3.0" + + name = local.name + cidr = "10.0.0.0/16" + + azs = ["${local.region}a", "${local.region}b", "${local.region}c"] + private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] + public_subnets = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"] + + enable_nat_gateway = true + single_nat_gateway = true + enable_dns_hostnames = true + + public_subnet_tags = { + "kubernetes.io/cluster/${local.name}" = "shared" + "kubernetes.io/role/elb" = 1 + } + + private_subnet_tags = { + "kubernetes.io/cluster/${local.name}" = "shared" + "kubernetes.io/role/internal-elb" = 1 + } + + tags = local.tags +} + +module "eks" { + source = "terraform-aws-modules/eks/aws" + version = "~> 18.6" + + cluster_name = local.name + cluster_version = local.cluster_version + + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.private_subnets + + eks_managed_node_groups = { + default = {} + } + + tags = local.tags +} diff --git a/examples/iam-role-for-service-accounts-eks/outputs.tf b/examples/iam-role-for-service-accounts-eks/outputs.tf new file mode 100644 index 00000000..f0754538 --- /dev/null +++ b/examples/iam-role-for-service-accounts-eks/outputs.tf @@ -0,0 +1,19 @@ +output "iam_role_arn" { + description = "ARN of IAM role" + value = module.irsa_role.iam_role_arn +} + +output "iam_role_name" { + description = "Name of IAM role" + value = module.irsa_role.iam_role_name +} + +output "iam_role_path" { + description = "Path of IAM role" + value = module.irsa_role.iam_role_path +} + +output "iam_role_unique_id" { + description = "Unique ID of IAM role" + value = module.irsa_role.iam_role_unique_id +} diff --git a/examples/iam-role-for-service-accounts-eks/variables.tf b/examples/iam-role-for-service-accounts-eks/variables.tf new file mode 100644 index 00000000..e69de29b diff --git a/examples/iam-role-for-service-accounts-eks/versions.tf b/examples/iam-role-for-service-accounts-eks/versions.tf new file mode 100644 index 00000000..fe1f6e88 --- /dev/null +++ b/examples/iam-role-for-service-accounts-eks/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 0.13.1" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 3.0" + } + } +} diff --git a/examples/iam-user/versions.tf b/examples/iam-user/versions.tf index 132c7df3..6c8fa913 100644 --- a/examples/iam-user/versions.tf +++ b/examples/iam-user/versions.tf @@ -2,6 +2,9 @@ terraform { required_version = ">= 0.12.6" required_providers { - aws = ">= 2.50" + aws = { + source = "hashicorp/aws" + version = ">= 2.50" + } } } diff --git a/modules/iam-account/outputs.tf b/modules/iam-account/outputs.tf index 3145d45d..c286d1e5 100644 --- a/modules/iam-account/outputs.tf +++ b/modules/iam-account/outputs.tf @@ -1,19 +1,19 @@ output "caller_identity_account_id" { description = "The AWS Account ID number of the account that owns or contains the calling entity" - value = element(concat(data.aws_caller_identity.this.*.account_id, [""]), 0) + value = try(data.aws_caller_identity.this[0].account_id, "") } output "caller_identity_arn" { description = "The AWS ARN associated with the calling entity" - value = element(concat(data.aws_caller_identity.this.*.arn, [""]), 0) + value = try(data.aws_caller_identity.this[0].arn, "") } output "caller_identity_user_id" { description = "The unique identifier of the calling entity" - value = element(concat(data.aws_caller_identity.this.*.user_id, [""]), 0) + value = try(data.aws_caller_identity.this[0].user_id, "") } output "iam_account_password_policy_expire_passwords" { description = "Indicates whether passwords in the account expire. Returns true if max_password_age contains a value greater than 0. Returns false if it is 0 or not present." - value = element(concat(aws_iam_account_password_policy.this.*.expire_passwords, [""]), 0) + value = try(aws_iam_account_password_policy.this[0].expire_passwords, "") } diff --git a/modules/iam-account/versions.tf b/modules/iam-account/versions.tf index fff6b757..4e3fe457 100644 --- a/modules/iam-account/versions.tf +++ b/modules/iam-account/versions.tf @@ -2,6 +2,9 @@ terraform { required_version = ">= 0.12.6" required_providers { - aws = ">= 2.23" + aws = { + source = "hashicorp/aws" + version = ">= 2.23" + } } } diff --git a/modules/iam-assumable-role-with-oidc/outputs.tf b/modules/iam-assumable-role-with-oidc/outputs.tf index cdd317a3..a6805310 100644 --- a/modules/iam-assumable-role-with-oidc/outputs.tf +++ b/modules/iam-assumable-role-with-oidc/outputs.tf @@ -1,19 +1,19 @@ output "iam_role_arn" { description = "ARN of IAM role" - value = element(concat(aws_iam_role.this.*.arn, [""]), 0) + value = try(aws_iam_role.this[0].arn, "") } output "iam_role_name" { description = "Name of IAM role" - value = element(concat(aws_iam_role.this.*.name, [""]), 0) + value = try(aws_iam_role.this[0].name, "") } output "iam_role_path" { description = "Path of IAM role" - value = element(concat(aws_iam_role.this.*.path, [""]), 0) + value = try(aws_iam_role.this[0].path, "") } output "iam_role_unique_id" { description = "Unique ID of IAM role" - value = element(concat(aws_iam_role.this.*.unique_id, [""]), 0) + value = try(aws_iam_role.this[0].unique_id, "") } diff --git a/modules/iam-assumable-role-with-oidc/versions.tf b/modules/iam-assumable-role-with-oidc/versions.tf index fff6b757..4e3fe457 100644 --- a/modules/iam-assumable-role-with-oidc/versions.tf +++ b/modules/iam-assumable-role-with-oidc/versions.tf @@ -2,6 +2,9 @@ terraform { required_version = ">= 0.12.6" required_providers { - aws = ">= 2.23" + aws = { + source = "hashicorp/aws" + version = ">= 2.23" + } } } diff --git a/modules/iam-assumable-role-with-saml/outputs.tf b/modules/iam-assumable-role-with-saml/outputs.tf index cdd317a3..a6805310 100644 --- a/modules/iam-assumable-role-with-saml/outputs.tf +++ b/modules/iam-assumable-role-with-saml/outputs.tf @@ -1,19 +1,19 @@ output "iam_role_arn" { description = "ARN of IAM role" - value = element(concat(aws_iam_role.this.*.arn, [""]), 0) + value = try(aws_iam_role.this[0].arn, "") } output "iam_role_name" { description = "Name of IAM role" - value = element(concat(aws_iam_role.this.*.name, [""]), 0) + value = try(aws_iam_role.this[0].name, "") } output "iam_role_path" { description = "Path of IAM role" - value = element(concat(aws_iam_role.this.*.path, [""]), 0) + value = try(aws_iam_role.this[0].path, "") } output "iam_role_unique_id" { description = "Unique ID of IAM role" - value = element(concat(aws_iam_role.this.*.unique_id, [""]), 0) + value = try(aws_iam_role.this[0].unique_id, "") } diff --git a/modules/iam-assumable-role-with-saml/versions.tf b/modules/iam-assumable-role-with-saml/versions.tf index fff6b757..4e3fe457 100644 --- a/modules/iam-assumable-role-with-saml/versions.tf +++ b/modules/iam-assumable-role-with-saml/versions.tf @@ -2,6 +2,9 @@ terraform { required_version = ">= 0.12.6" required_providers { - aws = ">= 2.23" + aws = { + source = "hashicorp/aws" + version = ">= 2.23" + } } } diff --git a/modules/iam-assumable-role/outputs.tf b/modules/iam-assumable-role/outputs.tf index a8d1c563..117ff077 100644 --- a/modules/iam-assumable-role/outputs.tf +++ b/modules/iam-assumable-role/outputs.tf @@ -1,21 +1,21 @@ output "iam_role_arn" { description = "ARN of IAM role" - value = element(concat(aws_iam_role.this.*.arn, [""]), 0) + value = try(aws_iam_role.this[0].arn, "") } output "iam_role_name" { description = "Name of IAM role" - value = element(concat(aws_iam_role.this.*.name, [""]), 0) + value = try(aws_iam_role.this[0].name, "") } output "iam_role_path" { description = "Path of IAM role" - value = element(concat(aws_iam_role.this.*.path, [""]), 0) + value = try(aws_iam_role.this[0].path, "") } output "iam_role_unique_id" { description = "Unique ID of IAM role" - value = element(concat(aws_iam_role.this.*.unique_id, [""]), 0) + value = try(aws_iam_role.this[0].unique_id, "") } output "role_requires_mfa" { @@ -25,22 +25,22 @@ output "role_requires_mfa" { output "iam_instance_profile_arn" { description = "ARN of IAM instance profile" - value = element(concat(aws_iam_instance_profile.this.*.arn, [""]), 0) + value = try(aws_iam_instance_profile.this[0].arn, "") } output "iam_instance_profile_name" { description = "Name of IAM instance profile" - value = element(concat(aws_iam_instance_profile.this.*.name, [""]), 0) + value = try(aws_iam_instance_profile.this[0].name, "") } output "iam_instance_profile_id" { description = "IAM Instance profile's ID." - value = element(concat(aws_iam_instance_profile.this.*.id, [""]), 0) + value = try(aws_iam_instance_profile.this[0].id, "") } output "iam_instance_profile_path" { description = "Path of IAM instance profile" - value = element(concat(aws_iam_instance_profile.this.*.path, [""]), 0) + value = try(aws_iam_instance_profile.this[0].path, "") } output "role_sts_externalid" { diff --git a/modules/iam-assumable-role/versions.tf b/modules/iam-assumable-role/versions.tf index cd54581c..1fe6583f 100644 --- a/modules/iam-assumable-role/versions.tf +++ b/modules/iam-assumable-role/versions.tf @@ -2,6 +2,9 @@ terraform { required_version = ">= 0.12.6" required_providers { - aws = ">= 3.34" + aws = { + source = "hashicorp/aws" + version = ">= 3.34" + } } } diff --git a/modules/iam-assumable-roles-with-saml/outputs.tf b/modules/iam-assumable-roles-with-saml/outputs.tf index 2ffbd2be..3b1c4d1a 100644 --- a/modules/iam-assumable-roles-with-saml/outputs.tf +++ b/modules/iam-assumable-roles-with-saml/outputs.tf @@ -1,61 +1,61 @@ #Admin output "admin_iam_role_arn" { description = "ARN of admin IAM role" - value = element(concat(aws_iam_role.admin.*.arn, [""]), 0) + value = try(aws_iam_role.admin[0].arn, "") } output "admin_iam_role_name" { description = "Name of admin IAM role" - value = element(concat(aws_iam_role.admin.*.name, [""]), 0) + value = try(aws_iam_role.admin[0].name, "") } output "admin_iam_role_path" { description = "Path of admin IAM role" - value = element(concat(aws_iam_role.admin.*.path, [""]), 0) + value = try(aws_iam_role.admin[0].path, "") } output "admin_iam_role_unique_id" { description = "Unique ID of IAM role" - value = element(concat(aws_iam_role.admin.*.unique_id, [""]), 0) + value = try(aws_iam_role.admin[0].unique_id, "") } output "poweruser_iam_role_arn" { description = "ARN of poweruser IAM role" - value = element(concat(aws_iam_role.poweruser.*.arn, [""]), 0) + value = try(aws_iam_role.poweruser[0].arn, "") } output "poweruser_iam_role_name" { description = "Name of poweruser IAM role" - value = element(concat(aws_iam_role.poweruser.*.name, [""]), 0) + value = try(aws_iam_role.poweruser[0].name, "") } output "poweruser_iam_role_path" { description = "Path of poweruser IAM role" - value = element(concat(aws_iam_role.poweruser.*.path, [""]), 0) + value = try(aws_iam_role.poweruser[0].path, "") } output "poweruser_iam_role_unique_id" { description = "Unique ID of IAM role" - value = element(concat(aws_iam_role.poweruser.*.unique_id, [""]), 0) + value = try(aws_iam_role.poweruser[0].unique_id, "") } # Readonly output "readonly_iam_role_arn" { description = "ARN of readonly IAM role" - value = element(concat(aws_iam_role.readonly.*.arn, [""]), 0) + value = try(aws_iam_role.readonly[0].arn, "") } output "readonly_iam_role_name" { description = "Name of readonly IAM role" - value = element(concat(aws_iam_role.readonly.*.name, [""]), 0) + value = try(aws_iam_role.readonly[0].name, "") } output "readonly_iam_role_path" { description = "Path of readonly IAM role" - value = element(concat(aws_iam_role.readonly.*.path, [""]), 0) + value = try(aws_iam_role.readonly[0].path, "") } output "readonly_iam_role_unique_id" { description = "Unique ID of IAM role" - value = element(concat(aws_iam_role.readonly.*.unique_id, [""]), 0) + value = try(aws_iam_role.readonly[0].unique_id, "") } diff --git a/modules/iam-assumable-roles-with-saml/versions.tf b/modules/iam-assumable-roles-with-saml/versions.tf index fff6b757..4e3fe457 100644 --- a/modules/iam-assumable-roles-with-saml/versions.tf +++ b/modules/iam-assumable-roles-with-saml/versions.tf @@ -2,6 +2,9 @@ terraform { required_version = ">= 0.12.6" required_providers { - aws = ">= 2.23" + aws = { + source = "hashicorp/aws" + version = ">= 2.23" + } } } diff --git a/modules/iam-assumable-roles/outputs.tf b/modules/iam-assumable-roles/outputs.tf index dff95d74..5918ba46 100644 --- a/modules/iam-assumable-roles/outputs.tf +++ b/modules/iam-assumable-roles/outputs.tf @@ -1,22 +1,22 @@ #Admin output "admin_iam_role_arn" { description = "ARN of admin IAM role" - value = element(concat(aws_iam_role.admin.*.arn, [""]), 0) + value = try(aws_iam_role.admin[0].arn, "") } output "admin_iam_role_name" { description = "Name of admin IAM role" - value = element(concat(aws_iam_role.admin.*.name, [""]), 0) + value = try(aws_iam_role.admin[0].name, "") } output "admin_iam_role_path" { description = "Path of admin IAM role" - value = element(concat(aws_iam_role.admin.*.path, [""]), 0) + value = try(aws_iam_role.admin[0].path, "") } output "admin_iam_role_unique_id" { description = "Unique ID of IAM role" - value = element(concat(aws_iam_role.admin.*.unique_id, [""]), 0) + value = try(aws_iam_role.admin[0].unique_id, "") } output "admin_iam_role_requires_mfa" { @@ -27,22 +27,22 @@ output "admin_iam_role_requires_mfa" { # Poweruser output "poweruser_iam_role_arn" { description = "ARN of poweruser IAM role" - value = element(concat(aws_iam_role.poweruser.*.arn, [""]), 0) + value = try(aws_iam_role.poweruser[0].arn, "") } output "poweruser_iam_role_name" { description = "Name of poweruser IAM role" - value = element(concat(aws_iam_role.poweruser.*.name, [""]), 0) + value = try(aws_iam_role.poweruser[0].name, "") } output "poweruser_iam_role_path" { description = "Path of poweruser IAM role" - value = element(concat(aws_iam_role.poweruser.*.path, [""]), 0) + value = try(aws_iam_role.poweruser[0].path, "") } output "poweruser_iam_role_unique_id" { description = "Unique ID of IAM role" - value = element(concat(aws_iam_role.poweruser.*.unique_id, [""]), 0) + value = try(aws_iam_role.poweruser[0].unique_id, "") } output "poweruser_iam_role_requires_mfa" { @@ -53,22 +53,22 @@ output "poweruser_iam_role_requires_mfa" { # Readonly output "readonly_iam_role_arn" { description = "ARN of readonly IAM role" - value = element(concat(aws_iam_role.readonly.*.arn, [""]), 0) + value = try(aws_iam_role.readonly[0].arn, "") } output "readonly_iam_role_name" { description = "Name of readonly IAM role" - value = element(concat(aws_iam_role.readonly.*.name, [""]), 0) + value = try(aws_iam_role.readonly[0].name, "") } output "readonly_iam_role_path" { description = "Path of readonly IAM role" - value = element(concat(aws_iam_role.readonly.*.path, [""]), 0) + value = try(aws_iam_role.readonly[0].path, "") } output "readonly_iam_role_unique_id" { description = "Unique ID of IAM role" - value = element(concat(aws_iam_role.readonly.*.unique_id, [""]), 0) + value = try(aws_iam_role.readonly[0].unique_id, "") } output "readonly_iam_role_requires_mfa" { diff --git a/modules/iam-assumable-roles/versions.tf b/modules/iam-assumable-roles/versions.tf index fff6b757..4e3fe457 100644 --- a/modules/iam-assumable-roles/versions.tf +++ b/modules/iam-assumable-roles/versions.tf @@ -2,6 +2,9 @@ terraform { required_version = ">= 0.12.6" required_providers { - aws = ">= 2.23" + aws = { + source = "hashicorp/aws" + version = ">= 2.23" + } } } diff --git a/modules/iam-group-with-assumable-roles-policy/outputs.tf b/modules/iam-group-with-assumable-roles-policy/outputs.tf index 89913015..a172a664 100644 --- a/modules/iam-group-with-assumable-roles-policy/outputs.tf +++ b/modules/iam-group-with-assumable-roles-policy/outputs.tf @@ -1,6 +1,6 @@ output "group_users" { description = "List of IAM users in IAM group" - value = flatten(aws_iam_group_membership.this.*.users) + value = flatten(aws_iam_group_membership.this[*].users) } output "assumable_roles" { diff --git a/modules/iam-group-with-assumable-roles-policy/versions.tf b/modules/iam-group-with-assumable-roles-policy/versions.tf index fff6b757..4e3fe457 100644 --- a/modules/iam-group-with-assumable-roles-policy/versions.tf +++ b/modules/iam-group-with-assumable-roles-policy/versions.tf @@ -2,6 +2,9 @@ terraform { required_version = ">= 0.12.6" required_providers { - aws = ">= 2.23" + aws = { + source = "hashicorp/aws" + version = ">= 2.23" + } } } diff --git a/modules/iam-group-with-policies/outputs.tf b/modules/iam-group-with-policies/outputs.tf index 64d93040..ee0835c0 100644 --- a/modules/iam-group-with-policies/outputs.tf +++ b/modules/iam-group-with-policies/outputs.tf @@ -5,15 +5,15 @@ output "aws_account_id" { output "group_arn" { description = "IAM group arn" - value = element(concat(aws_iam_group.this.*.arn, [""]), 0) + value = try(aws_iam_group.this[0].arn, "") } output "group_users" { description = "List of IAM users in IAM group" - value = flatten(aws_iam_group_membership.this.*.users) + value = flatten(aws_iam_group_membership.this[*].users) } output "group_name" { description = "IAM group name" - value = element(concat(aws_iam_group.this.*.name, [var.name]), 0) + value = try(aws_iam_group.this[0].name, var.name) } diff --git a/modules/iam-group-with-policies/versions.tf b/modules/iam-group-with-policies/versions.tf index fff6b757..4e3fe457 100644 --- a/modules/iam-group-with-policies/versions.tf +++ b/modules/iam-group-with-policies/versions.tf @@ -2,6 +2,9 @@ terraform { required_version = ">= 0.12.6" required_providers { - aws = ">= 2.23" + aws = { + source = "hashicorp/aws" + version = ">= 2.23" + } } } diff --git a/modules/iam-policy/outputs.tf b/modules/iam-policy/outputs.tf index d125c849..d8900802 100644 --- a/modules/iam-policy/outputs.tf +++ b/modules/iam-policy/outputs.tf @@ -1,29 +1,29 @@ output "id" { description = "The policy's ID" - value = element(concat(aws_iam_policy.policy.*.id, [""]), 0) + value = try(aws_iam_policy.policy[0].id, "") } output "arn" { description = "The ARN assigned by AWS to this policy" - value = element(concat(aws_iam_policy.policy.*.arn, [""]), 0) + value = try(aws_iam_policy.policy[0].arn, "") } output "description" { description = "The description of the policy" - value = element(concat(aws_iam_policy.policy.*.description, [""]), 0) + value = try(aws_iam_policy.policy[0].description, "") } output "name" { description = "The name of the policy" - value = element(concat(aws_iam_policy.policy.*.name, [""]), 0) + value = try(aws_iam_policy.policy[0].name, "") } output "path" { description = "The path of the policy in IAM" - value = element(concat(aws_iam_policy.policy.*.path, [""]), 0) + value = try(aws_iam_policy.policy[0].path, "") } output "policy" { description = "The policy document" - value = element(concat(aws_iam_policy.policy.*.policy, [""]), 0) + value = try(aws_iam_policy.policy[0].policy, "") } diff --git a/modules/iam-policy/versions.tf b/modules/iam-policy/versions.tf index c7bef0b2..38ab12c5 100644 --- a/modules/iam-policy/versions.tf +++ b/modules/iam-policy/versions.tf @@ -2,6 +2,9 @@ terraform { required_version = ">= 0.12.6" required_providers { - aws = ">= 3.35" + aws = { + source = "hashicorp/aws" + version = ">= 3.35" + } } } diff --git a/modules/iam-read-only-policy/outputs.tf b/modules/iam-read-only-policy/outputs.tf index 17989a74..c18731d5 100644 --- a/modules/iam-read-only-policy/outputs.tf +++ b/modules/iam-read-only-policy/outputs.tf @@ -5,30 +5,30 @@ output "policy_json" { output "id" { description = "The policy's ID" - value = element(concat(aws_iam_policy.policy.*.id, [""]), 0) + value = try(aws_iam_policy.policy[0].id, "") } output "arn" { description = "The ARN assigned by AWS to this policy" - value = element(concat(aws_iam_policy.policy.*.arn, [""]), 0) + value = try(aws_iam_policy.policy[0].arn, "") } output "description" { description = "The description of the policy" - value = element(concat(aws_iam_policy.policy.*.description, [""]), 0) + value = try(aws_iam_policy.policy[0].description, "") } output "name" { description = "The name of the policy" - value = element(concat(aws_iam_policy.policy.*.name, [""]), 0) + value = try(aws_iam_policy.policy[0].name, "") } output "path" { description = "The path of the policy in IAM" - value = element(concat(aws_iam_policy.policy.*.path, [""]), 0) + value = try(aws_iam_policy.policy[0].path, "") } output "policy" { description = "The policy document" - value = element(concat(aws_iam_policy.policy.*.policy, [""]), 0) + value = try(aws_iam_policy.policy[0].policy, "") } diff --git a/modules/iam-read-only-policy/versions.tf b/modules/iam-read-only-policy/versions.tf index fff6b757..4e3fe457 100644 --- a/modules/iam-read-only-policy/versions.tf +++ b/modules/iam-read-only-policy/versions.tf @@ -2,6 +2,9 @@ terraform { required_version = ">= 0.12.6" required_providers { - aws = ">= 2.23" + aws = { + source = "hashicorp/aws" + version = ">= 2.23" + } } } diff --git a/modules/iam-role-for-service-accounts-eks/README.md b/modules/iam-role-for-service-accounts-eks/README.md new file mode 100644 index 00000000..4f8816eb --- /dev/null +++ b/modules/iam-role-for-service-accounts-eks/README.md @@ -0,0 +1,166 @@ +# IAM Role for Service Accounts in EKS + +Creates an IAM role which can be assumed by AWS EKS `ServiceAccount`s with optional policies for commonly used controllers/custom resources within EKS. + +This module is intended to be used with AWS EKS. For details of how a `ServiceAccount` in EKS can assume an IAM role, see the [EKS documentation](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html). + +This module supports multiple `ServiceAccount`s across multiple clusters and/or namespaces. This allows for a single IAM role to be used when an application may span multiple clusters (e.g. for DR) or multiple namespaces (e.g. for canary deployments). For example, to create an IAM role named `my-app` that can be assumed from the `ServiceAccount` named `my-app-staging` in the namespace `default` and `canary` in a cluster in `us-east-1`; and also the `ServiceAccount` name `my-app-staging` in the namespace `default` in a cluster in `ap-southeast-1`, the configuration would be: + +```hcl +module "iam_eks_role" { + source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" + role_name = "my-app" + + oidc_providers = { + one = { + provider_arn = "arn:aws:iam::012345678901:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/5C54DDF35ER19312844C7333374CC09D" + service_accounts = ["default:my-app-staging", "canary:my-app-staging"] + } + two = { + provider_arn = "arn:aws:iam::012345678901:oidc-provider/oidc.eks.ap-southeast-1.amazonaws.com/id/5C54DDF35ER54476848E7333374FF09G" + service_accounts = ["default:my-app-staging"] + } + } +} +``` + +This module has been designed in conjunction with the [`terraform-aws-eks`](https://github.com/terraform-aws-modules/terraform-aws-eks) module to easily integrate with it: + +```hcl +module "vpc_cni_irsa_role" { + source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" + + role_name = "vpc-cni" + + attach_vpc_cni_policy = true + vpc_cni_enable_ipv4 = true + + oidc_providers = { + main = { + provider_arn = module.eks.oidc_provider_arn + service_accounts = ["default:my-app", "canary:my-app"] + } + } +} + +module "karpenter_irsa_role" { + source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" + + role_name = "karpenter_controller" + attach_karpenter_controller_policy = true + + karpenter_controller_cluster_ids = [module.eks.cluster_id] + karpenter_controller_node_iam_role_arns = [module.eks.eks_managed_node_groups["default"].iam_role_arn] + + attach_vpc_cni_policy = true + vpc_cni_enable_ipv4 = true + + oidc_providers = { + main = { + provider_arn = module.eks.oidc_provider_arn + service_accounts = ["default:my-app", "canary:my-app"] + } + } +} + +module "eks" { + source = "terraform-aws-modules/eks/aws" + version = "~> 18.6" + + cluster_name = "my-cluster" + cluster_version = "1.21" + + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.private_subnets + + eks_managed_node_groups = { + default = {} + } +} +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 0.13.1 | +| [aws](#requirement\_aws) | >= 3.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 3.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_iam_policy.cluster_autoscaler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.ebs_csi](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.external_dns](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.karpenter_controller](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.node_termination_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.vpc_cni](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy_attachment.cluster_autoscaler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.ebs_csi](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.external_dns](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.karpenter_controller](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.node_termination_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.vpc_cni](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_policy_document.cluster_autoscaler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.ebs_csi](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.external_dns](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.karpenter_controller](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.node_termination_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.vpc_cni](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [attach\_cluster\_autoscaler\_policy](#input\_attach\_cluster\_autoscaler\_policy) | Determines whether to attach the Cluster Autoscaler IAM policy to the role | `bool` | `false` | no | +| [attach\_ebs\_csi\_policy](#input\_attach\_ebs\_csi\_policy) | Determines whether to attach the EBS CSI IAM policy to the role | `bool` | `false` | no | +| [attach\_external\_dns\_policy](#input\_attach\_external\_dns\_policy) | Determines whether to attach the External DNS IAM policy to the role | `bool` | `false` | no | +| [attach\_karpenter\_controller\_policy](#input\_attach\_karpenter\_controller\_policy) | Determines whether to attach the Karpenter Controller policy to the role | `bool` | `false` | no | +| [attach\_node\_termination\_handler\_policy](#input\_attach\_node\_termination\_handler\_policy) | Determines whether to attach the Node Termination Handler policy to the role | `bool` | `false` | no | +| [attach\_vpc\_cni\_policy](#input\_attach\_vpc\_cni\_policy) | Determines whether to attach the VPC CNI IAM policy to the role | `bool` | `false` | no | +| [cluster\_autoscaler\_cluster\_ids](#input\_cluster\_autoscaler\_cluster\_ids) | List of cluster IDs to appropriately scope permissions within the Cluster Autoscaler IAM policy | `list(string)` | `[]` | no | +| [create\_role](#input\_create\_role) | Whether to create a role | `bool` | `true` | no | +| [ebs\_csi\_kms\_cmk\_ids](#input\_ebs\_csi\_kms\_cmk\_ids) | KMS CMK IDs to allow EBS CSI to manage encrypted volumes | `list(string)` | `[]` | no | +| [external\_dns\_hosted\_zone\_arns](#input\_external\_dns\_hosted\_zone\_arns) | Route53 hosted zone ARNs to allow external DNS to manage records | `list(string)` |
[
"arn:aws:route53:::hostedzone/*"
]
| no | +| [force\_detach\_policies](#input\_force\_detach\_policies) | Whether policies should be detached from this role when destroying | `bool` | `true` | no | +| [karpenter\_controller\_cluster\_ids](#input\_karpenter\_controller\_cluster\_ids) | List of cluster IDs to appropriately scope EC2 permissions within the Karpenter Controller policy | `list(string)` | `[]` | no | +| [karpenter\_controller\_node\_iam\_role\_arns](#input\_karpenter\_controller\_node\_iam\_role\_arns) | List of node IAM role ARNs Karpenter can use to launch nodes | `list(string)` |
[
"*"
]
| no | +| [karpenter\_controller\_ssm\_parameter\_arns](#input\_karpenter\_controller\_ssm\_parameter\_arns) | List of SSM Parameter ARNs that contain AMI IDs launched by Karpenter | `list(string)` |
[
"arn:aws:ssm:*:*:parameter/aws/service/*"
]
| no | +| [max\_session\_duration](#input\_max\_session\_duration) | Maximum CLI/API session duration in seconds between 3600 and 43200 | `number` | `null` | no | +| [node\_termination\_handler\_sqs\_queue\_arns](#input\_node\_termination\_handler\_sqs\_queue\_arns) | List of SQS ARNs that contain node termination events | `list(string)` |
[
"*"
]
| no | +| [oidc\_providers](#input\_oidc\_providers) | Map of OIDC providers where each provdier map should contain the `provider`, `provider_arns`, and `namespace_service_accounts` | `any` | `{}` | no | +| [role\_description](#input\_role\_description) | IAM Role description | `string` | `null` | no | +| [role\_name](#input\_role\_name) | Name of IAM role | `string` | `null` | no | +| [role\_name\_prefix](#input\_role\_name\_prefix) | IAM role name prefix | `string` | `null` | no | +| [role\_path](#input\_role\_path) | Path of IAM role | `string` | `null` | no | +| [role\_permissions\_boundary\_arn](#input\_role\_permissions\_boundary\_arn) | Permissions boundary ARN to use for IAM role | `string` | `null` | no | +| [role\_policy\_arns](#input\_role\_policy\_arns) | ARNs of any policies to attach to the IAM role | `list(string)` | `[]` | no | +| [tags](#input\_tags) | A map of tags to add the the IAM role | `map(any)` | `{}` | no | +| [vpc\_cni\_enable\_ipv4](#input\_vpc\_cni\_enable\_ipv4) | Determines whether to enable IPv4 permissions for VPC CNI policy | `bool` | `false` | no | +| [vpc\_cni\_enable\_ipv6](#input\_vpc\_cni\_enable\_ipv6) | Determines whether to enable IPv6 permissions for VPC CNI policy | `bool` | `false` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [iam\_role\_arn](#output\_iam\_role\_arn) | ARN of IAM role | +| [iam\_role\_name](#output\_iam\_role\_name) | Name of IAM role | +| [iam\_role\_path](#output\_iam\_role\_path) | Path of IAM role | +| [iam\_role\_unique\_id](#output\_iam\_role\_unique\_id) | Unique ID of IAM role | + diff --git a/modules/iam-role-for-service-accounts-eks/main.tf b/modules/iam-role-for-service-accounts-eks/main.tf new file mode 100644 index 00000000..f0c339b6 --- /dev/null +++ b/modules/iam-role-for-service-accounts-eks/main.tf @@ -0,0 +1,46 @@ +data "aws_iam_policy_document" "this" { + count = var.create_role ? 1 : 0 + + dynamic "statement" { + for_each = var.oidc_providers + + content { + effect = "Allow" + actions = ["sts:AssumeRoleWithWebIdentity"] + + principals { + type = "Federated" + identifiers = [statement.value.provider_arn] + } + + condition { + test = "StringEquals" + variable = "${replace(statement.value.provider_arn, "/^(.*provider/)/", "")}:sub" + values = [for sa in statement.value.namespace_service_accounts : "system:serviceaccount:${sa}"] + } + } + } +} + +resource "aws_iam_role" "this" { + count = var.create_role ? 1 : 0 + + name = var.role_name + name_prefix = var.role_name_prefix + path = var.role_path + description = var.role_description + + assume_role_policy = data.aws_iam_policy_document.this[0].json + max_session_duration = var.max_session_duration + permissions_boundary = var.role_permissions_boundary_arn + force_detach_policies = var.force_detach_policies + + tags = var.tags +} + +resource "aws_iam_role_policy_attachment" "this" { + for_each = var.create_role ? toset(var.role_policy_arns) : [] + + role = aws_iam_role.this[0].name + policy_arn = each.key +} diff --git a/modules/iam-role-for-service-accounts-eks/outputs.tf b/modules/iam-role-for-service-accounts-eks/outputs.tf new file mode 100644 index 00000000..a6805310 --- /dev/null +++ b/modules/iam-role-for-service-accounts-eks/outputs.tf @@ -0,0 +1,19 @@ +output "iam_role_arn" { + description = "ARN of IAM role" + value = try(aws_iam_role.this[0].arn, "") +} + +output "iam_role_name" { + description = "Name of IAM role" + value = try(aws_iam_role.this[0].name, "") +} + +output "iam_role_path" { + description = "Path of IAM role" + value = try(aws_iam_role.this[0].path, "") +} + +output "iam_role_unique_id" { + description = "Unique ID of IAM role" + value = try(aws_iam_role.this[0].unique_id, "") +} diff --git a/modules/iam-role-for-service-accounts-eks/policies.tf b/modules/iam-role-for-service-accounts-eks/policies.tf new file mode 100644 index 00000000..d8a92f62 --- /dev/null +++ b/modules/iam-role-for-service-accounts-eks/policies.tf @@ -0,0 +1,489 @@ +data "aws_partition" "current" {} + +locals { + partition = data.aws_partition.current.partition +} + +################################################################################ +# Cluster Autoscaler Policy +################################################################################ + +# https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/cloudprovider/aws/README.md +data "aws_iam_policy_document" "cluster_autoscaler" { + count = var.create_role && var.attach_cluster_autoscaler_policy ? 1 : 0 + + statement { + actions = [ + "autoscaling:DescribeAutoScalingGroups", + "autoscaling:DescribeAutoScalingInstances", + "autoscaling:DescribeLaunchConfigurations", + "autoscaling:DescribeTags", + "ec2:DescribeLaunchTemplateVersions", + ] + + resources = ["*"] + } + + dynamic "statement" { + for_each = toset(var.cluster_autoscaler_cluster_ids) + content { + actions = [ + "autoscaling:SetDesiredCapacity", + "autoscaling:TerminateInstanceInAutoScalingGroup", + "autoscaling:UpdateAutoScalingGroup", + ] + + resources = ["*"] + + condition { + test = "StringEquals" + variable = "autoscaling:ResourceTag/kubernetes.io/cluster/${statement.value}" + values = ["owned"] + } + } + } +} + +resource "aws_iam_policy" "cluster_autoscaler" { + count = var.create_role && var.attach_cluster_autoscaler_policy ? 1 : 0 + + name_prefix = "AmazonEKS_Cluster_Autoscaler_Policy-" + path = var.role_path + description = "Cluster autoscaler policy to allow examination and modification of EC2 Auto Scaling Groups" + policy = data.aws_iam_policy_document.cluster_autoscaler[0].json + + tags = var.tags +} + +resource "aws_iam_role_policy_attachment" "cluster_autoscaler" { + count = var.create_role && var.attach_cluster_autoscaler_policy ? 1 : 0 + + role = aws_iam_role.this[0].name + policy_arn = aws_iam_policy.cluster_autoscaler[0].arn +} + +################################################################################ +# External DNS Policy +################################################################################ + +# https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/aws.md#iam-policy +data "aws_iam_policy_document" "external_dns" { + count = var.create_role && var.attach_external_dns_policy ? 1 : 0 + + statement { + actions = ["route53:ChangeResourceRecordSets"] + resources = var.external_dns_hosted_zone_arns + } + + statement { + actions = [ + "route53:ListHostedZones", + "route53:ListResourceRecordSets", + ] + + resources = ["*"] + } +} + +resource "aws_iam_policy" "external_dns" { + count = var.create_role && var.attach_external_dns_policy ? 1 : 0 + + name_prefix = "AmazonEKS_External_DNS_Policy-" + path = var.role_path + description = "External DNS policy to allow management of Route53 hosted zone records" + policy = data.aws_iam_policy_document.external_dns[0].json + + tags = var.tags +} + +resource "aws_iam_role_policy_attachment" "external_dns" { + count = var.create_role && var.attach_external_dns_policy ? 1 : 0 + + role = aws_iam_role.this[0].name + policy_arn = aws_iam_policy.external_dns[0].arn +} + +################################################################################ +# EBS CSI Policy +################################################################################ + +# https://github.com/kubernetes-sigs/aws-ebs-csi-driver/blob/master/docs/example-iam-policy.json +data "aws_iam_policy_document" "ebs_csi" { + count = var.create_role && var.attach_ebs_csi_policy ? 1 : 0 + + statement { + actions = [ + "ec2:CreateSnapshot", + "ec2:AttachVolume", + "ec2:DetachVolume", + "ec2:ModifyVolume", + "ec2:DescribeAvailabilityZones", + "ec2:DescribeInstances", + "ec2:DescribeSnapshots", + "ec2:DescribeTags", + "ec2:DescribeVolumes", + "ec2:DescribeVolumesModifications", + ] + + resources = ["*"] + } + + statement { + actions = ["ec2:CreateTags"] + + resources = [ + "arn:${local.partition}:ec2:*:*:volume/*", + "arn:${local.partition}:ec2:*:*:snapshot/*", + ] + + condition { + test = "StringEquals" + variable = "ec2:CreateAction" + values = [ + "CreateVolume", + "CreateSnapshot" + ] + } + } + + statement { + actions = ["ec2:DeleteTags"] + + resources = [ + "arn:${local.partition}:ec2:*:*:volume/*", + "arn:${local.partition}:ec2:*:*:snapshot/*", + ] + } + + statement { + actions = ["ec2:CreateVolume"] + resources = ["*"] + + condition { + test = "StringLike" + variable = "aws:RequestTag/ebs.csi.aws.com/cluster" + values = [ + true + ] + } + } + + statement { + actions = ["ec2:CreateVolume"] + resources = ["*"] + + condition { + test = "StringLike" + variable = "aws:RequestTag/CSIVolumeName" + values = ["*"] + } + } + + statement { + actions = ["ec2:CreateVolume"] + resources = ["*"] + + condition { + test = "StringLike" + variable = "aws:RequestTag/kubernetes.io/cluster/*" + values = ["owned"] + } + } + + statement { + actions = ["ec2:DeleteVolume"] + resources = ["*"] + + condition { + test = "StringLike" + variable = "ec2:ResourceTag/ebs.csi.aws.com/cluster" + values = [true] + } + } + + statement { + actions = ["ec2:DeleteVolume"] + resources = ["*"] + + condition { + test = "StringLike" + variable = "ec2:ResourceTag/CSIVolumeName" + values = ["*"] + } + } + + statement { + actions = ["ec2:DeleteVolume"] + resources = ["*"] + + condition { + test = "StringLike" + variable = "ec2:ResourceTag/kubernetes.io/cluster/*" + values = ["owned"] + } + } + + statement { + actions = ["ec2:DeleteSnapshot"] + resources = ["*"] + + condition { + test = "StringLike" + variable = "ec2:ResourceTag/CSIVolumeSnapshotName" + values = ["*"] + } + } + + statement { + actions = ["ec2:DeleteSnapshot"] + resources = ["*"] + + condition { + test = "StringLike" + variable = "ec2:ResourceTag/ebs.csi.aws.com/cluster" + values = [true] + } + } + + dynamic "statement" { + for_each = length(var.ebs_csi_kms_cmk_ids) > 0 ? [1] : [] + content { + actions = [ + "kms:CreateGrant", + "kms:ListGrants", + "kms:RevokeGrant" + ] + + resources = statement.value + + condition { + test = "Bool" + variable = "kms:GrantIsForAWSResource" + values = [true] + } + } + } + + dynamic "statement" { + for_each = length(var.ebs_csi_kms_cmk_ids) > 0 ? [1] : [] + content { + actions = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey" + ] + + resources = statement.value + } + } +} + +resource "aws_iam_policy" "ebs_csi" { + count = var.create_role && var.attach_ebs_csi_policy ? 1 : 0 + + name_prefix = "AmazonEKS_EBS_CSI_Policy-" + path = var.role_path + description = "Provides permissions to manage EBS volumes via the container storage interface driver" + policy = data.aws_iam_policy_document.ebs_csi[0].json + + tags = var.tags +} + +resource "aws_iam_role_policy_attachment" "ebs_csi" { + count = var.create_role && var.attach_ebs_csi_policy ? 1 : 0 + + role = aws_iam_role.this[0].name + policy_arn = aws_iam_policy.ebs_csi[0].arn +} + +################################################################################ +# VPC CNI Policy +################################################################################ + +data "aws_iam_policy_document" "vpc_cni" { + count = var.create_role && var.attach_vpc_cni_policy ? 1 : 0 + + # arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy + dynamic "statement" { + for_each = var.vpc_cni_enable_ipv4 ? [1] : [] + content { + sid = "IPV4" + actions = [ + "ec2:AssignPrivateIpAddresses", + "ec2:AttachNetworkInterface", + "ec2:CreateNetworkInterface", + "ec2:DeleteNetworkInterface", + "ec2:DescribeInstances", + "ec2:DescribeTags", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeInstanceTypes", + "ec2:DetachNetworkInterface", + "ec2:ModifyNetworkInterfaceAttribute", + "ec2:UnassignPrivateIpAddresses" + ] + resources = ["*"] + } + } + + # https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html#cni-iam-role-create-ipv6-policy + dynamic "statement" { + for_each = var.vpc_cni_enable_ipv6 ? [1] : [] + content { + sid = "IPV6" + actions = [ + "ec2:AssignIpv6Addresses", + "ec2:DescribeInstances", + "ec2:DescribeTags", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeInstanceTypes" + ] + resources = ["*"] + } + } + + statement { + sid = "CreateTags" + actions = ["ec2:CreateTags"] + resources = ["arn:${local.partition}:ec2:*:*:network-interface/*"] + } +} + +resource "aws_iam_policy" "vpc_cni" { + count = var.create_role && var.attach_vpc_cni_policy ? 1 : 0 + + name_prefix = "AmazonEKS_CNI_Policy-" + path = var.role_path + description = "Provides the Amazon VPC CNI Plugin (amazon-vpc-cni-k8s) the permissions it requires to modify the IPv4/IPv6 address configuration on your EKS worker nodes" + policy = data.aws_iam_policy_document.vpc_cni[0].json + + tags = var.tags +} + +resource "aws_iam_role_policy_attachment" "vpc_cni" { + count = var.create_role && var.attach_vpc_cni_policy ? 1 : 0 + + role = aws_iam_role.this[0].name + policy_arn = aws_iam_policy.vpc_cni[0].arn +} + +################################################################################ +# Node Termination Handler Policy +################################################################################ + +# https://github.com/aws/aws-node-termination-handler#5-create-an-iam-role-for-the-pods +data "aws_iam_policy_document" "node_termination_handler" { + count = var.create_role && var.attach_node_termination_handler_policy ? 1 : 0 + + statement { + actions = [ + "autoscaling:CompleteLifecycleAction", + "autoscaling:DescribeAutoScalingInstances", + "autoscaling:DescribeTags", + "ec2:DescribeInstances", + ] + + resources = ["*"] + } + + statement { + actions = [ + "sqs:DeleteMessage", + "sqs:ReceiveMessage" + ] + + resources = var.node_termination_handler_sqs_queue_arns + } +} + +resource "aws_iam_policy" "node_termination_handler" { + count = var.create_role && var.attach_node_termination_handler_policy ? 1 : 0 + + name_prefix = "AmazonEKS_Node_Termination_Handler_Policy-" + path = var.role_path + description = "Provides permissions to handle node termination events via the Node Termination Handler" + policy = data.aws_iam_policy_document.node_termination_handler[0].json + + tags = var.tags +} + +resource "aws_iam_role_policy_attachment" "node_termination_handler" { + count = var.create_role && var.attach_node_termination_handler_policy ? 1 : 0 + + role = aws_iam_role.this[0].name + policy_arn = aws_iam_policy.node_termination_handler[0].arn +} + +################################################################################ +# Karpenter Controller Policy +################################################################################ + +# curl -fsSL https://karpenter.sh/v0.6.1/getting-started/cloudformation.yaml +data "aws_iam_policy_document" "karpenter_controller" { + count = var.create_role && var.attach_karpenter_controller_policy ? 1 : 0 + + statement { + actions = [ + "ec2:CreateLaunchTemplate", + "ec2:CreateFleet", + "ec2:CreateTags", + "ec2:DescribeLaunchTemplates", + "ec2:DescribeInstances", + "ec2:DescribeSecurityGroups", + "ec2:DescribeSubnets", + "ec2:DescribeInstanceTypes", + "ec2:DescribeInstanceTypeOfferings", + "ec2:DescribeAvailabilityZones", + ] + + resources = ["*"] + } + + dynamic "statement" { + for_each = toset(var.karpenter_controller_cluster_ids) + content { + actions = [ + "ec2:RunInstances", + "ec2:TerminateInstances", + "ec2:DeleteLaunchTemplate", + ] + + resources = ["*"] + + condition { + test = "StringEquals" + variable = "ec2:ResourceTag/karpenter.sh/discovery" + values = [statement.value] + } + } + } + + statement { + actions = ["ssm:GetParameter"] + resources = var.karpenter_controller_ssm_parameter_arns + } + + statement { + actions = ["iam:PassRole"] + resources = var.karpenter_controller_node_iam_role_arns + } +} + +resource "aws_iam_policy" "karpenter_controller" { + count = var.create_role && var.attach_karpenter_controller_policy ? 1 : 0 + + name_prefix = "AmazonEKS_Karpenter_Controller_Policy-" + path = var.role_path + description = "Provides permissions to handle node termination events via the Node Termination Handler" + policy = data.aws_iam_policy_document.karpenter_controller[0].json + + tags = var.tags +} + +resource "aws_iam_role_policy_attachment" "karpenter_controller" { + count = var.create_role && var.attach_karpenter_controller_policy ? 1 : 0 + + role = aws_iam_role.this[0].name + policy_arn = aws_iam_policy.karpenter_controller[0].arn +} diff --git a/modules/iam-role-for-service-accounts-eks/variables.tf b/modules/iam-role-for-service-accounts-eks/variables.tf new file mode 100644 index 00000000..ab0403b2 --- /dev/null +++ b/modules/iam-role-for-service-accounts-eks/variables.tf @@ -0,0 +1,166 @@ +variable "create_role" { + description = "Whether to create a role" + type = bool + default = true +} + +variable "role_name" { + description = "Name of IAM role" + type = string + default = null +} + +variable "role_path" { + description = "Path of IAM role" + type = string + default = null +} + +variable "role_permissions_boundary_arn" { + description = "Permissions boundary ARN to use for IAM role" + type = string + default = null +} + +variable "role_description" { + description = "IAM Role description" + type = string + default = null +} + +variable "role_name_prefix" { + description = "IAM role name prefix" + type = string + default = null +} + +variable "role_policy_arns" { + description = "ARNs of any policies to attach to the IAM role" + type = list(string) + default = [] +} + +variable "oidc_providers" { + description = "Map of OIDC providers where each provdier map should contain the `provider`, `provider_arns`, and `namespace_service_accounts`" + type = any + default = {} +} + +variable "tags" { + description = "A map of tags to add the the IAM role" + type = map(any) + default = {} +} + +variable "force_detach_policies" { + description = "Whether policies should be detached from this role when destroying" + type = bool + default = true +} + +variable "max_session_duration" { + description = "Maximum CLI/API session duration in seconds between 3600 and 43200" + type = number + default = null +} + +################################################################################ +# Policies +################################################################################ + +# Cluster autoscaler +variable "attach_cluster_autoscaler_policy" { + description = "Determines whether to attach the Cluster Autoscaler IAM policy to the role" + type = bool + default = false +} + +variable "cluster_autoscaler_cluster_ids" { + description = "List of cluster IDs to appropriately scope permissions within the Cluster Autoscaler IAM policy" + type = list(string) + default = [] +} + +# External DNS +variable "attach_external_dns_policy" { + description = "Determines whether to attach the External DNS IAM policy to the role" + type = bool + default = false +} + +variable "external_dns_hosted_zone_arns" { + description = "Route53 hosted zone ARNs to allow external DNS to manage records" + type = list(string) + default = ["arn:aws:route53:::hostedzone/*"] +} + +# EBS CSI +variable "attach_ebs_csi_policy" { + description = "Determines whether to attach the EBS CSI IAM policy to the role" + type = bool + default = false +} + +variable "ebs_csi_kms_cmk_ids" { + description = "KMS CMK IDs to allow EBS CSI to manage encrypted volumes" + type = list(string) + default = [] +} + +# VPC CNI +variable "attach_vpc_cni_policy" { + description = "Determines whether to attach the VPC CNI IAM policy to the role" + type = bool + default = false +} + +variable "vpc_cni_enable_ipv4" { + description = "Determines whether to enable IPv4 permissions for VPC CNI policy" + type = bool + default = false +} + +variable "vpc_cni_enable_ipv6" { + description = "Determines whether to enable IPv6 permissions for VPC CNI policy" + type = bool + default = false +} + +# Node termination handler +variable "attach_node_termination_handler_policy" { + description = "Determines whether to attach the Node Termination Handler policy to the role" + type = bool + default = false +} + +variable "node_termination_handler_sqs_queue_arns" { + description = "List of SQS ARNs that contain node termination events" + type = list(string) + default = ["*"] +} + +# Karpetner controller +variable "attach_karpenter_controller_policy" { + description = "Determines whether to attach the Karpenter Controller policy to the role" + type = bool + default = false +} + +variable "karpenter_controller_cluster_ids" { + description = "List of cluster IDs to appropriately scope EC2 permissions within the Karpenter Controller policy" + type = list(string) + default = [] +} + +variable "karpenter_controller_ssm_parameter_arns" { + description = "List of SSM Parameter ARNs that contain AMI IDs launched by Karpenter" + type = list(string) + # https://github.com/aws/karpenter/blob/ed9473a9863ca949b61b9846c8b9f33f35b86dbd/pkg/cloudprovider/aws/ami.go#L105-L123 + default = ["arn:aws:ssm:*:*:parameter/aws/service/*"] +} + +variable "karpenter_controller_node_iam_role_arns" { + description = "List of node IAM role ARNs Karpenter can use to launch nodes" + type = list(string) + default = ["*"] +} diff --git a/modules/iam-role-for-service-accounts-eks/versions.tf b/modules/iam-role-for-service-accounts-eks/versions.tf new file mode 100644 index 00000000..fe1f6e88 --- /dev/null +++ b/modules/iam-role-for-service-accounts-eks/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 0.13.1" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 3.0" + } + } +} diff --git a/modules/iam-user/outputs.tf b/modules/iam-user/outputs.tf index bf2be4f6..65eab49b 100644 --- a/modules/iam-user/outputs.tf +++ b/modules/iam-user/outputs.tf @@ -1,84 +1,63 @@ locals { - has_encrypted_password = length(compact(aws_iam_user_login_profile.this.*.encrypted_password)) > 0 - has_encrypted_secret = length(compact(aws_iam_access_key.this.*.encrypted_secret)) > 0 + has_encrypted_password = length(compact(aws_iam_user_login_profile.this[*].encrypted_password)) > 0 + has_encrypted_secret = length(compact(aws_iam_access_key.this[*].encrypted_secret)) > 0 } output "iam_user_name" { description = "The user's name" - value = element(concat(aws_iam_user.this.*.name, [""]), 0) + value = try(aws_iam_user.this[0].name, "") } output "iam_user_arn" { description = "The ARN assigned by AWS for this user" - value = element(concat(aws_iam_user.this.*.arn, [""]), 0) + value = try(aws_iam_user.this[0].arn, "") } output "iam_user_unique_id" { description = "The unique ID assigned by AWS" - value = element(concat(aws_iam_user.this.*.unique_id, [""]), 0) + value = try(aws_iam_user.this[0].unique_id, "") } output "iam_user_login_profile_key_fingerprint" { description = "The fingerprint of the PGP key used to encrypt the password" - value = element(concat(aws_iam_user_login_profile.this.*.key_fingerprint, [""]), 0) + value = try(aws_iam_user_login_profile.this[0].key_fingerprint, "") } output "iam_user_login_profile_encrypted_password" { description = "The encrypted password, base64 encoded" - value = element(concat(aws_iam_user_login_profile.this.*.encrypted_password, [""]), 0) + value = try(aws_iam_user_login_profile.this[0].encrypted_password, "") } output "iam_access_key_id" { description = "The access key ID" - value = element( - concat( - aws_iam_access_key.this.*.id, - aws_iam_access_key.this_no_pgp.*.id, - [""], - ), - 0 - ) + value = try(aws_iam_access_key.this[0].id, aws_iam_access_key.this_no_pgp[0].id, "") } output "iam_access_key_secret" { description = "The access key secret" - value = element(concat(aws_iam_access_key.this_no_pgp.*.secret, [""]), 0) + value = try(aws_iam_access_key.this_no_pgp[0].secret, "") sensitive = true } output "iam_access_key_key_fingerprint" { description = "The fingerprint of the PGP key used to encrypt the secret" - value = element(concat(aws_iam_access_key.this.*.key_fingerprint, [""]), 0) + value = try(aws_iam_access_key.this[0].key_fingerprint, "") } output "iam_access_key_encrypted_secret" { description = "The encrypted secret, base64 encoded" - value = element(concat(aws_iam_access_key.this.*.encrypted_secret, [""]), 0) + value = try(aws_iam_access_key.this[0].encrypted_secret, "") } output "iam_access_key_ses_smtp_password_v4" { description = "The secret access key converted into an SES SMTP password by applying AWS's Sigv4 conversion algorithm" - value = element( - concat( - aws_iam_access_key.this.*.ses_smtp_password_v4, - aws_iam_access_key.this_no_pgp.*.ses_smtp_password_v4, - [""], - ), - 0 - ) - sensitive = true + value = try(aws_iam_access_key.this[0].ses_smtp_password_v4, aws_iam_access_key.this_no_pgp[0].ses_smtp_password_v4, "") + sensitive = true } output "iam_access_key_status" { description = "Active or Inactive. Keys are initially active, but can be made inactive by other means." - value = element( - concat( - aws_iam_access_key.this.*.status, - aws_iam_access_key.this_no_pgp.*.status, - [""], - ), - 0 - ) + value = try(aws_iam_access_key.this[0].status, aws_iam_access_key.this_no_pgp[0].status, "") } output "pgp_key" { @@ -89,7 +68,7 @@ output "pgp_key" { output "keybase_password_decrypt_command" { description = "Decrypt user password command" value = !local.has_encrypted_password ? null : <