Skip to content

Commit

Permalink
feat(external-dns): add external dns
Browse files Browse the repository at this point in the history
  • Loading branch information
briancaffey committed Jun 18, 2021
1 parent 4683e77 commit e935ec3
Show file tree
Hide file tree
Showing 11 changed files with 265 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# copy this file to .env and change values, then source this file before
# running `cdk deploy`
export DOMAIN_NAME=example.com
export AWS_DEFAULT_REGION=us-east-1
export AWS_ACCOUNT_ID=111111111111
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*.tgz
*.tsbuildinfo
.cache
.env
.eslintcache
.jsii
.nyc_output
Expand Down
2 changes: 1 addition & 1 deletion .projenrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ const project = new AwsCdkConstructLibrary({
// dependabot: true, /* Include dependabot configuration. */
// dependabotOptions: undefined, /* Options for dependabot. */
// gitignore: undefined, /* Additional entries to .gitignore. */
gitignore: ['cdk.out', 'cdk.context.json', 'notes/'],
gitignore: ['cdk.out', 'cdk.context.json', 'notes/', '.env'],
// jest: true, /* Setup jest unit tests. */
// jestOptions: undefined, /* Jest options. */
// jsiiReleaseVersion: 'latest', /* Version requirement of `jsii-release` which is used to publish modules to npm. */
Expand Down
4 changes: 4 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ new DjangoEks(scope: Construct, id: string, props: DjangoEksProps)
* **props** (<code>[DjangoEksProps](#django-cdk-djangoeksprops)</code>) *No description*
* **imageDirectory** (<code>string</code>) The location of the Dockerfile used to create the main application image.
* **bucketName** (<code>string</code>) Name of existing bucket to use for media files. __*Optional*__
* **certificateArn** (<code>string</code>) Certificate ARN. __*Optional*__
* **domainName** (<code>string</code>) Domain name for backend (including sub-domain). __*Optional*__
* **useCeleryBeat** (<code>boolean</code>) Used to enable the celery beat service. __*Default*__: false
* **vpc** (<code>[IVpc](#aws-cdk-aws-ec2-ivpc)</code>) The VPC to use for the application. It must contain PUBLIC, PRIVATE and ISOLATED subnets. __*Optional*__
* **webCommand** (<code>Array<string></code>) The command used to run the API web service. __*Optional*__
Expand Down Expand Up @@ -122,6 +124,8 @@ Name | Type | Description
-----|------|-------------
**imageDirectory** | <code>string</code> | The location of the Dockerfile used to create the main application image.
**bucketName**? | <code>string</code> | Name of existing bucket to use for media files.<br/>__*Optional*__
**certificateArn**? | <code>string</code> | Certificate ARN.<br/>__*Optional*__
**domainName**? | <code>string</code> | Domain name for backend (including sub-domain).<br/>__*Optional*__
**useCeleryBeat**? | <code>boolean</code> | Used to enable the celery beat service.<br/>__*Default*__: false
**vpc**? | <code>[IVpc](#aws-cdk-aws-ec2-ivpc)</code> | The VPC to use for the application. It must contain PUBLIC, PRIVATE and ISOLATED subnets.<br/>__*Optional*__
**webCommand**? | <code>Array<string></code> | The command used to run the API web service.<br/>__*Optional*__
Expand Down
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,62 @@ This repository includes sample CDK applications that use the libraries.

### EKS

Overview of the EKS construct:

![png](/django-cdk.png)

1 - Resource in this diagram are defined by a CDK construct library called `django-eks` which is written in TypeScript and published to PyPi and npmjs.org. The project is managed by projen.

2 - The project uses jsii to transpile Typescript to Python, and the project is published to both PyPI and npm.

3 - The library is imported in a CDK application that is written in either TypeScript or Python.

4 - The CDK application is synthesized into CloudFormation templates which are used to build a CloudFormation stack that will contain all of the resources defined in the contstruct.

5 - An ECR registry is created when running `cdk bootstrap`, and it is used to store docker images that the application builds and later uses.

6 - An S3 bucket is also created by the `cdk bootstrap` command. This bucket is used for storing assets needed by CDK.

7 - The VPC is a the skeleton of the application. The CDK construct used for creating the VPC in our application sets up several resources including subnets, NAT gateways, internet gateway, route tables, etc.

8 - The Route53 record points to the Application Load Balancer (ALB) that routes traffic to our application. The record is created indirectly by CDK; external-dns creates the A Record resource based on annotations on the ALB.

9 - The Internet Gateway attached to our VPC

10 - The Application Load Balancer that is created by the AWS Load Balancer Controller

11 - EKS, the container orchestration layer in our application. AWS manages the control plane

12 - OpenIDConnect Provider used for handling permissions between pods and other AWS resources

13 - This is a node in the default node group of the EKS cluster

14 - The app namespace is where our application's Kubernetes resources will be deployed

15 - The Ingress that Routes traffic to the service for the Django application

16 - The service for the Django application

17 - The deployment/pods for the Django application. These pods have a service account that will give it access to other AWS resources through IRSA

18 - The deployment/pods for the celery workers in the Django application

19 - The IAM role and service account that are attached to the pods in our application. The service account is annotated with the IAM role's ARN (IRSA).

20 - external-dns is installed in our cluster to a dedicated namespace called external-dns. It is responsible for creating the Route53 record that points to the ALB. In future version of AWS Load Balancer Controller, external-dns may not be necessary.

21 - AWS Load Balancer Controller is installed into the kube-system namespace. This controller is responsible for provisioning an AWS Load Balancer when an Ingress object is deployed to the EKS cluster.

22 - RDS Postgres Instance that is placed in an isolated subnet. The security group for the default node group has access to the security group where the RDS instance is placed in an isolated subnet.

23 - Secrets Manager is used to provide the database password. The pods that run the Django application have access to the database secret in Secrets Manager, and they request it via a library that wraps boto3 calls and also caches secrets to reduce calls to secrets manager.

24 - ElastiCache Redis instance handles application caching and serves as the message broker for celery.

25 - Since the application runs in private subnets, outbound traffic is sent through NAT Gateways (Network Adress Translation) in public subnets that can be routed back to the public internet.

26 - An S3 bucket that our application can use for storing media assets.

Here's an example from `src/integ.django-eks.ts`:

```ts
Expand Down
4 changes: 4 additions & 0 deletions cdk.context.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,9 @@
"security-group:account=733623710918:region=us-east-1:securityGroupId=sg-0b18d301c815cd8b5": {
"securityGroupId": "sg-0b18d301c815cd8b5",
"allowAllOutbound": false
},
"hosted-zone:account=733623710918:domainName=jamescaffey.com:region=us-east-1": {
"Id": "/hostedzone/Z1EJVU8DMBV0XG",
"Name": "jamescaffey.com."
}
}
Binary file added django-cdk.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 52 additions & 7 deletions src/django-eks.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import * as acm from '@aws-cdk/aws-certificatemanager';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as ecrAssets from '@aws-cdk/aws-ecr-assets';
import * as eks from '@aws-cdk/aws-eks';
// import * as logs from '@aws-cdk/aws-logs';
import * as iam from '@aws-cdk/aws-iam';
import * as route53 from '@aws-cdk/aws-route53';
import * as s3 from '@aws-cdk/aws-s3';
import * as cdk from '@aws-cdk/core';
import { RdsPostgresInstance } from './common/database';
import { ApplicationVpc } from './common/vpc';
import { AwsLoadBalancerController } from './eks/awslbc';
// import { ElastiCacheCluster } from './elasticache';
import { ExternalDns } from './eks/external-dns';
import { Irsa } from './eks/irsa';
import { AppIngressResources } from './eks/resources/ingress';
import { MigrateJob } from './eks/resources/migrate';
Expand All @@ -20,20 +23,30 @@ import { WebResources } from './eks/resources/web';
*/
export interface DjangoEksProps {

/**
* Domain name for backend (including sub-domain)
*/
readonly domainName?: string;

/**
* Certificate ARN
*/
readonly certificateArn?: string;

/**
* Name of existing bucket to use for media files
*
* This name will be auto-generated if not specified
*/
readonly bucketName ? : string;
readonly bucketName?: string;

/**
* The VPC to use for the application. It must contain
* PUBLIC, PRIVATE and ISOLATED subnets
*
* A VPC will be created if this is not specified
*/
readonly vpc ? : ec2.IVpc;
readonly vpc?: ec2.IVpc;

/**
* The location of the Dockerfile used to create the main
Expand All @@ -46,14 +59,14 @@ export interface DjangoEksProps {
/**
* The command used to run the API web service.
*/
readonly webCommand ? : string[];
readonly webCommand?: string[];

/**
* Used to enable the celery beat service.
*
* @default false
*/
readonly useCeleryBeat ? : boolean;
readonly useCeleryBeat?: boolean;

}

Expand All @@ -78,8 +91,7 @@ export class DjangoEks extends cdk.Construct {
const applicationVpc = new ApplicationVpc(scope, 'AppVpc');
this.vpc = applicationVpc.vpc;
} else {
const vpc = props.vpc;
this.vpc = vpc;
this.vpc = props.vpc;
}

/**
Expand Down Expand Up @@ -134,10 +146,42 @@ export class DjangoEks extends cdk.Construct {
irsa.node.addDependency(appNamespace);
irsa.chart.node.addDependency(appNamespace);


/**
* Lookup Certificate from ARN or generate
* Deploy external-dns and related IAM resource if a domain name is included
*/
if (props.domainName) {

const hostedZone = route53.HostedZone.fromLookup(scope, 'hosted-zone', {
domainName: props.domainName,
});

/**
* Lookup or request ACM certificate depending on value of certificateArn
*/
if (props.certificateArn) {
// lookup ACM certificate from ACM certificate ARN
acm.Certificate.fromCertificateArn(scope, 'certificate', props.certificateArn);
} else {
// request a new certificate
new acm.Certificate(this, 'SSLCertificate', {
domainName: props.domainName,
validation: acm.CertificateValidation.fromDns(hostedZone),
});
}

new ExternalDns(scope, 'ExternalDns', {
hostedZone,
domainName: props.domainName,
cluster: this.cluster,
});
}

/**
* RDS instance
*/
const database = new RdsPostgresInstance(scope, 'RdsPostgresInstance', {
const database = new RdsPostgresInstance(scope, 'RdsPostgresInstance', {
vpc: this.vpc,
// TODO: base dbSecret on environment name
dbSecretName: 'dbSecret',
Expand Down Expand Up @@ -245,6 +289,7 @@ export class DjangoEks extends cdk.Construct {
const ingressResources = new AppIngressResources(scope, 'AppIngressResources', {
cluster: this.cluster,
domainName: 'test',
certificateArn: props.certificateArn,
});

/**
Expand Down
100 changes: 100 additions & 0 deletions src/eks/external-dns/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import * as eks from '@aws-cdk/aws-eks';
import * as iam from '@aws-cdk/aws-iam';
import * as route53 from '@aws-cdk/aws-route53';
import * as cdk from '@aws-cdk/core';

/**
* Props used to deploy external-dns to cluster
*/
export interface ExternalDnsProps {
domainName: string;
cluster: eks.ICluster;
hostedZone: route53.IHostedZone;
}

export class ExternalDns extends cdk.Construct {
constructor(scope: cdk.Construct, id: string, props: ExternalDnsProps) {
super(scope, id);

/**
* Namespace in which external-dns is deployed
*/
const ns = 'external-dns';
const externalDnsNamespace = props.cluster.addManifest('external-dns-ns', {
apiVersion: 'v1',
kind: 'Namespace',
metadata: {
name: ns,
},
});

/**
* Service account for external-dns
*/
const externalDNSServiceAccount = props.cluster.addServiceAccount(ns, {
name: ns,
namespace: ns,
});


/**
* Policies that will allow
*/
const r53ListPolicy = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'route53:ListHostedZones',
'route53:ListResourceRecordSets',
],
resources: ['*'],
});
const r53UpdateRecordPolicy = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'route53:ChangeResourceRecordSets',
],

resources: [props.hostedZone!.hostedZoneArn!],
});
externalDNSServiceAccount.addToPrincipalPolicy(r53ListPolicy);
externalDNSServiceAccount.addToPrincipalPolicy(r53UpdateRecordPolicy!);

/**
* Bitnami Helm chart for external-dns
*/
const externalDnsChart = new eks.HelmChart(scope, 'external-dns', {
cluster: props.cluster,
namespace: ns,
wait: true,
release: 'external-dns',
chart: 'external-dns',
repository: 'https://charts.bitnami.com/bitnami',
values: {
serviceAccount: {
create: false,
name: ns,
},
namespace: 'external-dns',
provider: 'aws',
// podAnnotations: {
// 'app.kubernetes.io/managed-by': 'Helm',
// },
aws: {
zoneType: 'public',
},
txtOwnerId: props.hostedZone.hostedZoneId,
domainFilters: [
props.hostedZone.zoneName,
],
},
});

/**
* The namespace that we deploy this chart into must be deployed before deploying the chart and service account
*/
externalDnsChart.node.addDependency(externalDnsNamespace);
externalDNSServiceAccount.node.addDependency(externalDnsNamespace);


}
}
Loading

0 comments on commit e935ec3

Please sign in to comment.