<p>AWS supports <a href="https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html" rel="noreferrer">IAM Roles for Service Accounts (IRSA)</a> that allows cluster operators to map AWS IAM Roles to Kubernetes Service Accounts.</p>
<p>To do so, one has to create an <a href="https://eksctl.io/usage/iamserviceaccounts/" rel="noreferrer">iamserviceaccount</a> in an EKS cluster:</p>
<pre><code>eksctl create iamserviceaccount \
    --name &lt;AUTOSCALER_NAME&gt; \
    --namespace kube-system \
    --cluster &lt;CLUSTER_NAME&gt; \
    --attach-policy-arn &lt;POLICY_ARN&gt; \
    --approve \
    --override-existing-serviceaccounts
</code></pre>
<p>The problem is that I don't want to use the above <code>eksctl</code> command because I want to declare my infrastructure using <code>terraform</code>.</p>
<p>Does eksctl command do anything other than creating a service account? If it only creates a service account, what is the <code>YAML</code> representation of it?</p>

<p>I am adding my answer here because I stumble upon the same issue, and accepted answer (and other answers above), do not provide full resolution to the issue - no code examples. They are just guidelines which I had to use to research much deeper. There are some issues which is really easy to miss - and without code examples its quite hard to conclude what is happening (especially part related with Conditions/StringEquals while creating IAM role)</p>
<p>The whole purpose of creating a service account which is going to be tied with the role - is possibility of creating aws resources from within cluster (most common case is load balancer, or roles for pushing logs to the cloudwatch).</p>
<p>So, question is how we can do this, using terraform, instead of using eks commands.</p>
<p>What we need to do, is:</p>
<ol>
<li>create eks oidc (which can be done with terraform)</li>
<li>create AWS IAM role (which can be done with terraform), create and use proper policies</li>
<li>Create k8s service account (needs to be done with kubectl commands - or with terraform using kubernetes resources</li>
<li>Annotate k8s service account with IAM role we created (meaning that we are linking k8s service account with IAM role)</li>
</ol>
<p>After this setup, our k8s service account will have k8s cluster role and k8s cluster role binding (which will allow that service account to perform actions within the k8s) and, our k8s service account will have IAM role attached to it, which will allow to perform actions outside of the cluster (like creating aws resources)</p>
<p>So lets start with it. Assumption bellow is that your eks cluster is already created with terraform, and we are focusing on creating resources areound that eks cluster necessary for working service account.</p>
<p><strong>Create eks_oidc</strong></p>
<pre><code>### First we need to create tls certificate
data &quot;tls_certificate&quot; &quot;eks-cluster-tls-certificate&quot; {
  url = aws_eks_cluster.eks-cluster.identity[0].oidc[0].issuer
}

# After that create oidc
resource &quot;aws_iam_openid_connect_provider&quot; &quot;eks-cluster-oidc&quot; {
  client_id_list  = [&quot;sts.amazonaws.com&quot;]
  thumbprint_list = [data.tls_certificate.eks-cluster-tls-certificate.certificates[0].sha1_fingerprint]
  url             = aws_eks_cluster.eks-cluster.identity[0].oidc[0].issuer
}
</code></pre>
<p><strong>Now, lets create AWS IAM role with all necessary policies.</strong></p>
<p>Terraform declarative code bellow will:</p>
<ul>
<li>create ALBIngressControllerIAMPolicy policy</li>
<li>create alb-ingress-controller-role role</li>
<li>attach ALBIngressControllerIAMPolicyr policy to alb-ingress-controller-role role</li>
<li>attach already existing AmazonEKS_CNI_Policy policy to the role</li>
</ul>
<p>Make a note that i used suffixes as alb ingress controller here, because that is primary use of my role from within the cluster. You can change the name of policy of the role or you can change permission access for the policy as well in dependency of what you are planing to do with it.</p>
<pre><code>data &quot;aws_caller_identity&quot; &quot;current&quot; {}
locals {
  account_id = data.aws_caller_identity.current.account_id
  eks_oidc = replace(replace(aws_eks_cluster.eks-cluster.endpoint, &quot;https://&quot;, &quot;&quot;), &quot;/\\..*$/&quot;, &quot;&quot;)
}

# Policy which will allow us to create application load balancer from inside of cluster
resource &quot;aws_iam_policy&quot; &quot;ALBIngressControllerIAMPolicy&quot; {
  name        = &quot;ALBIngressControllerIAMPolicy&quot;
  description = &quot;Policy which will be used by role for service - for creating alb from within cluster by issuing declarative kube commands&quot;

  policy = jsonencode({
    Version = &quot;2012-10-17&quot;,
    Statement = [
      {
        Effect = &quot;Allow&quot;,
        Action = [
          &quot;elasticloadbalancing:ModifyListener&quot;,
          &quot;wafv2:AssociateWebACL&quot;,
          &quot;ec2:AuthorizeSecurityGroupIngress&quot;,
          &quot;ec2:DescribeInstances&quot;,
          &quot;wafv2:GetWebACLForResource&quot;,
          &quot;elasticloadbalancing:RegisterTargets&quot;,
          &quot;iam:ListServerCertificates&quot;,
          &quot;wafv2:GetWebACL&quot;,
          &quot;elasticloadbalancing:SetIpAddressType&quot;,
          &quot;elasticloadbalancing:DeleteLoadBalancer&quot;,
          &quot;elasticloadbalancing:SetWebAcl&quot;,
          &quot;ec2:DescribeInternetGateways&quot;,
          &quot;elasticloadbalancing:DescribeLoadBalancers&quot;,
          &quot;waf-regional:GetWebACLForResource&quot;,
          &quot;acm:GetCertificate&quot;,
          &quot;shield:DescribeSubscription&quot;,
          &quot;waf-regional:GetWebACL&quot;,
          &quot;elasticloadbalancing:CreateRule&quot;,
          &quot;ec2:DescribeAccountAttributes&quot;,
          &quot;elasticloadbalancing:AddListenerCertificates&quot;,
          &quot;elasticloadbalancing:ModifyTargetGroupAttributes&quot;,
          &quot;waf:GetWebACL&quot;,
          &quot;iam:GetServerCertificate&quot;,
          &quot;wafv2:DisassociateWebACL&quot;,
          &quot;shield:GetSubscriptionState&quot;,
          &quot;ec2:CreateTags&quot;,
          &quot;elasticloadbalancing:CreateTargetGroup&quot;,
          &quot;ec2:ModifyNetworkInterfaceAttribute&quot;,
          &quot;elasticloadbalancing:DeregisterTargets&quot;,
          &quot;elasticloadbalancing:DescribeLoadBalancerAttributes&quot;,
          &quot;ec2:RevokeSecurityGroupIngress&quot;,
          &quot;elasticloadbalancing:DescribeTargetGroupAttributes&quot;,
          &quot;shield:CreateProtection&quot;,
          &quot;acm:DescribeCertificate&quot;,
          &quot;elasticloadbalancing:ModifyRule&quot;,
          &quot;elasticloadbalancing:AddTags&quot;,
          &quot;elasticloadbalancing:DescribeRules&quot;,
          &quot;ec2:DescribeSubnets&quot;,
          &quot;elasticloadbalancing:ModifyLoadBalancerAttributes&quot;,
          &quot;waf-regional:AssociateWebACL&quot;,
          &quot;tag:GetResources&quot;,
          &quot;ec2:DescribeAddresses&quot;,
          &quot;ec2:DeleteTags&quot;,
          &quot;shield:DescribeProtection&quot;,
          &quot;shield:DeleteProtection&quot;,
          &quot;elasticloadbalancing:RemoveListenerCertificates&quot;,
          &quot;tag:TagResources&quot;,
          &quot;elasticloadbalancing:RemoveTags&quot;,
          &quot;elasticloadbalancing:CreateListener&quot;,
          &quot;elasticloadbalancing:DescribeListeners&quot;,
          &quot;ec2:DescribeNetworkInterfaces&quot;,
          &quot;ec2:CreateSecurityGroup&quot;,
          &quot;acm:ListCertificates&quot;,
          &quot;elasticloadbalancing:DescribeListenerCertificates&quot;,
          &quot;ec2:ModifyInstanceAttribute&quot;,
          &quot;elasticloadbalancing:DeleteRule&quot;,
          &quot;cognito-idp:DescribeUserPoolClient&quot;,
          &quot;ec2:DescribeInstanceStatus&quot;,
          &quot;elasticloadbalancing:DescribeSSLPolicies&quot;,
          &quot;elasticloadbalancing:CreateLoadBalancer&quot;,
          &quot;waf-regional:DisassociateWebACL&quot;,
          &quot;elasticloadbalancing:DescribeTags&quot;,
          &quot;ec2:DescribeTags&quot;,
          &quot;elasticloadbalancing:*&quot;,
          &quot;elasticloadbalancing:SetSubnets&quot;,
          &quot;elasticloadbalancing:DeleteTargetGroup&quot;,
          &quot;ec2:DescribeSecurityGroups&quot;,
          &quot;iam:CreateServiceLinkedRole&quot;,
          &quot;ec2:DescribeVpcs&quot;,
          &quot;ec2:DeleteSecurityGroup&quot;,
          &quot;elasticloadbalancing:DescribeTargetHealth&quot;,
          &quot;elasticloadbalancing:SetSecurityGroups&quot;,
          &quot;elasticloadbalancing:DescribeTargetGroups&quot;,
          &quot;shield:ListProtections&quot;,
          &quot;elasticloadbalancing:ModifyTargetGroup&quot;,
          &quot;elasticloadbalancing:DeleteListener&quot;
        ],
        Resource = &quot;*&quot;
      }
    ]
  })
}

# Create IAM role
resource &quot;aws_iam_role&quot; &quot;alb-ingress-controller-role&quot; {
  name = &quot;alb-ingress-controller&quot;

  assume_role_policy = &lt;&lt;POLICY
{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Sid&quot;: &quot;&quot;,
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Principal&quot;: {
        &quot;Federated&quot;: &quot;${aws_iam_openid_connect_provider.eks-cluster-oidc.arn}&quot;
      },
      &quot;Action&quot;: &quot;sts:AssumeRoleWithWebIdentity&quot;,
      &quot;Condition&quot;: {
        &quot;StringEquals&quot;: {
          &quot;${replace(aws_iam_openid_connect_provider.eks-cluster-oidc.url, &quot;https://&quot;, &quot;&quot;)}:sub&quot;: &quot;system:serviceaccount:kube-system:alb-ingress-controller&quot;,
          &quot;${replace(aws_iam_openid_connect_provider.eks-cluster-oidc.url, &quot;https://&quot;, &quot;&quot;)}:aud&quot;: &quot;sts.amazonaws.com&quot;
        }
      }
    }
  ]
}
POLICY

  depends_on = [aws_iam_openid_connect_provider.eks-cluster-oidc]

  tags = {
    &quot;ServiceAccountName&quot; = &quot;alb-ingress-controller&quot;
    &quot;ServiceAccountNameSpace&quot; = &quot;kube-system&quot;
  }
}

# Attach policies to IAM role
resource &quot;aws_iam_role_policy_attachment&quot; &quot;alb-ingress-controller-role-ALBIngressControllerIAMPolicy&quot; {
  policy_arn = aws_iam_policy.ALBIngressControllerIAMPolicy.arn
  role       = aws_iam_role.alb-ingress-controller-role.name
  depends_on = [aws_iam_role.alb-ingress-controller-role]
}

resource &quot;aws_iam_role_policy_attachment&quot; &quot;alb-ingress-controller-role-AmazonEKS_CNI_Policy&quot; {
  role       = aws_iam_role.alb-ingress-controller-role.name
  policy_arn = &quot;arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy&quot;
  depends_on = [aws_iam_role.alb-ingress-controller-role]
}
</code></pre>
<p>After executing terraform above, you have successfully created terraform part of the resources. Now we need to create a k8s service account and bind IAM role with that service account.</p>
<p><strong>Creating cluster role, cluster role binding and service account</strong></p>
<p>You can use</p>
<p><a href="https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/master/docs/examples/rbac-role.yaml" rel="noreferrer">https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/master/docs/examples/rbac-role.yaml</a></p>
<p>directly (from the master branch), but having in mind that we need to annotate the iam arn, i have tendency to download this file, update it and store it as updated within my kubectl config files.</p>
<pre><code>apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: alb-ingress-controller
  name: alb-ingress-controller
rules:
  - apiGroups:
      - &quot;&quot;
      - extensions
    resources:
      - configmaps
      - endpoints
      - events
      - ingresses
      - ingresses/status
      - services
      - pods/status
    verbs:
      - create
      - get
      - list
      - update
      - watch
      - patch
  - apiGroups:
      - &quot;&quot;
      - extensions
    resources:
      - nodes
      - pods
      - secrets
      - services
      - namespaces
    verbs:
      - get
      - list
      - watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  labels:
    app.kubernetes.io/name: alb-ingress-controller
  name: alb-ingress-controller
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: alb-ingress-controller
subjects:
  - kind: ServiceAccount
    name: alb-ingress-controller
    namespace: kube-system
---
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    app.kubernetes.io/name: alb-ingress-controller
  name: alb-ingress-controller
  namespace: kube-system
  annotations:
    eks.amazonaws.com/role-arn: &lt;ARN OF YOUR ROLE HERE&gt;
...
</code></pre>
<p>At the bottom of this file, you will notice annotation where you will need to place your ANR role.</p>
<p><strong>Double check</strong></p>
<p>And that would be it. After that you have a k8s service account which is connected with iam role.</p>
<p>Check with:</p>
<pre><code>kubectl get sa -n kube-system
kubectl describe sa alb-ingress-controller -n kube-system
</code></pre>
<p>And you should get output similar to this (annotations is the most important part, because it confirms the attachment of iam role):</p>
<pre><code>Name:                alb-ingress-controller
Namespace:           kube-system
Labels:              app.kubernetes.io/managed-by=Helm
                     app.kubernetes.io/name=alb-ingress-controller
Annotations:         eks.amazonaws.com/role-arn: &lt;YOUR ANR WILL BE HERE&gt;
                     meta.helm.sh/release-name: testrelease
                     meta.helm.sh/release-namespace: default
Image pull secrets:  &lt;none&gt;
Mountable secrets:   alb-ingress-controller-token-l4pd8
Tokens:              alb-ingress-controller-token-l4pd8
Events:              &lt;none&gt;
</code></pre>
<p>From now on, you can use this service to manage internal k8s resources and external which are allowed by the policies you attached.</p>
<p>In my case, as mentioned before, I used it (beside other things) for creation of alb ingress controller and load balancer, hence all of the prefixes with &quot;alb-ingress&quot;</p>
