Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new: EKS Pod Identity lab #789

Merged
merged 11 commits into from
Feb 14, 2024
2 changes: 1 addition & 1 deletion manifests/base-application/carts/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ spec:
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
image: "public.ecr.aws/aws-containers/retail-store-sample-cart:0.4.0"
image: "public.ecr.aws/aws-containers/retail-store-sample-cart:0.7.0"
imagePullPolicy: IfNotPresent
ports:
- name: http
Expand Down
22 changes: 22 additions & 0 deletions manifests/modules/security/eks-pod-identity/.workshop/cleanup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/bash

set -e

POD_ASSOCIATION_ID=$(aws eks list-pod-identity-associations --region $AWS_REGION --cluster-name $EKS_CLUSTER_NAME --service-account carts --namespace carts --output text --query 'associations[0].associationId')

if [ ! -z "$POD_ASSOCIATION_ID" ]; then
logmessage "Deleting EKS Pod Identity Association..."

aws eks delete-pod-identity-association --region $AWS_REGION --association-id $POD_ASSOCIATION_ID --cluster-name $EKS_CLUSTER_NAME

fi

check=$(aws eks list-addons --cluster-name $EKS_CLUSTER_NAME --region $AWS_REGION --query "addons[? @ == 'eks-pod-identity-agent']" --output text)

if [ ! -z "$check" ]; then
logmessage "Deleting EKS Pod Identity Agent addon..."

aws eks delete-addon --cluster-name $EKS_CLUSTER_NAME --addon-name eks-pod-identity-agent --region $AWS_REGION

aws eks wait addon-deleted --cluster-name $EKS_CLUSTER_NAME --addon-name eks-pod-identity-agent --region $AWS_REGION
fi
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
module "eks_blueprints_addons" {
source = "aws-ia/eks-blueprints-addons/aws"
version = "~> 1.0"

enable_aws_load_balancer_controller = true
aws_load_balancer_controller = {
wait = true
}

cluster_name = local.addon_context.eks_cluster_id
cluster_endpoint = local.addon_context.aws_eks_cluster_endpoint
cluster_version = local.eks_cluster_version
oidc_provider_arn = local.addon_context.eks_oidc_provider_arn
}

resource "time_sleep" "wait" {
depends_on = [module.eks_blueprints_addons]

create_duration = "10s"
}

resource "kubernetes_manifest" "ui_nlb" {
manifest = {
"apiVersion" = "v1"
"kind" = "Service"
"metadata" = {
"name" = "ui-nlb"
"namespace" = "ui"
"annotations" = {
"service.beta.kubernetes.io/aws-load-balancer-type" = "external "
"service.beta.kubernetes.io/aws-load-balancer-scheme" = "internet-facing"
"service.beta.kubernetes.io/aws-load-balancer-nlb-target-type" = "instance"
}
}
"spec" = {
"type" = "LoadBalancer"
"ports" = [{
"port" = 80
"targetPort" = 8080
"name" = "http"
}]
"selector" = {
"app.kubernetes.io/name" = "ui"
"app.kubernetes.io/instance" = "ui"
"app.kubernetes.io/component" = "service"
}
}
}
}

resource "aws_dynamodb_table" "carts" {
#checkov:skip=CKV2_AWS_28:Point in time backup not required for workshop
name = "${local.addon_context.eks_cluster_id}-carts"
hash_key = "id"
billing_mode = "PAY_PER_REQUEST"
stream_enabled = true
stream_view_type = "NEW_AND_OLD_IMAGES"

server_side_encryption {
enabled = true
kms_key_arn = aws_kms_key.cmk_dynamodb.arn
}

attribute {
name = "id"
type = "S"
}

attribute {
name = "customerId"
type = "S"
}

global_secondary_index {
name = "idx_global_customerId"
hash_key = "customerId"
projection_type = "ALL"
}

tags = local.tags
}

module "iam_assumable_role_carts" {
source = "terraform-aws-modules/iam/aws//modules/iam-assumable-role"
version = "~> v5.5.5"
create_role = true
role_requires_mfa = false
role_name = "${local.addon_context.eks_cluster_id}-carts-dynamo"
trusted_role_services = ["pods.eks.amazonaws.com"]
custom_role_policy_arns = [aws_iam_policy.carts_dynamo.arn]
trusted_role_actions = ["sts:AssumeRole", "sts:TagSession"]

tags = local.tags
}

resource "aws_iam_policy" "carts_dynamo" {
name = "${local.addon_context.eks_cluster_id}-carts-dynamo"
path = "/"
description = "Dynamo policy for carts application"

policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllAPIActionsOnCart",
"Effect": "Allow",
"Action": "dynamodb:*",
"Resource": [
"arn:aws:dynamodb:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:table/${aws_dynamodb_table.carts.name}",
"arn:aws:dynamodb:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:table/${aws_dynamodb_table.carts.name}/index/*"
]
}
]
}
EOF
tags = local.tags
}

resource "aws_kms_key" "cmk_dynamodb" {
description = "KMS CMK for DynamoDB"
deletion_window_in_days = 10
enable_key_rotation = true
policy = data.aws_iam_policy_document.cmk_dynamodb.json
}

resource "aws_kms_alias" "cmk" {
name = "alias/${local.addon_context.eks_cluster_id}-cmk-dynamodb"
target_key_id = aws_kms_key.cmk_dynamodb.key_id
}

data "aws_iam_policy_document" "cmk_dynamodb" {
statement {
sid = "Enable IAM User Permissions"
actions = [
"kms:*",
]
effect = "Allow"
principals {
type = "AWS"
identifiers = [
format(
"arn:%s:iam::%s:root",
data.aws_partition.current.partition,
data.aws_caller_identity.current.account_id
)
]
}
resources = ["*"]
}
statement {
sid = "Allow DynamoDB to get information about the CMK"
actions = [
"kms:Describe*",
"kms:Get*",
"kms:List*"
]
effect = "Allow"
principals {
type = "Service"
identifiers = [
"dynamodb.amazonaws.com"
]
}
resources = ["*"]
}
statement {
sid = "Allow principals to encrypt."
actions = [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt",
"kms:GenerateDataKey*",
"kms:DescribeKey"
]
effect = "Allow"
principals {
type = "AWS"
identifiers = ["*"]
}
resources = ["*"]
condition {
test = "StringLike"
variable = "kms:ViaService"
values = ["dynamodb.*.amazonaws.com"]
}
condition {
test = "StringLike"
variable = "kms:CallerAccount"
values = [data.aws_caller_identity.current.account_id]
}
}
statement {
sid = "Allow principals to decrypt."
actions = [
"kms:Decrypt",
"kms:DescribeKey"
]
effect = "Allow"
principals {
type = "AWS"
identifiers = ["*"]
}
resources = ["*"]
condition {
test = "StringLike"
variable = "kms:ViaService"
values = ["dynamodb.*.amazonaws.com"]
}
condition {
test = "StringLike"
variable = "kms:CallerAccount"
values = [data.aws_caller_identity.current.account_id]
}
}
}

output "environment" {
value = <<EOF
export CARTS_DYNAMODB_TABLENAME="${aws_dynamodb_table.carts.name}"
export CARTS_IAM_ROLE="${module.iam_assumable_role_carts.iam_role_arn}"
EOF
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: carts
namespace: carts
labels:
app: carts
data:
CARTS_DYNAMODB_TABLENAME: ${CARTS_DYNAMODB_TABLENAME}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CARTS_DYNAMODB_TABLENAME=${CARTS_DYNAMODB_TABLENAME}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../../../base-application/carts
configMapGenerator:
- name: carts
namespace: carts
env: config.properties
behavior: replace
options:
disableNameSuffixHash: true
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions website/docs/security/amazon-eks-pod-identity/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
title: "Amazon EKS Pod Identity"
sidebar_position: 30
sidebar_custom_props: {"module": true}
---

{{% required-time %}}

:::tip Before you start
Prepare your environment for this section:

```bash timeout=300 wait=30
$ prepare-environment security/eks-pod-identity
```

This will make the following changes to your lab environment:

- Create an Amazon DynamoDB table
- Create an IAM role for AmazonEKS workloads to access the DynamoDB table
- Install the EKS Managed Addon for EKS Pod Identity Agent
- Install the AWS Load Balancer Controller in the Amazon EKS cluster

You can view the Terraform that applies these changes [here](https://github.com/VAR::MANIFESTS_OWNER/VAR::MANIFESTS_REPOSITORY/tree/VAR::MANIFESTS_REF/manifests/modules/security/pod-identity/.workshop/terraform).

:::

Applications in a Pod’s containers can use a supported AWS SDK or the AWS CLI to make API requests to AWS services using AWS Identity and Access Management (IAM) permissions. For example, applications may need to upload files to an S3 bucket or query a DynamoDB table, and in order to do so, they must sign their AWS API requests with AWS credentials. [EKS Pod Identities](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html) provide the ability to manage credentials for your applications, similar to the way that Amazon EC2 Instance Profiles provide credentials to instances. Instead of creating and distributing your AWS credentials to the containers or using the Amazon EC2 instance's Role, you can associate an IAM Role with a Kubernetes Service Account and configure your Pods with it. Check out EKS documentation [here](https://docs.aws.amazon.com/eks/latest/userguide/pod-id-minimum-sdk.html) for the exact list of versions supported.

In this chapter we'll re-configure one of the sample application components to leverage the AWS API and provide it with the appropriate privileges.
24 changes: 24 additions & 0 deletions website/docs/security/amazon-eks-pod-identity/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
title: "Introduction"
sidebar_position: 31
---

The `carts` component of our architecture uses Amazon DynamoDB as its storage backend, which is a common use-case you'll find for non-relational databases integration with Amazon EKS. The way in which the carts API is currently deployed uses a [lightweight version of Amazon DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html) running as a container in the EKS cluster.

You can see this by running the following command:

```bash
$ kubectl -n carts get pod
NAME READY STATUS RESTARTS AGE
carts-5d7fc9d8f-xm4hs 1/1 Running 0 14m
carts-dynamodb-698674dcc6-hw2bg 1/1 Running 0 14m
```

In the case above, the Pod `carts-dynamodb-698674dcc6-hw2bg` is our lightweight DynamoDB service. We can verify our `carts` application is using this by inspecting its environment:

```bash
$ kubectl -n carts exec deployment/carts -- env | grep CARTS_DYNAMODB_ENDPOINT
CARTS_DYNAMODB_ENDPOINT=http://carts-dynamodb:8000
```

This approach can be useful for testing, but we want to migrate our application to use the fully managed Amazon DynamoDB service in order to take full advantage of the scale and reliability it offers.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
set -Eeuo pipefail

before() {
echo "noop"
}

after() {
sleep 10

kubectl rollout status deployment/carts -n carts --timeout 180s

export endpoint=$(kubectl -n kube-system get svc -n ui ui-nlb -o json | jq -r '.status.loadBalancer.ingress[0].hostname')

EXIT_CODE=0

timeout -s TERM 400 bash -c \
'while [[ "$(curl -s -o /dev/null -L -w ''%{http_code}'' ${endpoint}/home)" != "500" ]];\
do echo "Waiting for ${endpoint}" && sleep 30;\
done' || EXIT_CODE=$?

if [ $EXIT_CODE -ne 0 ]; then
>&2 echo "Load balancer did not become available or return HTTP 500 for 180 seconds"
exit 1
fi
}

"$@"
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
set -Eeuo pipefail

before() {
echo "noop"
}

after() {
sleep 10

kubectl wait --for=condition=available --timeout=120s deployment/carts -n carts

export endpoint=$(kubectl -n kube-system get svc -n ui ui-nlb -o json | jq -r '.status.loadBalancer.ingress[0].hostname')

EXIT_CODE=0

timeout -s TERM 180 bash -c \
'while [[ "$(curl -s -o /dev/null -L -w ''%{http_code}'' ${endpoint}/home)" != "200" ]];\
do echo "Waiting for ${endpoint}" && sleep 30;\
done' || EXIT_CODE=$?

if [ $EXIT_CODE -ne 0 ]; then
>&2 echo "Load balancer did not become available or return HTTP 200 for 180 seconds"
exit 1
fi
}

"$@"
Loading
Loading