Книга: Practical GitOps
Назад: 3.9 Clean-Up
Дальше: 9.2.2 Encrypting EKS EBS

database using the AWS RDS offering.

Reference:

283

Chapter 7 Spring Boot app on aWS eKS

Figure 7-6. PostgreSQL using AWS RDS

The preceding diagram shows the implementation of the code defined

in the pgitops\infra\rds.tf, which is specifying the following:

A standalone PostgreSQL database with configurable

versions, size, family, storage, etc., in the database

subnet that was created earlier in the networking

section.

The module generates a 16-digit random password for

the database and is stored in the AWS SSM store.

The database connection URL is also stored in the AWS

SSM store.

7.4.1 Database Module

Let’s look at the infra/rds.tf file to better understand how our database has been set up.

284

Chapter 7 Spring Boot app on aWS eKS

Code Block 7-11. Defining the RDS database in Terraform

File: pgitops\infra\rds.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

39: module "pgsql" {

40:

41: source = "terraform-aws-modules/rds/aws"

42: version = "4.2.0"

43:

44: identifier = " ${module.clustername.

cluster_name}-pgsql"

45: engine = var.db_engine

46: engine_version = var.db_engine_version

47: family = var.db_engine_family

48: major_engine_version = var.db_major_

engine_version

52: storage_encrypted = true

53: db_name = var.db_name

54: username = var.db_user_name

55: create_random_password = true

56: random_password_length = 16

61: vpc_security_group_ids = [module.pgsql_sg_

ingress.id]

70: parameters = [

71: {

72: name = "client_encoding"

73: value = "utf8"

74: }

75: ]

84: }

285

Chapter 7 Spring Boot app on aWS eKS

Note i’ve omitted some fields and focused only on the important

ones for brevity.

L41–42: We are informing terraform to load the module from the

official terraform registry with a fixed version.

L44: Declaring the identifier with the clustername format as discussed

earlier.

L45–48: Specifying the details about the PostgreSQL database, its

version, engine, family, etc.

L55–56: Generating the random password with a length of 16

characters.

L61: Referring to the security group we created in the networking.tf file for PostgreSQL database connectivity. It is necessary to specify the security

group here in order to facilitate database connectivity.

L70–75: Specifying the database configuration parameters.

7.4.2 Database Configuration and Secrets

It is critically important to avoid storage of sensitive information like

database passwords, which is what we’ll understand next to avoid storing

credentials in static files. The following code shows how we can retrieve

the password from terraform configuration and store it in an AWS SSM

Parameter Store as a SecureString.

Code Block 7-12. Storing DB Creds in AWS SSM

File: pgitops\infra\rds.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

086: # Write the DB random password in SSM

087: module "ssmw-db-password" {

286

Chapter 7 Spring Boot app on aWS eKS

088: source = "./modules/ssmw"

089: parameter_name = "db_password"

090: parameter_path = "/${var.org_name}/database"

091: parameter_value = module.pgsql.db_instance_

password

092: parameter_description = "DB Password"

093: clustername = module.clustername.cluster_name

094: parameter_type = "SecureString"

095: environment = var.environment

096: }

097:

098: # Write the DB end point URL in SSM

099: module "ssmw-db-endpoint" {

100: source = "./modules/ssmw"

101:

102: parameter_name = "db_endpoint_url"

103: parameter_path = "/${var.org_name}/database"

104: parameter_value = module.pgsql.db_instance_address

105: parameter_description = "Endpoint URL of postgresql"

106: clustername = module.clustername.cluster_name

107: parameter_type = "String"

108: environment = var.environment

109: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

L87–96: We are extracting the random password created in the

database module referred to as module.pgsql.db_instance_password

and writing it as a SecureString SSM parameter at the path “$org_name/

database/db_password”. SecureString holds the data in an encrypted

format using the default AWS KMS key. We can, however, specify our own

KMS generated key to store the data, enhancing the security.

287

Chapter 7 Spring Boot app on aWS eKS

Note though we are encrypting the string in aWS SSM store, the

value of the password is still stored in the terraform state file in

clear-text format. anyone having access to the state file can derive

the password quite easily. hence, as a defense-in-depth measure, it

is necessary to restrict access to state files. We can, however, avoid

this by first creating the password manually in the aWS SSM store. in

Chapter , we shall explore how we can use aWS Secret Manager to store the password.

L99–109: Similar to storing of the password in the AWS SSM store, we

are storing the database connection URL in the AWS SSM store as well so

that any application wishes to access the database can access it by simply

viewing the SSM path “$org_name/database/db_endpoint_url”.

7.5 Kubernetes

Kubernetes is one of the most popular Container Orchestration products

and is also one of the key components for our GitOps cycle. AWS has an

offering called EKS (Elastic Kubernetes Service), which is a managed

Kubernetes service wherein AWS is responsible for all the control plane

components (etcd, apiserver, scheduler, controller) and the user is

responsible for all the data plane components (worker nodes).

References:

AWS EKS

Kubernetes Components

288

Chapter 7 Spring Boot app on aWS eKS

Figure 7-7. AWS EKS architecture

We’ll be setting up a two-node Kubernetes cluster using the code

defined in the pgitops\infra\eks.tf file utilizing the following features:

A master node that is totally abstracted from us and

deployed in a separate AWS VPC.

Two worker nodes, namely, app and system, both

deployed in the private subnet. The worker nodes are

nothing but EC2 instances with Kubernetes setup

deployed.

In terms of EKS, there are two types of worker nodes:

managed and unmanaged. Managed worker nodes are

where AWS takes full control and responsibility to

update and scale up the resources and align the worker

nodes with the master node. In unmanaged, it is our

responsibility to create the nodes, set up Kubernetes

dependencies, scale up/down, and authenticate to the

master node. This is generally achieved by creating EC2

templates.

289

Chapter 7 Spring Boot app on aWS eKS

All the network plumbing/communication between the

master node and the worker nodes is done

internally by AWS.

Once our EKS cluster is deployed and ready, we then

create an AWS ALB (Application Load Balancer) in the

public subnet, which is used as an ingress controller,

which routes traffic to the ingress services that will be

created in the EKS cluster.

7.5.1 EKS

Let’s look at the infra/eks.tf file to better understand how our EKS cluster has been set up.

Code Block 7-13. EKS server setup in Terraform

File: pgitops\infra\eks.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

15: # Main module that creates the master plane components

16: module "eks" {

17: source = "./modules/ekscluster"

18:

19: clustername = module.clustername.cluster_name

20: eks_version = var.eks_version

21: private_subnets = module.networking.private_subnets_id

22: vpc_id = module.networking.vpc_id

23: environment = var.environment

24: instance_types = var.instance_types

25:

26: }

290

Chapter 7 Spring Boot app on aWS eKS

L15–26: Initiates a module that creates the EKS cluster with master

node and two worker nodes. We need to feed in the EKS version, the

private subnet where the worker nodes will be spun up.

7.5.2 EKS Module

Let’s dig in a bit into the module source to find out what’s happening.

Detailed implementation can be found here.

Code Block 7-14. EKS Module definition

File: pgitops\code\infra\modules\ekscluster\main.tf

02: module "eks" {

03: source = "terraform-aws-modules/eks/aws"

04: version = "18.7.0"

05: cluster_name = var.clustername

06: cluster_version = var.eks_version

07: subnet_ids = var.private_subnets

08: vpc_id = var.vpc_id

09: enable_irsa = true

10:

11: cluster_addons = {

12: coredns = {

13: resolve_conflicts = "OVERWRITE"

14: }

15: vpc-cni = {

16: resolve_conflicts = "OVERWRITE"

17: }

18: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

291

Chapter 7 Spring Boot app on aWS eKS

L03: Refers the module in Terraform registry with a specific version

number. This module provided by Terraform follows the best practices

for setting up a managed/unmanaged EKS cluster and hence is highly

recommended to use. However, there are very frequent changes to the

minor/major versions, so it is also recommended to keep a tab on the

version.

L06–08: Provides input for the specific EKS version, the private

subnets, and the VPC where the EKS cluster needs to be placed.

L09: Enables IRSA (IAM Roles for Service Accounts), using which,

we can provide granular privileges to Kubernetes PODs by aligning their

services accounts to AWS IAM Roles. This is a very important security

feature, which we’ll explore in more detail in the next chapter.

L11–18: Here, we can specify installation/configuration of Kubernetes

add-ons. List of all add-ons supported by AWS EKS off the shelf

.

7.5.3 EKS Worker Nodes

One important aspect about EKS is that AWS creates only the master node

or the control plane component of Kubernetes. The worker nodes needed

to be created by us. The module that we are using allows us to specify the

worker node groups that need to be created within which multiple worker

nodes can be created as per the scaling configuration.

Let’s look at how we are setting up the Managed worker node groups as

shown here.

Code Block 7-15. EKS Managed Node groups

File: pgitops\code\infra\modules\ekscluster\main.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

59: eks_managed_node_groups = {

60:

292

Chapter 7 Spring Boot app on aWS eKS

61: system = {

62: name = "system"

63: use_name_prefix = true

64:

65: tags = {

66: Name = "system"

67: Environment = var.environment

68: terraform-managed = "true"

69: }

70: },

71: app = {

72: name = "app"

73: use_name_prefix = true

74:

75: tags = {

76: Name = "app"

77: Environment = var.environment

78: terraform-managed = "true"

79: }

80: }

81: }

82:

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

L61–81: Declaring the nodes with appropriate tags. Using “user_

name_prefix” helps in better identification of the nodes.

293

Chapter 7 Spring Boot app on aWS eKS

Code Block 7-16. EKS Managed Node Groups default settings

File: pgitops\code\infra\modules\ekscluster\main.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

44: eks_managed_node_group_defaults = {

45: ami_type = "AL2_x86_64"

46: disk_size = 50

47: ebs_optimized = true

48: enable_monitoring = true

49: instance_types = var.instance_types

50: capacity_type = "ON_DEMAND"

51: desired_size = 1

52: max_size = 3

53: min_size = 1

54: update_config = {

55: max_unavailable_percentage = 50

56: }

57: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

L44–57: We are declaring some common properties of each worker

node. Each of these properties can be overridden when we are exclusively

defining the node groups on L59–81.

We are defining the following:

L45 – AMI Type, which is the default AMI for

Kubernetes resources

L46 – Specifying the EBS Disk Size that will be attached

to the EC2 instance to be 50 GB

L48 – Enabled monitoring

294

Chapter 7 Spring Boot app on aWS eKS

L49 – Specifying the instance type of the EC2

L51–53 – Specifying the scaling sizes

L54–56 – Specifying at least 50% availability while

scaling up/down

7.5.4 EKS Security Groups

The next section of this EKS Module defines the security groups needed for

operational purposes.

Code Block 7-17. EKS Security Groups definitions

File: pgitops\code\infra\modules\ekscluster\main.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

21: node_security_group_additional_rules = {

22:

23: ingress_allow_access_from_control_plane = {

24: type = "ingress"

25: protocol = "tcp"

26: from_port = 9443

27: to_port = 9443

28: source_cluster_security_group = true

29: description = "Allow access from

control plane to

webhook port of

AWS load balancer

controller"

30: }

31:

32: egress_all = {

295

Chapter 7 Spring Boot app on aWS eKS

33: description = "Node all egress"

34: protocol = "-1"

35: from_port = 0

36: to_port = 0

37: type = "egress"

38: cidr_blocks = ["0.0.0.0/0"]

39: ipv6_cidr_blocks = ["::/0"]

40: }

41: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

The EKS Module provided by Terraform as mentioned earlier follows

the best practices; hence, in terms of accessibility, it’s quite locked down.

We need to specify security groups in order to facilitate communication

between master node and worker nodes, which is by default limited and

locked down to certain ports only like communication between the API

server and kubelet over port 10250, etc. These security groups are what we

need to define in the preceding section.

Figure 7-8. EKS Security Groups visualization

296

Chapter 7 Spring Boot app on aWS eKS

L23–30: Here, we are defining the security group that allows access

from the master node (AWS EKS Control Plane) in an AWS Managed VPC

to the worker nodes (EKS Data Plane) on port 9443. What’s running on

port 9443 is something we’ll see in the next subsection.

L32–41: Since the worker nodes are created in the private subnet,

they do not have access to the Internet. Hence, it’s necessary to set up this

default egress group to allow the worker nodes to access the Internet to

download docker images or perform DNS lookups. The protocol with a

value of “-1” indicates TCP/UDP to facilitate DNS lookups.

7.5.5 EKS Ingress Controller

In Kubernetes, ingress controllers and ingress resources help in shaping

up the entire traffic between the client and the individual pods. Ingress

controllers are like the load balancers, and ingress resources are the small

configuration pieces of that load balancer that help in modularizing

the various different configurations that are needed to facilitate the

communication to the respective pods. The same has been illustrated in

the following diagram.

Figure 7-9. Connectivity between AWS ALB and EKS Ingress

Resources

297

Chapter 7 Spring Boot app on aWS eKS

In order to set up an ingress controller, we need to create a Kubernetes

deployment resource of the specific controller that we need to set up. This

deployment resource serves as a load balancer controller, which is then

used to set up an actual Application Load Balancer in the public subnet

using the following code.

This deployment is exposed on port 9443, which is consumed by

the Kubernetes API server (master node/control plane) to supervise

the creation of the application load balancer. Hence, we need to ensure

that a security group exists with port 9443 between the master node and

the worker nodes, which was discussed in the earlier section (refer to

Figur

Code Block 7-18. Setting up AWS ALB ingress controller

File: pgitops\infra\eks.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

28: # Module that creates the Application Load Balancer for use

as an Ingress Controller

29: module "alb_ingress" {

30: source = "./modules/alb"

31:

32: clustername = module.clustername.cluster_name

33: oidc_url = module.eks.cluster_oidc_issuer_url

34: oidc_arn = module.eks.oidc_provider_arn

35: awslb_version = "1.4.1"

36: environment = var.environment

37:

38: depends_on = [module.eks, module.networking]

39: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

298

Chapter 7 Spring Boot app on aWS eKS

L29-39: Sets up an AWS Load balancer controller deployment by

providing the OIDC and the Load Balancer controller helm chart version.

Note We’ll be exploring oiDC (openiD Connect) and irSa (iaM roles

for Service accounts) in more detail in Cha.

7.6 DNS

7.6.1 Route53 Record Creation

Once the load balancer controller is created, a separate load balancer for

each individual ingress resource with a DNS name taking the format as

< ingress-name>-<account-id>.<region>.elb.amazonaws.com, for example, my-ingress-1234567890.us-west-2.elb.amazonaws.com. We can access our

application by following this path; however, it's not something feasible to

remember. Hence, we need to map this DNS name to our custom DNS,

which is done by the following piece of code.

Code Block 7-19. Creating the Route53 Record and inserting ALB

hostname

File: pgitops\infra\dns.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

20: # Fetch the Zone ID of the hosted domains

21: data "aws_route53_zone" "domain" {

22: name = "${var.domain}."

23:

24: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

299

Chapter 7 Spring Boot app on aWS eKS

38: # Fetching the ZoneID for ELB

39: data "aws_elb_hosted_zone_id" "main" {}

40:

41: # Mapping The Ingress controller hostname with DNS Record

42: resource "aws_route53_record" "app" {

43: zone_id = data.aws_route53_zone.domain.zone_id

44: name = local.url

45: type = "A"

46:

47: alias {

48: name = kubernetes_ingress_

v1.app.status.0.load_

balancer.0.ingress.0.hostname

49: zone_id = data.aws_elb_hosted_zone_

id.main.id

50: evaluate_target_health = true

51: }

52:

53: depends_on = [module.alb_ingress, kubernetes_

ingress_v1.app]

54:

55: }

L20–24: We are fetching the zone ID of the Route53 zone, which we

had created in the beginning by executing the aws route53 command. We

are querying it using the domain name that we’ll be using.

L39: Since every load balancer has a public DNS name as mentioned

previously, it also has a specific zone ID where it is deployed, which is

managed by AWS. We are simply querying that zone ID.

L43: Creating the DNS record in the Route53 zone that we created

earlier.

300

Chapter 7 Spring Boot app on aWS eKS

L47–51: The record that we need to create is an Alias record, which is recommended by AWS for setting up DNS records for Load Balancers. On

L48, we are fetching the hostname of the ingress resource that is created.

The value of the variable local.url will be used to map the ingress DNS

hostname. How the value of local.url is derived is explained in the next

section.

Tip this might sound astonishing but yes, aWS Load Balancer

Controller deployment will set up a separate aWS Load Balancer. So if

we have three ingress resources, we’ll see three different aWS Load

Balancers. this can be avoided using the ingress group annotation

when defining ingress resources that groups different ingress

resources as one. this will be discussed in more detail in Chapter

where we need to set up two ingress resources.

References:

Kubernetes Ingress Resource Annotations for AWS ALB

7.6.2 Multi-environment DNS

We need to keep our code DRY (Don’t Repeat Yourself) and at the same

time have it running in multiple environments. While other things can

remain the same across different environments, the domain name cannot

remain the same; hence, the following code sets up the layout where

depending on the environment, the value of the variable local.url will

change dynamically.

301

Chapter 7 Spring Boot app on aWS eKS

Code Block 7-20. Dynamically configuring App URL as per the

environment

File: pgitops\infra\dns.tf

01: // Pseudo Code

02: // if(environment==staging){

03: // url = staging.gitops.rohitsalecha. com

04: // }elseif(environment==prod){

05: // url = gitops.rohitsalecha. com

06: // }elseif(environment==dev){

07: // url = dev.gitops.rohitsalecha. com

08: // }

09:

10: locals {

11:

12: staging_url = var.environment == "staging" ?

"staging.${var.domain}" : ""

13: prod_url = var.environment == "prod" ? "${var.

domain}" : ""

14: dev_url = var.environment == "dev" ? "dev.${var.

domain}" : ""

15:

16: url = coalesce(local.staging_url, local.prod_url, local.

dev_url)

17:

18: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

L01–08: Is the Pseudo Code explanation of the multi-environment

problem that we are solving.

302

Chapter 7 Spring Boot app on aWS eKS

L10–18: Since terraform doesn’t provide syntaxes like if..else, etc., we

make use of the coalesce function, which accepts only a non-null value. So if the environment is staging, the variable staging_url will be non-null, but the other variables will be null. Hence, the final value of the url variables takes the value stored in staging_url.

This is a small hackety solution to simulate the if..else statement

behavior.

7.6.3 ACM

Now that we’ve mapped a custom domain for the load balancer hostname

(ingress resource hostname), we should also ensure that the application

is accessible over an HTTPS communication and not on clear-text

HTTP. This can be achieved by utilizing the following code, which

generates a public SSL certificate for a given domain specified by the local.

url variable and the Route53 Zone ID. The Route53 Zone ID is required

here because custom. What this essentially does is that it creates an ACM

certificate on top of an ELB (Elastic Load Balancer).

Code Block 7-21. Generating an HTTPS certificate using AWS ACM

File: pgitops\infra\dns.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

26: # Generate Certificate for the particular domain

27: module "acm" {

28: source = "./modules/acm"

29:

30: zone_id = data.aws_route53_zone.domain.zone_id

31: domain = local.url

32: environment = var.environment

33:

303

Chapter 7 Spring Boot app on aWS eKS

34: depends_on = [module.alb_ingress]

35:

36: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

7.7 Application

Figure 7-10. Relationship between Kubernetes ingress, service, and

deployment

Our application is deployed in the EKS and has the following components:

A specific namespace is created where all the components

like the ingress, service, and deployment will be created.

A deployment resource is created, which holds the

Kubernetes pod.

The Deployment is exposed to the Kubernetes network

through a Service configured as “NodePort”.

The Service is exposed to the external (Internet)

network through the ingress resource.

Each ingress resource created creates a unique load

balancer.

304

Chapter 7 Spring Boot app on aWS eKS

7.7.1 Namespace

Let’s look at how each of the components is described in pgitops\

infra\app.tf.

Code Block 7-22. Creating a Kubernetes namespace using

Terraform

File: pgitops\infra\app.tf

03: resource "kubernetes_namespace_v1" "app" {

04: metadata {

05: annotations = {

06: name = var.org_name

07: }

08: name = var.org_name

09: }

10: depends_on = [module.eks]

11: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

L03–11: We are declaring a Kubernetes namespace using a terraform

declaration syntax. We specify the annotation and the name of the

namespace using a variable var.org_name, which is specified in the

terraform.auto.tfvars file.

Just to draw some comparison, the following is the manifest file that

performs the same action in YAML format that would be executed using

the kubectl utility provided by Kubernetes.

Code Block 7-23. Creating a Kubernetes namespace using YAML

apiVersion: v1

kind: Namespace

metadata:

305

Chapter 7 Spring Boot app on aWS eKS

name: "gitops"

labels:

name: "gitops"

7.7.2 Deployment

Before we discuss the Kubernetes deployment resource, there is an

important prerequisite without which our deployment would not execute.

This important prerequisite is nothing but our PostgreSQL database

password that was stored in the AWS SSM store. The same is retrievable

using the following code snippet, which reads the value specified at

the path.

Code Block 7-24. Reading a database password from AWS SSM

File: pgitops\infra\app.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

16: module "ssmr-db-password" {

17: source = "./modules/ssmr"

18:

19: parameter_name = "db_password"

20: parameter_path = "/${var.org_name}/database"

21:

22: depends_on = [module.ssmw-db-password]

23: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

The value for the AWS SSM can be retrieved using the following syntax:

module.ssmr-db-password.ssm-value”, as we’ll see being used in the

Kubernetes deployment specification next.

306

Chapter 7 Spring Boot app on aWS eKS

Code Block 7-25. Kubernetes deployment

File: pgitops\infra\app.tf

25: resource "kubernetes_deployment_v1" "app" {

26: metadata {

27: name = var.org_name

28: namespace = kubernetes_namespace_v1.app.metadata.0.name

29: labels = {

30: app = var.org_name

31: }

32: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

50: spec {

051: container {

052: image = "salecharohit/practicalgitops"

053: name = var.org_name

054: env {

055: name = "DB_PORT"

056: value = "5432"

057: }

058: env {

059: name = "DB_HOST"

060: value = module.pgsql.db_instance_address

061: }

062: env {

063: name = "DB_NAME"

064: value = var.db_name

065: }

066: env {

067: name = "DB_USERNAME"

068: value = var.db_user_name

069: }

307

Chapter 7 Spring Boot app on aWS eKS

070: env {

071: name = "DB_PASSWORD"

072: value = module.ssmr-db-password.ssm-value

073: }

074:

75: port {

76: container_port = "8080"

77: protocol = "TCP"

78: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

L28: Here, we are explicitly referencing the namespace that we created

earlier using the terraform reference syntax. This ensures an implicit

dependency in a way that first the namespace will be created and then the

deployment. Hence, it is extremely important to add this line as we want

this dependency to be maintained.

L52: Specifying the container image for the deployment pod

specification. This image is currently stored in DockerHub; however, you

can store it in any container registry accessible by your EKS Node.

Note For the purpose of this book, i’ve used Dockerhub, but

yes using an eCr would be highly recommended especially when

working in a complete aWS environment as you get much lesser

latency while downloading the images and also capability to scan the

docker images for security vulnerabilities.

L54–73: Specifying the database environment variables like the

database port, hostname, name, username, and finally the password. The

password field is being populated dynamically from the AWS SSM read

module as specified earlier. Hence, no manual intervention of entering

password is now required.

308

Chapter 7 Spring Boot app on aWS eKS

L75–78: We are specifying the container port on which our pod will be

exposed, that is, port 8080.

7.7.3 NodePort Service

In Kubernetes, it is a best practice to expose a Deployment/Pod through a

service to the internal network. Hence, we’ve created a Kubernetes service

as shown in the following within the same namespace as the Deployment

and exposed it over port 8080.

Code Block 7-26. Kubernetes service

File: pgitops\infra\app.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

106: resource "kubernetes_service" "app" {

107: metadata {

108: name = var.org_name

109: namespace = kubernetes_namespace_v1.app.

metadata.0. name

110: }

111: spec {

112: port {

113: port = 8080

114: target_port = 8080

115: }

116: selector = {

117: app = var.org_name

118: }

119: type = "NodePort"

120: }

309

Chapter 7 Spring Boot app on aWS eKS

121: depends_on = [kubernetes_deployment_v1.app]

122: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

L116–118: The service connects the Deployment pods over port 8080

using the selector label app.

7.7.4 Ingress

The following code is used to create a Kubernetes ingress resource with all

the specific annotations described here.

Code Block 7-27. Creating a Kubernetes ingress resource

File: pgitops\infra\app.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

124: resource "kubernetes_ingress_v1" "app" {

125: wait_for_load_balancer = true

126: metadata {

127: name = var.org_name

128: namespace = kubernetes_namespace_v1.app.

metadata.0. name

129: annotations = {

130: "kubernetes.io/ingress.class" = "alb"

131: "alb.ingress.kubernetes.io/scheme" =

"internet-facing"

132: "alb.ingress.kubernetes.io/target-type" = "ip"

133: "alb.ingress.kubernetes.io/certificate-arn" =

module.acm.acm_arn

134: "alb.ingress.kubernetes.io/listen-ports" =

"[{\"HTTP\": 80}, {\"HTTPS\":443}]"

135: }

310

Chapter 7 Spring Boot app on aWS eKS

136: }

137: spec {

138: rule {

139: host = local.url

140: http {

141: path {

142: backend {

143: service {

144: name = var.org_name

145: port {

146: number = 8080

147: }

148: }

149: }

150: path = "/*"

151: }

152:

153: }

154: }

155: depends_on = [module.alb_ingress, kubernetes_

ingress_v1.app]

156: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

L129–134: We are defining the annotations that govern the complete

operation of the ingress resource.

L130: We are defining the ingress class as “alb” since we

are using the AWS ALB as the ingress controller

deployed earlier in the EKS section. Each ingress

resource will hence communicate with the ingress

controller deployment in EKS to spin up a fresh new

Load Balancer.

311

Chapter 7 Spring Boot app on aWS eKS

L131: There are two schemes: Internet facing and

internal. By configuring our ingress resource as Internet

facing, it automatically gets added in the public subnet

by querying for the particular tag. These are the same

tags we configured in Section 7.3 if you recall.

L132: The target type we are defining as IP based as we

are configuring the service as NodePort type.

L133: We are configuring the SSL certificate in the

ingress resource generated by the AWS ACM module.

Since the ingress resource is spinning up a unique ALB,

it is important to configure the SSL certificate here as

SSL offloading will happen at this point.

L134: Configuring the ports on which the AWS ALB will

be listening on to the external Internet. If we are

specifying 443 as a port, it is mandatory to have an AWS

ACM ARN, which we are providing on L133.

L155: Here, we are explicitly declaring the dependency

on the AWS ALB ingress controller deployment module

as it needs to be created prior to the ingress resource.

As stated earlier, this dependency is critical because the

ingress resource communicates with the ingress

controller deployment to spin up an AWS Application

Load Balancer.

L125: This line is of critical importance as our Route53

record creation as discussed in the DNS module

depends on the Application Load Balancer hostname,

which in turn depends on the ingress resource.

However, the ALB will take time to start up and have a

312

Chapter 7 Spring Boot app on aWS eKS

hostname assigned to it. Hence, this line performs

regular checks on whether the load balancer has been

set up or not and will wait before moving the execution

to other modules.

7.7.5 Kubernetes Provider

I discussed terraform providers in Section 3.2 where these providers are

nothing but plugins for different cloud and cloud-native environments.

Similar to how we’ve got for AWS, we are declaring a provider for

Kubernetes and Helm in pgitops/infra/kubernetes.tf as shown here.

Code Block 7-28. Defining a Kubernetes provider

File: pgitops\infra\kubernetes.tf

03: provider "kubernetes" {

04: host = module.eks.cluster_endpoint

05: cluster_ca_certificate = base64decode(module.eks.cluster_

certificate_authority_data)

06: exec {

07: api_version = "client.authentication.k8s.io/v1alpha1"

08: command = "aws"

09: args = [

10: "eks",

11: "get-token",

12: "--cluster-name",

13: module.eks.cluster_id

14: ]

15: }

16: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

313

Chapter 7 Spring Boot app on aWS eKS

Reference: Kubernetes Provider for Terraform Documentation

(

L04–05: Every provider needs authentication information in order

to perform the requested operations. For AWS, we are supplying the

AWS Access Key and ID using environment variables. However, for the

Kubernetes provider, we need the hostname of the Kubernetes cluster

and the CA certificate that is used for generating the authentication token

for the Kubernetes API. These details are obtained from the EKS Module

defined in the pgitops/infra/eks.tf file and are fetched once the EKS

Module has been set up.

L06–13: This Kubernetes provider directly interacts with the

Kubernetes API over HTTPS.

Once we have the hostname and the Cluster CA certificate, Terraform

needs to obtain the Kubernetes API token in order to authenticate to

the Kubernetes API. This authentication is performed using the aws

command, that is, aws eks get-token –cluster-name $cluster-id, where

$cluster-id is nothing but the cluster name created using the EKS Module.

This provider (Kubernetes and Helm) will be kicked into action

whenever we need to deploy any resource within Kubernetes. If we do not

define this provider, we won’t be able to deploy/create any Kubernetes

resource, not even a simple namespace. Hence, this provider is needed

purely for all Kubernetes operations.

7.7.6 Variables

Last, but one of the most important configurations, is setting the variable

values defined in pgitops/infra/terraform.auto.tfvars. We can keep all the variable values as is except for the following two.

314

Chapter 7 Spring Boot app on aWS eKS

Code Block 7-29. Defining Kubernetes variables

File: pgitops\infra\kubernetes.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

40: org_name = "gitops"

41: domain = "change.me.please"

L40–41: Kindly replace the following values before attempting to

execute the code. Your domain should be the one which you’ve set as per

the DNS configurations in Section 7.1.

Note the value that i’ve set for myself is gitops.rohitsalecha.com, which is what you’ll see throughout this book.

7.7.7 Default Environment

By default, the environment that has been configured is the dev

environment and the region as us-east-1 as is evident from the following

file. This is done because the developers need to independently test their

code from their local systems before pushing the changes into staging.

Hence, they can see their changes live and test it out.

While for the prod and staging environments, we need to configure

these two variables in Terraform Cloud as explained in the previous

chapter.

Code Block 7-30. Defining the default environment for Kubernetes

File: pgitops\infra\kubernetes.tf

04: variable "environment" {

05: description = "The Deployment environment"

315

Chapter 7 Spring Boot app on aWS eKS

06: type = string

07: default = "dev"

08: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

15: variable "region" {

16: description = "AWS region"

17: type = string

18: default = "us-east-1"

19: }

7.8 Execution

Now that we’ve a good understanding of the different infrastructure

items that we are going to spin up, let’s go ahead and start the entire

infrastructure.

Note prior to running terraform, it’s necessary to set up the

route53 hosted Zone as discussed in DnS configuration in

the prerequisites section of this chapter. i currently have my

route53 hosted Zone set up with the domain name as dev.gitops.

rohitsalecha.com.

7.8.1 Setting Up the EKS Cluster

Fire the following commands to get started; ensure you’ve got your AWS

credentials configured in the CLI as well.

316

Chapter 7 Spring Boot app on aWS eKS

CLI Output 7-31. Executing new infra

export AWS_PROFILE=gitops

cd pgitops/infra

terraform init

terraform apply -auto-approve

Terraform apply should take about close to ~15 minutes to complete.

Once done, simply browse to the development domain name as by default

for developers, application is hosted on dev.$domain_name as configured

in the pgitops/infra/dns.tf.

The following screenshot shows that our application is now up and

running.

Figure 7-11. Development environment up and running

Viewing all the users.

Let’s add a test user to check if everything is working fine.

317

Chapter 7 Spring Boot app on aWS eKS

Figure 7-12. User Adding screen

Figure 7-13. List of users added

7.8.2 Accessing the EKS Cluster

Now that our application is up and running let’s check how our Kubernetes

environment has been configured by accessing it using the kubectl utility.

318

Chapter 7 Spring Boot app on aWS eKS

AWS CLI has a very useful command to setup the Kubeconfig file

which the kubectl utility needs in order to authenticate to the Kubernetes

cluster. The command is

aws eks --region $REGION update-kubeconfig --name $CLUSTER

So, let’s execute this command and then explore the Kubernetes

cluster.

CLI Output 7-32. Accessing the Kubernetes development

environment

cmd> EXPORT AWS_PROFILE=gitops

cmd> aws eks --region us-east-1 update-kubeconfig -- name

gitops-us-east-1-dev

Added new context arn:aws:eks:us-east-1:147611229030:cluster/

gitops-us-east-1-dev to /Users/rohitsalecha/.kube/config

cmd> kubectl get nodes

NAME STATUS ROLES AGE

VERSION

ip-10-0-11-206.ec2.internal Ready <none> 6m

v1.20.11-eks-f17b81

ip-10-0-11-63.ec2.internal Ready <none> 6m11s

v1.20.11-eks-f17b81

cmd> kubectl get all -n gitops

NAME READY STATUS RESTARTS AGE

pod/gitops-795f75dd58-zmkv2 1/1 Running 0 3m1s

NAME TYPE CLUSTER-IP EXTERNAL- IP

PORT(S) AGE

service/gitops NodePort 172.20.192.96 <none>

8080:30307/TCP 2m50s

319

Chapter 7 Spring Boot app on aWS eKS

NAME READY UP- TO-DATE AVAILABLE AGE

deployment.apps/gitops 1/1 1 1 3m4s

NAME DESIRED CURRENT READY AGE

replicaset.apps/gitops- 795f75dd58 1 1 1 3m4s

7.9 Clean-Up

Since our use case is done, let’s destroy the environment using the

following commands. It should take approximately ten minutes to destroy

the entire environment.

CLI Output 7-33. Destroying the Kubernetes environment

cmd> export AWS_PROFILE=gitops

cmd> terraform destroy -- auto-approve

Note at any point, if terraform is unable to destroy, please check

annexure a where steps have been presented to delete the resources

manually.

7.10 Conclusion

In this chapter, we saw the different components we are spinning up

using Terraform and that other than setting the Route53 Zone manually,

everything else was completely automated.

However, we did not explore the GitOps workflow here; we did not

deploy this code in the staging and then in the production environment,

which this book is all about. The major reason to do that is that we haven’t

320

Chapter 7 Spring Boot app on aWS eKS

yet created separate AWS accounts for staging/production/development,

which is what we’ll explore in the next chapter. We’ll be creating

separate accounts and also creating users with different privileges and

authorization roles not just for accessing AWS but also for Kubernetes!

From the next chapter onward, we’ll start improving this code, and

as a first step, we’ll work on implementing a proper authentication and

authorization in the entire infrastructure including AWS + Kubernetes in

the next chapter.

321

CHAPTER 8

Authentication and

Authorization

In this chapter, we’ll look at how to create AWS accounts using AWS

organizations for different environments and use cases using Terraform.

We’ll create four different AWS accounts, which will be aligned with three

different organization units or OUs. Each account is being created for

specific use cases, which will be discussed in this chapter. We’ll also look

at how we can create Route53 Zones for each of the operational accounts

for better segregation of DNS records. However, this time around, we’ll

be doing completely through Terraform, and no manual intervention

will be required. Lastly, we’ll create IAM Users and IAM Roles with set

permissions to interact with different AWS accounts and also do mapping

of AWS IAM Roles with Kubernetes Groups to provide a seamless

authentication and authorization through code!

8.1 Prerequisites

In addition to all the previous requirements, we’ll need the following:

– GNU PG Utility to generate public/private keys.

– Please download and install for your respective OS

using the following link:

© Rohit Salecha 2023

323

R. Salecha, Practical GitOps,

ChApter 8 AuthentICAtIon And AuthorIzAtIon

– AWS account that we’ll be using here should not have

AWS organization setup previously, that is, the Root AWS

organization account should not be created.

Ensure that you have the following before getting started:

– The pgitops directory created in Chapter

– Access to the DNS console of your top-level domain or

sub-domain as we’ll be adding a few more NS records

– Terraform Cloud configured with prod and staging

workspaces

8.1.1 Code Update

Before diving into the code, let’s first ensure that we’ve updated the

pgitops directory with the code from Chapter folder. To do this, follow the following steps:

– First, copy contents of Chapter folder into the pgitops folder.

– Get inside the pgitops directory and then check which

files/folders are changing.

– Commit changes into the repository; however, while

committing, ensure to have the “[skip ci]” added in the

commit as shown in the following as I don't intend to

execute the actions yet as the code is still incomplete:

Tip Github Actions execute whenever there is a new commit in the

branch. If we wish to skip the execution for a specific commit, we can

simply add [skip ci] verbatim in the commit message.

324

ChApter 8 AuthentICAtIon And AuthorIzAtIon

cmd> cp -a chapter8/. pgitops/

cmd> cd pgitops

cmd> git status

On branch main

Your branch is up to date with 'origin/main'.

Changes not staged for commit:

(use "git add <file>..." to update what will be committed)

(use "git restore <file>..." to discard changes in working

directory)

modified: README.md

modified: infra/README.md

modified: infra/app.tf

modified: infra/dns.tf

modified: infra/kubernetes.tf

modified: infra/providers.tf

modified: infra/terraform.auto.tfvars

modified: infra/variables.tf

Untracked files:

(use "git add <file>..." to include in what will be

committed)

.github/workflows/global.yaml

global/

infra/rolebindings.tf

no changes added to commit (use "git add" and/or "git

commit -a")

cmd> git add .

# Please ensure to add [skip ci] in the commit message.

cmd> git commit -m "chapter 8 commit [skip ci]"

cmd> git push

325

ChApter 8 AuthentICAtIon And AuthorIzAtIon

the changes can be summarized as follows:

– Added a new folder called “global,” which contains all the

code related to creating AWS organizations, Route53

Zones, and IAM User/Roles. So basically, we’ll be having

terraform files in two folders (i.e., infra and global).

– Added a new workflow file called global.yaml to have a

proper Git workflow just like staging/production changes

– Added a new file called rolebindings.tf under the infra

folder, which does mapping of Kubernetes groups with

IAM roles in the AWS ConfigMap.

– Rest all files under the infra folder have been modified as

per the changes done with the global folder.

Each change/addition will be discussed in detail in this chapter.

8.2 AWS Organizations

AWS organizations is a service provided by AWS where we can create and

centrally administer/manage multiple different accounts that are created

for a specific purpose. There are two primary concepts that need to be

understood in AWS organizations, and they are root account/management

account and member accounts with OUs or organization units. Let’s

understand in more detail.

326

ChApter 8 AuthentICAtIon And AuthorIzAtIon

8.2.1 Root Account

Root account as the name suggests is the main account that we signed up

with. It is also called the management account as it has full control over

the billing and cloud deployments. As of now, I am accessing everything

through the Root account, and our current authentication framework

is extremely risky from a security and scalability perspective. Let’s

understand our current design and call out the drawbacks/limitations of it.

The previous diagram provides a glimpse into the operating model that

we have currently:

– Using the root user, I created an AWS account, which is

also known as the “root account” or the “Management

account.”

– I then created another user called “gitops” and provided

Administrator privileges.

– I then accessed the AWS Console using the “gitops” user

credentials to download the AWS Access Keys and AWS

Access ID.

327

ChApter 8 AuthentICAtIon And AuthorIzAtIon

– This set of AWS Access Keys and Access ID was then used

to configure the Terraform environment to spin up the

Dev/staging and prod environments.

Limitations/drawbacks of the previous architecture are as follows:

– Currently if we need to create new users, then everyone

will need to access the root account with certain pre-

defined privileges.

– Segregating environments in different regions is definitely

not a good idea as anyone who has access to the root

account with Admin privileges can easily gain access to

all the environments.

– From a billing perspective as well, it becomes extremely

difficult to create bill reports per region.

– Developers will be limited by the number of regions they

can use to perform their checks/tests.

These are just a few reasons; however, if we attempt to align with AWS

Best Practices, then it would almost fill two pages or maybe more.

8.2.2 Member Accounts and OUs

Member accounts and Organizational Units are created to have a proper

segregation of duties and responsibilities for the various different accounts

an Organization may have. These accounts can inherit the organizational

structure (but not limited to it) that a typical organization has like having

segregated departments like Finance, Marketing, Engineering, etc. Since

all these departments function separately, it’s best that their operations are

very well segregated. This is where AWS organizations can be utilized to its

fullest extent by helping to design and implement the operational aspects

of running an entire organization while at the same time having full control

over every small aspect of it.

328

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Figure 8-1. AWS organization high-level view

In the preceding diagram we are using AWS organizations to create

multiple OUs or Organizational Units and member accounts associated

with each OU. Let’s understand better:

– The management account or the root account is now

accessible only by the gitops user that I created and also

the root user credentials. Other than these two, no one

else has access and no one will ever need it as well with

exception for billing purposes.

– I’ve created OUs with specific purposes like Identity OU,

Deployment OU, and Development OU. OUs are created

to group similar member accounts where their function-

ality is aligned. We can then apply Service Control

Policies or SCPs at OU level, which makes it easier to

govern. We’ll be exploring SCPs in greater depth in the

next chapter.

– Each member account is serving a specific purpose.

– The prod and staging accounts are for deploying the

application and are LIVE environments.

329

ChApter 8 AuthentICAtIon And AuthorIzAtIon

– The development account is provided to the developers

to do their testing independently of the staging/prod

environment.

– Identity account is where all IAM Users, Roles, and

Groups will be created. One key thing to note here is that

we are not going to create users in any other account

except for identity accounts.

– Users will only have access to identity accounts where

they can change their passwords/update their MFA

and perform only IAM- related activities.

– They will not be allowed to create an EC2 instance or

any other functions in the identity account.

– They will be allowed to perform all other operations

only in dev environments, which are accessible by

Assuming IAM Roles created in the dev account. We’ll

be covering more details on the User and Role

Management later in this chapter.

Let’s now look into how the AWS organizations and the member

accounts have been created in Terraform.

All the magic is happening in the pgitops/ global/organisations.tf file as shown in the following.

Code Block 8-1. Creating the AWS organization using Terraform

File: pgitops/global/organisations.tf

01: # Initializing Root Organization

02: resource "aws_organizations_organization" "root" {

03: }

04:

330

ChApter 8 AuthentICAtIon And AuthorIzAtIon

05: # Create an OU for Identity management

06: resource "aws_organizations_organizational_unit"

"identity" {

07: name = "Identity Management Account"

08: parent_id = aws_organizations_organization.root.

roots[0].id

09: }

10:

11: # Create an OU for deployment accounts

12: resource "aws_organizations_organizational_unit"

"deployment" {

13: name = "Deployment Accounts"

14: parent_id = aws_organizations_organization.root.

roots[0].id

15: }

16:

17: # Create an OU for development/sandbox accounts

18: resource "aws_organizations_organizational_unit"

"development" {

19: name = "Development Accounts"

20: parent_id = aws_organizations_organization.root.

roots[0].id

21: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

L02–03: This is where it all starts by first creating a root organization.

This line is not actually creating anything; it’s just setting up the AWS

organization service and assigning the current account where you’ve

logged in as a root account or a management account.

L06–20: Creating the Organizational Units as discussed earlier and

assigning the root account as the parent account.

331

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Let’s look at how I am creating the member accounts in the same file

gitops/ global/organisations.tf.

Code Block 8-2. Creating member accounts

File: pgitos/global/organisations.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

25: module "identity_account" {

26: source = "./modules/awsaccounts"

27:

28: name = var.accounts["identity"]. name

29: email = var.accounts["identity"].email

30: parent_id = aws_organizations_organizational_unit.

identity.id

31:

32: }

33:

34: # Org hosting Production infrastructure

35: module "prod_account" {

36: source = "./modules/awsaccounts"

37:

38: name = var.accounts["prod"]. name

39: email = var.accounts["prod"].email

40: parent_id = aws_organizations_organizational_unit.

deployment.id

41:

42: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

332

ChApter 8 AuthentICAtIon And AuthorIzAtIon

L25–42: AWS account creation has been modularized as in general it

requires three primary information for a unique setup (i.e., account name,

email address, and parent account ID).

The parent account ID is nothing but the Organizational Unit IDs,

whereas the name and the email addresses are retrieved from the pgitops/

global/terraform.auto.tfvars, which is shown as follows.

Code Block 8-3. Member account email and names’ configurations

File: pgitops/global/terraform. auto.tfvars

02: accounts = {

03: dev = {

04: name = "dev"

05: email = "[email protected]"

06: },

07: identity = {

08: name = "identity"

09: email = "[email protected]"

10: },

11: prod = {

12: name = "prod"

13: email = "[email protected]"

14: },

15: staging = {

16: name = "staging"

17: email = "[email protected]"

18: }

19: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

333

ChApter 8 AuthentICAtIon And AuthorIzAtIon

L02-19: The accounts variable here is a “map of maps” type. The first-

level map is the accounts that we need to create, and the second-level map

is the details of those accounts.

Each account creation requires a specific email address, and since

I don't wish to manage multiple emails, I recommend to use the ‘+’

identifier as shown previously that maps all the different accounts (i.e.,

prod, identity, staging, and dev) to a single email inbox. This gives the

flexibility to create multiple emails with a single inbox.

It is not necessary to login using any of those accounts that are

generally used for “break-the-glass” emergency moments when the

management account/admin account is locked out and you need to access

any of these accounts. You would, however, receive email notifications on

these emails informing that your account has been created and, in some

scenarios, also be asked for verification without which you won’t be able to

perform any operations.

We’ll see the email notifications in action when we execute the

terraform files in the “Global” folder.

8.2.3 AWS Provider

An important question to answer is, what access will be needed to create

these accounts?

The answer is pretty simple; we’ll need the existing Admin account

keys that we’ve been using so far. All operations for AWS organizations

must be done through the admin user credentials.

An interesting observation to keep a note on is AWS organizations

cannot be created using your root account access. Hence, you need to

create a user account and provide administrator access to that user and

then only you’ll be able to operate AWS organizations.

A top-up question would be, how will Terraform administer the other

AWS accounts since the current credentials that we have can only access

the Management Account?

334

ChApter 8 AuthentICAtIon And AuthorIzAtIon

To answer this question, let’s look at the code in the awsaccounts

Module located at pgitops/global/modules/awsaccounts/main.tf.

Code Block 8-4. AWS accounts module

File: pgitops/global/modules/awsaccounts/main.tf

03: resource "aws_organizations_account" "default" {

04: name = var.name

05: email = var.email

06: parent_id = var.parent_id

07: role_name = "Administrator"

08: lifecycle {

09: ignore_changes = [role_name]

10: }

11: }

L07: While other lines are self-explanatory, it’s important to

understand what’s happening at this line. Whenever an AWS account is

created using AWS organizations, a default IAM Role with Administrator

privileges is created in the target account with a corresponding

AssumeRole created in the management account. This is also called

an Organization Administrator Role, which is then used to access and

create resources in the target account through the management account

credentials.

335

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Figure 8-2. High-level view of all AWS accounts

This operation is facilitated by declaring the following code in the

pgitops/global/providers.tf as shown as follows.

Code Block 8-5. Declaring AWS Providers for different accounts

using Assume Role

File: pgitops/global/providers.tf

17: provider "aws" {

18: # Any region can be set here as IAM is a global service

19: region = var.region

20: }

21:

22: # Provider configuration for Assuming Administrator Role in

Identity account.

23: provider "aws" {

336

ChApter 8 AuthentICAtIon And AuthorIzAtIon

24: assume_role {

25: role_arn = "arn:aws:iam::${module.identity_account.

id}:role/Administrator"

26: }

27:

28: alias = "identity"

29: region = var.region

30: }

L17–20: This is the primary provider that will utilize the provided

access keys of the management account (gitops in our case) and perform

all the magic.

L23–30: This is where we are declaring a different provider with an

alias as “identity.” This provider has an assume role configured at L25,

which will assume the “Administrator” IAM role created in the identity

account and perform all the requested operations.

Similarly, different providers are configured for prod, staging and

development accounts as well as shown in the following code.

Code Block 8-6. Declaring AWS Providers in code

File: pgitops/global/main.tf

02: module "staging" {

03: source = "./staging"

04:

05: account = "staging"

06: identity_account_id = module.identity_account.id

07: domain = "staging.${var.domain}"

08:

09: providers = {

10: aws = aws.staging

11: }

337

ChApter 8 AuthentICAtIon And AuthorIzAtIon

12:

13: }

14:

16: module "prod" {

17: source = "./prod"

18:

19: account = "prod"

20: identity_account_id = module.identity_account.id

21: domain = var.domain

22:

23: providers = {

24: aws = aws.prod

25: }

26:

27: }

8.3 AWS IAM

Now that we’ve seen how AWS organizations is being used to create

accounts, let’s do a deep dive to understand how we can go about creating

user accounts and provide them with the necessary privileges to perform

the requisite set of actions.

338

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Figure 8-3. AWS organizations and user access

The preceding diagram depicts what we are ultimately trying to

accomplish:

– All users will be created in the identity account only.

– Privileges assigned to the users in the identity account

will be limited and defined by the groups that will be

created. However, no user will be able to perform any

non-IAM operation in the identity account.

– IAM Roles with predefined permissions will be created in

each of the non-identity accounts.

– Each IAM User will be required to Assume the IAM Roles

created in the non-identity accounts. This is also called

Cross-Account IAM Roles Access.

– Terraform also will have access only to the identity

account and will be required to Assume roles into differ-

ent accounts to perform the required operations.

339

ChApter 8 AuthentICAtIon And AuthorIzAtIon

8.3.1 IAM Users

As discussed earlier, IAM Users are being created in the identity account

and will be having access only to the identity account. From there on,

they’ll have to assume a role and then access other accounts.

Let’s look at how we are creating IAM Users using Terraform.

Code Block 8-7. Creating AWS IAM Users using Terraform

File: pgitops/global/identity.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

62: module "users" {

63: for_each = var.users

64:

65: source = "./modules/awsusers"

66:

67: user_name = var.users[each. key].username

68: pgp_key = file("data/${var.users[each.key].pgp_key}")

69:

70: groups = var.users[each. key].role == "admin" ? [aws_iam_

group.iam_admin. name] : [aws_iam_group.self_manage. name]

71:

72: providers = {

73: aws = aws.identity

74: }

75: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

L63: Iterating through a map of users that we need to create. I’ll show

the map of users shortly.

340

ChApter 8 AuthentICAtIon And AuthorIzAtIon

L68: When creating IAM Users using terraform, we’ll need a Public

GPG Key that is used by Terraform to encrypt the passwords for each user.

The encrypted password is then shown on the console and decrypted only

by the private GPG Key, which only the user has access to. All Public PGP

keys need to be stored under the pgitops/global/data folder only.

L70: Within the identity account, I’ve created two groups, and

depending on the role, that is, admin/non-admin, the user is dynamically

assigned to that particular group in the identity account.

L72–74: In the “AWS Provider” section, we saw how Terraform will

assume the Administrator IAM Role in the identity account. Hence, here

we are specifying the provider to inform Terraform to execute all the code

written in this module in the identity account only. If the provider is not

specified, it will execute the code in the context of the current account,

which is nothing but the management account.

Let’s look at the users variable, which is a map of user-related

information as shown in the following.

Code Block 8-8. Configuring the Users variable

File: pgitops/global/terraform. auto.tfvars

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

21: users = {

22: raadha = {

23: username = "raadha"

24: pgp_key = "raadha.pub"

25: role = "developer"

26: },

27: sita = {

28: username = "sita"

29: pgp_key = "sita.pub"

341

ChApter 8 AuthentICAtIon And AuthorIzAtIon

30: role = "admin"

31: },

32: padma = {

33: username = "padma"

34: pgp_key = "padma.pub"

35: role = "readonly"

36: }

37: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

The first-level map is the username, and the second-level map is the

username with the role and the name of the GPG Key assigned for that

particular user.

I’ve currently defined three different organization roles, that is,

developer, admin, and readonly. IAM Roles with requisite permissions

will be created in different environments implementing access for the

respective roles as we’ll see in the “IAM Roles” section a little later.

Let’s understand what’s going behind the scenes in the /modules/

awsusers; the code for the main.tf file is shown in the following.

Code Block 8-9. AWS Users module implementation

File: pgitops/global/modules/awsusers/main.tf

01: # Create an AWS Human user with provided username

02: resource "aws_iam_user" "default" {

03: name = var.user_name

04: tags = {

05: terraform-managed = "true"

06: }

07: }

08:

342

ChApter 8 AuthentICAtIon And AuthorIzAtIon

09: # Create a login profile to generate a temporary password

encrypted with the provided GPG Key

10: # Mandatory Reset of password

11: resource "aws_iam_user_login_profile" "default" {

12: user = aws_iam_user.default. name

13: pgp_key = var.pgp_key

14: password_reset_required = true

15: lifecycle {

16: ignore_changes = [

17: password_reset_required

18: ]

19: }

20: }

21:

22: # Add User to the Groups provided

23: resource "aws_iam_user_group_membership" "default" {

24: user = aws_iam_user.default. name

25: groups = var.groups

26: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

L02-07: Creating an AWS IAM User with a specific username.

L11-20: Every user’s login profile needs to be created with a temporary

encrypted password, which mandatorily needs to change on first access.

Once the user accesses the console and changes the password, the

password_reset_required attribute in the user’s metadata flips to false.

However, whenever there is an update to the Terraform code, the

password_reset_required attribute flips to true and the user’s password is resetted. To avoid the ignore_changes lifecycle attribute on L16 is required, which states that ignore the changes to the password_reset_required

attribute.

343

ChApter 8 AuthentICAtIon And AuthorIzAtIon

If the user forgets the password, then the admin user will log into the

console and generate a new set of passwords for that user. It’s neither

feasible nor advisable to run the entire terraform code just to change a

user’s password.

L22-26: Lastly, every user in the identity account must be mapped to at

least one group, which is what we’ll explore next.

Lastly, we are enforcing the password policy as well for all Users

configured in the identity account using the following code.

Code Block 8-10. Setting Password policy on identity account

File: pgitops/global/identity.tf

01: # Create a Password Policy in the Identity Account as users

will be created in this account only

02: resource "aws_iam_account_password_policy" "strict" {

03: minimum_password_length = 10

04: require_lowercase_characters = true

05: require_numbers = true

06: require_uppercase_characters = true

07: require_symbols = true

08: allow_users_to_change_password = true

09:

10: provider = aws.identity

11:

12: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

344

ChApter 8 AuthentICAtIon And AuthorIzAtIon

8.3.2 IAM Groups

The identity account serves as the landing zone for all the users. Hence,

basic permissions need to be provided to each user so that they can

access their own account. Hence, I’ve created two sets of IAM Groups,

and depending on the role provided to the user, she is added to that

particular group.

Let’s look at the IAM Administrator Group.

Code Block 8-11. Creating IAMAdmin Role for identity account

File: pgitops/global/identity.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

38: resource "aws_iam_policy" "iam_admin" {

39: name = "IAM Administrator"

40: policy = file("data/iamadmin.json")

41:

42: provider = aws.identity

43: }

44:

45: # Create an IAM Administrator Group

46: resource "aws_iam_group" "iam_admin" {

47: name = "IAM Administrator"

48:

49: provider = aws.identity

50: }

51:

52: # Attach the IAM Administrator Policy with Group

53: resource "aws_iam_group_policy_attachment" "iam_admin" {

54: group = aws_iam_group.iam_admin. name

55: policy_arn = aws_iam_policy.iam_admin.arn

345

ChApter 8 AuthentICAtIon And AuthorIzAtIon

56:

57: provider = aws.identity

58: }

L45–50: Create an IAM group called IAM Administrator.

L38–43: Assign an IAM Policy with the permissions specified in the file

located at pgitops/global/data/iamadmin.json as shown in the following.

This Policy states that it provides all IAM-related permissions on all IAM

resources.

Code Block 8-12. IAM Administrator Permissions Policy

File: global/ data/iamadmin.json

01: {

02: "Version": "2012-10-17",

03: "Statement": [

04: {

05: "Sid": "IAMAdmin",

06: "Effect": "Allow",

07: "Action": [

08: "iam:*"

09: ],

10: "Resource": "*"

11: }

12: ]

13: }

L52–58: Attach the IAM Policy to the IAM Group.

The Self-Managed group allows users to be able to access the identity

account using the username/password provided by the administrator,

change her password, and download her access keys. All other actions are

prohibited.

346

ChApter 8 AuthentICAtIon And AuthorIzAtIon

The Terraform code for the Self-Managed group remains the same

as that of IAM Administrator with the exception of the policy that is too

verbose to mention here. Hence, a snippet of the policy can be seen in the

following.

Code Block 8-13. Self-Manage IAM Permissions Policy

File: global/ data/self_manage.json

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

13: {

14: "Sid": "AllowManageOwnPasswords",

15: "Effect": "Allow",

16: "Action": [

17: "iam:ChangePassword",

18: "iam:GetUser"

19: ],

20: "Resource": "arn:aws:iam::*:user/${aws:u

sername}"

21: },

22: {

23: "Sid": "AllowManageOwnAccessKeys",

24: "Effect": "Allow",

25: "Action": [

26: "iam:CreateAccessKey",

27: "iam:DeleteAccessKey",

28: "iam:ListAccessKeys",

29: "iam:UpdateAccessKey"

30: ],

31: "Resource": "arn:aws:iam::*:user/${aws:u

sername}"

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

347

ChApter 8 AuthentICAtIon And AuthorIzAtIon

L14–19: Policy allows users to manage their own passwords. L20

provides an explicit declaration of the username that also enforces

authentication.

L23–30: Policy allows users to manage their AWS Access Keys.

8.3.3 IAM Roles

Before we begin the discussion of how I am creating the role, let’s discuss a

little bit about how Cross-Account Roles function.

Figure 8-4. Cross-Account IAM Roles Access

Let’s take an example of an Admin user in the identity account

who needs to login and operate in the prod account. How will this

operation happen?

In the prod account, we first need to create an IAM Role

called “AssumeRoleAdminprod” and attach the existing policy of

“arn:aws:iam::aws:policy/AdministratorAccess”. In addition to this, we also

need to add an Assume Role Policy where we are specifying the identity

account ID as a Trusted Entity. In Terraform, this can be achieved through

the following code.

348

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Code Block 8-14. AWS IAM Cross-Account Role

01: # Create an IAM Role with an AssumeRole Policy for a

trusted entity

02: resource "aws_iam_role" "default" {

03: name = "AssumeRoleAdminprod"

04: assume_role_policy = jsonencode({

05: Version = "2012-10-17",

06: Statement = [

07: {

08: Effect = "Allow",

09: Action = "sts:AssumeRole",

10: Principal = {

11: "AWS" : "arn:aws:iam::1111111111:root"

12: }

13: }]

14: })

15: tags = {

16: terraform-managed = "true"

17: account = var.account

18: }

19: }

20:

21: # Attach the Policies provided to the IAM role

created above

22: resource "aws_iam_role_policy_attachment" "default" {

23:

24: policy_arn = ["arn:aws:iam::aws:policy/

AdministratorAccess "]

25: role = aws_iam_role.default. name

26: }

349

ChApter 8 AuthentICAtIon And AuthorIzAtIon

L02–19: In the prod account, we first need to create an IAM Role called

“AssumeRoleAdminprod” and attach an assume role policy specifying the

identity account ID as a Trusted Entity as shown on L11.

L21–26: We then attach the predefined policy of arn

arn:aws:iam::aws:policy/AdministratorAccess” to the IAM Role created

previously.

Next, in the identity account, we create an IAM Policy called

“AssumeRolesadmin” and attach it to the IAM User “admin,” thereby

granting only the admin user to access the Administrator role in the prod

account. A sample terraform code to do this is as shown in the following.

Code Block 8-15. AWS IAM Cross-Account Role Contd

02: resource "aws_iam_policy" "default" {

03: name = "AssumeRolesadmin"

04: description = "Allow IAM User to Assume the IAM Roles"

05: policy = jsonencode({

06: Version = "2012-10-17",

07: Statement = [

08: {

09: Effect = "Allow",

10: Action = "sts:AssumeRole",

11: Resource = ["arn:aws:iam::2222222222:role/

AssumeRoleAdminprod"]

12: }]

13: })

14: }

15:

16: resource "aws_iam_user_policy_attachment" "default" {

17: user = "admin"

18: policy_arn = aws_iam_policy.default.arn

19: }

350

ChApter 8 AuthentICAtIon And AuthorIzAtIon

L02-14: We are defining the AWS IAM Policy, which will allow the

user/group to whom this policy would be attached to, to assume the

specific role in the specific account mentioned on L11.

L16-19: Attaching the Policy created previously to the specific user.

Tip Many terraform developers and AWS IAM architects would

advise to attach these policies to a group rather than a user for

better/broader access management. I’ve attached the policy to a user

in this book to keep things simple.

The following code snippet shows how we are creating the AWS IAM

Roles in the prod account and supplying the identity account id to add as a trusted entity.

Code Block 8-16. Creating AWS IAM Roles in prod account

File: pgitops/global/prod/main.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

12: # Create an AWS ReadOnly Role with a Managed

ReadOnlyAccess Policy

13: module "assume_readonly_role" {

14: source = "./modules/assumerolepolicytrust"

15:

16: role_name = "AssumeRoleReadOnly${var.account}"

17: trusted_entity = "arn:aws:iam::${var.identity_account_

id}:root"

18: policy_arn = ["arn:aws:iam::aws:policy/

ReadOnlyAccess"]

19: account = var.account

20:

351

ChApter 8 AuthentICAtIon And AuthorIzAtIon

21: }

22:

23: # Create an Admin Role with Administrator Access Policy

24: module "assume_admin_role" {

25: source = "./modules/assumerolepolicytrust"

26:

27: role_name = "AssumeRoleAdmin${var.account}"

28: trusted_entity = "arn:aws:iam::${var.identity_account_

id}:root"

29: policy_arn = ["arn:aws:iam::aws:policy/

AdministratorAccess"]

30: account = var.account

31:

32: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

For performing the IAM Roles Mapping, we are using the local variable

called user_role_mapping as shown in the following.

Code Block 8-17. user_role_mapping variable

File: pgitops/global/identity.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

80: locals {

81: user_role_mapping = {

82: developer = [

83: module.staging.assume_dev_role_arn,

84: module.prod.assume_dev_role_arn,

85: module.dev.assume_admin_role_arn

86: ],

87: admin = [

88: module.staging.assume_admin_role_arn,

352

ChApter 8 AuthentICAtIon And AuthorIzAtIon

89: module.prod.assume_admin_role_arn,

90: module.dev.assume_admin_role_arn

91: ],

92: readonly = [

93: module.staging.assume_readonly_role_arn,

94: module.prod.assume_readonly_role_arn,

95: module.dev.assume_readonly_role_arn

96: ]

97: }

98: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

Here I am simply declaring which IAM Role should be assigned to

the role that I’ve defined in the users variable. For example, A developer

role will get access to the dev IAM Roles in the staging and the prod

environment and admin role in the dev environment. Any user who takes

up the developer role will automatically get access to the staging and prod

environment as a developer and the dev environment as an admin.

The following code performs this mapping between the users and their

respective roles by iterating over the user_role_mapping local variable we saw previously.

Code Block 8-18. Looping through user_role_mapping variable

File: pgitops/global/identity.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

103: module "user_role_mapping" {

104: source = "./modules/useriamrolepolicyattachment"

105:

106: for_each = var.users

107:

353

ChApter 8 AuthentICAtIon And AuthorIzAtIon

108: roles = local.user_role_mapping[each.value["role"]]

109: user_name = each. key

110:

111: providers = {

112: aws = aws.identity

113: }

114:

115: depends_on = [module.users]

116: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

Table should help summarize how I’ve configured the IAM Roles access for different users.

354

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Table 8-1. Permissions Table

User

Org Role

IAM Role

Sita

admin

AssumeroleAdminprod

AssumeroleAdminstaging

AssumeroleAdmindev

padma

readonly

Assumerolereadonlyprod

Assumerolereadonlystaging

Assumerolereadonlydev

raadha

developer

Assumeroledeveloperprod

Assumeroledeveloperstaging

AssumeroleAdmindev

8.4 AWS Route53

In the last chapter, we created a Route53 Zone record in the root account

through the AWS CLI. However, in this chapter, we’ll look at how we can

create the same using Terraform and one each for every deployment

account for better segregation as depicted in the following.

355

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Figure 8-5. AWS Route53 Hosted Zone for each account

The following code shows how AWS Route53 zone can be created for

the staging account.

Code Block 8-19. Creating Route53 Hosted Zone for Staging

File: pgitops/global/main.tf

02: module "staging" {

03: source = "./staging"

04:

05: account = "staging"

06: identity_account_id = module.identity_account.id

07: domain = "staging.${var.domain}"

08:

09: providers = {

10: aws = aws.staging

356

ChApter 8 AuthentICAtIon And AuthorIzAtIon

11: }

12:

13: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

L07: This is how I am passing the value of the domain name that is

needed for each environment. Similarly for every environment, I am

passing the respective domain value.

Under the hood in the individual module folder, this is how I am

creating the Route53 Zones by passing the value of the domain at L37 as

shown in the following.

Code Block 8-20. Creating Route53 Hosted Zone for Staging Contd

File: pgitoos/global/staging/main.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

36: resource "aws_route53_zone" "default" {

37: name = var.domain

38:

39: tags = {

40: Account = var.account

41: terraform-managed = "true"

42: }

43: }

8.5 Executing Global

Under the global folder, I’ve added only those resources that need to be

created only once and are required for operation of the staging/production

and development environments. Resources like IAM Users and their Roles,

Route53 Records, and AWS accounts need to be created only once; hence,

357

ChApter 8 AuthentICAtIon And AuthorIzAtIon

they all have been grouped together. It would not be possible to create

these resources along with the application infrastructure that we’ve set

up in the last chapter as things like User’s Access Keys, Route53 records,

and IAM Roles would be needed to create the entire infrastructure for the

application in different environments.

Hence, we’ll first need to ensure that all the terraform code in the

global folder is executed first and then execute each environment. Also, one

crucial information that will be needed while creating the environments is

the account IDs of the environments that we need to operate in.

For example, when spinning up the dev environment, we’ll need to

pass the account ID of the dev environment in terraform code of the infra

folder and same for each of the other accounts. We’ll see why we need the

account ID when I will be executing the infra folder.

8.5.1 Generate GNU PG Keys

Every user who needs access to the AWS Console needs to generate a pair

of GPG keys. The private key is configured in her system, and the public

key is to be shared to the administrator along with an approved level of

access required in the different environments. Once the Terraform is

executed, the administrator shall share the encrypted password string with

every user, which needs to be changed on first login. Once the user logs in,

she can download her access keys to operate on the AWS environment.

Let’s walk through the entire process for user “sita” who is having

administrator access to all environments.

Ensure that GNU PG has been installed as mentioned in the

“Prerequisites” section before going ahead. Once you fire the command

to generate the key as shown in the following, you’ll be asked to enter your

real name and email address and then confirm with an O(kay).

After that, you’ll be prompted to enter your passphrase twice.

358

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Note ensure you don’t lose the passphrase as without that you

won’t be able to decrypt your key back.

CLI Output 8-21. Generating GPG Keys

cmd> gpg --generate- key

prompt> Real name: sita

prompt> Email address: [email protected]

prompt> Change (N)ame, (E)mail, or (O)kay/(Q)uit?: O

prompt> Enter passphrase

prompt> Re-enter passphrase

Let’s now save the public key so that terraform can utilize it to encrypt

your console password. To save the key, first we’ll need to list the keys

to understand the Key ID as highlighted in blue in the following. Once

we have the Key ID, we can export it into the data folder as shown in the

following.

CLI Output 8-22. Listing and Exporting GPG Public Key

cmd> cd pgitops

cmd> gpg -- list-keys

gpg: checking the trustdb

gpg: marginals needed: 3 completes needed: 1 trust model: pgp

gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n,

0m, 0f, 1u

gpg: next trustdb check due at 2024-06-21

/Users/rohitsalecha/.gnupg/pubring.kbx

--------------------------------------

pub ed25519 2022-06-22 [SC] [expires: 2024-06-21]

27B6CF62B1352FC30F3F14C6D3B5FCD325AAEC25

359

ChApter 8 AuthentICAtIon And AuthorIzAtIon

uid [ultimate] sita

sub cv25519 2022-06-22 [E] [expires: 2024-06-21]

cmd> gpg --export 27B6CF62B1352FC30F3F14C6D3B5FCD325AAEC25 |

base64 > global/data/sita.pub

cmd> ls global/data/sita.pub

global/data/sita.pub

Note Similarly, we need to perform the same operations for other

users defined (i.e., padma and radha and have their public keys

saved in the global/data directory). It is extremely important to have

their public keys generated and saved as well because I need to

demonstrate the role-Based Access control that has been configured.

8.5.2 Configure Variables

Next, an important task that we need to do is ensure the pgitops/global/

terraform.auto.tfvars file is properly configured with all the values.

The emails for the various environments must be configured properly

and must be unique and valid as an example shown in the following,

which I’ll be using for showing the rest of the demonstrations through the

book. Please ensure to change these before moving ahead; else you’ll get

an error stating that this account already exists.

Warning these email addresses need to be unique and never used

for creating AWS accounts; if any existing email addresses are used,

you’ll get an error stating the email address already exists.

360

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Code Block 8-23. Configuring emails for the AWS accounts

File: pgitops/global/terraform. auto.tfvars

02: accounts = {

03: dev = {

04: name = "dev"

05: email = "[email protected]"

06: },

07: identity = {

08: name = "identity"

09: email = "[email protected]"

10: },

11: prod = {

12: name = "prod"

13: email = "[email protected]"

14: },

15: staging = {

16: name = "staging"

17: email = "[email protected]"

18: }

19: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

Configure the Users with the exact username and the exact name of the

public key as stored under the data folder as shown in the following.

Code Block 8-24. Configure users and their GPG keys

File: global/terraform. auto.tfvars

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

361

ChApter 8 AuthentICAtIon And AuthorIzAtIon

21: users = {

22: raadha = {

23: username = "raadha"

24: pgp_key = "raadha.pub"

25: role = "developer"

26: },

27: sita = {

28: username = "sita"

29: pgp_key = "sita.pub"

30: role = "admin"

31: },

32: padma = {

33: username = "padma"

34: pgp_key = "padma.pub"

35: role = "readonly"

36: }

37: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

And finally, the org name and the main domain name, which should

be either your subdomain or your top level whichever you’ve chosen in the

previous chapter.

Code Block 8-25. Adding Domain and Org values

File: pgitops/global/terraform. auto.tfvars

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

39: org_name = "gitops"

40: domain = "gitops.rohitsalecha.com"

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

362

ChApter 8 AuthentICAtIon And AuthorIzAtIon

8.5.3 Terraform Cloud Workspace

Next, one very important step is to configure Terraform Cloud

workspace for the global folder. All the information about the state of

the infrastructure setup in the global folder will now be available in

terraform cloud.

Figure 8-6. Terraform Cloud Global workspace

Ensure the name you choose for the Global workspace is also

recorded here.

Code Block 8-26. Global workspace configuration

File: pgitops/global/global.hcl

1: workspaces { name = "global" }

2: hostname = "app.terraform.io"

3: organization = "practicalgitops"

L1: Need to update the name of the global folder

363

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Next, ensure your AWS Keys have been configured as environment

variables. For the Global workspace, we only need the AWS Keys

configured in the variables section; nothing else is needed. These keys are

of the administrator user “gitops” that we created initially.

Figure 8-7. Configuring AWS Keys in Workspace

Note the account that you’ll be using here should not have an

existing AWS organization root account. It should be a fresh account.

8.5.4 Terraform Apply

Now that we are all set to perform our terraform apply, let’s follow the

process of first pushing our changes in a branch, creating a PR, reviewing

the PR, and then merging the branch into master.

CLI Output 8-27. Pushing Code for Global

cmd> git status

On branch main

364

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Your branch is up to date with 'origin/main'.

Changes not staged for commit:

(use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory)

modified: global/ data/padma.pub

modified: global/ data/raadha.pub

modified: global/ data/sita.pub

modified: global/terraform. auto.tfvars

no changes added to commit (use "git add" and/ or "git commit -a")

cmd> git checkout -b global1

Switched to a new branch 'global1'

cmd(global1)> git add .

cmd(global1)> git commit -m “first commit”

cmd(global1)> git push --set-upstream origin global1

As soon as the changes are pushed, you should be able to see the

message on github.com.

Let’s Compare and Pull Request and start the process.

Once the PR is created, you should see the following screen.

365

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Figure 8-8. PR Screen request to merge

After a while, it will start the Terraform Plan and show us the following

in our Github Comments and we can review the full plan.

366

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Figure 8-9. Review Terraform Plan in PR

Once the PR is merged, terraform apply shall start and finally

Terraform output would look something like the following once apply is

finished.

367

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Figure 8-10. Terraform output

8.5.5 Terraform Output

Let’s understand each of the terraform output.

Account IDs

The account IDs need to be noted as we’ll need to configure each account

ID for the respective environments as a Terraform variable, something that

we’ll discuss later while executing the infra folder.

Also, as soon as you see this output, you would also receive a Welcome

email from Amazon on each of the email addresses specified for the

account creation.

Links

The links provided here are just for bookmarking the identity account ID

URL for sign-in and the respective Roles that need to be assumed into

different accounts.

368

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Name Servers

The Name Servers provided here need to be updated in your DNS records

for each of the account as shown in the following.

Code Block 8-28. DNS nameservers

{

"dev": {

"domain": "dev.gitops.rohitsalecha.com",

"name_servers": "ns-1337.awsdns-39.org.\nns-1988.awsdns-56.

co.uk.\nns-42.awsdns-05.com.\nns-654.awsdns-17.net."

},

"prod": {

"domain": "gitops.rohitsalecha.com",

"name_servers": "ns-1046.awsdns-02.org.\nns-1921.awsdns-48.

co.uk.\nns-214.awsdns-26.com.\nns-706.awsdns-24.net."

},

"staging": {

"domain": "staging.gitops.rohitsalecha.com",

"name_servers": "ns-1040.awsdns-02.org.\nns-1595.awsdns-07.

co.uk.\nns-303.awsdns-37.com.\nns-807.awsdns-36.net."

}

}

So as an example, to update the prod account DNS Servers, I need to

add the following as NS Records against gitops.rohitsalecha.com:

ns-1046.awsdns-02.org.

ns-1921.awsdns-48.co.uk.

ns-214.awsdns-26.com.

ns-706.awsdns-24.net.

369

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Figure 8-11. Configuring nameservers

Users

Next, output of the users block consists of the users encrypted temporary

passwords and their assumable roles. As an example, let’s view the details

of the administrator Sita.

Code Block 8-29. Sample Users Output

"sita":{

"role_arns_assigned": [

"arn:aws:iam::1xxxxxxxx9:role/AssumeRoleAdminstaging",

"arn:aws:iam::2xxxxxxxx2:role/AssumeRoleAdminprod",

"arn:aws:iam::9xxxxxxxx97:role/AssumeRoleAdmindev"

],

"temp_password": "wV4Dg8bNTGnmb5USAQdA8+383VwQHuVL3HJ4YXF4

tlnRAYgOVSmJU9fHHV7R1UUwzKJgvFoJi7y+4784x4muIYeO+kialt/

6QfgB8iHPCxedgXovISPJzhYgw8ArlOoJ1FABBwEMkLPQrwCqZE+d3Z

KuwtOBsP7j4xAPYepnY6IBOxj55DiW04VdRLGzaaosLwthHkT5CSPX

crpfhHxpyfFeb2a70i8pXOn5SnJEw8pCA=="

}

Let’s decrypt the passwords encrypted using the GPG key generated

earlier.

370

ChApter 8 AuthentICAtIon And AuthorIzAtIon

CLI Output 8-30. Decrypting the password

cmd> export GPG_TTY=$(tty)

cmd> echo "<temp_password string>" | base64 -d | gpg --decrypt Using the password output on the console and the Identity URL link

provided in the links’ section, you can now log into the AWS Console

using the temporary password. At first, you’ll be prompted to change

your password, which must be done immediately. Once authenticated,

download the keys and configure an AWS Profile on the CLI using the

following commands.

CLI Output 8-31. Configuring Sita’s AWS Profile

cmd> aws configure --profile sita

prompt> AWS Access Key ID : AKIAXXXXXXXXXXXXXXXXXXX

prompt> AWS Secret Access Key : o9PxxxxxxxxxxxxxxxxxxxxxxxxxUc

prompt> Default region name : us-east-2

prompt> Default output format : json

Note You’ll need to repeat all the previous steps for all the other

users in order to see Access Control in action.

8.5.6 Possible Issues

This is a tidy bit of a complicated setup as there are many variables

involved here and in most scenarios executing terraform more than once

may be required, hence just highlighting some possible issues that you

may run into.

371

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Email Already Exists

CLI Output 8-32. Email already exists error

Error: error waiting for AWS Organizations Account (identity) create: unexpected state 'FAILED', wanted target 'SUCCEEDED'.

last error: EMAIL_ALREADY_EXISTS

│ with module.identity_account.aws_organizations_account.

default,

on modules/awsaccounts/main.tf line 3, in resource "aws_

organizations_account" "default":

│ 3: resource "aws_organizations_account" "default" {

This is a typical error if you are using an email address that is already

tied to an AWS account. Every AWS account needs an email; hence any

duplication globally can result in this error.

Hence, the solution here would be to change the email address in the

pgitops/global/terraform.auto.tfvars file and push the changes.

AWS Subscription Not Found

CLI Output 8-33. AWS subscription not found error

Error: error creating Route53 Hosted Zone: OptInRequired: The

AWS Access Key Id needs a subscription for the service

│ status code: 403, request id: 671818a6-464f-4815-b56a-

da2c74b3555c

│ with module.staging.aws_route53_zone.default,

on staging/main.tf line 36, in resource "aws_route53_zone"

"default":

│ 36: resource "aws_route53_zone" "default" {

372

ChApter 8 AuthentICAtIon And AuthorIzAtIon

If you encounter this error, follow the following solution.

The solution to this problem is ridiculously simple: Just login into

your AWS account using the root email address and password of the

management account and rerun the terraform code.

You can quite simply just select ActionsStart new run from the

Terraform Workspace as shown here.

Figure 8-12. Terraform Cloud running apply from cloud

If at all you get any error other than the preceding scenarios, the best

solution is to just search on Google as it’s impossible to cater into all

different scenarios.

8.6 EKS Authz

Services like RDS using MySQL/PostgreSQL or EKS using the Kubernetes

specifications have their own set of authentication when deployed

independently of AWS. However, once AWS provides these services,

the authentication and authorization (authz) to these services are then

managed or sometimes even overridden by AWS IAM for the sole purpose

of uniformity.

Having a uniform/standard approach toward authentication is very

important and strongly emphasized by AWS.

373

ChApter 8 AuthentICAtIon And AuthorIzAtIon

In AWS EKS as well, the authentication to the Kubernetes cluster is

through AWS IAM and not utilizing the existing methods for authenticating

to Kubernetes. We’ll be exploring the following different authentication

mechanisms employed by EKS:

A. Client Authentication – Authentication of kubectl to

AWS EKS.

B. Node Authentication – How Nodes authenticate and

join the EKS Cluster.

C. Pod Authentication – How individual pods running

on EKS authenticate to AWS. This we shall explore in

the next chapter.

8.6.1 EKS Client Authentication

Figure 8-13. EKS Client Authentication

The preceding diagram shows a very high-level overview of how AWS IAM

Authentication is used for authenticating a user and Kubernetes RBAC

374

ChApter 8 AuthentICAtIon And AuthorIzAtIon

(Role-Based Access Control) is used for implementing authorization. The

following are the highlights of the authentication and authorization process:

– AWS IAM recognizes an IAM User or an IAM Role as an

identifier as an AWS Identity. Hence, while authenticat-

ing using kubectl, the identifier information is first

passed to the API Server along with the authentication

token generated by the “aws eks get-token.” If you can

recall, this is the same command that is configured in

the kubernetes.tf file where we’ve declared the

Kubernetes provider.

– EKS API Server relays this information to AWS IAM and

performs the verification. This verification is nothing

but authentication.

– Once the authentication is successful, then the EKS API

Server checks the aws-auth ConfigMap that stores the

information about IAM Role Mapping against the

Kubernetes Groups. Based on this information, it

authorizes the AWS Identity to access the cluster.

This is how any client, be it kubectl or terraform, needs to perform to

access the Kubernetes cluster. In our case, we’ve implemented the AWS

Identity as an IAM Role and not an IAM User.

The following is the same AWS-Auth ConfigMap that shows the

mapping between the IAM Role that we created in the global folder for the

dev account.

Code Block 8-34. AWS ConfigMap (sample)

apiVersion: v1

data:

mapRoles: |

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

375

ChApter 8 AuthentICAtIon And AuthorIzAtIon

- rolearn: arn:aws:iam::XXXXXXXXXXX:role/AssumeRoleEKSAdmindev

username: admin

groups:

- admin

- rolearn: arn:aws:iam::XXXXXXXXXXX:role/

AssumeRoleEKSDeveloperdev

username: developer

groups:

- developer

- rolearn: arn:aws:iam::XXXXXXXXXXX:role/

AssumeRoleEKSReadOnlydev

username: readonly

groups:

- readonly

kind: ConfigMap

metadata:

name: aws-auth

namespace: kube-system

The following code is the template that is used to generate the

preceding ConfigMap.

Code Block 8-35. AWS ConfigMap creation in Terraform

File: pgitops/infra/rolebindings.tf

2: data "aws_caller_identity" "current" {}

3:

4: locals {

5:

6: # Creating the AWS-AUTH

07: aws_auth_configmap_yaml = <<-EOT

08: ${chomp(module.eks.aws_auth_configmap_yaml)}

376

ChApter 8 AuthentICAtIon And AuthorIzAtIon

09: - rolearn: arn:aws:iam::${data.aws_caller_identity.

current.id}:role/AssumeRoleAdmin${var.environment}

10: username: admin

11: groups:

12: - system:masters

13: - rolearn: arn:aws:iam::${data.aws_caller_identity.

current.id}:role/AssumeRoleDeveloper${var.

environment}

14: username: developer

15: groups:

16: - developer

17: - rolearn: arn:aws:iam::${data.aws_caller_identity.

current.id}:role/AssumeRoleReadOnly${var.

environment}

18: username: readonly

19: groups:

20: - readonly

21: EOT

22:

23: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

L09-12: We are first fetching the ID of the current account where

Terraform is executing, which is also the same environment configured.

We’ve templated the name of the IAM Role to match the environment. Here,

we are mapping the Admin IAM Role with the system-masters, which is a

default administrator group in Kubernetes. So any user assuming this role

will access the Kubernetes cluster with admin privileges.

L13-16: Here we are associating the Developer IAM Role with the

developer group created in Kubernetes. So any user who would be

assuming this role will access the Kubernetes cluster with developer

privileges. Same is implemented for the readonly group as well.

377

ChApter 8 AuthentICAtIon And AuthorIzAtIon

The following code shows how using Terraform we are creating the

developer Role and RoleBinding and assigning it to the “developer” group.

Code Block 8-36. Kubernetes RoleBindings

File: infra/rolebindings.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

27: resource "kubernetes_role_v1" "developer" {

28: metadata {

29: name = "developer"

30: namespace = kubernetes_namespace_v1.app.metadata.0.name

31: }

32:

33: rule {

34: api_groups = ["*"]

35: resources = ["pods", "deployments", "services",

"ingresses", "namespaces", "jobs", "daemonset"]

36: verbs = ["*"]

37: }

38: }

39:

40: resource "kubernetes_role_binding_v1" "developer" {

41: metadata {

42: name = "developer"

43: namespace = kubernetes_namespace_v1.app.metadata.0.name

44: }

45: role_ref {

46: api_group = "rbac.authorization.k8s.io"

47: kind = "Role"

48: name = "developer"

49: }

378

ChApter 8 AuthentICAtIon And AuthorIzAtIon

50: subject {

51: kind = "Group"

52: name = "developer"

53: api_group = "rbac.authorization.k8s.io"

54: }

55: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

L27-31: Here I am creating the developer role and assigning it specific

privileges that a developer may need while accessing the Kubernetes

cluster.

L40-55: Here we are binding the developer role to a group called

“developers,” which is what is mapped in the aws-auth ConfigMap as well.

Similarly, I’ve created the readonly group as well in the same file.

8.6.2 EKS Node Authentication

Node authentication is happening with the kubelet utility installed on

the node servers. However, it still needs a specific AWS Identity as all

authentication is with AWS IAM only and not with any X509 certificates

that are traditionally used for Node authentication.

Let’s look at the sample aws-auth ConfigMap again to better

understand how node authentication works as shown in the following.

Code Block 8-37. AWS ConfigMap post apply

apiVersion: v1

data:

mapRoles: |

- rolearn: arn:aws:iam::XXXXXXXXXXX:role/app-eks-node- gro

up-20220208122024106100000003

username: system:node:{{EC2PrivateDNSName}}

379

ChApter 8 AuthentICAtIon And AuthorIzAtIon

groups:

- system:bootstrappers

- system:nodes

- rolearn: arn:aws:iam::XXXXXXXXXXX:role/system-eks-node- gr

oup-20220208122024106000000002

username: system:node:{{EC2PrivateDNSName}}

groups:

- system:bootstrappers

- system:nodes

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

kind: ConfigMap

metadata:

name: aws-auth

namespace: kube-system

Here we can see that the mapping is between a specific IAM Role

and the groups as Kubernetes default groups of “system:bootstrappers”

and “system:nodes” that have the capabilities to join and manage

worker nodes.

This IAM Role is created by the EKS Module and has very specific

permissions that allow joining to the master node.

Hence, when kubelet needs to authenticate to the AWS IAM, it’ll

make use of this specific IAM Role as the AWS Identity to AWS IAM for

authentication.

Previously, it can be understood that aws_auth ConfigMap is an

extremely critical resource and hence I’ve created it separately as shown in

the following.

Code Block 8-38. Implementing AWS-Auth ConfigMap

File: pgitops/infra/rolebindings.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

380

ChApter 8 AuthentICAtIon And AuthorIzAtIon

091: resource "kubectl_manifest" "aws_auth" {

092: yaml_body = <<YAML

093: apiVersion: v1

094: kind: ConfigMap

095: metadata:

096: labels:

097: app.kubernetes.io/managed-by: Terraform

098: name: aws-auth

099: namespace: kube-system

100: ${local.aws_auth_configmap_yaml}

101: YAML

102:

103: depends_on = [module.eks]

104:

105: }

It is important to study the terraform plan generated during apply as

well as destroy as it indicates the approximate order in which the resources

will be created/destroyed. In both cases, the order is extremely important

and depends_on meta-attribute plays a pivotal role.

For example, if we don’t add “resource.kubectl_manifest.aws_auth” as

a depending resource for the namespace creation/destruction as shown

on L10, then terraform will happily destroy the aws_auth ConfigMap prior

to destroying the namespace, which will lead to errors and the namespace

will need to be manually destroyed.

Code Block 8-39. AWS-AUTH ConfigMap and Namespace

dependency

File: pgitops/infra/app.tf

03: resource "kubernetes_namespace_v1" "app" {

04: metadata {

381

ChApter 8 AuthentICAtIon And AuthorIzAtIon

05: annotations = {

06: name = var.org_name

07: }

08: name = var.org_name

09: }

10: depends_on = [module.eks, resource.kubectl_manifest.

aws_auth]

11: }

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

L10: Explicitly defining the dependency of aws_auth ConfigMap before

creating the namespace.

Note It’s also worth noting that the way I am creating the aws-auth

ConfigMap is not a standard terraform format. I’m using a third-party

provider called kubectl_manifest whose detailed documentation can

This provider is used when it’s required to execute yaml files directly as

we do using the kubectl utility such as kubectl apply -f k8s.yaml.

8.6.3 Provider Configuration

In the previous chapter, we were passing the identity as an AWS User by

simply configuring the AWS provider with ACCESS KEY ID and SECRET

ACCESS KEY. This worked well as I was creating resources within the same

account.

382

ChApter 8 AuthentICAtIon And AuthorIzAtIon

However, here I have all our users created in the identity account,

and all resources need to be created in the accounts of their respective

environments. Hence, if we need to access the other accounts using the

identity account, then we need to perform Cross-Account IAM Role access,

and hence, the IAM Role ARN has been specified in the assume_role

section on L51-52 as shown in the following.

Code Block 8-40. Cross-Account Provider configuration

File: pgitops/infra/providers.tf

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

47: locals {

48: assume_role_arn = "arn:aws:iam::${var.assume_role_

account_id}:role/AssumeRoleAdmin${var.environment}"

49: }

50: provider "aws" {

51: assume_role {

52: role_arn = local.assume_role_arn

53: }

54: region = var.region

55: }

The role that is being accessed is the Administrator IAM role that has

been created for each environment. So only a user who has access to this

IAM Role will be able to execute these Terraform files. In our case, it is

the user Sita whose access keys need to be configured in Terraform Cloud

staging and prod workspaces. However, in most scenarios, a Terraform

user can also be created and assigned the IAM Administrator role or any

specific high privilege role.

The variable var.assume_role_account_id is a new variable that has

been added that holds the value of the account id where the role needs to

be accessed. So, when we are running in prod, we need to populate this

value with the prod account ID and so on.

383

ChApter 8 AuthentICAtIon And AuthorIzAtIon

However, the default value for this would be the development

environment value and needs to be added in the terraform.auto.tfvars file

as shown in the following.

Code Block 8-41. Adding the development environment

Account ID

File: pgitops/infra/terraform.auto.tfvars

42: assume_role_account_id = "5XXXXXXXXXXXX5"

For prod and staging environments, this value needs to be added in

the respective Terraform Cloud workspaces in the variables section, which

we’ll see when we are ready to execute our infra changes.

In the pgitops/infra/kubernetes.tf file, we also need to update our aws eks get-token command to pass in the role arn to be able to assume the role as shown on L12-13.

Code Block 8-42. Adding Assume Role configuration in the

Kubernetes provider

File: infra/kubernetes.tf

03: provider "kubernetes" {

04: host = module.eks.cluster_endpoint

05: cluster_ca_certificate = base64decode(module.eks.cluster_

certificate_authority_data)

06: exec {

07: api_version = "client.authentication.k8s.io/v1alpha1"

08: command = "aws"

09: args = [

10: "eks",

11: "get-token",

12: "--role-arn",

13: local.assume_role_arn,

384

ChApter 8 AuthentICAtIon And AuthorIzAtIon

14: "--cluster-name",

15: module.eks.cluster_id

16: ]

17: }

8.7 Executing Infra

Let us now go ahead and start deployment of the application and its

related infrastructure in different environments. We shall first start with the

deployment into the development environment on the local system, and

then the changes will be migrated to the staging and prod environments,

which will be executed from the Terraform Cloud workspace.

8.7.1 AWS Profiles

Before we move ahead, we need to ensure that we’ve configured the

AWS Profiles of all the users that have been created (i.e., Sita, Padma

and Raadha). The following is the sample AWS Credentials file that I’ve

configured for Sita, which includes the ability to be able to assume roles

into different accounts. The same needs to be configured for Padma and

Raadha as well.

Code Block 8-43. AWS Credential file for Sita

[sita]

aws_access_key_id = AKIAxxxxxxxxxxWMX

aws_secret_access_key = o9PRxxxxxxxxxxxxxxxxxxxxxxxxxWfUc

[sita-dev]

role_arn = arn:aws:iam::9xxxxxxxxx97:role/AssumeRoleAdmindev

source_profile = sita

[sita-prod]

role_arn = arn:aws:iam::2xxxxxxxxx42:role/AssumeRoleAdminprod

385

ChApter 8 AuthentICAtIon And AuthorIzAtIon

source_profile = sita

[sita-staging]

role_arn = arn:aws:iam::1xxxxxxxxx9:role/AssumeRoleAdminstaging

source_profile = sita

You’ll need to update your account numbers as per the output shown

in your Terraform Cloud Global workspace. Let’s test the profiles if they

have been properly configured using the following commands.

Note everywhere for infrastructure provisioning we’ll be using

Sita’s access key as she has the administrator rights. We can always

create a terraform user and assign it those rights.

Code Block 8-44. Accessing all AWS Profiles configured

cmd> export AWS_PROIFLE=sita

cmd> aws sts get-caller-identity

{

"UserId": "AIDAWG4C7562DXE4Y3C3Q",

"Account": "4xxxxxxxxxx0",

"Arn": "arn:aws:iam::4xxxxxxxxxx0:user/sita"

}

cmd> export AWS_PROFILE=sita-prod

cmd> aws sts get-caller-identity

{

"UserId": "AROATOK7P4B7EGWNWTDXN:botocore-

session- 1656261720",

"Account": "2xxxxxxxxxxx2",

"Arn": "arn:aws:sts::2xxxxxxxxxxx2:assumed-role/

AssumeRoleAdminprod/botocore-session-1656261720"

}

386

ChApter 8 AuthentICAtIon And AuthorIzAtIon

8.7.2 Development

Let’s first start our development environment but prior to doing that

ensure that the value of assume_role_account_id in the pgitops/infra/

terraform.auto.tfvars file is populated with the development account id.

Code Block 8-45. Set up the development environment

cmd> cd pgitops/infra

cmd> export AWS_PROFILE=sita-dev

cmd> terraform init

cmd> terraform validate

cmd> terraform apply --auto-approve

Terraform apply would take about ~15 minutes, and DNS propagation

would take another 5 minutes after terraform apply is completed. The

following is the post that we can see our application up and running.

Figure 8-14. Development environment up and running

387

ChApter 8 AuthentICAtIon And AuthorIzAtIon

8.7.3 Staging

Open up your terraform cloud staging workspace and ensure that the

following variables are properly configured.

AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY should be

of the sita user as she has the rights to assume administrator roles into all

accounts.

assume_role_account_id variable must be populated with the staging

account id.

Figure 8-15. Adding an Assume Role Account ID variable in staging

workspace

Next, let’s create a new branch called staging and then check in the

code. Since this is the first time we are creating the staging branch and

since we’ve tested everything is working in development, terraform apply

would run directly as per the workflow rules setup in .github/workflows/

infra-staging.yaml :

cmd> git status

On branch main

Your branch is up to date with 'origin/main'.

Changes not staged for commit:

(use "git add <file>..." to update what will be committed)

388

ChApter 8 AuthentICAtIon And AuthorIzAtIon

(use "git restore <file>..." to discard changes in working directory)

modified: infra/terraform.auto.tfvars

no changes added to commit (use "git add" and/or "git

commit -a")

cmd> git checkout -b staging

cmd(staging)> git add .

cmd(staging)> git commit -m "deploying to staging"

cmd(staging)> git push --set-upstream origin staging

Staging environment live !

Figure 8-16. Staging environment is up

8.7.4 Prod

Let’s now create and merge the PR and deploy the changes into the prod

environment!

389

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Figure 8-17. PR for prod deployment

Prod environment is live !

Figure 8-18. Prod environment is up

8.8 EKS Access Control

Now that all three environments are live, let’s see how access control has

been implemented in the prod EKS environment.

390

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Before we move ahead, let’s have a summary of the Users and their

Roles for better understanding.

Table 8-2. AWS User Kubernetes Permissions

User

Role

K8s Permissions

Sita

Admin

perform any actions on all resources

raadha developer perform any action on pods, deployments, services, ingresses,

namespaces, jobs, daemonsets

padma readonly perform only read action on

pods, deployments, services, ingresses, namespaces, jobs,

daemonsets

Let’s understand the access control for Sita by executing the

commands as shown in the following. Since she is the administrator, she

has full access.

CLI Output 8-46. Testing Kubernetes Auth for Sita in Prod

cmd> export AWS_PROFILE=sita-prod

cmd> aws eks --region us-west-1 update-kubeconfig --name

gitops-us-west-1-prod

cmd> kubectl auth can-i get nodes

yes

cmd> kubectl auth can-i create pod

yes

cmd> kubectl auth can-i get secrets -n gitops

yes

391

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Let’s switch profile to that of Raadha and observe that she being

a developer has only read/write access to limited resources in gitops

namespace only.

CLI Output 8-47. Testing Kubernetes Auth for Radha in Pro

cmd> export AWS_PROFILE=radha-prod

cmd> aws eks --region us-west-1 update-kubeconfig --name

gitops-us-west-1-prod

cmd> kubectl auth can-i get nodes

no

cmd> kubectl auth can-i create pod

no

cmd> kubectl auth can-i create pod -n gitops

yes

cmd> kubectl auth can-i get secrets -n gitops

no

Let’s now switch profile and see the access that Padma has, which is

readonly and that too limited to gitops namespace only.

CLI Output 8-48. Testing Kubernetes Auth for Padma in Prod

cmd> export AWS_PROFILE=padma-prod

cmd> aws eks --region us-west-1 update-kubeconfig --name

gitops- us-west-1-prod

cmd> kubectl auth can-i get nodes

no

cmd> kubectl auth can-i create pod -n gitops

no

392

ChApter 8 AuthentICAtIon And AuthorIzAtIon

cmd> kubectl auth can-i get pod -n gitops

yes

cmd> kubectl auth can-i get pod

no

8.9 Clean-Up

It’s best that we do a proper clean-up for this chapter as we are using 3X

resources. We won’t be needing staging and prod environments in the

next chapters as we’ll be focusing on different aspects like security and

observability. Hence, it’s best to destroy all environments, and we’ll spin

them up again in next chapters.

8.9.1 Dev

Dev environment was created from the local machine; hence, we only

need to execute the following commands to destroy it.

CLI Output 8-49. Destroying Dev Environment

cmd> export AWS_PROFILE=sita-dev

cmd> cd pgitops/infra

cmd> terraform destroy --auto-approve

393

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Note At any point if terraform is unable to destroy, please check

Annexure A where steps have been presented to delete the resources

manually.

8.9.2 Staging/Prod

Staging and prod environments need to be destroyed from the

Terraform Cloud workspace as shown in the following by navigating to

SettingsDestruction and Deletion. Similar steps need to be followed for the prod environment as well.

Figure 8-19. Destroying the staging environment

394

ChApter 8 AuthentICAtIon And AuthorIzAtIon

Note At any point if terraform Cloud is erroring to destroy, please

check Annexure B where steps have been presented to delete the

resources manually. If this doesn’t work, then follow Annexure A.

The global environment can also be destroyed in the same

manner; however, hold on for now as we’ll be needing it for

the next two chapters ahead. Hence, please keep the Global

workspace as it is.

8.10 Conclusion

In this long chapter, we saw how Access Control can be implemented using

AWS IAM on the AWS environment in general as well as on the AWS EKS

environment completely through Terraform and that too in a completely

automated Git-controlled manner.

In the next chapter, we’ll explore a few other security measures that

are necessary for cloud environments like Secret encryption, HTTPS

redirection, Network Segregation, Service Control Policies, implementing

security scanning for IaC, and how pods running on EKS can access AWS

services securely!

395

CHAPTER 9

Security and Secrets

Management

In this chapter, we’ll look into various different areas pertaining to the

security of our application and infrastructure. We’ll discuss how to manage

the database using AWS Secrets Manager and how it can directly be

injected into the pods. Then we’ll look at a few small aspects of securing

our ALB and restricting the network for the RDS. After that, we’ll look at

how pods can authenticate to AWS and access different resources. Then

we’ll look at how the disk of the EKS nodes can be encrypted using AWS

KMS and how we can enforce Service Control Policies or SCPs on different

OUs such that developers cannot spin up expensive EC2 resources. Finally,

we’ll end by adding a tool called Checkov in the GitHub Actions pipeline,

which will continuously spill out security issues on every PR.

9.1 Code Update

Before diving into the code, let’s first ensure that we’ve updated the pgitops

directory with the code from the Chapter . To do this, follow these steps:

First copy the contents of the Chapt folder into the pgitops folder.

Get inside the pgitops directory and then check which

files/folders are changing.

© Rohit Salecha 2023

397

R. Salecha, Practical GitOps,

Chapter 9 SeCurity and SeCretS ManageMent

We shall commit only the global and .github directories

as we need the changes to be reflected in the global

environment.

Changes done in the infra folder can be committed,

and the CI can be skipped as we’ll be testing the

changes only in the development environment for this

chapter.

CLI Output 9-1. Updating Chapter 9 code

cmd> cp -a chapter9/. pgitops/

cmd> cd pgitops

cmd> git status

On branch main

Your branch is up to date with 'origin/main'.

Changes not staged for commit:

(use "git add <file>..." to update what will be committed)

(use "git restore <file>..." to discard changes in working

directory)

modified: .github/workflows/global.yaml

modified: .github/workflows/infra-prod.yaml

modified: .github/workflows/infra-staging.yaml

modified: README.md

modified: global/README.md

modified: global/organisations.tf

modified: infra/README.md

modified: infra/app.tf

modified: infra/eks.tf

modified: infra/modules/alb/iam_policy.json

modified: infra/modules/alb/main.tf

modified: infra/modules/alb/variables.tf

modified: infra/modules/ekscluster/main.tf

398

Chapter 9 SeCurity and SeCretS ManageMent

modified: infra/modules/ekscluster/variables.tf

modified: infra/networking.tf

modified: infra/rds.tf

modified: infra/variables.tf

Untracked files:

(use "git add <file>..." to include in what will be committed) global/data/deny_creating_users.json

global/data/ebs_rds_encryption_check.json

global/data/instance_type_limit.json

global/data/padma.pub.old

global/data/raadha.pub.old

global/data/region_lock.json

global/data/sita.pub.old

global/modules/awsorgpolicy/

global/scp.tf

global/terraform.auto.tfvars.old

infra/data/

infra/modules/awskms/

infra/modules/awssm/

infra/modules/csisecretsdriver/

infra/modules/irsa/

infra/terraform.auto.tfvars.old

no changes added to commit (use "git add" and/or "git commit -a") The changes can summarized as follows:

Enhancing security of EKS by encrypting the EBS using

the default AWS KMS key

Encrypting secrets stored in EKS using the custom AWS

KMS key.

Enhancing AWS ALB security using mandatory HTTPS

redirection and dropping invalid headers.

399

Chapter 9 SeCurity and SeCretS ManageMent

Restricting RDS to private subnet and not VPC.

Installing Secrets Store CSI (Container Storage

Interface) Driver that fetches secrets directly from AWS

Secrets Manager reducing exposure of secrets in

K8s files.

Implementing and understanding IRSA (IAM Roles for

Service Accounts) that fetches secrets from AWS Secrets

Manager.

In the workflow files, we’ve added a new step that

introduces a security scanning tool called “Checkov”

his tool scans terraform files for known bad security practices.

We’ve added multiple Service Control Policies (SCPs):

To lock the regions of deployment

Ensuring that only encrypted EBS and RDS are

deployed in staging/production

Disabling user creation in all accounts except identity

Restricting the instance types that can be created in

Development OU

9.1.1 Committing only Global

For the sake of simplicity and for ensuring that the global folder is

committed, let’s execute the following commands.

Note - - (double hyphens) after the commit message indicates that

we wish to commit only the specific directories, which is exactly the

use case here.

400

Chapter 9 SeCurity and SeCretS ManageMent

CLI Output 9-2. Comitting only changes in the Global folder

cmd> git add .

cmd> git commit -m 'committing only global and .github' --

global .github

cmd> git push

Once we fire git push, it can be observed that only global changes have

gone live as shown here.

Figure 9-1. Global folder Comit triggering action

With this, the following items have been created:

All the SCPs described previously have been created

and now operational.

Our new GitHub workflow with Checkov is now

operational.

401

Chapter 9 SeCurity and SeCretS ManageMent

9.1.2 Committing Infra

The other changes, that is, infra, etc., are still in staging in our local system

as shown in the following. Let’s push these changes as well but without

triggering the GitHub Actions, that is, by adding [skip ci] in our commit

message.

CLI Output 9-3. Skipping comitting changes in the infra folder

cmd> git status

On branch main

Your branch is up to date with 'origin/main'.

Changes to be committed:

(use "git restore --staged <file>..." to unstage)

modified: README.md

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

modified: infra/rds.tf

new file: infra/terraform.auto.tfvars.old

modified: infra/variables.tf

cmd> git commit -m '[skip ci] committing infra'

cmd> git push

9.2 Enhancing EKS Security

As stated earlier, the master node or the control plane of Kubernetes

in AWS EKS is managed completely by AWS. Hence, AWS takes full

responsibility of keeping it highly available and secure. However, security

of the data plane components like Kubernetes Secrets and of the worker

nodes (which are nothing but EC2 instances) like EBS encryption,

implementing IMDSv2 metadata, etc., needs to be done by us. In this

section, we’ll see how that can be done completely through terraform.

402

Chapter 9 SeCurity and SeCretS ManageMent

9.2.1 Encrypting K8s Secrets

One of the best practices of Kubernetes security is to encrypt the secrets.

We need encryption keys that’ll be used to encrypt the secrets. AWS KMS

(Key Management Services) is a popular service that has two types of keys:

Managed by AWS – These keys are generated and

managed by AWS internally and are not chargeable.

Managed by User – These keys are also generated by

AWS but managed by us and are chargeable.

If you recall, we set up our RDS database using the code as shown in

the following in pgitops/infra/rds.tf.

Code Block 9-4. Encrypting PostgreSQL

File: pgitops/infra/rds.tf

39: module "pgsql" {

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

52: storage_encrypted = true

53: db_name = var.db_name

54: username = var.db_user_name

XXXXXXXXXXXXXXX------SNIPPED-------XXXXXXXXXXXXXXXXXXXXX

84: }

85:

L52: By having the storage_encrypted=true, the module internally is

using the default AWS KMS keys and encrypting the entire database.

However, for storing secrets in Kubernetes, let’s make use of a custom

AWS KMS key, which is generated using the following module.

403

Chapter 9 SeCurity and SeCretS ManageMent

Code Block 9-5. Creating a KMS key

File: pgitops/infra/eks.tf

15: module "eks_kms" {

16: source = "./modules/awskms"

17:

18: environment = var.environment

19: description = "KMS Key to encrypt secrets in

the Cluster"

20: kms_alias = "eks2"

21: deletion_window_in_days = 7

22:

23: }

L20: This value should be unique for the account as it helps as an

identifier to the key.

L21: KMS keys are not deleted immediately; hence, we can configure

the time uptil which we can retrieve the keys back, which is generally

between 7 and 30 days.

We are then using the ARN (Amazon Resource Name) of the key

generated from the preceding module and then pass it into the EKS

module as shown here.

Code Block 9-6. Attaching the KMS key to EKS

File: pgitops/infra/eks.tf

25: module "eks" {

26: source = "./modules/ekscluster"

27:

28: clustername = module.clustername.cluster_name

29: eks_version = var.eks_version

404

Chapter 9 SeCurity and SeCretS ManageMent

30: private_subnets = module.networking.private_subnets_id

31: vpc_id = module.networking.vpc_id

32: environment = var.environment

33: kms_key_arn = module.eks_kms.key_arn

34: instance_types = var.instance_types

35:

36: }

L33: Reference to the KMS Key ARN from the KMS module.

Let’s dive inside the module and see how exactly the value is

being passed.

Code Block 9-7. Encrypting all Kubernetes Secrets with the

AWS KMS key

Назад: 3.9 Clean-Up
Дальше: 9.2.2 Encrypting EKS EBS