packages/constructs/L2/eks-constructs/lib/cluster.ts (690 lines of code) (raw):

/*! * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ import { MdaaConstructProps, MdaaParamAndOutput } from '@aws-mdaa/construct'; //NOSONAR import { MdaaEC2Instance, MdaaEC2InstanceProps, MdaaEC2SecretKeyPair, MdaaEC2SecretKeyPairProps, } from '@aws-mdaa/ec2-constructs'; import { IMdaaResourceNaming } from '@aws-mdaa/naming'; import { KubectlV27Layer } from '@aws-cdk/lambda-layer-kubectl-v27'; import { Fn, Size, Stack } from 'aws-cdk-lib'; import { ISecurityGroup, ISubnet, IVpc, Port, InstanceType, InstanceClass, InstanceSize, MachineImage, Subnet, UserData, IInstance, } from 'aws-cdk-lib/aws-ec2'; import { AlbControllerOptions, Cluster, ClusterLoggingTypes, ClusterProps, CoreDnsComputeType, EndpointAccess, FargateProfile, FargateProfileOptions, IKubectlProvider, IpFamily, KubernetesManifest, KubernetesVersion, } from 'aws-cdk-lib/aws-eks'; import { ArnPrincipal, Effect, IRole, ManagedPolicy, OpenIdConnectProvider, PolicyStatement, PrincipalWithConditions, Role, ServicePrincipal, } from 'aws-cdk-lib/aws-iam'; import { IKey } from 'aws-cdk-lib/aws-kms'; import { ILayerVersion } from 'aws-cdk-lib/aws-lambda'; import { LogGroup, LogGroupProps, RetentionDays } from 'aws-cdk-lib/aws-logs'; import { MdaaNagSuppressions } from '@aws-mdaa/construct'; //NOSONAR import * as cdk8s from 'cdk8s'; import { Construct } from 'constructs'; import * as k8s from '../imports/k8s'; import { CompliantKubectlProvider } from './mdaa-kubectl-provider'; import { MdaaManagedPolicy, MdaaRole, MdaaRoleProps } from '@aws-mdaa/iam-constructs'; export interface MgmtInstanceProps { readonly instanceType?: InstanceType; readonly subnetId: string; readonly availabilityZone: string; readonly keyPairName?: string; readonly userDataCommands?: string[]; readonly mgmtPolicyStatements?: PolicyStatement[]; } /** * Properties for creating a Compliance EKS cluster */ export interface MdaaEKSClusterProps extends MdaaConstructProps { /** * If defined, an EC2 instance will be created with connectivity, permissions, and tooling to manage the EKS cluster */ readonly mgmtInstance?: MgmtInstanceProps; readonly adminRoles: IRole[]; /** * The IAM role to pass to the Kubectl Lambda Handler. * * @default - Default Lambda IAM Execution Role */ readonly kubectlLambdaRole?: IRole; /** * The tags assigned to the EKS cluster * * @default - none */ readonly tags?: { [key: string]: string; }; /** * An IAM role that will be added to the `system:masters` Kubernetes RBAC * group. * * @see https://kubernetes.io/docs/reference/access-authn-authz/rbac/#default-roles-and-role-bindings * * @default - no masters role. */ readonly mastersRole?: IRole; /** * Controls the "eks.amazonaws.com/compute-type" annotation in the CoreDNS * configuration on your cluster to determine which compute type to use * for CoreDNS. * * @default CoreDnsComputeType.EC2 (for `FargateCluster` the default is FARGATE) */ readonly coreDnsComputeType?: CoreDnsComputeType; /** * Determines whether a CloudFormation output with the ARN of the "masters" * IAM role will be synthesized (if `mastersRole` is specified). * * @default false */ readonly outputMastersRoleArn?: boolean; /** * Environment variables for the kubectl execution. Only relevant for kubectl enabled clusters. * * @default - No environment variables. */ readonly kubectlEnvironment?: { [key: string]: string; }; /** * An AWS Lambda layer that contains the `aws` CLI. * * The handler expects the layer to include the following executables: * * ``` * /opt/awscli/aws * ``` * * @default - a default layer with the AWS CLI 1.x */ readonly awscliLayer?: ILayerVersion; /** * Amount of memory to allocate to the provider's lambda function. * * @default Size.gibibytes(1) */ readonly kubectlMemory?: Size; /** * Custom environment variables when interacting with the EKS endpoint to manage the cluster lifecycle. * * @default - No environment variables. */ readonly clusterHandlerEnvironment?: { [key: string]: string; }; /** * A security group to associate with the Cluster Handler's Lambdas. * The Cluster Handler's Lambdas are responsible for calling AWS's EKS API. * * Requires `placeClusterHandlerInVpc` to be set to true. * * @default - No security group. */ readonly clusterHandlerSecurityGroup?: ISecurityGroup; /** * An AWS Lambda Layer which includes the NPM dependency `proxy-agent`. This layer * is used by the onEvent handler to route AWS SDK requests through a proxy. * * By default, the provider will use the layer included in the * "aws-lambda-layer-node-proxy-agent" SAR application which is available in all * commercial regions. * * To deploy the layer locally define it in your app as follows: * * ```ts * const layer = new lambda.LayerVersion(this, 'proxy-agent-layer', { * code: lambda.Code.fromAsset(`${__dirname}/layer.zip`), * compatibleRuntimes: [lambda.Runtime.NODEJS_14_X], * }); * ``` * * @default - a layer bundled with this module. */ readonly onEventLayer?: ILayerVersion; /** * Indicates whether Kubernetes resources added through `addManifest()` can be * automatically pruned. When this is enabled (default), prune labels will be * allocated and injected to each resource. These labels will then be used * when issuing the `kubectl apply` operation with the `--prune` switch. * * @default true */ readonly prune?: boolean; /** * KMS secret for envelope encryption for Kubernetes secrets and data at rest. */ readonly kmsKey: IKey; /** * Specify which IP family is used to assign Kubernetes pod and service IP addresses. * * @default - IpFamily.IP_V4 * @see https://docs.aws.amazon.com/eks/latest/APIReference/API_KubernetesNetworkConfigRequest.html#AmazonEKS-Type-KubernetesNetworkConfigRequest-ipFamily */ readonly ipFamily?: IpFamily; /** * The CIDR block to assign Kubernetes service IP addresses from. * * @default - Kubernetes assigns addresses from either the * 10.100.0.0/16 or 172.20.0.0/16 CIDR blocks * @see https://docs.aws.amazon.com/eks/latest/APIReference/API_KubernetesNetworkConfigRequest.html#AmazonEKS-Type-KubernetesNetworkConfigRequest-serviceIpv4Cidr */ readonly serviceIpv4Cidr?: string; /** * Install the AWS Load Balancer Controller onto the cluster. * * @see https://kubernetes-sigs.github.io/aws-load-balancer-controller * * @default - The controller is not installed. */ readonly albController?: AlbControllerOptions; /** * The VPC in which to create the Cluster. * */ readonly vpc: IVpc; /** * Explicitly select individual subnets * */ readonly subnets: ISubnet[]; /** * Role that provides permissions for the Kubernetes control plane to make calls to AWS API operations on your behalf. * * @default - A role is automatically created for you */ readonly role?: IRole; /** * Name for the cluster. * * @default - Automatically generated name */ readonly clusterName?: string; /** * Security Group to use for Control Plane ENIs * * @default - A security group is automatically created */ readonly securityGroup?: ISecurityGroup; /** * The Kubernetes version to run in the cluster */ readonly version: KubernetesVersion; /** * Determines whether a CloudFormation output with the name of the cluster * will be synthesized. * * @default false */ readonly outputClusterName?: boolean; /** * Determines whether a CloudFormation output with the `aws eks * update-kubeconfig` command will be synthesized. This command will include * the cluster name and, if applicable, the ARN of the masters IAM role. * * @default true */ readonly outputConfigCommand?: boolean; } /** * A construct for creating a compliant EKS cluster resource. */ export class MdaaEKSCluster extends Cluster { private static KUBECTL_URL = 'https://s3.us-west-2.amazonaws.com/amazon-eks/1.27.9/2024-01-04/bin/linux/amd64/kubectl'; private static setProps(scope: Construct, props: MdaaEKSClusterProps): ClusterProps { const overrideProps = { clusterName: props.naming.resourceName(props.clusterName, 255), endpointAccess: EndpointAccess.PRIVATE, // Force private endpoint access only defaultCapacity: 0, // Force specification of a node group to ensure encryption at rest secretsEncryptionKey: props.kmsKey, kubectlLayer: new KubectlV27Layer(scope, 'kubectl-layer'), vpcSubnets: [ { subnets: props.subnets, }, ], // Force enabling all logging types clusterLogging: [ ClusterLoggingTypes.API, ClusterLoggingTypes.AUDIT, ClusterLoggingTypes.AUTHENTICATOR, ClusterLoggingTypes.CONTROLLER_MANAGER, ClusterLoggingTypes.SCHEDULER, ], }; const allProps: ClusterProps = { ...props, ...overrideProps, }; return allProps; } public readonly iamOidcIdentityProvider: OpenIdConnectProvider; public readonly efsStorageClassName: string; private readonly props: MdaaEKSClusterProps; public readonly mdaaKubeCtlProvider: IKubectlProvider; public readonly clusterFargateProfileArn: string; private podExecutionRolePolicy: ManagedPolicy; public readonly mgmtInstance?: IInstance; constructor(scope: Construct, id: string, props: MdaaEKSClusterProps) { super(scope, id, MdaaEKSCluster.setProps(scope, props)); this.props = props; this.clusterFargateProfileArn = `arn:${Stack.of(scope).partition}:eks:${Stack.of(scope).region}:${ Stack.of(scope).account }:fargateprofile/${props.naming.resourceName(props.clusterName, 255)}/*`; this.mdaaKubeCtlProvider = this.defineCompliantKubectlProvider(); props.adminRoles.forEach(adminRole => { this.awsAuth.addMastersRole(adminRole); }); const stackId = Fn.select(0, Fn.split('-', Fn.select(2, Fn.split('/', Stack.of(scope).stackId)))); const podLogGroupProps: LogGroupProps = { encryptionKey: this.props.kmsKey, logGroupName: `/aws/eks/${props.naming.resourceName(props.clusterName, 255)}/${stackId}/pods`, retention: RetentionDays.INFINITE, }; const podLogGroup = new LogGroup(this, 'pod-log-group', podLogGroupProps); MdaaNagSuppressions.addCodeResourceSuppressions( podLogGroup, [ { id: 'NIST.800.53.R5-CloudWatchLogGroupRetentionPeriod', reason: 'LogGroup retention is set to RetentionDays.INFINITE.', }, { id: 'HIPAA.Security-CloudWatchLogGroupRetentionPeriod', reason: 'LogGroup retention is set to RetentionDays.INFINITE.', }, { id: 'PCI.DSS.321-CloudWatchLogGroupRetentionPeriod', reason: 'LogGroup retention is set to RetentionDays.INFINITE.', }, ], true, ); this.iamOidcIdentityProvider = new OpenIdConnectProvider(this, 'iam-oidc-identity-provider', { url: this.clusterOpenIdConnectIssuerUrl, clientIds: ['sts.amazonaws.com'], }); this.podExecutionRolePolicy = new ManagedPolicy(this, 'systemPodExecutionRolePolicy', { statements: [ new PolicyStatement({ actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:DescribeLogStreams', 'logs:PutLogEvents'], resources: [ `arn:${Stack.of(this).partition}:logs:${Stack.of(this).region}:${Stack.of(this).account}:log-group:${ podLogGroup.logGroupName }*`, `arn:${Stack.of(this).partition}:logs:${Stack.of(this).region}:${Stack.of(this).account}:log-group:${ podLogGroup.logGroupName }:log-stream:*`, ], effect: Effect.ALLOW, }), ], }); MdaaNagSuppressions.addCodeResourceSuppressions( this.podExecutionRolePolicy, [{ id: 'AwsSolutions-IAM5', reason: 'Log stream name not known at deployment time.' }], true, ); this.addFargateProfile('system', { fargateProfileName: 'system', selectors: [ { namespace: 'default', }, { namespace: 'kube-system', }, { namespace: 'aws-observability', }, ], }); const cdk8sApp = new cdk8s.App(); const efsStorageClassChart = new EfsStorageClassChart(cdk8sApp, 'efs-sc-chart'); this.efsStorageClassName = efsStorageClassChart.storageClassName; this.addCdk8sChart('efs-sc-chart', efsStorageClassChart); const awsObservabilityNamespace = this.addNamespace( cdk8sApp, 'aws-observability-namespace', 'aws-observability', props.securityGroup, ); const awsObservabilityChart = this.addCdk8sChart( 'aws-observability-chart', new AwsObservabilityChart(cdk8sApp, 'aws-observability-chart', { logGroupName: podLogGroup.logGroupName, region: Stack.of(this).region, }), ); awsObservabilityChart.node.addDependency(awsObservabilityNamespace); MdaaNagSuppressions.addCodeResourceSuppressions( this.role, [{ id: 'AwsSolutions-IAM4', reason: 'AmazonEKSClusterPolicy is required for proper cluster function.' }], true, ); MdaaNagSuppressions.addCodeResourceSuppressions( this.adminRole, [ { id: 'AwsSolutions-IAM5', reason: 'EC2 resources not known at deployment time.', appliesTo: [`Resource::*`], }, { id: 'AwsSolutions-IAM5', reason: 'Permissions limited to specific cluster', }, { id: 'AwsSolutions-IAM5', reason: 'Permissions limited to specific cluster', }, { id: 'NIST.800.53.R5-IAMNoInlinePolicy', reason: 'Permissions are specific to cluster.' }, { id: 'HIPAA.Security-IAMNoInlinePolicy', reason: 'Permissions are specific to cluster.' }, { id: 'PCI.DSS.321-IAMNoInlinePolicy', reason: 'Permissions are specific to cluster.' }, ], true, ); if (this.kubectlLambdaRole) { MdaaNagSuppressions.addCodeResourceSuppressions( this.kubectlLambdaRole, [ { id: 'AwsSolutions-IAM4', reason: 'AWSLambdaBasicExecutionRole, AWSLambdaVPCAccessExecutionRole are least privilege.', }, { id: 'AwsSolutions-IAM5', reason: 'S3 CDK Asset names not known at deployment time' }, { id: 'NIST.800.53.R5-IAMNoInlinePolicy', reason: 'Permissions are specific to custom resource requirements.', }, { id: 'HIPAA.Security-IAMNoInlinePolicy', reason: 'Permissions are specific to custom resource requirements.', }, { id: 'PCI.DSS.321-IAMNoInlinePolicy', reason: 'Permissions are specific to custom resource requirements.' }, ], true, ); } const clusterResourceProvider = Stack.of(this).node.tryFindChild('@aws-cdk--aws-eks.ClusterResourceProvider'); if (clusterResourceProvider) { MdaaNagSuppressions.addCodeResourceSuppressions( clusterResourceProvider, [ { id: 'AwsSolutions-IAM4', reason: 'AWSLambdaBasicExecutionRole, AWSLambdaVPCAccessExecutionRole are least privilege.', }, { id: 'AwsSolutions-IAM5', reason: 'Resource names not known at deployment time.' }, { id: 'AwsSolutions-L1', reason: 'Function generated by EKS L2 construct.' }, { id: 'NIST.800.53.R5-LambdaConcurrency', reason: 'Function is used as Cfn Custom Resource only during deployment time. Concurrency managed via Cfn.', }, { id: 'NIST.800.53.R5-LambdaDLQ', reason: 'Function is used as Cfn Custom Resource only during deployment time. Error handling managed via Cfn.', }, { id: 'NIST.800.53.R5-IAMNoInlinePolicy', reason: 'Policy statements are specific to custom resource.' }, { id: 'NIST.800.53.R5-LambdaInsideVPC', reason: 'Function is used as Cfn Custom Resource only during deployment time.', }, { id: 'HIPAA.Security-LambdaInsideVPC', reason: 'Function is used as Cfn Custom Resource only during deployment time.', }, { id: 'PCI.DSS.321-LambdaInsideVPC', reason: 'Function is used as Cfn Custom Resource only during deployment time.', }, { id: 'HIPAA.Security-LambdaConcurrency', reason: 'Function is used as Cfn Custom Resource only during deployment time. Concurrency managed via Cfn.', }, { id: 'PCI.DSS.321-LambdaConcurrency', reason: 'Function is used as Cfn Custom Resource only during deployment time. Concurrency managed via Cfn.', }, { id: 'HIPAA.Security-LambdaDLQ', reason: 'Function is used as Cfn Custom Resource only during deployment time. Error handling managed via Cfn.', }, { id: 'PCI.DSS.321-LambdaDLQ', reason: 'Function is used as Cfn Custom Resource only during deployment time. Error handling managed via Cfn.', }, { id: 'HIPAA.Security-IAMNoInlinePolicy', reason: 'Policy statements are specific to custom resource.' }, { id: 'PCI.DSS.321-IAMNoInlinePolicy', reason: 'Policy statements are specific to custom resource.' }, { id: 'HIPAA.Security-CloudWatchLogGroupEncrypted', reason: 'Loggroup data is always encrypted in CloudWatch Logs', }, { id: 'PCI.DSS.321-CloudWatchLogGroupEncrypted', reason: 'Loggroup data is always encrypted in CloudWatch Logs', }, { id: 'NIST.800.53.R5-CloudWatchLogGroupEncrypted', reason: 'Loggroup data is always encrypted in CloudWatch Logs', }, { id: 'AwsSolutions-SF1', reason: 'Function is used as Cfn Custom Resource only during deployment time.' }, { id: 'AwsSolutions-SF2', reason: 'Function is used as Cfn Custom Resource only during deployment time.' }, ], true, ); } const kubeCtlProvider = Stack.of(this).node.tryFindChild('@aws-cdk--aws-eks.KubectlProvider'); if (kubeCtlProvider) { MdaaNagSuppressions.addCodeResourceSuppressions( kubeCtlProvider, [ { id: 'AwsSolutions-IAM4', reason: 'AWSLambdaBasicExecutionRole, AWSLambdaVPCAccessExecutionRole are least privilege.', }, { id: 'AwsSolutions-IAM5', reason: 'Resource names not known at deployment time.' }, { id: 'AwsSolutions-L1', reason: 'Function generated by EKS L2 construct.' }, { id: 'NIST.800.53.R5-LambdaConcurrency', reason: 'Function is used as Cfn Custom Resource only during deployment time. Concurrency managed via Cfn.', }, { id: 'NIST.800.53.R5-LambdaDLQ', reason: 'Function is used as Cfn Custom Resource only during deployment time. Error handling managed via Cfn.', }, { id: 'NIST.800.53.R5-IAMNoInlinePolicy', reason: 'Policy statements are specific to custom resource.' }, { id: 'HIPAA.Security-LambdaConcurrency', reason: 'Function is used as Cfn Custom Resource only during deployment time. Concurrency managed via Cfn.', }, { id: 'PCI.DSS.321-LambdaConcurrency', reason: 'Function is used as Cfn Custom Resource only during deployment time. Concurrency managed via Cfn.', }, { id: 'HIPAA.Security-LambdaDLQ', reason: 'Function is used as Cfn Custom Resource only during deployment time. Error handling managed via Cfn.', }, { id: 'PCI.DSS.321-LambdaDLQ', reason: 'Function is used as Cfn Custom Resource only during deployment time. Error handling managed via Cfn.', }, { id: 'HIPAA.Security-IAMNoInlinePolicy', reason: 'Policy statements are specific to custom resource.' }, { id: 'PCI.DSS.321-IAMNoInlinePolicy', reason: 'Policy statements are specific to custom resource.' }, ], true, ); } new MdaaParamAndOutput( this, { ...{ resourceType: 'cluster', resourceId: props.clusterName, name: 'arn', value: this.clusterFargateProfileArn, }, ...props, }, scope, ); new MdaaParamAndOutput( this, { ...{ resourceType: 'cluster', resourceId: props.clusterName, name: 'name', value: this.clusterName, }, ...props, }, scope, ); //Required to describe the cluster and configure kubectl const eksStatment = new PolicyStatement({ actions: ['eks:DescribeCluster'], effect: Effect.ALLOW, resources: [this.clusterArn], }); //Required to run SSM patching against instance const ssmStatement = new PolicyStatement({ actions: [ 'ssm:UpdateInstanceInformation', 'ssm:UpdateInstanceAssociationStatus', 'ssm:UpdateAssociationStatus', 'ssm:PutInventory', 'ssm:PutConfigurePackageResult', 'ssm:PutComplianceItems', 'ssm:ListInstanceAssociations', 'ssm:ListAssociations', 'ssm:GetManifest', 'ssm:GetDocument', 'ssm:GetDeployablePatchSnapshotForInstance', 'ssm:DescribeDocument', 'ssm:DescribeAssociation', 'ssmmessages:OpenDataChannel', 'ssmmessages:OpenControlChannel', 'ssmmessages:CreateDataChannel', 'ssmmessages:CreateControlChannel', 'ec2messages:SendReply', 'ec2messages:GetMessages', 'ec2messages:GetEndpoint', 'ec2messages:FailMessage', 'ec2messages:DeleteMessage', 'ec2messages:AcknowledgeMessage', ], effect: Effect.ALLOW, resources: ['*'], }); const mgmtPolicy = new MdaaManagedPolicy(this, 'cluster-mgmt-policy', { naming: props.naming, managedPolicyName: 'cluster-mgmt', statements: [eksStatment, ssmStatement, ...(props.mgmtInstance?.mgmtPolicyStatements || [])], }); MdaaNagSuppressions.addCodeResourceSuppressions( mgmtPolicy, [ { id: 'AwsSolutions-IAM5', reason: 'Resource names not known at deployment time.', }, ], true, ); this.mgmtInstance = this.createMgmtInstance(mgmtPolicy); } private createMgmtInstance(mgmtPolicy: ManagedPolicy): IInstance | undefined { if (this.props.mgmtInstance) { const instanceRoleProps: MdaaRoleProps = { roleName: 'mgmt-instance', assumedBy: new ServicePrincipal('ec2.amazonaws.com'), naming: this.props.naming, managedPolicies: [mgmtPolicy], }; const instanceRole = new MdaaRole(this, 'mgmt-instance-role', instanceRoleProps); this.awsAuth.addMastersRole(instanceRole); const createKeyPairProps: MdaaEC2SecretKeyPairProps = { name: 'mgmt-instance', kmsKey: this.props.kmsKey, naming: this.props.naming, readPrincipals: this.props.adminRoles.map(x => new ArnPrincipal(x.roleArn)), }; const keyPair = this.props.mgmtInstance?.keyPairName ? undefined : new MdaaEC2SecretKeyPair(this, `key-pair-mgmt-instance`, createKeyPairProps); const keyPairName = this.props.mgmtInstance?.keyPairName ?? keyPair?.name; const mgmtUserData: UserData = UserData.forLinux(); mgmtUserData.addCommands( `mkdir -p /usr/local/bin && cd /usr/local/bin && curl -O ${MdaaEKSCluster.KUBECTL_URL} && chmod +x /usr/local/bin/kubectl && cd ~`, `aws eks update-kubeconfig --region ${Stack.of(this).region} --name ${this.clusterName}`, `cp /root/.kube/config /etc/kubeconfig && chmod o+r /etc/kubeconfig`, `echo 'export KUBECONFIG=/etc/kubeconfig' >> /etc/profile.d/kubectl.sh`, ...(this.props.mgmtInstance.userDataCommands ?? []), ); const mgmtInstanceProps: MdaaEC2InstanceProps = { instanceName: 'mgmt', instanceType: this.props.mgmtInstance.instanceType ?? InstanceType.of(InstanceClass.T3, InstanceSize.MICRO), machineImage: MachineImage.latestAmazonLinux2023(), vpc: this.props.vpc, instanceSubnet: Subnet.fromSubnetAttributes(this, 'mgmt-instance-subnet', { subnetId: this.props.mgmtInstance.subnetId, availabilityZone: this.props.mgmtInstance.availabilityZone, }), blockDeviceProps: [ { deviceName: '/dev/xvda', volumeSizeInGb: 50, }, ], kmsKey: this.props.kmsKey, role: instanceRole, naming: this.props.naming, securityGroup: this.clusterSecurityGroup, keyName: keyPairName, userData: mgmtUserData, }; const instance = new MdaaEC2Instance(this, 'mgmt-instance', mgmtInstanceProps); if (keyPair) instance.node.addDependency(keyPair); return instance; } return undefined; } private defineCompliantKubectlProvider() { const uid = 'CompliantKubectlProvider'; // since we can't have the provider connect to multiple networks, and we // wanted to avoid resource tear down, we decided for now that we will only // support a single EKS cluster per CFN stack. if (this.stack.node.tryFindChild(uid)) { throw new Error('Only a single EKS cluster can be defined within a CloudFormation stack'); } return new CompliantKubectlProvider(this.stack, uid, { cluster: this }); } private createFargatePodExecutionRole(profileName: string, scope?: Construct, naming?: IMdaaResourceNaming): IRole { const podExecutionRole = new Role(scope ?? this, `fargate-pod-ex-${profileName}`, { roleName: (naming ?? this.props.naming).resourceName(profileName, 64), assumedBy: new PrincipalWithConditions(new ServicePrincipal('eks-fargate-pods.amazonaws.com'), { ArnLike: { 'aws:SourceArn': this.clusterFargateProfileArn, }, }), managedPolicies: [ManagedPolicy.fromAwsManagedPolicyName('AmazonEKSFargatePodExecutionRolePolicy')], }); MdaaNagSuppressions.addCodeResourceSuppressions( podExecutionRole, [ { id: 'AwsSolutions-IAM4', reason: 'AmazonEKSFargatePodExecutionRolePolicy is required for proper cluster function.', appliesTo: ['Policy::arn:<AWS::Partition>:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy'], }, ], true, ); podExecutionRole.addManagedPolicy(this.podExecutionRolePolicy); return podExecutionRole; } public addFargateProfile(id: string, profileOptions: FargateProfileOptions): FargateProfile { const podExecutionRole = profileOptions.podExecutionRole ?? this.createFargatePodExecutionRole(id, this, this.props.naming); return super.addFargateProfile(id, { podExecutionRole: podExecutionRole, ...profileOptions, }); } public addNamespace( scope: Construct, id: string, namespaceName: string, securityGroup?: ISecurityGroup, ): KubernetesManifest { if (securityGroup) { //Need to permit traffic between cluster security group and pod/namespace security group securityGroup.connections.allowFrom(this.clusterSecurityGroup, Port.allTraffic()); securityGroup.connections.allowTo(this.clusterSecurityGroup, Port.allTraffic()); this.clusterSecurityGroup.connections.allowFrom(securityGroup, Port.allTraffic()); this.clusterSecurityGroup.connections.allowTo(securityGroup, Port.allTraffic()); MdaaNagSuppressions.addCodeResourceSuppressions( securityGroup, [ { id: 'NIST.800.53.R5-EC2RestrictedCommonPorts', reason: 'Unrestricted traffic is required between cluster and pods.', }, { id: 'HIPAA.Security-EC2RestrictedCommonPorts', reason: 'Unrestricted traffic is required between cluster and pods.', }, { id: 'PCI.DSS.321-EC2RestrictedCommonPorts', reason: 'Unrestricted traffic is required between cluster and pods.', }, ], true, ); MdaaNagSuppressions.addCodeResourceSuppressions( this.clusterSecurityGroup, [ { id: 'NIST.800.53.R5-EC2RestrictedCommonPorts', reason: 'Unrestricted traffic is required between cluster and pods.', }, { id: 'HIPAA.Security-EC2RestrictedCommonPorts', reason: 'Unrestricted traffic is required between cluster and pods.', }, { id: 'PCI.DSS.321-EC2RestrictedCommonPorts', reason: 'Unrestricted traffic is required between cluster and pods.', }, ], true, ); } return this.addCdk8sChart( id, new NamespaceChart(scope, id, { namespaceName: namespaceName, securityGroupId: securityGroup?.securityGroupId }), ); } } interface AwsObservabilityChartProps { readonly region: string; readonly logGroupName: string; } class AwsObservabilityChart extends cdk8s.Chart { public static outputConfig = ` [OUTPUT] Name cloudwatch_logs Match * region CLOUDWATCH_LOGS_REGION_CODE log_group_name LOG_GROUP_NAME auto_create_group false`; constructor(scope: Construct, id: string, props: AwsObservabilityChartProps) { super(scope, id); const outputConfig = AwsObservabilityChart.outputConfig .replace('LOG_GROUP_NAME', props.logGroupName) .replace('CLOUDWATCH_LOGS_REGION_CODE', props.region); new k8s.KubeConfigMap(this, 'aws-observability-config-map', { metadata: { name: 'aws-logging', namespace: 'aws-observability', }, data: { flb_log_cw: 'false', 'output.conf': outputConfig, }, }); } } interface NamespaceChartProps { readonly namespaceName: string; readonly securityGroupId?: string; } class NamespaceChart extends cdk8s.Chart { constructor(scope: Construct, id: string, props: NamespaceChartProps) { super(scope, id); const namespace = new k8s.KubeNamespace(this, 'namespace', { metadata: { name: props.namespaceName, }, }); if (props.securityGroupId) { new cdk8s.ApiObject(this, 'security-group-policy', { apiVersion: 'vpcresources.k8s.aws/v1beta1', kind: 'SecurityGroupPolicy', metadata: { name: 'security-group-policy', namespace: namespace.name, }, spec: { podSelector: {}, securityGroups: { groupIds: [props.securityGroupId], }, }, }); } } } class EfsStorageClassChart extends cdk8s.Chart { public storageClassName: string; constructor(scope: Construct, id: string) { super(scope, id); this.storageClassName = 'efs-sc'; new k8s.KubeStorageClass(this, 'efs-storage-class', { metadata: { name: this.storageClassName, }, provisioner: 'efs.csi.aws.com', }); } }