packages/constructs/L3/dataops/dataops-nifi-l3-construct/lib/nifi-cluster.ts (517 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 } from '@aws-mdaa/eks-constructs';
import { IMdaaResourceNaming } from '@aws-mdaa/naming';
import { CfnJson } from 'aws-cdk-lib';
import { ISecurityGroup, ISubnet, IVpc, Protocol, SecurityGroup } from 'aws-cdk-lib/aws-ec2';
import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets';
import { AccessPoint, FileSystem, PerformanceMode } from 'aws-cdk-lib/aws-efs';
import { FargateProfile, KubernetesManifest } from 'aws-cdk-lib/aws-eks';
import {
Effect,
IRole,
ManagedPolicy,
OpenIdConnectPrincipal,
PolicyStatement,
PrincipalWithConditions,
Role,
} from 'aws-cdk-lib/aws-iam';
import { IKey } from 'aws-cdk-lib/aws-kms';
import { IHostedZone } from 'aws-cdk-lib/aws-route53';
import { ISecret, Secret } from 'aws-cdk-lib/aws-secretsmanager';
import { NagPackSuppression } from 'cdk-nag';
import * as cdk8s from 'cdk8s';
import { Construct } from 'constructs';
import { NifiClusterChart, NodeResources } from './cdk8s/nifi-cluster-chart';
import {
NamedNifiRegistryClientProps,
NifiClusterOptions,
NifiIdentityAuthorizationOptions,
NifiNetworkOptions,
NodeSize,
} from './nifi-options';
import { MdaaNagSuppressions } from '@aws-mdaa/construct'; //NOSONAR
export interface NifiClusterProps extends NifiClusterOptions, NifiIdentityAuthorizationOptions, NifiNetworkOptions {
readonly eksCluster: MdaaEKSCluster;
readonly clusterName: string;
readonly kmsKey: IKey;
readonly vpc: IVpc;
readonly subnets: ISubnet[];
readonly naming: IMdaaResourceNaming;
readonly region: string;
readonly zkConnectString: string;
readonly nifiHostedZone: IHostedZone;
readonly nifiCAIssuerName: string;
readonly nifiCertDuration: string;
readonly nifiCertRenewBefore: string;
readonly certKeyAlg: string;
readonly certKeySize: number;
readonly nifiManagerImage: DockerImageAsset;
readonly registryClients?: NamedNifiRegistryClientProps;
readonly fargateProfile: FargateProfile;
}
interface CreateEfsPvsProps {
scope: Construct;
naming: IMdaaResourceNaming;
name: string;
nodeCount: number;
vpc: IVpc;
subnets: ISubnet[];
kmsKey: IKey;
efsSecurityGroup: ISecurityGroup;
}
export class NifiCluster extends Construct {
private readonly props: NifiClusterProps;
public readonly nifiManifest: KubernetesManifest;
public readonly securityGroup: ISecurityGroup;
public readonly httpsPort: number;
public readonly remotePort: number;
public readonly clusterPort: number;
public readonly nodeList: string[];
public readonly adminIdentities: string[];
private static nodeSizeMap: { [key in NodeSize]: NodeResources } = {
SMALL: {
memory: '2Gi',
cpu: '1',
},
MEDIUM: {
memory: '4Gi',
cpu: '2',
},
LARGE: {
memory: '8Gi',
cpu: '4',
},
XLARGE: {
memory: '16Gi',
cpu: '8',
},
'2XLARGE': {
memory: '32Gi',
cpu: '16',
},
};
constructor(scope: Construct, id: string, props: NifiClusterProps) {
super(scope, id);
this.props = props;
const nifiNamespaceName = `nifi-${props.clusterName}`;
this.httpsPort = this.props.httpsPort ?? 8443;
this.remotePort = this.props.remotePort ?? 10000;
this.clusterPort = this.props.clusterPort ?? 14443;
const nodeCount = props.nodeCount ?? 1;
this.adminIdentities = props.adminIdentities;
this.securityGroup = this.createNifiSecurityGroup(props.vpc);
const additionalEfsIngressSecurityGroups = props.additionalEfsIngressSecurityGroupIds?.map(id => {
return SecurityGroup.fromSecurityGroupId(this, `nifi-cluster-efs-ingress-sg-${id}`, id);
});
const efsSecurityGroup = NifiCluster.createEfsSecurityGroup('nifi-cluster', this, props.naming, props.vpc, [
this.securityGroup,
...(additionalEfsIngressSecurityGroups || []),
]);
const nifiEfsPvs = NifiCluster.createEfsPvs({
scope: this,
naming: props.naming,
name: 'nifi',
nodeCount: nodeCount,
vpc: props.vpc,
subnets: props.subnets,
kmsKey: props.kmsKey,
efsSecurityGroup: efsSecurityGroup,
});
const efsManagedPolicy = NifiCluster.createEfsAccessPolicy(
'nifi-cluster',
this,
props.naming,
props.kmsKey,
nifiEfsPvs,
);
props.fargateProfile.podExecutionRole.addManagedPolicy(efsManagedPolicy);
const nifiAdminCredentialsSecret = NifiCluster.createSecret(
this,
'nifi-admin-creds-secret',
props.naming,
'admin-creds-secret',
props.kmsKey,
);
const nifiSensitivePropSecret = NifiCluster.createSecret(
this,
'nifi-sensitive-props-secret',
props.naming,
'sensitive-props-key',
props.kmsKey,
);
const keystorePasswordSecret = NifiCluster.createSecret(
this,
'keystore-password-secret',
props.naming,
'keystore-password',
props.kmsKey,
);
const externalSecretsRole = NifiCluster.createExternalSecretsServiceRole(
this,
'external-secrets',
props.naming,
nifiNamespaceName,
props.eksCluster,
props.kmsKey,
[nifiSensitivePropSecret, keystorePasswordSecret, nifiAdminCredentialsSecret],
);
const clusterServiceRole = NifiCluster.createServiceRole(
this,
'nifi-service-role',
props.naming.resourceName('nifi-service-role', 64),
nifiNamespaceName,
props.eksCluster,
);
this.props.clusterRoleAwsManagedPolicies?.forEach(managedPolicySpec => {
const managedPolicy = ManagedPolicy.fromAwsManagedPolicyName(managedPolicySpec.policyName);
clusterServiceRole.addManagedPolicy(managedPolicy);
MdaaNagSuppressions.addCodeResourceSuppressions(clusterServiceRole, [
{
id: 'AwsSolutions-IAM4',
reason: managedPolicySpec.suppressionReason,
},
]);
});
this.props.clusterRoleManagedPolicies?.forEach(managedPolicyName => {
const managedPolicy = ManagedPolicy.fromManagedPolicyName(
this,
`imported-policy-${managedPolicyName}`,
managedPolicyName,
);
clusterServiceRole.addManagedPolicy(managedPolicy);
});
const nodeSize = NifiCluster.nodeSizeMap[this.props.nodeSize || 'SMALL'];
this.props.nifiManagerImage.repository.grantPull(props.fargateProfile.podExecutionRole);
MdaaNagSuppressions.addCodeResourceSuppressions(
props.fargateProfile.podExecutionRole,
[
{ id: 'AwsSolutions-IAM5', reason: 'ecr:GetAuthorizationToken does not accept a resource.' },
{ id: 'NIST.800.53.R5-IAMNoInlinePolicy', reason: 'Permissions are appropriate as inline policy.' },
{ id: 'HIPAA.Security-IAMNoInlinePolicy', reason: 'Permissions are appropriate as inline policy.' },
{ id: 'PCI.DSS.321-IAMNoInlinePolicy', reason: 'Permissions are appropriate as inline policy.' },
],
true,
);
const nifiK8sChart = new NifiClusterChart(new cdk8s.App(), 'nifi-chart', {
namespace: nifiNamespaceName,
externalSecretsRoleArn: externalSecretsRole.roleArn,
nodeCount: nodeCount,
nodeCpu: nodeSize.cpu,
nodeMemory: nodeSize.memory,
nifiImageTag: props.nifiImageTag,
awsRegion: props.region,
adminCredsSecretName: nifiAdminCredentialsSecret.secretName,
nifiSensitivePropSecretName: nifiSensitivePropSecret.secretName,
keystorePasswordSecretName: keystorePasswordSecret.secretName,
efsPersistentVolumes: nifiEfsPvs.map(x => {
return { efsFsId: x[0].fileSystemId, efsApId: x[1].accessPointId };
}),
efsStorageClassName: props.eksCluster.efsStorageClassName,
saml: props.saml ? { ...props.saml, entityId: `org:apache:nifi:saml:sp-${props.clusterName}` } : undefined,
hostedZoneName: props.nifiHostedZone.zoneName,
zkConnectString: props.zkConnectString,
zkRootNode: `/nifi/${props.clusterName}`,
httpsPort: this.httpsPort,
remotePort: this.remotePort,
clusterPort: this.clusterPort,
caIssuerName: this.props.nifiCAIssuerName,
nifiServiceRoleArn: clusterServiceRole.roleArn,
nifiServiceRoleName: clusterServiceRole.roleName,
nifiCertDuration: this.props.nifiCertDuration,
nifiCertRenewBefore: this.props.nifiCertRenewBefore,
certKeyAlg: this.props.certKeyAlg ?? 'ECDSA',
certKeySize: this.props.certKeySize ?? '384',
nifiManagerImageUri: props.nifiManagerImage.imageUri,
adminIdentities: props.adminIdentities,
externalNodeIdentities: props.externalNodeIdentities,
identities: props.identities,
groups: props.groups,
authorizations: props.authorizations,
registryClients: props.registryClients,
});
this.nodeList = nifiK8sChart.nodeList.map(nodeName => `${nodeName}.${nifiK8sChart.domain}`);
const nifiNamespaceManifest = props.eksCluster.addNamespace(
new cdk8s.App(),
`nifi-namespace-${props.clusterName}`,
nifiNamespaceName,
this.securityGroup,
);
nifiNamespaceManifest.node.addDependency(props.fargateProfile);
this.nifiManifest = props.eksCluster.addCdk8sChart(`nifi-${props.clusterName}`, nifiK8sChart);
this.nifiManifest.node.addDependency(nifiNamespaceManifest);
const restartNifiCmdProps: KubernetesCmdProps = {
cluster: props.eksCluster,
namespace: nifiNamespaceName,
cmd: ['delete', 'pod', '-l', 'app=nifi'],
executionKey: nifiK8sChart.hash(),
};
const restartNifiCmd = new KubernetesCmd(this, 'restart-nifi-cmd', restartNifiCmdProps);
restartNifiCmd.node.addDependency(this.nifiManifest);
}
private createNifiSecurityGroup(vpc: IVpc) {
const ingressRules: MdaaSecurityGroupRuleProps = {
sg: this.props.securityGroupIngressSGs
?.map(sgId => {
return [
{
sgId: sgId,
protocol: Protocol.TCP,
port: this.clusterPort,
},
{
sgId: sgId,
protocol: Protocol.TCP,
port: this.httpsPort,
},
{
sgId: sgId,
protocol: Protocol.TCP,
port: this.remotePort,
},
];
})
.flat(),
ipv4: this.props.securityGroupIngressIPv4s
?.map(ipv4 => {
return [
{
cidr: ipv4,
protocol: Protocol.TCP,
port: this.clusterPort,
},
{
cidr: ipv4,
protocol: Protocol.TCP,
port: this.httpsPort,
},
{
cidr: ipv4,
protocol: Protocol.TCP,
port: this.remotePort,
},
];
})
.flat(),
};
const customEgress: boolean =
(this.props.securityGroupEgressRules?.ipv4 && this.props.securityGroupEgressRules?.ipv4.length > 0) ||
(this.props.securityGroupEgressRules?.prefixList && this.props.securityGroupEgressRules?.prefixList.length > 0) ||
(this.props.securityGroupEgressRules?.sg && this.props.securityGroupEgressRules?.sg.length > 0) ||
false;
const sgProps: MdaaSecurityGroupProps = {
securityGroupName: 'nifi',
vpc: vpc,
addSelfReferenceRule: true,
naming: this.props.naming,
allowAllOutbound: !customEgress,
ingressRules: ingressRules,
egressRules: this.props.securityGroupEgressRules,
};
return new MdaaSecurityGroup(this, 'nifi-sg', sgProps);
}
public static createEfsSecurityGroup(
name: string,
scope: Construct,
naming: IMdaaResourceNaming,
vpc: IVpc,
securityGroups?: ISecurityGroup[],
) {
const efsSgIngressRules: MdaaSecurityGroupRuleProps = {
sg: securityGroups?.map(sg => {
return {
sgId: sg.securityGroupId,
protocol: Protocol.TCP,
port: 2049,
};
}),
};
const sgProps: MdaaSecurityGroupProps = {
securityGroupName: `${name}-efs`,
vpc: vpc,
addSelfReferenceRule: true,
naming: naming,
allowAllOutbound: true,
ingressRules: efsSgIngressRules,
};
return new MdaaSecurityGroup(scope, `${name}-efs-sg`, sgProps);
}
public static createEfsAccessPolicy(
name: string,
scope: Construct,
naming: IMdaaResourceNaming,
kmsKey: IKey,
efsPvs: [FileSystem, AccessPoint][],
): ManagedPolicy {
const describeAzStatement = new PolicyStatement({
sid: 'AllowDescribeAz',
effect: Effect.ALLOW,
actions: ['ec2:DescribeAvailabilityZones'],
resources: ['*'],
});
const efsKmsKeyStatement = new PolicyStatement({
sid: 'AllowEfsKms',
effect: Effect.ALLOW,
actions: [
'kms:Encrypt',
'kms:Decrypt',
'kms:ReEncrypt*',
'kms:GenerateDataKey*',
'kms:CreateGrant',
'kms:DescribeKey',
],
resources: [kmsKey.keyArn],
});
const describeEFSStatement = new PolicyStatement({
sid: `AllowDescribeEFS`,
effect: Effect.ALLOW,
actions: [
'elasticfilesystem:DescribeAccessPoints',
'elasticfilesystem:DescribeMountTargets',
'elasticfilesystem:DescribeFileSystems',
],
resources: [...efsPvs.map(x => x[0].fileSystemArn), ...efsPvs.map(x => x[1].accessPointArn)],
});
const efsStatements = [describeEFSStatement, describeAzStatement, efsKmsKeyStatement];
const efsManagedPolicy = new ManagedPolicy(scope, `${name}-efs-access`, {
managedPolicyName: naming.resourceName(`${name}-efs-access`, 64),
statements: efsStatements,
});
MdaaNagSuppressions.addCodeResourceSuppressions(efsManagedPolicy, [
{
id: 'AwsSolutions-IAM5',
reason: 'Access Point Names not known at deployment time. Permissions restricted by condition.',
},
]);
return efsManagedPolicy;
}
public static createEfsPvs(createEfsPvsProps: CreateEfsPvsProps): [FileSystem, AccessPoint][] {
const efs = new FileSystem(createEfsPvsProps.scope, `efs-${createEfsPvsProps.name}`, {
fileSystemName: createEfsPvsProps.naming.resourceName(createEfsPvsProps.name, 256),
vpc: createEfsPvsProps.vpc,
vpcSubnets: {
subnets: createEfsPvsProps.subnets,
},
performanceMode: PerformanceMode.MAX_IO,
securityGroup: createEfsPvsProps.efsSecurityGroup,
encrypted: true,
kmsKey: createEfsPvsProps.kmsKey,
});
MdaaNagSuppressions.addCodeResourceSuppressions(efs, [
{
id: 'NIST.800.53.R5-EFSInBackupPlan',
reason: 'MDAA does not enforce NIST.800.53.R5-EFSInBackupPlan on EFS volume.',
},
{
id: 'HIPAA.Security-EFSInBackupPlan',
reason: 'MDAA does not enforce HIPAA.Security-EFSInBackupPlan on EFS volume.',
},
{
id: 'PCI.DSS.321-EFSInBackupPlan',
reason: 'MDAA does not enforce HIPAA.Security-EFSInBackupPlan on EFS volume.',
},
]);
return [...Array(createEfsPvsProps.nodeCount).keys()].map(i => {
const ap = new AccessPoint(createEfsPvsProps.scope, `${createEfsPvsProps.name}-pv-ap-${i}`, {
fileSystem: efs,
path: `/${createEfsPvsProps.name}/${i}`,
posixUser: {
uid: '1000',
gid: '1000',
},
createAcl: {
ownerGid: '1000',
ownerUid: '1000',
permissions: '750',
},
});
return [efs, ap];
});
}
public static createSecret(
scope: Construct,
id: string,
naming: IMdaaResourceNaming,
secretName: string,
kmsKey: IKey,
): ISecret {
const secretResourceName = naming.resourceName(secretName, 255);
const nifiSensitivePropSecret = new Secret(scope, id, {
secretName: secretResourceName,
encryptionKey: kmsKey,
generateSecretString: {
excludeCharacters: "'",
excludePunctuation: true,
},
});
MdaaNagSuppressions.addCodeResourceSuppressions(
nifiSensitivePropSecret,
[
{ id: 'AwsSolutions-SMG4', reason: 'Nifi does not support rotation of this secret' },
{ id: 'NIST.800.53.R5-SecretsManagerRotationEnabled', reason: 'Nifi does not support rotation of this secret' },
{ id: 'HIPAA.Security-SecretsManagerRotationEnabled', reason: 'Nifi does not support rotation of this secret' },
{ id: 'PCI.DSS.321-SecretsManagerRotationEnabled', reason: 'Nifi does not support rotation of this secret' },
],
true,
);
return nifiSensitivePropSecret;
}
public static createExternalSecretsServiceRole(
scope: Construct,
roleName: string,
naming: IMdaaResourceNaming,
namespaceName: string,
eksCluster: MdaaEKSCluster,
kmsKey: IKey,
secrets: ISecret[],
): IRole {
const externalSecretServiceRoleName = naming.resourceName('external-secrets-service-role', 64);
const kmsKeyStatement = new PolicyStatement({
sid: 'KmsDecrypt',
effect: Effect.ALLOW,
actions: ['kms:Decrypt'],
resources: [kmsKey.keyArn],
});
const secretsManagerStatement = new PolicyStatement({
sid: 'GetSecretValue',
effect: Effect.ALLOW,
actions: ['SecretsManager:GetSecretValue'],
resources: secrets.map(x => x.secretArn),
});
return NifiCluster.createServiceRole(scope, roleName, externalSecretServiceRoleName, namespaceName, eksCluster, [
kmsKeyStatement,
secretsManagerStatement,
]);
}
public static createServiceRole(
scope: Construct,
id: string,
roleName: string,
namespaceName: string,
eksCluster: MdaaEKSCluster,
statements?: PolicyStatement[],
policyMdaaNagSuppressions?: NagPackSuppression[],
): IRole {
const serviceRole = new Role(scope, `${id}-service-role`, {
roleName: roleName,
assumedBy: new PrincipalWithConditions(new OpenIdConnectPrincipal(eksCluster.iamOidcIdentityProvider), {
StringLike: new CfnJson(scope, `${id}-service-role-assume-conditions`, {
value: {
[`${eksCluster.clusterOpenIdConnectIssuer}:aud`]: 'sts.amazonaws.com',
[`${eksCluster.clusterOpenIdConnectIssuer}:sub`]: `system:serviceaccount:${namespaceName}:*`,
},
}),
}),
});
if (statements) {
const policy = new ManagedPolicy(scope, `${id}-service-policy`, {
managedPolicyName: roleName,
roles: [serviceRole],
statements: statements,
});
if (policyMdaaNagSuppressions) {
MdaaNagSuppressions.addCodeResourceSuppressions(policy, policyMdaaNagSuppressions, true);
}
}
return serviceRole;
}
}