Skip to content

Commit

Permalink
added distributions, websites and cnames
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbarbour committed Nov 3, 2023
1 parent c58c375 commit 03cbcd6
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 27 deletions.
25 changes: 15 additions & 10 deletions src/cloudformation/iam/PolicyDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,33 @@ export function iamPolicy(policyDocument: PolicyDocument): PolicyDocument { retu
export class Policy {
private constructor(readonly document: PolicyDocument = {statement: [], version: "2012-10-17"}) { }

grant(action: Action | Action[], effect: PolicyStatement['effect'], onResource: PolicyStatement['resource']): this {
grant(
action: Action | Action[],
effect: PolicyStatement['effect'],
onResource: PolicyStatement['resource'],
statement?: Omit<PolicyStatement, 'resource' | 'action' | 'effect'>
): this {
this.document.statement = [
...(this.document.statement ?? []),
{ action, effect, resource: onResource }
{ action, effect, resource: onResource, ...statement }
]
return this;
}

allow(action: Action | Action[], onResource: PolicyStatement['resource']): this {
return this.grant(action, 'Allow', onResource)
allow(action: Action | Action[], onResource: PolicyStatement['resource'], statement?: Omit<PolicyStatement, 'resource' | 'action' | 'effect'>): this {
return this.grant(action, 'Allow', onResource, statement)
}

deny(action: Action | Action[], onResource: PolicyStatement['resource']): this {
return this.grant(action, 'Deny', onResource)
deny(action: Action | Action[], onResource: PolicyStatement['resource'], statement?: Omit<PolicyStatement, 'resource' | 'action' | 'effect'>): this {
return this.grant(action, 'Deny', onResource, statement)
}

static allow(action: Action | Action[], onResource: PolicyStatement['resource']): Policy {
return new Policy().allow(action, onResource);
static allow(action: Action | Action[], onResource: PolicyStatement['resource'], statement?: Omit<PolicyStatement, 'resource' | 'action' | 'effect'>): Policy {
return new Policy().allow(action, onResource, statement);
}

static deny(action: Action | Action[], onResource: PolicyStatement['resource']): Policy {
return new Policy().deny(action, onResource);
static deny(action: Action | Action[], onResource: PolicyStatement['resource'], statement?: Omit<PolicyStatement, 'resource' | 'action' | 'effect'>): Policy {
return new Policy().deny(action, onResource, statement);
}
}

Expand Down
28 changes: 25 additions & 3 deletions src/cloudformation/modules/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@
import {Authorizer} from "../../aws/apigateway/Authorizer";
import {BasePathMapping} from "../../aws/apigateway/BasePathMapping";
import {Deployment} from "../../aws/apigateway/Deployment";
import { DomainName } from '../../aws/apigateway/DomainName';
import {Method} from "../../aws/apigateway/Method";
import {Resource} from "../../aws/apigateway/Resource";
import {RestApi} from "../../aws/apigateway/RestApi";
import {Permission} from "../../aws/lambda/Permission";
import { RecordSet } from '../../aws/route53/RecordSet';
import { AWSResourcesFor } from '../aws';
import {join, joinWith, Value} from "../Value";
import {Tag} from '../../aws/Tag';

export type ApiExpects = AWSResourcesFor<'apigateway' | 'lambda'>
export type ApiExpects = AWSResourcesFor<'apigateway' | 'lambda' | 'route53'>
export class Path {

constructor(
private readonly aws: AWSResourcesFor<'apigateway' | 'lambda'>,
private readonly aws: ApiExpects,
public api: Api,
public resources: Resource[],
public parent?: Path,
Expand Down Expand Up @@ -212,7 +214,9 @@ export class Api{
public lambdaArn?: Value<string>,
public lambdaPermissions: Permission[] = [],
public basePathMapping?: BasePathMapping,
private paths: Path[] = []
private paths: Path[] = [],
public customDomain?: DomainName,
public aRecord?: RecordSet,
) {}

pathMethodsFrom(path: Path): Array<{path: string, method: string}> {
Expand Down Expand Up @@ -244,6 +248,24 @@ export class Api{
return this;
}

mapToARecord(certificateArnInUsEast1: Value<string>, hostedZoneId: Value<string>, apiDomain: Value<string>): this {
this.customDomain = this.aws.apigateway.domainName({
domainName: apiDomain,
certificateArn: certificateArnInUsEast1,
endpointConfiguration: { types: ['EDGE'] }
});
this.aRecord = this.aws.route53.recordSet({
name: apiDomain,
type: 'A',
hostedZoneId: hostedZoneId,
aliasTarget: {
dNSName: this.customDomain.attributes.DistributionDomainName(),
hostedZoneId: this.customDomain.attributes.DistributionHostedZoneId()
}
})
return this.mapToDomain(this.customDomain);
}

mapToDomain(domainName: Value<string>): this {
this.basePathMapping = this.aws.apigateway.basePathMapping({
restApiId: this.restApi,
Expand Down
1 change: 0 additions & 1 deletion src/cloudformation/modules/dynamo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ export function dynamoTable<
}
}
const table = aws.dynamodb.table({
...(name ? {_logicalName: normalize(name) }: {}),
...props,
...(name ? {tableName: name }: {}),
keySchema: [{keyType: 'HASH', attributeName: definition.partitionKey as string}, ...(definition.sortKey ? [{keyType: 'RANGE', attributeName: definition.sortKey as unknown as string}]: [])],
Expand Down
102 changes: 90 additions & 12 deletions src/cloudformation/modules/website.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,106 @@
import { DistributionProperties, Distribution } from '../../aws/cloudfront/Distribution';
import { RecordSet } from '../../aws/route53/RecordSet';
import {Bucket} from "../../aws/s3/Bucket";
import {BucketPolicy} from "../../aws/s3/BucketPolicy";
import { AWSResourcesFor } from "../aws";
import { Policy } from '../iam/PolicyDocument';

import {join, Value} from "../Value";

export type WebsiteExpects = AWSResourcesFor<'s3' | 'cloudfront' | 'route53'>
export class Website {

bucket: Bucket;
bucketPolicy: BucketPolicy;

private constructor(bucket: Bucket, bucketPolicy: BucketPolicy) {
this.bucket = bucket;
this.bucketPolicy = bucketPolicy;

private constructor(
private aws: WebsiteExpects,
public bucket: Bucket,
public bucketPolicy: BucketPolicy,
public distribution?: Distribution,
public aRecord?: RecordSet,
) {
}

withARecord(domain: Value<string>, hostedZoneId: Value<string>) {
if(!this.distribution) throw new Error('You must call withDistribution to create A record');
this.aRecord = this.aws.route53.recordSet({
name: domain,
type: 'A',
hostedZoneId,
aliasTarget: {
dNSName: this.distribution!.attributes.DomainName(),
hostedZoneId: this.aws.cloudFrontHostedZoneId
}
});
}

withDistribution(
originId: Value<string>,
certificateArn: Value<string>,
aliases: Value<Value<string>[]>,
rootObject: Value<string> = 'index.html',
httpVersion: DistributionProperties['distributionConfig']['httpVersion'] = 'http2',
edgeLambdaArn?: Value<string>
): this {
this.distribution = this.aws.cloudfront.distribution({
distributionConfig: {
aliases,
customErrorResponses: [{ errorCode: 404, responseCode: 200, responsePagePath: '/' }],
defaultCacheBehavior: {
cachePolicyId: '658327ea-f89d-4fab-a63d-7e88639e58f6',
compress: false,
...(edgeLambdaArn ? { lambdaFunctionAssociations: [{
eventType: 'viewer-request',
lambdaFunctionARN: edgeLambdaArn,
}] } : {}),
responseHeadersPolicyId: '67f7725c-6f97-4210-82d7-5512b31e9d03',
targetOriginId: originId,
viewerProtocolPolicy: 'redirect-to-https'
},
defaultRootObject: rootObject,
enabled: true,
httpVersion: httpVersion,
iPV6Enabled: true,
origins: [
{
id: originId,
customOriginConfig: {
originProtocolPolicy: 'http-only',
originSSLProtocols: [ 'TLSv1.2' ],
},
domainName: this.aws.functions.select(2, this.aws.functions.split('/', this.bucket.attributes.WebsiteURL())),
}
],
viewerCertificate: {
acmCertificateArn: certificateArn,
minimumProtocolVersion: 'TLSv1.2_2021',
sslSupportMethod: 'sni-only'
}
}
});
return this;
}

static create(aws: AWSResourcesFor<'s3'>, indexDocument: Value<string> = 'index.html', errorDocument: Value<string> = indexDocument): Website {
static create(
aws: WebsiteExpects,
bucketName: Value<string>,
indexDocument: Value<string> = 'index.html',
errorDocument: Value<string> = indexDocument
): Website {
const bucket = aws.s3.bucket({
accessControl: 'PublicRead',
websiteConfiguration: { indexDocument, errorDocument }
bucketName: bucketName,
websiteConfiguration: { indexDocument, errorDocument },
publicAccessBlockConfiguration: {
blockPublicAcls: false,
blockPublicPolicy: false,
ignorePublicAcls: false,
restrictPublicBuckets: false
}
});

const policy = aws.s3.bucketPolicy({
bucket: bucket,
policyDocument: { statement: [{action: 's3:GetObject', effect: 'Allow', resource: [join('arn:aws:s3:::', bucket, '/*')], principal: '*'}] }
policyDocument: Policy.allow('s3:GetObject', [join(bucket.attributes.Arn(), '/*')], { principal: { AWS: '*' } }).document
});
return new Website(bucket, policy);

return new Website(aws, bucket, policy);
}
}
4 changes: 3 additions & 1 deletion src/cloudformation/template/template-creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type BuiltIns<Condition extends string = never> = {
stackId: { Ref: 'AWS::StackId' }
stackName: { Ref: 'AWS::StackName' }
urlSuffix: { Ref: 'AWS::URLSuffix' }
cloudFrontHostedZoneId: string;
condition(name: Condition): ConditionalValue;
functions: {
import<T>(name: Value<string>): T;
Expand Down Expand Up @@ -47,6 +48,7 @@ export class TemplateCreator {
return {
logicalName,
customResource,
cloudFrontHostedZoneId: 'Z2FDTNDATAQYW2',
accountId: ref('AWS::AccountId'),
notificationArns: ref('AWS::NotificationARNs'),
noValue: ref('AWS::NoValue'),
Expand Down Expand Up @@ -202,4 +204,4 @@ export class TemplateCreator {
}
return {template: output, outputs: outputs || undefined} as any;
}
}
}

0 comments on commit 03cbcd6

Please sign in to comment.