Create AWS EKS Fargate Using Terraform (EFS, HPA, Ingress, ALB, IRSA, Kubernetes, Helm, Tutorial)¶
- You can find the source code for this video in my GitHub Repo.
Intro¶
In this video, we're going to go over the following sections:
- Create AWS VPC Using Terraform
- Create AWS EKS Fargate Using Terraform
- Update CoreDNS to run on AWS Fargate
- Deploy App to AWS Fargate
- Deploy Metrics Server to AWS Fargate
- Auto Scale with HPA Based on CPU and Memory
- Improve Stability with Pod Disruption Budget
- Create IAM OIDC provider Using Terraform
- Deploy AWS Load Balancer Controller Using Terraform
- Create Simple Ingress
- Secure Ingress with SSL/TLS
- Create Network Loadbalancer
- Integrate Amazon EFS with AWS Fargate
You can find the timestamps in the video description.
Create AWS VPC Using Terraform¶
First of all, we need to declare aws terraform provider.
You may want to update the aws region, cluster name, and possibly a eks version.
We're also going to be using a new version of the aws provider, so let's set the constrain here as well.
I also include terraform lock file (.terraform.lock.hcl
), so if you encounter any issues, try to copy that file and rerun the terraform.
terraform/0-provider.tf | |
---|---|
Next, we need to create the AWS VPC itself. Here it's very important to enable dns support and hostnames, especially if you are planning to use the EFS file system in your cluster. Otherwise, the CSI driver will fail to resolve the EFS endpoint. Currently, AWS Fargate does not support EBS volumes, so EFS is the only option for you if you want to run stateful workloads in your Kubernetes cluster.
terraform/1-vpc.tf | |
---|---|
Then the Internet Gateway. It is used to provide internet access directly from the public subnets and indirectly from private subnets by using a NAT gateway.
terraform/2-igw.tf | |
---|---|
Now we need to create four subnets. Two private subnets and two public subnets. If you are using a different region, you need to update availability zones. Also, it's very important to tag your subnets with the following labels. Internal-elb tag used by EKS to select subnets to create private load balancers and elb tag for public load balancers. Also, you need to have a cluster tag with owned or shared value.
For the NAT Gateway, I prefer to allocate an Elastic IP address with terraform as well. We need to explicitly depend on the Internet Gateway here and place this NAT to one of the public subnets with a default route to the Internet Gateway.
terraform/4-nat.tf | |
---|---|
The last components that we need to create before we can start provisioning EKS are route tables. The first is the private route table with the default route to the NAT Gateway. The second is a public route table with the default route to the Internet Gateway. Finally, we need to associate previously created subnets with these route tables. Two private subnets and two public subnets.
Now, you can declare the rest of the components, including EKS, but I will do it step by step.
Let's go to the terminal and run terraform init
first. Then terraform apply
to create VPC and subnets.
Create AWS EKS Fargate Using Terraform¶
The next step is to create an EKS control plane without any additional nodes. This control plane can be used to attach self-managed, and aws managed nodes as well as you can create Fargate profiles.
First of all, let's create an IAM role for EKS. It will use it to make API calls to AWS services, for example, to create managed node pools.
terraform/6-eks.tf | |
---|---|
Then we need to attach AmazonEKSClusterPolicy to this role.
terraform/6-eks.tf | |
---|---|
And, of course, the EKS control plane itself. You would need to use our role to create a cluster. Also, if you have a bastion host or VPN configured that allows you to access private IP addresses within the VPC, I would highly recommend enabling a private endpoint.
I have a video on how to deploy OpenVPN to AWS if you are interested, including how to resolve private Route53 hosted zones. If you still decide to use a public endpoint, you can restrict access using CIDR blocks. Also, specify two private and two public subnets. AWS Fargate can only use private subnets with NAT gateway to deploy your pods. Public subnets can be used for load balancers to expose your application to the internet.
After you provisioned the EKS with terraform, you would need to update your Kubernetes context to access the cluster with the following command. Just update the region and cluster name to match yours.
EKS was built to be used as a regular Kubernetes cluster. It expects a default node pool to run system components such as CoreDNS.
If you run kubectl get pods -A
, you will see that CoreDNS pods are stuck in a pending state. Before we can proceed, we need to resolve this issue.
You can verify that you don't have any nodes by using this command.
Update CoreDNS to Run on AWS Fargate¶
To run CoreDNS or any other application in AWS Fargate, first, you need to create a Fargate profile. It is a setting that allows EKS automatically create nodes for your application based on Kubernetes namespace and optionally pod labels.
We need to create a single IAM role that can be shared between all the Fargate profiles. Similar to EKS, Fargate needs permissions to spin up the nodes and connect them to the EKS control plane.
terraform/7-kube-system-profile.tf | |
---|---|
Then we need to attach AWS managed IAM policy called AmazonEKSFargatePoExecutionRolePolicy.
terraform/7-kube-system-profile.tf | |
---|---|
For the AWS Fargate profile, we need to specify the EKS cluster. Then the name, I usually match it with the Kubernetes namespace and an IAM role. When you select subnets for your profile, make sure that you have appropriate tags with cluster name. Finally, you must specify the Kubernetes namespace that you want AWS Fargate to manage. Optionally you can filter by pods labels.
Let's go back to the terminal, and run terraform apply. It should take a minute or two.
Now, if you get pods again, you would expect that CoreDNS should be scheduled already. But most likely, if the EKS team won't fix it in later releases, those coreds pods will continue to be in a pending state.
You can try to describe the pod to get some kind of error from the Kubernetes controller. You should get something like: no nodes available. If you scroll up, you'll see a reason. These pods come with compute-type: ec2 annotation that prevents them from being scheduled on fargate nodes. The fix is simple, just remove the annotation from the deployment template.
First, I'll show you how to fix this manually and then a terraform code.
Let's split the terminal, and in the first window run, kubectl get events. It's very helpful when you need to debug Kubernetes issues.
In the second window, just run get pods.
Then let's use the kubectl patch command to remove this annotation from CoreDNS deployment.
kubectl patch deployment coredns \
-n kube-system \
--type json \
-p='[{"op": "remove", "path": "/spec/template/metadata/annotations/eks.amazonaws.com~1compute-type"}]'
When you apply, it will immediately recreate CoreDNS deployment without those annotations. In a few seconds, AWS Fargate should spin up a couple of nodes to fix coredns. It may take up to 5 minutes.
If you rerun kubectl get nodes, you should see two fargate instances. AWS Fargate creates a dedicated node for each pod with similar resource quotas.
Now, you can patch coredns deployment from the terraform code. But at this time, it will look a little bit ugly. There is a terraform Kubernetes annotation resource, but it only updates high-level annotations, for example, on the deployment object itself. But we need to update pod-level annotations. To patch, let's use null resource. It's just a similar kubectl patch command. It works just fine you just need to make sure that you have all the binaries in place, such as kubectl aws-authenticator, etc.
Since I already fixed this manually, I will comment this section out.
Deploy App to AWS Fargate¶
The next step is to deploy an application to AWS Fargate. You already know how to create a profile. Let's create another one for staging namespace this time.
The only difference here is a profile name and a selector. With this profile, you can only deploy applications to the staging namespace.
Let's quickly apply the terraform.
Now, let's create another folder for Kubernetes files. The first file is a simple-deployment.yaml. In the future, we will use this deployment for auto-scaling, and also we will expose it to the internet using the AWS Load Balancer controller.
Let's declare a staging namespace. Then the deployment object. It's a simple php-apache image that is provided by the Kubernetes community to test autoscaling. The important part here is the resource block. AWS Fargate will create a dedicated node for your application, so your resource limit and request should match.
Alright, let's go and create a deployment. In the first window, run get pods.
And in the second, use kubectl to apply the deployment.
Deploy Metrics Server to AWS Fargate¶
To be able to autoscale our app in Kubernetes, we can use either Prometheus or a metrics server as a source for CPU and memory usage. Since we're using terraform to provision our infrastructure, let's use terraform helm provider to deploy the metrics server as well.
There are multiple ways to authenticate with Kubernetes. If you use EKS, the preferred method would be to get a temporary token to authenticate with the Kubernetes api server and deploy a helm chart. This aws command is part of the provider, so you don't need to install anything extra.
Next is a helm release. Give it the name metrics-server. Specify the namespace, version, and I disabled internal metrics, which is actually a default setting. We also need to explicitly depend on the kube-system aws fargate profile.
Since we use a new helm provider, we need to initialize terraform again and apply after.
If it fails to deploy the helm chart, you can check the status of the helm release.
Also, check the status of the pod. In case the pod was not able to be scheduled, it may fail the terraform.
Auto Scale with HPA Based on CPU and Memory¶
Before we can test auto-scaling, we need to create a service for our deployment. Make sure that you specify the same namespace where you deployed the apache.
k8s/service.yaml | |
---|---|
Then fairly simple autoscaling policy. Specify the minimum number of pods and the maximum. Then the reference to the deployment object. For the target, let's just use the CPU threshold. If the average CPU usage of all pods in this deployment exceeds 50%, the horizontal pod autoscaller will add an additional replica.
k8s/hpa.yaml | |
---|---|
Now, you need to remove the replica count from the deployment itself in case you store your config in the git and synchronize with Kubernetes.
This time we can apply the whole folder.
For the demo, again, split the screen. In the first window, we can run get pods. Right now, we have a single replica.
In the second, you can watch horizontal pod autoscaller. It takes a few seconds for autoscaller to correctly update targets.
Lastly, let's run the load generating tool. It will spin up an additional pod and continuously run CPU-intensive tasks on apache.
kubectl run -i --tty -n staging load-generator --pod-running-timeout=5m0s --rm --image=busybox:1.28 --restart=Never -- /bin/sh -c "while sleep 0.01; do wget -q -O- http://php-apache; done"
Now, you can see that the CPU usage goes up. When it reaches 50%, we will get a new replica. If it's not enough to bring CPU usage under 50%, autoscaller will create another one. When you use AWS Fargate, you don't need to worry about cluster autoscaller since AWS Fargate automatically scales based on the pod requests.
Improve Stability with Pod Disruption Budget¶
Amazon EKS must periodically patch AWS Fargate pods to keep them secure. Sometimes it means that pods need to be recreated. To limit the impact on your application, you should set appropriate pod disruption budgets (PDBs) to control the number of pods that are down at the same time.
In this example, we limit to 1 unavailable pod at any given time. You also need to match the label on the deployment object to select appropriate pods. In this case, we use label run: php-apache.
k8s/pdb.yaml | |
---|---|
Let's apply it and get PDBs in the staging namespace.
You can see in real-time how many pods can be unavailable for this deployment.
Create IAM OIDC provider Using Terraform¶
You can associate an IAM role with a Kubernetes service account. This service account can then provide AWS permissions to the containers in any pod that uses that service account. With this feature, you no longer need to provide extended permissions to all Kubernetes nodes so that pods on those nodes can call AWS APIs.
Run terraform init and apply.
You can use list-open-id-connect-providers command to find out if the provider was created.
Deploy AWS Load Balancer Controller Using Terraform¶
The next step is to deploy AWS Load Balancer controller, but first, we need to create an IAM role and establish trust with the Kubernetes service account.
Also, let's create AWSLoadBalancerController IAM policy. You can get this from the official project for the controller.
terraform/AWSLoadBalancerController.json | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 |
|
Finally, we need to deploy controller to Kubernetes using Helm. It's going to be deployed to the same kube-system namespace. If you decide to deploy to a different namespace, you also need to create an AWS Fargate profile for that namespace.
Now we can apply terraform.
You can check the helm status.
Also, the pod status.
In the following example, we will be using this ingress class created by the aws load balancer controller.
Create Simple Ingress¶
Next, let's create ingress to expose apache to the internet using AWS Load Balancer controller. First, it's going to be a plain http, and in the following example, we will attach a certificate to this load balancer to terminate TLS.
By default, AWS Load Balancer controller will create load balancers with private IPs only. They can only be accessed within your VPC. To change that, we can use annotations. In general AWS Load Balancer controller supports two modes. Instance mode and IP mode. AWS fargate only can be used with IP mode. It will create a target group in AWS and use the pod IP address to route traffic.
I highly recommend when you create your first ingress, check the logs on the controller to make sure that there are no errors.
When you apply the ingress, the controller will reconcile and create an application load balancer. If you see permission denied or a similar error, check if the IAM role is properly configured to work with the Kubernetes service account.
To verify the ingress, we need to create a CNAME record in your DNS hosting provider and point to the load balancer hostname. Check if you can correctly resolve the DNS.
Then you can use curl or go to the browser to access the apache server.
Secure Ingress with SSL/TLS¶
It's very unlikely that you would want to expose your service using the plain HTTP protocol. Let's secure our apache server with TLS.
First, we need to request a certificate from the AWS Certificate Manager.
Then we can use either the autodiscovery mechanism or explicitly specify the ARN of the certificate. You can copy it from the AWS console.
Now we can apply the ingress.
You can check the certificate status in the browser.
Create Network Loadbalancer¶
AWS Load Balancer controller can also manage Kubernetes service of type LoadBalancer. In-tree Kuberetnes controller creates a classic load balancer, but the AWS Load Balancer controller will create a network load balancer.
When you apply a service, it should create a network load balancer and provide the external ip address.
To access it, you can use the hostname of the load balancer directly.
Integrate Amazon EFS with AWS Fargate¶
Currently, AWS Fargate doesn't support PersistentVolume back by EBS. Right now, you can use only EFS. With EFS, you can use ReadWriteMany mode and mount the same volume to multiple pods. With EBS, you can mount a volume only to a single pod.
Let's create an EFS file system using terraform. You can tweak settings based on your requirements; I'll keep them default except for encryption.
Let's apply the terraform.
Now, let's create a storage class for EFS. Then the persistent volume. EFS automatically grows and shrinks, but persistent volume requires us to provide some sort of capacity. It can by anything. Also, you need to specify your EFS ID under volumeHandle. You can get it from AWS.
In the terminal, for the first time, you can watch staging events in case you get an error.
When you apply, first, it will take a minute or so for aws fargate to spin up the node, and then it will allocate volume from the EFS file system.
If you misconfigured something, the pod would be stuck in a container-creating state.
AWS Fargate can be helpful in keeping costs under control. From my experience, it's hard to maintain the maximum utilization of Kubernetes resources, especially if you have a bunch of node groups in your cluster.