AWS EKS User Guide

Introduction

Amazon Elastic Kubernetes Service (AWS EKS) is a managed Kubernetes service on the AWS cloud platform. Instead of having to install, operate, and maintain a Kubernetes cluster from scratch yourself, AWS takes over the management of the system's "brain" (Control Plane), helping you focus entirely on deploying and running applications.

Advantages

  • Reduced administrative burden: AWS automatically performs difficult tasks such as version updates, security patching, and ensuring high availability for the Control Plane across multiple Availability Zones.
  • Optimal security: EKS integrates tightly with AWS security services such as IAM (for granular permissions for Pods), VPC (for network isolation), and AWS KMS (for data encryption).
  • Flexible scalability: You can easily increase or decrease the number of resources (nodes) based on the actual traffic of the application, helping to optimize costs.
  • Rich ecosystem: Easily connect with other AWS services such as CloudWatch (monitoring), ELB (load balancing), and Fargate (running containers without managing servers).
  • Full compatibility: Since EKS runs a standard Kubernetes version, you can easily migrate applications from On-premise environments or other clouds to AWS without changing the source code.


Prerequisites

In this article, I will use NestJS to create a simple API to get a list of files in an S3 Bucket. After that, I will build a docker image and push this image to AWS ECR and use this docker image in k8s; if you have any questions about any content, please review the previous articles I mentioned.

The content of the s3.service.ts file is as follows:

import {
  GetObjectCommand,
  ListObjectsV2Command,
  PutObjectCommand,
  S3Client,
} from '@aws-sdk/client-s3'
import {getSignedUrl} from '@aws-sdk/s3-request-presigner'
import {Injectable} from '@nestjs/common'
import {ConfigService} from '@nestjs/config'
import * as mime from 'mime-types'

@Injectable()
export class S3Service {
  private s3Client: S3Client
  private bucket = ''

  constructor(private configService: ConfigService) {
    this.bucket = this.configService.get<string>('BUCKET') || ''
    this.s3Client = new S3Client({})
  }

  async createPresignedUrl(fileName: string) {
    const ContentType = mime.lookup(fileName) || 'application/octet-stream'
    const command = new PutObjectCommand({
      Bucket: this.bucket,
      Key: fileName,
      ContentType,
    })
    const url = await getSignedUrl(this.s3Client, command, {expiresIn: 3600})
    return {url}
  }

  async listFiles() {
    const command = new ListObjectsV2Command({
      Bucket: this.bucket,
    })

    const {Contents} = await this.s3Client.send(command)

    if (!Contents) return []

    const fileList = await Promise.all(
      Contents.map(async file => {
        const getCommand = new GetObjectCommand({
          Bucket: this.bucket,
          Key: file.Key,
        })

        return {
          fileName: file.Key,
          size: file.Size,
          lastModified: file.LastModified,
          viewUrl: await getSignedUrl(this.s3Client, getCommand, {
            expiresIn: 3600,
          }),
        }
      })
    )
    return fileList
  }
}

  • The content is simply to allow uploading via presigned urls and listing the urls to access uploaded files
  • Pay attention to the BUCKET field; you will need to create this value in the .env file and in the .yaml file that will be created in the content below
  • As for the content regarding using CDK to create the S3 Bucket, controller, and how to use presigned urls, please review the previous article I shared

Detail

Use AWS CDK to create the file lib/eks-public-stack.ts as follows:

import { KubectlV34Layer } from "@aws-cdk/lambda-layer-kubectl-v34"
import * as cdk from "aws-cdk-lib"
import * as ec2 from "aws-cdk-lib/aws-ec2"
import * as eks from "aws-cdk-lib/aws-eks"
import * as iam from "aws-cdk-lib/aws-iam"
import { Construct } from "constructs"

export class EksPublicStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const vpc = new ec2.Vpc(this, "EksVpc", {
      maxAzs: 2,
      natGateways: 0,
      subnetConfiguration: [
        {
          name: "PublicSubnet",
          subnetType: ec2.SubnetType.PUBLIC,
        },
      ],
    })

    const cluster = new eks.Cluster(this, "MyEksCluster", {
      vpc,
      vpcSubnets: [{ subnetType: ec2.SubnetType.PUBLIC }],
      defaultCapacity: 0,
      version: eks.KubernetesVersion.V1_34,
      endpointAccess: eks.EndpointAccess.PUBLIC_AND_PRIVATE,
      kubectlLayer: new KubectlV34Layer(this, "KubectlLayer"),
      authenticationMode: eks.AuthenticationMode.API_AND_CONFIG_MAP,
      bootstrapClusterCreatorAdminPermissions: true,
    })

    const currentUserName = "<username>"
    cluster.grantAccess(
      "AdminAccess",
      `arn:aws:iam::${this.account}:user/${currentUserName}`,
      [
        eks.AccessPolicy.fromAccessPolicyName("AmazonEKSClusterAdminPolicy", {
          accessScopeType: eks.AccessScopeType.CLUSTER,
        }),
      ],
    )

    cluster.clusterSecurityGroup.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(443),
      "Allow kubectl access from anywhere",
    )

    cluster.addNodegroupCapacity("PublicNodeGroup", {
      instanceTypes: [new ec2.InstanceType("t3.small")],
      minSize: 1,
      maxSize: 2,
      subnets: { subnetType: ec2.SubnetType.PUBLIC },
      amiType: eks.NodegroupAmiType.AL2023_X86_64_STANDARD,
    })

    const s3Policy = new iam.PolicyStatement({
      actions: ["s3:PutObject", "s3:GetObject", "s3:ListBucket"],
      resources: ["*"],
    })

    const serviceAccount = cluster.addServiceAccount("NestJsServiceAccount", {
      name: "nestjs-s3-sa",
      namespace: "default",
    })
    serviceAccount.addToPrincipalPolicy(s3Policy)

    new cdk.CfnOutput(this, "ConfigCommand", {
      value: `aws eks update-kubeconfig --name ${cluster.clusterName}`,
    })
  }
}

  • natGateways: 0, here I create a VPC without using a NAT gateway because this is a service with quite high costs
  • defaultCapacity: 0, the initial number of nodes (EC2); I will configure addAutoScalingGroupCapacity below, so it is not needed here
  • eks.KubernetesVersion.V1_34: this is the current latest version corresponding to the library @aws-cdk/lambda-layer-kubectl-v34; when you use it, please check again to use it with the appropriate kubectlLayer
  • currentUserName: this is the username of the account you use to deploy this CDK; you can get the information as follows (the purpose here is to grant permissions for your current account to operate with the Cluster after it is created)

aws sts get-caller-identity
{
    "UserId": "<UserId>",
    "Account": "<Account>",
    "Arn": "arn:aws:iam::<Account>:user/<username>"
}


  • ec2.Peer.anyIpv4(): I am allowing all IPs to connect to this cluster; if you want more security, you can use ec2.Peer.ipv4('<IP address>')
  • instanceTypes: [new ec2.InstanceType("t3.small")]: this is the minimum instance for nodes to run; if you use micro, there may be too few resources to use
  • PolicyStatement: because the NestJS docker image uses the S3 service, I will authorize this cluster to have permission to use the S3 service
  • the output part of ConfigCommand is used to connect to k8s


Next is the k8s.yml file

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nestjs-app
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nestjs
  template:
    metadata:
      labels:
        app: nestjs
    spec:
      serviceAccountName: nestjs-s3-sa
      containers:
        - name: nestjs-container
          image: 758222924841.dkr.ecr.ap-southeast-1.amazonaws.com/nestjs-app:latest
          ports:
            - containerPort: 3000
          env:
            - name: REGION
              value: "ap-southeast-1"
            - name: BUCKET
              value: "bucket-public-cb2a91d6"
---
apiVersion: v1
kind: Service
metadata:
  name: nestjs-service
spec:
  type: LoadBalancer
  selector:
    app: nestjs
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000

  • serviceAccountName: note that this part must have the same name as the serviceAccount created in the CDK
  • image: please replace it with the image URI that you pushed to your ECR accordingly
  • LoadBalancer: to connect to the external internet


After deploying, the result is as follows

 EksPublicStack
 Deployment time: 961.89s

Outputs:
EksPublicStack.ConfigCommand = aws eks update-kubeconfig --name MyEksCluster83497DF9-b36f5821b0624f9cba4aa5d9414ea355    

 Total time: 1068.63s


Resources have been created on the AWS Console


Update the k8s config with the created cluster; you only need to execute the command outputted after deploying the CDK

aws eks update-kubeconfig --name {cluster name}

aws eks update-kubeconfig --name MyEksCluster83497DF9-b36f5821b0624f9cba4aa5d9414ea355
Updated context arn:aws:eks:ap-southeast-1:758222924841:cluster/MyEksCluster83497DF9-b36f5821b0624f9cba4aa5d9414ea355 in ~\.kube\config


Next is to apply the content of the k8s.yml file

$ kubectl apply -f k8s.yml
deployment.apps/nestjs-app created
service/nestjs-service created


Wait a moment for the resources to be created; when you check and see the status of the resources as ready like this, it is successful

$ kubectl get all
NAME                             READY   STATUS    RESTARTS   AGE
pod/nestjs-app-d5bd589dd-4h8c2   1/1     Running   0          3m45s

NAME                     TYPE           CLUSTER-IP       EXTERNAL-IP                                                                    PORT(S)        AGE
service/kubernetes       ClusterIP      172.20.0.1       <none>                                                                         443/TCP        15m
service/nestjs-service   LoadBalancer   172.20.222.102   a66b8971219564b5390f2a15cb8bf3f3-1141398222.ap-southeast-1.elb.amazonaws.com   80:30087/TCP   3m45s

NAME                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nestjs-app   1/1     1            1           3m45s

NAME                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/nestjs-app-d5bd589dd   1         1         1       3m45s


Pay attention to the LoadBalancer EXTERNAL-IP part, which is the endpoint you can use to access the API

Happy coding!

See more articles here.

Comments

Popular posts from this blog

All practice series

Deploying a NodeJS Server on Google Kubernetes Engine

Setting up Kubernetes Dashboard with Kind

Using Kafka with Docker and NodeJS

Monitoring with cAdvisor, Prometheus and Grafana on Docker

Kubernetes Practice Series

Kubernetes Deployment for Zero Downtime

Practicing with Google Cloud Platform - Google Kubernetes Engine to deploy nginx

NodeJS Practice Series

Helm for beginer - Deploy nginx to Google Kubernetes Engine