diff --git a/.github/workflows/commit-to-pr.yaml b/.github/workflows/commit-to-pr.yaml index 2bd9dc0..904c5fb 100644 --- a/.github/workflows/commit-to-pr.yaml +++ b/.github/workflows/commit-to-pr.yaml @@ -20,7 +20,7 @@ jobs: validation: strategy: matrix: - folder: ["add", "folders", "here"] + folder: ["./", "examples/step_function"] name: Terraform validate for ${{ matrix.folder }} runs-on: ubuntu-20.04 steps: @@ -41,7 +41,7 @@ jobs: linting: strategy: matrix: - folder: ["add", "folders", "here"] + folder: ["./", "examples/step_function"] name: Terraform lint for ${{ matrix.folder }} runs-on: ubuntu-20.04 steps: @@ -59,7 +59,7 @@ jobs: plan: strategy: matrix: - folder: ["add", "folders", "here"] + folder: ["examples/step_function"] name: Terraform plan for ${{ matrix.folder }} runs-on: ubuntu-20.04 needs: [validation, linting] diff --git a/.terraform-version b/.terraform-version new file mode 100644 index 0000000..8e03717 --- /dev/null +++ b/.terraform-version @@ -0,0 +1 @@ +1.5.1 \ No newline at end of file diff --git a/README.md b/README.md index 230c18a..e560d0f 100644 --- a/README.md +++ b/README.md @@ -42,11 +42,72 @@ The below documentation is intended to assist users in utilising the module, the the module itself, and the [examples](#examples) section which has examples of how to utilise the module. +## Requirements +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | ~> 1.5.0 | +| [aws](#requirement\_aws) | >= 5.61.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | 5.67.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [step\_function\_state\_machine](#module\_step\_function\_state\_machine) | terraform-aws-modules/step-functions/aws | 4.2.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_caller_identity.current_account](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy_document.attached_policies](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_region.current_region](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [application\_name](#input\_application\_name) | Name of the application utilising resource. | `string` | n/a | yes | +| [environment](#input\_environment) | Which environment this is being instantiated in. | `string` | n/a | yes | +| [raw\_state\_machines](#input\_raw\_state\_machines) | Data structure
---------------
A list of dictionaries, where each dictionary has the following attributes:

REQUIRED
---------
- template\_file : File path which this machine corresponds to
- template\_input : A dictionary of key/value pairs, outlining in detail the inputs needed for a template to be instantiated
- suffix : Friendly name for the state function
- iam\_policy\_statements : A list of dictionaries where each dictionary is an IAM statement defining glue job permissions
-- Each dictionary in this list must define the following attributes:
--- sid: Friendly name for the policy, no spaces or special characters allowed
--- actions: A list of IAM actions the state machine is allowed to perform
--- resources: Which resource(s) the state machine may perform the above actions against
--- conditions : An OPTIONAL list of dictionaries, which each defines:
---- test : Test condition for limiting the action
---- variable : Value to test
---- values : A list of strings, denoting what to test for


OPTIONAL
---------
- cloudwatch\_retention : How many days logs should be retained for in Cloudwatch, defaults to 90 |
list(
object({
template_file = string,
template_input = map(string),
suffix = string,
iam_policy_statements = list(
object({
sid = string,
actions = list(string),
resources = list(string),
conditions = optional(list(
object({
test : string,
variable : string,
values = list(string)
})
), [])
})
),
cloudwatch_retention = optional(number, 90)
})
)
| n/a | yes | + +## Outputs + +No outputs. ## Data structure - +``` +Data structure +--------------- +A list of dictionaries, where each dictionary has the following attributes: + +REQUIRED +--------- +- template_file : File path which this machine corresponds to +- template_input : A dictionary of key/value pairs, outlining in detail the inputs needed for a template to be instantiated +- suffix : Friendly name for the state function +- iam_policy_statements : A list of dictionaries where each dictionary is an IAM statement defining glue job permissions +-- Each dictionary in this list must define the following attributes: +--- sid: Friendly name for the policy, no spaces or special characters allowed +--- actions: A list of IAM actions the state machine is allowed to perform +--- resources: Which resource(s) the state machine may perform the above actions against +--- conditions : An OPTIONAL list of dictionaries, which each defines: +---- test : Test condition for limiting the action +---- variable : Value to test +---- values : A list of strings, denoting what to test for + + +OPTIONAL +--------- +- cloudwatch_retention : How many days logs should be retained for in Cloudwatch, defaults to 90gi +``` ## Examples See `examples` folder for an example setup. diff --git a/aws_iam_policy_document.tf b/aws_iam_policy_document.tf new file mode 100644 index 0000000..e0d1e45 --- /dev/null +++ b/aws_iam_policy_document.tf @@ -0,0 +1,79 @@ +locals { + actual_iam_policy_documents = { + for state_machine in var.raw_state_machines : + state_machine.suffix => { + statements = concat(state_machine.iam_policy_statements, local.barebones_statemachine_statements, + [ + { + sid = "ListOwnExecutions", + actions = [ + "states:ListExecutions" + ] + resources = [ + format( + "arn:aws:states:%s:%s:stateMachine:%s-%s-%s-stepfunction", + lower(data.aws_region.current_region.name), + lower(data.aws_caller_identity.current_account.id), + lower(var.environment), + lower(var.application_name), + lower(state_machine.suffix) + ) + ] + conditions = [] + }, + { + sid = "AllowCloudwatchStreamAccess", + actions = [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + resources = [ + "arn:aws:logs:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:log-group:/aws/stepfunction/${format("%s-%s-%s-stepfunction", var.environment, var.application_name, state_machine.suffix)}", + "arn:aws:logs:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:log-group:/aws/stepfunction/${format("%s-%s-%s-stepfunction", var.environment, var.application_name, state_machine.suffix)}:*" + ] + conditions = [] + }, + { + sid = "AllowCloudwatchLogDelivery", + actions = [ + "logs:CreateLogDelivery", + "logs:PutResourcePolicy", + "logs:UpdateLogDelivery", + "logs:DeleteLogDelivery", + "logs:DescribeResourcePolicies", + "logs:GetLogDelivery", + "logs:ListLogDeliveries", + "logs:DescribeLogGroups" + ], + resources = ["*"] + conditions = [] + } + ] + ) + } + } +} + +data "aws_iam_policy_document" "attached_policies" { + for_each = local.actual_iam_policy_documents + + dynamic "statement" { + for_each = each.value["statements"] + + content { + sid = statement.value["sid"] + actions = statement.value["actions"] + resources = statement.value["resources"] + + dynamic "condition" { + for_each = statement.value["conditions"] + + content { + test = condition.value["test"] + variable = condition.value["variable"] + values = condition.value["values"] + } + } + } + } +} \ No newline at end of file diff --git a/common_iam_policies.tf b/common_iam_policies.tf new file mode 100644 index 0000000..225051b --- /dev/null +++ b/common_iam_policies.tf @@ -0,0 +1,19 @@ +locals { + barebones_statemachine_statements = [ + { + sid = "BarebonesEventActionsForStatemachine" + actions = [ + "events:PutEvents", + "events:DescribeRule", + "events:PutRule", + "events:PutTargets" + ] + resources = [ + "arn:aws:events:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:rule/default/StepFunctionsGetEventsForECSTaskRule", + "arn:aws:events:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:rule/StepFunctionsGetEventsForECSTaskRule", + "arn:aws:events:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:event-bus/default" + ] + conditions = [] + } + ] +} \ No newline at end of file diff --git a/data.tf b/data.tf new file mode 100644 index 0000000..7ae4bae --- /dev/null +++ b/data.tf @@ -0,0 +1,5 @@ +# Get current region +data "aws_region" "current_region" {} + +# Retrieve the current AWS Account info +data "aws_caller_identity" "current_account" {} \ No newline at end of file diff --git a/examples/step_function/.terraform-version b/examples/step_function/.terraform-version new file mode 100644 index 0000000..8e03717 --- /dev/null +++ b/examples/step_function/.terraform-version @@ -0,0 +1 @@ +1.5.1 \ No newline at end of file diff --git a/examples/step_function/data.tf b/examples/step_function/data.tf new file mode 100644 index 0000000..7ae4bae --- /dev/null +++ b/examples/step_function/data.tf @@ -0,0 +1,5 @@ +# Get current region +data "aws_region" "current_region" {} + +# Retrieve the current AWS Account info +data "aws_caller_identity" "current_account" {} \ No newline at end of file diff --git a/examples/step_function/files/step-function.json b/examples/step_function/files/step-function.json new file mode 100644 index 0000000..852cd87 --- /dev/null +++ b/examples/step_function/files/step-function.json @@ -0,0 +1,11 @@ +{ + "Comment": "A Hello World example of the Amazon States Language using an AWS Lambda Function", + "StartAt": "HelloWorld", + "States": { + "HelloWorld": { + "Type": "Task", + "Resource": "${lambda-arn}", + "End": true + } + } +} \ No newline at end of file diff --git a/examples/step_function/locals.tf b/examples/step_function/locals.tf new file mode 100644 index 0000000..94f45ba --- /dev/null +++ b/examples/step_function/locals.tf @@ -0,0 +1,23 @@ +locals { + raw_state_machines = [ + { + suffix : "hello-world", + template_file : "${path.module}/files/step-function.json", + template_input : { + "lambda-arn" : "arn:aws:lambda:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:function:hello-world-function" + }, + iam_policy_statements : [ + { + sid : "AllowLambdaExecution", + actions : [ + "lambda:InvokeFunction", + "lambda:InvokeAsync", + ], + resources : [ + "arn:aws:lambda:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:function:hello-world-function" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/examples/step_function/main.tf b/examples/step_function/main.tf new file mode 100644 index 0000000..8a7561d --- /dev/null +++ b/examples/step_function/main.tf @@ -0,0 +1,21 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.61.0" + } + } + required_version = "~> 1.5.0" +} + +provider "aws" { + region = "eu-west-2" +} + +module "step_function" { + source = "github.com/sudoblark/sudoblark.terraform.module.aws.state_machine?ref=1.0.0" + + application_name = var.application_name + environment = var.environment + raw_state_machines = local.raw_state_machines +} \ No newline at end of file diff --git a/examples/step_function/variables.tf b/examples/step_function/variables.tf new file mode 100644 index 0000000..9ccf4b8 --- /dev/null +++ b/examples/step_function/variables.tf @@ -0,0 +1,15 @@ +variable "environment" { + description = "Which environment this is being instantiated in." + type = string + validation { + condition = contains(["dev", "test", "prod"], var.environment) + error_message = "Must be either dev, test or prod" + } + default = "prod" +} + +variable "application_name" { + description = "Name of the application utilising the resource resource." + type = string + default = "demo-app" +} \ No newline at end of file diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..f022823 --- /dev/null +++ b/main.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.61.0" + } + } + required_version = "~> 1.5.0" +} \ No newline at end of file diff --git a/state_machine.tf b/state_machine.tf new file mode 100644 index 0000000..194ff72 --- /dev/null +++ b/state_machine.tf @@ -0,0 +1,45 @@ +locals { + actual_state_machines = { + for state_machine in var.raw_state_machines : + state_machine.suffix => merge(state_machine, { + state_machine_definition = templatefile(state_machine.template_file, state_machine.template_input) + policy_json = data.aws_iam_policy_document.attached_policies[state_machine.suffix].json + state_machine_name = format("%s-%s-%s-stepfunction", var.environment, var.application_name, state_machine.suffix) + }) + } +} + +module "step_function_state_machine" { + for_each = local.actual_state_machines + + depends_on = [ + data.aws_iam_policy_document.attached_policies + ] + + + source = "terraform-aws-modules/step-functions/aws" + version = "4.2.0" + + name = each.value["state_machine_name"] + create_role = true + policy_jsons = [each.value["policy_json"]] + + definition = each.value["state_machine_definition"] + + logging_configuration = { + "include_execution_data" = true + "level" = "ALL" + } + cloudwatch_log_group_name = "/aws/vendedlogs/states/${each.value["state_machine_name"]}" + cloudwatch_log_group_retention_in_days = each.value["cloudwatch_retention"] + + service_integrations = { + stepfunction_Sync = { + # Set to true to use the default events (otherwise, set this to a list of ARNs; see the docs linked in locals.tf + # for more information). Without events permissions, you will get an error similar to this: + # Error: AccessDeniedException: 'arn:aws:iam::xxxx:role/step-functions-role' is not authorized to + # create managed-rule + events = true + } + } +} \ No newline at end of file diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..068ffe6 --- /dev/null +++ b/variables.tf @@ -0,0 +1,71 @@ +# Input variable definitions +variable "environment" { + description = "Which environment this is being instantiated in." + type = string + validation { + condition = contains(["dev", "test", "prod"], var.environment) + error_message = "Must be either dev, test or prod" + } +} + +variable "application_name" { + description = "Name of the application utilising resource." + type = string +} + +variable "raw_state_machines" { + description = <= 0) + ]) + error_message = "cloudwatch_retention for each state machine should be a valid integer greater than or equal to 0" + } +} \ No newline at end of file