This example will write a tile to deploy a EKS cluster from scratch
Make sure your aws cli is well configured, you will see something like below
$ aws configure list
Name Value Type Location
---- ----- ---- --------
profile <not set> None None
access_key ****************XAWA shared-credentials-file
secret_key ****************qEK5 shared-credentials-file
region us-east-2 config-file ~/.aws/config
Create a workspace for your local tile
$ mkdir -p $HOME/ws/local-tiles-repo
Lanuch dice daemon in DEV mode if not existing:
$ docker run -it -d -v $HOME/ws/local-tiles-repo:/workspace/tiles-repo \
-v ~/.aws:/root/.aws \
-e M_MODE=dev \
-p 9090:9090 \
docker.pkg.github.com/awslabs/aws-solutions-assembler/dice:latest
Create a tile based on CDK
$ cd $HOME/ws/local-tiles-repo # enter local tiles repository
$ mctl init tile -d myeks # init a CDK tile template under myeks folder
2020-07-22T11:33:28+08:00 [!] no such flag -filename
[ℹ] Loading templates from Tile-Repo started ...
########
[ℹ] Loading templates finished.
[ℹ] Generated file - myeks//0.1.0
[ℹ] Generated file - myeks/0.1.0/README.md.tp
[ℹ] Generated file - myeks/0.1.0/jest.config.js
[ℹ] Generated file - myeks/0.1.0/lib
[ℹ] Generated file - myeks/0.1.0/lib/index.ts.tp
[ℹ] Generated file - myeks/0.1.0/package-lock.json.tp
[ℹ] Generated file - myeks/0.1.0/package.json.tp
[ℹ] Generated file - myeks/0.1.0/test
[ℹ] Generated file - myeks/0.1.0/test/simple.test.ts.tp
[ℹ] Generated file - myeks/0.1.0/tile-spec.yaml.tp
[ℹ] Generated file - myeks/0.1.0/tsconfig.json
$ cd myeks
$ tree . # check your folder
.
└── myeks
└── 0.1.0
├── README.md
├── jest.config.js
├── lib
│ └── index.ts # main function
├── package-lock.json
├── package.json
├── test
│ └── simple.test.ts # unit test
├── tile-spec.yaml # the tile definition
└── tsconfig.json
4 directories, 8 files
$ rm myeks/0.1.0/package-lock.json # remove the generated package-lock.json because it's out of date
$ cd myeks/0.1.0/
$ npm install # install dependencies
Let’s first create a policy component to let myeks have enough permissions. Create lib/policy4eks.ts
// lib/policy4eks.ts
import cdk = require('@aws-cdk/core');
import ec2 = require('@aws-cdk/aws-ec2');
import iam = require('@aws-cdk/aws-iam');
export interface PolicyProps { }
export class NodePolicies extends cdk.Construct {
public eksInlinePolicy: { [name: string]: iam.PolicyDocument }
constructor(scope: cdk.Construct, id: string, props: PolicyProps) {
super(scope, id);
this.eksInlinePolicy = {
"Autoscaler4eks": new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: [
"autoscaling:DescribeAutoScalingGroups",
"autoscaling:DescribeAutoScalingInstances",
"autoscaling:DescribeLaunchConfigurations",
"autoscaling:DescribeTags",
"autoscaling:SetDesiredCapacity",
"autoscaling:TerminateInstanceInAutoScalingGroup",
"ec2:DescribeLaunchTemplateVersions"
],
resources: ["*"],
effect: iam.Effect.ALLOW
}),
]
}),
"ALBIngress": new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: [
"acm:DescribeCertificate",
"acm:ListCertificates",
"acm:GetCertificate",
"ec2:AuthorizeSecurityGroupIngress",
"ec2:CreateSecurityGroup",
"ec2:CreateTags",
"ec2:DeleteTags",
"ec2:DeleteSecurityGroup",
"ec2:DescribeAccountAttributes",
"ec2:DescribeAddresses",
"ec2:DescribeInstances",
"ec2:DescribeInstanceStatus",
"ec2:DescribeInternetGateways",
"ec2:DescribeNetworkInterfaces",
"ec2:DescribeSecurityGroups",
"ec2:DescribeSubnets",
"ec2:DescribeTags",
"ec2:DescribeVpcs",
"ec2:ModifyInstanceAttribute",
"ec2:ModifyNetworkInterfaceAttribute",
"ec2:RevokeSecurityGroupIngress",
"elasticloadbalancing:AddListenerCertificates",
"elasticloadbalancing:AddTags",
"elasticloadbalancing:CreateListener",
"elasticloadbalancing:CreateLoadBalancer",
"elasticloadbalancing:CreateRule",
"elasticloadbalancing:CreateTargetGroup",
"elasticloadbalancing:DeleteListener",
"elasticloadbalancing:DeleteLoadBalancer",
"elasticloadbalancing:DeleteRule",
"elasticloadbalancing:DeleteTargetGroup",
"elasticloadbalancing:DeregisterTargets",
"elasticloadbalancing:DescribeListenerCertificates",
"elasticloadbalancing:DescribeListeners",
"elasticloadbalancing:DescribeLoadBalancers",
"elasticloadbalancing:DescribeLoadBalancerAttributes",
"elasticloadbalancing:DescribeRules",
"elasticloadbalancing:DescribeSSLPolicies",
"elasticloadbalancing:DescribeTags",
"elasticloadbalancing:DescribeTargetGroups",
"elasticloadbalancing:DescribeTargetGroupAttributes",
"elasticloadbalancing:DescribeTargetHealth",
"elasticloadbalancing:ModifyListener",
"elasticloadbalancing:ModifyLoadBalancerAttributes",
"elasticloadbalancing:ModifyRule",
"elasticloadbalancing:ModifyTargetGroup",
"elasticloadbalancing:ModifyTargetGroupAttributes",
"elasticloadbalancing:RegisterTargets",
"elasticloadbalancing:RemoveListenerCertificates",
"elasticloadbalancing:RemoveTags",
"elasticloadbalancing:SetIpAddressType",
"elasticloadbalancing:SetSecurityGroups",
"elasticloadbalancing:SetSubnets",
"elasticloadbalancing:SetWebACL",
"iam:CreateServiceLinkedRole",
"iam:GetServerCertificate",
"iam:ListServerCertificates",
"waf-regional:GetWebACLForResource",
"waf-regional:GetWebACL",
"waf-regional:AssociateWebACL",
"waf-regional:DisassociateWebACL",
"tag:GetResources",
"tag:TagResources",
"waf:GetWebACL",
"wafv2:GetWebACL",
"wafv2:GetWebACLForResource",
"wafv2:AssociateWebACL",
"wafv2:DisassociateWebACL",
"shield:DescribeProtection",
"shield:GetSubscriptionState",
"shield:DeleteProtection",
"shield:CreateProtection",
"shield:DescribeSubscription",
"shield:ListProtections"
],
resources: ["*"],
effect: iam.Effect.ALLOW
}),
]
}),
"AppMesh": new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: [
"appmesh:*",
"servicediscovery:CreateService",
"servicediscovery:GetService",
"servicediscovery:RegisterInstance",
"servicediscovery:DeregisterInstance",
"servicediscovery:ListInstances",
"servicediscovery:ListNamespaces",
"servicediscovery:ListServices",
"route53:GetHealthCheck",
"route53:CreateHealthCheck",
"route53:UpdateHealthCheck",
"route53:ChangeResourceRecordSets",
"route53:DeleteHealthCheck"
],
resources: ["*"],
effect: iam.Effect.ALLOW
}),
]
}),
"CertManagerChangeSet": new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: [
"route53:ChangeResourceRecordSets"
],
resources: ["arn:aws:route53:::hostedzone/*"],
effect: iam.Effect.ALLOW
}),
]
}),
"CertManagerGetChange": new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: [
"route53:GetChange"
],
resources: ["arn:aws:route53:::change/*"],
effect: iam.Effect.ALLOW
}),
]
}),
"CertManagerHostedZone": new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: [
"route53:ListHostedZones",
"route53:ListResourceRecordSets",
"route53:ListHostedZonesByName",
"route53:ListTagsForResource"
],
resources: ["*"],
effect: iam.Effect.ALLOW
}),
]
}),
"EBS": new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: [
"ec2:AttachVolume",
"ec2:CreateSnapshot",
"ec2:CreateTags",
"ec2:CreateVolume",
"ec2:DeleteSnapshot",
"ec2:DeleteTags",
"ec2:DeleteVolume",
"ec2:DescribeAvailabilityZones",
"ec2:DescribeInstances",
"ec2:DescribeSnapshots",
"ec2:DescribeTags",
"ec2:DescribeVolumes",
"ec2:DescribeVolumesModifications",
"ec2:DetachVolume",
"ec2:ModifyVolume"
],
resources: ["*"],
effect: iam.Effect.ALLOW
}),
]
}),
"EFS": new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: [
"elasticfilesystem:*"
],
resources: ["*"],
effect: iam.Effect.ALLOW
}),
]
}),
"EFSEC2": new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: [
"ec2:DescribeSubnets",
"ec2:CreateNetworkInterface",
"ec2:DescribeNetworkInterfaces",
"ec2:DeleteNetworkInterface",
"ec2:ModifyNetworkInterfaceAttribute",
"ec2:DescribeNetworkInterfaceAttribute"
],
resources: ["*"],
effect: iam.Effect.ALLOW
}),
]
}),
"FSX": new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: [
"fsx:*"
],
resources: ["*"],
effect: iam.Effect.ALLOW
}),
]
}),
"ServiceLinkRole": new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: [
"iam:CreateServiceLinkedRole",
"iam:AttachRolePolicy",
"iam:PutRolePolicy"
],
resources: ["*"],
effect: iam.Effect.ALLOW
}),
]
}),
"XRay": new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: [
"xray:PutTraceSegments",
"xray:PutTelemetryRecords",
"xray:GetSamplingRules",
"xray:GetSamplingTargets",
"xray:GetSamplingStatisticSummaries"
],
resources: ["*"],
effect: iam.Effect.ALLOW
}),
]
})
}
}
}
Then, write a myeks CDK component. The key of CDK component is to define your input properties and output values. Belows are the inputs and outputs.
// inputs
export interface MyEKSProps {
vpc: ec2.Vpc,
vpcSubnets?: ec2.ISubnet[],
clusterName: string,
capacity?: number,
capacityInstance?: string,
clusterVersion?: string, // EKS cluster version, like: 1.16, 1.17
}
// outputs
export class MyEKS extends cdk.Construct {
public readonly clusterName: string; // The cluster name
public readonly clusterEndpoint: string; // The endpoint URL for the Cluster
public readonly masterRoleARN: string; // An IAM role that will be `system:masters` privileges on your cluster
public readonly clusterArn: string; // The AWS generated ARN for the Cluster resource
public readonly capacity: number; // Capacity
public readonly capacityInstance: string; // Instance type, like: t2.medium, m5.large
}
Below is the full source code of myeks CDK component
// lib/index.ts
import * as cdk from '@aws-cdk/core';
import eks = require('@aws-cdk/aws-eks');
import ec2 = require('@aws-cdk/aws-ec2');
import iam = require('@aws-cdk/aws-iam');
import { NodePolicies } from './policy4eks'
import { ManagedPolicy, ServicePrincipal, PolicyDocument, PolicyStatement } from '@aws-cdk/aws-iam';
/** Input parameters */
export interface MyEKSProps {
vpc: ec2.Vpc,
vpcSubnets?: ec2.ISubnet[],
clusterName: string,
capacity?: number,
capacityInstance?: string,
clusterVersion?: string,
}
export class MyEKS extends cdk.Construct {
/** Directly exposed to other stack */
public readonly clusterName: string;
public readonly clusterEndpoint: string;
public readonly masterRoleARN: string;
public readonly clusterArn: string;
public readonly capacity: number;
public readonly capacityInstance: string;
constructor(scope: cdk.Construct, id: string, props: MyEKSProps) {
super(scope, id);
const eksRole = new iam.Role(this, 'EksClusterMasterRole', {
assumedBy: new iam.AccountRootPrincipal(),
managedPolicies: [
ManagedPolicy.fromAwsManagedPolicyName("AmazonEKSServicePolicy"),
ManagedPolicy.fromAwsManagedPolicyName("AmazonEKSClusterPolicy"),
]
});
// Instance type for node group
let capacityInstance: ec2.InstanceType;
if (props.capacityInstance == undefined) {
capacityInstance = ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.LARGE);
} else {
capacityInstance = new ec2.InstanceType(props.capacityInstance);
}
// Prepared subnet for node group
let vpcSubnets: ec2.SubnetSelection[];
if (props.vpcSubnets == undefined) {
vpcSubnets = [{ subnets: props.vpc.publicSubnets }, { subnets: props.vpc.privateSubnets }];
} else {
vpcSubnets = [{ subnets: props.vpcSubnets }];
}
// Innitial EKS cluster
const cluster = new eks.Cluster(this, "BasicEKSCluster", {
vpc: props.vpc,
vpcSubnets: vpcSubnets,
clusterName: props.clusterName,
defaultCapacity: 0,
version: eks.KubernetesVersion.of(props.clusterVersion || '1.16'),
// Master role as initial permission to run Kubectl
mastersRole: eksRole,
});
/** managed nodegroup */
const nodegroupRole = new iam.Role(scope, 'NodegroupRole', {
assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
managedPolicies: [
ManagedPolicy.fromAwsManagedPolicyName("AmazonEKSWorkerNodePolicy"),
ManagedPolicy.fromAwsManagedPolicyName("AmazonEKS_CNI_Policy"),
ManagedPolicy.fromAwsManagedPolicyName("AmazonEC2ContainerRegistryReadOnly"),
],
inlinePolicies: new NodePolicies(scope, "inlinePolicies", {}).eksInlinePolicy
});
const managed = cluster.addNodegroup("managed-node", {
instanceType: capacityInstance,
minSize: Math.round(props.capacity! / 2),
maxSize: props.capacity,
nodeRole: nodegroupRole
});
/** Added CF Output */
new cdk.CfnOutput(this, "clusterName", { value: cluster.clusterName })
new cdk.CfnOutput(this, "masterRoleARN", { value: eksRole.roleArn })
new cdk.CfnOutput(this, "clusterEndpoint", { value: cluster.clusterEndpoint })
new cdk.CfnOutput(this, "clusterArn", { value: cluster.clusterArn })
new cdk.CfnOutput(this, "capacity", { value: String(props.capacity) || "0" })
new cdk.CfnOutput(this, "capacityInstance", { value: capacityInstance.toString() })
this.clusterName = cluster.clusterName;
this.masterRoleARN = eksRole.roleArn;
this.clusterEndpoint = cluster.clusterEndpoint;
this.clusterArn = cluster.clusterArn;
this.capacity = props.capacity || 0
this.capacityInstance = capacityInstance.toString()
}
}
Once we finished CDK component development, we can write a simple test to verify our cluster configuration
// test/myeks.test.ts
import { expect as expectCDK, haveResource, SynthUtils } from '@aws-cdk/assert';
import * as cdk from '@aws-cdk/core';
import MyEKS = require('../lib/index');
import ec2 = require('@aws-cdk/aws-ec2');
test('EKS Cluster Created', () => {
const app = new cdk.App();
const stack = new cdk.Stack(app, "TestStack");
const vpc = new ec2.Vpc(stack, "Vpc");
// WHEN
new MyEKS.MyEKS(stack, 'MyTestConstruct', { vpc: vpc, clusterName: "testCluster" });
// THEN
expectCDK(stack).to(haveResource("AWS::EKS::Nodegroup"));
});
$ npm run test
> myeks@0.1.0 test ***
> jest
PASS test/myeks.test.ts
✓ EKS Cluster Created (1441ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 4.341s, estimated 6s
Ran all test suites.
To be able to deploy your tile, we need one last thing tile-spec.yaml
# API version
apiVersion: mahjong.io/v1alpha1
# Kind of entity
kind: Tile
# Metadata
metadata:
# Name of entity
name: MyEKS
# Category of entity
category: ContainerProvider
# Vendor
vendorService: EKS
# Version of entity
version: 0.0.5
# Specification
spec:
# Dependencies represent dependency with other Tile
dependencies:
# As a reference name
- name: network
# Tile name
tileReference: Network0
# Tile version
tileVersion: 0.0.1
# Inputs are input parameters when lauching
inputs:
# String
- name: cidr
inputType: String
require: true
override:
name: network
field: cidr
# CDKObject
- name: vpc
inputType: CDKObject
description: 'Refer to VPC object on Tile - Network0'
dependencies:
# Reference name in Dependencies
- name: network
# Filed name in refered Tile
field: baseVpc
# Input is mandatory or not, true is mandatory and false is optional
require: false
# CDKObject[]
- name: vpcSubnets
inputType: CDKObject[]
description: ''
dependencies:
- name: network
field: publicSubnet1
- name: network
field: publicSubnet2
- name: network
field: privateSubnet1
- name: network
field: privateSubnet2
require: false
# String
- name: clusterName
inputType: String
description: ''
defaultValue: default-eks-cluster
require: true
# Number/ default: 2
- name: capacity
inputType: Number
description: ''
defaultValue: 2
require: false
# String/ default: 'c5.large'
- name: capacityInstance
inputType: String
description: ''
defaultValue: 'c5.large'
require: false
# String/ default: '1.15'
- name: clusterVersion
inputType: String
description: ''
defaultValue: '1.16'
require: false
# Ouptputs represnt output value after launched, for 'ContainerApplication' might need leverage specific command to retrive output.
outputs:
# String
- name: clusterName
outputType: String
description: AWS::EKS::Cluster.Name
# String
- name: clusterArn
outputType: String
description: AWS::EKS::Cluster.ARN
# String
- name: clusterEndpoint
outputType: String
description: AWS::EKS::Cluster.Endpoint
# String
- name: masterRoleARN
outputType: String
description: AWS::IAM::Role.ARN
# String
- name: capacityInstance
outputType: String
description: AWS::EKS::Cluster.capacityInstance
# String/ default: '1.15'
- name: capacity
outputType: String
description: AWS::EKS::Cluster.capacity
# Notes are description list for addtional information.
notes:
- "Tag public subnets with 'kubernetes.io/role/elb=1'"
- "Tag priavte subnets with 'kubernetes.io/role/internal-elb=1'"
You may want to ask why our tile need to depend on network tile. The reason is that,
in our input MyEKSProps, we need to provide VPC’s configuration which we can leverage the
existing network tile
Final we can write a deployment YAML try-myeks.yaml to try myeks tile
# try-myeks.yaml
apiVersion: mahjong.io/v1alpha1
kind: Deployment
metadata:
name: MyEKS
version: 0.1.0
spec:
template:
tiles:
MyEKS:
tileReference: MyEKS
tileVersion: 0.1.0
inputs:
- name: clusterName
inputValue: myeks-cluster
- name: capacity
inputValue: 3
- name: capacityInstance
inputValue: m5.large
- name: version
inputValue: 1.17
summary:
description:
outputs:
- name: EKS Cluster Name
value: $(MyEKS.outputs.clusterName)
- name: Master role arn for EKS Cluster
value: $(MyEKS.outputs.masterRoleARN)
- name: The API endpoint EKS Cluster
value: $(MyEKS.outputs.clusterEndpoint)
- name: Instance type of worker node
value: $(MyEKS.outputs.capacityInstance)
- name: Default capacity of worker node
value: $(MyEKS.outputs.capacity)
notes: []
$ mctl deploy -f ./try-myeks.yaml