diff --git a/examples/aws-eks/README.md b/examples/aws-eks/README.md new file mode 100644 index 00000000..dfc50a8b --- /dev/null +++ b/examples/aws-eks/README.md @@ -0,0 +1,12 @@ +# Kubernetes on AWS (EKS) + +This example creates a new EKS cluster and outputs its corresponding kubeconfig + +## How to run + +```sh +dagger compute . \ + --input-string awsConfig.accessKey="MY_AWS_ACCESS_KEY" \ + --input-string awsConfig.secretKey="MY_AWS_SECRET_KEY" \ + | jq -j '.kubeconfig.kubeconfig' > kubeconfig +``` diff --git a/examples/aws-eks/cfn_template_eks_controlplane.cue b/examples/aws-eks/cfn_template_eks_controlplane.cue new file mode 100644 index 00000000..522cc67e --- /dev/null +++ b/examples/aws-eks/cfn_template_eks_controlplane.cue @@ -0,0 +1,447 @@ +package main + +#CFNTemplate: eksControlPlane: { + AWSTemplateFormatVersion: "2010-09-09" + Description: "Amazon EKS Sample VPC - Private and Public subnets" + Parameters: { + VpcBlock: { + Type: "String" + Default: "192.168.0.0/16" + Description: "The CIDR range for the VPC. This should be a valid private (RFC 1918) CIDR range." + } + PublicSubnet01Block: { + Type: "String" + Default: "192.168.0.0/18" + Description: "CidrBlock for public subnet 01 within the VPC" + } + PublicSubnet02Block: { + Type: "String" + Default: "192.168.64.0/18" + Description: "CidrBlock for public subnet 02 within the VPC" + } + PrivateSubnet01Block: { + Type: "String" + Default: "192.168.128.0/18" + Description: "CidrBlock for private subnet 01 within the VPC" + } + PrivateSubnet02Block: { + Type: "String" + Default: "192.168.192.0/18" + Description: "CidrBlock for private subnet 02 within the VPC" + } + ClusterName: { + Type: "String" + Description: "The EKS cluster name" + } + } + Metadata: "AWS::CloudFormation::Interface": ParameterGroups: [ + { + Label: default: "Worker Network Configuration" + Parameters: [ + "VpcBlock", + "PublicSubnet01Block", + "PublicSubnet02Block", + "PrivateSubnet01Block", + "PrivateSubnet02Block", + ] + }, + ] + Resources: { + VPC: { + Type: "AWS::EC2::VPC" + Properties: { + CidrBlock: Ref: "VpcBlock" + EnableDnsSupport: true + EnableDnsHostnames: true + Tags: [ + { + Key: "Name" + Value: "Fn::Sub": "${AWS::StackName}-VPC" + }, + ] + } + } + InternetGateway: Type: "AWS::EC2::InternetGateway" + VPCGatewayAttachment: { + Type: "AWS::EC2::VPCGatewayAttachment" + Properties: { + InternetGatewayId: Ref: "InternetGateway" + VpcId: Ref: "VPC" + } + } + PublicRouteTable: { + Type: "AWS::EC2::RouteTable" + Properties: { + VpcId: Ref: "VPC" + Tags: [ + { + Key: "Name" + Value: "Public Subnets" + }, + { + Key: "Network" + Value: "Public" + }, + ] + } + } + PrivateRouteTable01: { + Type: "AWS::EC2::RouteTable" + Properties: { + VpcId: Ref: "VPC" + Tags: [ + { + Key: "Name" + Value: "Private Subnet AZ1" + }, + { + Key: "Network" + Value: "Private01" + }, + ] + } + } + PrivateRouteTable02: { + Type: "AWS::EC2::RouteTable" + Properties: { + VpcId: Ref: "VPC" + Tags: [ + { + Key: "Name" + Value: "Private Subnet AZ2" + }, + { + Key: "Network" + Value: "Private02" + }, + ] + } + } + PublicRoute: { + DependsOn: "VPCGatewayAttachment" + Type: "AWS::EC2::Route" + Properties: { + RouteTableId: Ref: "PublicRouteTable" + DestinationCidrBlock: "0.0.0.0/0" + GatewayId: Ref: "InternetGateway" + } + } + PrivateRoute01: { + DependsOn: [ + "VPCGatewayAttachment", + "NatGateway01", + ] + Type: "AWS::EC2::Route" + Properties: { + RouteTableId: Ref: "PrivateRouteTable01" + DestinationCidrBlock: "0.0.0.0/0" + NatGatewayId: Ref: "NatGateway01" + } + } + PrivateRoute02: { + DependsOn: [ + "VPCGatewayAttachment", + "NatGateway02", + ] + Type: "AWS::EC2::Route" + Properties: { + RouteTableId: Ref: "PrivateRouteTable02" + DestinationCidrBlock: "0.0.0.0/0" + NatGatewayId: Ref: "NatGateway02" + } + } + NatGateway01: { + DependsOn: [ + "NatGatewayEIP1", + "PublicSubnet01", + "VPCGatewayAttachment", + ] + Type: "AWS::EC2::NatGateway" + Properties: { + AllocationId: "Fn::GetAtt": [ + "NatGatewayEIP1", + "AllocationId", + ] + SubnetId: Ref: "PublicSubnet01" + Tags: [ + { + Key: "Name" + Value: "Fn::Sub": "${AWS::StackName}-NatGatewayAZ1" + }, + ] + } + } + NatGateway02: { + DependsOn: [ + "NatGatewayEIP2", + "PublicSubnet02", + "VPCGatewayAttachment", + ] + Type: "AWS::EC2::NatGateway" + Properties: { + AllocationId: "Fn::GetAtt": [ + "NatGatewayEIP2", + "AllocationId", + ] + SubnetId: Ref: "PublicSubnet02" + Tags: [ + { + Key: "Name" + Value: "Fn::Sub": "${AWS::StackName}-NatGatewayAZ2" + }, + ] + } + } + NatGatewayEIP1: { + DependsOn: [ + "VPCGatewayAttachment", + ] + Type: "AWS::EC2::EIP" + Properties: Domain: "vpc" + } + NatGatewayEIP2: { + DependsOn: [ + "VPCGatewayAttachment", + ] + Type: "AWS::EC2::EIP" + Properties: Domain: "vpc" + } + PublicSubnet01: { + Type: "AWS::EC2::Subnet" + Metadata: Comment: "Subnet 01" + Properties: { + MapPublicIpOnLaunch: true + AvailabilityZone: "Fn::Select": [ + "0", + { + "Fn::GetAZs": Ref: "AWS::Region" + }, + ] + CidrBlock: Ref: "PublicSubnet01Block" + VpcId: Ref: "VPC" + Tags: [ + { + Key: "Name" + Value: "Fn::Sub": "${AWS::StackName}-PublicSubnet01" + }, + { + Key: "Fn::Sub": "kubernetes.io/cluster/${ClusterName}" + Value: "shared" + }, + ] + } + } + PublicSubnet02: { + Type: "AWS::EC2::Subnet" + Metadata: Comment: "Subnet 02" + Properties: { + MapPublicIpOnLaunch: true + AvailabilityZone: "Fn::Select": [ + "1", + { + "Fn::GetAZs": Ref: "AWS::Region" + }, + ] + CidrBlock: Ref: "PublicSubnet02Block" + VpcId: Ref: "VPC" + Tags: [ + { + Key: "Name" + Value: "Fn::Sub": "${AWS::StackName}-PublicSubnet02" + }, + { + Key: "Fn::Sub": "kubernetes.io/cluster/${ClusterName}" + Value: "shared" + }, + ] + } + } + PrivateSubnet01: { + Type: "AWS::EC2::Subnet" + Metadata: Comment: "Subnet 03" + Properties: { + AvailabilityZone: "Fn::Select": [ + "0", + { + "Fn::GetAZs": Ref: "AWS::Region" + }, + ] + CidrBlock: Ref: "PrivateSubnet01Block" + VpcId: Ref: "VPC" + Tags: [ + { + Key: "Name" + Value: "Fn::Sub": "${AWS::StackName}-PrivateSubnet01" + }, + { + Key: "Fn::Sub": "kubernetes.io/cluster/${ClusterName}" + Value: "shared" + }, + ] + } + } + PrivateSubnet02: { + Type: "AWS::EC2::Subnet" + Metadata: Comment: "Private Subnet 02" + Properties: { + AvailabilityZone: "Fn::Select": [ + "1", + { + "Fn::GetAZs": Ref: "AWS::Region" + }, + ] + CidrBlock: Ref: "PrivateSubnet02Block" + VpcId: Ref: "VPC" + Tags: [ + { + Key: "Name" + Value: "Fn::Sub": "${AWS::StackName}-PrivateSubnet02" + }, + { + Key: "Fn::Sub": "kubernetes.io/cluster/${ClusterName}" + Value: "shared" + }, + ] + } + } + PublicSubnet01RouteTableAssociation: { + Type: "AWS::EC2::SubnetRouteTableAssociation" + Properties: { + SubnetId: Ref: "PublicSubnet01" + RouteTableId: Ref: "PublicRouteTable" + } + } + PublicSubnet02RouteTableAssociation: { + Type: "AWS::EC2::SubnetRouteTableAssociation" + Properties: { + SubnetId: Ref: "PublicSubnet02" + RouteTableId: Ref: "PublicRouteTable" + } + } + PrivateSubnet01RouteTableAssociation: { + Type: "AWS::EC2::SubnetRouteTableAssociation" + Properties: { + SubnetId: Ref: "PrivateSubnet01" + RouteTableId: Ref: "PrivateRouteTable01" + } + } + PrivateSubnet02RouteTableAssociation: { + Type: "AWS::EC2::SubnetRouteTableAssociation" + Properties: { + SubnetId: Ref: "PrivateSubnet02" + RouteTableId: Ref: "PrivateRouteTable02" + } + } + ControlPlaneSecurityGroup: { + Type: "AWS::EC2::SecurityGroup" + Properties: { + GroupDescription: "Cluster communication with worker nodes" + VpcId: Ref: "VPC" + } + } + EKSIAMRole: { + Type: "AWS::IAM::Role" + Properties: { + AssumeRolePolicyDocument: Statement: [ + { + Effect: "Allow" + Principal: Service: [ + "eks.amazonaws.com", + ] + Action: [ + "sts:AssumeRole", + ] + + }, + ] + ManagedPolicyArns: [ + "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy", + "arn:aws:iam::aws:policy/AmazonEKSServicePolicy", + ] + } + } + EKSCluster: { + Type: "AWS::EKS::Cluster" + Properties: { + Name: Ref: "ClusterName" + Version: "1.19" + RoleArn: "Fn::GetAtt": ["EKSIAMRole", "Arn"] + ResourcesVpcConfig: { + SecurityGroupIds: [{Ref: "ControlPlaneSecurityGroup"}] + SubnetIds: [ + {Ref: "PublicSubnet01"}, + {Ref: "PublicSubnet02"}, + {Ref: "PrivateSubnet01"}, + {Ref: "PrivateSubnet02"}, + ] + } + } + DependsOn: ["EKSIAMRole", "PublicSubnet01", "PublicSubnet02", "PrivateSubnet01", "PrivateSubnet02", "ControlPlaneSecurityGroup"] + } + } + Outputs: { + SubnetIds: { + Description: "Subnets IDs in the VPC" + Value: "Fn::Join": [ + ",", + [ + { + Ref: "PublicSubnet01" + }, + { + Ref: "PublicSubnet02" + }, + { + Ref: "PrivateSubnet01" + }, + { + Ref: "PrivateSubnet02" + }, + ], + ] + } + PublicSubnets: { + Description: "List of the public subnets" + Value: "Fn::Join": [ + ",", + [ + { + Ref: "PublicSubnet01" + }, + { + Ref: "PublicSubnet02" + }, + ], + ] + } + PrivateSubnets: { + Description: "List of the private subnets" + Value: "Fn::Join": [ + ",", + [ + { + Ref: "PrivateSubnet01" + }, + { + Ref: "PrivateSubnet02" + }, + ], + ] + } + DefaultSecurityGroup: { + Description: "Security group for the cluster control plane communication with worker nodes" + Value: "Fn::Join": [ + ",", + [ + { + Ref: "ControlPlaneSecurityGroup" + }, + ], + ] + } + VPC: { + Description: "The VPC Id" + Value: Ref: "VPC" + } + } +} diff --git a/examples/aws-eks/cfn_template_eks_nodegroup.cue b/examples/aws-eks/cfn_template_eks_nodegroup.cue new file mode 100644 index 00000000..ab38c50f --- /dev/null +++ b/examples/aws-eks/cfn_template_eks_nodegroup.cue @@ -0,0 +1,89 @@ +package main + +#CFNTemplate: eksNodeGroup: { + AWSTemplateFormatVersion: "2010-09-09" + Description: "Amazon EKS - Node Group" + Parameters: { + ClusterName: { + Type: "String" + Description: "The cluster name provided when the cluster was created. If it is incorrect, nodes will not be able to join the cluster." + } + NodeAutoScalingGroupDesiredCapacity: { + Type: "Number" + Default: 3 + Description: "Desired capacity of Node Group ASG." + } + NodeAutoScalingGroupMaxSize: { + Type: "Number" + Default: 4 + Description: "Maximum size of Node Group ASG. Set to at least 1 greater than NodeAutoScalingGroupDesiredCapacity." + } + NodeAutoScalingGroupMinSize: { + Type: "Number" + Default: 1 + Description: "Minimum size of Node Group ASG." + } + NodeInstanceType: { + Type: "String" + Default: "t3.medium" + ConstraintDescription: "Must be a valid EC2 instance type" + Description: "EC2 instance type for the node instances" + } + Subnets: { + Type: "List" + Description: "The subnets where workers can be created." + } + } + Resources: { + NodeInstanceRole: { + 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", + ] + Path: "/" + } + } + Nodegroup: { + Type: "AWS::EKS::Nodegroup" + Properties: { + ClusterName: Ref: "ClusterName" + NodeRole: "Fn::GetAtt": [ + "NodeInstanceRole", + "Arn", + ] + ScalingConfig: { + MaxSize: Ref: "NodeAutoScalingGroupMaxSize" + MinSize: Ref: "NodeAutoScalingGroupMinSize" + DesiredSize: Ref: "NodeAutoScalingGroupDesiredCapacity" + } + InstanceTypes: [{Ref: "NodeInstanceType"}] + AmiType: "AL2_x86_64" + Subnets: Ref: "Subnets" + } + } + } + Outputs: NodeInstanceRole: { + Description: "The node instance role" + Value: "Fn::GetAtt": [ + "NodeInstanceRole", + "Arn", + ] + } +} diff --git a/examples/aws-eks/infrastructure.cue b/examples/aws-eks/infrastructure.cue new file mode 100644 index 00000000..2e43c7d5 --- /dev/null +++ b/examples/aws-eks/infrastructure.cue @@ -0,0 +1,41 @@ +package main + +import ( + "encoding/json" + + "dagger.io/aws" + "dagger.io/aws/cloudformation" +) + +#Infrastructure: { + awsConfig: aws.#Config + namePrefix: *"" | string + workerNodeCapacity: *3 | >=1 + workerNodeInstanceType: *"t3.medium" | string + + clusterName: "\(namePrefix)eks-cluster" + + eksControlPlane: cloudformation.#Stack & { + config: awsConfig + source: json.Marshal(#CFNTemplate.eksControlPlane) + stackName: "\(namePrefix)eks-controlplane" + neverUpdate: true + timeout: 30 + parameters: ClusterName: clusterName + } + + eksNodeGroup: cloudformation.#Stack & { + config: awsConfig + source: json.Marshal(#CFNTemplate.eksNodeGroup) + stackName: "\(namePrefix)eks-nodegroup" + neverUpdate: true + timeout: 30 + parameters: { + ClusterName: clusterName + NodeAutoScalingGroupDesiredCapacity: 1 + NodeAutoScalingGroupMaxSize: NodeAutoScalingGroupDesiredCapacity + 1 + NodeInstanceType: workerNodeInstanceType + Subnets: eksControlPlane.outputs.SubnetIds + } + } +} diff --git a/examples/aws-eks/main.cue b/examples/aws-eks/main.cue new file mode 100644 index 00000000..e605701f --- /dev/null +++ b/examples/aws-eks/main.cue @@ -0,0 +1,31 @@ +package main + +import ( + "dagger.io/aws" + "dagger.io/aws/eks" +) + +// Fill using: +// --input-string awsConfig.accessKey=XXX +// --input-string awsConfig.secretKey=XXX +awsConfig: aws.#Config & { + region: *"us-east-2" | string +} + +// Auto-provision an EKS cluster: +// - VPC, Nat Gateways, Subnets, Security Group +// - EKS Cluster +// - Instance Node Group: auto-scaling-group, ec2 instances, etc... +// base config can be changed (number of EC2 instances, types, etc...) +infra: #Infrastructure & { + "awsConfig": awsConfig + namePrefix: "dagger-example-" + // Cluster size is 1 for the example purpose + workerNodeCapacity: 1 + workerNodeInstanceType: "t3.small" +} + +kubeconfig: eks.#KubeConfig & { + config: awsConfig + clusterName: infra.clusterName +} diff --git a/stdlib/aws/aws.cue b/stdlib/aws/aws.cue index a9d8bc20..4f6bbf6c 100644 --- a/stdlib/aws/aws.cue +++ b/stdlib/aws/aws.cue @@ -1,7 +1,12 @@ package aws -import "dagger.io/dagger" +import ( + "dagger.io/dagger" + "dagger.io/llb" + "dagger.io/alpine" +) +// Base AWS Config #Config: { // AWS region region: string @@ -10,3 +15,17 @@ import "dagger.io/dagger" // AWS secret key secretKey: dagger.#Secret } + +// Re-usable aws-cli component +#CLI: { + #compute: [ + llb.#Load & { + from: alpine.#Image & { + package: bash: "=5.1.0-r0" + package: jq: "=1.6-r1" + package: curl: "=7.74.0-r1" + package: "aws-cli": "=1.18.177-r0" + } + }, + ] +} diff --git a/stdlib/aws/cloudformation/cloudformation.cue b/stdlib/aws/cloudformation/cloudformation.cue index b27a38c7..43d3e52f 100644 --- a/stdlib/aws/cloudformation/cloudformation.cue +++ b/stdlib/aws/cloudformation/cloudformation.cue @@ -3,9 +3,8 @@ package cloudformation import ( "encoding/json" - "dagger.io/alpine" - "dagger.io/aws" "dagger.io/llb" + "dagger.io/aws" ) // AWS CloudFormation Stack @@ -45,57 +44,51 @@ import ( } } - outputs: { - [string]: string + outputs: [string]: string - #compute: [ - llb.#Load & { - from: alpine.#Image & { - package: bash: "=5.1.0-r0" - package: jq: "=1.6-r1" - package: "aws-cli": "=1.18.177-r0" + outputs: #compute: [ + llb.#Load & { + from: aws.#CLI + }, + llb.#Mkdir & { + path: "/src" + }, + for dest, content in #files { + llb.#WriteFile & { + "dest": dest + "content": content + } + }, + llb.#Exec & { + args: [ + "/bin/bash", + "--noprofile", + "--norc", + "-eo", + "pipefail", + "/entrypoint.sh", + ] + env: { + AWS_CONFIG_FILE: "/cache/aws/config" + AWS_ACCESS_KEY_ID: config.accessKey + AWS_SECRET_ACCESS_KEY: config.secretKey + AWS_DEFAULT_REGION: config.region + AWS_REGION: config.region + AWS_DEFAULT_OUTPUT: "json" + AWS_PAGER: "" + if neverUpdate { + NEVER_UPDATE: "true" } - }, - llb.#Mkdir & { - path: "/src" - }, - for dest, content in #files { - llb.#WriteFile & { - "dest": dest - "content": content - } - }, - llb.#Exec & { - args: [ - "/bin/bash", - "--noprofile", - "--norc", - "-eo", - "pipefail", - "/entrypoint.sh", - ] - env: { - AWS_CONFIG_FILE: "/cache/aws/config" - AWS_ACCESS_KEY_ID: config.accessKey - AWS_SECRET_ACCESS_KEY: config.secretKey - AWS_DEFAULT_REGION: config.region - AWS_REGION: config.region - AWS_DEFAULT_OUTPUT: "json" - AWS_PAGER: "" - if neverUpdate { - NEVER_UPDATE: "true" - } - STACK_NAME: stackName - TIMEOUT: "\(timeout)" - ON_FAILURE: onFailure - } - dir: "/src" - mount: "/cache/aws": "cache" - }, - llb.#Export & { - source: "/outputs.json" - format: "json" - }, - ] - } + STACK_NAME: stackName + TIMEOUT: "\(timeout)" + ON_FAILURE: onFailure + } + dir: "/src" + mount: "/cache/aws": "cache" + }, + llb.#Export & { + source: "/outputs.json" + format: "json" + }, + ] } diff --git a/stdlib/aws/eks/code.cue b/stdlib/aws/eks/code.cue new file mode 100644 index 00000000..9cb1a630 --- /dev/null +++ b/stdlib/aws/eks/code.cue @@ -0,0 +1,26 @@ +package eks + +#Code: #""" + [ -e /cache/bin/kubectl ] || { + curl -sfL https://dl.k8s.io/v1.19.9/bin/linux/amd64/kubectl -o /cache/bin/kubectl && chmod +x /cache/bin/kubectl + } + + export KUBECONFIG=/kubeconfig + export PATH="$PATH:/cache/bin" + + # Generate a kube configuration + aws eks update-kubeconfig --name "$EKS_CLUSTER" + + # Figure out the kubernetes username + CONTEXT="$(kubectl config current-context)" + USER="$(kubectl config view -o json | \ + jq -r ".contexts[] | select(.name==\"$CONTEXT\") | .context.user")" + + # Grab a kubernetes access token + ACCESS_TOKEN="$(aws eks get-token --cluster-name "$EKS_CLUSTER" | \ + jq -r .status.token)" + + # Remove the user config and replace it with the token + kubectl config unset "users.${USER}" + kubectl config set-credentials "$USER" --token "$ACCESS_TOKEN" + """# diff --git a/stdlib/aws/eks/eks.cue b/stdlib/aws/eks/eks.cue new file mode 100644 index 00000000..58eae8c5 --- /dev/null +++ b/stdlib/aws/eks/eks.cue @@ -0,0 +1,59 @@ +package eks + +import ( + "dagger.io/llb" + "dagger.io/aws" +) + +// KubeConfig config outputs a valid kube-auth-config for kubectl client +#KubeConfig: { + // AWS Config + config: aws.#Config + + // EKS cluster name + clusterName: string + + // kubeconfig is the generated kube configuration file + kubeconfig: { + dagger.#Secret + + #compute: [ + llb.#Load & { + from: aws.#CLI + }, + llb.#WriteFile & { + dest: "/entrypoint.sh" + content: #Code + }, + llb.#Exec & { + always: true + args: [ + "/bin/bash", + "--noprofile", + "--norc", + "-eo", + "pipefail", + "/entrypoint.sh", + ] + env: { + AWS_CONFIG_FILE: "/cache/aws/config" + AWS_ACCESS_KEY_ID: config.accessKey + AWS_SECRET_ACCESS_KEY: config.secretKey + AWS_DEFAULT_REGION: config.region + AWS_REGION: config.region + AWS_DEFAULT_OUTPUT: "json" + AWS_PAGER: "" + EKS_CLUSTER: clusterName + } + mount: { + "/cache/aws": "cache" + "/cache/bin": "cache" + } + }, + llb.#Export & { + source: "/kubeconfig" + format: "string" + }, + ] + } +}