AWS Elastic Kubernetes Service: a cluster creation automation, part 2 — Ansible, eksctl
Tags: kubernetes, aws, devops, tutorial
Published: 2020-05-04 04:14:41

---
title: AWS Elastic Kubernetes Service: a cluster creation automation, part 2 — Ansible, eksctl
published: true
date: 2020-05-01 12:36:43 UTC
tags: kubernetes,aws,devops,tutorial
canonical_url: https://rtfm.co.ua/en/aws-elastic-kubernetes-service-a-cluster-creation-automation-part-2-ansible-eksctl/
---

### AWS Elastic Kubernetes Service: a cluster creation automation, part 2 — Ansible, eksctl

![](https://cdn-images-1.medium.com/max/130/0*oqilpishMydwiwU9.png)

The first part — [AWS Elastic Kubernetes Service: a cluster creation automation, part 1 — CloudFormation](https://rtfm.co.ua/en/aws-elastic-kubernetes-service-a-cluster-creation-automation-part-1-cloudformation/).

To remind the whole idea is to create an automation process to create an EKS cluster:

1. Ansible uses the [cloudformation module](https://rtfm.co.ua/ansible-modul-cloudformation/) to create an infrastructure
2. by using an Outputs of the CloudFormation stack created — Ansible from a template will generate a cluster-config file for the eksctl
3. Ansible calls eksctl with that config-file to create an EKS cluster

All this will be done from a Jenkins job using a Docker image with AWS CLI, Ansible and eksctl. Maybe it will be the third part of this series.

All the resulted files after writing this post are available in the [eksctl-cf-ansible](https://rtfm.co.ua/goto/https://github.com/setevoy2/eksctl-cf-ansible/tree/rdy) Github repository. The link here is to the branch with an exact copy of the code below.

**Content**

- [AWS Authentification](https://rtfm.co.ua/en/aws-elastic-kubernetes-service-a-cluster-creation-automation-part-2-ansible-eksctl/#AWS_Authentification)
- [Ansible CloudFormation](https://rtfm.co.ua/en/aws-elastic-kubernetes-service-a-cluster-creation-automation-part-2-ansible-eksctl/#Ansible_CloudFormation)
- [CloudFormation Parameters and Ansible variables](https://rtfm.co.ua/en/aws-elastic-kubernetes-service-a-cluster-creation-automation-part-2-ansible-eksctl/#CloudFormation_Parameters_and_Ansible_variables)
- [The CloudFormation role](https://rtfm.co.ua/en/aws-elastic-kubernetes-service-a-cluster-creation-automation-part-2-ansible-eksctl/#The_CloudFormation_role)
- [Ansible eksctl](https://rtfm.co.ua/en/aws-elastic-kubernetes-service-a-cluster-creation-automation-part-2-ansible-eksctl/#Ansible_eksctl)
- [Cluster config — eks-cluster-config.yml](https://rtfm.co.ua/en/aws-elastic-kubernetes-service-a-cluster-creation-automation-part-2-ansible-eksctl/#Cluster_config_%E2%80%93_eksclusterconfig_yml)
- [eksctl create vs update](https://rtfm.co.ua/en/aws-elastic-kubernetes-service-a-cluster-creation-automation-part-2-ansible-eksctl/#eksctl_create_vs_update)
- [TEST: CloudFormation && EKS Custom parameters](https://rtfm.co.ua/en/aws-elastic-kubernetes-service-a-cluster-creation-automation-part-2-ansible-eksctl/#TEST_CloudFormation_EKS_Custom_parameters)
- [P.S. aws-auth ConfigMap](https://rtfm.co.ua/en/aws-elastic-kubernetes-service-a-cluster-creation-automation-part-2-ansible-eksctl/#P_S_awsauth_ConfigMap)
- [Useful links](https://rtfm.co.ua/en/aws-elastic-kubernetes-service-a-cluster-creation-automation-part-2-ansible-eksctl/#Useful_links)
- [Kubernetes](https://rtfm.co.ua/en/aws-elastic-kubernetes-service-a-cluster-creation-automation-part-2-ansible-eksctl/#Kubernetes)
- [Ansible](https://rtfm.co.ua/en/aws-elastic-kubernetes-service-a-cluster-creation-automation-part-2-ansible-eksctl/#Ansible)
- [AWS](https://rtfm.co.ua/en/aws-elastic-kubernetes-service-a-cluster-creation-automation-part-2-ansible-eksctl/#AWS)
- [EKS](https://rtfm.co.ua/en/aws-elastic-kubernetes-service-a-cluster-creation-automation-part-2-ansible-eksctl/#EKS)
- [CloudFormation](https://rtfm.co.ua/en/aws-elastic-kubernetes-service-a-cluster-creation-automation-part-2-ansible-eksctl/#CloudFormation)

#### AWS Authentification

It worth thinking about authentification beforehand to not remodel everything from scratch (I did).

Also, it’s highly recommended to read the [Kubernetes: part 4 — AWS EKS authentification, aws-iam-authenticator, and AWS IAM](https://rtfm.co.ua/en/kubernetes-part-4-aws-eks-authentification-aws-iam-authenticator-and-aws-iam/) post as in this current post will be used a lot of things described there.

So, we need to have some AWS authentification mechanism because we need to authenticate:

- Ansible for its _cloudformation_ role
- Ansible for its _eksctl_ role
- Ansible to execute AWS CLI commands

The most non-obvious issue here is the fact, that the IAM user used to create an EKS cluster becomes its “super-admin” and you can not change it.

For example, in the previous part we created the cluster using the following command:

```
$ eksctl --profile arseniy create cluster -f eks-cluster-config.yml
```

Now, you can check the authenticator logs in the CloudWatch, the very first entries there — and you’ll see the root-suer from the system:masters group with kubernetes-admin permissions:

![](https://cdn-images-1.medium.com/max/1024/0*YZ7_BGH54gjhoSs9.png)

Well — what can we use for AWS authentification??

1. access keys — AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
2. [AWS CLI named profiles](https://rtfm.co.ua/en/aws-cli-named-profiles/)
3. [AWS IAM EC2 Instance profiles](https://rtfm.co.ua/en/aws-iam-users-keys-rotation-ec2-iam-roles-and-jenkins/) (see also [AWS: IAM AssumeRole — описание, примеры](https://rtfm.co.ua/aws-iam-assumerole-opisanie-primery/), _Rus_)

But as we will use Jenkins here — we can use its IAM EC2 Instance role with necessary permissions.

For now, I realized the next scheme:

- manually create an IAM user called _eks-root_ with the EKS Admin policy
- Jenkins will create necessary resources using its IAM Instance role with the EKS Admin policy and will become a “super-admin” for all EKS clusters in an AWS account
- and in Ansible we will add eksctl create iamidentitymapping (see [Managing IAM users and roles](https://eksctl.io/usage/iam-identity-mappings/)) to add additional users.

Check the _aws-auth_ ConfigMap now:

```
$ eksctl --profile arseniy --region eu-west-2 get iamidentitymapping --cluster eks-dev
ARN USERNAME GROUPS
arn:aws:iam::534***385:role/eksctl-eks-dev-nodegroup-worker-n-NodeInstanceRole-UNGXZXVBL3ZP system:node:{{EC2PrivateDNSName}} system:bootstrappers,system:nodes
```

Or with this:

```
$ kubectl -n kube-system get cm aws-auth -o yaml
apiVersion: v1
data:
mapRoles: |
- groups:
- system:bootstrappers
- system:nodes
rolearn: arn:aws:iam::534***385:role/eksctl-eks-dev-nodegroup-worker-n-NodeInstanceRole-UNGXZXVBL3ZP
username: system:node:{{EC2PrivateDNSName}}
mapUsers: |
[]
kind: ConfigMap
…
```

Let’s do everything manually now to avoid surprised during the automation creation:

1. create an IAM user
2. create an “EKS Admin” policy
3. attach this policy to this user
4. configure local AWS CLI profile with this user
5. configure local kubectl for this AWS CLI profile
6. execute eksctl create iamidentitymapping
7. execute kubectl get nodes to check access

Go ahead — create the user:

```
$ aws --profile arseniy --region eu-west-2 iam create-user --user-name eks-root
{
“User”: {
“Path”: “/”,
“UserName”: “eks-root”,
“UserId”: “AID***PSA”,
“Arn”: “arn:aws:iam::534***385:user/eks-root”,
“CreateDate”: “2020–03–30T12:24:28Z”
}
}
```

His access keys:

```
$ aws --profile arseniy --region eu-west-2 iam create-access-key --user-name eks-root
{
“AccessKey”: {
“UserName”: “eks-root”,
“AccessKeyId”: “AKI****45Y”,
“Status”: “Active”,
“SecretAccessKey”: “Qrr***xIT”,
“CreateDate”: “2020–03–30T12:27:28Z”
}
}
```

Configure local AWS CLI profile:

```
$ aws configure --profile eks-root
AWS Access Key ID [None]: AKI***45Y
AWS Secret Access Key [None]: Qrr***xIT
Default region name [None]: eu-west-2
Default output format [None]: json
```

Try access now — must be declined:

```
$ aws --profile eks-roo eks list-clusters
An error occurred (AccessDeniedException) when calling the ListClusters operation […]
```

Create a policy, here are some [examples>>>](https://docs.aws.amazon.com/eks/latest/userguide/security_iam_id-based-policy-examples.html) (later it can be added to the CloudFormation stack as a nested stack, in the same way, SecurityGroups will be managed), save it in a dedicated file ../../cloudformation/files/eks-root-policy.json:

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "eks:*"
            ],
            "Resource": "*"
        }
    ]
}
```

Add this policy to AWS IAM:

```
$ aws --profile arseniy --region eu-west-2 iam create-policy --policy-name eks-root-policy --policy-document file://../../cloudformation/files/eks-root-policy.json
{
“Policy”: {
“PolicyName”: “eks-root-policy”,
“PolicyId”: “ANPAXY5JMBMEROIOD22GM”,
“Arn”: “arn:aws:iam::534***385:policy/eks-root-policy”,
“Path”: “/”,
“DefaultVersionId”: “v1”,
“AttachmentCount”: 0,
“PermissionsBoundaryUsageCount”: 0,
“IsAttachable”: true,
“CreateDate”: “2020–03–30T12:44:58Z”,
“UpdateDate”: “2020–03–30T12:44:58Z”
}
}
```

Attach the policy to the user created above:

```
$ aws --profile arseniy --region eu-west-2 iam attach-user-policy --user-name eks-root --policy-arn arn:aws:iam::534***385:policy/eks-root-policy
```

Check:

```
$ aws --profile arseniy --region eu-west-2 iam list-attached-user-policies --user-name eks-root --output text
ATTACHEDPOLICIES arn:aws:iam::534***385:policy/eks-root-policy eks-root-policy
```

Try access again:

```
$ aws --profile eks-root --region eu-west-2 eks list-clusters --output text
CLUSTERS eks-dev
```

And in another region:

```
$ aws --profile eks-root --region us-east-2 eks list-clusters --output text
CLUSTERS eksctl-bttrm-eks-production-1
CLUSTERS mobilebackend-dev-eks-0-cluster
```

The next tasks will be better to do from a box with no configured accesses at all, for example — from the RTFM blog’s server :-)

Install the AWS CLI:

```
root@rtfm-do-production:/home/setevoy# pip install awscli
```

Configure a default profile:

```
root@rtfm-do-production:/home/setevoy# aws configure
```

Check access to AWS EKS in general — in our IAM policy we’ve granted permissions to the [eks:ListClusters](https://docs.aws.amazon.com/eks/latest/APIReference/API_ListClusters.html) API-call, so it must be working:

```
root@rtfm-do-production:/home/setevoy# aws eks list-clusters — output text
CLUSTERS eks-dev
```

Okay.

Install the eksctl:

```
root@rtfm-do-production:/home/setevoy# curl --silent --location “https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz” | tar xz -C /tmp
root@rtfm-do-production:/home/setevoy# mv /tmp/eksctl /usr/local/bin
```

Repeat the eks:ListClusters - also must work, as eksctl will use the _default_ AWS CLI profile configured above:

```
root@rtfm-do-production:/home/setevoy# eksctl get cluster
NAME REGION
eks-dev eu-west-2
```

Install kubectl:

```
root@rtfm-do-production:/home/setevoy# curl -LO [https://storage.googleapis.com/kubernetes-release/release/`curl](https://storage.googleapis.com/kubernetes-release/release/%60curl) -s [https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl](https://storage.googleapis.com/kubernetes-release/release/stable.txt%60/bin/linux/amd64/kubectl)

root@rtfm-do-production:/home/setevoy# chmod +x ./kubectl

root@rtfm-do-production:/home/setevoy# mv ./kubectl /usr/local/bin/kubectl
```

Install aws-iam-authenticator to let kubectl perform authentification in AWS:

```
root@rtfm-do-production:/home/setevoy# curl -o aws-iam-authenticator [https://amazon-eks.s3.us-west-2.amazonaws.com/1.15.10/2020-02-22/bin/linux/amd64/aws-iam-authenticator](https://amazon-eks.s3.us-west-2.amazonaws.com/1.15.10/2020-02-22/bin/linux/amd64/aws-iam-authenticator)

root@rtfm-do-production:/home/setevoy# chmod +x ./aws-iam-authenticator

root@rtfm-do-production:/home/setevoy# mv aws-iam-authenticator /usr/local/bin/
```

Configure kubectl:

```
root@rtfm-do-production:/home/setevoy# aws eks update-kubeconfig --name eks-dev
Updated context arn:aws:eks:eu-west-2:534***385:cluster/eks-dev in /root/.kube/config
```

Try to execute a command on the cluster — must be authorization error:

```
root@rtfm-do-production:/home/setevoy# kubectl get nodes
error: You must be logged in to the server (Unauthorized)

root@rtfm-do-production:/home/setevoy# kubectl get pod
error: You must be logged in to the server (Unauthorized)
```

Go back to the working PC and add the _eks-root_ user to the _aws-auth_ ConfigMap:

```
$ eksctl --profile arseniy --region eu-west-2 create iamidentitymapping --cluster eks-dev --arn arn:aws:iam::534***385:user/eks-root --group system:masters --username eks-root
[ℹ] eksctl version 0.16.0
[ℹ] using region eu-west-2
[ℹ] adding identity “arn:aws:iam::534***385:user/eks-root” to auth ConfigMap
```

Check it:

```
$ eksctl --profile arseniy --region eu-west-2 get iamidentitymapping --cluster eks-dev
ARN USERNAME GROUPS
arn:aws:iam::534***385:role/eksctl-eks-dev-nodegroup-worker-n-NodeInstanceRole-UNGXZXVBL3ZP system:node:{{EC2PrivateDNSName}} system:bootstrappers,system:nodes
arn:aws:iam::534***385:user/eks-root eks-root system:masters
```

Go back to the testing host and try access again:

```
root@rtfm-do-production:/home/setevoy# kubectl get node
NAME STATUS ROLES AGE VERSION
ip-10–0–40–30.eu-west-2.compute.internal Ready <none> 133m v1.15.10-eks-bac369
ip-10–0–63–187.eu-west-2.compute.internal Ready <none> 133m v1.15.10-eks-bac369
```

And let’s check something else, the same _aws-auth_ ConfigMap for example:

```
root@rtfm-do-production:/home/setevoy# kubectl -n kube-system get cm aws-auth -o yaml
apiVersion: v1
data:
mapRoles: |
- groups:
- system:bootstrappers
- system:nodes
rolearn: arn:aws:iam::534***385:role/eksctl-eks-dev-nodegroup-worker-n-NodeInstanceRole-UNGXZXVBL3ZP
username: system:node:{{EC2PrivateDNSName}}
mapUsers: |
- groups:- system:masters
userarn: arn:aws:iam::534***385:user/eks-root
username: eks-root
kind: ConfigMap
…
```

The mapUsers with our user added - all good here.

Later we can use this _aws-auth_ to add new users or will add a task to Ansible to execute eksctl create iamidentitymapping, or we can generate our ownConfigMap and upload it to EKS.

I’ll proceed the tasks from our Jenkins host as we have [vim](https://rtfm.co.ua/vim-prevrashhaem-redaktor-v-ide-plaginy-i-vot-eto-vot-vsyo/) available anywhere.

#### Ansible CloudFormation

#### CloudFormation Parameters and Ansible variables

Before starting the following tasks — let’s think about parameters used in the CloudFormation stack from the previous part and what we will need to have in our Ansible.

The first thing came in mind is the act, that we will have _Dev_, _Stage_, _Production_ cluster + dynamic for QA team (to let them an ability to create a dedicated custom cluster from a Jenkins job to test something), so parameters have to be flexible enough to create a stack in any region with any VPC networks.

Thus we will have:

- common parameters for all:
- region
- AWS access/secret (or [IAM EC2 Instance Profile](https://rtfm.co.ua/aws-rotaciya-klyuchej-iam-polzovatelej-ec2-iam-roles-i-jenkins/))
- dedicated parameters for an environment:
- ENV(dev, stage, prod)
- VPC CIDR (10.0.0.0/16, 10.1.0.0/16, etc)
- SSH key for Kubernetes Worker Nodes access
- AWS ЕС2 instances types for Kubernetes Worker Nodes (t3.nano, c5.9xlarge, etc)

In particular, worth to pay attention to the namings of the stacks and clusters.

So, we will have for names which are used “globally” over Ansible playbook and roles and which we will see later in AWS Console:

1. an environment name — _dev_, _stage_, _prod_
2. a CloudFromation stack name created by us from the _cloudformation_ role (a root stack and its nested stacks)
3. a CloudFromation stack name created by the eksctl when creating a cluster and its WorkerNodes stacks
4. and an EKS cluster name

eksctl of course, will do some things behind the scene and will append some prefixes-postfixes to the names here.

For example, when creating a stack for EKS cluster itself — it will append its CloudFormation stack name with the _eksctl-_ prefix and will add a _-cluster_ postfix, and for its WokerNodes stacks — will use prefix _eksctl-_ and postfix _-nodegroup_ + name of the WorkerNodes group from a cluster config-file.

But still, the cluster itself will be named exactly as we set it in the eks-cluster-config.yml.

So, let’s grab altogether — which variables we will use:

- env:
- format: string “_dev_”
- description: used to compose values for other variables
- result: _dev_
- eks_cluster_name:
- format: bttrm-eks-${env}
- description: will set the metadata: name: in the eks-cluster-config.yml cluster-config will be used to compose values for other variables and CloudFormation tags
- result: _bttrm-eks-dev_
- cf_stack_name:
- format: eksctl-${eks_cluster_name}-stack
- description: with the -_stack_ here we are noting that this contains only AWS related resources, no EKS
- result: eksctl-bttrm-eks-dev-stack
- (AUTO) eks_cluster_stack_name:
- format: eksctl-${cluster_name}-cluster
- description: will be created by eksctl itself for a CloudFormation stack, just keeping it in mind
- result: eksctl-bttrm-eks-dev-cluster
- (AUTO) eks_nodegroup_stack_name:
- format: eksctl-${cluster_name}-nodegroup-${worker-nodes-name}, where _worker-nodes-name_ - is a value from the nodeGroups: - name: from the cluster-config eks-cluster-config.yml
- description: will be created by eksctl itself for a CloudFormation stack, just keeping it in mind
- result: eksctl-bttrm-eks-dev-cluster

Regarding different files for different environments — for now, can use common group_vars/all.yml, and later will see how to make them separated.

Create a directory:

```
$ mkdir group_vars
```

Create the all.yml file and set initial values:

```
###################
# ANSIBLE globals #
###################

ansible_connection: local

#####################
# ENV-specific vars #
#####################

env: "dev"

#################
# ROLES globals #
#################

region: "eu-west-2"

# (THIS) eks_cluster_name: used for EKS service to set an exactly cluster's name - "{{ eks_cluster_name }}"
# (AUTO) eks_cluster_stack_name: used for CloudFormation service to format a stack's name as "eksctl-{{ eks_cluster_name }}-cluster"
# (AUTO) eks_nodegroup_stack_name: used for CloudFormation service to format a stack's name as "eksctl-{{ eks_cluster_name }}-nodegroup-{{ worker-nodes-name }}"
eks_cluster_name: "bttrm-eks-{{ env }}"

# used bythe cloudformation role to st a stack's name
cf_stack_name: "eksctl-{{ eks_cluster_name }}-stack"

##################
# ROLES specific #
##################

# cloudforation role
vpc_cidr_block: "10.0.0.0/16"
```

Also, put shared variables here like region - it will be used by the _cloudformation_ and _eksctl_ roles, and vpc_cidr_block - only in the _cloudformation_.

#### The CloudFormation role

After we’ve finished our templates in the [previous part](https://rtfm.co.ua/en/aws-elastic-kubernetes-service-a-cluster-creation-automation-part-1-cloudformation/) we must have the following directories and files structure:

```
$ tree
.
└── roles
├── cloudformation
│ ├── files
│ │ ├── eks-azs-networking.json
│ │ ├── eks-region-networking.json
│ │ └── eks-root.json
│ ├── tasks
│ └── templates
└── eksctl
├── tasks
└── templates
└── eks-cluster-config.yml
```

Now, in the repository root create a new file eks-cluster.yml - this will be our Ansible playbook.

Add the _cloudformation_ execution there:

```
- hosts:
  - all
  become:
    true
  roles:
    - role: cloudformation
      tags: infra
```

Add tags to be able later run roles independently - will be helpful when we will start writing a role for the eksctl.

Create a roles/cloudformation/tasks/main.yml file - add the [cloudformation](https://rtfm.co.ua/ansible-modul-cloudformation/) module execution here and set necessary parameters for our templates via template_parameters - the VPCCIDRBlock from the vpc_cidr_block variable and EKSClusterName parameter from the eks_cluster_name variable:

```
- name: "Create EKS {{ cf_stack_name | upper }} CloudFormation stack"
  cloudformation:
    region: "{{ region }}"
    stack_name: "{{ cf_stack_name }}"
    state: "present"
    disable_rollback: true
    template: "/tmp/packed-eks-stacks.json"
    template_parameters:
      VPCCIDRBlock: "{{ vpc_cidr_block }}"
      EKSClusterName: {{ eks_cluster_name }},
    tags:
      Stack: "{{ cf_stack_name }}"
      Env: "{{ env }}"
      EKS-cluster: "{{ eks_cluster_name }}"
```

Didn’t use Ansible for half of the year — forgot everything(

Google for “_ansible inventory_” — and read the [Best Practices](https://rtfm.co.ua/goto/https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html)

In the repository root, create an ansible.cfg file with some defaults:

```
[defaults]
gather_facts = no
inventory = hosts.yml
```

In the same place create an inventory file — hosts.yml:

```
all:
  hosts:
    "localhost"
```

W era e using _localhost_, here as Ansible will not use SSH anywhere — all tasks will be done locally.

Check syntax:

```
admin@jenkins-production:~/devops-kubernetes$ ansible-playbook eks-cluster.yml — syntax-check
playbook: eks-cluster.yml
```

Looks good? Generate a template’s file (in Jenkins pipeline will have to have an additional stage for this:

```
admin@jenkins-production:~/devops-kubernetes$ cd roles/cloudformation/files/

admin@jenkins-production:~/devops-kubernetes/roles/cloudformation/files$ aws --region eu-west-2 cloudformation package --template-file eks-root.json --output-template /tmp/packed-eks-stacks.json --s3-bucket eks-cloudformation-eu-west-2 --use-json

Successfully packaged artifacts and wrote output template to file /tmp/packed-eks-stacks.json.
```

Execute the following command to deploy the packaged template

```
$ aws cloudformation deploy --template-file /tmp/packed-eks-stacks.json --stack-name <YOUR STACK NAME>
```

Run the stack creation:

```
admin@jenkins-production:~/devops-kubernetes$ ansible-playbook eks-cluster.yml
…
TASK [cloudformation : Setting the Stack name] ****
ok: [localhost]
TASK [cloudformation : Create EKS EKSCTL-BTTRM-EKS-DEV-STACK CloudFormation stack] ****
changed: [localhost]
PLAY RECAP ****
localhost : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
```

Check:

![](https://cdn-images-1.medium.com/max/1024/0*fF4DzSOcR-MskC25.png)

The stack’s name was set as _eksctl-bttrm-eks-dev-stack_ — nice.

Now, can start writing a role for the eksctl to create a cluster itself.

#### Ansible eksctl

We already have the cluster config file — let’s rename it to the .j2 (to note it as a Jinja's file - Ansible templating engine):

```
admin@jenkins-production:~/devops-kubernetes$ mv roles/eksctl/templates/eks-cluster-config.yml roles/eksctl/templates/eks-cluster-config.yml.j2
```

From the CloudFormation stack created in the first role we need to pass a bunch of values here — VPC ID, subnets IDs, etc, to set values in the cluster config for eksctl.

Let’s use Ansible module [cloudformation_info](https://rtfm.co.ua/goto/https://docs.ansible.com/ansible/latest/modules/cloudformation_info_module.html) to collect information about the stack created.

Add it, save its output to the stack_info variable and then will check its content.

Create a new file roles/eksctl/tasks/main.yml:

```
- cloudformation_info:
    region: "{{ region }}"
    stack_name: "cf_stack_name"
  register: stack_info

- debug:
    msg: "{{ stack_info }}"
```

Add the eksctl role execution to the eks-cluster.yml playbook, add tags:

```
- hosts:
  - all
  become:
    true
  roles:
    - role: cloudformation
      tags: infra
    - role: eksctl
      tags: eks
```

Run using --tags eks to execute the eksctl role only:

```
admin@jenkins-production:~/devops-kubernetes$ ansible-playbook — tags eks eks-cluster.yml
…
TASK [eksctl : cloudformation_info] ****
ok: [localhost]
TASK [eksctl : debug] ****
ok: [localhost] => {
“msg”: {
“changed”: false,
“cloudformation”: {
“eksctl-bttrm-eks-dev-stack”: {
“stack_description”: {
…
“outputs”: [
{
“description”: “EKS VPC ID”,
“output_key”: “VPCID”,
“output_value”: “vpc-042082cd2d011f44d”
},
…
```

Cool.

Now — let’s cut the { "outputs" } block only.

Update the task — from the stack_info into msg pass only the stack_outputs:

```
- cloudformation_info:
    region: "{{ region }}"
    stack_name: "{{ cf_stack_name }}"
  register: stack_info

- debug:
    msg: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs }}"
```

Run:

```
admin@jenkins-production:~/devops-kubernetes$ ansible-playbook — tags eks eks-cluster.yml
…
TASK [eksctl : debug] ****
ok: [localhost] => {
“msg”: {
“APrivateSubnetID”: “subnet-0471e7c28a3770828”,
“APublicSubnetID”: “subnet-07a0259b33ddbcb4c”,
“AStackAZ”: “eu-west-2a”,
“BPrivateSubnetID”: “subnet-0fa6eece43b2b6644”,
“BPublicSubnetID”: “subnet-072c107cef77fe859”,
“BStackAZ”: “eu-west-2b”,
“VPCID”: “vpc-042082cd2d011f44d”
}
}
…
```

Niiice!

And now — let’s try to set those values as variables to use them to generate the cluster config from its template.

Update the roles/eksctl/tasks/main.yml, add a vpc_id variable and print it via debug to check its value:

```
- cloudformation_info:
    region: "{{ region }}"
    stack_name: "{{ cf_stack_name }}"
  register: stack_info

- debug:
    msg: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs }}"

- set_fact:
    vpc_id: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.VPCID }}"

- debug:
    msg: "{{ vpc_id }}"

- name: "Check template's content"
  debug:
    msg: "{{ lookup('template', './eks-cluster-config.yml.j2') }}"
```

In the «_Check the template content_» by direct call of the [template](https://rtfm.co.ua/goto/https://docs.ansible.com/ansible/latest/modules/template_module.html) module let's check what we will have as a resulted file from the template file.

Edit the roles/eksctl/templates/eks-cluster-config.yml.j2 - set the `{{ vpc_id }}`:

```
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: eks-dev
  region: eu-west-2
  version: "1.15"
nodeGroups:
  - name: worker-nodes
    instanceType: t3.medium
    desiredCapacity: 2
    privateNetworking: true
vpc:
  id: "{{ vpc_id }}"
  subnets:
    public:
      eu-west-2a:
...
```

Check:

```
admin@jenkins-production:~/devops-kubernetes$ ansible-playbook — tags eks eks-cluster.yml
…
TASK [eksctl : debug] ****
ok: [localhost] => {
“msg”: {
“APrivateSubnetID”: “subnet-0471e7c28a3770828”,
“APublicSubnetID”: “subnet-07a0259b33ddbcb4c”,
“AStackAZ”: “eu-west-2a”,
“BPrivateSubnetID”: “subnet-0fa6eece43b2b6644”,
“BPublicSubnetID”: “subnet-072c107cef77fe859”,
“BStackAZ”: “eu-west-2b”,
“VPCID”: “vpc-042082cd2d011f44d”
}
}
TASK [eksctl : set_fact] *****ok: [localhost]
TASK [eksctl : debug] ****
ok: [localhost] => {
“msg”: “vpc-042082cd2d011f44d”
}
TASK [eksctl : Check the template’s content] ****
ok: [localhost] => {
“msg”: “apiVersion: eksctl.io/v1alpha5\nkind: ClusterConfig\nmetadata:\n name: eks-dev\n region: eu-west-2\n version: \”1.15\”\nnodeGroups:\n — name: worker-nodes\n instanceType: t3.medium\n desiredCapacity: 2\n privateNetworking: true\nvpc:\n id: \”vpc-042082cd2d011f44d\”\n […]
```

_vpc:\n id: \”vpc-vpc-042082cd2d011f44d\”_ — good, we got our VPC ID in the template.

Add other varialbes, so the template will look like:

```
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: "{{ eks_cluster_name }}"
  region: "{{ region }}"
  version: "{{ k8s_version }}"
nodeGroups:
  - name: "{{ k8s_worker_nodes_group_name }}"
    instanceType: "{{ k8s_worker_nodes_instance_type }}"
    desiredCapacity: {{ k8s_worker_nodes_capacity }}
    privateNetworking: true
vpc:
  id: "{{ vpc_id }}"
  subnets:
    public:
      {{ a_stack_az }}:
        id: "{{ a_stack_pub_subnet }}"
      {{ b_stack_az }}:
        id: "{{ b_stack_pub_subnet }}"
    private:
      {{ a_stack_az }}:
        id: "{{ a_stack_priv_subnet }}"
      {{ b_stack_az }}:
        id: "{{ b_stack_priv_subnet }}"
  nat:
    gateway: Disable
cloudWatch:
  clusterLogging:
    enableTypes: ["*"]
```

Next, need to add additional variables for Ansible to be used in this template:

- _eks_cluster_name _— is already added
- _vpc_id_ — is already added
- for the AvailablityZone-А stack need to add:
- _a_stack_pub_subnet_
- _a_stack_priv_subnet_
- for the AvailablityZone-В need to add:
- _a_stack_pub_subnet_
- _a_stack_priv_subnet_
- _region_ — is already added, will be set later from a Jenkins parameters
- _kubernetes_version_
- _kubernetes_worker_nodes_group_name_
- _kubernetes_worker_nodes_instance_type_
- _kubernetes_worker_nodes_capacity_

Add set_facts in the roles/eksctl/tasks/main.yml:

```
- cloudformation_info:
    region: "{{ region }}"
    stack_name: "{{ cf_stack_name }}"
  register: stack_info

- debug:
    msg: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs }}"

- set_fact:
    vpc_id: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.VPCID }}"
    a_stack_az: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.AStackAZ }}"
    a_stack_pub_subnet: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.APublicSubnetID }}"
    a_stack_priv_subnet: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.APrivateSubnetID }}"
    b_stack_az: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.BStackAZ }}"
    b_stack_pub_subnet: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.BPublicSubnetID }}"
    b_stack_priv_subnet: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.BPrivateSubnetID }}"

- name: "Check the template's content"
  debug:
    msg: "{{ lookup('template', './eks-cluster-config.yml.j2') }}"
```

In the group_vars/all.yml add values for the _eksctl_ role:

```
...
##################
# ROLES specific #
##################

# cloudforation role
vpc_cidr_block: "10.0.0.0/16"

# eksctl role
k8s_version: 1.15
k8s_worker_nodes_group_name: "worker-nodes"
k8s_worker_nodes_instance_type: "t3.medium"
k8s_worker_nodes_capacity: 2
```

Let’s check — for now in the way, by calling the template directory:

```
…
TASK [eksctl : Check the template’s content] ****
ok: [localhost] => {
“msg”: “apiVersion: eksctl.io/v1alpha5\nkind: ClusterConfig\nmetadata:\n name: \”bttrm-eks-dev\”\n region: \”eu-west-2\”\n version: \”1.15\”\nnodeGroups:\n — name: \”worker-nodes\”\n instanceType: \”t3.medium\”\n desiredCapacity: 2\n privateNetworking: true\nvpc:\n id: \”vpc-042082cd2d011f44d\”\n subnets:\n public:\n eu-west-2a:\n id: \”subnet-07a0259b33ddbcb4c\”\n eu-west-2b:\n id: \”subnet-072c107cef77fe859\”\n private:\n eu-west-2a:\n id: \”subnet-0471e7c28a3770828\”\n eu-west-2b:\n id: \”subnet-0fa6eece43b2b6644\”\n nat:\n gateway: Disable\ncloudWatch:\n clusterLogging:\n enableTypes: [\”\*\”]\n”
}
…
```

“It works!” ©.

Well — not it’s time to run the eksctl.

#### Cluster config — eks-cluster-config.yml

Before calling eksctl need to generate the config from the template.

At the end of the roles/eksctl/tasks/main.yml add template, and by using the role/eksctl/templates/eks-cluster-config.yml.j2 generate a eks-cluster-config.yml file in the /tmp directory:

```
...
- name: "Generate eks-cluster-config.yml"
  template:
    src: "eks-cluster-config.yml.j2"
    dest: /tmp/eks-cluster-config.yml
```

#### eksctl _create_ vs _update_

_A note: actually, the update for eksctl will perform a version upgrade instead of configuration update, but I’ll leave this part here just for example_

By the way, what if a cluster already exists? A job will fail, as we are passing the create.

Well — we can add a check if a cluster already present and then create a variable to store a value _create_ or _update_, similarly as I did it in the [BASH: скрипт создания AWS CloudFormation стека](https://rtfm.co.ua/bash-skript-sozdaniya-aws-cloudformation-steka/).

Thus, need to:

1. get a list of the EKS clusters in an AWS region
2. try to find the name of the cluster being created in this list
3. if **not** found then set value _create_
4. if found then set value _update_

Let’s try to do it in this way:

```
# populate a clusters_exist list with names of clusters devided by TAB "\t"
- name: "Getting existing clusters list"
  shell: "aws --region {{ region }} eks list-clusters --query '[clusters'] --output text"
  register: clusters_exist

- debug:
    msg: "{{ clusters_exist.stdout }}"

# create a list from the clusters_exist
- set_fact:
    found_clusters_list: "{{ clusters\_exist.stdout.split('\t') }}"

- debug:
    msg: "{{ found_clusters_list }}"

# check if a cluster's name is found in the existing clusters list
- fail:
    msg: "{{ eks_cluster_name }} already exist in the {{ region }}"
  when: eks_cluster_name not in found_clusters_list

- meta: end_play
...
```

Those tasks during testing add to the beginning of the roles/eksctl/tasks/main.yml, and to stop execution after them - add the - meta: end_play.

In the fail condition now set " **not in**" as we are just testing now and have no cluster created yet:

```
…
TASK [eksctl : debug] ****
ok: [localhost] => {
“msg”: “eks-dev”
}
TASK [eksctl : set_fact] ****
ok: [localhost]
TASK [eksctl : debug] ****
ok: [localhost] => {
“msg”: [
“eks-dev”
]
}
TASK [eksctl : fail] ****
fatal: [localhost]: FAILED! => {“changed”: false, “msg”: “bttrm-eks-dev already exist in the eu-west-2”}
…
```

Cool.

Now, add an eksctl_action variable with the "_create_" or "_update_", depending on was the cluster found or not:

```
# populate a clusters_exist list with names of clusters devided by TAB "\t"
- name: "Getting existing clusters list"
  shell: "aws --region {{ region }} eks list-clusters --query '[clusters'] --output text"
  register: clusters_exist

- debug:
    msg: "{{ clusters_exist.stdout }}"

# create a list from the clusters_exist
- set_fact:
    found_clusters_list: "{{ clusters_exist.stdout.split('\t') }}"

- debug:
    msg: "{{ found_clusters_list }}"

- set_fact:
    eksctl_action: "{{ 'create' if (eks_cluster_name not in found_clusters_list) else 'update' }}"

- debug:
    var: eksctl_action

# check if a cluster's name is found in the existing clusters list
- fail:
    msg: "{{ eks_cluster_name }} already exist in the {{ region }}"
  when: eks_cluster_name not in found_clusters_list

...
```

Check:

```
…
TASK [eksctl : debug] ****
ok: [localhost] => {
“eksctl_action”: “create”
}
…
```

Next, add this to the eksctltask - add the `{{ eksctl_action }}` to its arguments instead of direct "_create_":

```
- cloudformation_info:
    region: "{{ region }}"
    stack_name: "{{ cf_stack_name }}"
  register: stack_info

- set_fact:
    vpc_id: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.VPCID }}"
    a_stack_az: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.AStackAZ }}"
    a_stack_pub_subnet: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.APublicSubnetID }}"
    a_stack_priv_subnet: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.APrivateSubnetID }}"
    b_stack_az: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.BStackAZ }}"
    b_stack_pub_subnet: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.BPublicSubnetID }}"
    b_stack_priv_subnet: "{{ stack_info.cloudformation[cf_stack_name].stack_outputs.BPrivateSubnetID }}"
 
- name: "Generate eks-cluster-config.yml"
  template:
    src: "eks-cluster-config.yml.j2"
    dest: /tmp/eks-cluster-config.yml

# populate a clusters_exist list with names of clusters devided by TAB "\t"
- name: "Getting existing clusters list"
  command: "aws --region {{ region }} eks list-clusters --query '[clusters'] --output text"
  register: clusters_exist

# create a list from the clusters_exist
- set_fact:
    found_clusters_list: "{{ clusters_exist.stdout.split('\t') }}"

- name: "Setting eksctl action to either Create or Update"
  set_fact:
    eksctl_action: "{{ 'create' if (eks_cluster_name not in found_clusters_list) else 'update' }}"

- name: "Running eksctl eksctl_action {{ eksctl_action | upper }} cluster with name {{ eks_cluster_name | upper }}"
  command: "eksctl {{ eksctl_action }} cluster -f /tmp/eks-cluster-config.yml"
```

The moment of truth! :-)

Run:

```
admin@jenkins-production:~/devops-kubernetes$ ansible-playbook — tags eks eks-cluster.yml
…
TASK [eksctl : cloudformation_info] ****
ok: [localhost]
TASK [eksctl : set_fact] ****
ok: [localhost]
TASK [eksctl : Generate eks-cluster-config.yml] ****
ok: [localhost]
TASK [eksctl : Getting existing clusters list] ****
changed: [localhost]
TASK [eksctl : set_fact] ****
ok: [localhost]
TASK [eksctl : Setting eksctl action to either Create or Update] ****
ok: [localhost]
TASK [eksctl : Running eksctl eksctl_action CREATE cluster with name BTTRM-EKS-DEV]
```

The stack and cluster are creating, the stack’s name is _eksctl-bttrm-dev-cluster_ — everything as we planned:

![](https://cdn-images-1.medium.com/max/1024/0*2112O0MXICSoM0G7.png)

Hurray!

Left just one more test.

#### TEST: CloudFormation && EKS Custom parameters

The latest thing to test is the flexibility of everything we wrote.

For example, an QA team wants to create a brand new cluster named _qa-test_ with VPC CIDR _10.1.0.0/16_.

Update the group_vars/all.yml, set:

- env: "_dev_" > "_qa-test"_
- region: "_eu-west-2_" > "_eu-west-3_"
- vpc_cidr_block: "_10.0.0.0/16_" > "_10.1.0.0/16_"

```
...
env: "qa-test"
...
region: "eu-west-3"
...
vpc_cidr_block: "10.1.0.0/16"
```

Re-pack the template to re-geneteare the packed-eks-stack.json:

```
admin@jenkins-production:~/devops-kubernetes$ cd roles/cloudformation/files/

admin@jenkins-production:~/devops-kubernetes/roles/cloudformation/files$ aws — region eu-west-3 cloudformation package — template-file eks-root.json — output-template packed-eks-stacks.json — s3-bucket eks-cloudformation-eu-west-3 — use-json
```

Run Ansible with no --tags to create CloudFormation stacks and EKS cluster from scratch:

```
admin@jenkins-production:~/devops-kubernetes$ ansible-playbook eks-cluster.yml
…
TASK [cloudformation : Create EKS EKSCTL-BTTRM-EKS-QA-TEST-STACK CloudFormation stack] ****
changed: [localhost]
TASK [eksctl : cloudformation_info] ****
ok: [localhost]
TASK [eksctl : set_fact] ****
ok: [localhost]
TASK [eksctl : Generate eks-cluster-config.yml] ****
changed: [localhost]
TASK [eksctl : Getting existing clusters list] ****
changed: [localhost]
TASK [eksctl : set_fact] ****
ok: [localhost]
TASK [eksctl : Setting eksctl action to either Create or Update] ****
ok: [localhost]
TASK [eksctl : Running eksctl eksctl_action CREATE cluster with name BTTRM-EKS-QA-TEST] ****
…
```

Wait, check:

![](https://cdn-images-1.medium.com/max/1024/0*59-SvG_27Sep0lMX.png)

Yay!

Actually — that’s all.

The final thing will be to create a Jenkins-job.

Oh, wait! Forgot about ConfigMap.

#### P.S. aws-auth ConfigMap

No matter how hard you try — you’ll forget something.

So — need to add the _eks-root_, user created at the very beginning of this post to the cluster we are creating.

It can be done with one command and two variables.

_A note: actually, this will append the user each time when provisioning is running, so in my final solution I did it in a bit another way, but for now I’m leaving it here “as is” — will add a better way later._

In the group_vars/all.yml add a user's ARN:

```
...
eks_root_user_name: "eks-root"
eks_root_user_arn: "arn:aws:iam::534***385:user/eks-root"
```

And in the roles/eksctl/tasks/main.yml - add him to the cluster:

```
...
- name: "Update aws-auth ConfigMap with the EKS root user {{ eks_root_user_name | upper }}"
  command: "eksctl create iamidentitymapping --cluster {{ eks_cluster_name }} --arn {{ eks_root_user_arn }} --group system:masters --username {{ eks_root_user_name }}"
```

Run:

```
…
TASK [eksctl : Running eksctl eksctl_action UPDATE cluster with name BTTRM-EKS-QA-TEST] ****
changed: [localhost]
TASK [eksctl : Update aws-auth ConfigMap with the EKS root user EKS-ROOT] ****
changed: [localhost]
…
```

Go back to the RTFM’s server where we have “clean” access for only the _eks-root_ user, update its ~/.kube/config:

```
root@rtfm-do-production:/home/setevoy# aws — region eu-west-3 eks update-kubeconfig — name bttrm-eks-qa-test
Added new context arn:aws:eks:eu-west-3:53***385:cluster/bttrm-eks-qa-test to /root/.kube/config
```

Try to access nodes info:

```
root@rtfm-do-production:/home/setevoy# kubectl get node
NAME STATUS ROLES AGE VERSION
ip-10–1–40–182.eu-west-3.compute.internal Ready <none> 94m v1.15.10-eks-bac369
ip-10–1–52–14.eu-west-3.compute.internal Ready <none> 94m v1.15.10-eks-bac369
```

And pods:

```
root@rtfm-do-production:/home/setevoy# kubectl get pod -n kube-system
NAME READY STATUS RESTARTS AGE
aws-node-rgpn5 1/1 Running 0 95m
aws-node-xtr6m 1/1 Running 0 95m
coredns-7ddddf5cc7–5w5wt 1/1 Running 0 102m
…
```

Done.

#### Useful links

#### Kubernetes

- [Introduction to Kubernetes Pod Networking](https://rtfm.co.ua/goto/https://www.weave.works/blog/introduction-to-kubernetes-pod-networking--part-1)
- [Kubernetes on AWS: Tutorial and Best Practices for Deployment](https://rtfm.co.ua/goto/https://www.weave.works/technologies/kubernetes-on-aws/)
- [How to Manage Kubernetes With Kubectl](https://rtfm.co.ua/goto/https://rancher.com/learning-paths/how-to-manage-kubernetes-with-kubectl/)
- [Building large clusters](https://rtfm.co.ua/goto/https://kubernetes.io/docs/setup/best-practices/cluster-large/)
- [Kubernetes production best practices](https://rtfm.co.ua/goto/https://learnk8s.io/production-best-practices)

#### Ansible

- [Best Practices](https://rtfm.co.ua/goto/https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html)
- [Ansible & CloudFormation for Complex Workflows](https://rtfm.co.ua/goto/https://www.trek10.com/blog/ansible-cloudformation/)

#### AWS

EKS

- [kubernetes cluster on AWS EKS](https://rtfm.co.ua/goto/https://www.codementor.io/@slavko/kubernetes-cluster-on-aws-eks-lecygk6rl)
- [EKS vs GKE vs AKS — Evaluating Kubernetes in the Cloud](https://rtfm.co.ua/goto/https://www.stackrox.com/post/2020/02/eks-vs-gke-vs-aks/)
- [Modular and Scalable Amazon EKS Architecture](https://rtfm.co.ua/goto/https://s3.amazonaws.com/aws-quickstart/quickstart-amazon-eks/doc/amazon-eks-architecture.pdf)
- [Build a kubernetes cluster with eksctl](https://rtfm.co.ua/goto/https://www.agilepartner.net/en/build-a-kubernetes-cluster-with-eksctl/)
- [Amazon EKS Security Group Considerations](https://rtfm.co.ua/goto/https://docs.aws.amazon.com/en_us/eks/latest/userguide/sec-group-reqs.html)

CloudFormation

- [Managing AWS Infrastructure as Code using Ansible, CloudFormation, and CodeBuild](https://rtfm.co.ua/goto/https://programmaticponderings.com/2019/07/30/managing-aws-infrastructure-as-code-using-ansible-cloudformation-and-codebuild/)
- [Nested CloudFormation Stack: a guide for developers and system administrators](https://rtfm.co.ua/goto/https://cloudacademy.com/blog/understanding-nested-cloudformation-stacks/)
- [Walkthrough with Nested CloudFormation Stacks](https://rtfm.co.ua/goto/https://medium.com/@janethavishka/walkthrough-with-nested-cloudformation-stacks-d7709b568162)
- [How do I pass CommaDelimitedList parameters to nested stacks in AWS CloudFormation?](https://rtfm.co.ua/goto/https://aws.amazon.com/ru/premiumsupport/knowledge-center/cloudformation-parameters-nested-stacks/)
- [How do I use multiple values for individual parameters in an AWS CloudFormation template?](https://rtfm.co.ua/goto/https://aws.amazon.com/ru/premiumsupport/knowledge-center/multiple-values-list-parameter-cli/)
- [Two years with CloudFormation: lessons learned](https://rtfm.co.ua/goto/https://sanderknape.com/2018/08/two-years-with-cloudformation-lessons-learned/)
- [Shrinking Bloated CloudFormation Templates With Nested Stack](https://rtfm.co.ua/goto/https://www.jamesqmurphy.com/blog/2019/10/nested-stack)
- [CloudFormation Best-Practices](https://rtfm.co.ua/goto/https://sbstjn.com/cloudformation.html#nested-stacks)
- [7 Awesome CloudFormation Hacks](https://rtfm.co.ua/goto/https://garbe.io/blog/2017/07/17/cloudformation-hacks/)
- [AWS CloudFormation Best Practices — Certification](https://rtfm.co.ua/goto/https://jayendrapatil.com/aws-cloudformation-best-practices-certification/)
- [Defining Resource Properties Conditionally Using AWS::NoValue on CloudFormation](https://rtfm.co.ua/goto/https://blog.shikisoft.com/aws-cloudformation-no-value-pseudo-parameter/)

_Originally published at_ [_RTFM: Linux, DevOps и системное администрирование_](https://rtfm.co.ua/en/aws-elastic-kubernetes-service-a-cluster-creation-automation-part-2-ansible-eksctl/)_._

* * *