Skip to content

Commit

Permalink
feat(django-vue): add construct for app with both django and vue depl…
Browse files Browse the repository at this point in the history
…oyed to the same domain using cloudformation wip
  • Loading branch information
briancaffey committed Sep 18, 2021
1 parent a51a2be commit 32b74e5
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 26 deletions.
20 changes: 20 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,26 @@ static-site-destroy:
static-site-diff:
cdk diff --app='./lib/integ/integ.static-site.js';


## -- Django-Vue App Targets --

## synthesize Django-Vue App
django-vue-synth:
cdk synth --app='./lib/integ/integ.django-vue.js';

## deploy Django-Vue App
django-vue-deploy:
cdk deploy --app='./lib/integ/integ.django-vue.js';

## destroy Django-Vue App
django-vue-destroy:
cdk destroy --app='./lib/integ/integ.django-vue.js';

## diff Django-Vue App
django-vue-diff:
cdk diff --app='./lib/integ/integ.django-vue.js';


## describe CloudFormation stacks in the AWS account
describe-stacks:
aws cloudformation describe-stacks | jq
Expand Down
8 changes: 8 additions & 0 deletions src/django-ecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,15 @@ export class DjangoEcs extends cdk.Construct {
public vpc: ec2.IVpc;
public cluster: ecs.Cluster;
public image: ecs.ContainerImage;
public loadBalancer: elbv2.ApplicationLoadBalancer;
public apiDomainName: string;

constructor(scope: cdk.Construct, id: string, props: DjangoEcsProps) {
super(scope, id);

// expose api domain name
this.apiDomainName = props.apiDomainName!;

/**
* VPC must have public, private and isolated subnets
*
Expand Down Expand Up @@ -320,6 +325,9 @@ export class DjangoEcs extends cdk.Construct {

albfs.loadBalancer.logAccessLogs(albLogsBucket);

// expose the load balancer on the DjangoEcs construct
this.loadBalancer = albfs.loadBalancer;

// optionally disable the admin interface
// albfs.listener.addAction

Expand Down
88 changes: 88 additions & 0 deletions src/django-vue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
*
* This is a high-level construct that demonstrates
* how to use the static site CloudFrontWebDistribution
* together with the ECS Stack.
*
* CloudFront allows for both the static site and
* the `/api` calls to be served from the same
* domain (app.example.com, for example).
*
* This would be a good setup if you are using a Vue or React
* static website (such as an SPA) and your Django app's
* REST API uses either JWT (with HttpOnly refresh token)
* or Session-based API authentication.
*
* If you are using a Vue app and a Django app and don't mind
* storing JWT on the browser's localStorage, you can also use this
* approach, but you can also setup the frontend and backend on
* different domains.
*
**/

import * as cdk from '@aws-cdk/core';
import { DjangoEcs } from './index';
import { StaticSite } from './index';
import * as acm from '@aws-cdk/aws-certificatemanager';
import * as route53 from '@aws-cdk/aws-route53';

/**
* Django and Vue application stack props
*/
export interface DjangoVueProps {
/**
* Certificate ARN for looking up the Certificate to use for CloudFront and ALB
*/
readonly certificateArn?: string;

readonly domainName: string;

readonly zoneName: string;


}

/**
*
* Construct for projects using Django backend and static site for frontend
*/
export class DjangoVue extends cdk.Construct {
constructor(scope: cdk.Construct, id: string, props: DjangoVueProps) {
super(scope, id);

let certificate;
if (props.certificateArn) {
certificate = acm.Certificate.fromCertificateArn(scope, 'Cert', process.env.CERTIFICATE_ARN!);
} else {
certificate = new acm.DnsValidatedCertificate(scope, 'Cert', {
domainName: props.domainName,
hostedZone: route53.HostedZone.fromLookup(scope, 'HostedZone', {
domainName: props.zoneName,
})
})
}

// const apiBackend =
const apiBackend = new DjangoEcs(scope, 'DjangoEcsSample', {
imageDirectory: './test/django-step-by-step/backend',
webCommand: ['./scripts/start_prod.sh'],
useCeleryBeat: true,
apiDomainName: process.env.API_DOMAIN_NAME,
zoneName: process.env.ZONE_NAME,
useEcsExec: true,
frontendUrl: process.env.FRONTEND_URL,
certificateArn: certificate.certificateArn,
});

new StaticSite(scope, 'StaticSiteSample', {
frontendDomainName: props.domainName,
pathToDist: 'test/django-step-by-step/quasar-app/dist/pwa',
zoneName: props.zoneName,
loadBalancer: apiBackend.loadBalancer,
// assetsBucket: apiBackend.staticFileBucket,
});

new cdk.CfnOutput(this, 'loadBalancerName', { value: apiBackend.loadBalancer.loadBalancerDnsName });

};
};
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './django-ecs';
export * from './django-eks';
export * from './django-s3-storage';
export * from './static-site';
export * from './static-site';
export * from './django-vue';
22 changes: 22 additions & 0 deletions src/integ/integ.django-vue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as cdk from '@aws-cdk/core';
import { DjangoVue } from '../index';

const env = {
region: process.env.AWS_DEFAULT_REGION || 'us-east-1',
account: process.env.AWS_ACCOUNT_ID,
};


const app = new cdk.App();
const stack = new cdk.Stack(app, 'DjangoVueStack', { env });

// no props needed on this construct
const construct = new DjangoVue(stack, 'DjangoVueSample', {
domainName: process.env.FRONTEND_DOMAIN_NAME!,
zoneName: process.env.ZONE_NAME!,
});

/**
* Add tagging for this construct and all child constructs
*/
cdk.Tags.of(construct).add('stack', 'DjangoVueStack');
145 changes: 121 additions & 24 deletions src/static-site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import * as cloudfront from '@aws-cdk/aws-cloudfront';
import * as iam from '@aws-cdk/aws-iam';
import * as route53 from '@aws-cdk/aws-route53';
import * as targets from '@aws-cdk/aws-route53-targets';
import * as acm from '@aws-cdk/aws-certificatemanager';
import * as s3 from '@aws-cdk/aws-s3';
import * as s3Deployment from '@aws-cdk/aws-s3-deployment';
import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2';

import * as cdk from '@aws-cdk/core';

Expand All @@ -32,6 +34,26 @@ export interface StaticSiteProps {
*/
readonly certificateArn?: string;

/**
*
* Load Balancer
*
* If the backend and the frontend are served on the same site,
* this is required. CloudFront will act as a proxy, doing
* path-based routing to the backend load balancer (for example,
* all requests starting with `/api/*`)
*
*
**/
readonly loadBalancer?: elbv2.ApplicationLoadBalancer;

/**
* Assets bucket
*
* Proxy requests to /static and /media to the assets bucket
*/
readonly assetsBucket?: s3.Bucket;

/**
* Public Hosted Zone
*
Expand All @@ -48,6 +70,9 @@ export interface StaticSiteProps {
* https://github.com/aws-samples/aws-cdk-examples/blob/master/typescript/static-site/static-site.ts
*/
export class StaticSite extends cdk.Construct {

public distribution: cloudfront.CloudFrontWebDistribution;

constructor(scope: cdk.Construct, id: string, props: StaticSiteProps) {
super(scope, id);

Expand Down Expand Up @@ -75,32 +100,106 @@ export class StaticSite extends cdk.Construct {
domainName: props.zoneName,
});

// create the certificate if it doesn't exist
// TODO: optionally look up the certificate from props.certificateArn
const certificate = new DnsValidatedCertificate(scope, 'Certificate', {
domainName: props.frontendDomainName,
hostedZone,
});
// create the certificate if it doesn't exist or look up the certificate from props.certificateArn
let certificate;
if (props.certificateArn) {
certificate = acm.Certificate.fromCertificateArn(scope, 'Certificate', props.certificateArn);
} else {
certificate = new DnsValidatedCertificate(scope, 'Certificate', {
domainName: props.frontendDomainName,
hostedZone,
});
}

const originConfigs = [];

/**
* CloudFront Origins
*
* 1. Static site origin
* 2. Static assets origin
* 3. Load balancer origin
*
*/

// static site origin
const staticSiteOrigin = {
s3OriginSource: {
s3BucketSource: staticSiteBucket,
originAccessIdentity: cloudfrontOAI,
},
behaviors: [{
isDefaultBehavior: true,
compress: true,
allowedMethods: cloudfront.CloudFrontAllowedMethods.GET_HEAD_OPTIONS,
}],
};
originConfigs.push(staticSiteOrigin);

// if there is a S3 Bucket prop passed to the static site construct,
// create a fileStorageOrigin
if (props.assetsBucket ?? false) {
const fileStorageOrigin = {
s3OriginSource: {
s3BucketSource: props.assetsBucket!,
originAccessIdentity: cloudfrontOAI,
},
behaviors: ["/static/*", "/media/*"].map(path => {
return {
isDefaultBehavior: false,
compress: false,
allowedMethods: cloudfront.CloudFrontAllowedMethods.GET_HEAD_OPTIONS,
forwardedValues: { "query_string": true },
pathPattern: path,
minTtl: cdk.Duration.seconds(0),
defaultTtl: cdk.Duration.seconds(0),
maxTtl: cdk.Duration.seconds(0),
}
}),
}
originConfigs.push(fileStorageOrigin);
}

// load balancer origin
if (props.loadBalancer ?? false) {

// TODO: set this as a config option with a default
const pathPatterns = ['/api/*', '/graphql/*', '/admin/*'];

const behaviors = pathPatterns.map(path => {
return {
allowedMethods: cloudfront.CloudFrontAllowedMethods.ALL,
forwardedValues: {
"queryString": true,
"cookies": { forward: "all" },
"headers": ["*"],
},
pathPattern: path,
// minTtl: cdk.Duration.seconds(0),
// defaultTtl: cdk.Duration.seconds(0),
// maxTtl: cdk.Duration.seconds(0),
}
});

const loadBalancerOrigin = {
customOriginSource: {
domainName: props.loadBalancer!.loadBalancerDnsName,
originProtocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY,
},
behaviors,
};
originConfigs.push(loadBalancerOrigin);
}

console.log(originConfigs);

// create the cloudfront web distribution
const distribution = new cloudfront.CloudFrontWebDistribution(this, 'StaticSiteDistribution', {
this.distribution = new cloudfront.CloudFrontWebDistribution(this, 'StaticSiteDistribution', {
aliasConfiguration: {
acmCertRef: certificate.certificateArn,
names: [props.frontendDomainName],
},
originConfigs: [
{
s3OriginSource: {
s3BucketSource: staticSiteBucket,
originAccessIdentity: cloudfrontOAI,
},
behaviors: [{
isDefaultBehavior: true,
compress: true,
allowedMethods: cloudfront.CloudFrontAllowedMethods.GET_HEAD_OPTIONS,
}],
},
],
originConfigs,
// viewerCertificate: cloudfront.ViewerCertificate.fromAcmCertificate(certificate),
});

Expand All @@ -111,19 +210,17 @@ export class StaticSite extends cdk.Construct {
new s3Deployment.BucketDeployment(scope, 'BucketDeployment', {
sources: [s3Deployment.Source.asset(path.join(process.cwd(), props.pathToDist))],
destinationBucket: staticSiteBucket,
distribution,
distribution: this.distribution,
distributionPaths: ['/*'],
});
}


// create the A Record that will point to the CloudFront distribution
new route53.ARecord(scope, 'RecordTarget', {
target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(distribution)),
target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(this.distribution)),
zone: hostedZone,
// note that the recordName must end with a period (.)
recordName: `${props.frontendDomainName}.`,
});

}
}
2 changes: 1 addition & 1 deletion test/django-step-by-step

0 comments on commit 32b74e5

Please sign in to comment.