Книга: Practical GitOps
Назад: 7.4.1 Database Module
На главную: Предисловие

File: pgitops/infra/modules/ekscluster/main.tf

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

19:

20: cluster_encryption_config = [{

21: provide r_key_arn = var.kms_key_arn

22: resources = ["secrets"]

23: }]

24:

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

L21: Providing reference of the KMS Key ARN, which must be used to

encrypt the data.

L22: Configuring the Kubernetes resources that need to be encrypted;

currently, only “secrets” are supported.

405

Chapter 9 SeCurity and SeCretS ManageMent

9.2.2 Encrypting EKS EBS

EBS is the storage device attached to the EC2 instance, and most

compliance frameworks require it to be encrypted. There are two ways in

which this can be achieved: either using a custom-generated key or using

the AWS-provided key. A custom-generated key will incur cost and also

will require additional permissions. Hence, to keep things simple, I am

using an AWS-provided key by simply initializing the kms_key_id variable

as blank.

Using the following block of code in the EKS module, we can configure

the EBS device.

Code Block 9-8. Encrypting EBS block volume

File: pgitops/infra/modules/ekscluster/main.tf

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

66: block_device_mappings = {

67: xvda = {

68: device_name = "/dev/xvda"

69: ebs = {

70: volume_size = 75

71: volume_type = "gp3"

72: iops = 3000

73: throughput = 150

74: encrypted = true

75: kms_key_id = ""

76: delete_on_termination = true

77: }

78: }

79: }

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

406

Chapter 9 SeCurity and SeCretS ManageMent

9.2.3 Enhancing EC2 Metadata Security

AWS provides an internal functionality, using which, EC2 instances can

securely obtain privileges to perform actions on behalf of a user/account.

This privilege/permission can be obtained by first fetching the IAM Role by

performing a GET request t

oint. This provides an IAM Role name, which is then used to assume the permissions associated

with it.

All was well until someone figured out that using an SSRF (Server-

Side Request Forgery), this GET request could be spoofed by attackers

with malicious intent. So basically if a web application is vulnerable to

SSRF bug, it opens up the possibility of the attacker gaining access to

the temporary IAM Role permissions and hence accessing your AWS

environment. More on this attack can be read her

.

The IMDSv1 service was heavily abused by attackers to launch SSRF

attacks, and hence, IMDSv2 was released, which fixes this issue.

The following block of code can be utilized to set up the IMDSv2

(Instance Metadata Service) configuration, which is also a major security

requirement.

Code Block 9-9. Configuring all EC2 metadata options for

enabling IMDSv2

File: infra/modules/ekscluster/main.tf

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

81: metadata_options = {

82: http_endpoint = "enabled"

83: http_tokens = "required"

84: http_put_response_hop_limit = 2

407

Chapter 9 SeCurity and SeCretS ManageMent

85: instance_metadata_tags = "disabled"

86: }

87: }

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

Hence, IMDSv2 avoids this by ensuring that a specific header needs to

be passed in order to call the IMDS endpoint.

For more information on the attack and IMDSv2, refer to this link:

.

9.3 Enhancing AWS ALB Security

Any traffic reaching our application first needs to go through the load

balancer, and hence, ensuring certain security best practices on the

load balancer is very important. In this chapter, we’ll look at how we can

perform a mandatory HTTPS redirection and dropping of invalid header

values to combat specific application attacks. In the next chapter, we’ll look

at how we can store AWS ALB logs in an S3 bucket.

Allowing the website to be served over HTTP, that is, port 80, renders

it vulnerable to man-in-the-middle attacks where attackers can read and

tamper with the data in the request. Hence, it’s most commonly advisable

to have the HTTP port redirect to HTTPS, and the same is achievable

by configuring the following annotation in the ingress resource as

shown here.

Code Block 9-10. Securing ALB against malformed HTTP headers

and HTTPS redirection

File: pgitops/infra/app.tf

233: annotations = {

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

408

Chapter 9 SeCurity and SeCretS ManageMent

238: "alb.ingress.kubernetes.io/listen-ports" = "[{\"HTTP\": 80}, {\"HTTPS\":443}]"

239: "alb.ingress.kubernetes.io/actions.ssl-redirect"

= "{\"Type\": \"redirect\", \"RedirectConfig\":

{ \"Protocol\": \"HTTPS\", \"Port\": \"443\",

\"StatusCode\": \"HTTP_301\"}}"

240: "alb.ingress.kubernetes.io/load-balancer-attributes" =

"routing.http.drop_invalid_header_fields.enabled=true"

241: }

242: }

243: spec {

244: rule {

245: host = local.url

246: http {

247: path {

248: backend {

249: service {

250: name = "ssl-redirect"

251: port {

252: name = "use-annotation"

253: }

254: }

255: }

256: path = "/*"

257: }

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

274: }

L238: Declaring the ingress to open two listener ports in the AWS Load

Balancer that will be created.

409

Chapter 9 SeCurity and SeCretS ManageMent

L239: This is where we declare the annotation that configures the

redirection logic and the HTTP redirection code, that is, 301, which needs

to be used.

L247–257: The implementation logic of the HTTPS redirection at the

root path “/”.

L240: Having this configuration that is to disable invalid header

fields is very important as it helps in invalidating requests that do not

have proper HTTP headers. This helps in mitigating a class of application

vulnerabilities called HTTP Request Smuggling, which leverages

the desynchronization between the load balancer and its service on

acceptance of the content-length vs. transfer-encoding headers.

More information about this attack can be obtained from

9.4 Restricting RDS Exposure

Currently, our RDS service is exposed to the entire VPC as the security

group that was defined had the entire 10.0.0.0/16 CIDR address. This is

now solved by restricting the security group to allow access to the RDS

server on port 5432 only from the private subnet as shown here.

Code Block 9-11. Restricting AWS RDS access from the

private subnet

File: pgitops/infra/networking.tf

53: module "pgsql_sg_ingress" {

54: source = "./modules/securitygroup"

55:

56: sg_name = "pgsql_sg_ingress"

57: sg_description = "Allow Port 5432 from within the

Private Subnet"

410

Chapter 9 SeCurity and SeCretS ManageMent

58: environment = var.environment

59: vpc_id = module.networking.vpc_id

60: type = "ingress"

61: from_port = 5432

62: to_port = 5432

63: protocol = "tcp"

64: cidr_blocks = var.private_subnets_cidr

65:

66: depends_on = [module.networking]

67: }

L64: This was earlier scoped to the entire VPC and now narrowed

down to only the private subnet where our application is currently

deployed. Hence, only services deployed in this private network will have

access to the RDS database on port 5432.

9.5 Secrets Exposure

Currently, in our application, the most sensitive information is the

database password. Let’s identify the places where this password can be

potentially leaked or visible in clear-text.

9.5.1 Terraform State

If we were to simply grep for the string “password” in our terraform state, it

can reveal various different places where the database password string can

be found.

411

Chapter 9 SeCurity and SeCretS ManageMent

CLI Output 9-12. Searching for the password string in

terraform state

cmd> grep "password" terraform.tfstate

"module.pgsql.random_password.master_password",

"module.ssmr-db-password.data.aws_ssm_parameter.

parameter",

"module.ssmw-db-password.aws_ssm_parameter.ssm_

parameter"

"module.pgsql.random_password.master_password",

"module.ssmr-db-password.data.aws_ssm_parameter.

parameter",

"module.ssmw-db-password.aws_ssm_parameter.ssm_

parameter"

"repository_password": null,

"type": "random_password",

"name": "master_password",

"password": "EvlNUope03cXdIlG",

"value": "password"

If we were to now search for the value of the password string in the

terraform state, we’ll get a better idea of the number of places where the

password is stored in clear-text, a total of five locations as shown here.

CLI Output 9-13. Reverse lookup of the password string to know

occurrences

cmd> grep "EvlNUope03cXdIlG" terraform.tfstate

"value": "EvlNUope03cXdIlG",

"result": "EvlNUope03cXdIlG",

"password": "EvlNUope03cXdIlG",

"value": "EvlNUope03cXdIlG",

"value": "EvlNUope03cXdIlG",

412

Chapter 9 SeCurity and SeCretS ManageMent

Hence, it's extremely important that access to terraform state is

restricted to select few individuals only.

9.5.2 Kubernetes Deployment Descriptions

In the previous chapter, we implemented restrictions on Kubernetes

groups who can read secrets. Other than the administrator, no other

group has the privilege to access the Kubernetes Secrets. However, with

our secrets being embedded as environment variables in the Kubernetes

deployments as shown in the following code on L70–73, all groups who

have the read-only rights to view the deployment configurations can easily

read the database password in clear-text. We shall address this exposure in

the next section.

Code Block 9-14. Password in Kubernetes deployment descriptions

File: pgitops/infra/app.tf

50: spec {

51: container {

52: image = "salecharohit/practicalgitops"

53: name = var.org_name

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

66: env {

67: name = "DB_USERNAME"

68: value = var.db_user_name

69: }

70: env {

71: name = "DB_PASSWORD"

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

73: }

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

413

Chapter 9 SeCurity and SeCretS ManageMent

9.6 AWS Secrets Manager

In order to address the problem of database passwords being stored as

clear-text in the deployment descriptions, we’ll need to make use of the

Secrets Store CSI Driver for AWS, more details of which can be found here:

.

9.6.1 Secrets Management Design

Before we explore the new design, let’s first understand the old one.

Current Solution

Figure 9-2. AWS RDS Secrets Storage - Current Solution

414

Chapter 9 SeCurity and SeCretS ManageMent

1. Terraform generates a random password and stores

it in the Database

2. Once the RDS is setup it writes the password as a

SecureString in AWS SSM

3. Terraform then reads the password from AWS SSM

4. And then writes it into the deployment manifest file

as an environment variable.

So the database secret is embedded in the terraform Kubernetes

manifest file when terraform apply is running.

New Solution

Figure 9-3. AWS RDS Secrets Storage – New Solution

415

Chapter 9 SeCurity and SeCretS ManageMent

1. Terraform generates a random password and stores

it in the Database.

2. Once the RDS is set up, it writes the password as in

AWS Secrets Manager.

3. Terraform builds the Kubernetes manifest file

with the name of the key that is used to store the

password in the AWS Secrets Manager, for example,

db_password.

4. Upon successful deployment of the Secrets Store

CSI Driver, it’ll read the password string from AWS

Secrets Manager using the name of the key that

was passed, that is, db_password, and create a

Kubernetes Secrets volume object.

In this design, we are not relying on Terraform to build the manifest

descriptor file of the deployment to inject the secret. Rather, we are using

the Secrets Store CSI Driver to pull the secret from AWS Secrets Manager

and then make it available to the deployment before the deployment starts

its execution.

Note im using aWS Secrets Manager in lieu of aWS SSM just to

explore a new service. the Secrets Store CSi driver also works with

aWS SSM.

9.6.2 Secrets Store CSI Driver

So how does the Secrets Store CSI Driver really work? Let’s understand

using the following diagram.

416

Chapter 9 SeCurity and SeCretS ManageMent

Figure 9-4. An image showing how Secrets Store CSI Driver works

1. The Secrets Store CSI Driver is a deployment

configuration that will read the secrets from the AWS

Secrets Manager from a specified path and location,

for example, db_password.

2. Once it reads the value, it will write it to a secret

mount volume as an object with a name as db-

password-secret.

3. The App deployment will read the db-password-

secret object from the secrets volume and obtain

the database password, which will be stored as an

environment variable. However, this time, since it’s a

mounted volume from where the data is being read,

it won’t be shown in clear-text in the deployment

descriptor. We’ll see this practically when we run the

entire application.

4. The deployment then connects to the RDS database

using the password.

417

Chapter 9 SeCurity and SeCretS ManageMent

Let us now see this entire process being implemented in Terraform as

shown in the following where we’ll first start with how I am defining the

AWS Secrets Manager.

Code Block 9-15. Defining the AWS Secrets Manager

File: pgitops/infra/rds.tf

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

86: # Write the DB random password in AWS Secrets Manager

87: module "awssm-db-password" {

88: source = "./modules/awssm"

89:

90: parameter_name = var.db_pwd_parameter_name

91: secret_value = module.pgsql.db_instance_

password

92: description = "DB Password"

93: environment = var.environment

94: deletion_window_in_days = 0

95:

96: depends_on = [module.pgsql]

97: }

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

L87–97: We are defining the module AWS SM or AWS Secrets Manager,

which is retrieving the value of the database password on L91 from

the database module. A special note for L90 where we are defining the

parameter name against which the value will be stored as a global variable,

which will be used across different modules/files as we’ll see ahead.

L94: Just like the AWS KMS, secret deletion can be deferred to a certain

period of time; however, for this book, I’ve defined it as 0 days as it creates

issues while destroying the resource. For example, if I set it as 7 days,

418

Chapter 9 SeCurity and SeCretS ManageMent

then this particular resource value cannot be deleted till 7 days, and our

terraform will give error while destruction.

Let’s now look at how we are installing the Secrets Store CSI Driver.

Code Block 9-16. Installing CSI Secrets Driver

File: pgitops/infra/app.tf

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

15: data "kubectl_file_documents" "csi_secrets" {

16: content = file("modules/csisecretsdriver/aws-secrets-

csi.yaml")

17: }

18:

19: # Install the CSI Secrets Driver with Secrets Sync Enabled

20: module "csi-secrets-driver" {

21:

22: source = "./modules/csisecretsdriver"

23:

24: count_files = length(data.kubectl_file_documents.

csi_secrets.documents)

25: csi_secrets_version = "1.1.2"

26: k8s_path = "modules/csisecretsdriver/aws-

secrets-csi.yaml"

27: namespace = kubernetes_namespace_v1.app.

metadata.0.name

28:

29: depends_on = [module.eks, resource.kubernetes_

namespace_v1.app]

30:

31: }

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

419

Chapter 9 SeCurity and SeCretS ManageMent

L15–17: As discussed earlier, we are using a kubectl provider, which

helps in applying Kubernetes manifest files in YAML format. If we’ve a

single manifest file with a single resource definition, we can always declare

it in the terraform code itself; however, if there are multiple resources

divided by --- (three hyphens), then we need to first count the number of

resources and provide that to the kubectl provider, which is what we are

doing here, and pass this value on L24.

L20–31: Here, we’ve encapsulated the entire procedure of installing

the Secrets Store CSI Driver in a module as it requires installing the driver

using helm and then also separately deploying a DaemonSet, which is

defined in pgitops/ modules/csisecretsdriver/aws-secrets-csi.yaml and

installed using the kubectl provider.

Next, let’s discuss about a custom resource that needs to be created in

addition to the CSI driver and the DaemonSet as shown here.

Code Block 9-17. Deploying Kubernetes Secrets custom resource

definition

File: pgitops/infra/app.tf

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

69: resource "kubectl_manifest" "secrets" {

70: yaml_body = <<YAML

71: apiVersion: secrets-store.csi.x-k8s.io/v1alpha1

72: kind: SecretProviderClass

73: metadata:

74: name: aws-secret-application

75: namespace: ${var.org_name}

76: spec:

77: provider: aws

78: secretObjects:

79: - secretName: db-password-secret

420

Chapter 9 SeCurity and SeCretS ManageMent

80: type: Opaque

81: data:

82: - objectName: ${var.db_pwd_parameter_name}

83: key: db_password

84: parameters:

85: objects: |

86: - objectName: ${var.db_pwd_parameter_name}

87: objectType: "secretsmanager"

88: YAML

89:

90: depends_on = [module.pgsql,

91: module.csi-secrets-driver,

92: resource.kubernetes_namespace_v1.app

93: ]

94:

95: }

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

L69–70: We are declaring a raw YAML body that needs to be processed

where the term YAML is following the HEREDOC syntax ending on L88.

L71–72: We are defining the custom resource and the API supporting

that resource for Kubernetes.

L74–75: Naming the resource (which will be referenced when

mounting the volume in deployment) and scoping it to the same

namespace that we are using throughout the code, that is, “gitops”.

L78–83: Here, we are declaring the secrets object that will be created

with the name of the secret being db-password-secret defined on L79, which will be referenced in the deployment.

L81–83: Defining the data, that is, the key-value of the secret where the

key is defined by us as db_password, which is the same as how it’s defined

in the AWS Secrets Manager.

421

Chapter 9 SeCurity and SeCretS ManageMent

L84–87: Here, we need to provide the exact reference of the key with

which the value is stored in AWS Secrets Manager, which, as discussed

earlier, is defined as a global variable so we don’t mess up.

L90–93: Note the dependencies here, especially the CSI Secrets Driver.

The custom resource that’s defined L72 would error out if Terraform

attempted to execute this code before the CSI driver would be set up.

From the preceding code, it’s important to note the following

definitions, which will be referenced in the deployment file, which we’ll

explore next:

aws-secret-application – Name of the custom resource

that’s defined as SecretProviderClass

db-password-secret – Name of the Kubernetes Secret

resource that will be created by this Secret

Provider class

db_password – Name of the key that’ll be used for

identifying the secret

Let’s now look at the deployment descriptor file with the portion

relevant for adding the secrets.

Code Block 9-18. Kubernetes deployment descriptor file for

adding secrets

File: pgitops/infra/app.tf

099: resource "kubernetes_deployment_v1" "app" {

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

144: env {

145: name = "DB_PASSWORD"

146: value_from {

147: secret_key_ref {

148: name = "db-password-secret"

422

Chapter 9 SeCurity and SeCretS ManageMent

149: key = "db_password"

150: }

151: }

152: }

153:

154: volume_mount {

155: name = "db-password-vol"

156: mount_path = "/mnt/secrets-store"

157: read_only = true

158: }

159:

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

186: volume {

187: name = "db-password-vol"

188: csi {

189: driver = "secrets-store.csi.k8s.io"

190: read_only = true

191: volume_attributes = {

192: "secretProviderClass" = "aws-secret-

application"

193: }

194: }

195: }

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

200: }

L154–158: Defining the volume mount db-password-vol and loading

data from the path /mnt/secrets-store where the secrets store DaemonSet is storing all the data as Secret Resources.

423

Chapter 9 SeCurity and SeCretS ManageMent

L186–195: Declaring the volume db-password-vol as a CSI driver and

referencing the custom resource definition as aws-secret-application,

which we saw earlier.

L144–152: The environment variable declaration where the DB_

PASSWORD will be populated. The value of the DB_PASSWORD will be

populated from the volume mount and retrieved using the name of the

secret resource, that is, db-password-secret, and the key in that resource whose value needs to be fetched, that is, db_password.

Tip an alternative to this Secrets Store CSi driver approach is

the aWS Secret Sidecar injector, which follows the sidecar pattern

and gets deployed as a sidecar pod along with the application pod

9.7 IRSA

9.7.1 Background

In the previous chapter, we looked at the two different authentication/

authorization mechanisms implemented in EKS, and here, we’ll be

discussing the third type. In AWS, by default, any type of authentication is

done by the access keys and authorization using the IAM Roles/Groups

and the permissions assigned to them. However, there is another way

where the authentication decision can be delegated to a different identity

provider by establishing a trust between them.

Currently, AWS supports two such identity providers, that is, OIDC

(OpenID Connect) and SAML (Security Assertion Markup Language),

which are two different authentication protocols.

424

Chapter 9 SeCurity and SeCretS ManageMent

By setting up an OIDC/SAML IdP (identity Provider), we can

authenticate to these IdPs and then use the authentication token to access

any AWS services. While setting up our EKS cluster, we are setting up this

OIDC endpoint for establishing the trust relationship.

Within Kubernetes, if a pod needs to access any resource, it needs

proper permissions that are defined in the Service Account. A default

service account is always associated with every Kubernetes resource (pods,

deployments, DaemonSets, etc.), which can always be used to authenticate

to the Kube API server.

So now the question is, how can a pod running in AWS EKS access

AWS services?

This is where IRSA (IAM Roles for Service Accounts) comes into the

picture, which is an authentication/authorization architecture for EKS

pods to access AWS services by assigning an IAM Role to the service

account token that would be mounted on the pods.

425

Chapter 9 SeCurity and SeCretS ManageMent

9.7.2 Internal Working

Figure 9-5. An image showing how IRSA works internally

• The AWS IAM and OIDC Identity Provider have a trust

relationship established between them.

• Kubernetes Service Account is annotated with the

ARN of an IAM Role, which is then passed to the OIDC

IdP, which validates the IAM Role and provides the

authentication token.

426

Chapter 9 SeCurity and SeCretS ManageMent

• This authentication token is then passed to the AWS

service, which needs to be consumed by the pod.

There are two parts to creating a complete IRSA architecture: an IAM

Assume Role tied to an IAM permissions policy and a Kubernetes Service

account with the ARN of the IAM Role added as an annotation. Let’s look

at how this would be implemented in Terraform.

9.7.3 IAM Permissions Policy

For our pod to be able to access the Secrets from the AWS Secrets Manager,

we need the following permissions policy where we are also restricting

the ARN of the secret that we’ve created. This ensures that this pod is not

able to access any secret other than the one that is explicitly defined in the

policy document on L11.

Code Block 9-19. IAM permissions policy to read a secret from AWS

Secret Manager

File: pgitops/infra/data/secrets.json

01: {

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

03: "Statement": [

04: {

05: "Sid": "SecretsManagerReadOnly",

06: "Effect": "Allow",

07: "Action": [

08: "secretsmanager:GetSecretValue",

09: "secretsmanager:DescribeSecret"

10: ],

11: "Resource": "${db_password_arn}"

12: }

427

Chapter 9 SeCurity and SeCretS ManageMent

13: ]

14: }

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

The IAM permissions policy needs to be created using the resource

definition as shown in the following where we are providing the preceding

policy as a template and providing the database secret ARN to the policy.

Code Block 9-20. Creating the Policy resource

File: pgitops/infra/app.tf

35: resource "aws_iam_policy" "secrets_policy" {

36: name = "secrets_policy"

37: path = "/"

38: policy = templatefile("./data/secrets.json", {

39: db_password_arn = module.awssm-db-password.arn

40: })

41: }

The following module is used to tie the IAM Permissions to an IAM

Role and provide the ARN of this IAM Role as an output, which needs to be

annotated on the Kubernetes service account resource.

Code Block 9-21. Creating the IRSA Role

File: pgitops/infra/app.tf

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

43: module "irsa_aws_secrets" {

44: source = "./modules/irsa"

45:

46: oidc_url = module.eks.cluster_oidc_issuer_url

47: oidc_arn = module.eks.oidc_provider_arn

428

Chapter 9 SeCurity and SeCretS ManageMent

48: k8s_sa_namespace = kubernetes_namespace_v1.app.

metadata.0.name

49: k8s_irsa_name = "irsa-aws-secrets"

50: policy_arn = resource.aws_iam_policy.secrets_

policy.arn

51:

52: depends_on = [module.eks, resource.kubernetes_

namespace_v1.app]

53: }

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

Let’s dig into this module to understand better how the IAM Role is

being created.

Code Block 9-22. Implementing IRSA in Terraform

File: infra/modules/irsa/main.tf

02: data "aws_iam_policy_document" "default" {

03: statement {

04: actions = ["sts:AssumeRoleWithWebIdentity"]

05: effect = "Allow"

06:

07: condition {

08: test = "StringEquals"

09: variable = "${replace(var.oidc_url, "https://",

"")}:sub"

10: values = ["system:serviceaccount:${var.k8s_sa_

namespace}:${var.k8s_irsa_name}"]

11: }

12:

13: principals {

14: identifiers = [var.oidc_arn]

429

Chapter 9 SeCurity and SeCretS ManageMent

15: type = "Federated"

16: }

17: }

18: }

19:

20: resource "aws_iam_role" "default" {

21: name = var.k8s_irsa_name

22: path = "/"

23: assume_role_policy = data.aws_iam_policy_document.

default.json

24: }

25:

26: resource "aws_iam_role_policy_attachment" "default" {

27: role = aws_iam_role.default.name

28: policy_arn = var.policy_arn

29: }

L02–18: Defining the AssumeRole policy for the Web Identity Provider,

which is nothing but our OIDC URL. What this basically states is allow the

Kubernetes service account to Assume the web identity by authenticating

to the OIDC URL to access the AWS services whose permissions will be

attached later.

L20–24: Creating the IAM Role with a specific name and applying the

AssumeRole policy.

L26–29: The actual permissions that are allowed by the specific service

account and the IAM Role.

Finally, let’s look at the last piece of the puzzle that is the Kubernetes

service account creation and its attachment to the deployment pod.

430

Chapter 9 SeCurity and SeCretS ManageMent

Code Block 9-23. Creating a Kubernetes Service Account and

attaching in Kubernetes deployment

File: pgitops/infra/app.tf

55: resource "kubernetes_service_account" "irsa_aws_secrets" {

56: metadata {

57: name = module.irsa_aws_secrets.role_name

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

59: annotations = {

60: "eks.amazonaws.com/role-arn" = module.irsa_aws_

secrets.role_arn

61: }

62: }

63: automount_service_account_token = true

64: depends_on = [module.eks,

resource.kubernetes_

namespace_v1.app]

65: }

099: resource "kubernetes_deployment_v1" "app" {

100: metadata {

101: name = var.org_name

102: namespace = kubernetes_namespace_v1.app.

metadata.0.name

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

197: service_account_name = resource.kubernetes_

service_account.irsa_aws_

secrets.metadata[0].name

198: }

L55–65: Defining the Kubernetes Service Account with the same name

as that of the IAM Role that it would be consuming and then deploying in

the same namespace as well.

431

Chapter 9 SeCurity and SeCretS ManageMent

L59–61: This is the key element here where we need to specify the ARN

of the IAM Role that would be assumed by this service account.

L197: The service account hence created must be passed to the

deployment pod so that it can read the token and authenticate to the OIDC

IdP and then access the AWS Secrets Manager.

Tip you are encouraged to execute the command kubectl get sa -n

gitops -o yaml to view the service accounts that are created and the

iaM role arn annotated.

9.8 Checkov Scanning

It is critically important that the code written by developers be scanned

for any security vulnerabilities. However, scanning on Terraform code is

a little bit different than the regular code scanning that is employed by

various tools as Terraform is a declarative language.

Another challenge is that since developers would be pushing changes

quite often, it would be nearly impossible for a single person to scan each

and every change manually.

Both these problems are addressed by this tool called Checkov, which

is a very popular and powerful tool to scan for security bad practices

in terraform code. Moreover, the authors of the tool have also created

a GitHub Action template, which helps in automated scanning of

each change.

The following code shows integration of Checkov into the GitHub

Action workflow configured for staging/production and global

environments. (Shown here is of only staging; however, the same is

applicable for global/production).

432

Chapter 9 SeCurity and SeCretS ManageMent

Code Block 9-24. Adding Checkov scanning in GitHub Actions

File: pgitops/.github/workflows/staging.yaml

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

46: - name: Run Checkov action

47: id: checkov

48: uses: bridgecrewio/checkov-action@master

49: if: github.event_name == 'pull_request'

50: with:

51: directory: global/

52: quiet: true

53: soft_fail: true

54: framework: terraform

55: output_format: cli

56: continue-on-error: true

L46–48: Declaring the Checkov GitHub Action name

L49: Executing the action only on Pull Requests.

L51: Defining the name of the folder on which the Checkov action

needs to be executed. It’ll scan all the *.tf files in the specified folder.

L52: Showing only failed results.

L53: A very important configuration. If set as true, it would fail the

GitHub Actions pipeline and stop the build/terraform execution.

The following code is used for showcasing the output of the Checkov

scan in PR messages.

Code Block 9-25. Spooling Checkov results in PR

File: .github/workflows/infra-staging.yaml

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

58: - name: Checkov Results

59: uses: actions/github-script@v6

433

Chapter 9 SeCurity and SeCretS ManageMent

60: if: github.event_name == 'pull_request'

61: env:

62: CHECKOV: "checkov\n${{ env.CHECKOV_RESULTS }}"

63: with:

64: github-token: ${{ secrets.GITHUB_TOKEN }}

65: script: |

66: const output = `## Checkov Results

67: <details><summary>Show Checkov Results</summary> 68: ${process.env.CHECKOV}

69: </details>

70: *Pusher: @${{ github.actor }}, Action: \`${{

github.event_name }}\`*`;

71: github.rest.issues.createComment ({

72: issue_number: context.issue.number,

73: owner: context.repo.owner,

74: repo: context.repo.repo,

75: body: output

76: })

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

9.8.1 Checkov in Action

Let’s execute the infra workflow to better understand how Checkov will

work as part of the GitHub Actions pipeline and observe the results.

To do this, we’ll need to first do some small modification to the code

and then create a branch. Then raise a PR into the staging branch to see

Checkov in action. We won’t actually run the code in staging as we only

need to see the PR running all the checks.

For this, I’ll modify the value of the instance_types from t3.medium to

t3.xlarge and t3.small to t3.large in the pgitops/infra/terraform.auto.tfvars

file as shown here.

434

Chapter 9 SeCurity and SeCretS ManageMent

Code Block 9-26. Testing Checkov

File: pgitops/infra/terraform.auto.tfvars

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

37: instance_types = ["t3.xlarge","t3.large"]

Let’s now create a feature branch and push the branch.

CLI Output 9-27. Starting Checkov action

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: infra/terraform.auto.tfvars

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

commit -a")

cmd> git checkout -b checkov

Switched to a new branch 'checkov'

cmd(checkov)> git add .

cmd(checkov)> git commit -m "modifying instance types"

[checkov 462cfcc] modifying instance types

1 file changed, 1 insertion(+), 1 deletion(-)

cmd(checkov)> git push origin checkov

Next, let’s create a PR into staging as shown here.

435

Chapter 9 SeCurity and SeCretS ManageMent

Figure 9-6. PR for executing Checkov

Once the PR is created, we can see that the workflow has been initiated

as shown here.

Figure 9-7. Checkov executed in PR

Once the action has completed its execution, we should be able to see

the Checkov results as shown here.

436

Chapter 9 SeCurity and SeCretS ManageMent

Figure 9-8. Checkov results being spooled

Another failed compliance is shown in the following.

Figure 9-9. Another failed compliance

437

Chapter 9 SeCurity and SeCretS ManageMent

In this manner, we can create guardrails around Terraform code that

our developers are pushing by reviewing them in PR messages.

Note i am not merging the pr here as i said earlier that we’ll be

working on the development environment only in this chapter and

the next.

9.9 Service Control Policies

AWS organizations do just help in centralizing billing and management of

all the child accounts/organization units but also can help in strategically

restraining certain actions at the account/OU level. Service Control

Policies (SCP) are policies that can be applied at the account/OU level to

restrict/control the usage of AWS resources.

For example, if we want to disallow users from spinning up EC2

instances like t3.xlarge, etc., that can consume a lot of money, then we can

enforce that at the account/OU level, which we’ll see as an example soon.

Hence, SCPs can help in enforcing compliance centrally through AWS

organizations. The following code is used to initialize the SCP using Terraform.

Code Block 9-28. Instantiating Service Control Policies

File: pgitops/global/organisations.tf

02: resource "aws_organizations_organization" "root" {

03:

04: enabled_policy_types = [

05: "SERVICE_CONTROL_POLICY",

06: "TAG_POLICY"

07: ]

08:

438

Chapter 9 SeCurity and SeCretS ManageMent

09: feature_set = "ALL"

10:

11: }

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

L04–07: Initializing Service Control Policy and Tag Policies at the AWS

Root organization.

SCP enforces resource-level compliance, whereas Tag Policy helps in

enforcing proper tags on resources.

I’ve written four different SCPs in the pgitios/global/scp.tf file. Let’s look at the InstanceTypeLimitSCP as shown here, which restricts the type of

instances that can be launched in staging and development environments.

Code Block 9-29. Creating the Instance Type SCP

File: pgitops/global/scp.tf

12: module "instance_type_limit_scp" {

13: source = "./modules/awsorgpolicy"

14:

15: policy_name = "InstanceTypeLimitSCP"

16: description = "Restrict EC2 and DB instance types for

dev/staging environments"

17: policy_file = "data/instance_type_limit.json"

18:

19: account_ids = [module.staging_account.id, aws_

organizations_organizational_unit.

development.id]

20:

21: depends_on = [resource.aws_organizations_

organization.root]

22:

23: }

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

439

Chapter 9 SeCurity and SeCretS ManageMent

L12–13: Defining and loading the module, which will create SCPs.

L15–16: Naming and describing the SCP. Describing the SCP is really

important as it helps to build context.

L17: The actual JSON policy document that will enforce the restriction.

It follows the standard IAM Policy document structure as we’ll see in

a while.

L19: Account/OU IDs on which this SCP needs to be applied to.

Let’s look at the actual policy that is codified for enforcing the EC2

instance types.

Code Block 9-30. The actual policy for restricting instance types

File: pgitops/global/data/instance_type_limit.json

01: {

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

03: "Statement": [

04: {

05: "Sid": "LimitEC2Instances",

06: "Effect": "Deny",

07: "Action": [

08: "ec2:*"

09: ],

10: "Resource": "*",

11: "Condition": {

12: "ForAnyValue:StringNotLike": {

13: "ec2:InstanceType": [

14: "*.micro",

15: "*.medium",

16: "*.small",

17: "*.nano"

18: ]

19: }

440

Chapter 9 SeCurity and SeCretS ManageMent

20: }

21: },

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

39: ]

40: }

L01–03: Standard definition of an IAM Policy.

L04–09: Declaring the name of the policy and the effect and actions on

which the effect needs to be performed.

L10-18: Applying this to all resources that do not meet the condition of

the EC2 instance being of micro, medium, nano, or small type.

Hence, the policy states that “Deny any EC2 action if the instance

type doesn’t meet the criteria of being a micro, nano, small, or

medium.”

Similarly, three more policies have been defined as follows.

Table 9-1. All the SCPs configured

Policy Name

Accounts

Description

denyuserCreationSCp all except

deny Creation of users in all accounts except

identity

identity.

regionLockSCp

root account restrict aWS services and regions to

particular values, that is, us-east-1, us-

east-2, us-west-1, and us-west-2.

eBSrdSencryptionSCp production

enforce encryption on eBS block devices and

account

rdS storage for production account.

9.9.1 SCP in Action

Now that we know what SCP is, let's learn how it practically works.

441

Chapter 9 SeCurity and SeCretS ManageMent

Note Our SCps are already operational since we did the commit for

the global folder in the beginning of the chapter.

For this, we’ll first merge the changes between the Checkov branch

and the main branch since the PR request was not merged in the earlier

section.

CLI Output 9-31. Merging Checkov branch for testing the SCP

cmd(checkov)> git checkout main

Switched to branch 'main'

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

cmd> git merge checkov

Updating ffefeb3..6824a6b

Fast-forward

infra/terraform.auto.tfvars | 2 +-

1 file changed, 2 insertions(+), 2 deletions(-)

Next, just confirm that in the pgitops/infra/terraform.auto.tfvars file,

the instance_types is set to t3.large and t3.xlarge.

Now, as per our LimitEC2Instances SCP, the development

environment cannot have these instance types. Let’s test it out.

CLI Output 9-32. Testing the SCP

cmd> export AWS_PROFILE=sita-dev

cmd> cd infra

cmd> terraform init

cmd> terraform apply –auto-approve

Terraform would start its execution, and somewhere when it’s about

to create the EKS Node Groups, that is, the worker nodes, it will throw an

error looking something like the following.

442

Chapter 9 SeCurity and SeCretS ManageMent

CLI Output 9-33. Terraform fails to execute owing to InstanceType

limitation

│ Error: error creating EKS Node Group (gitops-us-east-1-dev:

app-20220707050520487800000019): InvalidRequestException: You are

not authorized to launch instances with this launch template.

Encoded authorization failure message: jkwlnVZ6DGXkSEoDoi2Qh21i

Dsrp4ZOLdVm0lOp1UqcrD3JtkrQmtSQoLirHj1pCgTxTA6hDvCahGWu-f73_gW2

TSBaBLLWV22cMCGyX0ef1mPyUSDE32dtJUD824WyFzVOXafE2UlaxM27_9purz

L0vmjP8qDOgQK88dOXXXXXXXXXXXXXXX------SNIPPED-------

XXXXXXXXXXXXXXXXXXXXXrHz6qM6MaGB8-B-1ZRSb8P3vPYuCKKuUQn8ImUP_

sezicOWKTvyoSW0XTRHjORPu7MS1gz2BQNROvnvvT8uLq9QfN504lO25d2nIuG

GI4GTrVRou0KpAoEWAJ8Z_ei_UP88I3J65fixRG0KHjeUHMMfkkh9JH1uQWHw9c

x1_Yfrvq1reTE4RHOAWafoTncVR-YCbv1CAkY-mdVF9drepeKOy7O0MSgkQUE4p

80w2p4MlII3CfmfMKUTvyXMAD0IlfSO6JhDWCx_qqHTI539PG-s67p1hAigvffMP

│ {

│ RespMetadata: {

│ StatusCode: 400,

│ RequestID: "a620ee2c-f788-45a9-857a-90de4b7f452d"

│ },

Let’s decode the message to better understand what’s happening. We’ll

need to make use of the following command:

aws sts decode-authorization-message --encoded-message

<encoded-message>

And apply little bit for formatting using sed 's/\\"/"/g' | sed 's/^"//' | sed

's/"$//'

as shown here.

443

Chapter 9 SeCurity and SeCretS ManageMent

CLI Output 9-34. Executing AWS STS decode-

authorization-message

cmd> aws sts decode-authorization-message --encoded-message

jkwlnVZ6DGXkSEoDoi2Qh21iDsrp4ZOLdVm0lOp1UqcrD3JtkrQmtSQoLirHj1p

CgTxTA6hDvCahGWu-f73_gW2TSBaBLLWV22cMCGyX0ef1mPyUSDE32dtJUD824

WyFzVOXafE2UlaxM27_9purzL0vmjP8qDOgQK88dOz7waPgIObTxOFYxLNak7U

LjfYQPtFzwzGN5SDpIg0cw_YwTgFZBXmcEA9tHXXXXXXXXXXXXXXX------

SNIPPED-------XXXXXXXXXXXXXXXXXXXXXQfN504lO25d2nIuGGI4GTrVRou0

KpAoEWAJ8Z_ei_UP88I3J65fixRG0KHjeUHMMfkkh9JH1uQWHw9cx1_Yfrvq1re

TE4RHOAWafoTncVR-YCbv1CAkY-mdVF9drepeKOy7O0MSgkQUE4p80w2p4MlII3

CfmfMKUTvyXMAD0IlfSO6JhDWCx_qqHTI539PG-s67p1hAigvffMP |

sed 's/\\"/"/g' | sed 's/^"//' | sed 's/"$//'

{

"DecodedMessage": "{"allowed":false,"explicitDeny":true,"ma tchedStatements":{"items":[{"statementId":"LimitEC2Instances","

effect":"DENY","principals":{"items":[{"value":"AROA5JNUXXXXXXX

XXXXXX"}]},"principalGroups":{"items":[]},"actions":{"items":[{

"value":"ec2:*"}]},"resources":{"items":[{"value":"*"}]},"

conditions":{"items":[{"key":"ec2:InstanceType","values":{"i tems":[{"value":"*.micro"},{"value":"*.medium"},{"value":"*.

small"},{"value":"*.nano"}]}}]}}]},"failures":{"items":[]},"c ontext":{"principal":{"id":"AROA5JNUXXXXXXXXXXXXX:aws-go-sdk-1657169654646371000","arn":"arn:aws:sts::91XXXXXXXX7:assumed-

role/AssumeRoleAdmindev/aws-go-sdk-1657169654646371000"},"act

ion":"ec2:RunInstances","resource":"arn:aws:ec2:us-east-1:91

XXXXXXXX7:instance/*","conditions":{"items":[{"key":"ec2:Metad ataHttpPutResponseHopLimit","values":{"items":[{"value":"2"}]}

},{"key":"ec2:InstanceMarketType","values":{"items":[{"value":

"on-demand"}]}},{"key":"aws:Resource","values":{"items":[{"val 444

Chapter 9 SeCurity and SeCretS ManageMent

ue":"instance/*"}]}},{"key":"aws:Account","values":{"items":[{

"value":"91XXXXXXXX7"}]}},{"key":"ec2:AvailabilityZone","value s":{"items":[{"value":"us-east-1c"}]}},{"key":"ec2:ebsOptimize d","values":{"items":[{"value":"true"}]}},{"key":"ec2:IsLaunch TemplateResource","values":{"items":[{"value":"true"}]}},{"ke y":"ec2:InstanceType","values":{"items":[{"value":"t3.large"}

]}},{"key":"ec2:RootDeviceType","values":{"items":[{"value":"

ebs"}]}},{"key":"aws:Region","values":{"items":[{"value":"us-east-1"}]}},{"key":"ec2:MetadataHttpEndpoint","values":{"items"

:[{"value":"enabled"}]}},{"key":"ec2:InstanceMetadataTags","val ues":{"items":[{"value":"disabled"}]}},{"key":"aws:Service","v alues":{"items":[{"value":"ec2"}]}},{"key":"ec2:InstanceID","v alues":{"items":[{"value":"*"}]}},{"key":"ec2:MetadataHttpToke ns","values":{"items":[{"value":"required"}]}},{"key":"aws:Type

","values":{"items":[{"value":"instance"}]}},{"key":"ec2:Tenanc y","values":{"items":[{"value":"default"}]}},{"key":"ec2:Region

","values":{"items":[{"value":"us-east-1"}]}},{"key":"aws:ARN",

"values":{"items":[{"value":"arn:aws:ec2:us-east-1:91XXXXXXXX7

:instance/*"}]}},{"key":"ec2:LaunchTemplate","values":{"items"

:[{"value":"arn:aws:ec2:us-east-1:91XXXXXXXX7:launch-template/

lt-007064a3db6d169af"}]}}]}}}

}

The preceding error message clearly mentions the SCP that blocked

the creation of our t3.large EC2 instance! It truly shows the power of policy

enforcement!

Of course, there is one drawback here: that this message is shown a

little later when almost half of the infrastructure is almost already set up.

445

Chapter 9 SeCurity and SeCretS ManageMent

9.10 Clean-Up

Since our infrastructure was only partially set up, it’s best to destroy it

using the usual terraform destroy command as shown here.

CLI Output 9-35. Destroying the Dev environment

cmd> export AWS_PROFILE=sita-dev

cmd> cd infra

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.

We still are not destroying the global folder as it’s needed in the next

chapter.

Also we need to revert back the instance_type configuration for the

development environment, without which, we won’t be able to set up the

development environment. Let’s revert the values back to t3.small and

t3.medium as shown here.

Code Block 9-36. Reverting back the configuration

File: pgitops/infra/terraform.auto.tfvars

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

37: instance_types = ["t3.small","t3.medium"]

446

Chapter 9 SeCurity and SeCretS ManageMent

Once this configuration has been reverted, you are strongly advised to

run the terraform apply to set up the entire infrastructure and then check

the following:

Is HTTP being redirected to HTTPS?

Can you view the database credentials when viewing

the deployment manifests in Kubernetes? kubectl

describe deploy gitops -n gitops

Which service-account-token is mounted on gitops

deployment?

9.11 Conclusion

In this chapter, we saw how certain security features can be introduced

using Terraform to make our infrastructure a tidy bit more secure like

secrets being stored in AWS KMS and synced real time. Our website is

no longer serving on a clear-text port; rather, it is redirected to a secure

connection, that is HTTPS. We looked at how individual pods can securely

communicate with the AWS services using IRSA, implemented guardrails

with Checkov to better understand the security hygiene of our terraform

code, and finally enforced policy compliance centrally to ensure certain

best practices using SCPs.

In the next chapter, we’ll look at the logging, monitoring, and

observability aspects of this infrastructure that we’ve set up.

447

CHAPTER 10

Observability

Observability is the science of observing a running infrastructure through

various metrics and data points that the tools provide. While an entire

book can be written on how to actually observe your infrastructure, this

chapter's focus is more on how to set up the supporting infrastructure that

can help us drive decisions based on the behavior of the application that is

observable through various metrics and data points.

In this chapter, we’ll look at setting up an OpenSearch cluster to view

all Kubernetes and application logs in real time, observing performance

metrics using Prometheus and Grafana stack, centralizing CloudTrail of

all accounts into a single account, capturing ALB logs in an S3 bucket,

installing Karpenter (a horizontal node scaling tool to scale the nodes

based on pod requirements), upgrading the Kubernetes version from

1.20 to 1.21, and observing the changes happening in real time and finally

creating CloudWatch alerts based on events like root login.

10.1 Code Update

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

directory with the code from the Chapt folder. To do this, follow these steps:

– First, copy the contents of the Chaptto the pgitops folder.

– Get inside the pgitops directory and then check which

files/folders are changing.

© Rohit Salecha 2023

449

R. Salecha, Practical GitOps,

Chapter 10 Observability

– We shall commit only the global directory 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 10-1. Updating Chapter code cmd> cp -a chapter10/. 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: global/README.md

modified: global/data/padma.pub.old

modified: global/data/raadha.pub.old

modified: global/data/sita.pub.old

modified: global/identity.tf

modified: global/organisations.tf

modified: global/outputs.tf

modified: global/prod/main.tf

modified: global/staging/main.tf

modified: global/terraform.auto.tfvars.old

modified: global/variables.tf

modified: infra/README.md

modified: infra/app.tf

450

Chapter 10 Observability

modified: infra/eks.tf

modified: infra/modules/acm/main.tf

modified: infra/modules/alb/iam_policy.json

modified: infra/modules/alb/main.tf

modified: infra/modules/alb/variables.tf

modified: infra/modules/csisecretsdriver/aws-

secrets- csi.yaml

modified: infra/modules/ekscluster/main.tf

modified: infra/modules/ekscluster/output.tf

modified: infra/modules/vpc/main.tf

modified: infra/terraform.auto.tfvars.old

Untracked files:

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

committed)

global/data/rds_encryption_check.json

global/logging.tf

global/logging/

global/modules/cwalarm/

global/modules/snsemail/

infra/modules/alb/s3_log.json

infra/modules/karpenter/

infra/modules/logging/

infra/modules/monitoring/

infra/observability.tf

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

commit -a")

451

Chapter 10 Observability

The changes can summarized as follows:

– We are creating an Organization Trail that centralizes

CloudTrail from all accounts into the master account and

archiving it in an S3 bucket.

– Creating a CloudWatch rule that sends an email alert

whenever a root user logs into the accounts.

– Creating an Admin IAM Role in the master account and

allowing a Sita user to access the master account by

assuming this IAM Role.

– Creating an S3 bucket that stores all ALB access logs.

– Deploying Karpenter, a horizontal node scaler by AWS.

– Creating an OpenSearch cluster that will have application

and Kubernetes logs pushed into using Fluent bit.

– Creating a Monitoring stack using Prometheus and

Grafana, obtaining metrics using Metrics Server.

10.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. However, there is a

small addition that needs to be done to the pgitops/global/terraform.auto.

tfvars file before pushing the code. There is an additional variable, alert_

email, that needs to be initialized without which our code will not run.

Ensure to add an appropriate value to it before pushing the code.

Code Block 10-2. Comitting only global changes

File: pgitops/global/terraform.auto.tfvars

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

452

Chapter 10 Observability

39: org_name = "gitops"

40: domain = "gitops.rohitsalecha.com"

41: alert_email = "[email protected]"

Of course, the ideal way to push this code would be to create a branch,

raise a PR, and then merge it. However, for sake of simplicity, let’s push it

directly into the main branch as shown here.

CLI Output 10-3. Pushing Global changes

cmd> git add .

cmd> git commit -m 'committing only global for chapter10

-- global

cmd> git push

Once we fire git push, it can be observed that only global changes have

gone live.

With these, the following items have been created:

– Organization Trail spooling all logs in CloudWatch of the

master account

– An S3 bucket archiving all CloudTrail data as backup

since CloudWatch has a limit of only 30 days of storage

– An IAM Role created in the master/root account for

Admin to access

– A CloudWatch Alarm with an SNS topic

10.1.2 Committing Infra

The other changes, for example, 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.

453

Chapter 10 Observability

CLI Output 10-4. Committing infra changes

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/modules/alb/s3_log.json

new file: infra/modules/karpenter/

modified: infra/variables.tf

cmd> git commit -m '[skip ci] committing infra'

cmd> git push

10.2 Executing Infra

Unlike the previous chapters, this time, let’s first start executing the infra in

the development environment to view all the different changes that we’ve

made as we progress through the code.

One small prerequisite for our infra code to run is an additional

variable alb_s3_bucket in the pgitops/infra/terraform.auto.tfvars file that needs to be instantiated as shown in the following. Please add an

appropriate value to this variable before executing the infra terraform files.

This variable holds the name of the S3 bucket, which will hold our ALB

Access Logs.

Code Block 10-5. Executing infra

File: pgitops/infra/terraform.auto.tfvars

454

Chapter 10 Observability

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

36: eks_version = "1.20"

37: instance_types = ["t3.medium","t3.small"]

38: alb_s3_bucket = "gitops-logs-s3-bucket-5"

Standard process now to execute our infra terraform files.

CLI Output 10-6. Executing infra changes in the dev environment

cmd> export AWS_PROFILE=sita-dev

cmd> cd pgitops/infra

cmd> terraform init

cmd> terraform apply --auto-approve

10.3 Organization Trail

AWS records all activities performed by a user/IAM Role through the

console as well as programmatically as events. These events are then

stored as records in a service called CloudTrail.

In order to get these records, CloudTrail needs to be enabled in all

accounts that are operational. When working with AWS organizations and

using various different accounts, it’s not feasible to go ahead and enable

CloudTrail in all accounts. Moreover, if you ever manage to do so, you’ll

always need to log into that account to view the CloudTrail logs under the

Logs section.

However, AWS in 2018 introduced a feature called Organization Trail,

which can be launched from the master account. Once enabled, this

feature enables CloudTrail in all existing accounts as well as new accounts

and most importantly gets all the CloudTrail logs from the member

accounts into the master account for central logging purpose.

455

Chapter 10 Observability

Let’s explore how this feature can be enabled through Terraform, and

in the Master Account Login section, we’ll actually log in and view the

CloudTrail records from other accounts.

Code Block 10-7. Enabling CloudTrail

File: pgitops\global\organisations.tf

2: resource "aws_organizations_organization" "root" {

3:

4: aws_service_access_principals = [

5: "cloudtrail.amazonaws.com"

6: ]

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

L4–6: When creating a service role in AWS IAM - that is, a role that

can be assumed by an AWS Service - you need to specify the service by a

unique identifier. That unique identifier is known as a service principal.

This service principal now allows access to CloudTrail service for all

accounts.

Tip Unofficial list of service Control principals

Code Block 10-8. Configuring Organization Trail

File: pgitops\global\logging.tf

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

10: resource "aws_cloudtrail" "central_cloud_trail" {

456

Chapter 10 Observability

11:

12: name = "CentralCloudTrail"

13: s3_bucket_name = module.logging.s3_bucket_id

14: s3_key_prefix = "trails"

15: enable_log_file_validation = true

16: enable_logging = true

17: include_global_service_events = true

18: is_multi_region_trail = true

19: is_organization_trail = true

20: cloud_watch_logs_role_arn = module.logging.cw_logs_

role_arn

21: cloud_watch_logs_group_arn = "${module.logging.cw_

logs_arn}:*"

22: depends_on = [module.logging,

23: aws_organizations_organization.root]

24: tags = {

25: terraform-managed = "true"

26: }

27: }

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

L10: This is the standard way of enabling a CloudTrail in AWS using

Terraform.

L13: This S3 bucket is created in the master account and is primarily

used for archiving.

L17–19: These are the actual flags that distinguish a local CloudTrail

from an Organization Trail. If set to true, the resource becomes an

Organization Trail enabling CloudTrail in all member accounts.

L20–21: Specification of the CloudWatch Group that is needed to view

the CloudTrail logs by default. This CloudWatch group is created in the

master account.

457

Chapter 10 Observability

Code Block 10-9. Adding logging configurations in the

master account

File: pgitops\global\logging.tf

2: module "logging" {

3: source = "./logging"

4: account = "master"

5: identity_account_id = module.identity_account.id

6:

7: }

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

In the preceding module, the following things are getting created in the

master account:

– CloudWatch Group for storing Organization Trail logs

– S3 bucket for Organization Trail with necessary IAM

Permissions such that CloudWatch can push the data

– IAM Admin Role for the master account

10.4 CloudWatch Alarm

In this section, we’ll look into how we’ve set up the CloudWatch Alarm for

root login by first creating a subscription using the AWS SNS and an alarm

on specific conditions using CloudWatch Alarm. Before we move on, let’s

first understand CloudWatch and SNS because this is one of the most

powerful combinations in AWS infrastructure as it helps in creating an

EDA (Event-Driven Architecture).

458

Chapter 10 Observability

CloudWatch keeps a track of all the events like bucket creation, new file

added to a bucket, EC2 creation, etc. You can attach a filter of your choice

to fish out the events that you are interested in like filter all events when an

EC2 is destroyed.

SNS can be used to send email or text notifications to end users. In

this case, you can trigger a notification based on an event that matches the

filter criteria that you have set inside CloudWatch; for example, if we create

a filter for the preceding use case that an EC2 is being destroyed, then it’ll

trigger an SNS notification, which in turn will trigger an SMS or an email to

the user.

10.4.1 Configuring AWS SNS

Once the terraform code in the global folder is executed successfully, you

should receive an email on the value specified in the alert_email variable as shown here.

Figure 10-1. AWS SNS Subscription confirmation for Alert Email

Warning ensure to click Confirm subscription to have the following

confirmation.

459

Chapter 10 Observability

Figure 10-2. Subscription confirmation

The following is the code that is responsible for doing the same. It

creates an AWS SNS (Simple Notification Service) subscription with the

email specified in the variable.

Code Block 10-10. Configuring SNS notification

File: pgitops\global\logging.tf

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

29: module "sns_alert" {

30: source = "./modules/snsemail"

31:

32: name = "EmailAlerts"

33: alert_email = var.alert_email

34:

35: }

460

Chapter 10 Observability

10.4.2 Configuring CloudWatch Alarm

Code Block 10-11. Configuring CloudWatch Alarm

File: pgitops\global\logging.tf

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

39: module "root_user_login_alarm" {

40: source = "./modules/cwalarm"

41: name = "RootLogin"

42: cw_group_name = module.logging.cw_log_

group_name

43: pattern = "{ ($.userIdentity.

type = \"Root\") &&

($.userIdentity.

invokedBy NOT EXISTS)

&& ($.eventType !=

\"AwsServiceEvent\") }"

44: metric_filter_count = "1"

45: metric_filter_namespace = "CloudTrailMetrics"

46: description = "A CloudWatch Alarm that

triggers if a root user

is logging in."

47: metric_alarm_statistic = "Sum"

48: metric_alarm_period = "60"

49: metric_alarm_threshold = "1"

50: metric_alarm_evaluation_periods = "1"

51: metric_alarm_comparison_operator = "GreaterThanOrEqualTo

Threshold"

52: metric_alarm_treat_missing_data = "notBreaching"

461

Chapter 10 Observability

53: sns_topic_arn = module.sns_alert.arn

54:

55: depends_on = [aws_cloudtrail.central_cloud_trail]

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

L39–41: Invoking the CloudWatch Alarm module and configuring the

name of this alarm as RootLogin.

L42: The CloudWatch group that will be monitored to raise the alarm,

basically the input to the alarm.

L43: Most important configuration, which is checking if the root user is

performing any AWS Activity, then please flag!

L47–51: Configuring the alarm to check for the CloudWatch data every

60 seconds for any breach.

L52: Configuring the default condition when there is no data.

L53: Configuring the SNS Topic created earlier where in case of a

breach or alarm being triggered, a notification will be sent to the AWS SNS

topic, and subsequently an email will be sent.

10.4.3 Root Login Alarm in Action

Let’s test this alarm by simply logging in using the root email address and

observing the email as shown in the following.

Note Wait for about two minutes after logging into the account to

get the email.

462

Chapter 10 Observability

Figure 10-3. Root Login Alarm in Action

We can also see the Alarm being breached in the CloudWatch

console. Search for CloudWatch in the search bar and then open the

RootLoginCount Alarm as shown here.

463

Chapter 10 Observability

Figure 10-4. Root Login breach count

10.5 Master Account Login

So we created an Organization Trail in the master account, and the only

way to access the master/root account is to use the root credentials or the

credentials of the “gitops” admin user, which was the very first user that

we created. Both these credentials must be locked down and used only for

emergency purposes. Then how do we access the management account to

view the CloudTrail logs?

Very simple, create an IAM Role with administrator privileges and

then allow the Sita user to assume that role, which is what is being done as

shown in the following.

Note Of Course, you could use a lesser privilege role rather than an

administrator role like a simple Cloudtrailsadmin role as an example.

464

Chapter 10 Observability

Code Block 10-12. Updating Admin role privileges

File: pgitops\global\identity.tf

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

87: admin = [

88: module.staging.assume_admin_role_arn,

89: module.prod.assume_admin_role_arn,

90: module.dev.assume_admin_role_arn,

91: module.logging.assume_admin_role_arn

92: ],

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

Earlier we logged into the master account using the root credentials;

let’s login into the master account now using this newly created IAM Role

assumed by user Sita.

1. Firstly, we need the account ID of the master

account in order to log in; hence, from the CLI, fire

the following commands.

CLI Output 10-13. Login using GitOps user credentials

cmd> export AWS_PROFILE=gitops

cmd> aws sts get-caller-identity

{

"UserId": "AIDASEXSQTNTGAEZIZL3P",

"Account": "14XXXXXXX30",

"Arn": "arn:aws:iam::14XXXXXXX30:user/gitops"

}

Make a note of this account ID.

465

Chapter 10 Observability

2. Let’s log in into the console of the identity account

using the Sita user’s credentials and click Switch role

as shown here.

Figure 10-5. Switch role through Console

3. Now in the Switch role console, enter the Account

ID obtained earlier and name of the IAM Role,

which can be obtained from the Terraform Cloud

Global workspace outputs as shown here.

466

Chapter 10 Observability

Figure 10-6. Switch Role details

4. If everything is correctly entered, you should be

navigated to the master account as shown here.

467

Chapter 10 Observability

Figure 10-7. Switched Role to master account

5. Navigate to the CloudWatch Console and view the

CloudTrail group as shown in the following. Ensure

you are in the correct region, that is, (us-east-2).

468

Chapter 10 Observability

Figure 10-8. Navigating to the CloudWatch console

6. Streams from different accounts can be seen.

469

Chapter 10 Observability

Figure 10-9. Streams from different accounts

10.6 ALB Access Logs

The load balancer is the first point of entry into our application, and hence,

access logs need to be maintained and stored as per many compliance

frameworks. These logs can help in identifying any unusual activity or

debug an issue.

470

Chapter 10 Observability

Also, setting this up is relatively easy; we only have to set up an S3 bucket

and reference the bucket in an annotation of the ingress resource as shown here.

Code Block 10-14. Updating Kubernetes ingress resource for

ALB Logging

File: pgitops\infra\app.tf

229: resource "kubernetes_ingress_v1" "app" {

230: wait_for_load_balancer = true

231: metadata {

232: name = var.org_name

233: namespace = kubernetes_namespace_v1.app.

metadata.0.name

234: annotations = {

235: "kubernetes.io/ingress.

class" = "alb"

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

241: "alb.ingress.kubernetes.io/load-balancer-attributes"

= "access_logs.s3.enabled=true,access_logs.

s3.bucket=${var.alb_s3_bucket},access_logs.

s3.prefix=gitops,routing.http.drop_invalid_header_

fields.enabled=true"

242: "alb.ingress.kubernetes.io/group.name"

= "gitops"

L241: An additional annotation that has been added, “alb.ingress.

kubernetes.io/load-balancer-attributes”, which holds the name of the S3

bucket and its prefix where the logs will be stored.

L242: Another annotation that has been added to group ingress rules

within the same resource. As we’ll see in Section 10.8, when we set up

Grafana, this same annotation will be added to Grafana’s ingress resource.

471

Chapter 10 Observability

Thereby we avoid setting up two different AWS ALBs, which is the default

behavior as discussed in Section 7.6.

10.7 Logging

Setting up a logging infrastructure is extremely important as any

debugging if needed has to be done by sifting through the logs we have.

Logging infrastructure gives us feedback of what’s really happening, what

data is coming in, and how the application is processing the data.

The EFK or the Elastic, Fluent Bit, and Kibana is one of the most

popular stacks for setting up the logging infrastructure. AWS has set up

its own stack called OpenSearch, which was forked out of Elastic Stack v

7.10 onward.

It is an onerous and a mammoth task to set up a logging infrastructure

because of the two most important factors that need to be considered,

namely, scalability and security.

A logging infrastructure needs to be scalable as application usage

can go up during certain periods and down during certain ones. Hence, a

standard production-grade EFK stack will have multi-node clusters.

It also has to maintain top-notch security because plenty of sensitive

information like credit card numbers, passwords, etc., that would be

part of HTTP requests/responses could be stored/processed in this

infrastructure.

AWS through OpenSearch provides both scalability and security;

hence, it’s prudent to use this service.

The entire logging infrastructure setup has been encapsulated in a

single module in Terraform as shown in the following.

Code Block 10-15. Creating the OpenSearch Stack

File: pgitops\infra\observability.tf

472

Chapter 10 Observability

06: module "logging" {

07: source = "./modules/logging"

08:

09: es_instance_type = "t3.medium.elasticsearch"

10: es_instance_count = 1

11: es_version = "OpenSearch_1.2"

12: fb_version = "2.23.3"

13: oidc_url = module.eks.cluster_oidc_issuer_url

14: oidc_arn = module.eks.oidc_provider_arn

15: acm_arn = module.acm.acm_arn

16: url = local.url

17: zone_id = data.aws_route53_zone.

domain.zone_id

18: environment = var.environment

19: fluentbit_count_yaml = length(data.kubectl_file_

documents.fluentbit_yaml_count.

documents)

20:

21: depends_on = [

22: module.eks,

23: resource.kubectl_manifest.aws_auth

24: ]

25: }

L09–11: Important prerequisites that need to be provided to set up

the OpenSearch cluster. It has its own instance naming convention like

t3.medium.elasticsearch. Cluster count is important, and for production

purposes, this should be a minimum of 3.

L12–14: Providing information to set up Fluent Bit, which is a tool

that ships the logs to the OpenSearch cluster. It’ll run as a DaemonSet on

each node, and an IRSA (hence sharing OIDC information) setup has to be

made so that it can write to the OpenSearch cluster.

473

Chapter 10 Observability

L15–17: The OpenSearch corresponds to Elasticsearch, and for Kibana,

we have OpenDashboard provided by AWS, and it can be accessible over

the Internet. Hence, providing it the requisite information to set up on a

customer domain taking the format as logging.<environment>.<domain>.

For example, for the development environment, it’ll be accessible on

the FQDN as logging.dev.gitops.rohitsalecha.com where gitops.

rohitsalecha.com is the main domain and dev is the environment.

10.7.1 Exploring OpenSearch

The full URL for accessing our OpenSearch cluster in the dev environment

is as follows:

and the credentials are elastic:Elastic@123, which are currently

hard-coded in pgitops\infra\modules\logging\main.tf, and it is strongly

recommended that they are changed and also make use of a secrets

managing service to store them.

Figure 10-10. Login Console of OpenSearch

474

Chapter 10 Observability

Post authentication, two options are provided; select “Add data” as

shown here.

Figure 10-11. Add data after login

Next, we need to create an Index in the OpenSearch cluster as

shown here.

475

Chapter 10 Observability

Figure 10-12. Create Index Pattern

By default, the index name logstash-<date> will show up, which needs

to be copied and pasted in the Index pattern name field as shown in the

following. Then click Next step.

476

Chapter 10 Observability

Figure 10-13. Add logstash pattern

Select timestamp and then click “Create index pattern” as shown here.

477

Chapter 10 Observability

Figure 10-14. Selecting timestamp

Once the index pattern is created, click on the hamburger menu and

then click Discover.

478

Chapter 10 Observability

Figure 10-15. Navigating to the Discover page

In this page, we can now see all our Application as well as Kubernetes

logs as shown in the following pictures.

479

Chapter 10 Observability

Figure 10-16. Application as well as Kubernetes logs

Kubernetes logs can be seen by default.

480

Chapter 10 Observability

Figure 10-17. Kubernetes logs

To see the application logs, visit the application and then create a user

with testata as shown in the following.

Click Add User.

481

Chapter 10 Observability

Figure 10-18. Adding test data

Now go back to the OpenDashboard Discover page, and in the query

box, search for “test” as shown in the following, and we can see our

application logs being shown.

Figure 10-19. Viewing test data in the OpenSearch console

482

Chapter 10 Observability

Thus, this sets up our logging infrastructure where we can view all our

logs in a single place.

10.8 Monitoring

Monitoring our infrastructure is very critical especially when we are

having so many moving parts like pods, ingresses, nodes, etc. How do we

know how many pods are running or how many nodes are currently there

without actually accessing the Kubernetes API?

Kubernetes dashboard provides a one-stop solution for logging as well

as monitoring; however, Prometheus and Grafana stack are one of the

most popular as they can be used to monitor not just Kubernetes but also

EC2 instances and many more types of infrastructure that Prometheus

supports.

Hence, the monitoring infrastructure in this book has been set up

using Prometheus, Grafana, and the default metrics server provided by

Kubernetes. Installation of this entire stack has been encapsulated in the

following module:

483

Chapter 10 Observability

&ŝůĞ͗ŝŶĨƌĂͰŽďƐĞƌǀĂďŝůŝƚLJ͘ƞ

Ϯϴ͗ŵŽĚƵůĞΗŵŽŶŝƚŽƌŝŶŐΗ΂

Ϯϵ͗ƐŽƵƌĐĞсΗͬ͘ŵŽĚƵůĞƐͬŵŽŶŝƚŽƌŝŶŐΗ

ϯϬ͗

ϯϭ͗ŐƌĂĨĂŶĂͺǀĞƌƐŝŽŶсΗϲ͘Ϯϲ͘ϬΗ

ϯϮ͗ƉƌŽŵĞƚŚĞƵƐͺǀĞƌƐŝŽŶсΗϭϱ͘ϴ͘ϬΗ

ϯϯ͗ŵƐͺǀĞƌƐŝŽŶсΗϯ͘ϴ͘ϬΗ

ϯϰ͗ĂĐŵͺĂƌŶсŵŽĚƵůĞ͘ĂĐŵ͘ĂĐŵͺĂƌŶ

ϯϱ͗ƵƌůсůŽĐĂů͘Ƶƌů

ϯϲ͗njŽŶĞͺŝĚсĚĂƚĂ͘ĂǁƐͺƌŽƵƚĞϱϯͺnjŽŶĞ͘ĚŽŵĂŝŶ͘njŽŶĞͺŝĚ

ϯϳ͗

ϯϴ͗ĚĞƉĞŶĚƐͺŽŶс΀

ϯϵ͗ŵŽĚƵůĞ͘ĞŬƐ͕

ϰϬ͗ƌĞƐŽƵƌĐĞ͘ŬƵďĞĐƚůͺŵĂŶŝĨĞƐƚ͘ĂǁƐͺĂƵƚŚ

ϰϭ͗΁

ϰϮ͗΃

Figure 10-20. Setting up monitoring using Prometheus, Grafana,

and Metrics Server

L31–33: Grafana, Prometheus, and Metrics Server are being set up using

Helm charts, and their specific versions have been provided here. All three are

being set up on our Kubernetes Nodes and Grafana exposed to the Internet.

L34–36: These details are needed to expose Grafana over to the

Internet as monitoring.<environment>.<domain-name>. For example, for

our case, it would be monitoring.dev.gitops.rohitsalecha.com.

10.8.1 Exploring Grafana

Let’s access our Grafana application located a

Credentials are grafana:Gr@fAna123, hard-coded in

pgitops\infra\modules\monitoring\main.tf.

484

Chapter 10 Observability

Figure 10-21. Logging into Grafana

It is strongly recommended to change these credentials and also

implement a Secrets Management strategy as discussed in Chapting AWS Secrets Manager.

Post authentication, the default screen of Grafana can be seen. Since

no dashboard has been set up, let’s import one by clicking Import as

shown here.

485

Chapter 10 Observability

Figure 10-22. Importing a Grafana dashboard

Enter the ID 14623 as shown in the following and click Load.

Figure 10-23. Enter the ID 14623

486

Chapter 10 Observability

Select the Datasource as Prometheus, leaving everything else as

default. Then click Import.

Figure 10-24. Selecting Prometheus as the default data source

And our super cool Grafana dashboard is ready for monitoring almost

all metrics: CPU usage, RAM usage, number of pods, number of nodes, etc.

487

Chapter 10 Observability

Figure 10-25. Grafana dashboard showing all monitoring metrics

10.9 Karpenter Autoscaler

One of the biggest advantages of using Kubernetes is the ability to scale

based on various factors. We can scale up pods based on the incoming

network traffic and also scale up nodes to accommodate the rising number

of pods. The former is also called vertical scaling, and the latter is called

horizontal scaling.

This chapter will discuss horizontal scaling, that is, scaling nodes

based on the increasing/decreasing number of pods.

For AWS EKS, AWS has an open source project called Karpenter that

can be installed using Helm charts, and the entire configuration has been

encapsulated in a module as shown here.

488

Chapter 10 Observability

Code Block 10-16. Installing Karpenter

File: pgitops\infra\eks.tf

58: module "karpenter" {

59: source = "./modules/karpenter"

60:

61: node_group_role_arn = module.eks.eks_managed_node_

groups["app"].iam_role_name

62: eks_cluster_id = module.eks.cluster_id

63: eks_cluster_endpoint_url = module.eks.cluster_endpoint

64: oidc_url = module.eks.cluster_oidc_

issuer_url

65: oidc_arn = module.eks.oidc_provider_arn

66: karpenter_version = "v0.6.0"

67:

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

69: }

L61: The EKS module creates a role that has permissions to create/

delete EC2 nodes, and that Role name is being passed on to Karpenter so

that it can also get the same permissions to create EC2 instances and join

them into the EKS cluster.

L62–66: Details about the cluster are needed so that Karpenter can

join the correct cluster.

10.9.1 Karpenter in Action

Let’s view Karpenter in action. As per the Grafana dashboard we saw

earlier, only two nodes were configured. Let’s now increase the number

of deployment replicas from 1 to 10 as shown below and then perform

terraform apply.

489

Chapter 10 Observability

Code Block 10-17. Updating deployments to view Karpenter

in action

File: pgitops\infra\app.tf

099: resource "kubernetes_deployment_v1" "app" {

100: metadata {

101: name = var.org_name

102: namespace = kubernetes_namespace_v1.app.

metadata.0.name

103: labels = {

104: app = var.org_name

105: }

106: }

107:

108: spec {

109: replicas = 10

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

L109: Add a zero.

Let’s run terraform apply and observe the nodes and metrics in

Grafana.

CLI Output 10-18. Executing changes of ten deployments

cmd> export AWS_PROFILE=sita-dev

cmd> cd pgitops/infra

cmd> terraform apply --auto-approve

Before Apply

490

Chapter 10 Observability

Figure 10-26. Before applying the changes

After Apply

Ten minutes after applying the changes, CPU usage is high, the

number of nodes is 3, and the number of pods shots up to 40!

491

Chapter 10 Observability

Figure 10-27. After applying changes

Let’s view the change from the console.

CLI Output 10-19. Viewing changes from the console

cmd> aws eks update-kubeconfig --name gitops-us-east-1-dev

--region us-east-1

cmd> kubectl get nodes -o wide

Output shows three nodes; however, if carefully observed, the runtimes

of the three nodes are different as shown here.

492

Chapter 10 Observability

CLI Output 10-20. Three nodes are up, one by Karpenter

That is because when Karpenter launches a node, it builds it with the

default runtime as containerd://1.4.13

Hence, setting up the nodes with Karpenter is an easy task; however,

if the infrastructure was to be destroyed at this stage, Terraform would

error out as it doesn’t have the capability to delete a node set up using

Karpenter. This is a known issue captured here:

Hence, if a Karpenter launched node needs to be deleted, then an

instance with the name “karpenter.sh/provisioner-name/default” needs to

be searched in the EC2 home page and manually deleted.

To avoid such a scenario, let’s revert back the setting from ten replicas

to one and apply terraform apply.

CLI Output 10-21. Reverting the changes

cmd> export AWS_PROFILE=sita-dev

cmd> cd pgitops/infra

cmd> terraform apply --auto-approve

The Grafana dashboard should now show the reduced numbers as it

was earlier once terraform apply is completed.

10.10 Upgrading Kubernetes

Upgrading Kubernetes infrastructure is challenging for any administrator.

However, with AWS EKS and Terraform, it is quite simple where we only

need to modify the version (and also ensure that the deployment resources

493

Chapter 10 Observability

are compatible with that version) in the pgitops\infra\terraform.auto.tfvars

from 1.20 to 1.21 and then perform terraform apply!

Code Block 10-22. Upgrading to version 1.21

File: pgitops\infra\terraform.auto.tfvars

36: eks_version = "1.21"

CLI Output 10-23. Executing the change

cmd> export AWS_PROFILE=sita-dev

cmd> cd pgitops/infra

cmd> terraform apply --auto-approve

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

Plan: 0 to add, 20 to change, 0 to destroy

The apply operation will take approximately 40–45 minutes as the

following actions will be undertaken:

1. AWS Master node will be upgraded which is totally

abstracted from us which would take about 10

minutes as shown below. New TLS certificates being

generated shows a new cluster has been created.

CLI Output 10-24. New cluster being created

494

Chapter 10 Observability

2. New nodes with the new version will be created first.

CLI Output 10-25. New nodes being created

The Grafana dashboard at this point shows six nodes.

Figure 10-28. Grafana dashboard showing six nodes

3. Once new nodes are created, pods from the old

nodes will be cordoned off and consequently will

be installed in new nodes. At this point, Grafana

will sign you out, and you’ll need to re-log in and

re-import the dashboard. Once done, it should show

around nine nodes as shown here.

495

Chapter 10 Observability

Figure 10-29. Grafana dashboard showing nine nodes

4. Old nodes will be destroyed/unscheduled once

the pods are cordoned off. The following Grafana

dashboard shows one node is unschedulable,

meaning old nodes are now being destroyed.

496

Chapter 10 Observability

Figure 10-30. One node is unschedulable

5. Finally, the entire cluster will fall back to normalcy

with 2 nodes and 25 pods as shown here.

497

Chapter 10 Observability

Figure 10-31. Cluster back to normal

In this entire process, the application never goes down! It’s always up.

Kubernetes has now been upgraded to 1.21 as shown here!

Figure 10-32. New cluster is up

10.11 Clean-Up

While cleaning the infra folder is quite easy and straightforward as in

this chapter, only the development environment was set up. So a simple

terraform destroy would do the work as shown here.

498

Chapter 10 Observability

CLI Output 10-26. Destroying the environment

cmd> export AWS_PROFILE=sita-dev

cmd> cd pgitops/infra

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.

However, cleaning up the global folder is a bit challenging as there are

plenty of dependencies involved. These steps are totally optional, and the

only cost that is being incurred if the global folder is not destroyed is the

three Route53 Zones that have been set up. The rest is all nonchargeable.

The very first thing that needs to be done is that each user needs to

first delete their access keys by manually logging into the AWS console and

then deactivating and deleting the keys.

However, for us since Sita is the IAMAdmin in the Identity account we

can login into the account and delete the keys of all users.

Once the access keys have been deleted, browse to the Global

workspace of the Terraform Cloud application and destroy the plan as

shown here.

499

Chapter 10 Observability

Figure 10-33. Destroy Global in Terraform Cloud

Terraform destroy will run effectively and destroy all the resources

except the AWS accounts and will show the following error.

500

Chapter 10 Observability

Figure 10-34. Terraform account deletion error

AWS accounts cannot be deleted using Terraform, and hence, the only

way to delete them is to follow these steps:

– Reset the password of the root user for each of the pre-

ceding accounts using the forgot password functionality.

Please set a strong password and enable 2FA if possible.

– You can now close your account by simply navigating to

the Accounts section here:

. Scroll down and click Close Account.

501

Chapter 10 Observability

Figure 10-35. Closing the AWS account

Once the account is closed, you can no longer use the email address

associated with the closed account to create a new account.

Tip if you ever plan to re-run the terraform code in the global folder,

ensure to use new email addresses like instead of

, .

Also do remove the NS mapping in your hosted zone records that were

created in Chapter

502

Chapter 10 Observability

10.12 Conclusion

This concludes the chapter and this book where we saw how cloud and

cloud-native resources can be spun up and teared down with the power

of Infrastructure as Code all the while with full automation and guardrails

in place.

I hope by reading this book, the reader can get a better understanding

and insight into Terraform, AWS, and GitHub Actions, the three main

pillars of discussion throughout this book, and how all of them can be

tied together to bring forth a completely automated system that is version

controlled in Git, making it practical GitOps. :-)

503

ANNEXURE A

Manually Delete

Resources

In order to delete resources from a specific account, first, we need to as-

sume a role into that account, namely, prod, staging, and dev, where the

resources are stuck.

The same can be done by logging into the identity account and then

clicking Switch role as shown here.

Assume the role by entering the requisite information. All the

information is available in the Global workspace of the Terraform cloud

application.

© Rohit Salecha 2023

505

R. Salecha, Practical GitOps,

Annexure A MAnuAlly Delete resources

Now navigate through the following links to destroy the specific

resources.

506

Annexure A MAnuAlly Delete resources

Ensure that the resources are destroyed in this specific order. Also ensure you are in the correct region.

1) Delete RDS –

2) Delete RDS Subnet Group –

3) Delete SSM Parameter –

4) Delete EKS – Click on the EKS Cluster configured:

a) Delete NodeGroups – Click on each Node Group as

shown here.

b) Delete the App NodeGroup.

507

Annexure A MAnuAlly Delete resources

c) Navigate back. Select the System NodeGroup and then delete

the System NodeGroup.

d) Wait for about five minutes and then delete the EKS Cluster as

shown in the following once the NodeGroup is shown empty.

5) Delete the OpenSearch Cluster –

508

Annexure A MAnuAlly Delete resources

6) Disable and delete KMS keys – All KMS keys with

alias as “eks” need to be first disabled and then

delete

7) Delete Identity Provider –

8) Delete LB –

9) Delete ACM Certificates –

10) Delete Secret from Secrets Manager –

11) Delete CloudWatch Log Groups (All) –

12) Detach VPC Gateways – First, select the VP Gateway

and then detach it. Once detached, delete it:

13) Delete NAT Gateway –

509

Annexure A MAnuAlly Delete resources

14) Release Elastic IPs – (Wait for NAT Gateway to

be deleted before tryin

15) Delete any residual Network Interfaces(if any) –

16) Delete the VP

17) Delete Customer-Created Policies – AWSLoadBa

lancerControllerIAMPolicy1,secrets_policy and

AmazonEKS_EFS_CSI_Driver_Policy

18) Delete the following IAM Roles – app-

eks-node-group-2*************** ,

aws-load-balancer-controller,gitops-

us-east-1-dev-cluster-2*************,system-eks-

node-group-20*****************,irsa-aws-secrets,efs-

csi-controller-sa

510

ANNEXURE B

Terraform Cloud

Destroy Problem

While destroying the staging/production environment from the Terra-

form Cloud, you may encounter an error like as follows.

In this scenario, we need to download the state file from the terraform

cloud, add it in the location where the source code for the development

environment is located, modify the variables, and then run terraform

destroy as shown in the following.

1. Download the state file.

© Rohit Salecha 2023

511

R. Salecha, Practical GitOps,

Annexure B terrAforM clouD Destroy ProBleM

2. Move the state file to the code location.

cmd> ls -al

total 4064

-rw-r--r--@ 1 rohit rohit 974819 Jul 8 18:45

sv-1JTUXEs2GQTWoU3D.tfstate

cmd> mv sv-1JTUXEs2GQTWoU3D.tfstate terraform.tfstate

cmd> mv terraform.tfstate pgitops/infra

3. Update the default variables to reflect the staging/

prod environment.

File: pgitops/infra/variables.tf

4: variable "environment" {

5: description = "The Deployment environment"

6: type = string

7: default = "staging"

8: }

9:

15: variable "region" {

16: description = "AWS region"

17: type = string

18: default = "us-east-1"

19: }

512

Annexure B terrAforM clouD Destroy ProBleM

4. Update the account of the staging/prod

environment that needs to be destroyed.

File: infra/terraform.auto.tfvars

43: assume_role_account_id = "16XXXXXXXXX9"

5. Finally, run terraform destroy.

cmd> export AWS_PROFILE=sita-staging

cmd> terraform destroy --auto-approve

513

ANNEXURE C

Code Compatibility

on OS

C.1 Mac M1

All chapters have been thoroughly tested, and code is working fine

on Mac M1.

C.2 Windows Native (cmd.exe)

Code works absolutely fine on all chapters except for Chapt

In Chapt, I am making use of a local-exec as shown in the following where curl needs to be used. This local-exec is not working on Windows

even after installing curl on the Windows path. Hence, Chapt

doesn’t work on Windows native.

File: pgitops\infra\modules\logging\main.tf

156: resource "null_resource" "fluentbit-role" {

157: provisioner "local-exec" {

158: command = <<EOT

159: curl -sS -u "${local.es_username}:${local.es_

password}" -X PATCH \

© Rohit Salecha 2023

515

R. Salecha, Practical GitOps,

Annexure c coDe coMPAtiBility on os

160: https://${aws_elasticsearch_domain.es.endpoint}/_

opendistro/_security/api/rolesmapping/all_

access?pretty \

161: -H 'Content-Type: application/json' \

162: -d'

163: [

164: {

165: "op": "add", "path": "/backend_roles",

"value": ["'${aws_iam_role.irsa_

fluentbit.arn}'"]

166: }

167: ]

168: '

169: EOT

170: }

171: triggers = {

172: endpoint = aws_elasticsearch_domain.es.endpoint

173: }

174:

175: depends_on = [resource.aws_elasticsearch_domain.es]

176: }

C.3 WSL2 (Ubuntu)

Tested the code on WSL2 for all chapters and it works absolutely fine till

Chaptowever, from Chapt onward, you might get this error:

│ Error: Kubernetes cluster unreachable: Get "https://

B29481B1D200A05798E568DCF979DE62.gr7.us-east-1.eks.amazonaws.

com/version": getting credentials: decoding stdout: no

kind "ExecCredential" is registered for version "client.

516

Annexure c coDe coMPAtiBility on os

authentication.k8s.io/v1alpha1" in scheme "pkg/runtime/

scheme.go:100"

│ with module.alb_ingress.helm_release.lbc,

│ on modules/alb/main.tf line 73, in resource "helm_

release" "lbc":

│ 73: resource "helm_release" "lbc" {

To resolve this error on WSL (Ubuntu), modify

api_version = “client.authentication.k8s.io/v1beta1” to v1alpha1 as shown below

provider "helm" {

kubernetes {

host = module.eks.cluster_endpoint

cluster_ca_certificate = base64decode(module.eks.cluster_

certificate_authority_data)

exec {

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

command = "aws"

args = [

"eks",

"get-token",

"--role-arn",

local.assume_role_arn,

"--cluster-name",

module.eks.cluster_id

]

}

}

}

This issue has been reported (

517

Index

A, B

production environment, 240

profiles, 385–387

Amazon Web Services (AWS), 1

Route53 Zone, 355–357

access control, 390–393

scalability, 32

account creation

secrets management

access keys screen, 37

design, 414–424

cost explorer, 35

email address, 33

security/secrets management,

final screen, 37

397, 408–418

IAM dashboard, 34

Spring Boot app, 263

policy/permissions screen, 36

terraform, 68

user screen, 35

Application Load Balancer (ALB)

authentication/authorization

access logs, 470–472

(authz), 372–385

security/secrets

cloud computing, 33

management, 408–410

documentation, 31

Application programming

EC2 machine, 40

interface (API)

Elastic Compute Cloud, 33

actions tab, 213

historical overview, 32

driven terraform workspace,

identity and access

210, 211

management

environment variables, 211, 212

(IAM), 338–355

pushing workflow, 212

infrastructure resources, 32

workflow execution

login information

apply stage, 215–217

access keys file, 38

configuration, 213

CLI profile configuration, 39

execution, 215

sign-in screen, 39

logs streaming, 218

organization trail, 455

trigger, 214

organization units, 326–338

triggering workflow, 214

© Rohit Salecha 2023

519

R. Salecha, Practical GitOps,

INDEX

Application programming

auth ConfigMap, 380

language (API)

client authentication, 374,

driven workspace, 232

375, 377, 379

Authentication/authorization

ConfigMap creation, 376

(authz), 323

cross-account role

access control, 390–393

access, 383

AWS profiles, 385–387

development

clean-up process, 393

environment, 384

dev environment, 393

MySQL/PostgreSQL, 373

staging/prod environments,

node authentication,

394, 395

379–382

code update, 324–326

prod and staging

credential file, 385

environments, 384

development environment, 387

provider

global execution

configuration, 382–385

account IDs, 368

RoleBinding code, 378

apply process, 364–367

pgitops directory, 324

email information, 371

prod environment, 389, 390

GNU PG keys, 358–360

requirements, 323

links, 368

staging workspace, 388, 389

name servers, 369

Automation

output process, 368

cloud application

PR screen request, 366

authorization, 176, 177

resources, 357

connection, 176

subscription, 372

creation, 175

terraform cloud workspace,

execution plan, 184

363, 364

GitHub repository, 178

users block, 370

limitation, 194

variable

queue destroy plan, 193

configuration, 360–363

repositories, 178, 179

IAM roles for service

version control

accounts, 424–432

workflow, 175

infrastructure, 385–390

driven workspaces, 179–182

Kubernetes specifications

EC2 status, 219

520

INDEX

environment variables, 183

automation, 175–178

execution plan

backend.tf file, 142

accessing public IP, 187

cleanup commands

apply completed, 186

confirmation screen, 165

confirm/apply terraform

deletion, 164

plan, 186

destroy, 161

manual trigger, 184

destruction and

plan output, 185

deletion, 163

GitHub repository

EC2 console, 161

creation, 172

workspaces, 162–165

HTTPS URL, 173

CLI output, 151

source code, 173

configuration file, 142, 143

SSH URL, 173

destroy command, 151

steps, 171

dev.hcl file, 142

init/plan/apply commands, 171

directory structure, 140

manual trigger, 187–191

multi-environment (DRY),

prerequisites, 170

156–160

queue destroy plan, 218–220

operations, 127, 128

plan output, 145

C

prerequisite, 124

requirements, 126

Checkov scanning

running terraform plan, 143, 144

action, 434, 435

runs/states tab

definition, 432

configurations, 154

execution process, 436

diff, 154, 155

GitHub actions, 433

log command, 152, 153

PR messages, 438

states log, 153

results, 436, 437

states tab, 153

security vulnerabilities, 432

security plan, 165, 166

spooling results, 433

sensitive/critical file, 123

testing, 435

state management

client-server architecture, 14

availability, 125

Cloud infrastructure/workspaces

confidentiality, 124

apply information, 145–150

state file, 126

521

INDEX

Cloud infrastructure/

Container Storage Interface (CSI),

workspaces ( cont. )

400

terraform.tfstate, 124

Continuous deployment (CD)

tfstate file, 125, 126

developers/administrators, 8

workspaces

docker image, 9

access settings, 132

GitHub actions, 194, 195

API token screen, 138

manifest file, 8, 10

CLI-driven workflow, 130

pipeline as code, 8, 9, 24

configuration, 131, 132

problems, 23–25

dev creation, 131

software engineering

environment variables, 136

approach, 8–11

login, 137–140

Continuous integration (CI), 5–7

modifying version, 133

GitHub actions, 194, 195

options, 129

Continuous monitoring, 12–14

organization screen, 129

scratch screen, 128

terraform variables, 136

D

variables tab, 134–137

Database

version control

configuration/secrets, 286–288

workflow, 129

implementation, 284

Code compatibility

module, 284–286

Mac M1, 517

PostgreSQL database, 283

Windows native (cmd.exe), 517

storing database, 286

WSL2 (Ubuntu), 518, 519

Delete resources

Command line interface (CLI), 67

account information, 507

attributes, 61

App NodeGroup, 509, 510

cloud infrastructure/

EKS Cluster configuration, 509

workspaces, 130

links, 508

environment configuration, 57

NAT gateway, 511

lacking automation, 57

network interfaces, 512

launching creation, 60, 61

OpenSearch cluster, 510

security group ID, 58

resources, 509–512

SubnetID, 59, 60

Terraform cloud

termination, 63

application, 507

522

INDEX

DevOps system, 2

dev and prod accounts, 156

central repository, 6, 7

init error, 157

challenges/limitations, 4

init reconfiguration, 158

continuous deployment (CD),

options, 158

8–11, 23–25

prod.hcl, 156

continuous integration, 5–7

variables, 157

Elasticsearch/Kibana, 12

TF variables, 159, 160

Infrastructure as Code, 14–23

methodology, 2, 5, 13

E, F

monitoring, 11–13

Prometheus/Grafana, 12

Elastic Compute Cloud (EC2), 67

requirements, 3

CLI tool, 57–64

SDLC model, 3

definition, 33

waterfall model, 2

instance selection screen, 44, 45

Domain name system (DNS)

metadata security, 407, 408

ACM certificate, 303

name and environment

authentication/authorization

tags, 48

(authz), 369

region selection, 40

delete records, 271–273

base operating system, 42

HTTPS certificate, 303

default screen, 41

ingress resource hostname, 303

launch instance screen, 42

load balancer controller, 299

search bar, 41

multi-environments, 301

virtual private cloud, 44

possibilities, 268

resources, 64

record creation, 299–304

security groups, 49, 50

subdomain, 270, 271

service control policies, 440

top-level domain, 269, 270

SSH key-pair generation

zone files/hosted zones, 268

creation screen, 52

Don’t Repeat Yourself (DRY)

final review screen, 51

domain name system, 301

gitops, 52

multi-environment, 156

instances, 53, 54

production environment

Launch information, 50

backend configuration

name and download

file, 157

screen, 53

523

INDEX

Elastic Compute

cluster process

Cloud (EC2) ( cont. )

access environment, 318–320

storage configuration

development

screen, 47, 48

environment, 317

tags, 47, 48

setup information,

termination, 56

316–318

user data script

user lists, 318

configuration, 46–48

components, 288, 289

version controlled

features, 289

system, 187–191

IAM Roles for Service

view instance

Accounts, 425–433

browser screen, 56

ingress controller, 297, 298

IP address, 55

Karpenter autoscaler, 489–494

launch status screen, 54, 55

module source, 291

Elastic, Fluent Bit (EFK), 472

secrets management, 413, 414

Elastic Kubernetes Service (EKS),

security groups, 295, 296

263, 399

security/secrets

access control, 390–393

components, 402

application

EBS device, 406, 407

components, 304

EC2 instances, 407, 408

default environment, 315

encryption keys, 403–405

deployment

PostgreSQL, 403

resource, 306–309

server setup, 290

ingress resource, 310–313

worker nodes, 292, 294

ingress/service/

Elastic Load Balancer (ELB), 303

deployment, 304

Event-Driven Architecture

namespace, 305, 306

(EDA), 458

NodePort service, 309, 310

providers, 313, 314

G, H

variables, 314

YAML file, 305

GitHub

architecture, 289

automation, 169

authentication/authorization

API-driven workflow,

(authz), 373–385

210–213

524

INDEX

code walkthrough, 207–209

output.tf, 246

login token, 209, 210

process, 243

CI/CD operations, 194, 195

dev (pull request)

docker image, 199–202

branches, 250

github/workflows, 196

cloud application, 255

GitOps process, 221

comments, 251

HTML template, 203

creation, 248–256

Jenkins stages, 195

GitHub actions, 252

logs, 206

init/validate/plan review

remote repository, 202

screen, 253

repositories, 264–273

IP addresses, 255

security/secrets

merge option, 254, 255

management, 397

Prod, 256–259

Spring Boot app, 263

repository online, 249

trigger, 202–207

staging/branches, 250

workflow stages, 204, 205

prerequisites, 222

YAML file, 196–198

production

GitHub Actions, 8

environment, 239–242

GitOps

pull request (PR), 221

business applications, 26

staging environment workflow

components, 30

backend.tf file, 226

environments, 26

code walkthrough, 224–231

infrastructure deployments, 26

directory structure, 224

manifests, 28

GitHub repository,

pipeline, 30

233–235

process, 27, 28

high-level plan, 222–224

PUSH/PULL, 28

infra-staging.yaml,

GitOps process

225, 226, 228, 230

clean-up section, 259, 260

scenario, 227, 228

complete workflow

set up, 231–233

developer testing, 247, 248

staging and prod

diagram, 245

environments, 228

feature request, 245, 246

staging branch workflow, 237

main.tf file, 245

terraform apply, 237, 238

525

INDEX

GitOps process ( cont. )

IAMAdministrator permissions

variables tab, 232

Policy, 346

workflow execution,

IAMAdmin role, 345

235–239

IRSA access key, 424–432

workspace variables, 233

organizations/user access, 339

Grafana application

password policy, 344

credentials, 484

permissions table, 355

dashboard, 485, 486

security/secrets

logging process, 485

management, 400

monitoring metrics, 488

self-managed group, 347

Prometheus selection, 487

user_role_mapping, 352

upgrading Kubernetes, 495

users module

Graphical user interface (GUI),

implementation, 342–347

40, 67

Immutable infrastructure, 19

Infrastructure as Code (IaC), 156

I, J

configuration tools, 18

mutable vs. immutable

IAM Roles for Service

infrastructure, 18–20

Accounts (IRSA)

provisioning tools, 17

account resource, 428

requirements, 14–23

authentication/

server infrastructure

authorization, 424

advantage, 17

internal working, 426, 427

client-server architecture, 14

module implemention, 429

container orchestration

OpenID Connect, 424

system, 16

permissions policy, 427–432

Dockerfile, 16

policy resource, 428

terraform code, 17

secret manager, 427

virtualization, 15

service account creation, 431

state, 20–23

Identity and access

Terraform state, 20

management (IAM), 323

tools of trade, 17, 18

cross-account roles

Instance Metadata

function, 348–355

Service (IMDSv2), 407

groups, 345–348

integrity, 125

526

INDEX

K, L

access keys, 499

account deletion error, 501

Karpenter autoscaler

close account, 501, 502

deployment replicas, 489

destroy global plan, 500

deployments, 490, 492

development environment,

Helm charts, 488

498, 499

installation, 489

CloudWatch alarm

nodes, 493

bucket creation, 459

nodes/metrics, 490

configuration, 461, 462

vertical/horizontal

EDA infrastructure, 458

scaling, 488

root login alarm, 462, 463

Key Management Services (KMS),

root login breach count, 464

403–405

send email/text

Kubernetes, 288

notifications, 459

SNS configuration, 459, 460

M

code update, 449–452

Mutable vs. immutable

executing infra, 454, 455

infrastructure, 18–20

global folder, 452, 453

infrastructure, 449

infra system, 453

N

Karpenter autoscaler, 488–493

Networking

logging infrastructure, 472–483

code description, 277

master account login, 464–470

high-level architecture, 276

admin role privileges, 465

module/vpc folder, 279, 280

GitOps user credentials, 465

networking.tf file, 277

navigation, 469

security groups, 282, 283

root credentials, 464

tags, 280, 281

streams, 470

switch role console, 466, 468

O

monitoring

Grafana application, 484–487

Observability

one-stop solution, 483

ALB access logs, 470–472

Prometheus/Grafana/

cleaning process

Metrics server, 483, 484

527

INDEX

Observability ( cont. )

Organization units (OUs)

OpenSearch, 449

member accounts

creation, 472, 473

creation, 332

data adding, 475

duties/responsibilities, 328

definition, 472

email/names configurations,

discover page, 479

333, 334

EFK/Kibana, 472

high-level view, 328, 329

index pattern, 476

identity account, 330

Kubernetes logs, 480, 481

management account, 329

login console, 474

Terraform creation, 330, 331

logstash pattern, 477

providers, 336–340

post authentication, 475

root account, 326

scalability and security, 472

limitations/drawbacks, 328

test data, 482

management account, 327

timestamp selection, 478

operating model, 327

viewing data, 482

organization trail

P, Q

CloudTrail logs, 455

configuration, 456, 457

Production environment

enable option, 456

definition, 239

features, 456

pull request page

logging configuration, 458

GitHub action, 257

master account, 458

main/staging

service principal, 456

branches, 256

standard process, 455

terraform apply, 258, 259

upgrading Kubernetes

SSH key-pair generation

apply operation, 494

apply execution, 242

cluster creation, 494

AWS command, 240

cluster process, 498

checking-in prod.hcl, 241

execution plan, 494

steps, 240

Grafana dashboard, 495

trigger, 242

modification, 493

workspace configuration,

nodes, 496, 497

239, 240

528

INDEX

R

global folder, 400, 401

IAM Roles for Service

Respiratory Distress

Accounts (IRSA), 425–433

Syndrome (RDS)

infra, 402

security/secrets management,

instance_type

410, 411

configuration, 446

Role-Based Access

Kubernetes master node/

Control (RBAC), 375

control plane, 402–408

RDS service, 410, 411

S

secrets exposure, 411

Secrets management

service control policies,

AWS design

438–445

current solution, 414, 415

Server-Side Request

definition, 418

Forgery (SSRF), 407

deployment file, 422

Service Control Policies (SCP),

descriptor file, 422

329, 400

installation, 419

action, 441

new solution, 415, 416

Checkov branch, 442

resource definition, 420

decode-authorization-

store CSI driver, 416–424

message, 444, 445

design, 414

InstanceType limitation, 443

Kubernetes deployment

testing, 442

descriptions, 413, 414

configuration, 441

password string, 412

definition, 438–445

terraform state, 411–413

EC2 instance types, 440

Security Assertion Markup

initialization, 438

Language (SAML), 424

tag policies, 439, 440

Security/secrets management

Simple Notification Service (SNS)

ALB logs, 408–410

notification, 460

Checkov, 397

subscription confirmation,

Checkov scanning, 432–438

459, 460

code update, 397–400

Source Code

dev environment, 446, 447

Management (SCM), 169

529

INDEX

Spring Boot app

cloud/workspaces, 123

application, 304–316

code folders, 71

database, 283–288

command iteration

destroy environment, 320

data source, 94, 95

domain name, 299–304

output declaration, 97–99

execution plan

problem execution, 91, 92

access process, 318–320

resources, 95–97

development environment,

Ubuntu package

317, 319

repository, 96

EKS cluster, 316–318

variable declaration,

infrastructure, 316

92–94

GitHub repository

commands reference, 121

auto apply, 268

core components, 69, 70

DNS records, 268–273

destroy commands, 119

pgitops folder, 264

drift

pushing code, 265

actions tab, 114, 115

single directory, 264

additional security group

terraform cloud

rule, 115

workspaces, 267

code execution, 113

TF_API_TOKEN, 266, 267

definition, 112

verification, 266

reconciliation, 116–118

kubectl utility, 263

rule edition

Kubernetes, 288–299

confirmation, 116

networking, 276–283

security groups section,

requirements, 263

113, 114

solution architecture, 273–276

tfstate file, 118

GitOps process, 221

T, U

iteration

apply command, 82

Terraform

dependencies, 73

aspects, 70

destroy, 87–91

automation, 169

documentation, 73

530

INDEX

folder structure, 76

version, 71

init command, 75–77

Terraform Cloud destroying

main.tf file, 72, 73

problem, 513–515

plan execution, 77–81

state file, 82–87

V, W, X, Y, Z

working directory, 74, 75

iteration1 code, 68

Version Controlled System

prerequisites, 67, 68

(VCS), 171

provision/configuration, 68

automation, 176

selective destroy

driven workspace

code execution, 109, 110

advanced options, 180, 181

deletion protection, 112

configuration, 182

main.tf file, 107

GitHub repositories, 179

output declaration, 108

working directory, 182

protection, 110–112

limitation, 194

security groups, 107

manual trigger

Spring Boot app, 263

apply completed, 191

third iteration

deployments, 191

alphabetical order, 100

EC2 code, 187, 188

code structure, 101, 102

fine tuning branch settings,

coding perspective, 99

191, 192

implementation, 99

GitHub repository, 188, 189

implicit/explicit, 100

infra folder, 187

modularize, 100

plan execution, 189, 190

module declarations/

server, 191

invocation, 103–108

Virtual private cloud (VPC), 44

531

Назад: 7.4.1 Database Module
На главную: Предисловие