AWSTemplateFormatVersion: '2010-09-09' Description: 'Cloudformation Template to create EKS cluster' Metadata: 'AWS::CloudFormation::Interface': ParameterGroups: - Label: default: 'VPC Parameters' Parameters: - ClassB - Label: default: Cloud9 Configuration Parameters: - C9InstanceType ParameterLabels: Application: default: Application Name ClassB: default: ClassB 2nd Octet C9InstanceType: default: Cloud9 Instance Type Parameters: Application: Description: 'Specify Application Name' Type: String Default: 'eksack' EKSIAMRoleName: Type: String Description: The name of the IAM role for the EKS service to assume. Default: EKSIAMAdminRole EKSClusterName: Type: String Description: The desired name of your AWS EKS Cluster. Default: eksclu KubernetesVersion: Description: The Kubernetes version to install Type: String Default: 1.23 AllowedValues: - 1.25 - 1.24 - 1.23 NumWorkerNodes: Type: Number Description: Number of worker nodes to create Default: 2 NodeImageIdSSMParam: Type: "AWS::SSM::Parameter::Value" Default: /aws/service/eks/optimized-ami/1.14/amazon-linux-2/recommended/image_id Description: AWS Systems Manager Parameter Store parameter of the AMI ID for the worker node instances. NodeInstanceType: Description: EC2 instance type for the node instances Type: String Default: t3.large LoabdBalancerVersion: Description: EKS Load Balancer Controller Version Type: String Default: v2.4.1 SubnetAPrivate: Description: SubnetAPrivate Type: String Default: subnet-0c40f93bba8c7a898 SubnetBPrivate: Description: SubnetBPrivate Type: String Default: subnet-0f890153b313f45b6 SubnetCPrivate: Description: SubnetCPrivate Type: String Default: subnet-0ad40ae4a0953a6e6 VPC: Description: VPC ID Type: String Default: vpc-0ebb65d1194794109 Resources: #============================================================================# # Creating IAM role for ACK #============================================================================# ACKGrants: Type: Custom::ACKGrants DependsOn: - EKSCluster Properties: ServiceToken: !GetAtt 'ACKGrantsLambda.Arn' eks_cluster_name: !Ref EKSClusterName ACKGrantsRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Path: / Policies: - PolicyName: lambda-createkeypair1 PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - eks:DescribeCluster - iam:CreateRole - iam:DescribeRole - iam:AttachRolePolicy - iam:GetRole - iam:CreatePolicy - ec2:DescribeSubnets Resource: - '*' ACKGrantsLambda: Type: AWS::Lambda::Function Properties: Description: Creates a keypair and stores private key in SSM parameter store. Handler: index.lambda_handler Runtime: python3.9 Environment: Variables: loadbalancerversion: !Ref LoabdBalancerVersion vpcid: !Ref VPC Role: !GetAtt 'ACKGrantsRole.Arn' Timeout: 300 Code: ZipFile: | import json import boto3 import sys import os import urllib.request import cfnresponse import traceback from botocore.exceptions import ClientError def get_doc(url): try: res = urllib.request.urlopen(urllib.request.Request( url=url, headers={'Accept': 'application/json'}, method='GET'), timeout=5) result = res.read().decode().strip() return result except: return {} def get_arns(url): try: res = urllib.request.urlopen(urllib.request.Request( url=url, headers={'Accept': 'application/json'}, method='GET'), timeout=5) result = res.read().decode().strip().split("\n") return result except: return [] def ack_permissions(eks_cluster_name): service_names = ['rds'] output = {} aws_account_id=boto3.client('sts').get_caller_identity().get('Account') print ("Account id {}".format(aws_account_id)) oidc_provider = boto3.client('eks').describe_cluster(name=eks_cluster_name)['cluster']['identity']['oidc']['issuer'].replace("https://","") ack_k8s_namespace="ack-system" for service in service_names: ack_k8s_service_account_name="ack-{}-controller".format(service) trust_json = """{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::"""+aws_account_id+""":oidc-provider/"""+oidc_provider+"""" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { \""""+ oidc_provider+""":sub": "system:serviceaccount:"""+ack_k8s_namespace+""":"""+ack_k8s_service_account_name+"""" } } } ] }""" ack_controller_iam_role = "ack-{}-controller".format(service) ack_controller_iam_role_description = "IRSA role for ACK {} controller deployment on EKS cluster using Helm charts".format(service) response = None try: response = boto3.client("iam").create_role(RoleName=ack_controller_iam_role, AssumeRolePolicyDocument=trust_json, Description = ack_controller_iam_role_description) except ClientError as e: if e.response['Error']['Code'] == 'EntityAlreadyExists': response = boto3.client("iam").get_role(RoleName=ack_controller_iam_role) print("Iam Role {} already exists".format(ack_controller_iam_role)) ack_controller_iam_role_arn = response['Role']['Arn'] output[ack_controller_iam_role] = ack_controller_iam_role_arn base_url="https://raw.githubusercontent.com/aws-controllers-k8s/{}-controller/main".format(service) policy_arn_url = "{}/config/iam/recommended-policy-arn".format(base_url) policy_arn_strings = get_arns(policy_arn_url) for policy_arn in policy_arn_strings: response = boto3.client("iam").attach_role_policy(RoleName=ack_controller_iam_role, PolicyArn = policy_arn) inline_policy_url="{}/config/iam/recommended-inline-policy".format(base_url) inline_policy=get_arns(inline_policy_url) loadbalancerversion = os.environ.get('loadbalancerversion') policyDoc = get_doc("https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/{}/docs/install/iam_policy.json".format(loadbalancerversion)) try: response = boto3.client("iam").create_policy(PolicyName="AWSLoadBalancerControllerIAMPolicy", PolicyDocument=policyDoc, Description="Load balancer controller policy") except ClientError as e: if e.response['Error']['Code'] == 'EntityAlreadyExists': pass output["AWSLoadBalancerControllerIAMPolicy"] = "arn:aws:iam::{}:policy/AWSLoadBalancerControllerIAMPolicy".format(aws_account_id) return str(json.dumps(output,indent=2)) def lambda_handler(event, context): status = cfnresponse.SUCCESS data = {} eks_cluster_name = event['ResourceProperties']['eks_cluster_name'] print ("My event {}".format(event)) print("Eks cluster name {}".format(eks_cluster_name)) key_name = eks_cluster_name if event.get('RequestType') != 'Create': cfnresponse.send(event, context, status, data, key_name, noEcho=True) try: data['Arns'] = ack_permissions(eks_cluster_name) except: traceback.print_exc() status = cfnresponse.FAILED cfnresponse.send(event, context, status, data, key_name, noEcho=True) DBSubnetGroup: Type: 'AWS::RDS::DBSubnetGroup' Properties: DBSubnetGroupName: rds-db-subnet DBSubnetGroupDescription: !Ref 'AWS::StackName' SubnetIds: - !Ref SubnetAPrivate - !Ref SubnetBPrivate - !Ref SubnetCPrivate ClusterSecurityGroup: Type: 'AWS::EC2::SecurityGroup' Properties: GroupDescription: !Ref 'AWS::StackName' SecurityGroupEgress: - IpProtocol: -1 CidrIp: "0.0.0.0/0" SecurityGroupIngress: - IpProtocol: tcp FromPort: 5432 ToPort: 5432 CidrIp: !Sub '10.0.0.0/8' Description: 'Access to AppServer Host Security Group for PG' - IpProtocol: tcp FromPort: 6379 ToPort: 6379 CidrIp: !Sub '10.0.0.0/8' Description: 'Access to AppServer Host Security Group for Redis' VpcId: !Ref VPC Tags: - Key: Name Value: !Sub '${AWS::StackName}-AuroraClusterSecurityGroup' ClusterSecurityGroupIngress: Type: 'AWS::EC2::SecurityGroupIngress' Properties: GroupId: !GetAtt 'ClusterSecurityGroup.GroupId' IpProtocol: -1 SourceSecurityGroupId: !Ref ClusterSecurityGroup Description: 'Self Reference' #============================================================================# # Control plane security group #============================================================================# ControlPlaneSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Cluster communication with worker nodes VpcId: !Ref VPC ControlPlaneIngressFromWorkerNodesHttps: Type: AWS::EC2::SecurityGroupIngress Properties: Description: Allow incoming HTTPS traffic (TCP/443) from worker nodes (for API server) GroupId: !Ref ControlPlaneSecurityGroup SourceSecurityGroupId: !Ref WorkerNodesSecurityGroup IpProtocol: tcp ToPort: 443 FromPort: 443 ControlPlaneEgressToWorkerNodesKubelet: Type: AWS::EC2::SecurityGroupEgress Properties: Description: Allow outgoing kubelet traffic (TCP/10250) to worker nodes GroupId: !Ref ControlPlaneSecurityGroup DestinationSecurityGroupId: !Ref WorkerNodesSecurityGroup IpProtocol: tcp FromPort: 10250 ToPort: 10250 ControlPlaneEgressToWorkerNodesHttps: Type: AWS::EC2::SecurityGroupEgress Properties: Description: Allow outgoing HTTPS traffic (TCP/442) to worker nodes (for pods running extension API servers) GroupId: !Ref ControlPlaneSecurityGroup DestinationSecurityGroupId: !Ref WorkerNodesSecurityGroup IpProtocol: tcp FromPort: 443 ToPort: 443 #============================================================================# # EKS configuration #============================================================================# EKSCluster: Type: AWS::EKS::Cluster Properties: Name: !Ref EKSClusterName RoleArn: !GetAtt EKSIAMRole.Arn # RoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:root" Version: !Ref KubernetesVersion ResourcesVpcConfig: SecurityGroupIds: - !Ref ControlPlaneSecurityGroup SubnetIds: - !Ref SubnetAPrivate - !Ref SubnetBPrivate - !Ref SubnetCPrivate DependsOn: [EKSIAMRole,ControlPlaneSecurityGroup] EKSIAMRole: Type: AWS::IAM::Role Properties: Path: / AssumeRolePolicyDocument: Version: '2012-10-17' Statement: Effect: Allow Principal: Service: - eks.amazonaws.com - ec2.amazonaws.com - eks-connector.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonEKSClusterPolicy - arn:aws:iam::aws:policy/AmazonEKSServicePolicy - arn:aws:iam::aws:policy/AdministratorAccess #============================================================================# # WorkerNode security group #============================================================================# WorkerNodesSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Security group for all the worker nodes VpcId: !Ref VPC Tags: - Key: Name Value: !Sub "${AWS::StackName}-WorkerNodesSecurityGroup" - Key: !Sub "kubernetes.io/cluster/${EKSCluster}" Value: "owned" WorkerNodesEgressFromWorkerNodes: Type: AWS::EC2::SecurityGroupEgress Properties: Description: Allow all outcoming traffic from other worker nodes GroupId: !Ref WorkerNodesSecurityGroup DestinationSecurityGroupId: !Ref WorkerNodesSecurityGroup IpProtocol: "-1" WorkerNodesIngressFromWorkerNodes: Type: AWS::EC2::SecurityGroupIngress Properties: Description: Allow all incoming traffic from other worker nodes GroupId: !Ref WorkerNodesSecurityGroup SourceSecurityGroupId: !Ref WorkerNodesSecurityGroup IpProtocol: "-1" WorkerNodesIngressFromControlPlaneKubelet: Type: AWS::EC2::SecurityGroupIngress Properties: Description: Allow incoming kubelet traffic (TCP/10250) from control plane GroupId: !Ref WorkerNodesSecurityGroup SourceSecurityGroupId: !Ref ControlPlaneSecurityGroup IpProtocol: tcp FromPort: 10250 ToPort: 10250 WorkerNodesIngressFromControlPlaneHttps: Type: AWS::EC2::SecurityGroupIngress Properties: Description: Allow incoming HTTPS traffic (TCP/443) from control plane (for pods running extension API servers) GroupId: !Ref WorkerNodesSecurityGroup SourceSecurityGroupId: !Ref ControlPlaneSecurityGroup IpProtocol: tcp FromPort: 443 ToPort: 443 #============================================================================# # Worker Nodes Group #============================================================================# WorkerNodesRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: Effect: Allow Principal: Service: - ec2.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy - arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly - arn:aws:iam::aws:policy/AmazonRDSFullAccess - arn:aws:iam::aws:policy/AmazonElastiCacheFullAccess - arn:aws:iam::aws:policy/ElasticLoadBalancingFullAccess - arn:aws:iam::aws:policy/AutoScalingReadOnlyAccess # IMPORTANT NOTE: We have to define NodeGroup (type: AWS::EKS::Nodegroup), without this no woker nodes will be attach to cluster WorkerNodegroup: Type: AWS::EKS::Nodegroup DependsOn: EKSCluster Properties: ClusterName: !Ref EKSClusterName NodeRole: !GetAtt WorkerNodesRole.Arn ScalingConfig: MinSize: Ref: NumWorkerNodes DesiredSize: Ref: NumWorkerNodes MaxSize: Ref: NumWorkerNodes Subnets: - !Ref SubnetAPrivate - !Ref SubnetBPrivate - !Ref SubnetCPrivate Outputs: TemplateID: Description: 'Template ID' Value: 'eks-aurora-global-database' Region: Description: 'Region' Value: '${AWS::Region}' StackName: Description: 'Stack name' Value: !Sub '${AWS::StackName}' EKSRole: Value: !Ref EKSIAMRole Export: Name: 'Fn::Sub': '${AWS::StackName}-EKSRole' EKSClusterName: Value: !Ref EKSClusterName Export: Name: 'Fn::Sub': '${AWS::StackName}-EKSClusterName' DBSubnetGroup: Value: !Ref DBSubnetGroup Export: Name: 'Fn::Sub': '${AWS::StackName}-DBSubnetGroup' ClusterSecurityGroup: Value: !Sub ClusterSecurityGroup Export: Name: 'Fn::Sub': '${AWS::StackName}-ClusterSecurityGroup' ACKIamArns: Value: !GetAtt ACKGrants.Arns