Please for more info on how all this works on EKS and IAM, please refers to the post IAM Roles for EKS Service Account where eksctl has been used to demonstrate a pod using an IAM role to perform AWS API calls. And in this tutorial, I will do the same demo but using Terraform. In this demo hands on, I will create the following:
I will be using terraform version greater or equal to 0.12.0 (in my case I am using 0.12.18 to be precise) in this tutorial to provision the necessary AWS resources and please make sure kubergrunt is installed which will be used to retrieve the OIDC thumbprint using terraform external data source.
kubergrunt: https://github.com/gruntwork-io/gruntwork-installer
Terraform: https://www.terraform.io/
Git repository located containing all files: https://github.com/kalilou/eks-iam-tutorial and the structure look the following:
├── OIDCAssumeRole.json.tmpl # Contains the role assume policy
├── eks_cluster.tf # EKS cluster resources
├── eks_worker_node.tf # EKS managed node groups resources
├── iam.tf # IAM Roles/Policies resources
├── main.tf # Terraform AWS provider configuration
├── security_group.tf # Security group resources
└── thumprint.sh # Script to retrieve the OIDC thumbprint
Minor note, please replace REDACTED with the right value for your use case.
I will assume that you have your VPC setup already. And now here the content of the terraform file for creating the EKS cluster (eks_cluster.tf).
resource "aws_eks_cluster" "this" {
name = "eks_iam_example"
role_arn = aws_iam_role.cluster_role.arn
vpc_config {
subnet_ids = local.subnet_ids
security_group_ids = [aws_security_group.control_plane_sg.id]
}
// You can also remove it and let terraform use the default values
timeouts {
create = "35m" // Default value is set to 20 minutes
delete = "20m" // Default value is set to 15 minutes
}
enabled_cluster_log_types = [
"api",
"audit",
"scheduler",
"authenticator",
"controllerManager"
]
depends_on = [
"aws_cloudwatch_log_group.this",
"aws_iam_role_policy_attachment.AmazonEKSClusterPolicy",
"aws_iam_role_policy_attachment.AmazonEKSServicePolicy",
]
}
resource "aws_cloudwatch_log_group" "this" {
name = "/aws/eks/eks_iam_example/cluster"
retention_in_days = 7
tags = {
Name = "eks-cluster-example"
}
}
data "external" "thumb" {
program = ["kubergrunt", "eks", "oidc-thumbprint", "--issuer-url", aws_eks_cluster.this.identity.0.oidc.0.issuer]
}
// Resource for creating the OIDC provider
resource "aws_iam_openid_connect_provider" "this" {
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [data.external.thumb.result.thumbprint]
url = aws_eks_cluster.this.identity.0.oidc.0.issuer
}
resource "aws_security_group" "control_plane_sg" {
name = "control-plane-sg"
description = "EKS control plane security group"
vpc_id = "REDACTED"
// Allow worker node to communicate with eks cluster
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
self = true
}
// Allow eks to access the internet
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
The resource awsiamopenidconnectprovider is the OIDC provider. This will support the federation access hence allowing to assume an IAM role via the Secure Token Service (STS).
Now create the managed node group file (eksworkernode.tf) with the following content.
resource "aws_eks_node_group" "example" {
cluster_name = aws_eks_cluster.this.name
node_group_name = "eks-iam-workernode-example"
node_role_arn = aws_iam_role.worker_node_role.arn
subnet_ids = local.subnet_ids
scaling_config {
desired_size = 1
max_size = 1
min_size = 1
}
instance_types = [
"t3.small"
]
depends_on = [
aws_iam_role_policy_attachment.AmazonEKSWorkerNodePolicy,
aws_iam_role_policy_attachment.AmazonEKS_CNI_Policy,
aws_iam_role_policy_attachment.AmazonEC2ContainerRegistryReadOnly,
]
}
Now create the IAM roles/policies file (iam.tf) with the following contains.
resource "aws_iam_role" "cluster_role" {
name = "eks-iam-cluster-role"
assume_role_policy = jsonencode({
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "eks.amazonaws.com"
}
}]
Version = "2012-10-17"
})
tags = {
Name = "eks-cluster-example"
}
}
resource "aws_iam_role_policy_attachment" "AmazonEKSClusterPolicy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
role = aws_iam_role.cluster_role.name
}
resource "aws_iam_role_policy_attachment" "AmazonEKSServicePolicy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSServicePolicy"
role = aws_iam_role.cluster_role.name
}
resource "aws_iam_role" "worker_node_role" {
name = "eks-node-group-example"
assume_role_policy = jsonencode({
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}]
Version = "2012-10-17"
})
tags = {
eksCluster = aws_eks_cluster.this.id
Name = "eks-cluster-example"
}
}
resource "aws_iam_role_policy_attachment" "AmazonEKSWorkerNodePolicy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
role = aws_iam_role.worker_node_role.name
}
resource "aws_iam_role_policy_attachment" "AmazonEKS_CNI_Policy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
role = aws_iam_role.worker_node_role.name
}
resource "aws_iam_role_policy_attachment" "AmazonEC2ContainerRegistryReadOnly" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
role = aws_iam_role.worker_node_role.name
}
Create now the main file containing AWS provider and terraform configuration blocks.
provider "aws" {
region = "REDACTED"
version = "2.43.0"
}
terraform {
required_version = "> 0.12.0"
backend "s3" {
bucket = "eks-iam-example-revolight"
key = "eks-iam.state"
region = "REGION_REDACTED"
}
}
locals {
subnet_ids = [
"subnet-a1REDACTED",
"subnet-b9REDACTED",
"subnet-d7REDACTED"
]
}
Now run terraform to create the specified resources.
$ terraform init
Initializing the backend...
Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "aws" (hashicorp/aws) 2.43.0...
Terraform has been successfully initialized!
.........
$ terraform apply
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
................
Now let's verify the cluster has been properly created, prior to the verification, I will setup my kubeconfig in order to run kubectl CLI.
# Set up the kubeconfig by running this command below
$ aws eks --region REDACTED update-kubeconfig --name eks_iam_example --alias eks-iam
Added new context eks-iam to /Users/REDACTED/.kube/config</code>
Verify the cluster has been created.
$ aws eks list-clusters --region REDACTED
{
"clusters": [
"eks_iam_example"
]
}
Verify the worker node has been also created.
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
ip-172-31-9-73.REDACTED.compute.internal Ready <none> 31m v1.14.7-eks-1861c5
Verify the OIDC provider has been created for the cluster.
$ aws eks describe-cluster --name eks_iam_example --query cluster.identity.oidc.issuer --region REDACTED
"https://oidc.eks.REDACTED.amazonaws.com/id/296C***REDACTED***EBC3"
In this section, I will create a Kubernetes Service Account and an IAM role which will associated the service account and later on used by a pod to perform AWS API calls.
Create a policy template file which will be used in the IAM role creation.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "${OIDC_ARN}"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"${OIDC_URL}:sub": "system:serviceaccount:${NAMESPACE}:${SA_NAME}"
}
}
}
]
}
Add these lines to the "iam.tf" file to create the IAM role.
resource "aws_iam_role" "read_only" {
name = "AWSReadonly"
assume_role_policy = templatefile("OIDCAssumeRole.json.tmpl", {
OIDC_ARN = aws_iam_openid_connect_provider.this.arn,
OIDC_URL = replace(aws_iam_openid_connect_provider.this.url, "https://", ""),
NAMESPACE = "default", SA_NAME = "eks-iam-example"
})
depends_on = [aws_iam_openid_connect_provider.this]
}
resource "aws_iam_role_policy_attachment" "read_only" {
role = aws_iam_role.read_only.name
policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}
Apply terraform to create the role.
$ terraform apply
aws_cloudwatch_log_group.this: Refreshing state... [id=/aws/eks/eks_iam_example/cluster]
aws_iam_role.cluster_role: Refreshing state... [id=eks-iam-cluster-role]
.............
Create Kubernetes service account yaml file which will use the IAM Role created above via annotation (service_account.yaml).
apiVersion: v1
kind: ServiceAccount
metadata:
name: eks-iam-example
namespace: kube-system
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::REDACTED:role/AWSReadonly
Create the Service Account.
$ kubectl apply -f service_account.yaml -n default
serviceaccount/eks-iam-example created
List Service Accounts.
$ kubectl get sa -n default
NAME SECRETS AGE
default 1 65m
eks-iam-example 1 2s
Describe Service Account eks-iam-example.
$ kubectl describe sa eks-iam-example -n default
Name: eks-iam-example
Namespace: default
Labels: <none>
Annotations: eks.amazonaws.com/role-arn: arn:aws:iam::943700067695:role/AWSReadonly
kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"v1","kind":"ServiceAccount","metadata":{"annotations":{"eks.amazonaws.com/role-arn":"arn:aws:iam::REDACTED:role/AWSRead...
Image pull secrets: <none>
Mountable secrets: eks-iam-example-token-dk5n4
Tokens: eks-iam-example-token-dk5n4
Events: <none>
Create a deployment file (eksiamexample.yaml).
apiVersion: apps/v1
kind: Deployment
metadata:
name: eks-iam-example
spec:
replicas: 1
selector:
matchLabels:
app: eks-iam-example
template:
metadata:
labels:
app: eks-iam-example
spec:
serviceAccountName: eks-iam-example
containers:
- name: eks-iam-example
image: sdscello/awscli:latest
ports:
- containerPort: 80
Create the eks-iam-example deployment.
$ kubectl apply -f kubernetes/eks_iam_example.yaml -n default
deployment.apps/eks-iam-example created
List all pods.
$ kubectl get pods -n default
NAME READY STATUS RESTARTS AGE
eks-iam-example-854b4f5ddc-pnt64 1/1 Running 0 32s
List all environment variables to verify that AWSROLEARN and AWSWEBIDENTITYTOKENFILE have been injected.
$ kubectl exec eks-iam-example-854b4f5ddc-pnt64 -n default env|grep AWS
AWS_ROLE_ARN=arn:aws:iam::REDACTED:role/AWSReadonly
AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/eks.amazonaws.com/serviceaccount/token
Run AWS CLI to list all buckets which should work as expected.
$ kubectl exec eks-iam-example-854b4f5ddc-pnt64 -n default aws s3 ls
2019-09-16 11:08:18 REDACTED-***********************
2017-11-01 09:37:38 REDACTED-***********************
2018-06-27 09:06:16 thesis.kaliloudiaby.com
Run AWS CLI to create a bucket which should be denied as expected.
$ kubectl exec -it eks-iam-example-854b4f5ddc-pnt64 -n default bash
root@eks-iam-example-854b4f5ddc-pnt64:/ aws s3api create-bucket --bucket eks-iam-test-revolight
An error occurred (AccessDenied) when calling the CreateBucket operation: Access Denied</code>
$ terraform destroy
This tutorial has been also done using eks, refer to: IAM Roles for EKS Service Account