Skip to content

Instantly share code, notes, and snippets.

@arbon
Last active February 23, 2024 07:04
Show Gist options
  • Save arbon/fd001ae9040fc2b675ed31d6f022d784 to your computer and use it in GitHub Desktop.
Save arbon/fd001ae9040fc2b675ed31d6f022d784 to your computer and use it in GitHub Desktop.
TypeScript / AWS CDK / Implements various related stacks. Experiments with classical inheritance...
/**
* @license
* Copyright (c) 2024 Zach Arbon. All rights reserved.
* SPDX-License-Identifier: MIT
*/
import { Construct } from 'constructs'
import { CfnOutput, Duration, Environment, RemovalPolicy, Stack, Tags } from 'aws-cdk-lib'
import { DataClassification, Package, Stage } from '..'
import { BlockPublicAccess, Bucket, BucketEncryption, BucketProps, ObjectOwnership, StorageClass } from 'aws-cdk-lib/aws-s3'
import { Dashboard, DimensionsMap, GraphWidget, Metric, TextWidget } from 'aws-cdk-lib/aws-cloudwatch'
import { ILogGroup } from 'aws-cdk-lib/aws-logs'
/**
* Defines stack creation properties.
*/
export interface AppStackProps {
/**
* The resource removal policy as a function of `cdk.RemovalPolicy`.
* By default, when a resource is removed, it will be physically destroyed.
*/
dataRemovalPolicy?: RemovalPolicy
/**
* The classification of application data.
*/
dataClassification?: DataClassification
/**
* Provides a description of the stack.
*/
description?: string
/**
* Defines the stack environment.
*/
env?: Environment
/**
* The SDLC development stage of the application as defined by `Stage`.
*/
stage: Stage
}
/**
* Extends the base cdk.Stack class to provide common features across Stack subclasses.
*/
export class AppStack extends Stack implements AppStackProps {
/**
* The SDLC development stage of the application as defined by `Stage`.
*/
readonly stage: Stage
/**
* The resource removal policy.
*/
readonly dataRemovalPolicy: RemovalPolicy
/**
* The classification of application data.
*/
readonly dataClassification: DataClassification
/**
* The Cloudwatch dashboard for the stack.
*/
dashboard: Dashboard
/**
* Creates a new stack, setting properties defined in `AppStackProps` for subclasses.
* Sets the resource data removal policy (for Cloudformation management) and data classification (for tags).
*
* @param scope Parent of this stack, usually an `App`
* @param id The construct ID of this stack.
* @param props Stack properties as defined by `AppStackProps`.
*/
constructor(scope: Construct, id?: string, props: Partial<AppStackProps> = {}) {
super(scope, id, props)
this.dataClassification = props?.dataClassification || DataClassification.PRIVATE
this.dataRemovalPolicy = props?.dataRemovalPolicy || RemovalPolicy.DESTROY
this.stage = props?.stage || Stage.DEVELOPMENT
// Add package metadata.
this.addMetadata('Package', {
Name: Package.name, Author: Package.author, Version: Package.version,
RepositoryUrl: Package?.repository?.url
})
// For now, use our utility method to add tags. Ideally, these are set via props.
// See: https://github.com/aws/aws-cdk/issues/20549
this.addTags({
'app:data:classification': this.dataClassification,
'app:data:removal-policy': this.dataRemovalPolicy,
'app:package:author': Package.author,
'app:package:name': Package.name,
'app:package:version': Package.version,
'app:stage': this.stage,
})
this.dashboard = new Dashboard(this, 'Dashboard')
this.dashboard.addWidgets(new TextWidget({
markdown: `# ${Package.name}-${Package.version}
**Stack** ${this.stackId} / **Region** ${this.region} / **Stage** ${this.stage} / **Data Classification** ${this.dataClassification} / **Removal Policy** ${this.dataRemovalPolicy}`,
height: 2, width: 24
}))
}
/**
* Creates metrics associated with Cloudwatch.
* Adds metrics to the stack dashboard.
*/
createLogMetrics(logGroup: ILogGroup): void {
const dimensionsMap: DimensionsMap = {
'LogGroupName': logGroup.logGroupName
}
const incomingBytes = new Metric({
dimensionsMap,
namespace: 'AWS/Logs', metricName: 'IncomingBytes', statistic: 'min',
})
const incomingLogEvents = new Metric({
dimensionsMap,
namespace: 'AWS/Logs', metricName: 'IncomingLogEvents', statistic: 'sum'
})
this.dashboard.addWidgets(
new GraphWidget({
title: `Cloudwatch / Events (${logGroup.logGroupName})`,
width: 12,
height: 5,
left: [
incomingLogEvents
],
}),
new GraphWidget({
title: `Cloudwatch / Bytes (${logGroup.logGroupName})`,
width: 12,
height: 5,
left: [
incomingBytes,
incomingBytes.with({
statistic: 'avg'
}),
incomingBytes.with({
statistic: 'max'
})
]
})
)
}
/**
* Tag this resource based on object key-value pairs.
* @param {object} tags An object containing properties used to set tags.
*/
addTags(tags: Record<string, string> = {}): void {
for (const [key, value] of Object.entries(tags)) {
Tags.of(this).add(key, value)
}
}
/**
* Creates a new encrypted, private bucket.
*
* @param id The bucket id.
* @param bucketProps Bucket properties for custom behavior.
* @returns A new bucket.
*/
createBucket(id: string, bucketProps?: BucketProps): Bucket {
const bucket = new Bucket(this, id, {
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
encryption: BucketEncryption.S3_MANAGED,
enforceSSL: true,
objectOwnership: ObjectOwnership.BUCKET_OWNER_PREFERRED,
removalPolicy: this.dataRemovalPolicy,
...bucketProps
})
this.export(`${id}Arn`, bucket.bucketArn)
this.export(`${id}Name`, bucket.bucketName)
return bucket
}
/**
* Creates a private, encrypted bucket for logs.
* Lifecycle rules are added to expire and transition objects.
*
* @param id The bucket id.
* @param bucketProps Bucket properties for custom behavior.
* @returns A new bucket.
*/
createLogsBucket(id: string, bucketProps?: BucketProps): Bucket {
return this.createBucket(id, {
lifecycleRules: [
{
expiration: Duration.days(365)
},
{
transitions: [
{
storageClass: StorageClass.INFREQUENT_ACCESS,
transitionAfter: Duration.days(60)
}
]
}
],
...bucketProps
})
}
/**
* Appends a suffix that contains, account, stage and region.
*
* @param value The base value to use.
* @returns The value with a
*/
appendSuffix(value: string) {
return `${value}-${this.account}-${this.stage}-${this.region}`
}
/**
* Gets a value for the key from the current context, potentially returning a default value.
*
* @param key The context key to try.
* @param defaultValue The default value.
* @returns The value for the key or default value, undefined.
*/
getContextValue(key: string, defaultValue: string) {
return this.node.tryGetContext(key) || defaultValue
}
/**
* Creates a `CfnOutput` value for the stack.
*
* @param name The name of the output value.
* @param value The value to export.
* @returns The resulting `CfnOutput`
*/
export(name: string, value: string): CfnOutput {
return new CfnOutput(this, name, {
exportName: `${this.stackName}:${name}`, value
})
}
}
/**
* @license
* Copyright (c) 2024 Zach Arbon. All rights reserved.
* SPDX-License-Identifier: MIT
*/
import { Construct } from 'constructs'
import { EmailIdentity, EmailIdentityProps, Identity, ReceiptRuleSet, ReceiptRuleSetProps, TlsPolicy } from 'aws-cdk-lib/aws-ses'
import { CfnRecordSetGroup, HostedZone, RecordType } from 'aws-cdk-lib/aws-route53'
import { Sns } from 'aws-cdk-lib/aws-ses-actions'
import { TopicStack, TopicStackProps } from './TopicStack'
import { ServicePrincipal } from 'aws-cdk-lib/aws-iam'
/**
* Defines properties for a key stack.
*/
export interface EmailTopicProps extends TopicStackProps {
/**
* The subdomain for mail/topic use.
*/
subdomain: string
/**
* An array of names or "local parts" used to build a list of recipients
* given the `domainName` and `subdomain`.
*/
recipients: string[]
/**
* The TLD associated with a Route53 hosted zone.
*/
domainName: string
/**
* Email identity properties for custom behavior.
*/
emailIdentityProps?: EmailIdentityProps
/**
* Email rule set properties for custom behavior.
*/
receiptRuleSetProps?: ReceiptRuleSetProps
}
export class EmailTopicStack extends TopicStack {
/**
* The default DNS subdomain for email to topic delivery.
*/
static SUBDOMAIN: string = 'topics'
/**
* Creates resources to forward email to an SNS topic. Specifically:
*
* - a new `AWS::SES::EmailIdentity` associated with an existing Route 53 zone.
* - a new `AWS::Route53::RecordSetGroup` for subdomain DKIM and MX records.
* - a new `AWS::SES::ReceiptRuleSet` that maps addresses to a topic.
* - a new or existing `AWS::KMS::Key` for encryption.
* - a new or existing `AWS::SNS::Topic` to receive messages.
*
* NOTE: The SES rule set must be manually activated.
*
* @param scope Parent of this stack, usually an `App`.
* @param id The construct ID of this stack.
* @param props Stack properties as defined by `EmailTopicProps`.
*/
constructor(scope: Construct, id: string, props: Partial<EmailTopicProps> = {}) {
super(scope, id, props)
props.subdomain = props.subdomain || EmailTopicStack.SUBDOMAIN
if (!props.domainName) {
throw new Error('A domain name for a Route53 hosted zone is required.')
}
if (!props.recipients || props.recipients.length < 1) {
throw new Error(`One or more recipients are required.`)
}
// Get the zone from our domain name.
const hostedZone = HostedZone.fromLookup(this, 'HostedZone', {
domainName: props.domainName
})
// Create a new identity for our subdomain.
const mailDomain = `${props.subdomain}.${hostedZone.zoneName}`
const identity = new EmailIdentity(this, `EmailIdentity`, {
identity: Identity.domain(mailDomain),
...props.emailIdentityProps
})
identity.applyRemovalPolicy(this.dataRemovalPolicy)
this.export('EmailIdentityName', identity.emailIdentityName)
// Create CNAME records for DKIM values. Add an MX record for inbound mail.
const ttl = '600'
const recordSetGroup = new CfnRecordSetGroup(this, 'RecordSetGroup', {
hostedZoneId: hostedZone.hostedZoneId,
recordSets: [{
name: identity.dkimDnsTokenName1,
resourceRecords: [identity.dkimDnsTokenValue1],
type: RecordType.CNAME,
ttl
},
{
name: identity.dkimDnsTokenName2,
resourceRecords: [identity.dkimDnsTokenValue2],
type: RecordType.CNAME,
ttl
},
{
name: identity.dkimDnsTokenName3,
resourceRecords: [identity.dkimDnsTokenValue3],
type: RecordType.CNAME,
ttl
},
{
name: mailDomain,
resourceRecords: [`10 inbound-smtp.${this.region}.amazonaws.com`],
type: RecordType.MX,
ttl
}
]
})
recordSetGroup.applyRemovalPolicy(this.dataRemovalPolicy)
recordSetGroup.node.addDependency(identity)
// Create a receipt rule set with an SNS action for our topic.
const receiptRuleSet = new ReceiptRuleSet(this, 'ReceiptRuleSet', {
rules: [{
actions: [
new Sns({
topic: this.topic
})
],
enabled: true,
// Add recipients based on our mail subdomain.
recipients: props.recipients.map((value) => {
return `${value}@${mailDomain}`
}),
tlsPolicy: TlsPolicy.REQUIRE
}],
...props.receiptRuleSetProps
})
receiptRuleSet.applyRemovalPolicy(this.dataRemovalPolicy)
receiptRuleSet.node.addDependency(this.topic)
this.export('ReceiptRuleSetName', receiptRuleSet.receiptRuleSetName)
// Update key and topic policies.
const sesService = new ServicePrincipal('ses.amazonaws.com')
this.topic.grantPublish(sesService)
this.key.grantEncryptDecrypt(sesService)
console.log('NOTE: When complete, please activate the SES rule set.',
`https://${this.region}.console.aws.amazon.com/ses/home?region=${this.region}#/email-receiving`)
}
}
/**
* @license
* Copyright (c) 2024 Zach Arbon. All rights reserved.
* SPDX-License-Identifier: MIT
*/
export * from "./stacks"
import { readFileSync } from 'fs'
/**
* Defines regions.
*/
export class Regions {
static US_EAST = ['us-east-1', 'us-east-2']
static US_WEST = ['us-west-1', 'us-west-2']
static US = [...this.US_EAST, ...this.US_WEST]
}
/**
* Provides package metadata.
*/
export const Package = JSON.parse(readFileSync('package.json', {
encoding: 'utf-8'
}))
/**
* Defines stack application stages.
*/
export enum Stage {
DEVELOPMENT = 'dev',
PRODUCTION = 'prod',
STAGING = 'staging'
}
/**
* Defines data classification types for stack metadata.
*/
export enum DataClassification {
CONFIDENTIAL = 'confidential',
INTERNAL = 'internal',
PRIVATE = 'private',
PUBLIC = 'public',
RESTRICTED = 'restricted'
}
/**
* @license
* Copyright (c) 2024 Zach Arbon. All rights reserved.
* SPDX-License-Identifier: MIT
*/
import { Construct } from 'constructs'
import { IKey, Key, KeyProps } from 'aws-cdk-lib/aws-kms'
import { AppStack, AppStackProps } from './AppStack'
/**
* Defines properties for a key stack.
*/
export interface KeyStackProps extends AppStackProps {
/**
* The ARN of an existing KMS key.
*/
keyArn?: string
/**
* The properties for creation of a new KMS key.
*/
keyProps?: KeyProps
}
export class KeyStack extends AppStack {
/** The KMS key. */
readonly key: IKey
/**
* Creates a new `KeyStack`.
*
* @param scope Parent of this stack, usually an `App`.
* @param id The construct ID of this stack.
* @param props Stack properties as defined by `KeyStackProps`.
*/
constructor(scope: Construct, id: string, props: Partial<KeyStackProps> = {}) {
super(scope, id, props)
// Use an existing key as needed.
const key = 'Key'
if (props.keyArn) {
this.key = Key.fromKeyArn(this, key, props.keyArn)
} else {
this.key = new Key(this, key, {
enableKeyRotation: true,
removalPolicy: this.dataRemovalPolicy,
...props.keyProps
})
}
this.export('KeyArn', this.key.keyArn)
}
}
/**
* @license
* Copyright (c) 2024 Zach Arbon. All rights reserved.
* SPDX-License-Identifier: MIT
*/
import { Duration, } from 'aws-cdk-lib'
import { Construct } from 'constructs'
import { IQueue, Queue, QueueEncryption, QueueProps } from 'aws-cdk-lib/aws-sqs'
import { SqsSubscription } from 'aws-cdk-lib/aws-sns-subscriptions'
import { TopicStack, TopicStackProps } from './TopicStack'
/**
* Defines properties for an SQS queue stack.
*/
export interface QueueStackProps extends TopicStackProps {
/**
* Create a dead-letter queue?
*/
deadLetterQueueEnabled: boolean
/**
* Propteries for dead-letter queue creation.
*/
deadLetterQueueProps?: QueueProps
/**
* The ARN of an existing queue to use.
*/
queueArn?: string
/**
* Propteries for queue creation.
*/
queueProps?: QueueProps
}
export class QueueStack extends TopicStack {
/**
* The SQS queue.
*/
readonly queue: IQueue
/**
* The SQS dead-letter queue.
*/
readonly deadLetterQueue: IQueue
/**
* Creates a new `QueueStack` based on `TopicStack`.
*
* @param scope Parent of this stack, usually an `App`.
* @param id The construct ID of this stack.
* @param props Stack properties as defined by `TopicStackProps`.
*/
constructor(scope: Construct, id: string, props: Partial<QueueStackProps> = {}) {
super(scope, id, props)
const queue = 'Queue'
if (props.queueArn) {
this.queue = Queue.fromQueueArn(this, queue, props.queueArn)
} else {
props.deadLetterQueueEnabled = props.deadLetterQueueEnabled || true
if (props.deadLetterQueueEnabled) {
this.deadLetterQueue = new Queue(this, 'DeadLetterQueue', {
encryption: QueueEncryption.KMS,
encryptionMasterKey: this.key,
retentionPeriod: Duration.days(14),
...props.deadLetterQueueProps
})
this.deadLetterQueue.applyRemovalPolicy(this.dataRemovalPolicy)
this.export('DeadLetterQueueName', this.deadLetterQueue.queueName)
this.export('DeadLetterQueueArn', this.deadLetterQueue.queueArn)
this.export('DeadLetterQueueUrl', this.deadLetterQueue.queueUrl)
}
this.queue = new Queue(this, queue, {
encryption: QueueEncryption.KMS,
encryptionMasterKey: this.key,
deadLetterQueue: props.deadLetterQueueEnabled ? {
maxReceiveCount: 5, queue: this.deadLetterQueue
} : undefined,
retentionPeriod: Duration.days(14),
...props.queueProps
})
}
this.queue.applyRemovalPolicy(this.dataRemovalPolicy)
this.topic.addSubscription(new SqsSubscription(this.queue))
this.export('QueueName', this.queue.queueName)
this.export('QueueArn', this.queue.queueArn)
this.export('QueueUrl', this.queue.queueUrl)
}
}
/**
* @license
* Copyright (c) 2024 Zach Arbon. All rights reserved.
* SPDX-License-Identifier: MIT
*/
import { Construct } from 'constructs'
import { ITopic, Topic, TopicProps } from 'aws-cdk-lib/aws-sns'
import { EmailSubscription } from 'aws-cdk-lib/aws-sns-subscriptions'
import { KeyStack, KeyStackProps } from './KeyStack'
import { DimensionsMap, GraphWidget, Metric } from 'aws-cdk-lib/aws-cloudwatch'
/**
* Defines properties for an SNS topic stack.
*/
export interface TopicStackProps extends KeyStackProps {
/**
* The ARN of an existing topic to use.
*/
topicArn?: string
/**
* Propteries for new topic creation.
*/
topicProps?: TopicProps
/**
* Defines one or more email addresses for topic subscription(s).
*/
emailSubscribers?: string[]
}
export class TopicStack extends KeyStack {
/**
* The SNS topic.
*/
readonly topic: ITopic
/**
* Creates a new `TopicStack` based on `KmsKeyStack`.
*
* @param scope Parent of this stack, usually an `App`.
* @param id The construct ID of this stack.
* @param props Stack properties as defined by `LogGroupStackProps`.
*/
constructor(scope: Construct, id: string, props: Partial<TopicStackProps> = {}) {
super(scope, id, props)
const topic = 'Topic'
if (props.topicArn) {
this.topic = Topic.fromTopicArn(this, topic, props.topicArn)
} else {
this.topic = new Topic(this, topic, {
masterKey: this.key, ...props.topicProps
})
this.topic.node.addDependency(this.key)
this.topic.applyRemovalPolicy(this.dataRemovalPolicy)
}
props.emailSubscribers && props.emailSubscribers.forEach(emailAddress => {
this.topic.addSubscription(new EmailSubscription(emailAddress))
})
this.export('TopicArn', this.topic.topicArn)
this.createTopicMetrics(this.topic)
}
/**
* Creates Cloudwatch dashboard widgets.
*/
createTopicMetrics(topic: ITopic): void {
const dimensionMap: DimensionsMap = {
'TopicName': topic.topicName
}
const publishSize = new Metric({
dimensionsMap: dimensionMap,
namespace: 'AWS/SNS', metricName: 'PublishSize', statistic: 'min'
})
this.dashboard.addWidgets(
new GraphWidget({
title: `SNS / Messages (${topic.topicName})`,
width: 12,
height: 5,
left: [
new Metric({
dimensionsMap: dimensionMap,
namespace: 'AWS/SNS', metricName: 'NumberOfMessagesPublished', statistic: 'sum', label: 'Published'
}),
new Metric({
dimensionsMap: dimensionMap,
namespace: 'AWS/SNS', metricName: 'NumberOfNotificationsDelivered', statistic: 'sum', label: 'Delivered'
}),
],
right: [
new Metric({
dimensionsMap: dimensionMap,
namespace: 'AWS/SNS', metricName: 'NumberOfNotificationsFailed', statistic: 'sum', label: 'Failed'
})
]
}),
new GraphWidget({
title: `SNS / Message Size (${topic.topicName})`,
width: 12,
height: 5,
left: [
publishSize,
publishSize.with({
statistic: 'avg'
}),
publishSize.with({
statistic: 'max'
})
],
right: [
publishSize.with({
statistic: 'sum'
}),
]
})
)
}
}
/**
* @license
* Copyright (c) 2024 Zach Arbon. All rights reserved.
* SPDX-License-Identifier: MIT
*/
import * as cdk from 'aws-cdk-lib'
import * as iam from 'aws-cdk-lib/aws-iam'
import * as kms from 'aws-cdk-lib/aws-kms'
import * as s3 from 'aws-cdk-lib/aws-s3'
import { Construct } from 'constructs'
import { AppStack, AppStackProps } from './AppStack'
import { GraphWidget, Metric } from 'aws-cdk-lib/aws-cloudwatch'
import { CloudWatchStorageType } from '../metrics'
/**
* Defines properties for a bucket stack.
*/
export interface WebBucketStackProps extends AppStackProps {
/**
* Defines the S3 bucket for regional replication.
*/
replicaBucket?: s3.Bucket | undefined
/**
* Defines the KMS key used for replication bucket encryption.
*/
replicaBucketKey?: kms.IKey | undefined
/**
* Defines the object expiration period.
*/
dataExpiration?: cdk.Duration | undefined
/**
* Defines the object transition period.
*/
dataTransition?: cdk.Duration | undefined
/**
* Defines the storage class to transition to.
*/
dataTransitionClass?: s3.StorageClass | undefined
/**
* Defines the expiration period for older versions of objects.
*/
dataVersionExpiration?: cdk.Duration | undefined
}
/**
* Provides a stack that creates S3 buckets for web content. Features include:
* - Bucket objects are private and encrypted via SSE with a master key managed by S3.
* - Lifecycle rules to transition and expire older objects are defined.
* - Cross-region replication is supported given a `replicaBucket` and `replicaBucketKey`.
* - Resources implement a standard removal policy.
*/
export class WebBucketStack extends AppStack implements WebBucketStackProps {
/**
* Provides a bucket for logs.
*/
logsBucket: s3.Bucket
/**
* Provides a bucket for content.
*/
contentBucket: s3.Bucket
/**
* The KMS key used for bucket encryption.
*/
bucketKey: kms.IKey
crossRegionReferences: true
dataExpiration: cdk.Duration
dataTransition: cdk.Duration
dataTransitionClass: s3.StorageClass
dataVersionExpiration: cdk.Duration
replicaBucket?: s3.Bucket | undefined
replicaBucketKey?: kms.IKey | undefined
/**
* Creates a new web bucket stack with a handful of resources:
* - S3 Buckets - One for content and one for server logs.
* - IAM Role - Optional for policies associated with replication.
*
* @param scope Parent of this stack, usually an `App`
* @param id The construct ID of this stack.
* @param props Stack properties as defined by `WebBucketStackProps`.
*/
constructor(scope: Construct, id?: string, props: Partial<WebBucketStackProps> = {}) {
super(scope, id, props)
this.dataExpiration = props.dataExpiration || cdk.Duration.days(365)
this.dataTransition = props.dataTransition || cdk.Duration.days(60)
this.dataTransitionClass = props.dataTransitionClass || s3.StorageClass.INFREQUENT_ACCESS
this.dataVersionExpiration = props.dataVersionExpiration || cdk.Duration.days(30)
this.replicaBucket = props.replicaBucket
this.replicaBucketKey = props.replicaBucketKey
// Establish the key used for bucket encryption.
// TODO: Support other KMS keys; Cloudfront may have issues.
this.bucketKey = kms.Key.fromLookup(this, 'Key', {
aliasName: 'alias/aws/s3'
})
// Create a bucket for server logs?
this.logsBucket = this.createLogsBucket('Logs', {
bucketName: `${id}-logs-${this.account}-${this.stage}-${this.region}`,
})
// Create a bucket for content.
const serverAccessLogsPrefix = 'server/'
this.contentBucket = this.createBucket('Content', {
bucketName: this.appendSuffix(`${id}-content`),
lifecycleRules: [
{
noncurrentVersionExpiration: this.dataVersionExpiration
}
],
serverAccessLogsBucket: this.logsBucket,
serverAccessLogsPrefix: serverAccessLogsPrefix,
versioned: true
})
this.createBucketMetrics(this.contentBucket)
this.createBucketMetrics(this.logsBucket, CloudWatchStorageType.STANDARD_IA_STORAGE)
// Enable regional replication?
// Create a replication role and add a replication configuration to the content bucket.
if (!this.replicaBucket || !this.replicaBucketKey) {
return
}
const replicationRole = new iam.Role(this, 'ReplicationRole', {
assumedBy: new iam.ServicePrincipal('s3.amazonaws.com')
})
replicationRole.applyRemovalPolicy(this.dataRemovalPolicy)
// Allow retrieval of object lists, tags, ACLs, etc.
// See: https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazons3.html#amazons3-resources-for-iam-policies
replicationRole.addToPolicy(new iam.PolicyStatement({
actions: [
's3:GetObjectVersionAcl',
's3:GetObjectVersionForReplication',
's3:GetObjectVersionTagging',
's3:GetReplicationConfiguration',
's3:ListBucket'
],
resources: [
this.contentBucket.bucketArn, `${this.contentBucket.bucketArn}/*`
]
}))
// Allow decryption via S3 for content bucket objects.
replicationRole.addToPolicy(new iam.PolicyStatement({
actions: ['kms:Decrypt'],
resources: [this.bucketKey.keyArn],
conditions: {
StringLike: {
'kms:ViaService': `s3.${this.region}.amazonaws.com`,
'kms:EncryptionContext:aws:s3:arn': [
`${this.contentBucket.bucketArn}/*`
]
}
}
}))
// Allow encryption via S3 for replica bucket objects.
replicationRole.addToPolicy(new iam.PolicyStatement({
actions: ['kms:Encrypt'],
resources: [this.replicaBucketKey.keyArn],
conditions: {
StringLike: {
'kms:ViaService': `s3.${this.region}.amazonaws.com`,
'kms:EncryptionContext:aws:s3:arn': [
`${this.replicaBucket.bucketArn}/*`
]
}
}
}))
// Allow replication actions for replica bucket objects.
replicationRole.addToPolicy(new iam.PolicyStatement({
actions: [
's3:ReplicateDelete',
's3:ReplicateObject',
's3:ReplicateTags'
],
resources: [`${this.replicaBucket.bucketArn}/*`]
}))
// Add a replication configuration to the bucket.
const bucket = this.contentBucket.node.defaultChild as s3.CfnBucket
bucket && bucket.addPropertyOverride('ReplicationConfiguration', {
Role: replicationRole.roleArn,
Rules: [
{
Destination: {
Bucket: this.replicaBucket.bucketArn
},
Status: 'Enabled'
}
]
})
}
/**
* Creates metrics associated with S3 buckets.
* Adds metrics to the stack dashboard.
*/
createBucketMetrics(bucket: s3.Bucket,
storageType: CloudWatchStorageType = CloudWatchStorageType.STANDARD_STORAGE): void {
this.dashboard.addWidgets(
new GraphWidget({
title: `S3 / Size & Objects (${bucket.bucketName})`,
width: 24,
height: 6,
left: [
new Metric({
dimensionsMap: {
'BucketName': bucket.bucketName, 'StorageType': storageType
},
namespace: 'AWS/S3', metricName: 'BucketSizeBytes', statistic: 'avg'
})
],
right: [
new Metric({
dimensionsMap: {
'BucketName': bucket.bucketName, 'StorageType': 'AllStorageTypes'
},
namespace: 'AWS/S3', metricName: 'NumberOfObjects', statistic: 'avg'
})
]
})
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment