This repo shows examples on how to create add-ons for the ssp-amazon-eks cluster creator
It provides instructions to create:
- Helm chart based add-ons
- YAML manifest based add-ons
For additional details on creating and using these type of add-ons, please visit the project extensibility page
In this section of the workshop, we will create a new Helm based add-on that will quickly and easily deploy Kubevious Kubernetes dashboard to an EKS cluster.
Let's create a new project to host our add-on:
mkdir ssp_eks_kubevious_addon
cd ssp_eks_kubevious_addon
cdk init app --language typescript
Update the context section of the cdk.json file to allow CDK to use the new style synthesis:
"@aws-cdk/core:newStyleStackSynthesis": true
Bootstrap environment (if it has not been strapped before):
cdk bootstrap --trust=$ACCOUNT_ID \
--cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess \
aws://$ACCOUNT_ID/$AWS_REGION
Install ssp-eks-module
npm i @aws-quickstart/ssp-amazon-eks
When CDK initialized our project it created a couple of files with a generic cdk project structure. We will be replacing the content of those files with code specific to developing an add-on.
Let's start by replacing the bin/ssp_eks_kubevious_addon.ts
file contents with the code below. This will allow us to create an EKS cluster where we can eventually test our addon
import { App } from '@aws-cdk/core';
import * as ssp from '@aws-quickstart/ssp-amazon-eks';
const app = new App();
ssp.EksBlueprint.builder()
.addOns()
.build(app, 'my-kubevious-addon');
Now, replace the content of lib/ssp_eks_kubevious_addon.ts
with the following code to get started with our add-on creation:
import { Construct } from '@aws-cdk/core';
import * as ssp from '@aws-quickstart/ssp-amazon-eks';
/**
* User provided options for the Helm Chart
*/
export interface MyKubeviousAddOnProps extends ssp.addons.HelmAddOnUserProps {}
/**
* Default props to be used when creating the Helm chart
*/
export const defaultProps: ssp.addons.HelmAddOnProps & MyKubeviousAddOnProps = {
name: "", // Internal identifyer for our add-on
namespace: "", // Namesapce used to deploy the chart
chart: "", // Name of the Chart to be deployed
version: "", // version of the chart
release: "", // Name for our chart in Kubernetes
repository: "", // HTTPS address of the repository that holde the chart
cloudWatchRegion: "", // Region to which to send CloudWatchLogs
values: {} // Additional chart values (empty unless user provides values)
}
/**
* Main class to instantiate the Helm chart
*/
export class MyKubeviousAddOn extends ssp.addons.HelmAddOn {
readonly options: MyKubeviousAddOnProps
constructor(props: MyKubeviousAddOnProps) {
super({...defaultProps, ...props});
this.options = this.props as MyKubeviousAddOnProps;
}
deploy(clusterInfo: ssp.ClusterInfo): Promise<Construct> {
const values: ssp.Values = {};
const chart = this.addHelmChart(clusterInfo, values);
return Promise.resolve(chart);
}
}
This is the basic structure for a Helm based add-on. It contains:
MyKubeviousAddOn class
: Main logic structure for the add-on- The
deploy
method calls this.addHelchart which is in charge of fetching the chart from the appropriate Helm repo and applying user provided Helm overrides values
- The
MyKubeviousAddOnProps
: User provided values for the Helm chartdefaultProps:
Combination of the mandatory values and the user provided Helm chart values to be applied to the chart
In the lib/ssp_eks_kubevious_addon.js, let's replace the empty values with values that reflect the Helm chart we want to deploy as part of the add-on:
export const defaultProps: ssp.addons.HelmAddOnProps & sspKubeviousAddOnProps = {
name: "ssp-kubevious-addon",
namespace: "kubevious",
chart: "kubevious",
version: "0.8.15",
release: "kubevious",
repository: "https://helm.kubevious.io",
cloudWatchRegion: "us-east-2",
values: {}
}
This is enough information for us to deploy the add-on to our cluster. However, before we do that, let's allow the users of our add-on the ability to customize the chart with some commonly used options.
Let's start by adding the new options in the sspKubeviousAddOnProps
interface
export interface sspKubeviousAddOnProps extends ssp.addons.HelmAddOnUserProps {
/**
* Cloudwatch region where logs are forwarded
*/
cloudWatchRegion?: string,
version?: string,
ingress_enabled?: boolean,
kubevious_service_type?: string,
mysql_root_password?: string,
}
To simplify the manipulation of these optional values, let's also add so default values for our user provided values in the defaultProps:
export const defaultProps: ssp.addons.HelmAddOnProps & sspKubeviousAddOnProps = {
name: "ssp-kubevious-addon",
namespace: "kubevious",
chart: "kubevious",
version: "0.8.15",
release: "kubevious",
repository: "https://helm.kubevious.io",
cloudWatchRegion: "us-east-2",
values: {},
// Start new code
ingress_enabled: false,
kubevious_service_type: "ClusterIP",
mysql_root_password: "kubevious"
// End new code
}
For Helm to use our new values, we need to put the in the appropriate format. To do the formatting, a new populateValues function is added to the code
/**
* populateValues populates the appropriate values used to customize the Helm chart
* @param helmOptions User provided values to customize the chart
*/
function populateValues(helmOptions: sspKubeviousAddOnProps): ssp.Values {
return {
ingress: {
enabled: helmOptions.ingress_enabled
},
kubevious: {
service: {
type: helmOptions.kubevious_service_type
}
},
mysql: {
root: {
password: helmOptions.mysql_root_password
}
}
};
}
Finally, the new function is called from the main sspKubeviousAddOn
class:
export class sspKubeviousAddOn extends ssp.addons.HelmAddOn {
readonly options: sspKubeviousAddOnProps
constructor(props: sspKubeviousAddOnProps) {
super({...defaultProps, ...props});
this.options = this.props as sspKubeviousAddOnProps;
}
deploy(clusterInfo: ssp.ClusterInfo): Promise<Construct> {
let values: ssp.Values = populateValues(this.options); // Updated
const chart = this.addHelmChart(clusterInfo, values);
return Promise.resolve(chart);
}
}
After all the modifications, our lib/ssp_eks_kubevious_addon.js
file should like this:
import { Construct } from '@aws-cdk/core';
import * as ssp from '@aws-quickstart/ssp-amazon-eks';
/**
* User provided options for the Helm Chart
*/
export interface sspKubeviousAddOnProps extends ssp.addons.HelmAddOnUserProps {
/**
* Cloudwatch region where logs are forwarded
*/
cloudWatchRegion?: string,
version?: string,
ingress_enabled?: boolean,
kubevious_service_type?: string,
mysql_root_password?: string,
}
/**
* Default props to be used when creating the Helm chart
*/
export const defaultProps: ssp.addons.HelmAddOnProps & sspKubeviousAddOnProps = {
name: "ssp-kubevious-addon",
namespace: "kubevious",
chart: "kubevious",
version: "0.8.15",
release: "kubevious",
repository: "https://helm.kubevious.io",
cloudWatchRegion: "us-east-2",
values: {},
ingress_enabled: false,
kubevious_service_type: "ClusterIP",
mysql_root_password: "kubevious"
}
/**
* Main class to instantiate the Helm chart
*/
export class sspKubeviousAddOn extends ssp.addons.HelmAddOn {
readonly options: sspKubeviousAddOnProps
constructor(props: sspKubeviousAddOnProps) {
super({...defaultProps, ...props});
this.options = this.props as sspKubeviousAddOnProps;
}
deploy(clusterInfo: ssp.ClusterInfo): Promise<Construct> {
let values: ssp.Values = populateValues(this.options);
const chart = this.addHelmChart(clusterInfo, values);
return Promise.resolve(chart);
}
}
/**
* populateValues populates the appropriate values used to customize the Helm chart
* @param helmOptions User provided values to customize the chart
*/
function populateValues(helmOptions: sspKubeviousAddOnProps): ssp.Values {
return {
ingress: {
enabled: helmOptions.ingress_enabled
},
kubevious: {
service: {
type: helmOptions.kubevious_service_type
}
},
mysql: {
root: {
password: helmOptions.mysql_root_password
}
}
}
}
To test that our add-on can be deployed properly, let update the bin/ssp_eks_kubevious_addon.ts
to have a reference to our add-on
import { App } from '@aws-cdk/core';
import * as ssp from '@aws-quickstart/ssp-amazon-eks';
import { sspKubeviousAddOn } from '../lib/ssp_eks_kubevious_addon-stack' // Add
const app = new App();
ssp.EksBlueprint.builder()
.addOns(new sspKubeviousAddOn({ // Update
kubevious_service_type: 'NodePort',
}))
.build(app, 'my-kubevious-addon');
Now, before we deploy, let's make validate that we did not make any errors by running:
cdk list
This should return the following:
my-kubevious-addon
Finally, we can create our cluster with the new add-on:
cdk deploy
This command may take several minutes to run since it is creating a new EKS cluster for us. When the command is complete, it output a set of commands similar to the ones listed below:
my-kubevious-addon.mykubeviousaddonClusterName<randomValue> = my-kubevious-addon
my-kubevious-addon.mykubeviousaddonConfigCommand<randomValue> = aws eks update-kubeconfig --name my-kubevious-addon --region <your-region> --role-arn <your-role>
my-kubevious-addon.mykubeviousaddonGetTokenCommand<randomValue> = aws eks get-token --cluster-name my-kubevious-addon --region <your-region> --role-arn <your-role>
To access the cluster, we need to update your Kubernetes config file using the mykubeviousaddonConfigCommand that we got from the output above
aws eks update-kubeconfig --name my-kubevious-addon --region <your-region> --role-arn <your-role>
Finally, we can see our add-on in action by running the command below
kubectl port-forward $(kubectl get pods -n kubevious -l "app.kubernetes.io/component=kubevious-ui" -o jsonpath="{.items[0].metadata.name}") 8080:80 -n kubevious
and navigating to http://localhost:8080
In this section, we will create a YAML manifest add-on to easily deploy an ingressClass to the cluster. Ingress classes interact with items like the Application Load Balancer to allow external access to the services in the cluster via an Ingress.
While creating a new Add-on should always be done on its own repo, we will leverage the repo that we created in the previous section to develop the YAML manifest add-on so that we do not have to wait for a new cluster to spin up.
We will start by creating a new file called ssp_eks_ingressClass_addon-stack.ts
in the lib
folder and adding the basic structure for this type of add-on
import { ClusterAddOn, ClusterInfo } from "@aws-quickstart/ssp-amazon-eks";
import { loadYaml } from "@aws-quickstart/ssp-amazon-eks/dist/utils";
import { KubernetesManifest } from "@aws-cdk/aws-eks";
/**
* User provided overrides for the K8s Manifest
*/
export interface sspIngressClassProps {
}
/**
* Default values for the K8s manifest
*/
const defaultProps: sspIngressClassProps = {
}
/**
* Main class to instantiate the Kubernetes resource
*/
export class sspIngressClassAddOn implements ClusterAddOn {
readonly options: sspIngressClassProps
constructor(props: sspIngressClassProps) {
this.options = {...defaultProps, ...props} as sspIngressClassProps;
}
deploy(clusterInfo: ClusterInfo): void {
new KubernetesManifest(cluster.stack, 'sspIngressClassManifest', {
cluster: cluster,
manifest: <your manifest>,
overwrite: true
})
}
}
Similar to the Helm add-on, this basic structure has the following components:
sspIngressClassAddOn class
: Main logic structure for the add-on- The
deploy
method callsKubernetesManifest
which is in charge of deploying the manifest to the cluster
- The
sspIngressClassProps:
Optional user provided values to update the manifestdefaultProps
: Contains the default values required to deploy the manifest
The next step is to decide which values we will allow the user of our add-on to customize. In our case, users can provide: name, labels, annotation, controller type and whether the ingressClass will be the default for the cluster
/**
* User provided overrides for the Manifest
*/
export interface sspIngressClassProps {
name?: string,
labels?: { [name: string]: string };
annotations?: { [name: string]: string };
controller?: string;
defaultController?: boolean;
}
/**
* Default values for the K8s manifest
*/
const defaultProps: sspIngressClassProps = {
name: "alb",
controller: "ingress.k8s.aws/alb",
defaultController: false
}
To load and manipulate the YAML manifest of the resource we want to create, we will create two functions:
/**
* Function return the YAML manifest to be manipulated and deployed
*/
function getYaml(): string {
return `
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name:
spec:
controller:
`
}
/** Update YAML manifest wiht correct option values
*
* @param ingressClassManifest Record representation of a valid YAML manifest
* @param YamlOptions Options used to update the appropriate values in the YAML manifest
*/
function updateYaml(ingressClassManifest: Record<string, any>[], YamlOptions: sspIngressClassProps): Record<string, any>[] {
ingressClassManifest[0].metadata.name = YamlOptions.name
ingressClassManifest[0].spec.controller = YamlOptions.controller
ingressClassManifest[0].metadata.labels = YamlOptions.labels
let annotations: { [name: string]: string } = YamlOptions.annotations? YamlOptions.annotations: {}
if (YamlOptions.defaultController) {
annotations = { ...annotations, 'ingressclass.kubernetes.io/is-default-class' : "true"}
}
ingressClassManifest[0].metadata.annotations = annotations
return ingressClassManifest
}
getYaml
returns the raw YAML needed to create our resource. Note that in this case we are using an inline string, but from more complex scenarios, it may be useful to load a separate file containing the required YAML.updateYAML
Takes the record representation of a YAML manifest and updates it with all the required and user provides values.
Finally, we will update the sspIngressClassAddOn
class to call our new function and deploy our new add-on
/**
* Main class to instantiate the Kubernetes resource
*/
export class sspIngressClassAddOn implements ClusterAddOn {
readonly options: sspIngressClassProps
constructor(props: sspIngressClassProps) {
this.options = {...defaultProps, ...props} as sspIngressClassProps;
}
deploy(clusterInfo: ClusterInfo): void {
// New Code
const cluster = clusterInfo.cluster;
const ingressClassRawYaml: string = getYaml();
const ingressClassManifest: Record<string, any>[] = [ loadYaml(ingressClassRawYaml) ]
const UpdatedIngressClassManifest: Record<string, any>[] = updateYaml(ingressClassManifest, this.options)
// End New code
new KubernetesManifest(cluster.stack, 'sspIngressClassManifest', {
cluster: cluster,
manifest: UpdatedIngressClassManifest, // Update
overwrite: true
})
}
}
Now let's go back to the bin/ssp_eks_kubevious_addon.ts
and reference our IngressClass add-on
import { App } from '@aws-cdk/core';
import * as ssp from '@aws-quickstart/ssp-amazon-eks';
import { sspKubeviousAddOn } from '../lib/ssp_eks_kubevious_addon-stack'
import { sspKubeviousAddOn2 } from '../lib/ssp_eks_kubevious_addon-stack'
import { sspIngressClassAddOn } from '../lib/ssp_eks_ingressClass_addon-stack'
const app = new App();
ssp.EksBlueprint.builder()
.addOns(new sspKubeviousAddOn({
kubevious_service_type: 'NodePort',
}))
.addOns(new sspIngressClassAddOn({
annotations: { "test": "test"}, labels: { "test2": "test2"}
}))
.build(app, 'my-kubevious-addon');
Now, before we deploy, let make validate that we did not make any errors by running:
cdk list
This should return the following assuming processed properly:
my-kubevious-addon
Next, let's update our cluster with the new add-on:
cdk deploy
And finally you can see our Add-on in action by:
kubectl get ingressClass
The result of the above command should display our IngressClass running in the cluster