Skip to content

Instantly share code, notes, and snippets.

@jeremiahlukus
Created November 21, 2024 00:32
Show Gist options
  • Save jeremiahlukus/fe7cacaa5704a622f5d22e8d5af5f5ff to your computer and use it in GitHub Desktop.
Save jeremiahlukus/fe7cacaa5704a622f5d22e8d5af5f5ff to your computer and use it in GitHub Desktop.
Snowflake Password Rotation
locals {
// AWS Configuration
aws_region = "us-east-1"
availability_zone = "us-east-1a"
subnet_id = "subnet-"
// Team Information
team_name = ""
team_email = ""
pipeline_repo = ""
portfolio = ""
delivery_stream = ""
release_train = ""
// Snowflake Configuration
snowflake_name = ""
snowflake_url = ""
snowflake_role = ""
snowflake_warehouse = ""
// Secret Names
source_secret_name = ""
target_secret_name = "glue-snowflake-connection-secret"
// Lambda Configuration
lambda_function_name = "snowflake-secret-sync"
lambda_role_name = "snowflake_secret_sync_lambda_role"
lambda_timeout = 30
}
provider "aws" {
region = local.aws_region
default_tags {
tags = {
team = local.team_name
owner_email = local.team_email
email_recipients = local.team_email
pipeline_repo = local.pipeline_repo
portfolio = local.portfolio
delivery_stream = local.delivery_stream
release_train = local.release_train
}
}
ignore_tags {
key_prefixes = [
"cai:catalog",
"coxauto:ssm",
]
keys = [
"coxauto-ssm-managed-scan",
]
}
}
provider "alks" {
url = "https://alks.YOUR_DOMAIN.com/rest"
}
terraform {
required_version = "1.8.5"
backend "s3" {
bucket = ""
key = ""
region = "us-east-1"
}
required_providers {
aws = {
source = "hashicorp/aws"
}
alks = {
source = "Cox-Automotive/alks"
version = "~> 2.0"
}
}
}
data "aws_secretsmanager_secret" "existing_snowflake_secret" {
name = local.source_secret_name
}
data "aws_secretsmanager_secret_version" "existing_snowflake_secret" {
secret_id = data.aws_secretsmanager_secret.existing_snowflake_secret.id
}
resource "aws_secretsmanager_secret" "glue_snowflake_secret" {
name = local.target_secret_name
description = "Derived secret for Glue Snowflake connection. Source: ${data.aws_secretsmanager_secret.existing_snowflake_secret.name}"
}
resource "aws_secretsmanager_secret_version" "glue_snowflake_secret" {
secret_id = aws_secretsmanager_secret.glue_snowflake_secret.id
secret_string = jsonencode({
sfUser = jsondecode(data.aws_secretsmanager_secret_version.existing_snowflake_secret.secret_string)["USER"]
sfPassword = jsondecode(data.aws_secretsmanager_secret_version.existing_snowflake_secret.secret_string)["PASSWORD"]
sfWarehouse = local.snowflake_warehouse
})
}
resource "alks_iamrole" "lambda_role" {
name = local.lambda_role_name
type = "AWS Lambda"
include_default_policies = false
}
resource "aws_iam_role_policy" "lambda_policy" {
name = "snowflake_secret_sync_lambda_policy"
role = alks_iamrole.lambda_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"secretsmanager:GetSecretValue",
"secretsmanager:PutSecretValue"
]
Resource = [
data.aws_secretsmanager_secret.existing_snowflake_secret.arn,
aws_secretsmanager_secret.glue_snowflake_secret.arn
]
},
{
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Resource = ["arn:aws:logs:*:*:*"]
}
]
})
}
data "archive_file" "lambda_zip" {
type = "zip"
output_path = "${path.module}/secret_sync.zip"
source {
content = <<EOF
import { SecretsManager } from '@aws-sdk/client-secrets-manager';
const secretsManager = new SecretsManager();
// Helper function to sleep
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
export const handler = async (event, context) => {
try {
// Wait for 10 seconds
console.log('Waiting for 10 seconds to ensure event processing...');
await sleep(10000); // 10000 milliseconds = 10 seconds
// Get source secret
const sourceSecret = await secretsManager.getSecretValue({
SecretId: process.env.SOURCE_SECRET_ARN
});
// Parse source secret
const sourceData = JSON.parse(sourceSecret.SecretString);
// Create new secret value
const newSecretValue = {
sfUser: sourceData.USER,
sfPassword: sourceData.PASSWORD,
sfWarehouse: process.env.WAREHOUSE_NAME,
lastUpdated: new Date().toISOString()
};
// Update target secret
await secretsManager.putSecretValue({
SecretId: process.env.TARGET_SECRET_ARN,
SecretString: JSON.stringify(newSecretValue)
});
console.log('Secret synchronized successfully');
return {
statusCode: 200,
body: 'Secret synchronized successfully'
};
} catch (error) {
console.error('Error:', error);
throw error;
}
};
EOF
filename = "index.mjs"
}
}
resource "aws_lambda_function" "secret_sync" {
filename = data.archive_file.lambda_zip.output_path
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
function_name = local.lambda_function_name
role = alks_iamrole.lambda_role.arn
handler = "index.handler"
runtime = "nodejs20.x"
timeout = local.lambda_timeout
environment {
variables = {
SOURCE_SECRET_ARN = data.aws_secretsmanager_secret.existing_snowflake_secret.arn
TARGET_SECRET_ARN = aws_secretsmanager_secret.glue_snowflake_secret.arn
WAREHOUSE_NAME = local.snowflake_warehouse
}
}
}
resource "aws_cloudwatch_event_rule" "secret_changed" {
name = "track-snowflake-secret-changes"
description = "Track changes to the source Snowflake secret"
event_pattern = jsonencode({
source = ["aws.secretsmanager"]
detail-type = ["AWS API Call via CloudTrail"]
detail = {
eventSource = ["secretsmanager.amazonaws.com"]
eventName = ["PutSecretValue", "UpdateSecret", "RotationSucceeded", "CreateSecret"]
requestParameters = {
secretId = [
data.aws_secretsmanager_secret.existing_snowflake_secret.name,
data.aws_secretsmanager_secret.existing_snowflake_secret.arn
]
}
}
})
}
resource "aws_cloudwatch_event_target" "lambda_target" {
rule = aws_cloudwatch_event_rule.secret_changed.name
target_id = "SyncSnowflakeSecrets"
arn = aws_lambda_function.secret_sync.arn
}
resource "aws_lambda_permission" "allow_eventbridge" {
statement_id = "AllowEventBridgeInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.secret_sync.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.secret_changed.arn
}
resource "aws_glue_connection" "snowflake_connection" {
name = local.snowflake_name
connection_type = "SNOWFLAKE"
connection_properties = {
SparkProperties = jsonencode({
sfUrl = local.snowflake_url
secretId = aws_secretsmanager_secret.glue_snowflake_secret.name
sfRole = local.snowflake_role
})
}
physical_connection_requirements {
subnet_id = local.subnet_id
availability_zone = local.availability_zone
}
depends_on = [aws_secretsmanager_secret_version.glue_snowflake_secret]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment