Skip to content

Instantly share code, notes, and snippets.

@NickDarvey
Created March 20, 2022 23:31
Show Gist options
  • Save NickDarvey/731a6315b99e6a7b14383c09aad22616 to your computer and use it in GitHub Desktop.
Save NickDarvey/731a6315b99e6a7b14383c09aad22616 to your computer and use it in GitHub Desktop.
Deploy FSharp.AWS.DynamoDB record schema to DynamoDB with AWS CDK
namespace Stacks
open FSharp.AWS.DynamoDB
open Amazon.CDK
open Amazon.CDK.AWS.DynamoDB
open State
type StateProps
(
GeodeRegions : Set<string>
) =
inherit StackProps ()
member _.GeodeRegions = GeodeRegions
type StateOutputs = {
DataProtectionKeysTable : TableOutput
SigningKeysTable : TableOutput
GrantsTable : TableOutput
} with
member this.ToList () = [
this.DataProtectionKeysTable
this.SigningKeysTable
this.GrantsTable
]
and TableOutput = {
Table : Table
GetRegionalArn : string -> string
}
module private Table =
module MagicNumbers =
let TableMinCapacity = 1
let TableMaxCapacity = 5
let IndexCapacity = 2.
let TargetUtilizationPercent = 80
let from<'record> (props : {| Stack : Stack; Id: string; TableName : string; Regions : Set<string>; RemovalPolicy : RemovalPolicy |}) =
// WARNING: If you're here adding support for indexes, you need to investigate permissions because they may be technically a separate (AWS IAM) resource.
// https://github.com/aws/aws-cdk/issues/13703
let toAttribute (schema : KeyAttributeSchema) = Attribute (
Name = schema.AttributeName,
Type = (
if schema.KeyType = Amazon.DynamoDBv2.ScalarAttributeType.N then
AttributeType.NUMBER
elif schema.KeyType = Amazon.DynamoDBv2.ScalarAttributeType.S then
AttributeType.STRING
elif schema.KeyType = Amazon.DynamoDBv2.ScalarAttributeType.B then
AttributeType.BINARY
else
invalidOp $"Unknown attribute type '{schema.KeyType.Value}'"
)
)
let toGlobalSecondaryIndex provisioned (schema : TableKeySchema) = GlobalSecondaryIndexProps (
IndexName = schema.Type.IndexName.Value,
PartitionKey = (
schema.HashKey
|> toAttribute
),
SortKey = (
schema.RangeKey
|> Option.map toAttribute
|> Option.toObj
),
// That's all that's supported.
// https://github.com/fsprojects/FSharp.AWS.DynamoDB/blob/a1b7ca4a532fef8574c7d2a83a8d6ad428a5929b/src/FSharp.AWS.DynamoDB/RecordKeySchema.fs#L343-L346
ProjectionType = ProjectionType.ALL,
ReadCapacity = (
if provisioned
then System.Nullable MagicNumbers.IndexCapacity
else System.Nullable ()
),
WriteCapacity = (
if provisioned
then System.Nullable MagicNumbers.IndexCapacity
else System.Nullable ()
)
)
let billingMode =
if Set.isEmpty props.Regions
then BillingMode.PAY_PER_REQUEST
else BillingMode.PROVISIONED
let schema = RecordTemplate.Define<'record> ()
let table = Table(props.Stack, props.Id, TableProps(
BillingMode = billingMode,
ReplicationRegions = Set.toArray props.Regions,
TableName = props.TableName,
ContributorInsightsEnabled = true,
PartitionKey = (
schema.PrimaryKey.HashKey
|> toAttribute
),
SortKey = (
schema.PrimaryKey.RangeKey
|> Option.map toAttribute
|> Option.toObj
),
RemovalPolicy = props.RemovalPolicy
))
for lsi in schema.LocalSecondaryIndices do
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html#cfn-dynamodb-table-lsi
invalidOp $"
Local secondary indicies (LSIs) are not implemented so idk what to do for {lsi.Type.IndexName}.\n\
Be aware that changing LSIs on an existing table requires _replacement_."
for gsi in schema.GlobalSecondaryIndices do
table.AddGlobalSecondaryIndex (gsi |> toGlobalSecondaryIndex (billingMode = BillingMode.PROVISIONED))
if billingMode = BillingMode.PROVISIONED then do
table
.AutoScaleWriteCapacity(EnableScalingProps(MinCapacity = MagicNumbers.TableMinCapacity, MaxCapacity = MagicNumbers.TableMaxCapacity))
.ScaleOnUtilization(UtilizationScalingProps(TargetUtilizationPercent = MagicNumbers.TargetUtilizationPercent ))
{
Table = table
GetRegionalArn = fun region ->
if props.Stack.Region = region then table.TableArn
// https://github.com/aws/aws-cdk/blob/884d3a84a23900633fdefbd69cbaa76f864a3d9d/packages/%40aws-cdk/aws-dynamodb/lib/table.ts#L1600-L1605
elif props.Regions.Contains region then props.Stack.FormatArn(ArnComponents(
Region = region,
Service = "dynamodb",
Resource = "table",
ResourceName = table.TableName
))
else
invalidOp $"'{region}' not in known geode regions {props.Regions}."
}
type State (scope, id, props : StateProps) as stack =
inherit Stack (scope, id, props)
let grantsTable =
Table.from<Grants.MyRecord> {|
Stack = stack
Id = $"{id}MyTable"
TableName = "My"
Regions = props.GeodeRegions
RemovalPolicy = RemovalPolicy.RETAIN
|}
member _.Outputs = {
MyTable = myTable
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment