├── .gitignore ├── .gitlab-ci.yml ├── README.md ├── acm_certificate ├── README.md ├── main.tf ├── outputs.tf ├── variables.tf └── versions.tf ├── asg_lifecycle_notifications ├── README.md ├── main.tf └── versions.tf ├── asg_recycle ├── README.md ├── main.tf ├── schedule.tf ├── variables.tf └── versions.tf ├── bin └── lambda-utilities │ ├── lint.sh │ ├── requirements.txt │ └── test.sh ├── cloudwatch_dashboard_alb ├── README.md ├── example.png ├── main.tf └── versions.tf ├── cloudwatch_dashboard_rds ├── README.md ├── cloudwatch-rds-sample.png ├── main.tf └── versions.tf ├── config_fedramp_conformance ├── README.md ├── main.tf ├── variables.tf └── versions.tf ├── dnssec ├── README.md ├── main.tf ├── outputs.tf ├── variables.tf └── versions.tf ├── elb_access_logs_bucket ├── README.md ├── main.tf ├── outputs.tf ├── variables.tf └── versions.tf ├── elb_http_alerts ├── README.md ├── main.tf └── versions.tf ├── git2s3_artifacts ├── README.md ├── git2s3.template ├── main.tf ├── outputs.tf ├── variables.tf └── versions.tf ├── git2s3_sync ├── README.md ├── apigateway-webhook.json ├── apigateway.tf ├── buildspec.yml ├── codebuild.tf ├── kms.tf ├── lambda.tf ├── lambda │ ├── LICENSE.txt │ ├── NOTICE.txt │ └── lambda_function.py ├── main.tf ├── outputs.tf ├── s3-artifact.tf ├── s3-output.tf ├── secretsmanager-tls.tf ├── variables.tf └── versions.tf ├── guardduty ├── README.md ├── main.tf ├── outputs.tf ├── variables.tf └── versions.tf ├── iam_assumegroup ├── README.md ├── main.tf └── versions.tf ├── iam_assumerole ├── README.md ├── main.tf └── versions.tf ├── iam_masterassume ├── README.md ├── main.tf └── versions.tf ├── iam_masterusers ├── README.md ├── main.tf └── versions.tf ├── kinesis_destination ├── README.md ├── main.tf ├── outputs.tf ├── variables.tf └── versions.tf ├── kms_keymaker ├── README.md ├── main.tf ├── variables.tf └── versions.tf ├── kms_keymaker_multiregion_primary ├── README.md ├── cloudwatch.tf ├── kms.tf ├── main.tf ├── outputs.tf ├── sns.tf ├── variables.tf └── versions.tf ├── kms_keymaker_multiregion_replica ├── README.md ├── cloudwatch.tf ├── kms.tf ├── main.tf ├── sns.tf ├── variables.tf └── versions.tf ├── kms_log ├── README.md ├── cloudtrail-processor.tf ├── cloudtrail-requeue.tf ├── cloudwatch-processor.tf ├── cloudwatch.tf ├── event-processor.tf ├── kinesis.tf ├── main.tf ├── outputs.tf ├── slack-processor.tf ├── variables.tf └── versions.tf ├── lambda_alerts ├── README.md ├── main.tf ├── variables.tf └── versions.tf ├── lambda_function ├── README.md ├── iam.tf ├── main.tf ├── output.tf ├── trigger.tf ├── variables.tf └── versions.tf ├── lambda_insights ├── README.md ├── main.tf ├── output.tf ├── variables.tf └── versions.tf ├── lambda_layer ├── main.tf ├── outputs.tf └── variables.tf ├── lambda_pipeline ├── README.md ├── codebuild.tf ├── codepipeline.tf ├── main.tf ├── outputs.tf ├── variables.tf └── versions.tf ├── launch_template ├── README.md ├── main.tf ├── output.tf ├── variables.tf └── versions.tf ├── null_archive ├── README.md ├── main.tf ├── outputs.tf └── variables.tf ├── s3_batch_inventory ├── README.md ├── main.tf └── versions.tf ├── s3_bucket_block ├── README.md ├── main.tf └── versions.tf ├── s3_config ├── README.md ├── main.tf └── versions.tf ├── ses_dkim_r53 ├── README.md ├── main.tf └── versions.tf ├── slack_lambda ├── README.md ├── main.tf ├── outputs.tf ├── src │ ├── aws_health_event_message.json │ ├── aws_incident_manager_shift_message.json │ ├── cloudwatch_alarm_message.json │ ├── codepipeline_message.json │ ├── generic_message.json │ ├── slack_lambda.py │ └── slack_lambda_test.py ├── variables.tf └── versions.tf ├── slo_lambda ├── main.tf ├── outputs.tf ├── src │ ├── windowed_slo.py │ ├── windowed_slo_fixture_happy.json │ ├── windowed_slo_fixture_sad.json │ └── windowed_slo_test.py ├── variables.tf └── versions.tf ├── sqs_alerts ├── README.md ├── main.tf ├── variables.tf └── versions.tf ├── squid_cloudwatch_filters ├── README.md ├── main.tf ├── variables.tf └── versions.tf ├── ssm ├── README.md ├── main.tf ├── outputs.tf ├── variables.tf └── versions.tf ├── state_bucket ├── README.md ├── data.tf ├── main.tf ├── outputs.tf ├── variables.tf └── versions.tf ├── versions.tf └── vpc_flow_cloudwatch_filters ├── README.md ├── main.tf └── versions.tf /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__/ 3 | .pytest_cache/ 4 | /*/src/*.zip 5 | **/.zip 6 | env/ 7 | htmlcov 8 | .coverage 9 | coverage.xml 10 | tmp 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | 2 | 3 | .merge_request: 4 | rules: 5 | - if: $CI_PIPELINE_SOURCE == "merge_request_event" 6 | - if: $CI_COMMIT_BRANCH == "main" 7 | 8 | 9 | terraform-fmt: 10 | image: 11 | name: hashicorp/terraform 12 | entrypoint: [""] 13 | script: 14 | - terraform fmt -recursive -diff -check . 15 | rules: 16 | - !reference [.merge_request, rules] 17 | 18 | 19 | # Scanning top level modules to avoid extreme duplication 20 | tfsec-check-soft: 21 | image: aquasec/tfsec-ci:v1.26.3 22 | script: 23 | - tfsec --no-color --soft-fail . 24 | rules: 25 | - !reference [.merge_request, rules] 26 | 27 | # Add folders that should be vuln free and STAY THAT WAY 28 | tfsec-check-enforce: 29 | image: aquasec/tfsec-ci:v1.26.3 30 | script: 31 | - tfsec --no-color . 32 | rules: 33 | - !reference [.merge_request, rules] 34 | 35 | 36 | include: 37 | - template: Jobs/SAST-IaC.gitlab-ci.yml 38 | - template: Security/Secret-Detection.gitlab-ci.yml 39 | - template: Security/SAST.gitlab-ci.yml 40 | - template: Jobs/Dependency-Scanning.gitlab-ci.yml 41 | 42 | secret_detection: 43 | allow_failure: false 44 | variables: 45 | SECRET_DETECTION_EXCLUDED_PATHS: 'keys.example,config/artifacts.example,public/acuant/*/opencv.min.js,tmp/0.0.0.0-3000.key' 46 | SECRET_DETECTION_REPORT_FILE: 'gl-secret-detection-report.json' 47 | rules: 48 | - if: $SECRET_DETECTION_DISABLED 49 | when: never 50 | - if: '$CI_COMMIT_BRANCH || $CI_COMMIT_TAG' 51 | - if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main" 52 | variables: 53 | SECRET_DETECTION_LOG_OPTIONS: origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}..HEAD 54 | - if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME != "main" && $CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME == "main" 55 | variables: 56 | SECRET_DETECTION_LOG_OPTIONS: origin/${CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME}..HEAD 57 | before_script: 58 | - apk add --no-cache jq 59 | - git fetch origin --quiet 60 | script: 61 | - | 62 | if [ -z "$SECRET_DETECTION_LOG_OPTIONS" ]; then 63 | /analyzer run 64 | if [ -f "$SECRET_DETECTION_REPORT_FILE" ]; then 65 | # check if '{ "vulnerabilities": [], ..' is empty in the report file if it exists 66 | if [ "$(jq ".vulnerabilities | length" $SECRET_DETECTION_REPORT_FILE)" -gt 0 ]; then 67 | echo "Vulnerabilities detected. Please analyze the artifact $SECRET_DETECTION_REPORT_FILE produced by the 'secret-detection' job." 68 | exit 80 69 | fi 70 | else 71 | echo "Artifact $SECRET_DETECTION_REPORT_FILE does not exist. The 'secret-detection' job likely didn't create one. Hence, no evaluation can be performed." 72 | fi 73 | else 74 | echo "Skipping because this is not a PR or is not targeting main" 75 | exit 0 76 | fi 77 | 78 | lambda-unit-tests: 79 | image: "$ECR_REGISTRY/ecr-public/docker/library/python:3.12.0-alpine3.18" 80 | script: 81 | - apk add --no-cache bash git 82 | - ./bin/lambda-utilities/test.sh --coverage --xml 83 | rules: 84 | - !reference [.merge_request, rules] 85 | - changes: 86 | - "**/src/*" 87 | artifacts: 88 | reports: 89 | coverage_report: 90 | coverage_format: cobertura 91 | path: coverage.xml 92 | junit: tmp/unittest.xml 93 | 94 | lambda-lint: 95 | image: "$ECR_REGISTRY/ecr-public/docker/library/python:3.12.0-alpine3.18" 96 | script: 97 | - apk add --no-cache bash git 98 | - ./bin/lambda-utilities/lint.sh 99 | rules: 100 | - !reference [.merge_request, rules] 101 | - changes: 102 | - "**/src/*" 103 | variables: 104 | ECR_REGISTRY: '${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com' 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # identity-terraform 2 | 3 | Terraform modules that may be useful to the community. 4 | -------------------------------------------------------------------------------- /acm_certificate/README.md: -------------------------------------------------------------------------------- 1 | # `acm_certificate` 2 | 3 | This Terraform module is a helper for issuing TLS certificates with Amazon Certificate Manager (ACM) using DNS (Route 53) validation. 4 | 5 | Terraform syntax makes it very tricky to get this right when you have conditional resources or multiple subject alternative names. 6 | 7 | ## Example 8 | 9 | ```hcl 10 | resource "aws_route53_zone" "default" { 11 | name = "example.com" 12 | } 13 | 14 | module "acm-cert" { 15 | source = "github.com/18F/identity-terraform//acm_certificate?ref=main" 16 | enabled = 1 17 | domain_name = "test.example.com" 18 | subject_alternative_names = [ 19 | "alt1.example.com", 20 | "alt2.exampel.com", 21 | ] 22 | validation_zone_id = "${aws_route53_zone.default.zone_id}" 23 | } 24 | 25 | resource "aws_alb_listener" "ssl" { 26 | certificate_arn = "${module.acm-cert.cert_arn}" 27 | ... 28 | } 29 | ``` 30 | 31 | ## Variables 32 | 33 | - `domain_name` - The primary domain name on the certificate 34 | - `enabled` — Like count, but for the whole module. 1 for True, 0 for False 35 | - `subject_alternative_names` — A list of additional names on the certificate 36 | - `validation_zone_id` — Route53 zone for validation CNAMEs 37 | - `validation_cname_ttl` — TTL for the validation CNAMEs 38 | 39 | ## Outputs 40 | 41 | - `cert_arn` — ARN of the issued ACM certificate 42 | - `finished_id` — Reference this output variable in order to depend on 43 | validation being complete. In TF 0.12 you can `depends_on` this variable 44 | directly, otherwise you may need a `null_resource`. 45 | 46 | -------------------------------------------------------------------------------- /acm_certificate/main.tf: -------------------------------------------------------------------------------- 1 | # Create the certificate with the specified SubjectAltNames 2 | resource "aws_acm_certificate" "main" { 3 | domain_name = var.domain_name 4 | subject_alternative_names = var.subject_alternative_names 5 | validation_method = "DNS" 6 | 7 | lifecycle { 8 | create_before_destroy = true 9 | } 10 | } 11 | 12 | # Create each validation CNAME 13 | resource "aws_route53_record" "validation-cnames" { 14 | for_each = { 15 | for item in aws_acm_certificate.main.domain_validation_options : item.domain_name => { 16 | name = item.resource_record_name 17 | record = item.resource_record_value 18 | type = item.resource_record_type 19 | } 20 | } 21 | 22 | name = each.value.name 23 | type = each.value.type 24 | zone_id = var.validation_zone_id 25 | records = [each.value.record] 26 | ttl = var.validation_cname_ttl 27 | allow_overwrite = true 28 | } 29 | 30 | # Synthetic Terraform resource that blocks on validation completion 31 | # You can depend_on this to wait for the ACM cert to be ready. 32 | resource "aws_acm_certificate_validation" "main" { 33 | certificate_arn = aws_acm_certificate.main.arn 34 | validation_record_fqdns = [for record in aws_route53_record.validation-cnames : record.fqdn] 35 | } 36 | 37 | -------------------------------------------------------------------------------- /acm_certificate/outputs.tf: -------------------------------------------------------------------------------- 1 | output "cert_arn" { 2 | description = "ARN of the issued ACM certificate" 3 | value = aws_acm_certificate.main.arn 4 | } 5 | 6 | output "finished_id" { 7 | description = "Reference this output in order to depend on validation being complete." 8 | value = aws_acm_certificate_validation.main.id 9 | } 10 | -------------------------------------------------------------------------------- /acm_certificate/variables.tf: -------------------------------------------------------------------------------- 1 | variable "domain_name" { 2 | description = "The primary name used on the issued TLS certificate" 3 | } 4 | 5 | variable "subject_alternative_names" { 6 | default = [] 7 | description = "A list of additional names to add to the certificate" 8 | } 9 | 10 | variable "validation_zone_id" { 11 | description = "Zone ID used to create the validation CNAMEs" 12 | } 13 | 14 | variable "validation_cname_ttl" { 15 | default = 300 16 | } 17 | -------------------------------------------------------------------------------- /acm_certificate/versions.tf: -------------------------------------------------------------------------------- 1 | ../versions.tf -------------------------------------------------------------------------------- /asg_lifecycle_notifications/README.md: -------------------------------------------------------------------------------- 1 | # ASG Lifecycle Notifications 2 | 3 | This module creates two default autoscaling group lifecycle notifications that 4 | hook into instance creation. They are intended to allow provision.sh to signal 5 | success or failure. 6 | -------------------------------------------------------------------------------- /asg_lifecycle_notifications/main.tf: -------------------------------------------------------------------------------- 1 | # Create two lifecycle hooks for the specified Auto Scaling Group that hook 2 | # into instance launches. By default, these will be called provision-main 3 | # and provision-private. 4 | 5 | variable "asg_name" { 6 | description = "Name of the auto scaling group that we're adding lifecycle hooks to." 7 | } 8 | 9 | variable "enabled" { 10 | description = "Whether to enable the lifecycle hooks. This is a hack around terraform not supporting count for modules." 11 | default = 1 12 | } 13 | 14 | variable "lifecycle_name_prefix" { 15 | description = "Prefix for the lifecycle hook names" 16 | default = "provision" 17 | } 18 | 19 | # TODO: change _enabled vars from int to bool 20 | variable "main_hook_enabled" { 21 | description = "Whether to create the $prefix-main lifecycle hook" 22 | default = 1 23 | } 24 | 25 | variable "private_hook_enabled" { 26 | description = "Whether to create the $prefix-private lifecycle hook" 27 | default = 1 28 | } 29 | 30 | variable "main_heartbeat_timeout" { 31 | description = "How long to wait before the main lifecycle hook times out" 32 | default = 1800 # 30 minutes 33 | } 34 | 35 | variable "private_heartbeat_timeout" { 36 | description = "How long to wait before the private lifecycle hook times out" 37 | default = 900 # 15 minutes 38 | } 39 | 40 | locals { 41 | main_hook_count = var.enabled * var.main_hook_enabled 42 | private_hook_count = var.enabled * var.private_hook_enabled 43 | } 44 | 45 | resource "aws_autoscaling_lifecycle_hook" "provision-private" { 46 | count = local.private_hook_count 47 | 48 | name = "${var.lifecycle_name_prefix}-private" 49 | autoscaling_group_name = var.asg_name 50 | default_result = "ABANDON" 51 | heartbeat_timeout = var.private_heartbeat_timeout 52 | lifecycle_transition = "autoscaling:EC2_INSTANCE_LAUNCHING" 53 | } 54 | 55 | resource "aws_autoscaling_lifecycle_hook" "provision-main" { 56 | count = local.main_hook_count 57 | 58 | name = "${var.lifecycle_name_prefix}-main" 59 | autoscaling_group_name = var.asg_name 60 | default_result = "ABANDON" 61 | heartbeat_timeout = var.main_heartbeat_timeout 62 | lifecycle_transition = "autoscaling:EC2_INSTANCE_LAUNCHING" 63 | } 64 | 65 | output "lifecycle_hook_names" { 66 | value = concat( 67 | aws_autoscaling_lifecycle_hook.provision-private.*.name, 68 | aws_autoscaling_lifecycle_hook.provision-main.*.name 69 | ) 70 | } 71 | 72 | -------------------------------------------------------------------------------- /asg_lifecycle_notifications/versions.tf: -------------------------------------------------------------------------------- 1 | ../versions.tf -------------------------------------------------------------------------------- /asg_recycle/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | schedule_map = var.custom_schedule == {} ? local.rotation_schedules : var.custom_schedule 3 | schedule = lookup( 4 | { for k, v in local.schedule_map : k => v if k == var.scale_schedule }, 5 | var.scale_schedule 6 | ) 7 | } 8 | 9 | resource "aws_autoscaling_schedule" "recycle_spinup" { 10 | for_each = toset(local.schedule["recycle_up"]) 11 | 12 | scheduled_action_name = join(".", [ 13 | "auto-recycle.spinup", 14 | index(local.schedule["recycle_up"], each.key) 15 | ]) 16 | min_size = var.min_size 17 | max_size = var.max_size 18 | desired_capacity = var.normal_desired * var.spinup_mult_factor 19 | recurrence = each.key 20 | time_zone = var.time_zone 21 | autoscaling_group_name = var.asg_name 22 | } 23 | 24 | resource "aws_autoscaling_schedule" "recycle_spindown" { 25 | for_each = toset(local.schedule["recycle_down"]) 26 | 27 | scheduled_action_name = join(".", [ 28 | "auto-recycle.spindown", 29 | index(local.schedule["recycle_down"], each.key) 30 | ]) 31 | min_size = var.min_size 32 | max_size = var.max_size 33 | desired_capacity = var.override_spindown_capacity == -1 ? ( 34 | var.normal_desired) : var.override_spindown_capacity 35 | recurrence = each.key 36 | time_zone = var.time_zone 37 | autoscaling_group_name = var.asg_name 38 | } 39 | 40 | # Spin down to 0 hosts, on a regular schedule. Depending upon selection, 41 | # do this either daily after working hours, weekly (same time), or nightly. 42 | # Follow a similar schedule to the recycle one above. 43 | # Ensure that ASG can spin down/up, instead of capping min/max at 0 hosts. 44 | 45 | resource "aws_autoscaling_schedule" "autozero_spinup" { 46 | for_each = toset(local.schedule["autozero_up"]) 47 | 48 | scheduled_action_name = join(".", [ 49 | "auto-zero.spinup", 50 | index(local.schedule["autozero_up"], each.key) 51 | ]) 52 | min_size = var.normal_min 53 | max_size = var.normal_max == 0 ? ( 54 | var.normal_min == 0 ? 1 : var.normal_min) : var.normal_max 55 | desired_capacity = ( 56 | var.normal_desired > var.normal_max || var.normal_desired < var.normal_min ? ( 57 | var.normal_max) : var.normal_desired 58 | ) 59 | recurrence = each.key 60 | time_zone = var.time_zone 61 | autoscaling_group_name = var.asg_name 62 | } 63 | 64 | resource "aws_autoscaling_schedule" "autozero_spindown" { 65 | for_each = toset(local.schedule["autozero_down"]) 66 | 67 | scheduled_action_name = join(".", [ 68 | "auto-zero.spindown", 69 | index(local.schedule["autozero_down"], each.key) 70 | ]) 71 | min_size = 0 72 | max_size = var.max_size 73 | desired_capacity = 0 74 | recurrence = each.key 75 | time_zone = var.time_zone 76 | autoscaling_group_name = var.asg_name 77 | } 78 | -------------------------------------------------------------------------------- /asg_recycle/schedule.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | rotation_schedules = { 3 | "nozero_norecycle" = { 4 | recycle_up = [] 5 | recycle_down = [] 6 | autozero_up = [] 7 | autozero_down = [] 8 | } 9 | "nozero_normal" = { 10 | recycle_up = ["0 5,11,17,23 * * *"] 11 | recycle_down = ["15 5,11,17,23 * * *"] 12 | autozero_up = [] 13 | autozero_down = [] 14 | } 15 | "nozero_business" = { 16 | recycle_up = ["0 17 * * 1-5"] 17 | recycle_down = ["15 17 * * 1-5"] 18 | autozero_up = [] 19 | autozero_down = [] 20 | } 21 | "dailyzero_norecycle" = { 22 | recycle_up = [] 23 | recycle_down = [] 24 | autozero_up = ["0 5 * * 1-5"] 25 | autozero_down = ["0 17 * * 1-5"] 26 | } 27 | "dailyzero_normal" = { 28 | recycle_up = ["0 11 * * 1-5"] 29 | recycle_down = ["15 11 * * 1-5"] 30 | autozero_up = ["0 5 * * 1-5"] 31 | autozero_down = ["0 17 * * 1-5"] 32 | } 33 | "dailyzero_business" = { 34 | recycle_up = [] 35 | recycle_down = [] 36 | autozero_up = ["0 5 * * 1-5"] 37 | autozero_down = ["0 17 * * 1-5"] 38 | } 39 | "nightlyzero_norecycle" = { 40 | recycle_up = [] 41 | recycle_down = [] 42 | autozero_up = ["0 5 * * 1-5"] 43 | autozero_down = ["0 21 * * 1-5"] 44 | } 45 | "nightlyzero_normal" = { 46 | recycle_up = ["0 11,17 * * 1-5"] 47 | recycle_down = ["15 11,17 * * 1-5"] 48 | autozero_up = ["0 5 * * 1-5"] 49 | autozero_down = ["0 21 * * 1-5"] 50 | } 51 | "nightlyzero_business" = { 52 | recycle_up = ["0 17 * * 1-5"] 53 | recycle_down = ["15 17 * * 1-5"] 54 | autozero_up = ["0 5 * * 1-5"] 55 | autozero_down = ["0 21 * * 1-5"] 56 | } 57 | "weeklyzero_norecycle" = { 58 | recycle_up = [] 59 | recycle_down = [] 60 | autozero_up = ["0 5 * * 1"] 61 | autozero_down = ["0 17 * * 5"] 62 | } 63 | "weeklyzero_normal" = { 64 | recycle_up = [ 65 | "0 11,17,23 * * 1", 66 | "0 5,11,17,23 * * 2-4", 67 | "0 5,11 * * 5", 68 | ] 69 | recycle_down = [ 70 | "15 11,17,23 * * 1", 71 | "15 5,11,17,23 * * 2-4", 72 | "15 5,11 * * 5", 73 | ] 74 | autozero_up = ["0 5 * * 1"] 75 | autozero_down = ["0 17 * * 5"] 76 | } 77 | "weeklyzero_business" = { 78 | recycle_up = ["0 17 * * 1-4"] 79 | recycle_down = ["15 17 * * 1-4"] 80 | autozero_up = ["0 5 * * 1"] 81 | autozero_down = ["0 17 * * 5"] 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /asg_recycle/variables.tf: -------------------------------------------------------------------------------- 1 | variable "asg_name" { 2 | description = "Name of the Auto Scaling Group to apply scheduled actions to." 3 | type = string 4 | } 5 | 6 | variable "override_spindown_capacity" { 7 | description = < /dev/null && pwd ) 26 | ROOT_DIR=$(git rev-parse --show-toplevel) 27 | 28 | cd "$SCRIPT_DIR" 29 | python3.12 -m venv env 30 | . env/bin/activate 31 | pip install -r requirements.txt > /dev/null 32 | 33 | files_to_format=$( 34 | git ls-files "$ROOT_DIR" --full-name | 35 | grep '.*\(src\).*\.py') 36 | 37 | cd "$ROOT_DIR" 38 | if [[ ${1:-''} == "--fix" ]]; then 39 | echo "$program_name: Fixing python code format" 40 | black $files_to_format 41 | else 42 | echo "$program_name: Checking python code format" 43 | black --diff --check $files_to_format 44 | fi 45 | -------------------------------------------------------------------------------- /bin/lambda-utilities/requirements.txt: -------------------------------------------------------------------------------- 1 | black==24.3.0 2 | boto3==1.34.67 3 | botocore==1.34.68 4 | certifi==2024.2.2 5 | cffi==1.16.0 6 | charset-normalizer==3.3.2 7 | click==8.1.7 8 | coverage==7.4.4 9 | cryptography==42.0.5 10 | idna==3.7 11 | Jinja2==3.1.4 12 | jmespath==1.0.1 13 | lxml==5.2.1 14 | MarkupSafe==2.1.5 15 | moto==5.0.0 16 | mypy-extensions==1.0.0 17 | packaging==24.0 18 | pathspec==0.12.1 19 | platformdirs==4.2.0 20 | pycparser==2.21 21 | pytest==7.3.1 22 | pytest-cov==5.0.0 23 | python-dateutil==2.8.2 24 | PyYAML==6.0.1 25 | requests==2.31.0 26 | responses==0.25.0 27 | s3transfer==0.10.0 28 | six==1.16.0 29 | tomli==2.0.1 30 | typing_extensions==4.10.0 31 | unittest-xml-reporting==3.2.0 32 | urllib3==1.26.18 33 | Werkzeug==3.0.6 34 | xmltodict==0.13.0 35 | -------------------------------------------------------------------------------- /bin/lambda-utilities/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Runs unit tests on python lambda functions 5 | 6 | coverage=false 7 | xml=false 8 | 9 | if [[ " $@ " =~ [[:space:]]--help[[:space:]] ]]; then 10 | cat < /dev/null && pwd ) 40 | ROOT_DIR=$(git rev-parse --show-toplevel) 41 | 42 | cd "$SCRIPT_DIR" 43 | python3.12 -m venv env 44 | . env/bin/activate 45 | pip install -r requirements.txt > /dev/null 46 | 47 | cd "$ROOT_DIR" 48 | 49 | runner="python3.12" 50 | if $coverage; then 51 | runner="coverage run" 52 | fi 53 | 54 | unittest="pytest" 55 | if $xml; then 56 | mkdir -p tmp 57 | unittest="$unittest --junitxml=tmp/unittest.xml" 58 | fi 59 | 60 | $runner -m $unittest $( 61 | git ls-files $ROOT_DIR --full-name -- | grep '_test.py' 62 | ) 63 | 64 | if $coverage; then 65 | coverage html 66 | 67 | if $xml; then 68 | coverage xml -o coverage.xml 69 | fi 70 | fi 71 | -------------------------------------------------------------------------------- /cloudwatch_dashboard_alb/README.md: -------------------------------------------------------------------------------- 1 | # CloudWatch Dashboard for ALB 2 | 3 | This module presents a standard CloudWatch dashboard appropriate for any ALB. 4 | The caller specifies an ALB and target group ARN, and the module takes care 5 | of creating a CloudWatch dashboard showing a variety of useful HTTP metrics. 6 | 7 | ## Example screenshot 8 | 9 | 10 | ![Screenshot of cloudwatch dashboard](./example.png) 11 | -------------------------------------------------------------------------------- /cloudwatch_dashboard_alb/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/identity-terraform/942f87eb77b11879183b637cb54270bcab2743be/cloudwatch_dashboard_alb/example.png -------------------------------------------------------------------------------- /cloudwatch_dashboard_alb/versions.tf: -------------------------------------------------------------------------------- 1 | ../versions.tf -------------------------------------------------------------------------------- /cloudwatch_dashboard_rds/README.md: -------------------------------------------------------------------------------- 1 | # CloudWatch Dashboard for RDS 2 | 3 | This module presents a standard CloudWatch dashboard appropriate for any RDS 4 | instance. The caller specifies the ARN of an RDS instance, and the module takes 5 | care of creating a CloudWatch dashboard showing a variety of useful metrics. 6 | 7 | ## Usage 8 | 9 | ### Example 10 | 11 | ```hcl 12 | module "rds_dashboard_my_database" { 13 | source = "github.com/18F/identity-terraform//cloudwatch_dashboard_rds?ref=main" 14 | 15 | dashboard_name = "my-rds-dashboard" 16 | 17 | region = "us-east-1" 18 | 19 | db_instance_identifier = "${aws_db_instance.my_rds_instance.id}" 20 | iops = "${aws_db_instance.my_rds_instance.iops}" 21 | 22 | # optional vertical annotations of important time markers 23 | vertical_annotations = < 2500", 28 | "value": "2018-01-25:05:00.000Z" 29 | } 30 | ] 31 | EOM 32 | 33 | } 34 | 35 | ``` 36 | 37 | ### Screenshot in action 38 | 39 | ![Cloudwatch RDS dashboard sample screenshot](./cloudwatch-rds-sample.png) 40 | -------------------------------------------------------------------------------- /cloudwatch_dashboard_rds/cloudwatch-rds-sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/identity-terraform/942f87eb77b11879183b637cb54270bcab2743be/cloudwatch_dashboard_rds/cloudwatch-rds-sample.png -------------------------------------------------------------------------------- /cloudwatch_dashboard_rds/versions.tf: -------------------------------------------------------------------------------- 1 | ../versions.tf -------------------------------------------------------------------------------- /config_fedramp_conformance/README.md: -------------------------------------------------------------------------------- 1 | # This Module is in progress for deprecation 2 | 3 | This module is being replaced with the recommended conformance packs. 4 | -------------------------------------------------------------------------------- /config_fedramp_conformance/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_config_config_rule" "cloudtrail_security_trail_enabled" { 2 | name = "fedramp-cloudtrail-security-trail-enabled" 3 | source { 4 | owner = "AWS" 5 | source_identifier = "CLOUDTRAIL_SECURITY_TRAIL_ENABLED" 6 | } 7 | } 8 | 9 | resource "aws_config_config_rule" "dynamodb_in_backup_plan" { 10 | name = "fedramp-dynamodb-in-backup-plan" 11 | source { 12 | owner = "AWS" 13 | source_identifier = "DYNAMODB_IN_BACKUP_PLAN" 14 | } 15 | } 16 | 17 | resource "aws_config_config_rule" "dynamodb_table_encrypted_kms" { 18 | name = "fedramp-dynamodb-table-encrypted-kms" 19 | source { 20 | owner = "AWS" 21 | source_identifier = "DYNAMODB_TABLE_ENCRYPTED_KMS" 22 | } 23 | scope { 24 | compliance_resource_types = [ 25 | "AWS::DynamoDB::Table" 26 | ] 27 | } 28 | } 29 | 30 | resource "aws_config_config_rule" "ebs_in_backup_plan" { 31 | name = "fedramp-ebs-in-backup-plan" 32 | source { 33 | owner = "AWS" 34 | source_identifier = "EBS_IN_BACKUP_PLAN" 35 | } 36 | } 37 | 38 | resource "aws_config_config_rule" "efs_in_backup_plan" { 39 | name = "fedramp-efs-in-backup-plan" 40 | source { 41 | owner = "AWS" 42 | source_identifier = "EFS_IN_BACKUP_PLAN" 43 | } 44 | } 45 | 46 | resource "aws_config_config_rule" "elb_acm_certificate_required" { 47 | name = "fedramp-elb-acm-certificate-required" 48 | source { 49 | owner = "AWS" 50 | source_identifier = "ELB_ACM_CERTIFICATE_REQUIRED" 51 | } 52 | scope { 53 | compliance_resource_types = [ 54 | "AWS::ElasticLoadBalancing::LoadBalancer" 55 | ] 56 | } 57 | } 58 | 59 | resource "aws_config_config_rule" "emr_kerberos_enabled" { 60 | name = "fedramp-emr-kerberos-enabled" 61 | source { 62 | owner = "AWS" 63 | source_identifier = "EMR_KERBEROS_ENABLED" 64 | } 65 | } 66 | 67 | resource "aws_config_config_rule" "internet_gateway_authorized_vpc_only" { 68 | name = "fedramp-internet-gateway-authorized-vpc-only" 69 | source { 70 | owner = "AWS" 71 | source_identifier = "INTERNET_GATEWAY_AUTHORIZED_VPC_ONLY" 72 | } 73 | scope { 74 | compliance_resource_types = [ 75 | "AWS::EC2::InternetGateway" 76 | ] 77 | } 78 | } 79 | 80 | resource "aws_config_config_rule" "rds_in_backup_plan" { 81 | name = "fedramp-rds-in-backup-plan" 82 | source { 83 | owner = "AWS" 84 | source_identifier = "RDS_IN_BACKUP_PLAN" 85 | } 86 | } 87 | 88 | resource "aws_config_config_rule" "s3_account_level_public_access_blocks" { 89 | name = "fedramp-s3-account-level-public-access-blocks" 90 | source { 91 | owner = "AWS" 92 | source_identifier = "S3_ACCOUNT_LEVEL_PUBLIC_ACCESS_BLOCKS" 93 | } 94 | scope { 95 | compliance_resource_types = [ 96 | "AWS::S3::AccountPublicAccessBlock" 97 | ] 98 | } 99 | input_parameters = < tomap({ 6 | digest_algorithm = v.digest_algorithm_mnemonic, 7 | digest_value = v.digest_value, 8 | signing_algorithm = v.signing_algorithm_mnemonic, 9 | ds_record = v.ds_record 10 | }) 11 | }) 12 | } 13 | 14 | output "active_ds_value" { 15 | description = "DS value for the KSK marked as active" 16 | 17 | # This mess pulls DS for the key with a value of "active". 18 | # If no keys in var.dnssec_ksks have a value of "active" it will fail 19 | value = aws_route53_key_signing_key.dnssec[[for k, v in var.dnssec_ksks : k if v == "active"][0]].ds_record 20 | } 21 | 22 | -------------------------------------------------------------------------------- /dnssec/variables.tf: -------------------------------------------------------------------------------- 1 | variable "alarm_actions" { 2 | type = list(string) 3 | description = "A list of ARNs to notify via the CloudWatch Alarms, i.e. SNS topics." 4 | } 5 | 6 | variable "dnssec_ksk_max_days" { 7 | description = "Maximum allowed age of a DNSSEC KSK before triggering a CloudWatch alarm." 8 | type = number 9 | default = 366 10 | } 11 | 12 | variable "dnssec_ksks" { 13 | description = "Map of Key Signing Keys (KSKs) to provision for each hosted zone." 14 | # See the notes in the README for more information regarding the key rotation process! 15 | type = map(string) 16 | default = { 17 | # "2111005" = "old", 18 | "20211006" = "active" 19 | } 20 | } 21 | 22 | variable "dnssec_zone_name" { 23 | description = "Name of the Route53 DNS domain where DNSSEC signing will be enabled." 24 | type = string 25 | } 26 | 27 | variable "dnssec_zone_id" { 28 | description = "ID of the Route53 DNS domain where DNSSEC signing will be enabled." 29 | type = string 30 | } 31 | 32 | variable "dnssec_ksks_action_req_alarm_desc" { 33 | type = string 34 | description = < ~/.ssh/id_rsa 12 | - ls ~/.ssh/ 13 | - echo "Setting SSH config profile" 14 | - | 15 | cat > ~/.ssh/config < roles -> accounts, with each *element* being a map of the format below: 57 | 58 | ``` 59 | { 60 | "GROUP" = [ 61 | { 62 | "ROLE" = [ 63 | "ACCOUNT_TYPE" 64 | ] 65 | } 66 | ] 67 | } 68 | ``` 69 | `policy_depends_on` - If Terraform is dependent upon the policy ARNs to be _calculated_ outside of this module, 70 | it will be stuck in a circular dependency loop. Thus, the policy ARNs are created from the input map. 71 | However, `policy_depends_on` can be used to wait for those policies to ACTUALLY exist 72 | before attempting to create the policy attachments. 73 | 74 | ## Outputs 75 | 76 | - `group_names`: A list of the names of the newly-created groups. Reference this output in order to depend on group creation being complete. 77 | 78 | -------------------------------------------------------------------------------- /iam_assumegroup/main.tf: -------------------------------------------------------------------------------- 1 | # -- Variables -- 2 | variable "partition" { 3 | description = "which aws partition this is deployed in" 4 | type = string 5 | default = "aws" 6 | } 7 | 8 | variable "master_account_id" { 9 | description = "AWS account ID for the master account." 10 | } 11 | 12 | variable "group_role_map" { 13 | description = "Roles map for IAM groups, along with account types per role to grant access to." 14 | type = map(list(map(list(string)))) 15 | } 16 | 17 | variable "policy_depends_on" { 18 | description = < flatten([ 31 | for perm in perms : flatten([ 32 | for pair in setproduct(keys(perm), flatten([values(perm)])) : join("", [pair[1], "Assume", pair[0]]) 33 | ]) 34 | ]) 35 | } 36 | ) 37 | } 38 | 39 | # -- Resources -- 40 | 41 | resource "aws_iam_group" "iam_group" { 42 | for_each = var.group_role_map 43 | 44 | name = each.key 45 | } 46 | 47 | resource "aws_iam_policy_attachment" "group_policy" { 48 | for_each = local.role_group_map 49 | 50 | name = each.key 51 | groups = each.value 52 | policy_arn = "arn:${var.partition}:iam::${var.master_account_id}:policy/${each.key}" 53 | 54 | depends_on = [ 55 | var.policy_depends_on, 56 | aws_iam_group.iam_group 57 | ] 58 | } 59 | 60 | # -- Outputs -- 61 | 62 | output "group_names" { 63 | description = "Reference this output in order to depend on group creation being complete." 64 | value = values(aws_iam_group.iam_group)[*]["name"] 65 | } 66 | -------------------------------------------------------------------------------- /iam_assumegroup/versions.tf: -------------------------------------------------------------------------------- 1 | ../versions.tf -------------------------------------------------------------------------------- /iam_assumerole/README.md: -------------------------------------------------------------------------------- 1 | # `iam_assumerole` 2 | 3 | This Terraform module is designed to create all of the IAM resources necessary for cross-account AssumeRole access, via: 4 | 5 | - a role that can be assumed by any IAM user in a 'master' account with access to assume that role (via user/group privileges) 6 | - one or more policy documents dictating access via `statement{}` blocks 7 | - one or more policies created from the policy document(s) 8 | - one or more attachments of the role to the policy(ies) 9 | - (optionally) additional attachment(s) if there are other IAM policies that should be attached to the assumable role 10 | 11 | Because Policy Documents have a size limit, it is often necessary to break policies up with multiple documents. Creating multiple policies and documents is possible using a `list(object)` variable in Terraform, which allows each object in the list to contain: 12 | 13 | 1. the policy name 14 | 2. the policy description 15 | 3. the policy document statement(s) 16 | 17 | ## Example 18 | 19 | ```hcl 20 | locals { 21 | custom_iam_policies = [ 22 | aws_iam_policy.rds_delete_prevent.arn, 23 | aws_iam_policy.region_restriction.arn, 24 | ] 25 | master_assumerole_policy = data.aws_iam_policy_document.master_account_assumerole.json 26 | } 27 | 28 | module "billing-assumerole" { 29 | source = "github.com/18F/identity-terraform//iam_assumerole?ref=main" 30 | 31 | role_name = "BillingReadOnly" 32 | enabled = var.iam_billing_enabled 33 | master_assumerole_policy = local.master_assumerole_policy 34 | custom_iam_policies = local.custom_iam_policies 35 | 36 | iam_policies = [ 37 | { 38 | policy_name = "BillingReadOnly" 39 | policy_description = "Policy for reporting group read-only access to Billing ui" 40 | policy_document = [ 41 | { 42 | sid = "BillingReadOnly" 43 | effect = "Allow" 44 | actions = [ 45 | "aws-portal:ViewBilling", 46 | ] 47 | resources = [ 48 | "*", 49 | ] 50 | }, 51 | ] 52 | }, 53 | ] 54 | } 55 | ``` 56 | 57 | ## Variables 58 | 59 | - `enabled` - **bool**: Whether or not to create the role + policy + attachments. Used when declaring a role via a Terraform template which is NOT used across all accounts. Defaults to _true_. 60 | - `role_name` - **string**: Name of the IAM role to be created. 61 | - `role_duration` - **number**: Value of the `max_session_duration` for the role, in seconds. Defaults to _43200_ (12 hours). 62 | - `master_assumerole_policy` - **object**: JSON object of the policy document to attach to the role allowing AssumeRole access from a master account. Pass in using `data.aws_iam_policy_document..json` as shown in the example above. 63 | - `custom_iam_policies` - **list**: Names of any additional IAM policies to attach to the role. 64 | - `iam_policies` - **list(object)**: List of objects, each of which contains: 65 | - `policy_name` - **string**: Name of the IAM policy to be created. 66 | - `policy_description` - **string**: Description of the IAM policy. 67 | - `policy_document` - **list(object)**: List of Statements included in the policy document. Each _object_ in the list should include the contents of a Statement, i.e. the `sid`, `effect`, `actions`, and `resources`. 68 | - `permissions_boundary_policy_arn` - **string**: ARN of an existing IAM policy (from another module/source) which will be used as the Permissions Boundary for the IAM role. -------------------------------------------------------------------------------- /iam_assumerole/versions.tf: -------------------------------------------------------------------------------- 1 | ../versions.tf -------------------------------------------------------------------------------- /iam_masterassume/README.md: -------------------------------------------------------------------------------- 1 | # `iam_masterassume` 2 | 3 | This Terraform module creates IAM policies, and associated policy documents, allowing groups/users in a 'master' AWS account to assume roles in other accounts. 4 | 5 | ## Account Type 6 | 7 | The "account type" concept allows for more granular control of permissions for IAM groups across multiple categories -- or "types" -- of AWS accounts. As an example: 8 | 9 | An organization has the following accounts: 10 | 11 | 1. Dev / Infrastructure 12 | 2. Dev / S3 Buckets 13 | 3. Prod / Infrastructure 14 | 4. Prod / S3 Buckets 15 | 5. Master 16 | 17 | Each account has the same list of roles, e.g.: FullAdmin, PowerUser, ReadOnly, SOCAdmin. 18 | 19 | 1. 3 AWS accounts for development 20 | 2. 2 accounts for production 21 | 3. 1 account for 'master' 22 | 23 | A module can be added to the Terraform configuration for each "type" of account, which will create all necessary IAM policies (and documents) to allow AssumeRole access for each Role to all accounts within that "type". 24 | 25 | ## Example 26 | 27 | ```hcl 28 | module "assume_roles_prod" { 29 | source = "github.com/18F/identity-terraform//iam_masterassume?ref=main" 30 | 31 | role_list = [ 32 | "FullAdministrator", 33 | "PowerUser", 34 | "ReadOnly", 35 | "BillingReadOnly", 36 | "ReportsReadOnly", 37 | "KMSAdministrator", 38 | "SOCAdministrator", 39 | ] 40 | account_type = "Prod" 41 | account_numbers = [ 42 | "111111111111", 43 | "222222222222" 44 | ] 45 | } 46 | 47 | ``` 48 | 49 | ## Variables 50 | 51 | - `account_type`: The "type", aka "category", of AWS account(s) that this module will create policies for. 52 | - `account_numbers`: A list of AWS account number(s) within the `account_type` category. 53 | - `role_list`: A list of the roles available to be assumed from within the account(s). 54 | 55 | ## Outputs 56 | 57 | - `policy_arns`: A list of the ARNs of the newly-created policies. Reference this output in order to depend on policy creation being complete. 58 | 59 | -------------------------------------------------------------------------------- /iam_masterassume/main.tf: -------------------------------------------------------------------------------- 1 | # -- Variables -- 2 | variable "partition" { 3 | description = "which aws partition this is deployed in" 4 | type = string 5 | default = "aws" 6 | } 7 | 8 | variable "aws_account_types" { 9 | description = "Mapping of account types to lists of AWS account numbers." 10 | type = map(list(string)) 11 | } 12 | 13 | variable "role_list" { 14 | description = "Type/name of the Assumable role(s). Should correspond to actual role name(s) in the account(s) listed." 15 | type = list(any) 16 | } 17 | 18 | variable "username_tag" { 19 | type = string 20 | default = "ec2_username" 21 | description = < [ 32 | for pair in setproduct(var.aws_account_types[rolepair[0]], [rolepair[1]]) : 33 | join("", ["arn:${var.partition}:iam::", pair[0], ":role/", pair[1]]) 34 | ] 35 | } 36 | } 37 | 38 | # -- Resources -- 39 | 40 | data "aws_iam_policy_document" "role_policy_doc" { 41 | for_each = local.role_expansion 42 | 43 | statement { 44 | sid = each.key 45 | effect = "Allow" 46 | actions = [ 47 | "sts:AssumeRole" 48 | ] 49 | resources = [ 50 | for arn in each.value : arn 51 | ] 52 | condition { 53 | test = "StringEquals" 54 | variable = "sts:RoleSessionName" 55 | 56 | values = [ 57 | "$${aws:username}", 58 | ] 59 | } 60 | condition { 61 | test = "Bool" 62 | variable = "aws:MultiFactorAuthPresent" 63 | 64 | values = [ 65 | "true", 66 | ] 67 | } 68 | } 69 | 70 | statement { 71 | sid = "${each.key}TagSessions" 72 | effect = "Allow" 73 | actions = [ 74 | "sts:TagSession" 75 | ] 76 | resources = [ 77 | for arn in each.value : arn 78 | ] 79 | condition { 80 | test = "Bool" 81 | variable = "aws:MultiFactorAuthPresent" 82 | 83 | values = [ 84 | "true", 85 | ] 86 | } 87 | 88 | condition { 89 | test = "StringEquals" 90 | variable = "aws:RequestTag/SSMSessionRunAs" 91 | 92 | values = [ 93 | "&{aws:PrincipalTag/${var.username_tag}}", 94 | ] 95 | } 96 | } 97 | } 98 | 99 | resource "aws_iam_policy" "account_role_policy" { 100 | for_each = local.role_expansion 101 | 102 | name = each.key 103 | path = "/" 104 | description = "Policy to allow user to assume ${split("Assume", each.key)[1]} role in ${split("Assume", each.key)[0]} account(s)." 105 | policy = data.aws_iam_policy_document.role_policy_doc[each.key].json 106 | } 107 | 108 | # -- Outputs -- 109 | 110 | output "policy_arns" { 111 | description = "Reference this output in order to depend on policy creation being complete." 112 | value = values(aws_iam_policy.account_role_policy)[*]["arn"] 113 | } 114 | -------------------------------------------------------------------------------- /iam_masterassume/versions.tf: -------------------------------------------------------------------------------- 1 | ../versions.tf -------------------------------------------------------------------------------- /iam_masterusers/README.md: -------------------------------------------------------------------------------- 1 | # `iam_masteruser` 2 | 3 | This Terraform module can be used to create one or more IAM users. 4 | Group memberships are assigned per-user for simpler user management. 5 | 6 | An IAM policy called ***ManageYourAccount*** will also be created, 7 | which is attached to all users in `user_map`. It allows each user 8 | basic access to configure their own account, i.e. updating their own 9 | password, adding an MFA device, etc., and sets an explicit **Deny** on 10 | numerous actions -- *including* `sts:AssumeRole` -- if the user does 11 | not have an MFA device configured. 12 | 13 | ## Example 14 | 15 | ```hcl 16 | module "our_cool_master_users" { 17 | source = "github.com/18F/identity-terraform//iam_masterusers?ref=main" 18 | 19 | user_map = { 20 | 'fred.flinstone' = ['development'], 21 | 'barny.rubble' = ['devops'], 22 | 'space.ghost' = ['devops', 'host'], 23 | } 24 | } 25 | ``` 26 | 27 | ## Variables 28 | 29 | `user_map` - Map with user name as key and a list of group memberships as the value. 30 | `group_depends_on` - Can be used to wait for the groups in `user_map` to ACTUALLY exist 31 | before attempting to create the group memberships. 32 | -------------------------------------------------------------------------------- /iam_masterusers/versions.tf: -------------------------------------------------------------------------------- 1 | ../versions.tf -------------------------------------------------------------------------------- /kinesis_destination/README.md: -------------------------------------------------------------------------------- 1 | # `kinesis_destination` 2 | 3 | This module creates and manages a CloudWatch Logs Destination, which is used to send data to an externally-created Kinesis resource (either a Data Stream or a Firehose delivery stream) in the same account. It also adds an IAM policy to the Destination so that CloudWatch Subscription Filters can be pointed to it (both individual and account-level Subscription Filters), allowing any number of log groups to send to the same Kinesis resource. 4 | 5 | Both the Kinesis resource _and_ the Destination(s) must be in the same _account_. However, this module may be called multiple times to create additional CloudWatch Logs Destinations in separate _regions_, if desired. 6 | 7 | ## Example 8 | 9 | ```hcl 10 | #### Kinesis Data Stream already created in us-west-2 #### 11 | 12 | # create Destination in us-west-2 that accepts Subscription Filters/Logs from two AWS accounts 13 | module "kinesis_stream_destination_uw2" { 14 | source = "github.com/18F/identity-terraform//kinesis_destination?ref=main" 15 | providers = { 16 | aws = aws.usw2 17 | } 18 | 19 | kinesis_arn = aws_kinesis_stream.logarchive.arn 20 | source_account_ids = ["111111111111", "222222222222"] 21 | } 22 | 23 | # create Destination in us-east-1 which points to Firehose in us-west-2, logging only one AWS account 24 | module "kinesis_firehose_destination_ue1" { 25 | source = "github.com/18F/identity-terraform//kinesis_destination?ref=main" 26 | providers = { 27 | aws = aws.use1 28 | } 29 | 30 | kinesis_arn = aws_kinesis_firehose_delivery_stream.logarchive.arn 31 | source_account_ids = ["111111111111"] 32 | } 33 | ``` 34 | 35 | ## Variables 36 | 37 | | Name | Type | Description | Required | Default | 38 | | --------------- | ------ | ------------------------------------------------------------------------------------------- | -------- | ------- | 39 | | `kinesis_arn` | string | ARN of the Kinesis resource (Firehose/Data Stream) that the Destination points to. | YES | N/A | 40 | | `source_account_ids` | list(string) | ID(s) of the AWS Account(s) where log data will be sent FROM. | YES | N/A | 41 | | `role_name` | string | Identifier string used to name the IAM role/policies and the CloudWatch Destination itself (uses `"cloudwatch-to-kinesis-${REGION}"` if not set) | NO | `""` | 42 | 43 | ## Outputs 44 | 45 | | Name | Description | Value | 46 | | ----- | ----- | ----- | 47 | | `kinesis_destination_arn` | ARN of the CloudWatch Logs Destination for the Kinesis resource. | `aws_cloudwatch_log_destination.kinesis.arn` | 48 | -------------------------------------------------------------------------------- /kinesis_destination/main.tf: -------------------------------------------------------------------------------- 1 | # Data Sources 2 | 3 | data "aws_caller_identity" "current" {} 4 | 5 | data "aws_region" "current" {} 6 | 7 | data "aws_iam_policy_document" "cloudwatch_assume" { 8 | statement { 9 | effect = "Allow" 10 | actions = ["sts:AssumeRole"] 11 | 12 | principals { 13 | type = "Service" 14 | identifiers = ["logs.${local.region}.amazonaws.com"] 15 | } 16 | 17 | condition { 18 | test = "StringLike" 19 | values = distinct(flatten([ 20 | formatlist("arn:aws:logs:${local.region}:%s:*", var.source_account_ids), 21 | "arn:aws:logs:${local.region}:${local.dest_acct_id}:*", 22 | ])) 23 | variable = "aws:SourceArn" 24 | } 25 | } 26 | } 27 | 28 | data "aws_iam_policy_document" "cloudwatch_kinesis_access" { 29 | statement { 30 | effect = "Allow" 31 | actions = [ 32 | strcontains( 33 | var.kinesis_arn, 34 | "arn:aws:kinesis" 35 | ) ? "kinesis:PutRecord" : "firehose:*" 36 | ] 37 | resources = [ 38 | var.kinesis_arn 39 | ] 40 | } 41 | } 42 | 43 | data "aws_iam_policy_document" "subscription_access" { 44 | statement { 45 | sid = "SubscriptionFilterAccess" 46 | actions = [ 47 | "logs:PutSubscriptionFilter", 48 | "logs:PutAccountPolicy" 49 | ] 50 | 51 | principals { 52 | type = "AWS" 53 | identifiers = var.source_account_ids 54 | } 55 | 56 | resources = [ 57 | aws_cloudwatch_log_destination.kinesis.arn 58 | ] 59 | } 60 | } 61 | 62 | # Resources 63 | 64 | resource "aws_iam_role" "cloudwatch_to_kinesis" { 65 | name = local.identifier_name 66 | assume_role_policy = data.aws_iam_policy_document.cloudwatch_assume.json 67 | } 68 | 69 | resource "aws_iam_role_policy" "cloudwatch_kinesis_access" { 70 | name = "${local.identifier_name}-access" 71 | role = aws_iam_role.cloudwatch_to_kinesis.id 72 | policy = data.aws_iam_policy_document.cloudwatch_kinesis_access.json 73 | } 74 | 75 | resource "aws_cloudwatch_log_destination" "kinesis" { 76 | name = local.identifier_name 77 | role_arn = aws_iam_role.cloudwatch_to_kinesis.arn 78 | target_arn = var.kinesis_arn 79 | } 80 | 81 | resource "aws_cloudwatch_log_destination_policy" "subscription_access" { 82 | destination_name = aws_cloudwatch_log_destination.kinesis.name 83 | access_policy = data.aws_iam_policy_document.subscription_access.json 84 | } 85 | -------------------------------------------------------------------------------- /kinesis_destination/outputs.tf: -------------------------------------------------------------------------------- 1 | output "kinesis_destination_arn" { 2 | description = "ARN of the CloudWatch Logs Destination for the Kinesis resource." 3 | value = aws_cloudwatch_log_destination.kinesis.arn 4 | } 5 | -------------------------------------------------------------------------------- /kinesis_destination/variables.tf: -------------------------------------------------------------------------------- 1 | # Locals 2 | 3 | locals { 4 | region = data.aws_region.current.name 5 | dest_acct_id = data.aws_caller_identity.current.account_id 6 | 7 | identifier_name = var.role_name == "" ? ( 8 | "cloudwatch-to-kinesis-${local.region}") : var.role_name 9 | } 10 | 11 | # Variables 12 | 13 | variable "kinesis_arn" { 14 | type = string 15 | description = <SQS (for multi-region support) 46 | resource "aws_sns_topic_subscription" "kms_ct_sqs" { 47 | topic_arn = aws_sns_topic.kms_events.arn 48 | protocol = "sqs" 49 | endpoint = var.sqs_queue_arn 50 | } 51 | 52 | # sets the receiver of the cloudwatch events 53 | # to the SNS topic 54 | resource "aws_cloudwatch_event_target" "decrypt" { 55 | rule = aws_cloudwatch_event_rule.decrypt.name 56 | target_id = "${var.env_name}-sns" 57 | arn = aws_sns_topic.kms_events.arn 58 | } 59 | 60 | # send Cloudwatch events to alarm SNS topic 61 | resource "aws_cloudwatch_event_target" "replicate" { 62 | rule = aws_cloudwatch_event_rule.replicate.name 63 | target_id = "${var.env_name}-sns" 64 | arn = var.alarm_sns_topic_arn 65 | } 66 | 67 | resource "aws_cloudwatch_event_target" "update_primary_region" { 68 | rule = aws_cloudwatch_event_rule.update_primary_region.name 69 | target_id = "${var.env_name}-sns" 70 | arn = var.alarm_sns_topic_arn 71 | } -------------------------------------------------------------------------------- /kms_keymaker_multiregion_primary/variables.tf: -------------------------------------------------------------------------------- 1 | variable "ec2_kms_arns" { 2 | default = [] 3 | type = list(string) 4 | description = "ARN(s) of EC2 roles permitted access to KMS" 5 | } 6 | 7 | variable "env_name" { 8 | type = string 9 | description = "Environment name" 10 | } 11 | 12 | variable "sqs_queue_arn" { 13 | type = string 14 | description = "ARN of the SQS queue used as the CloudWatch event target." 15 | } 16 | 17 | variable "alarm_sns_topic_arn" { 18 | type = string 19 | description = "ARN of the SNS topic used to send KMS alarms." 20 | } -------------------------------------------------------------------------------- /kms_keymaker_multiregion_primary/versions.tf: -------------------------------------------------------------------------------- 1 | ../versions.tf -------------------------------------------------------------------------------- /kms_keymaker_multiregion_replica/README.md: -------------------------------------------------------------------------------- 1 | # `kms_keymaker_multiregion_replica` 2 | 3 | This Terraform module will generate a replicated KMS key + alias for use by the Login.gov IdP application, along with a CloudWatch event monitor, and its target (an SNS topic/subscription). It is best utilized alongside the `kms_log` module, which creates the SQS queue (whose ARN is a required variable in this module) that SNS sends messages to. 4 | 5 | These resources were, previously, all included within `kms_log` in a more monolithic design; they have been split out separately to allow for multi-region KMS keys and associated resources. 6 | 7 | ## Example 8 | 9 | ```hcl 10 | module "kms_keymaker_replica_ue1" { 11 | source = "github.com/18F/identity-terraform//kms_keymaker_multiregion_replica?ref=main" 12 | 13 | providers = { 14 | aws = aws.use1 15 | } 16 | 17 | env_name = "testing" 18 | ec2_kms_arns = local.kms_arns 19 | sqs_queue_arn = module.kms_logging.kms-ct-events-queue 20 | primary_key_arn = module.kms_keymaker_uw2.multi_region_primary_key_arn 21 | } 22 | 23 | ## Variables 24 | 25 | `ec2_kms_arns` - ARN(s) of EC2 roles permitted access to KMS 26 | `env_name` - Environment name 27 | `sqs_queue_arn` - ARN of the SQS queue used as the CloudWatch event target 28 | `primary_key_arn` - ARN of the multi-region key to replicate 29 | -------------------------------------------------------------------------------- /kms_keymaker_multiregion_replica/cloudwatch.tf: -------------------------------------------------------------------------------- 1 | # cloudwatch event rule to capture cloudtrail kms decryption events 2 | # this filter will only capture events where the 3 | # encryption context is set and has the values of 4 | # password-digest or pii-encryption 5 | resource "aws_cloudwatch_event_rule" "decrypt" { 6 | name = "${var.env_name}-mr-decryption-events" 7 | description = "Capture decryption events" 8 | 9 | event_pattern = <SQS (for multi-region support) 41 | # resource "aws_sns_topic_subscription" "kms_ct_sqs" { 42 | # topic_arn = aws_sns_topic.kms_events.arn 43 | # protocol = "sqs" 44 | # endpoint = var.sqs_queue_arn 45 | # } 46 | 47 | # # sets the receiver of the cloudwatch events 48 | # # to the SNS topic 49 | # resource "aws_cloudwatch_event_target" "decrypt" { 50 | # rule = aws_cloudwatch_event_rule.decrypt.name 51 | # target_id = "${var.env_name}-sns" 52 | # arn = aws_sns_topic.kms_events.arn 53 | # } 54 | 55 | # #Send Cloudwatch Event to Alarm SNS Topic 56 | # resource "aws_cloudwatch_event_target" "replicate" { 57 | # rule = aws_cloudwatch_event_rule.replicate.name 58 | # target_id = "${var.env_name}-sns" 59 | # arn = var.alarm_sns_topic_arn 60 | # } 61 | 62 | # resource "aws_cloudwatch_event_target" "update_primary_region" { 63 | # rule = aws_cloudwatch_event_rule.update_primary_region.name 64 | # target_id = "${var.env_name}-sns" 65 | # arn = var.aws_sns_topic.kms_events.arn 66 | # } -------------------------------------------------------------------------------- /kms_keymaker_multiregion_replica/variables.tf: -------------------------------------------------------------------------------- 1 | variable "ec2_kms_arns" { 2 | default = [] 3 | type = list(string) 4 | description = "ARN(s) of EC2 roles permitted access to KMS" 5 | } 6 | 7 | variable "env_name" { 8 | type = string 9 | description = "Environment name" 10 | } 11 | 12 | variable "sqs_queue_arn" { 13 | type = string 14 | description = "ARN of the SQS queue used as the CloudWatch event target." 15 | } 16 | 17 | # variable "alarm_sns_topic_arn" { 18 | # type = string 19 | # description = "ARN of the SNS topic used to send KMS alarms." 20 | # } 21 | 22 | variable "primary_key_arn" { 23 | type = string 24 | description = "ARN of the multi-region kms key." 25 | } 26 | -------------------------------------------------------------------------------- /kms_keymaker_multiregion_replica/versions.tf: -------------------------------------------------------------------------------- 1 | ../versions.tf -------------------------------------------------------------------------------- /kms_log/cloudwatch-processor.tf: -------------------------------------------------------------------------------- 1 | data "aws_iam_policy_document" "cwprocessor" { 2 | statement { 3 | sid = "CreateLogGroupAndEvents" 4 | effect = "Allow" 5 | actions = [ 6 | "logs:CreateLogGroup", 7 | "logs:CreateLogStream", 8 | "logs:PutLogEvents", 9 | ] 10 | 11 | resources = [ 12 | aws_cloudwatch_log_group.cloudwatch_processor.arn, 13 | "${aws_cloudwatch_log_group.cloudwatch_processor.arn}:*" 14 | ] 15 | } 16 | statement { 17 | sid = "cwprocessorSNS" 18 | effect = "Allow" 19 | actions = [ 20 | "sns:Publish", 21 | ] 22 | 23 | resources = [ 24 | aws_sns_topic.kms_logging_events.arn, 25 | ] 26 | } 27 | statement { 28 | sid = "Kinesis" 29 | effect = "Allow" 30 | actions = [ 31 | "kinesis:GetShardIterator", 32 | "kinesis:GetRecords", 33 | "kinesis:DescribeStream", 34 | ] 35 | 36 | resources = [ 37 | aws_kinesis_stream.datastream.arn, 38 | ] 39 | } 40 | } 41 | 42 | resource "aws_iam_role" "cloudwatch_processor" { 43 | name = "${local.cw_processor_lambda_name}-execution" 44 | assume_role_policy = data.aws_iam_policy_document.assume_role_lambda.json 45 | 46 | lifecycle { 47 | create_before_destroy = true 48 | } 49 | } 50 | 51 | resource "aws_iam_role_policy" "cwprocessor" { 52 | name = "cwprocessor" 53 | role = aws_iam_role.cloudwatch_processor.id 54 | policy = data.aws_iam_policy_document.cwprocessor.json 55 | 56 | lifecycle { 57 | create_before_destroy = true 58 | } 59 | } 60 | 61 | resource "aws_iam_role_policy" "cwprocessor_dynamodb" { 62 | name = "cwprocessor_dynamodb" 63 | role = aws_iam_role.cloudwatch_processor.id 64 | policy = data.aws_iam_policy_document.lambda_dynamodb.json 65 | } 66 | 67 | resource "aws_iam_role_policy" "cwprocessor_kms" { 68 | name = "cwprocessor_kms" 69 | role = aws_iam_role.cloudwatch_processor.id 70 | policy = data.aws_iam_policy_document.lambda_kms.json 71 | } 72 | 73 | resource "aws_iam_role_policy_attachment" "cwprocessor_insights" { 74 | role = aws_iam_role.cloudwatch_processor.id 75 | policy_arn = data.aws_iam_policy.insights.arn 76 | } 77 | 78 | # manage log group in Terraform 79 | resource "aws_cloudwatch_log_group" "cloudwatch_processor" { 80 | name = "/aws/lambda/${local.cw_processor_lambda_name}" 81 | retention_in_days = var.cloudwatch_retention_days 82 | } 83 | 84 | resource "aws_lambda_function" "cloudwatch_processor" { 85 | filename = var.lambda_kms_cw_processor_zip 86 | function_name = local.cw_processor_lambda_name 87 | description = "KMS CW Log Processor" 88 | role = aws_iam_role.cloudwatch_processor.arn 89 | handler = "main.IdentityKMSMonitor::CloudWatchKMSHandler.process" 90 | runtime = "ruby3.2" 91 | timeout = 120 # seconds 92 | 93 | layers = [ 94 | local.lambda_insights_arn 95 | ] 96 | 97 | memory_size = var.cw_processor_memory_size 98 | 99 | ephemeral_storage { 100 | size = var.cw_processor_storage_size 101 | 102 | } 103 | 104 | environment { 105 | variables = { 106 | DEBUG = var.kmslog_lambda_debug 107 | DRY_RUN = var.kmslog_lambda_dry_run 108 | RETENTION_DAYS = var.dynamodb_retention_days 109 | DDB_TABLE = aws_dynamodb_table.kms_events.id 110 | SNS_EVENT_TOPIC_ARN = aws_sns_topic.kms_logging_events.arn 111 | } 112 | } 113 | 114 | tags = { 115 | environment = var.env_name 116 | } 117 | 118 | depends_on = [aws_cloudwatch_log_group.cloudwatch_processor] 119 | } 120 | 121 | module "cw-processor-github-alerts" { 122 | source = "github.com/18F/identity-terraform//lambda_alerts?ref=e0e39adea82243d66c3c1218c7a4316b81f64560" 123 | #source = "../lambda_alerts" 124 | 125 | enabled = 1 126 | function_name = local.cw_processor_lambda_name 127 | alarm_actions = var.alarm_sns_topic_arns 128 | error_rate_threshold = 5 # percent 129 | datapoints_to_alarm = 5 130 | evaluation_periods = 5 131 | insights_enabled = true 132 | duration_setting = aws_lambda_function.cloudwatch_processor.timeout 133 | treat_missing_data = "ignore" 134 | } 135 | 136 | resource "aws_lambda_event_source_mapping" "cloudwatch_processor" { 137 | event_source_arn = aws_kinesis_stream.datastream.arn 138 | function_name = aws_lambda_function.cloudwatch_processor.arn 139 | starting_position = "LATEST" 140 | parallelization_factor = 10 141 | } 142 | -------------------------------------------------------------------------------- /kms_log/kinesis.tf: -------------------------------------------------------------------------------- 1 | # create kinesis data stream for application kms events 2 | resource "aws_kinesis_stream" "datastream" { 3 | name = local.kinesis_stream_name 4 | shard_count = var.kinesis_shard_count 5 | 6 | retention_period = var.kinesis_retention_hours 7 | encryption_type = "KMS" 8 | kms_key_id = "alias/aws/kinesis" 9 | 10 | shard_level_metrics = [ 11 | "ReadProvisionedThroughputExceeded", 12 | "WriteProvisionedThroughputExceeded", 13 | ] 14 | 15 | tags = { 16 | environment = var.env_name 17 | } 18 | } 19 | 20 | # policy to allow kinesis access to cloudwatch 21 | data "aws_iam_policy_document" "assume_role_kinesis" { 22 | statement { 23 | sid = "AssumeRole" 24 | actions = ["sts:AssumeRole"] 25 | 26 | principals { 27 | type = "Service" 28 | identifiers = ["logs.${var.region}.amazonaws.com"] 29 | } 30 | } 31 | } 32 | 33 | moved { 34 | from = data.aws_iam_policy_document.assume_role 35 | to = data.aws_iam_policy_document.assume_role_kinesis 36 | } 37 | 38 | # policy to allow cloudwatch to put log records into kinesis 39 | data "aws_iam_policy_document" "cloudwatch_access" { 40 | statement { 41 | sid = "KinesisPut" 42 | effect = "Allow" 43 | actions = [ 44 | "kinesis:PutRecord", 45 | ] 46 | resources = [ 47 | aws_kinesis_stream.datastream.arn, 48 | ] 49 | } 50 | } 51 | 52 | # kinesis role 53 | resource "aws_iam_role" "cloudwatch_to_kinesis" { 54 | name = local.kinesis_stream_name 55 | path = "/" 56 | assume_role_policy = data.aws_iam_policy_document.assume_role_kinesis.json 57 | } 58 | 59 | # add cloudwatch access to kinesis role 60 | resource "aws_iam_role_policy" "cloudwatch_access" { 61 | name = "cloudwatch_access" 62 | role = aws_iam_role.cloudwatch_to_kinesis.name 63 | policy = data.aws_iam_policy_document.cloudwatch_access.json 64 | } 65 | 66 | # set cloudwatch destination 67 | resource "aws_cloudwatch_log_destination" "datastream" { 68 | name = local.kinesis_stream_name 69 | role_arn = aws_iam_role.cloudwatch_to_kinesis.arn 70 | target_arn = aws_kinesis_stream.datastream.arn 71 | 72 | depends_on = [ 73 | aws_kinesis_stream.datastream 74 | ] 75 | } 76 | 77 | # configure policy to allow subscription acccess 78 | data "aws_iam_policy_document" "subscription" { 79 | statement { 80 | sid = "PutSubscription" 81 | actions = ["logs:PutSubscriptionFilter"] 82 | 83 | principals { 84 | type = "AWS" 85 | identifiers = [data.aws_caller_identity.current.account_id] 86 | } 87 | 88 | resources = [ 89 | aws_cloudwatch_log_destination.datastream.arn, 90 | ] 91 | } 92 | } 93 | 94 | # create destination policy 95 | resource "aws_cloudwatch_log_destination_policy" "subscription" { 96 | destination_name = aws_cloudwatch_log_destination.datastream.name 97 | access_policy = data.aws_iam_policy_document.subscription.json 98 | } 99 | 100 | data "aws_cloudwatch_log_group" "kinesis_source" { 101 | name = var.kinesis_source_log_group 102 | } 103 | 104 | # create subscription filter 105 | # this filter will send the kms.log events to kinesis 106 | resource "aws_cloudwatch_log_subscription_filter" "kinesis" { 107 | name = "${var.env_name}-kms-app-log" 108 | log_group_name = data.aws_cloudwatch_log_group.kinesis_source.name 109 | filter_pattern = var.cloudwatch_filter_pattern 110 | destination_arn = aws_kinesis_stream.datastream.arn 111 | role_arn = aws_iam_role.cloudwatch_to_kinesis.arn 112 | } 113 | -------------------------------------------------------------------------------- /kms_log/outputs.tf: -------------------------------------------------------------------------------- 1 | output "kms-dead-letter-queue" { 2 | description = "Arn for the kms dead letter queue" 3 | value = aws_sqs_queue.dead_letter.arn 4 | } 5 | 6 | output "kms-ct-events-queue" { 7 | description = "Arn for the kms cloudtrail queue" 8 | value = aws_sqs_queue.kms_ct_events.arn 9 | } 10 | 11 | output "kms-logging-events-topic" { 12 | description = "SNS topic for kms logging events" 13 | value = aws_sns_topic.kms_logging_events.arn 14 | } 15 | 16 | output "kms-cloudwatch-events-queue" { 17 | description = "Queue for kms logging events to cloudwatch" 18 | value = aws_sqs_queue.kms_cloudwatch_events.arn 19 | } 20 | 21 | output "lambda-log-groups" { 22 | description = "Names of the CloudWatch Log Groups for Lambda functions in this module." 23 | value = [ 24 | aws_cloudwatch_log_group.cloudtrail_processor.name, 25 | aws_cloudwatch_log_group.cloudtrail_requeue.name, 26 | aws_cloudwatch_log_group.cloudwatch_processor.name, 27 | aws_cloudwatch_log_group.event_processor.name, 28 | aws_cloudwatch_log_group.slack_processor.name, 29 | ] 30 | } 31 | 32 | output "unmatched-log-group" { 33 | description = "Name of the CloudWatch Log Group for unmatched events." 34 | value = aws_cloudwatch_log_group.unmatched.name 35 | } -------------------------------------------------------------------------------- /kms_log/versions.tf: -------------------------------------------------------------------------------- 1 | ../versions.tf -------------------------------------------------------------------------------- /lambda_alerts/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudwatch_metric_alarm" "lambda_error_rate" { 2 | count = var.enabled 3 | 4 | alarm_name = length(var.error_rate_alarm_name_override) > 0 ? var.error_rate_alarm_name_override : join("-", compact([ 5 | var.env_name, 6 | var.function_name, 7 | "LambdaErrorRate" 8 | ]) 9 | ) 10 | 11 | alarm_description = length(var.error_rate_alarm_description) > 0 ? var.error_rate_alarm_description : local.default_error_rate_alarm_description 12 | 13 | comparison_operator = var.error_rate_operator 14 | evaluation_periods = var.evaluation_periods 15 | threshold = var.error_rate_threshold 16 | insufficient_data_actions = [] 17 | datapoints_to_alarm = var.datapoints_to_alarm 18 | treat_missing_data = var.treat_missing_data 19 | alarm_actions = var.alarm_actions 20 | ok_actions = var.ok_actions 21 | 22 | metric_query { 23 | id = "error_rate" 24 | expression = "errors / invocations * 100" 25 | label = "Error Rate" 26 | return_data = "true" 27 | } 28 | metric_query { 29 | id = "errors" 30 | metric { 31 | metric_name = "Errors" 32 | namespace = "AWS/Lambda" 33 | dimensions = { 34 | FunctionName = var.function_name 35 | } 36 | period = var.period 37 | stat = "Sum" 38 | } 39 | } 40 | metric_query { 41 | id = "invocations" 42 | metric { 43 | metric_name = "Invocations" 44 | namespace = "AWS/Lambda" 45 | dimensions = { 46 | FunctionName = var.function_name 47 | } 48 | period = var.period 49 | stat = "Sum" 50 | } 51 | } 52 | 53 | lifecycle { 54 | create_before_destroy = true 55 | } 56 | } 57 | 58 | resource "aws_cloudwatch_metric_alarm" "lambda_memory_usage" { 59 | count = var.enabled == 1 && var.insights_enabled ? 1 : 0 60 | 61 | alarm_name = length(var.memory_usage_alarm_name_override) > 0 ? var.memory_usage_alarm_name_override : join("-", compact([ 62 | var.env_name, 63 | var.function_name, 64 | "LambdaMemoryUsage" 65 | ]) 66 | ) 67 | alarm_description = length(var.memory_usage_alarm_description) > 0 ? var.memory_usage_alarm_description : local.default_memory_usage_alarm_description 68 | 69 | comparison_operator = "GreaterThanOrEqualToThreshold" 70 | evaluation_periods = var.evaluation_periods 71 | threshold = var.memory_usage_threshold 72 | insufficient_data_actions = [] 73 | datapoints_to_alarm = var.datapoints_to_alarm 74 | treat_missing_data = var.treat_missing_data 75 | alarm_actions = var.alarm_actions 76 | ok_actions = var.ok_actions 77 | 78 | metric_name = "memory_utilization" 79 | namespace = "LambdaInsights" 80 | period = var.period 81 | statistic = "Maximum" 82 | dimensions = { 83 | function_name = var.function_name 84 | } 85 | 86 | lifecycle { 87 | create_before_destroy = true 88 | } 89 | } 90 | 91 | resource "aws_cloudwatch_metric_alarm" "lambda_duration" { 92 | count = var.enabled 93 | 94 | alarm_name = length(var.duration_alarm_name_override) > 0 ? var.duration_alarm_name_override : join("-", compact([ 95 | var.env_name, 96 | var.function_name, 97 | "LambdaDuration" 98 | ]) 99 | ) 100 | alarm_description = length(var.duration_alarm_description) > 0 ? var.duration_alarm_description : local.default_duration_alarm_description 101 | 102 | comparison_operator = "GreaterThanOrEqualToThreshold" 103 | evaluation_periods = var.evaluation_periods 104 | threshold = local.duration_settings_in_milliseconds * (var.duration_threshold * 0.01) 105 | insufficient_data_actions = [] 106 | datapoints_to_alarm = var.datapoints_to_alarm 107 | treat_missing_data = var.treat_missing_data 108 | alarm_actions = var.alarm_actions 109 | ok_actions = var.ok_actions 110 | 111 | metric_name = "Duration" 112 | namespace = "AWS/Lambda" 113 | period = var.period 114 | statistic = "Maximum" 115 | dimensions = { 116 | FunctionName = var.function_name 117 | } 118 | 119 | lifecycle { 120 | create_before_destroy = true 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lambda_alerts/variables.tf: -------------------------------------------------------------------------------- 1 | variable "enabled" { 2 | type = number 3 | description = "Whether or not to create the Lambda alert monitor." 4 | default = 1 5 | } 6 | 7 | variable "function_name" { 8 | type = string 9 | description = "Name of the lambda function to monitor" 10 | } 11 | 12 | variable "env_name" { 13 | type = string 14 | description = "Name of the environment in which the lambda function lives" 15 | default = "" 16 | } 17 | 18 | variable "alarm_actions" { 19 | type = list(string) 20 | description = "A list of ARNs to notify when the alarm fires" 21 | } 22 | 23 | variable "ok_actions" { 24 | type = list(string) 25 | description = "A list of ARNs to notify when the alarm goes to ok state" 26 | default = [] 27 | } 28 | 29 | variable "runbook" { 30 | type = string 31 | description = "A link to a runbook associated with any metric in this module" 32 | default = "" 33 | } 34 | 35 | variable "error_rate_operator" { 36 | type = string 37 | description = "The operator used to compare a calculated error rate against a threshold" 38 | default = "GreaterThanOrEqualToThreshold" 39 | } 40 | 41 | variable "error_rate_threshold" { 42 | type = number 43 | description = "The threshold error rate (as a percentage) for triggering an alert" 44 | default = 1 45 | } 46 | 47 | variable "memory_usage_threshold" { 48 | type = number 49 | description = "The threshold memory utilization (as a percentage) for triggering an alert" 50 | default = 90 51 | } 52 | 53 | variable "duration_setting" { 54 | type = number 55 | description = "The duration setting of the lambda to monitor (in seconds)" 56 | } 57 | 58 | variable "duration_threshold" { 59 | type = number 60 | description = "The duration threshold (as a percentage) for triggering an alert" 61 | default = 80 62 | } 63 | 64 | variable "datapoints_to_alarm" { 65 | type = number 66 | description = "The number of datapoints that must be breaching to trigger the alarm." 67 | default = 1 68 | } 69 | 70 | variable "evaluation_periods" { 71 | type = number 72 | default = 1 73 | } 74 | 75 | variable "period" { 76 | type = number 77 | description = "The period in seconds over which the specified statistic is applied." 78 | default = 60 79 | } 80 | 81 | variable "treat_missing_data" { 82 | type = string 83 | default = "missing" 84 | } 85 | 86 | variable "insights_enabled" { 87 | type = bool 88 | description = "Creates lambda insights specific alerts" 89 | default = false 90 | } 91 | 92 | variable "error_rate_alarm_name_override" { 93 | type = string 94 | description = "Overrides the default alarm naming convention with a custom name" 95 | default = "" 96 | } 97 | variable "memory_usage_alarm_name_override" { 98 | type = string 99 | description = "Overrides the default alarm naming convention with a custom name" 100 | default = "" 101 | } 102 | variable "duration_alarm_name_override" { 103 | type = string 104 | description = "Overrides the default alarm naming convention with a custom name" 105 | default = "" 106 | } 107 | 108 | variable "error_rate_alarm_description" { 109 | type = string 110 | description = "Overrides the default alarm description for error rate alarm" 111 | default = "" 112 | } 113 | 114 | variable "memory_usage_alarm_description" { 115 | type = string 116 | description = "Overrides the default alarm description for memory usage alarm" 117 | default = "" 118 | } 119 | 120 | variable "duration_alarm_description" { 121 | type = string 122 | description = "Overrides the default alarm description for duration alarm" 123 | default = "" 124 | } 125 | 126 | locals { 127 | duration_settings_in_milliseconds = var.duration_setting * 1000 128 | default_error_rate_alarm_description = "Lambda error rate has exceeded ${var.error_rate_threshold}%\n\n${var.runbook}" 129 | default_memory_usage_alarm_description = "Lambda memory usage has exceeded ${var.memory_usage_threshold}%\n\n${var.runbook}" 130 | default_duration_alarm_description = "Lambda duration has exceeded ${var.duration_threshold}%\n\n${var.runbook}" 131 | } 132 | -------------------------------------------------------------------------------- /lambda_alerts/versions.tf: -------------------------------------------------------------------------------- 1 | ../versions.tf -------------------------------------------------------------------------------- /lambda_function/iam.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | lambda_role_name = var.role_name_prefix == null ? ( 3 | var.lambda_iam_role_name != null ? var.lambda_iam_role_name : var.function_name 4 | ) : null 5 | role_name_prefix = var.role_name_prefix != null ? substr(var.role_name_prefix, 0, 38) : null 6 | } 7 | 8 | data "aws_iam_policy_document" "lambda_assume_role" { 9 | statement { 10 | actions = ["sts:AssumeRole"] 11 | 12 | principals { 13 | type = "Service" 14 | identifiers = ["lambda.amazonaws.com"] 15 | } 16 | } 17 | } 18 | 19 | resource "aws_iam_role" "lambda" { 20 | name = local.lambda_role_name 21 | name_prefix = local.role_name_prefix 22 | path = "/" 23 | assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json 24 | description = var.iam_role_description 25 | 26 | lifecycle { 27 | create_before_destroy = true 28 | } 29 | } 30 | 31 | data "aws_iam_policy_document" "lambda" { 32 | source_policy_documents = length(var.lambda_iam_policy_document) > 0 ? [var.lambda_iam_policy_document] : [] 33 | statement { 34 | sid = "CreateLogStreamAndEvents" 35 | effect = "Allow" 36 | actions = [ 37 | "logs:CreateLogStream", 38 | "logs:PutLogEvents", 39 | ] 40 | 41 | resources = [ 42 | aws_cloudwatch_log_group.lambda.arn, 43 | "${aws_cloudwatch_log_group.lambda.arn}:*" 44 | ] 45 | } 46 | 47 | } 48 | 49 | resource "aws_iam_role_policy" "lambda" { 50 | role = aws_iam_role.lambda.name 51 | policy = data.aws_iam_policy_document.lambda.json 52 | 53 | lifecycle { 54 | create_before_destroy = true 55 | } 56 | } 57 | 58 | resource "aws_iam_role_policy_attachment" "lambda_insights" { 59 | count = var.insights_enabled ? 1 : 0 60 | role = aws_iam_role.lambda.id 61 | policy_arn = module.lambda_insights[0].iam_policy_arn 62 | } 63 | -------------------------------------------------------------------------------- /lambda_function/main.tf: -------------------------------------------------------------------------------- 1 | module "lambda_insights" { 2 | count = var.insights_enabled ? 1 : 0 3 | source = "github.com/18F/identity-terraform//lambda_insights?ref=5c1a8fb0ca08aa5fa01a754a40ceab6c8075d4c9" 4 | #source = "../../../../identity-terraform/lambda_insights" 5 | 6 | region = var.region 7 | } 8 | 9 | resource "aws_cloudwatch_log_group" "lambda" { 10 | name = "/aws/lambda/${var.function_name}" 11 | retention_in_days = var.cloudwatch_retention_days 12 | skip_destroy = var.log_skip_destroy 13 | } 14 | 15 | module "lambda_code" { 16 | source = "github.com/18F/identity-terraform//null_archive?ref=2d05076e1d089d9e9ab251fa0f11a2e2ceb132a3" 17 | #source = "../../../../identity-terraform/null_archive" 18 | 19 | source_code_filename = var.source_code_filename 20 | source_dir = var.source_dir 21 | zip_filename = "${replace(var.function_name, "-", "_")}_code.zip" 22 | } 23 | 24 | resource "aws_lambda_function" "lambda" { 25 | filename = module.lambda_code.zip_output_path 26 | function_name = var.function_name 27 | role = aws_iam_role.lambda.arn 28 | description = var.description 29 | handler = var.handler != "" ? ( 30 | var.handler 31 | ) : ( 32 | "${replace(var.source_code_filename, "/\\..*/", "")}.${var.handler_function_name}" 33 | ) 34 | 35 | source_code_hash = module.lambda_code.zip_output_base64sha256 36 | memory_size = var.memory_size 37 | runtime = var.runtime 38 | timeout = var.timeout 39 | reserved_concurrent_executions = var.reserved_concurrent_executions 40 | 41 | layers = compact(flatten([ 42 | var.insights_enabled ? module.lambda_insights[0].layer_arn : "", 43 | var.layers 44 | ])) 45 | 46 | environment { 47 | variables = var.environment_variables 48 | } 49 | 50 | logging_config { 51 | log_format = "Text" 52 | log_group = aws_cloudwatch_log_group.lambda.name 53 | } 54 | 55 | depends_on = [ 56 | module.lambda_code.resource_check, 57 | ] 58 | } 59 | 60 | module "lambda_alerts" { 61 | source = "github.com/18F/identity-terraform//lambda_alerts?ref=a4dfd80b0e40a96d2a0c7c09262f84d2ea3d9104" 62 | #source = "../../../../identity-terraform/lambda_alerts" 63 | 64 | enabled = var.enabled 65 | function_name = aws_lambda_function.lambda.function_name 66 | env_name = var.env_name 67 | alarm_actions = var.alarm_actions 68 | ok_actions = var.ok_actions 69 | runbook = var.runbook 70 | error_rate_operator = var.error_rate_operator 71 | error_rate_threshold = var.error_rate_threshold 72 | memory_usage_threshold = var.memory_usage_threshold 73 | duration_setting = aws_lambda_function.lambda.timeout 74 | duration_threshold = var.duration_threshold 75 | datapoints_to_alarm = var.datapoints_to_alarm 76 | evaluation_periods = var.evaluation_periods 77 | period = var.period 78 | treat_missing_data = var.treat_missing_data 79 | insights_enabled = var.insights_enabled 80 | error_rate_alarm_name_override = var.error_rate_alarm_name_override 81 | memory_usage_alarm_name_override = var.memory_usage_alarm_name_override 82 | duration_alarm_name_override = var.duration_alarm_name_override 83 | error_rate_alarm_description = var.error_rate_alarm_description 84 | memory_usage_alarm_description = var.memory_usage_alarm_description 85 | duration_alarm_description = var.duration_alarm_description 86 | } 87 | -------------------------------------------------------------------------------- /lambda_function/output.tf: -------------------------------------------------------------------------------- 1 | output "function_name" { 2 | value = aws_lambda_function.lambda.function_name 3 | } 4 | 5 | output "lambda_arn" { 6 | value = aws_lambda_function.lambda.arn 7 | description = "The ARN of the Lambda Function" 8 | } 9 | 10 | output "lambda_role_name" { 11 | value = aws_iam_role.lambda.name 12 | description = "The name of the IAM Role associated with the lambda" 13 | } 14 | 15 | output "lambda_role_arn" { 16 | value = aws_iam_role.lambda.arn 17 | description = "The arn of the IAM Role associated with the lambda" 18 | } 19 | 20 | output "log_group_name" { 21 | value = aws_cloudwatch_log_group.lambda.name 22 | description = "The name of the cloudwatch log group associated with the lambda" 23 | } 24 | 25 | -------------------------------------------------------------------------------- /lambda_function/trigger.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | enable_trigger = ( 3 | var.schedule_expression == "" && var.event_pattern != "") || ( 4 | var.schedule_expression != "" && var.event_pattern == "") 5 | } 6 | 7 | resource "aws_cloudwatch_event_rule" "lambda" { 8 | count = local.enable_trigger ? 1 : 0 9 | name = var.function_name 10 | description = "Trigger ${var.function_name} from EventBridge" 11 | schedule_expression = var.schedule_expression 12 | event_pattern = var.event_pattern 13 | } 14 | 15 | resource "aws_cloudwatch_event_target" "lambda" { 16 | count = local.enable_trigger ? 1 : 0 17 | rule = aws_cloudwatch_event_rule.lambda[0].name 18 | arn = aws_lambda_function.lambda.arn 19 | } 20 | 21 | resource "aws_lambda_permission" "lambda" { 22 | count = local.enable_trigger ? 1 : 0 23 | statement_id = "AllowExecutionFromEventBridge" 24 | action = "lambda:InvokeFunction" 25 | function_name = aws_lambda_function.lambda.function_name 26 | principal = "events.amazonaws.com" 27 | source_arn = aws_cloudwatch_event_rule.lambda[0].arn 28 | } 29 | -------------------------------------------------------------------------------- /lambda_function/versions.tf: -------------------------------------------------------------------------------- 1 | ../versions.tf -------------------------------------------------------------------------------- /lambda_insights/README.md: -------------------------------------------------------------------------------- 1 | # lambda_insights module 2 | 3 | This module provides the minimum resources required for finding the insights layer ARN and required IAM policy. 4 | 5 | 6 | ## Example: 7 | ```hcl 8 | module "lambda_insights" { 9 | source = github.com/18F/identity-terraform//lambda_insights?ref=main 10 | } 11 | ``` 12 | 13 | ## Requirements 14 | 15 | | Name | Version | 16 | |------|---------| 17 | | [terraform](#requirement\_terraform) | >= 1.3.5 | 18 | 19 | ## Providers 20 | 21 | | Name | Version | 22 | |------|---------| 23 | | [aws](#provider\_aws) | n/a | 24 | 25 | ## Modules 26 | 27 | No modules. 28 | 29 | ## Resources 30 | 31 | | Name | Type | 32 | |------|------| 33 | | [aws_iam_policy.insights](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy) | data source | 34 | 35 | ## Inputs 36 | 37 | | Name | Description | Type | Default | Required | 38 | |------|-------------|------|---------|:--------:| 39 | | [lambda\_insights\_account](#input\_lambda\_insights\_account) | The lambda insights account provided by AWS for monitoring | `string` | `"580247275435"` | no | 40 | | [lambda\_insights\_version](#input\_lambda\_insights\_version) | The lambda insights layer version to use for monitoring | `number` | `52` | no | 41 | | [region](#input\_region) | Target AWS Region | `string` | `"us-west-2"` | no | 42 | 43 | ## Outputs 44 | 45 | | Name | Description | 46 | |------|-------------| 47 | | [iam\_policy\_arn](#output\_iam\_policy\_arn) | The IAM Policy ARN for attaching to iam\_roles for writing to insights | 48 | | [layer\_arn](#output\_layer\_arn) | The insights lambda layer arn for attaching to aws\_lambda\_functions | 49 | 50 | -------------------------------------------------------------------------------- /lambda_insights/main.tf: -------------------------------------------------------------------------------- 1 | /* 2 | * ## Example: 3 | * ```hcl 4 | * module "lambda_insights" { 5 | * source = github.com/18F/identity-terraform//lambda_insights?ref=main 6 | * } 7 | * ``` 8 | */ 9 | 10 | locals { 11 | layer_arn = join(":", [ 12 | "arn:aws:lambda:${var.region}:${var.lambda_insights_account}:layer", 13 | "LambdaInsightsExtension:${var.lambda_insights_version}" 14 | ]) 15 | } 16 | 17 | data "aws_iam_policy" "insights" { 18 | arn = "arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy" 19 | } 20 | -------------------------------------------------------------------------------- /lambda_insights/output.tf: -------------------------------------------------------------------------------- 1 | output "layer_arn" { 2 | description = "The insights lambda layer arn for attaching to aws_lambda_functions" 3 | value = local.layer_arn 4 | } 5 | 6 | output "iam_policy_arn" { 7 | description = "The IAM Policy ARN for attaching to iam_roles for writing to insights" 8 | value = data.aws_iam_policy.insights.arn 9 | } 10 | -------------------------------------------------------------------------------- /lambda_insights/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" { 2 | default = "us-west-2" 3 | description = "Target AWS Region" 4 | type = string 5 | } 6 | 7 | variable "lambda_insights_account" { 8 | default = "580247275435" 9 | description = "The lambda insights account provided by AWS for monitoring" 10 | type = string 11 | } 12 | 13 | variable "lambda_insights_version" { 14 | default = 52 15 | description = "The lambda insights layer version to use for monitoring" 16 | type = number 17 | } 18 | -------------------------------------------------------------------------------- /lambda_insights/versions.tf: -------------------------------------------------------------------------------- 1 | ../versions.tf -------------------------------------------------------------------------------- /lambda_layer/main.tf: -------------------------------------------------------------------------------- 1 | module "layer_archive" { 2 | source = "github.com/18F/identity-terraform//null_archive?ref=6cdd1037f2d1b14315cc8c59b889f4be557b9c17" 3 | #source = "../null_archive" 4 | 5 | source_code_filename = var.source_code_filename 6 | source_dir = var.source_dir 7 | zip_filename = var.zip_filename 8 | } 9 | 10 | resource "aws_lambda_layer_version" "lambda_layer" { 11 | filename = module.layer_archive.zip_output_path 12 | layer_name = var.layer_name 13 | 14 | compatible_runtimes = var.compatible_runtimes 15 | source_code_hash = module.layer_archive.zip_output_base64sha256 16 | 17 | } 18 | -------------------------------------------------------------------------------- /lambda_layer/outputs.tf: -------------------------------------------------------------------------------- 1 | output "arn" { 2 | value = aws_lambda_layer_version.lambda_layer.arn 3 | } -------------------------------------------------------------------------------- /lambda_layer/variables.tf: -------------------------------------------------------------------------------- 1 | variable "source_code_filename" { 2 | description = "Name (with extension) of file containing function source code." 3 | type = string 4 | } 5 | 6 | variable "source_dir" { 7 | description = < (known after apply) 12 | ~ source_code_hash = "AnOriginalHashValue09jshfkgbkhjx36ferjkbf=" -> "ADifferentHashValue3785yhutrefg7gt358vbhe=" 13 | ``` 14 | Thus, if multiple sources are acting on a ZIP file (i.e. an automated pipeline vs. an engineer running Terraform on an ad hoc basis), Terraform will constantly show a `plan` with pending changes, even when no _actual_ changes have been made to the source code itself. 15 | 16 | `null_resource.source_hash_check` is used to monitor the Base64-encoded SHA256 hash of the main source code file, and will cause the `archive_file` resource to _always_ create a new ZIP file -- but only if updates are _actually_ made to the function's source code file. 17 | 18 | For maximum compatibility, a `depends_on` statement/list item should be used in the accompanying `aws_lambda_function` resource, pointing to the `resource_check` output, ensuring that the function _only_ updates if the `null_resource.source_hash_check` resource is triggered/replaced. 19 | 20 | ## Example 21 | 22 | ```hcl 23 | module "smart_archive_file" { 24 | source = "github.com/18F/identity-terraform//null_archive?ref=main" 25 | 26 | source_code_filename = "lambda_function.py" 27 | source_dir = "${path.module}/src/" 28 | zip_filename = var.example_lambda_code 29 | } 30 | 31 | resource "aws_lambda_function" "sample_lambda" { 32 | function_name = "${var.env}-sample-lambda-${data.aws_region.current.name}" 33 | description = "Sample AWS Lambda Function" 34 | filename = module.smart_archive_file.zip_output_path 35 | source_code_hash = module.smart_archive_file.zip_output_base64sha256 36 | runtime = "python3.12" 37 | handler = "main.lambda_handler" 38 | timeout = 90 39 | memory_size = 128 40 | role = aws_iam_role.lambda_role.arn 41 | environment { 42 | variables = { 43 | env = "${var.env}" 44 | log_group_name = aws_cloudwatch_log_group.example_log_group.name 45 | log_stream_name = aws_cloudwatch_log_stream.example_log_stream.name 46 | region = data.aws_region.current.name 47 | } 48 | } 49 | 50 | depends_on = [module.smart_archive_file.resource_check] 51 | } 52 | ``` 53 | 54 | ## Variables 55 | 56 | `source_code_filename` - (REQUIRED) Name (with extension) of file containing function source code. 57 | `source_dir` - (REQUIRED) Name of directory where source_code_filename + any other files to be added to the ZIP file reside. 58 | `zip_filename` - (REQUIRED) Desired name (with .zip extension) of resultant output file. 59 | 60 | ## Outputs 61 | 62 | `zip_output_path` - Output path/filename of ZIP file created from source code filename. 63 | `zip_output_base64sha256` - base64-encoded SHA256 checksum of ZIP file. 64 | `resource_check` - ID (in Terraform state) of the `null_resource.source_hash_check` resource. Use with a `depends_on` block to ensure that a parent resource (e.g. a Lambda function) _only_ updates when the source code changes. 65 | -------------------------------------------------------------------------------- /null_archive/main.tf: -------------------------------------------------------------------------------- 1 | # -- Data Sources -- 2 | 3 | data "archive_file" "lambda" { 4 | depends_on = [null_resource.source_hash_check] 5 | type = "zip" 6 | source_dir = var.source_dir 7 | output_path = var.zip_filename 8 | } 9 | 10 | # -- Resources -- 11 | 12 | resource "null_resource" "source_hash_check" { 13 | triggers = { 14 | source_hash = filebase64sha256("${var.source_dir}/${var.source_code_filename}") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /null_archive/outputs.tf: -------------------------------------------------------------------------------- 1 | output "zip_output_path" { 2 | description = "Output path/filename of ZIP file created from source code filename." 3 | value = data.archive_file.lambda.output_path 4 | } 5 | 6 | output "zip_output_base64sha256" { 7 | description = "base64-encoded SHA256 checksum of ZIP file." 8 | value = data.archive_file.lambda.output_base64sha256 9 | } 10 | 11 | output "resource_check" { 12 | description = "ID of null_resource (changes when triggered)." 13 | value = null_resource.source_hash_check.id 14 | } -------------------------------------------------------------------------------- /null_archive/variables.tf: -------------------------------------------------------------------------------- 1 | variable "source_code_filename" { 2 | description = "(REQUIRED) Name (with extension) of file containing function source code." 3 | type = string 4 | default = "lambda_function.py" 5 | } 6 | 7 | variable "source_dir" { 8 | description = < `aws_s3_bucket.bucket[*]["id"]` allowing one to obtain the full bucket name from the shorter key reference. -------------------------------------------------------------------------------- /s3_bucket_block/versions.tf: -------------------------------------------------------------------------------- 1 | ../versions.tf -------------------------------------------------------------------------------- /s3_config/README.md: -------------------------------------------------------------------------------- 1 | # `s3_config` 2 | 3 | This module is used to add a public access block, S3 Inventory configuration, and access logging to the provided bucket name. It can be configured to use either: 4 | 5 | - a templated bucket name, using the `PREFIX.NAME.ACCOUNTID-REGION` schema, or 6 | - a manually-set bucket name, using the `bucket_name_override` variable 7 | 8 | To work properly: 9 | 10 | - An S3 bucket for collecting Inventory reports must already exist; its ARN is required for the `inventory_bucket_arn` variable. 11 | - An S3 bucket for access logging must already exist; its ID is required for the `logging_bucket_id` variable. 12 | 13 | ## Example 14 | 15 | ```hcl 16 | module "secrets_bucket_config" { 17 | source = "github.com/18F/identity-terraform//s3_config?ref=main" 18 | 19 | bucket_name_prefix = var.bucket_name_prefix 20 | bucket_name = var.secrets_bucket_type 21 | region = var.region 22 | inventory_bucket_arn = local.inventory_bucket_arn 23 | logging_bucket_id = data.aws_s3_bucket.access_logging_bucket.id 24 | } 25 | ``` 26 | 27 | ## Requirements 28 | 29 | | Name | Version | 30 | |------|---------| 31 | | [terraform](#requirement\_terraform) | >= 1.3.5 | 32 | 33 | ## Providers 34 | 35 | | Name | Version | 36 | |------|---------| 37 | | [aws](#provider\_aws) | n/a | 38 | 39 | ## Modules 40 | 41 | No modules. 42 | 43 | ## Resources 44 | 45 | | Name | Type | 46 | |------|------| 47 | | [aws_s3_bucket_inventory.daily](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_inventory) | resource | 48 | | [aws_s3_bucket_logging.access_logging](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_logging) | resource | 49 | | [aws_s3_bucket_public_access_block.public_block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | 50 | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | 51 | 52 | ## Inputs 53 | 54 | | Name | Description | Type | Default | Required | 55 | |------|-------------|------|---------|:--------:| 56 | | [inventory\_bucket\_arn](#input\_inventory\_bucket\_arn) | ARN of the S3 bucket used for collecting the S3 Inventory reports. | `string` | n/a | yes | 57 | | [logging\_bucket\_id](#input\_logging\_bucket\_id) | ID of the S3 bucket used for collecting the S3 access events | `string` | n/a | yes | 58 | | [block\_public\_access](#input\_block\_public\_access) | Whether or not to enable the public access block for this bucket. | `bool` | `true` | no | 59 | | [bucket\_name](#input\_bucket\_name) | Main/second substring in S3 bucket name of $bucket\_name\_prefix.$bucket\_name.$account\_id-$region | `string` | `""` | no | 60 | | [bucket\_name\_override](#input\_bucket\_name\_override) | Set this to override the normal bucket naming schema. | `string` | `""` | no | 61 | | [bucket\_name\_prefix](#input\_bucket\_name\_prefix) | First substring in S3 bucket name of $bucket\_name\_prefix.$bucket\_name.$account\_id-$region | `string` | `""` | no | 62 | | [optional\_fields](#input\_optional\_fields) | List of optional data fields to collect in S3 Inventory reports. | `list(string)` |
[
"Size",
"LastModifiedDate",
"StorageClass",
"ETag",
"IsMultipartUploaded",
"ReplicationStatus",
"EncryptionStatus",
"ObjectLockRetainUntilDate",
"ObjectLockMode",
"ObjectLockLegalHoldStatus",
"IntelligentTieringAccessTier"
]
| no | 63 | | [region](#input\_region) | AWS Region | `string` | `"us-west-2"` | no | 64 | 65 | ## Outputs 66 | 67 | No outputs. 68 | 69 | -------------------------------------------------------------------------------- /s3_config/main.tf: -------------------------------------------------------------------------------- 1 | # -- Variables -- 2 | variable "bucket_name_prefix" { 3 | description = "First substring in S3 bucket name of $bucket_name_prefix.$bucket_name.$account_id-$region" 4 | type = string 5 | default = "" 6 | } 7 | 8 | variable "bucket_name" { 9 | description = "Main/second substring in S3 bucket name of $bucket_name_prefix.$bucket_name.$account_id-$region" 10 | type = string 11 | default = "" 12 | } 13 | 14 | variable "bucket_name_override" { 15 | description = "Set this to override the normal bucket naming schema." 16 | type = string 17 | default = "" 18 | } 19 | 20 | variable "region" { 21 | description = "AWS Region" 22 | type = string 23 | default = "us-west-2" 24 | } 25 | 26 | variable "inventory_bucket_arn" { 27 | description = "ARN of the S3 bucket used for collecting the S3 Inventory reports." 28 | type = string 29 | } 30 | 31 | variable "logging_bucket_id" { 32 | description = "ID of the S3 bucket used for collecting the S3 access events" 33 | type = string 34 | } 35 | 36 | variable "optional_fields" { 37 | description = "List of optional data fields to collect in S3 Inventory reports." 38 | type = list(string) 39 | default = [ 40 | "Size", 41 | "LastModifiedDate", 42 | "StorageClass", 43 | "ETag", 44 | "IsMultipartUploaded", 45 | "ReplicationStatus", 46 | "EncryptionStatus", 47 | "ObjectLockRetainUntilDate", 48 | "ObjectLockMode", 49 | "ObjectLockLegalHoldStatus", 50 | "IntelligentTieringAccessTier", 51 | ] 52 | } 53 | 54 | variable "block_public_access" { 55 | description = "Whether or not to enable the public access block for this bucket." 56 | type = bool 57 | default = true 58 | } 59 | 60 | locals { 61 | bucket_fullname = var.bucket_name_override != "" ? var.bucket_name_override : join(".", 62 | [ 63 | var.bucket_name_prefix, 64 | var.bucket_name, 65 | "${data.aws_caller_identity.current.account_id}-${var.region}" 66 | ] 67 | ) 68 | } 69 | 70 | # -- Data Sources -- 71 | data "aws_caller_identity" "current" { 72 | } 73 | 74 | # -- Resources -- 75 | resource "aws_s3_bucket_public_access_block" "public_block" { 76 | bucket = local.bucket_fullname 77 | block_public_acls = var.block_public_access 78 | block_public_policy = var.block_public_access 79 | ignore_public_acls = var.block_public_access 80 | restrict_public_buckets = var.block_public_access 81 | } 82 | 83 | resource "aws_s3_bucket_inventory" "daily" { 84 | depends_on = [aws_s3_bucket_public_access_block.public_block] 85 | bucket = local.bucket_fullname 86 | name = "FullBucketDailyInventory" 87 | included_object_versions = "All" 88 | 89 | schedule { 90 | frequency = "Daily" 91 | } 92 | 93 | destination { 94 | bucket { 95 | format = "Parquet" 96 | bucket_arn = var.inventory_bucket_arn 97 | } 98 | } 99 | 100 | optional_fields = var.optional_fields 101 | } 102 | 103 | resource "aws_s3_bucket_logging" "access_logging" { 104 | bucket = local.bucket_fullname 105 | 106 | target_bucket = var.logging_bucket_id 107 | target_prefix = "${local.bucket_fullname}/" 108 | } 109 | -------------------------------------------------------------------------------- /s3_config/versions.tf: -------------------------------------------------------------------------------- 1 | ../versions.tf -------------------------------------------------------------------------------- /ses_dkim_r53/README.md: -------------------------------------------------------------------------------- 1 | # `ses_dkim_r53` 2 | 3 | Given a domain name and a Route53 zone ID, this Terraform module will create: 4 | 5 | - an SES identity resource for the provided domain + corresponding Route53 TXT verification record 6 | - three (3) SES domain DKIM generation resources for the provided domain + corresponding Route53 CNAME records 7 | 8 | ***NOTE:*** To allow for multi-region support, a `aws_route53_record.primary_verification_record` resource must be created separately (i.e. in the parent module), which uses the output value `ses_token` from each instance of `aws_ses_domain_identity.primary`. 9 | 10 | ## Example 11 | 12 | ```hcl 13 | module "core_ses" { 14 | source = "github.com/18F/identity-terraform//ses_dkim_r53?ref=main" 15 | 16 | domain = var.root_domain 17 | zone_id = module.common_dns.primary_zone_id 18 | ttl_dkim_records = "1800" 19 | } 20 | ``` 21 | 22 | ## Variables 23 | 24 | - `domain` - Name of the owned/managed domain. 25 | - `zone_id` - ID for the Route53 zone where the domain exists. 26 | - `ttl_dkim_records` - TTL value for the SES DKIM records. Defaults to ***1800***. -------------------------------------------------------------------------------- /ses_dkim_r53/main.tf: -------------------------------------------------------------------------------- 1 | # -- Variables -- 2 | 3 | variable "domain" { 4 | description = "Name of the owned/managed domain." 5 | type = string 6 | default = "example.com" 7 | } 8 | 9 | variable "zone_id" { 10 | description = "ID for the Route53 zone where the domain exists." 11 | type = string 12 | default = "ABCDEFGHIJ123" 13 | } 14 | 15 | variable "ttl_dkim_records" { 16 | description = "TTL value for the SES DKIM records." 17 | type = string 18 | default = "1800" 19 | } 20 | 21 | # -- Resources -- 22 | 23 | resource "aws_ses_domain_identity" "primary" { 24 | domain = var.domain 25 | } 26 | 27 | resource "aws_ses_domain_dkim" "primary" { 28 | domain = aws_ses_domain_identity.primary.domain 29 | } 30 | 31 | resource "aws_route53_record" "primary_ses_dkim" { 32 | count = 3 33 | zone_id = var.zone_id 34 | name = "${element(aws_ses_domain_dkim.primary.dkim_tokens, count.index)}._domainkey.${var.domain}" 35 | type = "CNAME" 36 | ttl = var.ttl_dkim_records 37 | records = ["${element(aws_ses_domain_dkim.primary.dkim_tokens, count.index)}.dkim.amazonses.com"] 38 | } 39 | 40 | output "ses_token" { 41 | description = "Token for the primary verification record in Route 53." 42 | value = aws_ses_domain_identity.primary.verification_token 43 | } -------------------------------------------------------------------------------- /ses_dkim_r53/versions.tf: -------------------------------------------------------------------------------- 1 | ../versions.tf -------------------------------------------------------------------------------- /slack_lambda/main.tf: -------------------------------------------------------------------------------- 1 | # -- Data Sources -- 2 | 3 | data "aws_region" "current" {} 4 | data "aws_caller_identity" "current" {} 5 | 6 | data "aws_iam_policy_document" "lambda_policy" { 7 | statement { 8 | sid = "SSM" 9 | effect = "Allow" 10 | actions = [ 11 | "ssm:DescribeParameters", 12 | "ssm:GetParameter" 13 | ] 14 | resources = [ 15 | "arn:aws:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:parameter${var.slack_webhook_url_parameter}" 16 | ] 17 | } 18 | } 19 | 20 | module "slack_lambda" { 21 | source = "github.com/18F/identity-terraform//lambda_function?ref=026f69d0a5e2b8af458888a5f21a72d557bbe1fe" 22 | #source = "../lambda_function" 23 | 24 | // region = var.region 25 | function_name = var.lambda_name 26 | description = var.lambda_description 27 | source_code_filename = "slack_lambda.py" 28 | source_dir = "${path.module}/src/" 29 | runtime = "python3.12" 30 | timeout = var.lambda_timeout 31 | memory_size = var.lambda_memory 32 | 33 | environment_variables = { 34 | slack_webhook_url_parameter = var.slack_webhook_url_parameter 35 | slack_channel = var.slack_channel, 36 | slack_username = var.slack_username, 37 | slack_icon = var.slack_icon 38 | slack_alarm_emoji = var.slack_alarm_emoji 39 | slack_warn_emoji = var.slack_warn_emoji 40 | slack_notice_emoji = var.slack_notice_emoji 41 | slack_ok_emoji = var.slack_ok_emoji 42 | } 43 | 44 | cloudwatch_retention_days = 365 45 | insights_enabled = false 46 | alarm_actions = [ 47 | ] 48 | 49 | role_name_prefix = var.lambda_name 50 | 51 | lambda_iam_policy_document = data.aws_iam_policy_document.lambda_policy.json 52 | } 53 | 54 | moved { 55 | from = aws_lambda_function.slack_lambda 56 | to = module.slack_lambda.aws_lambda_function.lambda 57 | } 58 | 59 | moved { 60 | from = aws_cloudwatch_log_group.slack_lambda 61 | to = module.slack_lambda.aws_cloudwatch_log_group.lambda 62 | } 63 | 64 | moved { 65 | from = aws_iam_role.slack_lambda 66 | to = module.slack_lambda.aws_iam_role.lambda 67 | } 68 | 69 | moved { 70 | from = aws_iam_role_policy.slack_lambda 71 | to = module.slack_lambda.aws_iam_role_policy.lambda 72 | } 73 | 74 | resource "aws_lambda_permission" "allow_sns_trigger" { 75 | statement_id = "AllowExecutionBySNS" 76 | action = "lambda:InvokeFunction" 77 | function_name = module.slack_lambda.lambda_arn 78 | principal = "sns.amazonaws.com" 79 | source_arn = var.slack_topic_arn 80 | } 81 | 82 | resource "aws_sns_topic_subscription" "sns_to_lambda" { 83 | topic_arn = var.slack_topic_arn 84 | protocol = "lambda" 85 | endpoint = module.slack_lambda.lambda_arn 86 | } 87 | -------------------------------------------------------------------------------- /slack_lambda/outputs.tf: -------------------------------------------------------------------------------- 1 | output "cw_log_group" { 2 | description = "Name of the CloudWatch Log Group for the slack_lambda function." 3 | value = module.slack_lambda.log_group_name 4 | } 5 | -------------------------------------------------------------------------------- /slack_lambda/src/aws_health_event_message.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "Sns": { 5 | "Message": "{\"version\": \"0\",\"id\": \"9eb71b72-556e-16d6-d19a-b71bf17e758d\",\"detail-type\": \"AWS Health Event\",\"source\": \"aws.health\",\"account\": \"100000000001\",\"time\": \"2024-05-20T02:17:35Z\",\"region\": \"us-west-2\",\"resources\": [\"arn:aws:acm:us-west-2:100000000001:certificate/c8a4d81d-6004-4c2a-9465-000000000000\"],\"detail\": {\"eventArn\": \"arn:aws:health:us-west-2::event/ACM/AWS_ACM_RENEWAL_STATE_CHANGE/AWS_ACM_RENEWAL_STATE_CHANGE-306f4893-06d0-435f-969e-a08de52c5415\",\"service\": \"ACM\",\"eventTypeCode\": \"AWS_ACM_RENEWAL_STATE_CHANGE\",\"eventTypeCategory\": \"scheduledChange\",\"eventScopeCode\": \"ACCOUNT_SPECIFIC\",\"communicationId\": \"80abcaefd238d5fd3c5aed232825534cd302929c-1\",\"startTime\": \"Mon, 20 May 2024 02:17:24 GMT\",\"endTime\": \"Mon, 20 May 2024 02:17:24 GMT\",\"lastUpdatedTime\": \"Mon, 20 May 2024 02:17:35 GMT\",\"statusCode\": \"closed\",\"eventRegion\": \"us-west-2\",\"eventDescription\": [{\"language\": \"en_US\",\"latestDescription\": \"This is to notify you that AWS Certificate Manager (ACM) has completed the renewal of an SSL/TLS certificate that includes the primary domain idp.origin.test.identitysandbox.gov and a total of 2 domains.\\\\n\\\\nAWS account ID: 100000000001\\\\nAWS Region name: us-west-2\\\\nCertificate identifier: arn:aws:acm:us-west-2:100000000001:certificate/c8a4d81d-6004-4c2a-9465-000000000000\\\\n\\\\nYour new certificate expires on Jun 19, 2025 at 23:59:59 UTC. \\\\nIf you have questions about this process, please use the Support Center at https://console.aws.amazon.com/support to contact AWS Support. If you don’t have an AWS support plan, post a new thread in the AWS Certificate Manager discussion forum at https://repost.aws/tags/TAJ7zd4vjzSfC_8JNlsbq2tA?forumID=206\\\\n\\\\nThis notification is intended solely for authorized individuals for idp.origin.test.identitysandbox.gov. To express any concerns about this notification or if it has reached you in error, forward it along with a brief explanation of your concern to validation-questions@amazon.com.\\\\n\"}],\"affectedEntities\": [{\"entityValue\": \"arn:aws:acm:us-west-2:100000000001:certificate/c8a4d81d-6004-4c2a-9465-000000000000\",\"lastUpdatedTime\": \"Mon, 20 May 2024 02:17:34 GMT\"}],\"affectedAccount\": \"100000000001\",\"page\": \"1\",\"totalPages\": \"1\"}}" 6 | } 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /slack_lambda/src/aws_incident_manager_shift_message.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "Sns": { 5 | "Message": "{\"IncidentManagerEvent\": \"ShiftChange\", \"Details\": {\"RotationName\": \"Platform Primary\", \"ContactName\": \"j_doe\", \"Status\": \"OFF\"}}" 6 | } 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /slack_lambda/src/cloudwatch_alarm_message.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "Sns": { 5 | "Message": "{\"AlarmName\": \"test-idp-unhealthy-instances\",\"AlarmDescription\": \"test-idp: Previously healthy instances have fallen ill\",\"AWSAccountId\": \"100000000001\",\"AlarmConfigurationUpdatedTimestamp\": \"2020-09-23T20:54:28.256+0000\",\"NewStateValue\": \"ALARM\",\"NewStateReason\": \"Threshold Crossed: 1 datapoint [1.0 (21/05/24 08:29:00)] was greater than the threshold (0.0).\",\"StateChangeTime\": \"2024-05-21T08:35:10.393+0000\",\"Region\": \"US West (Oregon)\",\"AlarmArn\": \"arn:aws:cloudwatch:us-west-2:100000000001:alarm:test-idp-unhealthy-instances\",\"OldStateValue\": \"OK\",\"OKActions\": [\"arn:aws:sns:us-west-2:100000000001:slack-otherevents\"],\"AlarmActions\": [\"arn:aws:sns:us-west-2:100000000001:slack-otherevents\"],\"InsufficientDataActions\": [],\"Trigger\": {\"MetricName\": \"UnHealthyHostCount\",\"Namespace\": \"AWS/ApplicationELB\",\"StatisticType\": \"Statistic\",\"Statistic\": \"MAXIMUM\",\"Unit\": null,\"Dimensions\": [{\"value\": \"targetgroup/test-ssl-target-group/cd55f79e5f49f3f3\",\"name\": \"TargetGroup\"},{\"value\": \"app/login-idp-alb-test/6924b3f01ae8ec6f\",\"name\": \"LoadBalancer\"}],\"Period\": 300,\"EvaluationPeriods\": 1,\"ComparisonOperator\": \"GreaterThanThreshold\",\"Threshold\": 0.0,\"TreatMissingData\": \"notBreaching\",\"EvaluateLowSampleCountPercentile\": \"\"}}" 6 | } 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /slack_lambda/src/codepipeline_message.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "Sns": { 5 | "Message": "{ \"account\": \"100000000001\", \"detailType\": \"CodePipeline Pipeline Execution State Change\", \"region\": \"us-west-2\", \"source\": \"aws.codepipeline\", \"time\": \"2025-05-05T20:24:48Z\", \"notificationRuleArn\": \"arn:aws:codestar-notifications:us-west-2:100000000001:notificationrule/8554765b869f2601f2a73c4819abfc8e53b18e11\", \"detail\": { \"pipeline\": \"imagebuild-alpha-main-rails\", \"execution-id\": \"56bd28bc-37af-4c7b-bc69-6ef89a2d59f4\", \"start-time\": \"2025-05-05T20:05:08.896Z\", \"state\": \"FAILED\", \"version\": 4.0, \"pipeline-execution-attempt\": 1.0 }, \"resources\": [ \"arn:aws:codepipeline:us-west-2:100000000001:imagebuild-alpha-main-rails\" ], \"additionalAttributes\": { \"failedActionCount\": 1, \"failedActions\": [ { \"action\": \"Build\", \"additionalInformation\": \"Build terminated with state: FAILED. Phase: PRE_BUILD, Code: COMMAND_EXECUTION_ERROR, Message: Error while executing command: ./packer validate src/login-image-terraform.pkr.hcl. Reason: exit status 1\" } ], \"failedStage\": \"CodeBuild\" }}" 6 | } 7 | } 8 | ] 9 | } 10 | 11 | 12 | -------------------------------------------------------------------------------- /slack_lambda/src/generic_message.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "Sns": { 5 | "Message": "This is a generic text message" 6 | } 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /slack_lambda/variables.tf: -------------------------------------------------------------------------------- 1 | variable "lambda_name" { 2 | description = "Name of the Lambda function" 3 | type = string 4 | default = "SnsToSlack" 5 | } 6 | 7 | variable "lambda_description" { 8 | description = "Lambda description" 9 | type = string 10 | default = "Sends a message sent to an SNS topic to Slack." 11 | } 12 | 13 | variable "lambda_timeout" { 14 | description = "Timeout for Lambda function" 15 | type = number 16 | default = 120 17 | } 18 | 19 | variable "lambda_memory" { 20 | description = "Memory allocated to Lambda function, 128MB to 3,008MB in 64MB increments" 21 | type = number 22 | default = 128 23 | } 24 | 25 | variable "lambda_runtime" { 26 | description = "Lambda runtime" 27 | type = string 28 | default = "python3.12" 29 | } 30 | 31 | variable "slack_webhook_url_parameter" { 32 | description = "Slack Webhook URL SSM Parameter." 33 | type = string 34 | } 35 | 36 | variable "slack_channel" { 37 | description = "Name of the Slack channel to send messages to. DO NOT include the # sign." 38 | type = string 39 | } 40 | 41 | variable "slack_username" { 42 | description = "Displayed username of the posted message." 43 | type = string 44 | } 45 | 46 | variable "slack_icon" { 47 | description = "Displayed icon used by Slack for the message." 48 | type = string 49 | } 50 | 51 | variable "slack_alarm_emoji" { 52 | description = "Emoji used by Slack for a CloudWatch ALARM message." 53 | type = string 54 | default = ":large_red_square:" 55 | } 56 | 57 | variable "slack_warn_emoji" { 58 | description = "Emoji used by Slack for a Lambda WARN message." 59 | type = string 60 | default = ":large_orange_square:" 61 | } 62 | 63 | variable "slack_notice_emoji" { 64 | description = "Emoji used by Slack for a Lambda NOTICE message." 65 | type = string 66 | default = ":large_yellow_square:" 67 | } 68 | 69 | variable "slack_ok_emoji" { 70 | description = "Emoji used by Slack for a CloudWatch OK message." 71 | type = string 72 | default = ":large_green_square:" 73 | } 74 | 75 | variable "slack_topic_arn" { 76 | description = "ARN of the SNS topic for the Lambda to subscribe to." 77 | type = string 78 | } 79 | -------------------------------------------------------------------------------- /slack_lambda/versions.tf: -------------------------------------------------------------------------------- 1 | ../versions.tf -------------------------------------------------------------------------------- /slo_lambda/outputs.tf: -------------------------------------------------------------------------------- 1 | output "cw_log_group" { 2 | description = "Name of the CloudWatch Log Group for the windowed_slo Lambda." 3 | value = aws_cloudwatch_log_group.windowed_slo_lambda.name 4 | } 5 | -------------------------------------------------------------------------------- /slo_lambda/src/windowed_slo_fixture_happy.json: -------------------------------------------------------------------------------- 1 | { 2 | "all_availability": { 3 | "numerator": [ 4 | { 5 | "namespace": "AWS/ApplicationELB", 6 | "metric_name": "HTTPCode_Target_2XX_Count", 7 | "dimensions": [ 8 | { 9 | "Name": "LoadBalancer", 10 | "Value": "LoadyMcLoadFace" 11 | } 12 | ] 13 | }, 14 | { 15 | "namespace": "AWS/ApplicationELB", 16 | "metric_name": "HTTPCode_Target_3XX_Count", 17 | "dimensions": [ 18 | { 19 | "Name": "LoadBalancer", 20 | "Value": "LoadyMcLoadFace" 21 | } 22 | ] 23 | }, 24 | { 25 | "namespace": "AWS/ApplicationELB", 26 | "metric_name": "HTTPCode_Target_4XX_Count", 27 | "multiplier": -1, 28 | "dimensions": [ 29 | { 30 | "Name": "LoadBalancer", 31 | "Value": "LoadyMcLoadFace" 32 | } 33 | ] 34 | } 35 | ], 36 | "denominator": [ 37 | { 38 | "namespace": "AWS/ApplicationELB", 39 | "metric_name": "RequestCount", 40 | "dimensions": [ 41 | { 42 | "Name": "LoadBalancer", 43 | "Value": "LoadyMcLoadFace" 44 | } 45 | ] 46 | }, 47 | { 48 | "namespace": "AWS/ApplicationELB", 49 | "metric_name": "HTTPCode_Target_5XX_Count", 50 | "dimensions": [ 51 | { 52 | "Name": "LoadBalancer", 53 | "Value": "LoadyMcLoadFace" 54 | } 55 | ] 56 | } 57 | ] 58 | }, 59 | "interesting_availability": { 60 | "numerator": [ 61 | { 62 | "namespace": "test/sli", 63 | "metric_name": "InterestingUrisSuccess", 64 | "dimensions": [ 65 | { 66 | "Name": "Hostname", 67 | "Value": "highly.reliable.foo" 68 | } 69 | ] 70 | } 71 | ], 72 | "denominator": [ 73 | { 74 | "namespace": "test/sli", 75 | "metric_name": "InterestingUrisTotal", 76 | "dimensions": [ 77 | { 78 | "Name": "Hostname", 79 | "Value": "highly.reliable.foo" 80 | } 81 | ] 82 | } 83 | ] 84 | }, 85 | "interesting_latency": { 86 | "window_days": 30, 87 | "numerator": [ 88 | { 89 | "namespace": "test/sli", 90 | "metric_name": "InterestingUrisSuccessLatency", 91 | "extended_statistic": "TC(0.1)", 92 | "dimensions": [ 93 | { 94 | "Name": "Hostname", 95 | "Value": "highly.reliable.foo" 96 | } 97 | ] 98 | } 99 | ], 100 | "denominator": [ 101 | { 102 | "namespace": "test/sli", 103 | "metric_name": "InterestingUrisSuccessLatency", 104 | "statistic": "SampleCount", 105 | "dimensions": [ 106 | { 107 | "Name": "Hostname", 108 | "Value": "highly.reliable.foo" 109 | } 110 | ] 111 | } 112 | ] 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /slo_lambda/src/windowed_slo_fixture_sad.json: -------------------------------------------------------------------------------- 1 | { 2 | "all_availability": { 3 | "nomnomnomerator": [ 4 | { 5 | "namespace": "AWS/ApplicationELB", 6 | "metric_name": "HTTPCode_Target_2XX_Count", 7 | "dimensions": [ 8 | { 9 | "Name": "LoadBalancer", 10 | "Value": "LoadyMcLoadFace" 11 | } 12 | ] 13 | }, 14 | { 15 | "namespace": "AWS/ApplicationELB", 16 | "metric_name": "HTTPCode_Target_3XX_Count", 17 | "dimensions": [ 18 | { 19 | "Name": "LoadBalancer", 20 | "Value": "LoadyMcLoadFace" 21 | } 22 | ] 23 | }, 24 | { 25 | "namespace": "AWS/ApplicationELB", 26 | "metric_name": "HTTPCode_Target_4XX_Count", 27 | "dimensions": [ 28 | { 29 | "Name": "LoadBalancer", 30 | "Value": "LoadyMcLoadFace" 31 | } 32 | ] 33 | } 34 | ], 35 | "denominator": [ 36 | { 37 | "namespace": "AWS/ApplicationELB", 38 | "metric_name": "RequestCount", 39 | "dimensions": [ 40 | { 41 | "Name": "LoadBalancer", 42 | "Value": "LoadyMcLoadFace" 43 | } 44 | ] 45 | }, 46 | { 47 | "namespace": "AWS/ApplicationELB", 48 | "metric_name": "HTTPCode_Target_5XX_Count", 49 | "dimensions": [ 50 | { 51 | "Name": "LoadBalancer", 52 | "Value": "LoadyMcLoadFace" 53 | } 54 | ] 55 | } 56 | ] 57 | }, 58 | "interesting_availability": { 59 | "window_days": "hi-mom", 60 | "numerator": { 61 | "namespace": "test/sli", 62 | "metric_name": "InterestingUrisSuccess", 63 | "dimensions": [ 64 | { 65 | "Name": "Hostname", 66 | "Value": "highly.reliable.foo" 67 | } 68 | ] 69 | }, 70 | "denominator": { 71 | "namespace": "test/sli", 72 | "metric_name": "InterestingUrisTotal", 73 | "dimensions": [ 74 | { 75 | "Name": "Hostname", 76 | "Value": "highly.reliable.foo" 77 | } 78 | ] 79 | } 80 | }, 81 | "interesting_latency": { 82 | "window_days": 30, 83 | "numerator": { 84 | "namespace": "test/sli", 85 | "metric_name": "InterestingUrisSuccessLatency", 86 | "meaningless_statistic": "TC(0.1)", 87 | "dimensions": [ 88 | { 89 | "Name": "Hostname", 90 | "Value": "highly.reliable.foo" 91 | } 92 | ] 93 | }, 94 | "denominator": { 95 | "namespace": "test/sli", 96 | "metric_name": "InterestingUrisSuccessLatency", 97 | "statistic": "SampleCount", 98 | "dimensions": [ 99 | { 100 | "Name": "Hostname", 101 | "Value": "highly.reliable.foo" 102 | } 103 | ] 104 | } 105 | }, 106 | "boring_latency": { 107 | "window_days": 30, 108 | "numerator": { 109 | "namespace": "test/sli", 110 | "metric_name": "BoringUrisSuccessLatency", 111 | "extended_statistic": "TC(0.1)", 112 | "multiplier": "lots", 113 | "dimensions": [ 114 | { 115 | "Name": "Hostname", 116 | "Value": "highly.reliable.foo" 117 | } 118 | ] 119 | }, 120 | "denominator": { 121 | "namespace": "test/sli", 122 | "metric_name": "BoringUrisSuccessLatency", 123 | "statistic": "SampleCount", 124 | "dimensions": [ 125 | { 126 | "Name": "Hostname", 127 | "Value": "highly.reliable.foo" 128 | } 129 | ] 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /slo_lambda/variables.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | name = "${var.env_name}-cloudwatch-sli" 3 | } 4 | 5 | variable "env_name" { 6 | description = "Environment name" 7 | type = string 8 | } 9 | 10 | variable "lambda_runtime" { 11 | description = "Lambda runtime" 12 | type = string 13 | default = "python3.12" 14 | } 15 | 16 | variable "slo_lambda_code" { 17 | type = string 18 | description = "Filename of the compressed lambda source code." 19 | default = "windowed_slo.zip" 20 | } 21 | 22 | variable "window_days" { 23 | description = "Global SLI window in days. A four-week window is a good general-purpose interval, based on https://sre.google/workbook/implementing-slos/" 24 | type = number 25 | default = 28 26 | } 27 | 28 | variable "namespace" { 29 | description = <= 1 for any 1 minute out of 15 eval periods 38 | statistic = "Sum" 39 | comparison_operator = "GreaterThanOrEqualToThreshold" 40 | threshold = 1 41 | period = 60 42 | datapoints_to_alarm = 1 43 | evaluation_periods = 15 44 | 45 | treat_missing_data = var.treat_missing_data 46 | 47 | alarm_actions = var.alarm_actions 48 | } 49 | 50 | resource "aws_cloudwatch_metric_alarm" "squid_total_requests" { 51 | alarm_name = "${local.alarm_environment}-squid-total-requests" 52 | alarm_description = <