Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions terraform/aws/codepipeline/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,17 +155,35 @@ No modules.
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_app"></a> [app](#input\_app) | Application name used for resource naming and SSM parameter paths. | `string` | n/a | yes |
| <a name="input_codebuild_project_arn"></a> [codebuild\_project\_arn](#input\_codebuild\_project\_arn) | ARN of the CodeBuild project. Used for IAM permissions. | `string` | n/a | yes |
| <a name="input_codebuild_project_name"></a> [codebuild\_project\_name](#input\_codebuild\_project\_name) | Name of the CodeBuild project to use in the Build stage. | `string` | n/a | yes |
| <a name="input_artifact_bucket_name"></a> [artifact\_bucket\_name](#input\_artifact\_bucket\_name) | Name of existing S3 bucket for artifacts. Required when create\_artifact\_bucket is false. | `string` | `null` | no |
| <a name="input_build_output_artifact_name"></a> [build\_output\_artifact\_name](#input\_build\_output\_artifact\_name) | Name of the build output artifact. | `string` | `"BuildArtifact"` | no |
| <a name="input_codebuild_project_arn"></a> [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 |
| <a name="input_codebuild_project_name"></a> [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 |
| <a name="input_codedeploy_applications"></a> [codedeploy\_applications](#input\_codedeploy\_applications) | List of CodeDeploy deployment configurations.<br/>Each deployment should have: application\_name, deployment\_group\_name, and optional action\_name, namespace, run\_order. | <pre>list(object({<br/> application_name = string<br/> deployment_group_name = string<br/> action_name = optional(string, null)<br/> namespace = optional(string, null)<br/> run_order = optional(number, null)<br/> }))</pre> | `[]` | no |
| <a name="input_codestar_connection_arn"></a> [codestar\_connection\_arn](#input\_codestar\_connection\_arn) | ARN of CodeStar connection for GitHub V2 integration. Required when source\_provider is CodeStarSourceConnection. | `string` | `null` | no |
| <a name="input_create_artifact_bucket"></a> [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 |
| <a name="input_create_service_role"></a> [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 |
| <a name="input_deploy_action_namespace"></a> [deploy\_action\_namespace](#input\_deploy\_action\_namespace) | Namespace for deploy action variables. | `string` | `"DeployVariables"` | no |
| <a name="input_detect_changes"></a> [detect\_changes](#input\_detect\_changes) | Whether to detect changes in the source repository for CodeStarSourceConnection. | `bool` | `false` | no |
| <a name="input_enable_build_stage"></a> [enable\_build\_stage](#input\_enable\_build\_stage) | Enable Build stage with CodeBuild. If false, pipeline goes directly from Source to Deploy. | `bool` | `false` | no |
| <a name="input_enable_deploy_stage"></a> [enable\_deploy\_stage](#input\_enable\_deploy\_stage) | Enable Deploy stage with CodeDeploy. | `bool` | `true` | no |
| <a name="input_env"></a> [env](#input\_env) | Environment name used for resource naming and SSM parameter paths. | `string` | n/a | yes |
| <a name="input_execution_mode"></a> [execution\_mode](#input\_execution\_mode) | Execution mode for V2 pipelines. Valid values: QUEUED, SUPERSEDED, PARALLEL | `string` | `"SUPERSEDED"` | no |
| <a name="input_github_branch"></a> [github\_branch](#input\_github\_branch) | GitHub branch to monitor for changes and trigger pipeline executions. | `string` | `"main"` | no |
| <a name="input_github_full_repository_id"></a> [github\_full\_repository\_id](#input\_github\_full\_repository\_id) | Full repository ID in format 'owner/repo' for CodeStarSourceConnection. | `string` | `null` | no |
| <a name="input_github_owner"></a> [github\_owner](#input\_github\_owner) | GitHub repository owner (organization or username). | `string` | n/a | yes |
| <a name="input_github_repo"></a> [github\_repo](#input\_github\_repo) | GitHub repository name. | `string` | n/a | yes |
| <a name="input_kms_key_id"></a> [kms\_key\_id](#input\_kms\_key\_id) | KMS key ID for S3 bucket encryption.<br/>If not provided, uses AWS-managed encryption (AES256).<br/>For enhanced security, provide a customer-managed KMS key ARN or alias.<br/>Examples:<br/>- Key ARN: arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012<br/>- Alias ARN: arn:aws:kms:us-east-1:123456789012:alias/my-key<br/>- Key ID: 12345678-1234-1234-1234-123456789012 | `string` | `null` | no |
| <a name="input_mock_account_id"></a> [mock\_account\_id](#input\_mock\_account\_id) | Mock AWS account ID to use when skip\_data\_source\_lookup is true. | `string` | `"123456789012"` | no |
| <a name="input_mock_github_token"></a> [mock\_github\_token](#input\_mock\_github\_token) | Mock GitHub token to use when skip\_data\_source\_lookup is true. | `string` | `"mock-token"` | no |
| <a name="input_output_artifact_format"></a> [output\_artifact\_format](#input\_output\_artifact\_format) | Output artifact format for CodeStarSourceConnection. Valid values: CODE\_ZIP, CODEBUILD\_CLONE\_REF | `string` | `"CODE_ZIP"` | no |
| <a name="input_pipeline_name"></a> [pipeline\_name](#input\_pipeline\_name) | Name of the CodePipeline. Used for resource naming and tagging. | `string` | n/a | yes |
| <a name="input_pipeline_type"></a> [pipeline\_type](#input\_pipeline\_type) | Pipeline type. Valid values: V1, V2 | `string` | `"V2"` | no |
| <a name="input_service_role_arn"></a> [service\_role\_arn](#input\_service\_role\_arn) | ARN of existing IAM role for CodePipeline. Required when create\_service\_role is false. | `string` | `null` | no |
| <a name="input_skip_data_source_lookup"></a> [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 |
| <a name="input_source_action_namespace"></a> [source\_action\_namespace](#input\_source\_action\_namespace) | Namespace for source action variables. | `string` | `"SourceVariables"` | no |
| <a name="input_source_output_artifact_name"></a> [source\_output\_artifact\_name](#input\_source\_output\_artifact\_name) | Name of the source output artifact. | `string` | `"SourceArtifact"` | no |
| <a name="input_source_provider"></a> [source\_provider](#input\_source\_provider) | Source provider type. Valid values: CodeStarSourceConnection, GitHub | `string` | `"CodeStarSourceConnection"` | no |
| <a name="input_tags"></a> [tags](#input\_tags) | A map of tags to add to all resources. | `map(string)` | `{}` | no |

## Outputs
Expand Down
176 changes: 142 additions & 34 deletions terraform/aws/codepipeline/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,45 @@ 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
}

# -----------------------------------------------------------------------------
# S3 Bucket for Pipeline Artifacts
# -----------------------------------------------------------------------------

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"
}
}

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 {
Expand All @@ -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
Expand All @@ -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({
Expand All @@ -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"
Expand All @@ -105,32 +120,57 @@ 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 = [
"codebuild:BatchGetBuilds",
"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"
Expand All @@ -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
Expand All @@ -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
}
14 changes: 7 additions & 7 deletions terraform/aws/codepipeline/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

# -----------------------------------------------------------------------------
Expand All @@ -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
}
15 changes: 10 additions & 5 deletions terraform/aws/codepipeline/tests/basic/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading