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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## [1.0.83] - 2023-12-04

- [SSR Automation Documents] Centralize/modularize SSR's SSM automation documents

## [1.0.82] - 2023-12-01

- [CloudFront Geo Restriction] Mark country codes as nonsensitive in terraform.
Expand Down
18 changes: 18 additions & 0 deletions ssr-automation-documents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
SSR Automation Documents
===================

Set of modules for creating Simple Systems Manager (SSM) automation documents

## Usage

```hcl
resource "aws_sns_topic" "my_cool_topic" {
name = "my-cool-topic"
display_name = "My Super Cool SNS Topic"
}

module "scan_ecr_images" {
source = "github.com/massgov/mds-terraform-common//ssr-automation-documents/scan-ecr-image?ref=1.x"
default_alerting_topic = aws_sns_topic.my_cool_topic.arn
}
```
18 changes: 18 additions & 0 deletions ssr-automation-documents/check-tableau-license/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
locals {
region = coalesce(var.region, data.aws_region.current.name)
}

data "aws_region" "current" {}

resource "aws_ssm_document" "ssr_check_tableau_license" {
name = "SSR-CheckTableauLicenses"
document_format = "YAML"
document_type = "Automation"
content = templatefile(
"${path.module}/templates/check_tableau_licenses.yml",
{
region = local.region
alerts_topic_arn = var.default_alerting_topic
}
)
}
7 changes: 7 additions & 0 deletions ssr-automation-documents/check-tableau-license/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
output "document_arn" {
value = aws_ssm_document.ssr_check_tableau_license.arn
}

output "latest_document_version" {
value = aws_ssm_document.ssr_check_tableau_license.latest_version
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
description: |-
### Purpose
Checks the number of days til tableau license expires
### Usage
#### Parameters
- `AutomationAssumeRole` - (required, string); ARN of role to assume while performing this automation
- `InstanceIds` - (required, list<string>); EC2 instances on which automation should check licenses
- `AlertsTopicArn` - (optional, string); the ARN of the SNS Topic to which results should be pushed (default: `${alerts_topic_arn}`)
- `DaysTilLicenseExpirationThreshold` - (optional, number); number of days left until tableau license expires before automation should send an alert (default: 7)
schemaVersion: '0.3'
assumeRole: '{{AutomationAssumeRole}}'
parameters:
InstanceIds:
type: 'List<AWS::EC2::Instance::Id>'
AlertsTopicArn:
type: String
default: ${alerts_topic_arn}
DaysTilLicenseExpirationThreshold:
type: Integer
default: 7
AutomationAssumeRole:
type: 'AWS::IAM::Role::Arn'
mainSteps:
- name: PerformLicenseCheck
action: 'aws:runCommand'
onFailure: 'step:AlertOnError'
inputs:
DocumentName: AWS-RunShellScript
InstanceIds:
- '{{InstanceIds}}'
ServiceRoleArn: '{{AutomationAssumeRole}}'
MaxErrors: '1'
Parameters:
commands:
- sudo su
- 'if [[ -z $TABLEAU_SERVER_DATA_DIR_VERSION ]]; then source /etc/opt/tableau/tableau_server/environment.bash; fi'
- /opt/tableau/tableau_server/packages/customer-bin.$TABLEAU_SERVER_DATA_DIR_VERSION/tsm license list | grep -oP "\d\d?\/\d\d?\/\d\d"
outputs:
- Name: commandId
Selector: $.CommandId
Type: String
- name: EvaluateLicenseCheckOutput
action: 'aws:executeScript'
onFailure: 'step:AlertOnError'
inputs:
Runtime: python3.8
Handler: check_license_expiration
Script: |
import boto3
from datetime import datetime, timedelta

ssm_client = boto3.client('ssm')

def check_license_expiration(events, context):
instance_ids = events["instance_ids"]
license_list_command_id = events["license_list_command_id"]
days_til_license_expiration_threshold = events["days_til_license_expiration_threshold"]

now = datetime.now()

send_message = False
message_parts = [
f"Automated Tableau maintenance was performed at {now.strftime('%d %b %Y %H:%M')} UTC.",
"Results:"
]

for instance_id in instance_ids:
license_list_command_invocation = ssm_client.get_command_invocation(
CommandId=license_list_command_id,
InstanceId=instance_id
)

send_message_for_instance = False
instance_message_parts = []

if (license_list_command_invocation["Status"] != "Success"):
send_message_for_instance = True
command_url = f"https://${region}.console.aws.amazon.com/systems-manager/run-command/{license_list_command_id}?region=${region}"
instance_message_parts = [
f"- Command to check Tableau license status on EC2 instance `{instance_id}` "
+ f"unexpectedly returned a `{license_list_command_invocation['Status']}` status. "
+ f"(Command results: {command_url})"
]
else:
license_list_command_output = license_list_command_invocation["StandardOutputContent"]

[expiration_date_string, *_] = license_list_command_output.split("\n")
expiration_date = datetime.strptime(expiration_date_string, "%m/%d/%y")

send_message_for_instance = expiration_date <= (now + timedelta(days=days_til_license_expiration_threshold))
if (send_message_for_instance):
instance_message_parts = [
f"- Tableau license on EC2 instance `{instance_id}` expires on {expiration_date.date().strftime('%b %-d %Y')}",
]

send_message = send_message or send_message_for_instance
message_parts.extend(instance_message_parts)

message = "\n".join(message_parts)

return {
"message": message,
"send_message": "True" if send_message else "False"
}
InputPayload:
instance_ids: '{{InstanceIds}}'
license_list_command_id: '{{ PerformLicenseCheck.commandId}}'
days_til_license_expiration_threshold: '{{DaysTilLicenseExpirationThreshold}}'
outputs:
- Name: message
Selector: $.Payload.message
Type: String
- Name: sendMessage
Selector: $.Payload.send_message
Type: String
- name: BranchOnLicenseCheck
action: 'aws:branch'
inputs:
Choices:
- NextStep: NotifyResults
Variable: '{{EvaluateLicenseCheckOutput.sendMessage}}'
StringEquals: 'True'
Default: Exit
- name: NotifyResults
action: 'aws:executeAutomation'
isEnd: true
inputs:
DocumentName: AWS-PublishSNSNotification
RuntimeParameters:
TopicArn: '{{AlertsTopicArn}}'
Message: '{{EvaluateLicenseCheckOutput.message}}'
- name: Exit
action: 'aws:sleep'
isEnd: true
inputs:
Duration: PT5S
- name: AlertOnError
action: 'aws:executeAutomation'
inputs:
DocumentName: AWS-PublishSNSNotification
DocumentVersion: $LATEST
RuntimeParameters:
TopicArn: '{{AlertsTopicArn}}'
Message: 'There was an error running `SSR-CheckTableauLicenses` automation'
description: Alert
isEnd: true
10 changes: 10 additions & 0 deletions ssr-automation-documents/check-tableau-license/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
variable "region" {
type = string
description = "Region where automation should be performed. Defaults to provider-configured region"
default = null
}

variable "default_alerting_topic" {
type = string
description = "Default SNS topic to use for alerting"
}
9 changes: 9 additions & 0 deletions ssr-automation-documents/check-tableau-license/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 0.13"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 4.0"
}
}
}
18 changes: 18 additions & 0 deletions ssr-automation-documents/clean-rds-cluster-snapshots/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
locals {
region = coalesce(var.region, data.aws_region.current.name)
}

data "aws_region" "current" {}

resource "aws_ssm_document" "ssr_clean_up_rds_cluster_snapshots" {
name = "SSR-CleanUpRDSClusterSnapshots"
document_format = "YAML"
document_type = "Automation"
content = templatefile(
"${path.module}/templates/clean_up_rds_cluster_snapshots.yml",
{
region = local.region
alerts_topic_arn = var.default_alerting_topic
}
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
output "document_arn" {
value = aws_ssm_document.ssr_clean_up_rds_cluster_snapshots.arn
}

output "latest_document_version" {
value = aws_ssm_document.ssr_clean_up_rds_cluster_snapshots.latest_version
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
description: |-
### Purpose
This automation performs an RDS snapshot cleanup
### Usage
#### Parameters
- `AutomationAssumeRole` - (required, string) role to assume while performing this automation
- `DBClusterIdentifier` - (required, string) identifier of parent RDS cluster
- `RetentionPeriodDays` - (optional, Integer) number of days after which snapshots should be cleaned up (default: 90)
- `AlertsTopicArn` - (optional, string) the ARN of the SNS topic to alert in case of failure (default: `${alerts_topic_arn}`)
schemaVersion: '0.3'
assumeRole: '{{AutomationAssumeRole}}'
parameters:
AutomationAssumeRole:
type: 'AWS::IAM::Role::Arn'
description: Role to assume
DBClusterIdentifier:
type: String
description: Identifier of the parent RDS cluster
RetentionPeriodDays:
type: Integer
default: 90
description: 'number of days after which snapshots should be cleaned up '
AlertsTopicArn:
type: String
default: '${alerts_topic_arn}'
mainSteps:
- name: DeleteDbClusterSnapshots
action: 'aws:executeScript'
description: |-
## DeleteDbClusterSnapshots

Accepts an RDS cluster identifier and a retention period and deletes cluster snapshots
outside the retention period. If all snapshots are stale, the most recent will be
retained
timeoutSeconds: 300
onFailure: 'step:AlertOnError'
onCancel: Abort
inputs:
Runtime: python3.8
Handler: clean_up_db_snapshots
Script: |-
import boto3
from datetime import date

rds_client = boto3.client('rds', region_name='${region}')

def clean_up_db_snapshots(events, context):
db_cluster_identifier = events["db_cluster_identifier"]
retention_period_days = events["retention_period_days"]

describe_results = rds_client.describe_db_cluster_snapshots(
SnapshotType="manual",
DBClusterIdentifier=db_cluster_identifier,
IncludeShared=False,
IncludePublic=False
)

today = date.today()
deleted_snapshot_identifiers = []

snapshots_ascending = sorted(
describe_results["DBClusterSnapshots"],
key=(
lambda snapshot:
snapshot["SnapshotCreateTime"] if "SnapshotCreateTime" in snapshot
else snapshot["ClusterCreateTime"]
)
)

for snapshot in snapshots_ascending:
if snapshot["Status"] != "available":
continue
time_delta = today - snapshot["SnapshotCreateTime"].date()
if time_delta.days < retention_period_days:
continue
if not len(deleted_snapshot_identifiers) < len(snapshots_ascending) - 1:
# leave at least one snapshot, preferring the most recently created one
break

snapshot_identifier = snapshot["DBClusterSnapshotIdentifier"]
rds_client.delete_db_cluster_snapshot(
DBClusterSnapshotIdentifier=snapshot_identifier
)
deleted_snapshot_identifiers.append(snapshot_identifier)

return {'deleted_snapshot_identifiers': deleted_snapshot_identifiers}
InputPayload:
db_cluster_identifier: '{{DBClusterIdentifier}}'
retention_period_days: '{{RetentionPeriodDays}}'
outputs:
- Name: deletedClusterSnapshotIdentifiers
Selector: $.Payload.deleted_snapshot_identifiers
Type: StringList
isEnd: true
- name: AlertOnError
action: 'aws:executeAutomation'
inputs:
DocumentName: AWS-PublishSNSNotification
DocumentVersion: $LATEST
RuntimeParameters:
TopicArn: '{{AlertsTopicArn}}'
Message: 'Error running `SSR-CleanUpRDSSnapshots` automation: failed to delete `{{DBClusterIdentifier}}` snapshot(s)'
description: Alert
isEnd: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
variable "region" {
type = string
description = "Region where automation should be performed. Defaults to provider-configured region"
default = null
}

variable "default_alerting_topic" {
type = string
description = "Default SNS topic to use for alerting"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

terraform {
required_version = ">= 0.13"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 4.0"
}
}
}
Loading