diff --git a/terraform/aws/codepipeline/README.md b/terraform/aws/codepipeline/README.md index 2946473..b39751d 100644 --- a/terraform/aws/codepipeline/README.md +++ b/terraform/aws/codepipeline/README.md @@ -155,17 +155,35 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [app](#input\_app) | Application name used for resource naming and SSM parameter paths. | `string` | n/a | yes | -| [codebuild\_project\_arn](#input\_codebuild\_project\_arn) | ARN of the CodeBuild project. Used for IAM permissions. | `string` | n/a | yes | -| [codebuild\_project\_name](#input\_codebuild\_project\_name) | Name of the CodeBuild project to use in the Build stage. | `string` | n/a | yes | +| [artifact\_bucket\_name](#input\_artifact\_bucket\_name) | Name of existing S3 bucket for artifacts. Required when create\_artifact\_bucket is false. | `string` | `null` | no | +| [build\_output\_artifact\_name](#input\_build\_output\_artifact\_name) | Name of the build output artifact. | `string` | `"BuildArtifact"` | no | +| [codebuild\_project\_arn](#input\_codebuild\_project\_arn) | ARN of the CodeBuild project. Used for IAM permissions. Required when enable\_build\_stage is true. | `string` | `null` | no | +| [codebuild\_project\_name](#input\_codebuild\_project\_name) | Name of the CodeBuild project to use in the Build stage. Required when enable\_build\_stage is true. | `string` | `null` | no | +| [codedeploy\_applications](#input\_codedeploy\_applications) | List of CodeDeploy deployment configurations.
Each deployment should have: application\_name, deployment\_group\_name, and optional action\_name, namespace, run\_order. |
list(object({
application_name = string
deployment_group_name = string
action_name = optional(string, null)
namespace = optional(string, null)
run_order = optional(number, null)
}))
| `[]` | no | +| [codestar\_connection\_arn](#input\_codestar\_connection\_arn) | ARN of CodeStar connection for GitHub V2 integration. Required when source\_provider is CodeStarSourceConnection. | `string` | `null` | no | +| [create\_artifact\_bucket](#input\_create\_artifact\_bucket) | Create a new S3 bucket for artifacts. If false, use existing bucket specified in artifact\_bucket\_name. | `bool` | `true` | no | +| [create\_service\_role](#input\_create\_service\_role) | Create a new IAM service role for CodePipeline. If false, use existing role specified in service\_role\_arn. | `bool` | `true` | no | +| [deploy\_action\_namespace](#input\_deploy\_action\_namespace) | Namespace for deploy action variables. | `string` | `"DeployVariables"` | no | +| [detect\_changes](#input\_detect\_changes) | Whether to detect changes in the source repository for CodeStarSourceConnection. | `bool` | `false` | no | +| [enable\_build\_stage](#input\_enable\_build\_stage) | Enable Build stage with CodeBuild. If false, pipeline goes directly from Source to Deploy. | `bool` | `false` | no | +| [enable\_deploy\_stage](#input\_enable\_deploy\_stage) | Enable Deploy stage with CodeDeploy. | `bool` | `true` | no | | [env](#input\_env) | Environment name used for resource naming and SSM parameter paths. | `string` | n/a | yes | +| [execution\_mode](#input\_execution\_mode) | Execution mode for V2 pipelines. Valid values: QUEUED, SUPERSEDED, PARALLEL | `string` | `"SUPERSEDED"` | no | | [github\_branch](#input\_github\_branch) | GitHub branch to monitor for changes and trigger pipeline executions. | `string` | `"main"` | no | +| [github\_full\_repository\_id](#input\_github\_full\_repository\_id) | Full repository ID in format 'owner/repo' for CodeStarSourceConnection. | `string` | `null` | no | | [github\_owner](#input\_github\_owner) | GitHub repository owner (organization or username). | `string` | n/a | yes | | [github\_repo](#input\_github\_repo) | GitHub repository name. | `string` | n/a | yes | | [kms\_key\_id](#input\_kms\_key\_id) | KMS key ID for S3 bucket encryption.
If not provided, uses AWS-managed encryption (AES256).
For enhanced security, provide a customer-managed KMS key ARN or alias.
Examples:
- Key ARN: arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012
- Alias ARN: arn:aws:kms:us-east-1:123456789012:alias/my-key
- Key ID: 12345678-1234-1234-1234-123456789012 | `string` | `null` | no | | [mock\_account\_id](#input\_mock\_account\_id) | Mock AWS account ID to use when skip\_data\_source\_lookup is true. | `string` | `"123456789012"` | no | | [mock\_github\_token](#input\_mock\_github\_token) | Mock GitHub token to use when skip\_data\_source\_lookup is true. | `string` | `"mock-token"` | no | +| [output\_artifact\_format](#input\_output\_artifact\_format) | Output artifact format for CodeStarSourceConnection. Valid values: CODE\_ZIP, CODEBUILD\_CLONE\_REF | `string` | `"CODE_ZIP"` | no | | [pipeline\_name](#input\_pipeline\_name) | Name of the CodePipeline. Used for resource naming and tagging. | `string` | n/a | yes | +| [pipeline\_type](#input\_pipeline\_type) | Pipeline type. Valid values: V1, V2 | `string` | `"V2"` | no | +| [service\_role\_arn](#input\_service\_role\_arn) | ARN of existing IAM role for CodePipeline. Required when create\_service\_role is false. | `string` | `null` | no | | [skip\_data\_source\_lookup](#input\_skip\_data\_source\_lookup) | Skip AWS data source lookups for testing without credentials. Uses mock values instead. | `bool` | `false` | no | +| [source\_action\_namespace](#input\_source\_action\_namespace) | Namespace for source action variables. | `string` | `"SourceVariables"` | no | +| [source\_output\_artifact\_name](#input\_source\_output\_artifact\_name) | Name of the source output artifact. | `string` | `"SourceArtifact"` | no | +| [source\_provider](#input\_source\_provider) | Source provider type. Valid values: CodeStarSourceConnection, GitHub | `string` | `"CodeStarSourceConnection"` | no | | [tags](#input\_tags) | A map of tags to add to all resources. | `map(string)` | `{}` | no | ## Outputs diff --git a/terraform/aws/codepipeline/main.tf b/terraform/aws/codepipeline/main.tf index 73fc656..fab05b9 100644 --- a/terraform/aws/codepipeline/main.tf +++ b/terraform/aws/codepipeline/main.tf @@ -21,14 +21,17 @@ data "aws_region" "current" { } data "aws_ssm_parameter" "github_token" { - count = var.skip_data_source_lookup ? 0 : 1 + count = var.skip_data_source_lookup || var.source_provider != "GitHub" ? 0 : 1 name = "/${var.env}/${var.app}/github-token" } locals { - account_id = var.skip_data_source_lookup ? var.mock_account_id : data.aws_caller_identity.current[0].account_id - region = var.skip_data_source_lookup ? "us-east-1" : data.aws_region.current[0].name - github_token = var.skip_data_source_lookup ? var.mock_github_token : data.aws_ssm_parameter.github_token[0].value + account_id = var.skip_data_source_lookup ? var.mock_account_id : data.aws_caller_identity.current[0].account_id + region = var.skip_data_source_lookup ? "us-east-1" : data.aws_region.current[0].name + github_token = var.skip_data_source_lookup ? var.mock_github_token : (var.source_provider == "GitHub" ? data.aws_ssm_parameter.github_token[0].value : null) + github_full_repo_id = var.github_full_repository_id != null ? var.github_full_repository_id : "${var.github_owner}/${var.github_repo}" + artifact_bucket = var.create_artifact_bucket ? aws_s3_bucket.pipeline_artifacts[0].bucket : var.artifact_bucket_name + pipeline_role_arn = var.create_service_role ? aws_iam_role.pipeline[0].arn : var.service_role_arn } # ----------------------------------------------------------------------------- @@ -36,13 +39,17 @@ locals { # ----------------------------------------------------------------------------- resource "aws_s3_bucket" "pipeline_artifacts" { + count = var.create_artifact_bucket ? 1 : 0 + bucket = "${var.env}-${var.app}-artifacts-${local.account_id}" tags = var.tags } resource "aws_s3_bucket_versioning" "pipeline_artifacts" { - bucket = aws_s3_bucket.pipeline_artifacts.id + count = var.create_artifact_bucket ? 1 : 0 + + bucket = aws_s3_bucket.pipeline_artifacts[0].id versioning_configuration { status = "Enabled" @@ -50,7 +57,9 @@ resource "aws_s3_bucket_versioning" "pipeline_artifacts" { } resource "aws_s3_bucket_server_side_encryption_configuration" "pipeline_artifacts" { - bucket = aws_s3_bucket.pipeline_artifacts.id + count = var.create_artifact_bucket ? 1 : 0 + + bucket = aws_s3_bucket.pipeline_artifacts[0].id rule { apply_server_side_encryption_by_default { @@ -61,7 +70,9 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "pipeline_artifact } resource "aws_s3_bucket_public_access_block" "pipeline_artifacts" { - bucket = aws_s3_bucket.pipeline_artifacts.id + count = var.create_artifact_bucket ? 1 : 0 + + bucket = aws_s3_bucket.pipeline_artifacts[0].id block_public_acls = true block_public_policy = true @@ -74,6 +85,8 @@ resource "aws_s3_bucket_public_access_block" "pipeline_artifacts" { # ----------------------------------------------------------------------------- resource "aws_iam_role" "pipeline" { + count = var.create_service_role ? 1 : 0 + name = "${var.pipeline_name}-role" assume_role_policy = jsonencode({ @@ -91,8 +104,10 @@ resource "aws_iam_role" "pipeline" { } resource "aws_iam_role_policy" "pipeline" { + count = var.create_service_role ? 1 : 0 + name = "${var.pipeline_name}-policy" - role = aws_iam_role.pipeline.id + role = aws_iam_role.pipeline[0].id policy = jsonencode({ Version = "2012-10-17" @@ -105,15 +120,34 @@ resource "aws_iam_role_policy" "pipeline" { "s3:GetObjectVersion", "s3:PutObject" ] - Resource = "${aws_s3_bucket.pipeline_artifacts.arn}/*" + Resource = "arn:aws:s3:::${local.artifact_bucket}/*" }, { Effect = "Allow" Action = [ "s3:ListBucket" ] - Resource = aws_s3_bucket.pipeline_artifacts.arn + Resource = "arn:aws:s3:::${local.artifact_bucket}" }, + { + Effect = "Allow" + Action = [ + "ssm:GetParameter", + "ssm:GetParameters" + ] + Resource = "arn:aws:ssm:${local.region}:${local.account_id}:parameter/${var.env}/*" + } + ], + var.source_provider == "CodeStarSourceConnection" && var.codestar_connection_arn != null ? [ + { + Effect = "Allow" + Action = [ + "codestar-connections:UseConnection" + ] + Resource = var.codestar_connection_arn + } + ] : [], + var.enable_build_stage && var.codebuild_project_arn != null ? [ { Effect = "Allow" Action = [ @@ -121,16 +155,22 @@ resource "aws_iam_role_policy" "pipeline" { "codebuild:StartBuild" ] Resource = var.codebuild_project_arn - }, + } + ] : [], + var.enable_deploy_stage && length(var.codedeploy_applications) > 0 ? [ { Effect = "Allow" Action = [ - "ssm:GetParameter", - "ssm:GetParameters" + "codedeploy:CreateDeployment", + "codedeploy:GetApplication", + "codedeploy:GetApplicationRevision", + "codedeploy:GetDeployment", + "codedeploy:GetDeploymentConfig", + "codedeploy:RegisterApplicationRevision" ] - Resource = "arn:aws:ssm:${local.region}:${local.account_id}:parameter/${var.env}/*" + Resource = "*" } - ], + ] : [], var.kms_key_id != null ? [ { Effect = "Allow" @@ -152,26 +192,36 @@ resource "aws_iam_role_policy" "pipeline" { # ----------------------------------------------------------------------------- resource "aws_codepipeline" "this" { - name = var.pipeline_name - role_arn = aws_iam_role.pipeline.arn + name = var.pipeline_name + role_arn = local.pipeline_role_arn + pipeline_type = var.pipeline_type + execution_mode = var.pipeline_type == "V2" ? var.execution_mode : null artifact_store { - location = aws_s3_bucket.pipeline_artifacts.bucket + location = local.artifact_bucket type = "S3" } + # Source Stage stage { name = "Source" action { name = "Source" category = "Source" - owner = "ThirdParty" - provider = "GitHub" + owner = var.source_provider == "CodeStarSourceConnection" ? "AWS" : "ThirdParty" + provider = var.source_provider version = "1" - output_artifacts = ["source_output"] + output_artifacts = [var.source_output_artifact_name] + namespace = var.source_action_namespace - configuration = { + configuration = var.source_provider == "CodeStarSourceConnection" ? { + ConnectionArn = var.codestar_connection_arn + FullRepositoryId = local.github_full_repo_id + BranchName = var.github_branch + DetectChanges = tostring(var.detect_changes) + OutputArtifactFormat = var.output_artifact_format + } : { Owner = var.github_owner Repo = var.github_repo Branch = var.github_branch @@ -180,23 +230,81 @@ resource "aws_codepipeline" "this" { } } - stage { - name = "Build" + # Build Stage (Optional) + dynamic "stage" { + for_each = var.enable_build_stage ? [1] : [] + content { + name = "Build" - action { - name = "Build" - category = "Build" - owner = "AWS" - provider = "CodeBuild" - version = "1" - input_artifacts = ["source_output"] - output_artifacts = ["build_output"] + action { + name = "Build" + category = "Build" + owner = "AWS" + provider = "CodeBuild" + version = "1" + input_artifacts = [var.source_output_artifact_name] + output_artifacts = [var.build_output_artifact_name] + + configuration = { + ProjectName = var.codebuild_project_name + } + } + } + } + + # Deploy Stage (Optional) + dynamic "stage" { + for_each = var.enable_deploy_stage && length(var.codedeploy_applications) > 0 ? [1] : [] + content { + name = "Deploy" + + dynamic "action" { + for_each = var.codedeploy_applications + content { + name = action.value.action_name != null ? action.value.action_name : (length(var.codedeploy_applications) == 1 ? "Deploy" : "Deploy-${action.value.deployment_group_name}") + category = "Deploy" + owner = "AWS" + provider = "CodeDeploy" + version = "1" + input_artifacts = [var.enable_build_stage ? var.build_output_artifact_name : var.source_output_artifact_name] + namespace = action.value.namespace != null ? action.value.namespace : (action.key == 0 ? var.deploy_action_namespace : null) + run_order = action.value.run_order != null ? action.value.run_order : (action.key + 1) - configuration = { - ProjectName = var.codebuild_project_name + configuration = { + ApplicationName = action.value.application_name + DeploymentGroupName = action.value.deployment_group_name + } + } } } } + lifecycle { + precondition { + condition = var.create_service_role || var.service_role_arn != null + error_message = "service_role_arn must be provided when create_service_role is false." + } + + precondition { + condition = var.create_artifact_bucket || var.artifact_bucket_name != null + error_message = "artifact_bucket_name must be provided when create_artifact_bucket is false." + } + + precondition { + condition = var.source_provider != "CodeStarSourceConnection" || var.codestar_connection_arn != null + error_message = "codestar_connection_arn must be provided when source_provider is CodeStarSourceConnection." + } + + precondition { + condition = !var.enable_build_stage || (var.codebuild_project_name != null && var.codebuild_project_arn != null) + error_message = "codebuild_project_name and codebuild_project_arn must be provided when enable_build_stage is true." + } + + precondition { + condition = !var.enable_deploy_stage || length(var.codedeploy_applications) > 0 + error_message = "codedeploy_applications must be provided when enable_deploy_stage is true." + } + } + tags = var.tags } diff --git a/terraform/aws/codepipeline/outputs.tf b/terraform/aws/codepipeline/outputs.tf index 8c48c55..b7f2aaa 100644 --- a/terraform/aws/codepipeline/outputs.tf +++ b/terraform/aws/codepipeline/outputs.tf @@ -23,22 +23,22 @@ output "pipeline_arn" { output "artifact_bucket_name" { description = "The name of the S3 bucket for pipeline artifacts." - value = aws_s3_bucket.pipeline_artifacts.id + value = local.artifact_bucket } output "artifact_bucket_id" { description = "The ID of the S3 bucket for pipeline artifacts." - value = aws_s3_bucket.pipeline_artifacts.id + value = local.artifact_bucket } output "artifact_bucket_arn" { description = "The ARN of the S3 bucket for pipeline artifacts." - value = aws_s3_bucket.pipeline_artifacts.arn + value = var.create_artifact_bucket ? aws_s3_bucket.pipeline_artifacts[0].arn : "arn:aws:s3:::${var.artifact_bucket_name}" } output "artifact_bucket_region" { description = "The AWS region of the S3 bucket." - value = aws_s3_bucket.pipeline_artifacts.region + value = var.create_artifact_bucket ? aws_s3_bucket.pipeline_artifacts[0].region : local.region } # ----------------------------------------------------------------------------- @@ -47,15 +47,15 @@ output "artifact_bucket_region" { output "pipeline_role_name" { description = "The name of the CodePipeline IAM role." - value = aws_iam_role.pipeline.name + value = var.create_service_role ? aws_iam_role.pipeline[0].name : null } output "pipeline_role_id" { description = "The ID of the CodePipeline IAM role." - value = aws_iam_role.pipeline.id + value = var.create_service_role ? aws_iam_role.pipeline[0].id : null } output "pipeline_role_arn" { description = "The ARN of the CodePipeline IAM role." - value = aws_iam_role.pipeline.arn + value = local.pipeline_role_arn } diff --git a/terraform/aws/codepipeline/tests/basic/main.tf b/terraform/aws/codepipeline/tests/basic/main.tf index a168335..59526be 100644 --- a/terraform/aws/codepipeline/tests/basic/main.tf +++ b/terraform/aws/codepipeline/tests/basic/main.tf @@ -31,15 +31,20 @@ module "test_basic_pipeline" { env = "dev" app = "myapp" - # GitHub configuration - github_owner = "myorg" - github_repo = "myrepo" - github_branch = "main" + # Source configuration (GitHub V1) + source_provider = "GitHub" + github_owner = "myorg" + github_repo = "myrepo" + github_branch = "main" - # CodeBuild integration + # Build stage (enabled) + enable_build_stage = true codebuild_project_name = "test-build-project" codebuild_project_arn = "arn:aws:codebuild:us-east-1:123456789012:project/test-build-project" + # Deploy stage (disabled for basic test) + enable_deploy_stage = false + # Skip real AWS calls for testing skip_data_source_lookup = true mock_account_id = "123456789012" diff --git a/terraform/aws/codepipeline/variables.tf b/terraform/aws/codepipeline/variables.tf index 65a09f1..8a214a6 100644 --- a/terraform/aws/codepipeline/variables.tf +++ b/terraform/aws/codepipeline/variables.tf @@ -17,6 +17,28 @@ variable "pipeline_name" { } } +variable "pipeline_type" { + description = "Pipeline type. Valid values: V1, V2" + type = string + default = "V2" + + validation { + condition = contains(["V1", "V2"], var.pipeline_type) + error_message = "Pipeline type must be either V1 or V2." + } +} + +variable "execution_mode" { + description = "Execution mode for V2 pipelines. Valid values: QUEUED, SUPERSEDED, PARALLEL" + type = string + default = "SUPERSEDED" + + validation { + condition = contains(["QUEUED", "SUPERSEDED", "PARALLEL"], var.execution_mode) + error_message = "Execution mode must be one of: QUEUED, SUPERSEDED, PARALLEL." + } +} + variable "env" { description = "Environment name used for resource naming and SSM parameter paths." type = string @@ -48,9 +70,34 @@ variable "app" { } # ----------------------------------------------------------------------------- -# GitHub Configuration +# Source Configuration # ----------------------------------------------------------------------------- +variable "source_provider" { + description = "Source provider type. Valid values: CodeStarSourceConnection, GitHub" + type = string + default = "CodeStarSourceConnection" + + validation { + condition = contains(["CodeStarSourceConnection", "GitHub"], var.source_provider) + error_message = "Source provider must be either CodeStarSourceConnection or GitHub." + } +} + +variable "codestar_connection_arn" { + description = "ARN of CodeStar connection for GitHub V2 integration. Required when source_provider is CodeStarSourceConnection." + type = string + default = null + + validation { + condition = var.codestar_connection_arn == null || can(regex( + "^arn:aws:(codestar-connections|codeconnections):[a-z0-9-]+:[0-9]{12}:connection/[a-f0-9-]+$", + var.codestar_connection_arn + )) + error_message = "CodeStar connection ARN must be a valid ARN format." + } +} + variable "github_owner" { description = "GitHub repository owner (organization or username)." type = string @@ -82,25 +129,108 @@ variable "github_branch" { } } +variable "github_full_repository_id" { + description = "Full repository ID in format 'owner/repo' for CodeStarSourceConnection." + type = string + default = null +} + +variable "detect_changes" { + description = "Whether to detect changes in the source repository for CodeStarSourceConnection." + type = bool + default = false +} + +variable "output_artifact_format" { + description = "Output artifact format for CodeStarSourceConnection. Valid values: CODE_ZIP, CODEBUILD_CLONE_REF" + type = string + default = "CODE_ZIP" + + validation { + condition = contains(["CODE_ZIP", "CODEBUILD_CLONE_REF"], var.output_artifact_format) + error_message = "Output artifact format must be either CODE_ZIP or CODEBUILD_CLONE_REF." + } +} + +variable "source_output_artifact_name" { + description = "Name of the source output artifact." + type = string + default = "SourceArtifact" +} + +variable "build_output_artifact_name" { + description = "Name of the build output artifact." + type = string + default = "BuildArtifact" +} + +variable "source_action_namespace" { + description = "Namespace for source action variables." + type = string + default = "SourceVariables" +} + +variable "deploy_action_namespace" { + description = "Namespace for deploy action variables." + type = string + default = "DeployVariables" +} + # ----------------------------------------------------------------------------- -# CodeBuild Integration +# Build Stage Configuration # ----------------------------------------------------------------------------- +variable "enable_build_stage" { + description = "Enable Build stage with CodeBuild. If false, pipeline goes directly from Source to Deploy." + type = bool + default = false +} + variable "codebuild_project_name" { - description = "Name of the CodeBuild project to use in the Build stage." + description = "Name of the CodeBuild project to use in the Build stage. Required when enable_build_stage is true." type = string + default = null } variable "codebuild_project_arn" { - description = "ARN of the CodeBuild project. Used for IAM permissions." + description = "ARN of the CodeBuild project. Used for IAM permissions. Required when enable_build_stage is true." type = string + default = null validation { - condition = can(regex("^arn:aws:codebuild:[a-z0-9-]+:[0-9]{12}:project/[a-zA-Z0-9_-]+$", var.codebuild_project_arn)) + condition = var.codebuild_project_arn == null || can(regex( + "^arn:aws:codebuild:[a-z0-9-]+:[0-9]{12}:project/[a-zA-Z0-9_-]+$", + var.codebuild_project_arn + )) error_message = "CodeBuild project ARN must be a valid ARN format." } } +# ----------------------------------------------------------------------------- +# Deploy Stage Configuration +# ----------------------------------------------------------------------------- + +variable "enable_deploy_stage" { + description = "Enable Deploy stage with CodeDeploy." + type = bool + default = true +} + +variable "codedeploy_applications" { + description = <<-EOT + List of CodeDeploy deployment configurations. + Each deployment should have: application_name, deployment_group_name, and optional action_name, namespace, run_order. + EOT + type = list(object({ + application_name = string + deployment_group_name = string + action_name = optional(string, null) + namespace = optional(string, null) + run_order = optional(number, null) + })) + default = [] +} + # ----------------------------------------------------------------------------- # Testing Configuration # ----------------------------------------------------------------------------- @@ -124,6 +254,43 @@ variable "mock_github_token" { sensitive = true } +# ----------------------------------------------------------------------------- +# Artifact Store Configuration +# ----------------------------------------------------------------------------- + +variable "create_artifact_bucket" { + description = "Create a new S3 bucket for artifacts. If false, use existing bucket specified in artifact_bucket_name." + type = bool + default = true +} + +variable "artifact_bucket_name" { + description = "Name of existing S3 bucket for artifacts. Required when create_artifact_bucket is false." + type = string + default = null +} + +# ----------------------------------------------------------------------------- +# Service Role Configuration +# ----------------------------------------------------------------------------- + +variable "create_service_role" { + description = "Create a new IAM service role for CodePipeline. If false, use existing role specified in service_role_arn." + type = bool + default = true +} + +variable "service_role_arn" { + description = "ARN of existing IAM role for CodePipeline. Required when create_service_role is false." + type = string + default = null + + validation { + condition = var.service_role_arn == null || can(regex("^arn:aws:iam::[0-9]{12}:role/.+$", var.service_role_arn)) + error_message = "Service role ARN must be a valid IAM role ARN." + } +} + # ----------------------------------------------------------------------------- # Security Configuration # -----------------------------------------------------------------------------