packages/constructs/L3/dataops/dataops-nifi-l3-construct/lib/dataops-nifi-l3-construct.ts (1,055 lines of code) (raw):
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import { MdaaSecurityGroup, MdaaSecurityGroupProps, MdaaSecurityGroupRuleProps } from '@aws-mdaa/ec2-constructs';
import {
KubernetesCmd,
KubernetesCmdProps,
MdaaEKSCluster,
MdaaEKSClusterProps,
MgmtInstanceProps,
} from '@aws-mdaa/eks-constructs';
import { MdaaRoleRef } from '@aws-mdaa/iam-role-helper';
import { MdaaKmsKey } from '@aws-mdaa/kms-constructs';
import { MdaaL3Construct, MdaaL3ConstructProps } from '@aws-mdaa/l3-construct';
import { ISecurityGroup, ISubnet, IVpc, Port, Protocol, SecurityGroup, Subnet, Vpc } from 'aws-cdk-lib/aws-ec2';
import { CoreDnsComputeType, FargateProfile, KubernetesManifest, KubernetesVersion } from 'aws-cdk-lib/aws-eks';
import { Effect, IRole, PolicyStatement, Role } from 'aws-cdk-lib/aws-iam';
import { IKey } from 'aws-cdk-lib/aws-kms';
import { HostedZone, PrivateHostedZone } from 'aws-cdk-lib/aws-route53';
import * as cdk8s from 'cdk8s';
import { Construct } from 'constructs';
import {
CfnCertificate,
CfnCertificateAuthority,
CfnCertificateAuthorityActivation,
CfnCertificateAuthorityProps,
} from 'aws-cdk-lib/aws-acmpca';
import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets';
import { CaIssuerChart } from './cdk8s/ca-chart';
import { ExternalDnsChart, ExternalDnsChartProps } from './cdk8s/external-dns-chart';
import * as k8s from './cdk8s/imports/k8s';
import { NifiRegistryChart, NifiRegistryChartProps } from './cdk8s/nifi-registry-chart';
import { ZookeeperChart } from './cdk8s/zookeeper-chart';
import { NifiCluster, NifiClusterProps } from './nifi-cluster';
import {
AwsManagedPolicySpec,
NamedNifiRegistryClientProps,
NifiClusterOptions,
NifiIdentityAuthorizationOptions,
NifiNetworkOptions,
PolicyAction,
} from './nifi-options';
export interface NifiClusterOptionsWithPeers extends NifiClusterOptions {
/**
* Other clusters within this module which will be provided SecurityGroup and Node remote access to this cluster.
*/
readonly peerClusters?: string[];
}
export interface NamedNifiClusterOptions {
/**
* @jsii ignore
*/
[name: string]: NifiClusterOptionsWithPeers;
}
export interface NifiProps {
/**
* If defined, an EC2 instance will be created with connectivity, permissions, and tooling to manage the EKS cluster
*/
readonly mgmtInstance?: MgmtInstanceProps;
/**
* List of admin roles which will be provided access to EKS cluster resources
*/
readonly adminRoles: MdaaRoleRef[];
/**
* VPC on which EKS and Nifi clusters will be deployed
*/
readonly vpcId: string;
/**
* Subnets on which EKS and Nifi clusters will be deployed
*/
readonly subnetIds: { [name: string]: string };
/**
* Ingress rules to be added to the EKS control plane security group
*/
readonly eksSecurityGroupIngressRules?: MdaaSecurityGroupRuleProps;
/**
* Egress rules to be added to all Nifi cluster security groups.
* These may also be specified for each cluster.
*/
readonly securityGroupEgressRules?: MdaaSecurityGroupRuleProps;
/**
* Security groups which will be provided ingress access to all Nifi cluster security groups.
* These may also be specified for each cluster.
*/
readonly securityGroupIngressSGs?: string[];
/**
* IPv4 CIDRs which will be provided ingress access to all Nifi cluster security groups.
* These may also be specified for each cluster.
*/
readonly securityGroupIngressIPv4s?: string[];
/**
* Security groups which will be provided ingress access to all Nifi cluster EFS security groups.
* These may also be specified for each cluster.
*/
readonly additionalEfsIngressSecurityGroupIds?: string[];
/**
* Nifi cluster configurations to be created.
*/
readonly clusters?: NamedNifiClusterOptions;
/**
* The certificate validity period for the internal CA cert. If using an ACM Private CA with short-term certificates,
* this should be set to less than 7 days. Defaults to 6 days.
*/
readonly caCertDuration?: string;
/**
* The time before CA cert expiration at which point the internal CA cert will be renewed.
* Defaults to 12 hours.
*/
readonly caCertRenewBefore?: string;
/**
* The certificate validity period for the Zookeeper and Nifi Node certs. If using an ACM Private CA with short-term certificates,
* this should be set to less than 6 days. Defaults to 5 days.
*/
readonly nodeCertDuration?: string;
/**
* The time before CA cert expiration at which point the Zookeeper and Nifi Node certs will be renewed.
* Defaults to 12 hours.
*/
readonly nodeCertRenewBefore?: string;
/**
* (Optional) If specified, this ACM Private CA will be used to sign the internal CA running
* within EKS. If not specified, an ACM Private CA will be created.
*/
readonly existingPrivateCaArn?: string;
readonly certKeyAlg?: string;
readonly certKeySize?: number;
readonly registry?: NifiRegistryProps;
}
export type NifiRegistryBucketProps = {
[key in PolicyAction]?: {
readonly identities?: string[];
readonly groups?: string[];
};
};
export interface NifiRegistryProps extends NifiIdentityAuthorizationOptions, NifiNetworkOptions {
/**
* The tag of the Nifi docker image to use. If not specified,
* defaults to the latest tested version (currently 1.25.0). Specify 'latest' to pull
* the latest version (might be untested).
*/
readonly registryImageTag?: string;
/**
* AWS managed policies which will be granted to the Nifi cluster role for access to AWS services.
*/
readonly registryRoleAwsManagedPolicies?: AwsManagedPolicySpec[];
/**
* Customer managed policies which will be granted to the Nifi cluster role for access to AWS services.
*/
readonly registryRoleManagedPolicies?: string[];
/**
* @jsii ignore
*/
readonly buckets?: { [bucketName: string]: NifiRegistryBucketProps };
}
export interface NifiL3ConstructProps extends MdaaL3ConstructProps {
/**
* Arn of KMS key which will be used to encrypt the cluster resources
*/
readonly kmsArn: string;
/**
* Nifi clusters to be created.
*/
readonly nifi: NifiProps;
}
interface AddNifiServiceProps {
eksCluster: MdaaEKSCluster;
vpc: IVpc;
subnets: ISubnet[];
hostedZone: HostedZone;
caIssuerCdk8sChart: CaIssuerChart;
fargateProfile: FargateProfile;
}
interface AddNifiClustersProps extends AddNifiServiceProps {
eksCluster: MdaaEKSCluster;
zkK8sChart: ZookeeperChart;
zkSecurityGroup: ISecurityGroup;
dependencies: Construct[];
nifiManagerImage: DockerImageAsset;
registryUrl?: string;
}
interface AddNifiClusterProps {
nifiClusterName: string;
nifiClusterOptions: NifiClusterOptionsWithPeers;
vpc: IVpc;
subnets: ISubnet[];
eksCluster: MdaaEKSCluster;
hostedZone: HostedZone;
zkK8sChart: ZookeeperChart;
caIssuerCdk8sChart: CaIssuerChart;
nifiManagerImage: DockerImageAsset;
registryUrl?: string;
fargateProfile: FargateProfile;
}
interface AddRegistryProps extends AddNifiServiceProps {
registryProps: NifiRegistryProps;
kmsKey: IKey;
registryHostname: string;
nifiClusters: { [clusterName: string]: NifiCluster };
nifiManagerImageUri: string;
dependencies: Construct[];
}
export class NifiL3Construct extends MdaaL3Construct {
protected readonly props: NifiL3ConstructProps;
private readonly projectKmsKey: IKey;
private static readonly CERT_MANAGER_NAMESPACE = 'cert-manager';
private static readonly EXTERNAL_DNS_NAMESPACE = 'external-dns';
private static readonly EXTERNAL_SECRETS_NAMESPACE = 'external-secrets';
private static readonly REGISTRY_NAMESPACE = 'registry';
private static readonly ZOOKEEPER_NAMESPACE = 'zookeeper';
constructor(scope: Construct, id: string, props: NifiL3ConstructProps) {
super(scope, id, props);
this.props = props;
this.projectKmsKey = MdaaKmsKey.fromKeyArn(this.scope, 'project-kms', this.props.kmsArn);
const vpc = Vpc.fromVpcAttributes(this, 'vpc', {
vpcId: this.props.nifi.vpcId,
availabilityZones: ['dummy'],
});
const subnets = Object.entries(this.props.nifi.subnetIds).map(entry => {
return Subnet.fromSubnetId(this, `subnet-${entry[0]}`, entry[1]);
});
const clusterSecurityGroupProps: MdaaSecurityGroupProps = {
securityGroupName: 'eks',
vpc: vpc,
addSelfReferenceRule: true,
naming: props.naming,
allowAllOutbound: true,
ingressRules: props.nifi.eksSecurityGroupIngressRules,
};
const clusterSecurityGroup = new MdaaSecurityGroup(scope, 'cluster-sg', clusterSecurityGroupProps);
const [privateCaArn, privateCa] = this.createAcmPca();
const hostedZone = this.createHostedZone(vpc);
const eksCluster = this.createEksCluster(vpc, subnets, this.projectKmsKey, clusterSecurityGroup, privateCaArn);
const servicesFargateProfile = eksCluster.addFargateProfile('services-fargate-profile', {
fargateProfileName: 'services',
selectors: [
{
namespace: NifiL3Construct.EXTERNAL_DNS_NAMESPACE,
},
{
namespace: NifiL3Construct.EXTERNAL_SECRETS_NAMESPACE,
},
{
namespace: NifiL3Construct.CERT_MANAGER_NAMESPACE,
},
{
namespace: NifiL3Construct.ZOOKEEPER_NAMESPACE,
},
{
namespace: NifiL3Construct.REGISTRY_NAMESPACE,
},
],
});
const nifiNamespaces = Object.entries(props.nifi.clusters || {}).map(cluster => {
return {
namespace: `nifi-${cluster[0]}`,
};
});
if (nifiNamespaces.length > 0) {
const nifiFargateProfile = eksCluster.addFargateProfile('nifi-fargate-profile', {
fargateProfileName: 'nifi',
selectors: nifiNamespaces,
});
const externalSecretsNamespaceManifest = eksCluster.addNamespace(
new cdk8s.App(),
'external-secrets-namespace',
NifiL3Construct.EXTERNAL_SECRETS_NAMESPACE,
clusterSecurityGroup,
);
externalSecretsNamespaceManifest.node.addDependency(servicesFargateProfile);
const externalSecretsReadyCmd = this.addExternalSecrets(eksCluster, externalSecretsNamespaceManifest);
const externalSecretsDnsManifest = eksCluster.addNamespace(
new cdk8s.App(),
'external-dns-namespace',
NifiL3Construct.EXTERNAL_DNS_NAMESPACE,
clusterSecurityGroup,
);
externalSecretsDnsManifest.node.addDependency(servicesFargateProfile);
const externalDnsReady = this.addExternalDns(hostedZone, eksCluster, externalSecretsDnsManifest);
const certManagerNamespaceManifest = eksCluster.addNamespace(
new cdk8s.App(),
'cert-manager-namespace',
NifiL3Construct.CERT_MANAGER_NAMESPACE,
clusterSecurityGroup,
);
certManagerNamespaceManifest.node.addDependency(servicesFargateProfile);
const certManagerReady = this.addCertManager(eksCluster, certManagerNamespaceManifest);
certManagerReady.node.addDependency(externalSecretsReadyCmd);
const [caIssuerManifest, caIssuerCdk8sChart] = this.addCA(eksCluster, certManagerNamespaceManifest, privateCa);
caIssuerManifest.node.addDependency(certManagerReady);
const registryHostname = this.props.nifi.registry ? `nifi-registry.${hostedZone.zoneName}` : undefined;
const registryUrl =
this.props.nifi.registry && registryHostname
? `https://${registryHostname}:${this.props.nifi.registry?.httpsPort || 8443}`
: undefined;
const [zkManifest, zkK8sChart, zkSecurityGroup] = this.addZookeeper(
vpc,
subnets,
this.projectKmsKey,
eksCluster,
hostedZone,
caIssuerCdk8sChart,
servicesFargateProfile,
);
zkManifest.node.addDependency(externalSecretsReadyCmd);
zkManifest.node.addDependency(externalDnsReady);
zkManifest.node.addDependency(caIssuerManifest);
zkManifest.node.addDependency(certManagerReady);
const nifiManagerImage = this.createNifiManagerImage();
const nifiClusters = this.addNifiClusters({
eksCluster: eksCluster,
vpc: vpc,
subnets: subnets,
hostedZone: hostedZone,
zkK8sChart: zkK8sChart,
caIssuerCdk8sChart: caIssuerCdk8sChart,
zkSecurityGroup: zkSecurityGroup,
dependencies: [externalDnsReady, externalSecretsReadyCmd, certManagerReady, caIssuerManifest],
nifiManagerImage: nifiManagerImage,
registryUrl: registryUrl,
fargateProfile: nifiFargateProfile,
});
if (this.props.nifi.registry && registryHostname) {
this.addRegistry({
registryProps: this.props.nifi.registry,
vpc: vpc,
subnets: subnets,
kmsKey: this.projectKmsKey,
eksCluster: eksCluster,
registryHostname: registryHostname,
hostedZone,
caIssuerCdk8sChart,
nifiClusters,
nifiManagerImageUri: nifiManagerImage.imageUri,
dependencies: [externalDnsReady, externalSecretsReadyCmd, certManagerReady, caIssuerManifest],
fargateProfile: servicesFargateProfile,
});
}
}
}
private addNifiClusters(addClusterProps: AddNifiClustersProps): { [clusterName: string]: NifiCluster } {
const nifiClusters = Object.fromEntries(
Object.entries(this.props.nifi.clusters || {}).map(nifiClusterEntry => {
const nifiClusterName = nifiClusterEntry[0];
const nifiClusterOptions = nifiClusterEntry[1];
const nifiCluster = this.addNifiCluster({
nifiClusterName: nifiClusterName,
nifiClusterOptions: nifiClusterOptions,
vpc: addClusterProps.vpc,
subnets: addClusterProps.subnets,
eksCluster: addClusterProps.eksCluster,
hostedZone: addClusterProps.hostedZone,
zkK8sChart: addClusterProps.zkK8sChart,
caIssuerCdk8sChart: addClusterProps.caIssuerCdk8sChart,
nifiManagerImage: addClusterProps.nifiManagerImage,
registryUrl: addClusterProps.registryUrl,
fargateProfile: addClusterProps.fargateProfile,
});
addClusterProps.dependencies.forEach(dependency => nifiCluster.nifiManifest.node.addDependency(dependency));
return [nifiClusterName, { cluster: nifiCluster, options: nifiClusterOptions }];
}),
);
Object.entries(nifiClusters).forEach(nifiClusterEntry => {
const nifiClusterName = nifiClusterEntry[0];
const nifiCluster = nifiClusterEntry[1];
nifiCluster.cluster.node.addDependency(addClusterProps.zkK8sChart);
addClusterProps.zkSecurityGroup.connections.allowFrom(nifiCluster.cluster.securityGroup, Port.tcp(2181));
nifiCluster.options.peerClusters?.forEach(peerClusterName => {
const peerCluster = nifiClusters[peerClusterName];
if (!peerCluster) {
throw new Error(`Unknown peer cluster ${peerClusterName} referenced by cluster ${nifiClusterName}`);
}
//Allow peer cluster to connect to this cluster
nifiCluster.cluster.securityGroup.connections.allowFrom(
peerCluster.cluster.securityGroup,
Port.tcp(nifiCluster.cluster.remotePort),
);
nifiCluster.cluster.securityGroup.connections.allowFrom(
peerCluster.cluster.securityGroup,
Port.tcp(nifiCluster.cluster.httpsPort),
);
});
});
return Object.fromEntries(Object.entries(nifiClusters).map(x => [x[0], x[1].cluster]));
}
private addRegistry(addRegistryProps: AddRegistryProps) {
const allIngressSgIds = [
...Object.entries(addRegistryProps.nifiClusters).map(x => x[1].securityGroup.securityGroupId),
...(this.props.nifi.securityGroupIngressSGs || []),
];
const registryHttpsPort = addRegistryProps.registryProps.httpsPort ?? 8443;
const ingressRules: MdaaSecurityGroupRuleProps = {
sg: allIngressSgIds
.map(sgId => {
return [
{
sgId: sgId,
protocol: Protocol.TCP,
port: registryHttpsPort,
},
];
})
.flat(),
ipv4: this.props.nifi.securityGroupIngressIPv4s
?.map(ipv4 => {
return [
{
cidr: ipv4,
protocol: Protocol.TCP,
port: registryHttpsPort,
},
];
})
.flat(),
};
const registrySecurityGroupProps: MdaaSecurityGroupProps = {
securityGroupName: 'registry',
vpc: addRegistryProps.vpc,
addSelfReferenceRule: true,
naming: this.props.naming,
allowAllOutbound: true,
ingressRules: ingressRules,
};
const registrySecurityGroup = new MdaaSecurityGroup(this, 'registry-sg', registrySecurityGroupProps);
const registryKeystorePasswordSecret = NifiCluster.createSecret(
this,
'registry-keystore-password-secret',
this.props.naming,
'registry-keystore-password',
this.projectKmsKey,
);
const registryAdminCredentialsSecret = NifiCluster.createSecret(
this,
'registry-admin-creds-secret',
this.props.naming,
'registry-admin-creds-secret',
addRegistryProps.kmsKey,
);
const kmsKeyStatement = new PolicyStatement({
sid: 'KmsDecrypt',
effect: Effect.ALLOW,
actions: ['kms:Decrypt'],
resources: [this.projectKmsKey.keyArn],
});
const secretsManagerStatement = new PolicyStatement({
sid: 'GetSecretValue',
effect: Effect.ALLOW,
actions: ['SecretsManager:GetSecretValue'],
resources: [registryKeystorePasswordSecret.secretArn, registryAdminCredentialsSecret.secretArn],
});
const externalSecretsServiceRole = NifiCluster.createServiceRole(
this,
'registry-external-secrets',
this.props.naming.resourceName('registry-external-secrets-service-role', 64),
NifiL3Construct.REGISTRY_NAMESPACE,
addRegistryProps.eksCluster,
[kmsKeyStatement, secretsManagerStatement],
);
const additionalEfsIngressSecurityGroups = this.props.nifi.additionalEfsIngressSecurityGroupIds?.map(id => {
return SecurityGroup.fromSecurityGroupId(this, `registry-efs-ingress-sg-${id}`, id);
});
const efsSecurityGroup = NifiCluster.createEfsSecurityGroup(
'registry',
this,
this.props.naming,
addRegistryProps.vpc,
[registrySecurityGroup, ...(additionalEfsIngressSecurityGroups || [])],
);
const registryEfsPvs = NifiCluster.createEfsPvs({
scope: this,
naming: this.props.naming,
name: 'registry',
nodeCount: 1,
vpc: addRegistryProps.vpc,
subnets: addRegistryProps.subnets,
kmsKey: addRegistryProps.kmsKey,
efsSecurityGroup: efsSecurityGroup,
})[0];
const efsManagedPolicy = NifiCluster.createEfsAccessPolicy(
'registry',
this,
this.props.naming,
this.projectKmsKey,
[registryEfsPvs],
);
addRegistryProps.fargateProfile.podExecutionRole.addManagedPolicy(efsManagedPolicy);
const registryNamespaceManifest = addRegistryProps.eksCluster.addNamespace(
new cdk8s.App(),
'registry-ns',
NifiL3Construct.REGISTRY_NAMESPACE,
registrySecurityGroup,
);
const clusterServiceRole = NifiCluster.createServiceRole(
this,
'registry-service-role',
this.props.naming.resourceName('registry-service-role', 64),
NifiL3Construct.REGISTRY_NAMESPACE,
addRegistryProps.eksCluster,
);
const registryChartProps: NifiRegistryChartProps = {
namespace: NifiL3Construct.REGISTRY_NAMESPACE,
awsRegion: this.region,
adminCredsSecretName: registryAdminCredentialsSecret.secretName,
keystorePasswordSecretName: registryKeystorePasswordSecret.secretName,
externalSecretsRoleArn: externalSecretsServiceRole.roleArn,
efsPersistentVolume: { efsFsId: registryEfsPvs[0].fileSystemId, efsApId: registryEfsPvs[1].accessPointId },
efsStorageClassName: addRegistryProps.eksCluster.efsStorageClassName,
caIssuerName: addRegistryProps.caIssuerCdk8sChart.caIssuerName,
hostname: addRegistryProps.registryHostname,
hostedZoneName: addRegistryProps.hostedZone.zoneName,
httpsPort: registryHttpsPort,
nifiRegistryServiceRoleArn: clusterServiceRole.roleArn,
nifiRegistryServiceRoleName: clusterServiceRole.roleName,
nifiRegistryCertDuration: this.props.nifi.nodeCertDuration ?? '24h0m0s',
nifiRegistryCertRenewBefore: this.props.nifi.nodeCertRenewBefore ?? '1h0m0s',
certKeyAlg: this.props.nifi.certKeyAlg ?? 'ECDSA',
certKeySize: this.props.nifi.certKeySize ?? 384,
nifiClusters: addRegistryProps.nifiClusters,
nifiManagerImageUri: addRegistryProps.nifiManagerImageUri,
adminIdentities: addRegistryProps.registryProps.adminIdentities,
buckets: addRegistryProps.registryProps.buckets,
};
const registryChart = new NifiRegistryChart(new cdk8s.App(), 'registry-chart', registryChartProps);
const registryManifest = addRegistryProps.eksCluster.addCdk8sChart('registry', registryChart);
registryManifest.node.addDependency(registryNamespaceManifest);
const restartRegistryCmdProps: KubernetesCmdProps = {
cluster: addRegistryProps.eksCluster,
namespace: NifiL3Construct.REGISTRY_NAMESPACE,
cmd: ['delete', 'pod', '-l', 'app=nifi-registry'],
executionKey: registryChart.hash(),
};
const restartRegistryCmd = new KubernetesCmd(this, 'restart-registry-cmd', restartRegistryCmdProps);
restartRegistryCmd.node.addDependency(registryManifest);
addRegistryProps.dependencies.forEach(dependency => registryManifest.node.addDependency(dependency));
}
private addCA(
eksCluster: MdaaEKSCluster,
servicesNamespaceManifest: KubernetesManifest,
privateCa?: CfnCertificateAuthorityActivation,
): [KubernetesManifest, CaIssuerChart] {
const [rootClusterIssuerName, rootClusterIssuerReadyCmd] = this.addPrivateCAChart(
eksCluster,
servicesNamespaceManifest,
privateCa,
);
const caKeystorePasswordSecret = NifiCluster.createSecret(
this,
'ca-keystore-password-secret',
this.props.naming,
'ca-keystore-password',
this.projectKmsKey,
);
const caExternalSecretsRole = NifiCluster.createExternalSecretsServiceRole(
this,
'ca-external-secrets',
this.props.naming,
NifiL3Construct.CERT_MANAGER_NAMESPACE,
eksCluster,
this.projectKmsKey,
[caKeystorePasswordSecret],
);
const caIssuerCdk8sChart = new CaIssuerChart(new cdk8s.App(), 'ca-issuer', {
namespace: NifiL3Construct.CERT_MANAGER_NAMESPACE,
awsRegion: this.region,
keystorePasswordSecretName: caKeystorePasswordSecret.secretName,
externalSecretsRoleArn: caExternalSecretsRole.roleArn,
rootClusterIssuerName: rootClusterIssuerName,
caCertDuration: this.props.nifi.caCertDuration ?? '144h0m0s',
caCertRenewBefore: this.props.nifi.caCertRenewBefore ?? '48h0m0s',
certKeyAlg: this.props.nifi.certKeyAlg ?? 'ECDSA',
certKeySize: this.props.nifi.certKeySize ?? 384,
});
const caManifest = eksCluster.addCdk8sChart('ca-issuer', caIssuerCdk8sChart);
caManifest.node.addDependency(rootClusterIssuerReadyCmd);
return [caManifest, caIssuerCdk8sChart];
}
private createNifiManagerImage(): DockerImageAsset {
return new DockerImageAsset(this, 'nifi-update-image', {
directory: `${__dirname}/../docker/nifi-manager`,
});
}
private computePeerNodeIdentities(addNifiClusterProps: AddNifiClusterProps) {
return addNifiClusterProps.nifiClusterOptions.peerClusters
?.map(peerClusterName => {
const peerClusterOptions = (this.props.nifi.clusters || {})[peerClusterName];
if (!peerClusterOptions) {
throw new Error(
`Unknown peer cluster ${peerClusterName} referenced by cluster ${addNifiClusterProps.nifiClusterName}`,
);
}
const peerNodeIds = [...Array(peerClusterOptions.nodeCount ?? 1).keys()];
return peerNodeIds.map(peerNodeId => {
return `CN=nifi-${peerNodeId}.nifi-${peerClusterName}.${addNifiClusterProps.hostedZone.zoneName}`;
});
})
.flat();
}
private computeDefaultRegistryClient(addNifiClusterProps: AddNifiClusterProps) {
return addNifiClusterProps.registryUrl
? {
[this.props.naming.resourceName('registry')]: {
url: addNifiClusterProps.registryUrl,
},
}
: {};
}
private computeClusterSecurityGroups(addNifiClusterProps: AddNifiClusterProps) {
return {
securityGroupEgressRules: MdaaSecurityGroup.mergeRules(
this.props.nifi.securityGroupEgressRules || {},
addNifiClusterProps.nifiClusterOptions.securityGroupEgressRules || {},
),
securityGroupIngressSGs: [
...(this.props.nifi.securityGroupIngressSGs || []),
...(addNifiClusterProps.nifiClusterOptions.securityGroupIngressSGs || []),
],
securityGroupIngressIPv4s: [
...(this.props.nifi.securityGroupIngressIPv4s || []),
...(addNifiClusterProps.nifiClusterOptions.securityGroupIngressIPv4s || []),
],
additionalEfsIngressSecurityGroupIds: [
...(this.props.nifi.additionalEfsIngressSecurityGroupIds || []),
...(addNifiClusterProps.nifiClusterOptions.additionalEfsIngressSecurityGroupIds || []),
],
};
}
private computeClusterCertProps() {
return {
nifiCertDuration: this.props.nifi.nodeCertDuration ?? '24h0m0s',
nifiCertRenewBefore: this.props.nifi.nodeCertRenewBefore ?? '1h0m0s',
certKeyAlg: this.props.nifi.certKeyAlg ?? 'ECDSA',
certKeySize: this.props.nifi.certKeySize ?? 384,
};
}
private addNifiCluster(addNifiClusterProps: AddNifiClusterProps): NifiCluster {
const peerNodeIdentities: string[] | undefined = this.computePeerNodeIdentities(addNifiClusterProps);
const defaultRegistryClient: NamedNifiRegistryClientProps = this.computeDefaultRegistryClient(addNifiClusterProps);
const clusterProps: NifiClusterProps = {
...addNifiClusterProps.nifiClusterOptions,
eksCluster: addNifiClusterProps.eksCluster,
clusterName: addNifiClusterProps.nifiClusterName,
kmsKey: this.projectKmsKey,
vpc: addNifiClusterProps.vpc,
subnets: addNifiClusterProps.subnets,
naming: this.props.naming.withModuleName(
`${this.props.naming.props.moduleName}-${addNifiClusterProps.nifiClusterName}`,
),
region: this.region,
zkConnectString: addNifiClusterProps.zkK8sChart.zkConnectString,
nifiHostedZone: addNifiClusterProps.hostedZone,
nifiCAIssuerName: addNifiClusterProps.caIssuerCdk8sChart.caIssuerName,
...this.computeClusterCertProps(),
nifiManagerImage: addNifiClusterProps.nifiManagerImage,
externalNodeIdentities: [
...(addNifiClusterProps.nifiClusterOptions.externalNodeIdentities || []),
...(peerNodeIdentities || []),
],
registryClients: {
...(addNifiClusterProps.nifiClusterOptions.registryClients || {}),
...defaultRegistryClient,
},
...this.computeClusterSecurityGroups(addNifiClusterProps),
fargateProfile: addNifiClusterProps.fargateProfile,
};
return new NifiCluster(this, `nifi-cluster-${addNifiClusterProps.nifiClusterName}`, clusterProps);
}
private addZookeeper(
vpc: IVpc,
subnets: ISubnet[],
kmsKey: IKey,
eksCluster: MdaaEKSCluster,
hostedZone: HostedZone,
caIssuerCdk8sChart: CaIssuerChart,
fargateProfile: FargateProfile,
): [KubernetesManifest, ZookeeperChart, ISecurityGroup] {
const zkSecurityGroupProps: MdaaSecurityGroupProps = {
securityGroupName: 'zk',
vpc: vpc,
addSelfReferenceRule: true,
naming: this.props.naming,
allowAllOutbound: true,
ingressRules: this.props.nifi.eksSecurityGroupIngressRules,
};
const zkSecurityGroup = new MdaaSecurityGroup(this, 'zk-sg', zkSecurityGroupProps);
const zkCeystorePasswordSecret = NifiCluster.createSecret(
this,
'zk-keystore-password-secret',
this.props.naming,
'zk-keystore-password',
this.projectKmsKey,
);
const kmsKeyStatement = new PolicyStatement({
sid: 'KmsDecrypt',
effect: Effect.ALLOW,
actions: ['kms:Decrypt'],
resources: [this.projectKmsKey.keyArn],
});
const secretsManagerStatement = new PolicyStatement({
sid: 'GetSecretValue',
effect: Effect.ALLOW,
actions: ['SecretsManager:GetSecretValue'],
resources: [zkCeystorePasswordSecret.secretArn],
});
const externalSecretsServiceRole = NifiCluster.createServiceRole(
this,
'zk-external-secrets',
this.props.naming.resourceName('zk-external-secrets-service-role', 64),
NifiL3Construct.ZOOKEEPER_NAMESPACE,
eksCluster,
[kmsKeyStatement, secretsManagerStatement],
);
const additionalEfsIngressSecurityGroups = this.props.nifi.additionalEfsIngressSecurityGroupIds?.map(id => {
return SecurityGroup.fromSecurityGroupId(this, `zk-efs-ingress-sg-${id}`, id);
});
const efsSecurityGroup = NifiCluster.createEfsSecurityGroup('zookeeper', this, this.props.naming, vpc, [
zkSecurityGroup,
...(additionalEfsIngressSecurityGroups || []),
]);
const zkEfsPvs = NifiCluster.createEfsPvs({
scope: this,
naming: this.props.naming,
name: 'zk',
nodeCount: 3,
vpc: vpc,
subnets: subnets,
kmsKey: kmsKey,
efsSecurityGroup: efsSecurityGroup,
});
const efsManagedPolicy = NifiCluster.createEfsAccessPolicy(
'zookeeper',
this,
this.props.naming,
this.projectKmsKey,
zkEfsPvs,
);
fargateProfile.podExecutionRole.addManagedPolicy(efsManagedPolicy);
const zkNamespaceManifest = eksCluster.addNamespace(
new cdk8s.App(),
'zookeeper-ns',
NifiL3Construct.ZOOKEEPER_NAMESPACE,
zkSecurityGroup,
);
zkNamespaceManifest.node.addDependency(fargateProfile);
const zkK8sChart = new ZookeeperChart(new cdk8s.App(), 'zookeeper-chart', {
namespace: NifiL3Construct.ZOOKEEPER_NAMESPACE,
hostedZoneName: hostedZone.zoneName,
externalSecretsRoleArn: externalSecretsServiceRole.roleArn,
caIssuerName: caIssuerCdk8sChart.caIssuerName,
awsRegion: this.region,
keystorePasswordSecretName: zkCeystorePasswordSecret.secretName,
efsStorageClassName: eksCluster.efsStorageClassName,
efsPersistentVolumes: zkEfsPvs.map(x => {
return { efsFsId: x[0].fileSystemId, efsApId: x[1].accessPointId };
}),
zookeeperCertDuration: this.props.nifi.nodeCertDuration ?? '24h0m0s',
zookeeperCertRenewBefore: this.props.nifi.nodeCertRenewBefore ?? '1h0m0s',
certKeyAlg: this.props.nifi.certKeyAlg ?? 'ECDSA',
certKeySize: this.props.nifi.certKeySize ?? 384,
});
const zkManifest = eksCluster.addCdk8sChart('zookeeper', zkK8sChart);
zkManifest.node.addDependency(zkNamespaceManifest);
zkManifest.node.addDependency(caIssuerCdk8sChart);
const restartNifiCmdProps: KubernetesCmdProps = {
cluster: eksCluster,
namespace: NifiL3Construct.ZOOKEEPER_NAMESPACE,
cmd: ['delete', 'pod', '-l', 'app=zookeeper'],
executionKey: zkK8sChart.hash(),
};
const restartNifiCmd = new KubernetesCmd(this, 'restart-zk-cmd', restartNifiCmdProps);
restartNifiCmd.node.addDependency(zkManifest);
return [zkManifest, zkK8sChart, zkSecurityGroup];
}
private createHostedZone(vpc: IVpc): HostedZone {
return new PrivateHostedZone(this, 'hosted-zone', {
vpc: vpc,
zoneName: `${this.props.naming.resourceName()}.internal`,
});
}
private addExternalDns(
hostedZone: HostedZone,
eksCluster: MdaaEKSCluster,
servicesNamespaceManifest: KubernetesManifest,
): KubernetesCmd {
const externalDnsRole = this.createExternalDnsServiceRole(
NifiL3Construct.EXTERNAL_DNS_NAMESPACE,
eksCluster,
hostedZone,
);
const chartProps: ExternalDnsChartProps = {
namespace: NifiL3Construct.EXTERNAL_DNS_NAMESPACE,
region: this.region,
externalDnsRoleArn: externalDnsRole.roleArn,
};
const externalDnsManifest = eksCluster.addCdk8sChart(
'external-dns',
new ExternalDnsChart(new cdk8s.App(), 'external-dns', chartProps),
);
externalDnsManifest.node.addDependency(servicesNamespaceManifest);
//Ensure External Dns is Ready
const checkReadyProps: KubernetesCmdProps = {
cluster: eksCluster,
namespace: NifiL3Construct.EXTERNAL_DNS_NAMESPACE,
cmd: ['get', 'deployment.apps', 'external-dns', '-o', "jsonpath='{.status.readyReplicas}'"],
expectedOutput: '1',
};
const checkReadyCmd = new KubernetesCmd(this, 'check-external-dns-ready', checkReadyProps);
checkReadyCmd.node.addDependency(externalDnsManifest);
return checkReadyCmd;
}
private createEksCluster(
vpc: IVpc,
subnets: ISubnet[],
kmsKey: IKey,
clusterSecurityGroup: ISecurityGroup,
privateCaArn: string,
): MdaaEKSCluster {
const resolvedAdminRoles = this.props.roleHelper.resolveRoleRefsWithOrdinals(this.props.nifi.adminRoles, 'Admin');
const adminRoles = resolvedAdminRoles.map(resolvedRole => {
return Role.fromRoleArn(this, `admin-role-${resolvedRole.refId()}`, resolvedRole.arn());
});
const mgmtInstanceProps = this.createMgmtInstanceProps(privateCaArn);
const clusterProps: MdaaEKSClusterProps = {
mgmtInstance: mgmtInstanceProps,
version: KubernetesVersion.V1_27,
coreDnsComputeType: CoreDnsComputeType.FARGATE,
adminRoles: adminRoles,
kmsKey: kmsKey,
vpc: vpc,
subnets: subnets,
naming: this.props.naming,
securityGroup: clusterSecurityGroup,
tags: this.props.tags,
};
return new MdaaEKSCluster(this, 'eks-cluster', clusterProps);
}
private createMgmtInstanceProps(privateCaArn: string): MgmtInstanceProps | undefined {
if (this.props.nifi.mgmtInstance) {
const mgmtInstanceKeystorePasswordSecret = NifiCluster.createSecret(
this,
'mgmt-instance-keystore-secret',
this.props.naming,
'mgmt-instance-keystore-password',
this.projectKmsKey,
);
const secretsManagerStatement = new PolicyStatement({
sid: 'GetSecretValue',
effect: Effect.ALLOW,
actions: ['SecretsManager:GetSecretValue'],
resources: [mgmtInstanceKeystorePasswordSecret.secretFullArn || mgmtInstanceKeystorePasswordSecret.secretArn],
});
const projectKmsStatement = new PolicyStatement({
sid: 'ProjectKms',
effect: Effect.ALLOW,
actions: ['kms:Decrypt'],
resources: [this.projectKmsKey.keyArn],
});
const issueCertStatement = new PolicyStatement({
sid: 'IssueCert',
effect: Effect.ALLOW,
actions: ['acm-pca:IssueCertificate', 'acm-pca:GetCertificate'],
resources: [privateCaArn],
});
return {
...this.props.nifi.mgmtInstance,
mgmtPolicyStatements: [secretsManagerStatement, issueCertStatement, projectKmsStatement],
userDataCommands: [
...(this.props.nifi.mgmtInstance?.userDataCommands ?? []),
`yum install -y java-21-amazon-corretto.x86_64`,
`aws secretsmanager get-secret-value --secret-id ${mgmtInstanceKeystorePasswordSecret.secretArn} |jq -r '.SecretString' > /tmp/keystore-passwd`,
`openssl ecparam -name secp384r1 -genkey -noout -out /root/mgmt-instance.key.pem`,
`openssl req -new -sha256 -key /root/mgmt-instance.key.pem -out /root/mgmt-instance.csr -subj "/CN=mgmt-instance"`,
`aws acm-pca issue-certificate --certificate-authority-arn ${privateCaArn} --csr fileb:///root/mgmt-instance.csr --signing-algorithm "SHA512WITHECDSA" --validity Value=7,Type="DAYS"|jq -r '.CertificateArn' > /tmp/certificate-arn`,
`cd /root && wget https://dlcdn.apache.org/nifi/1.25.0/nifi-toolkit-1.25.0-bin.zip && unzip nifi-toolkit-1.25.0-bin.zip && mv /root/nifi-toolkit-1.25.0 /opt/nifi-toolkit`,
`export CERT_ARN=\`cat /tmp/certificate-arn\` && aws acm-pca get-certificate --certificate-authority-arn ${privateCaArn} --certificate-arn $CERT_ARN | jq -r .Certificate > /root/mgmt-instance.cert.pem`,
`export CERT_ARN=\`cat /tmp/certificate-arn\` && aws acm-pca get-certificate --certificate-authority-arn ${privateCaArn} --certificate-arn $CERT_ARN | jq -r .CertificateChain > /root/ca.cert.pem`,
`openssl pkcs12 -export -in /root/mgmt-instance.cert.pem -inkey /root/mgmt-instance.key.pem -out /opt/nifi-toolkit/conf/mgmt-instance.cert.p12 -name mgmt-instance -password pass:\`cat /tmp/keystore-passwd\``,
``,
],
};
} else {
return undefined;
}
}
private addExternalSecrets(eksCluster: MdaaEKSCluster, servicesNamespaceManifest: KubernetesManifest): KubernetesCmd {
const externalSecretsHelm = eksCluster.addHelmChart('external-secrets-helm', {
repository: 'https://charts.external-secrets.io',
chart: 'external-secrets',
version: '0.9.5',
release: 'external-secrets',
namespace: NifiL3Construct.EXTERNAL_SECRETS_NAMESPACE,
createNamespace: false,
values: {
installCRDs: true,
tolerations: [
{
key: 'eks.amazonaws.com/compute-type',
value: 'fargate',
},
],
webhook: {
port: 9443,
},
},
});
externalSecretsHelm.node.addDependency(servicesNamespaceManifest);
//Ensure External Secrets is Ready
const checkReadyProps: KubernetesCmdProps = {
cluster: eksCluster,
namespace: NifiL3Construct.EXTERNAL_SECRETS_NAMESPACE,
cmd: ['get', 'deployment.apps', 'external-secrets-webhook', '-o', "jsonpath='{.status.readyReplicas}'"],
expectedOutput: '1',
};
const checkReadyCmd = new KubernetesCmd(this, 'check-external-secrets-ready', checkReadyProps);
checkReadyCmd.node.addDependency(externalSecretsHelm);
return checkReadyCmd;
}
private createAcmPca(): [string, CfnCertificateAuthorityActivation | undefined] {
if (this.props.nifi.existingPrivateCaArn) {
return [this.props.nifi.existingPrivateCaArn, undefined];
}
const pcaProps: CfnCertificateAuthorityProps = {
keyAlgorithm: 'EC_secp384r1',
signingAlgorithm: 'SHA512WITHECDSA',
type: 'ROOT',
subject: {
commonName: this.props.naming.resourceName(),
organization: this.props.naming.props.org,
organizationalUnit: this.props.naming.props.domain,
},
usageMode: 'SHORT_LIVED_CERTIFICATE',
};
const pca = new CfnCertificateAuthority(this, 'acm-pca', pcaProps);
const caCert = new CfnCertificate(this, 'acm-pca-cert', {
certificateAuthorityArn: pca.attrArn,
certificateSigningRequest: pca.attrCertificateSigningRequest,
signingAlgorithm: 'SHA512WITHECDSA',
validity: {
type: 'YEARS',
value: 10,
},
templateArn: `arn:${this.partition}:acm-pca:::template/RootCACertificate/V1`,
});
const pcaAct = new CfnCertificateAuthorityActivation(this, 'acm-pca-activation', {
certificateAuthorityArn: pca.attrArn,
certificate: caCert.attrCertificate,
status: 'ACTIVE',
});
return [pcaAct.certificateAuthorityArn, pcaAct];
}
private addPrivateCAChart(
eksCluster: MdaaEKSCluster,
servicesNamespaceManifest: KubernetesManifest,
privateCa?: CfnCertificateAuthorityActivation,
): [string, Construct] {
let privateCaArn: string;
if (privateCa) {
privateCaArn = privateCa.certificateAuthorityArn;
} else {
/* istanbul ignore next */
if (!this.props.nifi.existingPrivateCaArn) {
throw new Error('Impossible condition');
}
privateCaArn = this.props.nifi.existingPrivateCaArn;
}
const acmPcaStatement = new PolicyStatement({
sid: 'awspcaissuer',
actions: ['acm-pca:DescribeCertificateAuthority', 'acm-pca:GetCertificate', 'acm-pca:IssueCertificate'],
effect: Effect.ALLOW,
resources: [privateCaArn],
});
const serviceRole = NifiCluster.createServiceRole(
this,
'private-ca-service-role',
this.props.naming.resourceName('private-ca-svc', 64),
NifiL3Construct.CERT_MANAGER_NAMESPACE,
eksCluster,
[acmPcaStatement],
);
const serviceAccountChart = new (class extends cdk8s.Chart {
public serviceAccountName: string;
constructor(scope: Construct, id: string) {
super(scope, id);
const serviceAccount = new k8s.KubeServiceAccount(this, 'service-account', {
metadata: {
name: 'private-ca-service-account',
namespace: NifiL3Construct.CERT_MANAGER_NAMESPACE,
labels: {
'app.kubernetes.io/name': 'private-ca',
},
annotations: {
'eks.amazonaws.com/role-arn': serviceRole.roleArn,
},
},
});
this.serviceAccountName = serviceAccount.name;
}
})(new cdk8s.App(), 'private-ca-service-account-chart');
const serviceAccountManifest = eksCluster.addCdk8sChart('private-ca-service-account', serviceAccountChart);
serviceAccountManifest.node.addDependency(servicesNamespaceManifest);
const pcaManagerHelm = eksCluster.addHelmChart('private-ca-helm', {
repository: 'https://cert-manager.github.io/aws-privateca-issuer',
chart: 'aws-privateca-issuer',
version: '1.2.5',
release: 'aws-privateca-issuer',
namespace: NifiL3Construct.CERT_MANAGER_NAMESPACE,
createNamespace: false,
values: {
installCRDs: true,
tolerations: [
{
key: 'eks.amazonaws.com/compute-type',
value: 'fargate',
},
],
serviceAccount: {
create: false,
name: serviceAccountChart.serviceAccountName,
},
},
});
pcaManagerHelm.node.addDependency(serviceAccountManifest);
if (privateCa) {
pcaManagerHelm.node.addDependency(privateCa);
}
const pcaClusterIssuerChart = new (class extends cdk8s.Chart {
public readonly clusterIssuerName: string;
constructor(scope: Construct, id: string, privateCaArn: string, region: string) {
super(scope, id);
const clusterIssuer = new cdk8s.ApiObject(this, 'private-ca-cluster-issuer', {
apiVersion: 'awspca.cert-manager.io/v1beta1',
kind: 'AWSPCAClusterIssuer',
metadata: {
name: 'private-ca-cluster-issuer',
},
spec: {
arn: privateCaArn,
region: region,
},
});
this.clusterIssuerName = clusterIssuer.name;
}
})(new cdk8s.App(), 'private-ca-cluster-issuer-chart', privateCaArn, this.region);
const clusterIssuerManifest = eksCluster.addCdk8sChart('private-ca-cluster-issuer-chart', pcaClusterIssuerChart);
clusterIssuerManifest.node.addDependency(pcaManagerHelm);
//Ensure PCA Cluster Issuer is Ready
const checkPcaClusterIssuerReadyProps: KubernetesCmdProps = {
cluster: eksCluster,
namespace: NifiL3Construct.CERT_MANAGER_NAMESPACE,
cmd: [
'get',
'awspcaclusterissuer',
'private-ca-cluster-issuer',
'-o',
'jsonpath="{.status.conditions[?(@.type==\'Ready\')].status }"',
],
expectedOutput: 'True',
};
const checkPcaClusterIssuerReadyCmd = new KubernetesCmd(
this,
'check-pca-cluster-issuer-ready',
checkPcaClusterIssuerReadyProps,
);
checkPcaClusterIssuerReadyCmd.node.addDependency(clusterIssuerManifest);
return [pcaClusterIssuerChart.clusterIssuerName, checkPcaClusterIssuerReadyCmd];
}
private addCertManager(eksCluster: MdaaEKSCluster, servicesNamespaceManifest: KubernetesManifest): KubernetesCmd {
const certManagerHelm = eksCluster.addHelmChart('cert-manager-helm', {
repository: 'https://charts.jetstack.io',
chart: 'cert-manager',
version: '1.13.0',
release: 'cert-manager',
namespace: NifiL3Construct.CERT_MANAGER_NAMESPACE,
createNamespace: false,
values: {
namespace: NifiL3Construct.CERT_MANAGER_NAMESPACE,
installCRDs: true,
global: {
tolerations: [
{
key: 'eks.amazonaws.com/compute-type',
value: 'fargate',
},
],
},
webhook: {
securePort: 10260,
tolerations: [
{
key: 'eks.amazonaws.com/compute-type',
value: 'fargate',
},
],
},
cainjector: {
tolerations: [
{
key: 'eks.amazonaws.com/compute-type',
value: 'fargate',
},
],
},
},
});
certManagerHelm.node.addDependency(servicesNamespaceManifest);
//Ensure External Secrets is Ready
const checkReadyProps: KubernetesCmdProps = {
cluster: eksCluster,
namespace: NifiL3Construct.CERT_MANAGER_NAMESPACE,
cmd: ['get', 'deployment.apps', 'cert-manager-webhook', '-o', "jsonpath='{.status.readyReplicas}'"],
expectedOutput: '1',
};
const checkReadyCmd = new KubernetesCmd(this, 'check-cert-manager-ready', checkReadyProps);
checkReadyCmd.node.addDependency(certManagerHelm);
return checkReadyCmd;
}
private createExternalDnsServiceRole(namespaceName: string, eksCluster: MdaaEKSCluster, zone: HostedZone): IRole {
const route53UpdateStatement = new PolicyStatement({
sid: 'Route53Update',
effect: Effect.ALLOW,
actions: ['route53:ChangeResourceRecordSets'],
resources: [zone.hostedZoneArn],
});
const route53ListStatement = new PolicyStatement({
sid: 'Route53List',
effect: Effect.ALLOW,
actions: ['route53:ListHostedZones', 'route53:ListResourceRecordSets', 'route53:ListTagsForResource'],
resources: ['*'],
});
const suppressions = [
{
id: 'AwsSolutions-IAM5',
reason: 'Access Point Names not known at deployment time. Permissions restricted by condition.',
},
];
return NifiCluster.createServiceRole(
this,
'external-dns',
this.props.naming.resourceName('external-dns-service-role', 64),
namespaceName,
eksCluster,
[route53UpdateStatement, route53ListStatement],
suppressions,
);
}
}