Skip to content

Commit

Permalink
feat: added construct for application ingress
Browse files Browse the repository at this point in the history
  • Loading branch information
nemanja-kovacevic-thinkit committed May 29, 2024
1 parent 526214c commit bf32e0d
Show file tree
Hide file tree
Showing 10 changed files with 826 additions and 3 deletions.
File renamed without changes.
1 change: 1 addition & 0 deletions lib/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from "./name-util";
export * from "./statefulset-util";
export * from "./value-util";
export * from "./workload-props";
export * from "./annotation-util";
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from "./postgres";
export * from "./redis";
export * from "./talis-chart";
export * from "./web-service";
export * from "./ingress";
2 changes: 2 additions & 0 deletions lib/ingress/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./ingress-props";
export * from "./ingress";
75 changes: 75 additions & 0 deletions lib/ingress/ingress-props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
export interface ServiceRouteProps {
/**
* Service name.
*/
readonly name: string;

/**
* Service port.
*/
readonly port: number;

/**
* Service namespace.
*/
readonly namespace: string;

/**
* Weight of the service. Has to be an integer in between 0 and 100.
*/
readonly weight: number;
}

export interface IngressProps {
/**
* Custom labels, they will be merged with the default app, role, and instance.
* @default { app: "<app label from chart>", role: "server", instance: "<construct id>" }
*/
readonly labels?: { [key: string]: string };

/**
* ARN of one or more certificate managed by AWS Certificate Manager
*/
readonly certificateArn?: string[];

/**
* Overrides for Ingress annotations.
* @see https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.3/guide/ingress/annotations/
*/
readonly ingressAnnotations?: { [key: string]: string };

/**
* Hostname to add External DNS record for the ingress.
*/
readonly externalHostname?: string;

/**
* Hostnames, they will be added as Ingress rules.
*/
readonly hostnames: string[];

/**
* Defines routing for each service. This will be used to create target groups for service weighting action.
*/
readonly serviceRouting: ServiceRouteProps[];

/**
* Ingress class name.
*/
readonly ingressClassName: string;

/**
* Ingress class priority, between -1000 and 1000
*/
readonly ingressClassPriority?: number;

/**
* Application name.
*/
readonly app: string;

/**
* Instance name.
*/
readonly instance: string;
}
165 changes: 165 additions & 0 deletions lib/ingress/ingress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { Chart } from "cdk8s";
import { Construct } from "constructs";
import {
IngressRule,
IntOrString,
KubeIngress,
KubeService,
} from "../../imports/k8s";
import {
convertToStringMap,
convertToJsonContent,
joinNameParts,
convertToStringList,
} from "../common";
import { IngressProps } from ".";
import { ServiceSpecType } from "../k8s";

export class Ingress extends Construct {
constructor(scope: Construct, id: string, props: IngressProps) {
super(scope, id);
this.validateProps(props);

const chart = Chart.of(this);
const environment = chart.labels.environment;
const region = chart.labels.region;
const service = joinNameParts([props.app, environment, region]);

const labels: { [key: string]: string } = {
...chart.labels,
...props.labels,
app: props.app,
instance: props.instance,
role: "server",
service: service,
};

const externalDns: Record<string, string> = {};
const ingressRules: IngressRule[] = [];
const ingressListenPorts: Record<string, number>[] = [
{ HTTP: 80 },
{ HTTPS: 443 },
];
const ingressTlsAnnotations: Record<string, string> = {};

if (props.externalHostname) {
externalDns["external-dns.alpha.kubernetes.io/hostname"] =
props.externalHostname;
}

const targetGroups: Record<string, string | number>[] = [];
for (const service of props.serviceRouting) {
const serviceName = `${service.name}-${service.namespace}`;
new KubeService(this, `${id}-${serviceName}-service`, {
metadata: {
name: serviceName,
labels: labels,
},
spec: {
type: ServiceSpecType.EXTERNAL_NAME,
externalName: `${service.name}.${service.namespace}.svc.cluster.local`,
ports: [
{
port: service.port,
targetPort: IntOrString.fromNumber(service.port),
},
],
},
});

const targetGroup = {
serviceName: serviceName,
servicePort: service.port,
weight: service.weight,
};
targetGroups.push(targetGroup);
}

const ingressAnnotations: { [key: string]: string } = {
"alb.ingress.kubernetes.io/listen-ports":
convertToJsonContent(ingressListenPorts),
"alb.ingress.kubernetes.io/success-codes": "200,303",
"alb.ingress.kubernetes.io/target-type": "instance",
"alb.ingress.kubernetes.io/group.order": String(
props.ingressClassPriority ?? 0,
),
"alb.ingress.kubernetes.io/tags": convertToStringMap({
service: labels.service,
instance: id,
environment: environment,
}),
"alb.ingress.kubernetes.io/actions.service-weighting":
convertToJsonContent({
type: "forward",
forwardConfig: {
targetGroups: targetGroups,
},
}),
...ingressTlsAnnotations,
...props.ingressAnnotations,
...externalDns,
};

if (props.certificateArn) {
ingressAnnotations["alb.ingress.kubernetes.io/certificate-arn"] =
convertToStringList(props.certificateArn);
}

for (const hostname of props.hostnames) {
ingressRules.push({
host: hostname,
http: {
paths: [
{
pathType: "Prefix",
path: "/",
backend: {
service: {
name: "service-weighting",
port: {
name: "use-annotation",
},
},
},
},
],
},
});
}

new KubeIngress(this, id, {
metadata: {
annotations: ingressAnnotations,
labels: labels,
},
spec: {
ingressClassName: props.ingressClassName,
rules: ingressRules,
},
});
}

private validateProps(props: IngressProps): void {
if (props.ingressClassPriority)
if (
props.ingressClassPriority < -1000 ||
props.ingressClassPriority > 1000
)
throw new Error(
"Ingress class priority has to be between -1000 and 1000",
);

if (props.hostnames.length < 1)
throw new Error("At least one hostname has to be defined.");

if (props.serviceRouting.length < 1)
throw new Error("At least one service route has to be defined.");

const totalWeight = props.serviceRouting.reduce(
(sum, serviceRoute) => sum + serviceRoute.weight,
0,
);
if (totalWeight != 100)
throw new Error("Total service routing weigth must be 100.");
}
}
1 change: 0 additions & 1 deletion lib/web-service/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from "./annotation-util";
export * from "./nginx-util";
export * from "./resque-web";
export * from "./web-service-props";
Expand Down
4 changes: 2 additions & 2 deletions lib/web-service/web-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import {
Volume,
} from "../../imports/k8s";
import {
convertToJsonContent,
convertToStringMap,
HorizontalPodAutoscalerProps,
NginxContainerProps,
PodDisruptionBudgetProps,
Expand All @@ -27,6 +25,8 @@ import {
makeLoadBalancerName,
ensureArray,
getValueFromIntOrPercent,
convertToJsonContent,
convertToStringMap,
} from "../common";
import { supportsTls } from "./tls-util";
import {
Expand Down
Loading

0 comments on commit bf32e0d

Please sign in to comment.