Skip to content

Commit

Permalink
Merge pull request #15 from appvia/support-multiple-env-statefiles
Browse files Browse the repository at this point in the history
Support multiple env statefiles
  • Loading branch information
KashifSaadat authored May 17, 2024
2 parents d6ba967 + 5ff99b2 commit 6bf9abc
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 37 deletions.
20 changes: 8 additions & 12 deletions modules/role/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,11 @@ No modules.

| Name | Type |
|------|------|
| [aws_iam_policy.tfstate_apply](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
| [aws_iam_policy.tfstate_plan](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
| [aws_iam_policy.tfstate_remote](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
| [aws_iam_role.ro](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
| [aws_iam_role.rw](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
| [aws_iam_role.sr](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
| [aws_iam_role_policy_attachment.ro](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
| [aws_iam_role_policy_attachment.rw](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
| [aws_iam_role_policy_attachment.tfstate_apply](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
| [aws_iam_role_policy_attachment.tfstate_plan](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
| [aws_iam_role_policy_attachment.tfstate_remote](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source |
| [aws_iam_openid_connect_provider.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_openid_connect_provider) | data source |
| [aws_iam_policy_document.base](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
Expand All @@ -136,24 +130,26 @@ No modules.
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_additional_audiences"></a> [additional\_audiences](#input\_additional\_audiences) | Additional audiences to be allowed in the OIDC federation mapping | `list(string)` | `[]` | no |
| <a name="input_common_provider"></a> [common\_provider](#input\_common\_provider) | The name of a common OIDC provider to be used as the trust for the role | `string` | `""` | no |
| <a name="input_custom_provider"></a> [custom\_provider](#input\_custom\_provider) | An object representing an `aws_iam_openid_connect_provider` resource | <pre>object({<br> url = string<br> audiences = list(string)<br> subject_reader_mapping = string<br> subject_branch_mapping = string<br> subject_tag_mapping = string<br> })</pre> | `null` | no |
| <a name="input_common_provider"></a> [common\_provider](#input\_common\_provider) | The name of a common OIDC provider to be used as the trust for the role | `string` | `"github"` | no |
| <a name="input_custom_provider"></a> [custom\_provider](#input\_custom\_provider) | An object representing an `aws_iam_openid_connect_provider` resource | <pre>object({<br> url = string<br> audiences = list(string)<br> subject_reader_mapping = string<br> subject_branch_mapping = string<br> subject_env_mapping = string<br> subject_tag_mapping = string<br> })</pre> | `null` | no |
| <a name="input_description"></a> [description](#input\_description) | Description of the role being created | `string` | n/a | yes |
| <a name="input_force_detach_policies"></a> [force\_detach\_policies](#input\_force\_detach\_policies) | Flag to force detachment of policies attached to the IAM role. | `bool` | `null` | no |
| <a name="input_name"></a> [name](#input\_name) | Name of the role to create | `string` | n/a | yes |
| <a name="input_permission_boundary"></a> [permission\_boundary](#input\_permission\_boundary) | The name of the policy that is used to set the permissions boundary for the IAM role | `string` | n/a | yes |
| <a name="input_protected_branch"></a> [protected\_branch](#input\_protected\_branch) | The name of the protected branch under which the read-write role can be assumed | `string` | `"main"` | no |
| <a name="input_protected_tag"></a> [protected\_tag](#input\_protected\_tag) | The name of the protected tag under which the read-write role can be assume | `string` | `"*"` | no |
| <a name="input_permission_boundary"></a> [permission\_boundary](#input\_permission\_boundary) | The name of the policy that is used to set the permissions boundary for the IAM role | `string` | `null` | no |
| <a name="input_permission_boundary_arn"></a> [permission\_boundary\_arn](#input\_permission\_boundary\_arn) | The full ARN of the permission boundary to attach to the role | `string` | `null` | no |
| <a name="input_protected_by"></a> [protected\_by](#input\_protected\_by) | The branch, environment and/or tag to protect the role against | <pre>object({<br> branch = optional(string)<br> environment = optional(string)<br> tag = optional(string)<br> })</pre> | <pre>{<br> "branch": "main",<br> "environment": "production",<br> "tag": "*"<br>}</pre> | no |
| <a name="input_read_only_inline_policies"></a> [read\_only\_inline\_policies](#input\_read\_only\_inline\_policies) | Inline policies map with policy name as key and json as value. | `map(string)` | `{}` | no |
| <a name="input_read_only_max_session_duration"></a> [read\_only\_max\_session\_duration](#input\_read\_only\_max\_session\_duration) | The maximum session duration (in seconds) that you want to set for the specified role | `number` | `null` | no |
| <a name="input_read_only_policy_arns"></a> [read\_only\_policy\_arns](#input\_read\_only\_policy\_arns) | List of IAM policy ARNs to attach to the read-only role | `list(string)` | `[]` | no |
| <a name="input_read_write_inline_policies"></a> [read\_write\_inline\_policies](#input\_read\_write\_inline\_policies) | Inline policies map with policy name as key and json as value. | `map(string)` | `{}` | no |
| <a name="input_read_write_max_session_duration"></a> [read\_write\_max\_session\_duration](#input\_read\_write\_max\_session\_duration) | The maximum session duration (in seconds) that you want to set for the specified role | `number` | `null` | no |
| <a name="input_read_write_policy_arns"></a> [read\_write\_policy\_arns](#input\_read\_write\_policy\_arns) | List of IAM policy ARNs to attach to the read-write role | `list(string)` | `[]` | no |
| <a name="input_repository"></a> [repository](#input\_repository) | List of repositories to be allowed i nthe OIDC federation mapping | `string` | n/a | yes |
| <a name="input_region"></a> [region](#input\_region) | The region in which the role will be used (defaulting to the provider region) | `string` | `null` | no |
| <a name="input_repository"></a> [repository](#input\_repository) | List of repositories to be allowed in the OIDC federation mapping | `string` | n/a | yes |
| <a name="input_role_path"></a> [role\_path](#input\_role\_path) | Path under which to create IAM role. | `string` | `null` | no |
| <a name="input_shared_repositories"></a> [shared\_repositories](#input\_shared\_repositories) | List of repositories to provide read access to the remote state | `list(string)` | `[]` | no |
| <a name="input_tags"></a> [tags](#input\_tags) | Tags to apply resoures created by this module | `map(string)` | n/a | yes |
| <a name="input_tf_state_suffix"></a> [tf\_state\_suffix](#input\_tf\_state\_suffix) | A suffix for the terraform statefile, e.g. <repo>-<tf\_state\_suffix>.tfstate | `string` | `""` | no |

## Outputs

Expand Down
22 changes: 22 additions & 0 deletions modules/role/checks.tf
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,28 @@ check "provider_config" {
}
}

check "protected_by_config" {
assert {
condition = !(var.protected_by.branch == null && var.protected_by.environment == null && var.protected_by.tag == null)
error_message = "At least one of 'protected_by.branch', 'protected_by.environment', or 'protected_by.tag' must be specified"
}

assert {
condition = !(var.protected_by.branch == "")
error_message = "'protected_by.branch' must not be an empty string"
}

assert {
condition = !(var.protected_by.environment == "")
error_message = "'protected_by.environment' must not be an empty string"
}

assert {
condition = !(var.protected_by.tag == "")
error_message = "'protected_by.tag' must not be an empty string"
}
}

check "policy_config" {
assert {
condition = !(length(var.read_only_policy_arns) == 0 && length(var.read_only_inline_policies) == 0)
Expand Down
10 changes: 5 additions & 5 deletions modules/role/locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ locals {

subject_reader_mapping = "repo:{repo}:*"
subject_branch_mapping = "repo:{repo}:ref:refs/heads/{ref}"
subject_env_mapping = "repo:{repo}:environment:{env}"
subject_tag_mapping = "repo:{repo}:ref:refs/tags/{ref}"
}

Expand All @@ -37,16 +38,15 @@ locals {
}

locals {
selected_provider = coalesce(
var.custom_provider,
lookup(local.common_providers, var.common_provider, null),
)
common_provider = lookup(local.common_providers, var.common_provider, null)
selected_provider = var.custom_provider != null ? var.custom_provider : local.common_provider

# Extract just the repository name part of the full path
repo_name = element(split("/", var.repository), length(split("/", var.repository)) - 1)

# Keys to search for in the subject mapping template
template_keys_regex = "{(repo|type|ref)}"
template_keys_regex = "{(repo|type|ref|env)}"
# The prefix for the terraform state key in the S3 bucket
tf_state_prefix = format("%s-%s", local.account_id, local.region)
tf_state_suffix = var.tf_state_suffix != "" ? format("-%s", var.tf_state_suffix) : ""
}
23 changes: 15 additions & 8 deletions modules/role/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -101,23 +101,30 @@ data "aws_iam_policy_document" "rw" {
condition {
test = "StringLike"
variable = format("%s:sub", trimprefix(local.selected_provider.url, "https://"))
values = [
format(replace(local.selected_provider.subject_branch_mapping, format("/%s/", local.template_keys_regex), "%s"), [
values = compact([
var.protected_by.branch != null ? format(replace(local.selected_provider.subject_branch_mapping, format("/%s/", local.template_keys_regex), "%s"), [
for v in flatten(regexall(local.template_keys_regex, local.selected_provider.subject_branch_mapping)) : {
repo = var.repository
type = "branch"
ref = var.protected_branch
ref = var.protected_by.branch
}[v]
]...) : "",

var.protected_by.environment != null ? format(replace(local.selected_provider.subject_env_mapping, format("/%s/", local.template_keys_regex), "%s"), [
for v in flatten(regexall(local.template_keys_regex, local.selected_provider.subject_env_mapping)) : {
repo = var.repository
env = var.protected_by.environment
}[v]
]...),
]...) : "",

format(replace(local.selected_provider.subject_tag_mapping, format("/%s/", local.template_keys_regex), "%s"), [
var.protected_by.tag != null ? format(replace(local.selected_provider.subject_tag_mapping, format("/%s/", local.template_keys_regex), "%s"), [
for v in flatten(regexall(local.template_keys_regex, local.selected_provider.subject_tag_mapping)) : {
repo = var.repository
type = "tag"
ref = var.protected_tag
ref = var.protected_by.tag
}[v]
]...)
]
]...) : ""
])
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions modules/role/policies.tf
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ data "aws_iam_policy_document" "base" {
]

resources = [
format("arn:aws:s3:::%s-tfstate/%s.tfstate", local.tf_state_prefix, local.repo_name)
format("arn:aws:s3:::%s-tfstate/%s%s.tfstate", local.tf_state_prefix, local.repo_name, local.tf_state_suffix),
]
}
}
Expand Down Expand Up @@ -56,7 +56,7 @@ data "aws_iam_policy_document" "tfstate_apply" {
]

resources = [
format("arn:aws:s3:::%s-tfstate/%s.tfstate", local.tf_state_prefix, local.repo_name)
format("arn:aws:s3:::%s-tfstate/%s%s.tfstate", local.tf_state_prefix, local.repo_name, local.tf_state_suffix)
]
}
}
Expand Down
31 changes: 21 additions & 10 deletions modules/role/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ variable "custom_provider" {
audiences = list(string)
subject_reader_mapping = string
subject_branch_mapping = string
subject_env_mapping = string
subject_tag_mapping = string
})

Expand All @@ -39,9 +40,15 @@ variable "additional_audiences" {
description = "Additional audiences to be allowed in the OIDC federation mapping"
}

variable "tf_state_suffix" {
type = string
default = ""
description = "A suffix for the terraform statefile, e.g. <repo>-<tf_state_suffix>.tfstate"
}

variable "repository" {
type = string
description = "List of repositories to be allowed i nthe OIDC federation mapping"
description = "List of repositories to be allowed in the OIDC federation mapping"
}

variable "shared_repositories" {
Expand All @@ -50,16 +57,20 @@ variable "shared_repositories" {
description = "List of repositories to provide read access to the remote state"
}

variable "protected_branch" {
type = string
default = "main"
description = "The name of the protected branch under which the read-write role can be assumed"
}
variable "protected_by" {
type = object({
branch = optional(string)
environment = optional(string)
tag = optional(string)
})

variable "protected_tag" {
type = string
default = "*"
description = "The name of the protected tag under which the read-write role can be assume"
default = {
branch = "main"
environment = "production"
tag = "*"
}

description = "The branch, environment and/or tag to protect the role against"
}

variable "role_path" {
Expand Down

0 comments on commit 6bf9abc

Please sign in to comment.