packages/constructs/L3/dataops/dataops-nifi-l3-construct/lib/cdk8s/zookeeper-chart.ts (470 lines of code) (raw):
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import * as cdk8s from 'cdk8s';
import { Construct } from 'constructs';
import * as k8s from './imports/k8s';
import { ExternalSecretStore } from './external-secret-store';
import * as fs from 'fs';
export interface EfsPersistentVolume {
readonly efsFsId: string;
readonly efsApId: string;
}
export interface ZookeeperChartProps extends cdk8s.ChartProps {
readonly caIssuerName: string;
readonly hostedZoneName: string;
readonly awsRegion: string;
readonly externalSecretsRoleArn: string;
readonly keystorePasswordSecretName: string;
readonly efsPersistentVolumes: EfsPersistentVolume[];
readonly efsStorageClassName: string;
readonly zookeeperCertDuration: string;
readonly zookeeperCertRenewBefore: string;
readonly certKeyAlg: string;
readonly certKeySize: number;
}
export class ZookeeperChart extends cdk8s.Chart {
public readonly zkConnectString: string;
private readonly props: ZookeeperChartProps;
constructor(scope: Construct, id: string, props: ZookeeperChartProps) {
super(scope, id, props);
this.props = props;
const zkService = this.createZkService();
const zkSecretName = this.createExternalSecrets(props);
const persistentVolumes = this.createPersistentVolumes();
this.createSslResources(zkService, zkSecretName);
this.createZkStatefulSet(zkService, zkSecretName, persistentVolumes);
this.zkConnectString = [...Array(3).keys()]
.map(i => {
return `zookeeper-${i}.${this.namespace}.${this.props.hostedZoneName}:2181`;
})
.join(',');
}
public hash(): string {
const json = JSON.stringify(this.toJson(), undefined, 2);
const stableJson = json.replace(/Token\[.*?\]/g, 'Token');
// nosemgrep
const crypto = require('crypto');
// nosemgrep
const hash = crypto //NOSONAR not used in senstive context
.createHash('sha1') //NOSONAR not used in senstive context
.update(stableJson)
.digest('hex');
return hash;
}
private createExternalSecrets(props: ZookeeperChartProps): string {
const secretStoreChart = new ExternalSecretStore(this, 'secret-store', {
storeName: 'external-secret-store',
...props,
});
const targetSecretName = 'zk-secret';
new cdk8s.ApiObject(this, 'zk-external-secret', {
apiVersion: 'external-secrets.io/v1beta1',
kind: 'ExternalSecret',
metadata: {
name: 'zk-external-secret',
},
spec: {
refreshInterval: '1h',
secretStoreRef: {
name: secretStoreChart.secretStoreName,
kind: 'SecretStore',
},
target: {
name: targetSecretName,
creationPolicy: 'Owner',
},
data: [
{
secretKey: 'keystore-password',
remoteRef: {
key: props.keystorePasswordSecretName,
},
},
],
},
});
return targetSecretName;
}
private createSslResources(zkService: k8s.KubeService, zkSecretName: string): { [name: string]: cdk8s.ApiObject } {
const nodeCerts = Object.fromEntries(
[...Array(3).keys()].map(i => {
const certName = `zookeeper-${i}-cert`;
const cert = new cdk8s.ApiObject(this, certName, {
apiVersion: 'cert-manager.io/v1',
kind: 'Certificate',
metadata: {
name: certName,
},
spec: {
isCA: false,
commonName: `zookeeper-${i}`,
dnsNames: [
`zookeeper-${i}.${zkService.name}.${this.namespace}.svc.cluster.local`,
`zookeeper-${i}.${this.namespace}.${this.props.hostedZoneName}`,
],
secretName: `zookeeper-${i}-ssl`,
privateKey: {
algorithm: this.props.certKeyAlg,
encoding: 'PKCS1',
size: this.props.certKeySize,
},
usages: ['server auth', 'client auth'],
issuerRef: {
name: this.props.caIssuerName,
kind: 'ClusterIssuer',
},
keystores: {
jks: {
create: true,
passwordSecretRef: {
name: zkSecretName,
key: 'keystore-password',
},
},
},
duration: this.props.zookeeperCertDuration,
renewBefore: this.props.zookeeperCertRenewBefore,
},
});
return [certName, cert];
}),
);
return nodeCerts;
}
private createZkService(): k8s.KubeService {
const zookeeperServiceProps: k8s.KubeServiceProps = {
metadata: {
name: 'zookeeper-svc',
labels: {
app: 'zookeeper',
},
annotations: {
'external-dns.alpha.kubernetes.io/hostname': `${this.namespace}.${this.props.hostedZoneName}`,
'external-dns.alpha.kubernetes.io/ttl': '60',
},
},
spec: {
ports: [
{
port: 2181,
name: 'zookeeper',
},
],
clusterIp: 'None',
selector: {
app: 'zookeeper',
},
},
};
return new k8s.KubeService(this, 'zookeeper-svc', zookeeperServiceProps);
}
private createPersistentVolumes(): k8s.KubePersistentVolume[] {
let volId = 0;
const pvs = this.props.efsPersistentVolumes.map(efsPvProps => {
const pv = new k8s.KubePersistentVolume(this, `zookeeper-persistent-volume-${volId}`, {
metadata: {
name: `zookeeper-vol-${efsPvProps.efsFsId}-${efsPvProps.efsApId}`,
labels: {
app: 'zookeeper',
},
},
spec: {
volumeMode: 'Filesystem',
capacity: {
storage: k8s.Quantity.fromString('60Gi'),
},
accessModes: ['ReadWriteOnce'],
persistentVolumeReclaimPolicy: 'Retain',
storageClassName: this.props.efsStorageClassName,
csi: {
driver: 'efs.csi.aws.com',
volumeHandle: `${efsPvProps.efsFsId}::${efsPvProps.efsApId}`,
},
claimRef: {
name: `zookeeper-data-zookeeper-${volId}`,
namespace: this.namespace,
},
},
});
volId = volId + 1;
return pv;
});
return pvs;
}
private createZkStatefulSet(
zookeeperService: k8s.KubeService,
zkSecretName: string,
persistentVolumes: k8s.KubePersistentVolume[],
): k8s.KubeStatefulSet {
const nodeIds = [...Array(3).keys()];
const nodeList = nodeIds.map(i => {
return `zookeeper-${i}`;
});
const sslBasePath = '/zookeeper-ssl';
const zkDataDir = '/zookeeper-data';
const zkInitBaseDir = '/zookeeper-init';
const sslSecretVolumes: [k8s.Volume, k8s.VolumeMount][] = nodeList.map(hostname => {
const volume: k8s.Volume = {
name: `${hostname}-ssl`,
secret: {
secretName: `${hostname}-ssl`,
},
};
const mount: k8s.VolumeMount = {
mountPath: `${sslBasePath}/${hostname}`,
name: volume.name,
};
return [volume, mount];
});
// nosemgrep
const zookeeperInitScriptsConfigMapData = Object.fromEntries(
fs.readdirSync(`${__dirname}/../../scripts/zookeeper`).map(fileName => {
console.log(`Reading zookeeper init script from ${__dirname}/../../scripts/zookeeper/${fileName}`);
// nosemgrep
return [fileName, fs.readFileSync(`${__dirname}/../../scripts/zookeeper/${fileName}`, 'utf-8')];
}),
);
const zookeeperInitConfigMap = new k8s.KubeConfigMap(this, 'zookeeper-init-configmap', {
metadata: {
name: 'zookeeper-init-scripts',
},
data: zookeeperInitScriptsConfigMapData,
});
const zkConfigMap = this.createZkConfigMap(zookeeperService.name, sslBasePath, zkDataDir, nodeList);
const zookeeperStsProps: k8s.KubeStatefulSetProps = {
metadata: {
name: 'zookeeper',
},
spec: {
selector: {
matchLabels: {
app: 'zookeeper',
},
},
serviceName: zookeeperService.name,
replicas: 3,
updateStrategy: {
type: 'RollingUpdate',
},
podManagementPolicy: 'Parallel',
persistentVolumeClaimRetentionPolicy: {
whenDeleted: 'Retain',
whenScaled: 'Delete',
},
volumeClaimTemplates: [
{
metadata: {
name: 'zookeeper-data',
},
spec: {
selector: {
matchLabels: {
app: 'nifi',
},
},
storageClassName: this.props.efsStorageClassName,
accessModes: ['ReadWriteOnce'],
resources: {
requests: {
storage: k8s.Quantity.fromString('5Gi'),
},
},
},
},
],
template: {
metadata: {
labels: {
app: 'zookeeper',
},
},
spec: {
tolerations: [
{
key: 'eks.amazonaws.com/compute-type',
value: 'fargate',
},
],
securityContext: {
runAsUser: 1000,
runAsGroup: 1000,
fsGroup: 1000,
},
volumes: [
{
name: 'zookeeper-init-scripts',
configMap: {
name: zookeeperInitConfigMap.name,
defaultMode: 0o755,
},
},
{
name: 'zookeeper-config',
configMap: {
name: zkConfigMap.name,
defaultMode: 0o755,
},
},
...sslSecretVolumes.map(x => x[0]),
],
containers: [
{
name: 'zookeeper',
imagePullPolicy: 'Always',
image: 'zookeeper:3.9.0',
ports: [
{
containerPort: 2181,
name: 'client',
},
{
containerPort: 2888,
name: 'server',
},
{
containerPort: 3888,
name: 'leader-election',
},
],
resources: {
requests: {
memory: k8s.Quantity.fromString('0.5Gi'),
cpu: k8s.Quantity.fromString('250m'),
},
limits: {
memory: k8s.Quantity.fromString('0.5Gi'),
cpu: k8s.Quantity.fromString('250m'),
},
},
command: ['bash', '-c', `${zkInitBaseDir}/scripts/zookeeper_start.sh`],
env: [
{
name: 'ZOO_DATA_DIR',
value: zkDataDir,
},
{
name: 'ZK_INIT_DIR',
value: zkInitBaseDir,
},
{
name: 'ZK_KEYSTORE_PASSWORD',
valueFrom: {
secretKeyRef: {
name: zkSecretName,
key: 'keystore-password',
optional: false,
},
},
},
{
name: 'ZK_TRUSTSTORE_PASSWORD',
valueFrom: {
secretKeyRef: {
name: zkSecretName,
key: 'keystore-password',
optional: false,
},
},
},
],
securityContext: {
runAsUser: 1000,
},
volumeMounts: [
{
name: 'zookeeper-config',
mountPath: `${zkInitBaseDir}/conf`,
},
{
name: 'zookeeper-init-scripts',
mountPath: `${zkInitBaseDir}/scripts`,
},
{
name: 'zookeeper-data',
mountPath: `${zkDataDir}`,
},
...sslSecretVolumes.map(x => x[1]),
],
},
],
},
},
},
};
const sts = new k8s.KubeStatefulSet(this, 'zookeeper-sts', zookeeperStsProps);
persistentVolumes.forEach(pv => {
sts.addDependency(pv);
});
return sts;
}
private createZkConfigMap(
zookeeperServiceName: string,
sslBasePath: string,
zookeeperDataDir: string,
nodeList: string[],
): k8s.KubeConfigMap {
// nosemgrep
const zookeeperBaseConfigMapData = Object.fromEntries(
fs.readdirSync(`${__dirname}/../../base_conf/zookeeper`).map(fileName => {
console.log(`Reading zookeeper base_conf/zookeeper from ${__dirname}/../../base_conf/zookeeper/${fileName}`);
// nosemgrep
return [fileName, fs.readFileSync(`${__dirname}/../../base_conf/zookeeper/${fileName}`, 'utf-8')];
}),
);
const zookeeperConfigMap = new k8s.KubeConfigMap(this, 'zookeeper-configmap', {
metadata: {
name: 'zookeeper-config',
},
data: {
...zookeeperBaseConfigMapData,
'zoo.cfg': this.updateZooCfg(
zookeeperBaseConfigMapData['zoo.cfg'],
nodeList,
zookeeperServiceName,
sslBasePath,
zookeeperDataDir,
),
},
});
return zookeeperConfigMap;
}
private updateZooCfg(
zooCfgData: string,
nodeList: string[],
zookeeperServiceName: string,
sslBasePath: string,
zookeeperDataDir: string,
): string {
const zooCfgMap: { [key: string]: string } = Object.fromEntries(
zooCfgData
.split('\n')
.filter(line => {
return !/^\s*#/.test(line) && !/^\s*$/.test(line);
})
.map(line => {
return [line.split(/=(.*)/s)[0], line.split(/=(.*)/s)[1]];
}),
);
//perform config-driven overrides here
let nodeIndex = 0;
nodeList.forEach(nodeName => {
zooCfgMap[
`server.${nodeIndex}`
] = `${nodeName}.${zookeeperServiceName}.${this.namespace}.svc.cluster.local:2888:3888`;
nodeIndex = nodeIndex + 1;
});
zooCfgMap['dataDir'] = zookeeperDataDir;
zooCfgMap['secureClientPort'] = '2181';
zooCfgMap['serverCnxnFactory'] = 'org.apache.zookeeper.server.NettyServerCnxnFactory';
zooCfgMap['sslQuorum'] = 'true';
zooCfgMap['ssl.quorum.keyStore.type'] = 'JKS';
zooCfgMap['ssl.quorum.keyStore.location'] = `${sslBasePath}/INIT_HOSTNAME/keystore.jks`;
zooCfgMap['ssl.quorum.keyStore.password'] = 'INIT_KEYSTORE_PASSWORD'; //NOSONAR placeholder value replaced at runtime
zooCfgMap['ssl.quorum.trustStore.type'] = 'JKS';
zooCfgMap['ssl.quorum.trustStore.location'] = `${sslBasePath}/INIT_HOSTNAME/truststore.jks`;
zooCfgMap['ssl.quorum.trustStore.password'] = 'INIT_TRUSTSTORE_PASSWORD'; //NOSONAR placeholder value replaced at runtime
zooCfgMap['ssl.keyStore.type'] = 'JKS';
zooCfgMap['ssl.keyStore.location'] = `${sslBasePath}/INIT_HOSTNAME/keystore.jks`;
zooCfgMap['ssl.keyStore.password'] = 'INIT_KEYSTORE_PASSWORD'; //NOSONAR placeholder value replaced at runtime
zooCfgMap['ssl.trustStore.type'] = 'JKS';
zooCfgMap['ssl.trustStore.location'] = `${sslBasePath}/INIT_HOSTNAME/truststore.jks`;
zooCfgMap['ssl.trustStore.password'] = 'INIT_TRUSTSTORE_PASSWORD'; //NOSONAR placeholder value replaced at runtime
const zooCfg = Object.entries(zooCfgMap)
.map(entry => {
return `${entry[0]}=${entry[1]}`;
})
.join('\n');
return zooCfg;
}
}