Merge pull request #171 from cloud-neutral-toolkit/codex/fix-iam-role-creation-permission-issue

Allow bootstrapping with existing IAM identity
This commit is contained in:
cloudneutral 2025-12-10 15:29:35 +08:00 committed by GitHub
commit bc54f640d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 343 additions and 49 deletions

View File

@ -8,7 +8,18 @@ terraform {
}
provider "alicloud" {
region = var.region
region = var.region
access_key = var.access_key
secret_key = var.secret_key
security_token = var.security_token
dynamic "assume_role" {
for_each = var.ram_role_arn == null ? [] : [var.ram_role_arn]
content {
role_arn = assume_role.value
session_name = var.session_name
}
}
}
locals {

View File

@ -4,6 +4,43 @@ variable "region" {
default = "cn-hangzhou"
}
variable "access_key" {
description = "Alibaba Cloud Access Key ID"
type = string
default = null
validation {
condition = (var.access_key == null && var.secret_key == null) || (var.access_key != null && var.secret_key != null)
error_message = "Provide both access_key and secret_key, or leave both null to rely on environment-sourced credentials."
}
}
variable "secret_key" {
description = "Alibaba Cloud Access Key Secret"
type = string
default = null
sensitive = true
}
variable "security_token" {
description = "Optional security token when using STS credentials"
type = string
default = null
sensitive = true
}
variable "ram_role_arn" {
description = "Optional RAM role ARN to assume for operations"
type = string
default = null
}
variable "session_name" {
description = "Session name when assuming a RAM role"
type = string
default = "terraform"
}
variable "account_id" {
description = "Alibaba Cloud account ID used for trust policy"
type = string

View File

@ -8,7 +8,18 @@ terraform {
}
provider "alicloud" {
region = var.region
region = var.region
access_key = var.access_key
secret_key = var.secret_key
security_token = var.security_token
dynamic "assume_role" {
for_each = var.ram_role_arn == null ? [] : [var.ram_role_arn]
content {
role_arn = assume_role.value
session_name = var.session_name
}
}
}
resource "alicloud_ots_instance" "this" {

View File

@ -4,6 +4,43 @@ variable "region" {
default = "cn-hangzhou"
}
variable "access_key" {
description = "Alibaba Cloud Access Key ID"
type = string
default = null
validation {
condition = (var.access_key == null && var.secret_key == null) || (var.access_key != null && var.secret_key != null)
error_message = "Provide both access_key and secret_key, or leave both null to rely on environment-sourced credentials."
}
}
variable "secret_key" {
description = "Alibaba Cloud Access Key Secret"
type = string
default = null
sensitive = true
}
variable "security_token" {
description = "Optional security token when using STS credentials"
type = string
default = null
sensitive = true
}
variable "ram_role_arn" {
description = "Optional RAM role ARN to assume for operations"
type = string
default = null
}
variable "session_name" {
description = "Session name when assuming a RAM role"
type = string
default = "terraform"
}
variable "instance_name" {
description = "Name of the OTS instance"
type = string

View File

@ -8,12 +8,22 @@ terraform {
}
provider "alicloud" {
region = var.region
region = var.region
access_key = var.access_key
secret_key = var.secret_key
security_token = var.security_token
dynamic "assume_role" {
for_each = var.ram_role_arn == null ? [] : [var.ram_role_arn]
content {
role_arn = assume_role.value
session_name = var.session_name
}
}
}
resource "alicloud_oss_bucket" "state" {
bucket = var.state_bucket
acl = var.acl
versioning {
status = "Enabled"
@ -24,6 +34,11 @@ resource "alicloud_oss_bucket" "state" {
}
}
resource "alicloud_oss_bucket_acl" "state" {
bucket = alicloud_oss_bucket.state.bucket
acl = var.acl
}
output "bucket" {
value = alicloud_oss_bucket.state.bucket
}

View File

@ -4,6 +4,43 @@ variable "region" {
default = "cn-hangzhou"
}
variable "access_key" {
description = "Alibaba Cloud Access Key ID"
type = string
default = null
validation {
condition = (var.access_key == null && var.secret_key == null) || (var.access_key != null && var.secret_key != null)
error_message = "Provide both access_key and secret_key, or leave both null to rely on environment-sourced credentials."
}
}
variable "secret_key" {
description = "Alibaba Cloud Access Key Secret"
type = string
default = null
sensitive = true
}
variable "security_token" {
description = "Optional security token when using STS credentials"
type = string
default = null
sensitive = true
}
variable "ram_role_arn" {
description = "Optional RAM role ARN to assume for operations"
type = string
default = null
}
variable "session_name" {
description = "Session name when assuming a RAM role"
type = string
default = "terraform"
}
variable "state_bucket" {
description = "Name of the OSS bucket used for remote state"
type = string

View File

@ -9,7 +9,18 @@ terraform {
}
provider "alicloud" {
region = var.region
region = var.region
access_key = coalesce(var.access_key, "mock-access-key")
secret_key = coalesce(var.secret_key, "mock-secret-key")
security_token = var.security_token
dynamic "assume_role" {
for_each = var.ram_role_arn == null ? [] : [var.ram_role_arn]
content {
role_arn = assume_role.value
session_name = var.session_name
}
}
}
module "network" {

View File

@ -4,6 +4,38 @@ variable "region" {
default = "cn-hangzhou"
}
variable "access_key" {
description = "Alibaba Cloud Access Key ID"
type = string
default = null
}
variable "secret_key" {
description = "Alibaba Cloud Access Key Secret"
type = string
default = null
sensitive = true
}
variable "security_token" {
description = "Optional security token when using STS credentials"
type = string
default = null
sensitive = true
}
variable "ram_role_arn" {
description = "Optional RAM role ARN to assume for operations"
type = string
default = null
}
variable "session_name" {
description = "Session name when assuming a RAM role"
type = string
default = "terraform"
}
variable "vpc_name" {
description = "VPC name"
type = string

View File

@ -1,6 +1,5 @@
resource "alicloud_oss_bucket" "this" {
bucket = var.name
acl = var.acl
versioning {
status = var.enable_versioning ? "Enabled" : "Suspended"
@ -11,6 +10,11 @@ resource "alicloud_oss_bucket" "this" {
}
}
resource "alicloud_oss_bucket_acl" "this" {
bucket = alicloud_oss_bucket.this.bucket
acl = var.acl
}
output "bucket" {
value = alicloud_oss_bucket.this.bucket
}

View File

@ -8,6 +8,11 @@ variable "access_key" {
description = "Alibaba Cloud Access Key ID"
type = string
default = null
validation {
condition = (var.access_key == null && var.secret_key == null) || (var.access_key != null && var.secret_key != null)
error_message = "Provide both access_key and secret_key, or leave both null to rely on environment-sourced credentials."
}
}
variable "secret_key" {

View File

@ -7,6 +7,9 @@ locals {
config_terraform_user = coalesce(var.terraform_user_name, local.bootstrap.iam.terraform_user_name)
environment = coalesce(try(local.bootstrap.environment, null), try(local.bootstrap.iam.environment, null), "bootstrap")
extra_tags = try(local.bootstrap.tags, {})
role_name = coalesce(var.existing_role_name, local.config_role_name)
terraform_user_name = coalesce(var.existing_user_name, local.config_terraform_user)
}
locals {

View File

@ -2,7 +2,9 @@
# IAM Role: Terraform Deploy Role
# ----------------------------------------
resource "aws_iam_role" "terraform_deploy_role" {
name = local.config_role_name
count = var.create_role ? 1 : 0
name = local.role_name
assume_role_policy = jsonencode({
Version = "2012-10-17"
@ -25,26 +27,106 @@ resource "aws_iam_role" "terraform_deploy_role" {
)
}
# Admin full access
#
resource "aws_iam_role_policy_attachment" "attach_admin" {
role = aws_iam_role.terraform_deploy_role.name
policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
resource "aws_iam_role_policy" "terraform_deploy_role_policy" {
count = var.create_role ? 1 : 0
name = "${local.role_name}-bootstrap-minimal"
role = aws_iam_role.terraform_deploy_role[0].id
policy = jsonencode({
Version = "2012-10-17",
Statement = [
# Bootstrap S3 backend (state bucket)
{
Effect = "Allow",
Action = [
"s3:CreateBucket",
"s3:GetBucketLocation",
"s3:ListBucket",
"s3:PutBucketVersioning",
"s3:PutBucketPolicy",
"s3:PutBucketTagging",
"s3:PutEncryptionConfiguration",
"s3:PutBucketPublicAccessBlock",
],
Resource = "arn:aws:s3:::${local.bootstrap.state.bucket_name}"
},
{
Effect = "Allow",
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:PutObjectTagging"
],
Resource = "arn:aws:s3:::${local.bootstrap.state.bucket_name}/*"
},
# DynamoDB state lock table
{
Effect = "Allow",
Action = [
"dynamodb:CreateTable",
"dynamodb:DescribeTable",
"dynamodb:UpdateTable",
"dynamodb:TagResource",
"dynamodb:UntagResource"
],
Resource = "arn:aws:dynamodb:${local.config_region}:${local.account.account_id}:table/${local.bootstrap.state.dynamodb_table_name}"
},
# IAM roles needed for bootstrap lifecycle
{
Effect = "Allow",
Action = [
"iam:GetRole",
"iam:CreateRole",
"iam:DeleteRole",
"iam:UpdateAssumeRolePolicy",
"iam:TagRole",
"iam:UntagRole"
],
Resource = [
"arn:aws:iam::${local.account.account_id}:role/${local.role_name}",
"arn:aws:iam::${local.account.account_id}:role/bootstrap-*",
"arn:aws:iam::${local.account.account_id}:role/terraform-*"
]
},
{
Effect = "Allow",
Action = [
"iam:PutRolePolicy",
"iam:DeleteRolePolicy",
"iam:AttachRolePolicy",
"iam:DetachRolePolicy"
],
Resource = [
"arn:aws:iam::${local.account.account_id}:role/${local.role_name}",
"arn:aws:iam::${local.account.account_id}:role/bootstrap-*",
"arn:aws:iam::${local.account.account_id}:role/terraform-*"
]
}
]
})
}
#
# IAM User for Terraform (AK/SK)
# ----------------------------------------
resource "aws_iam_user" "terraform_user" {
name = local.config_terraform_user
count = var.create_user ? 1 : 0
name = local.terraform_user_name
}
#
# IAM User Policy:
# ----------------------------------------
resource "aws_iam_user_policy" "terraform_user_policy" {
name = "${local.config_terraform_user}-iac-policy"
user = aws_iam_user.terraform_user.name
count = var.create_user ? 1 : 0
name = "${local.terraform_user_name}-iac-policy"
user = aws_iam_user.terraform_user[0].name
policy = jsonencode({
Version = "2012-10-17",
@ -55,38 +137,7 @@ resource "aws_iam_user_policy" "terraform_user_policy" {
Action = [
"sts:AssumeRole"
],
Resource = aws_iam_role.terraform_deploy_role.arn
},
# S3: Terraform state bucket
{
Effect = "Allow",
Action = [
"s3:ListBucket"
],
Resource = "arn:aws:s3:::${local.bootstrap.state.bucket_name}"
},
{
Effect = "Allow",
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
Resource = "arn:aws:s3:::${local.bootstrap.state.bucket_name}/*"
},
# DynamoDB: state lock table
{
Effect = "Allow",
Action = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem",
"dynamodb:UpdateItem",
"dynamodb:DescribeTable"
],
Resource = "arn:aws:dynamodb:${local.config_region}:${local.account.account_id}:table/${local.bootstrap.state.dynamodb_table_name}"
Resource = var.create_role ? aws_iam_role.terraform_deploy_role[0].arn : var.existing_role_arn
}
]
})

View File

@ -1,9 +1,9 @@
output "iam_role_arn" {
value = aws_iam_role.terraform_deploy_role.arn
value = var.create_role ? aws_iam_role.terraform_deploy_role[0].arn : var.existing_role_arn
description = "The ARN of the role assumed by Terraform"
}
output "terraform_user_name" {
value = aws_iam_user.terraform_user.name
value = var.create_user ? aws_iam_user.terraform_user[0].name : local.terraform_user_name
description = "Terraform IAM User"
}

View File

@ -10,14 +10,54 @@ variable "account_name" {
default = null
}
variable "create_role" {
description = "Whether to create the Terraform deploy IAM role"
type = bool
default = true
validation {
condition = var.create_role || (var.existing_role_arn != null && var.existing_role_name != null)
error_message = "existing_role_name and existing_role_arn must be provided when create_role is false."
}
}
variable "existing_role_name" {
description = "Existing IAM role name to reference when create_role is false"
type = string
default = null
}
variable "existing_role_arn" {
description = "Existing IAM role ARN to reference when create_role is false"
type = string
default = null
}
variable "role_name" {
type = string
description = "IAM role name to create (e.g., TerraformDeployRole-Dev)"
default = null
}
variable "existing_user_name" {
description = "Existing IAM username to reference when create_user is false"
type = string
default = null
}
variable "terraform_user_name" {
type = string
description = "IAM username for Terraform IAC runner"
default = null
}
variable "create_user" {
description = "Whether to create the IAM user for Terraform"
type = bool
default = true
validation {
condition = var.create_user || var.existing_user_name != null
error_message = "existing_user_name must be provided when create_user is false."
}
}