diff --git a/.github/labeler.yml b/.github/labeler.yml index b4d975122..2cdf12e11 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,27 +1,31 @@ content/introduction: - - environment/modules/introduction/**/* + - manifests/modules/introduction/**/* - website/docs/introduction/**/* content/fundamentals: - - environment/modules/fundamentals/**/* + - manifests/modules/fundamentals/**/* - website/docs/fundamentals/**/* content/autoscaling: - - environment/modules/autoscaling/**/* + - manifests/modules/autoscaling/**/* - website/docs/autoscaling/**/* content/security: - - environment/modules/security/**/* + - manifests/modules/security/**/* - website/docs/security/**/* content/networking: - - environment/modules/networking/**/* + - manifests/modules/networking/**/* - website/docs/networking/**/* content/observability: - - environment/modules/observability/**/* + - manifests/modules/observability/**/* - website/docs/observability/**/* content/cost-optimization: - - environment/modules/costoptimization/**/* + - manifests/modules/costoptimization/**/* - website/docs/costoptimization/**/* + +content/fastpaths: + - manifests/modules/fastpaths/**/* + - website/docs/fastpaths/**/* diff --git a/.github/workflows/test-fastpaths.yaml b/.github/workflows/test-fastpaths.yaml index db919f339..31724cb05 100644 --- a/.github/workflows/test-fastpaths.yaml +++ b/.github/workflows/test-fastpaths.yaml @@ -66,7 +66,7 @@ jobs: DOCKER_DNS_OVERRIDE: "8.8.8.8" run: | export AWS_DEFAULT_REGION="$AWS_REGION" - bash hack/run-tests.sh "$CLUSTER_ID" "-" "{fastpaths/getting-started,fastpaths/getting-started/**,fastpaths/operator,fastpaths/operator/**,fastpaths/developer,fastpaths/developer/**}" + bash hack/run-tests.sh "$CLUSTER_ID" "-" "{fastpaths/getting-started,fastpaths/getting-started/**,fastpaths/operator,fastpaths/operator/**,fastpaths/developer,fastpaths/developer/**,fastpaths/eks-capabilities,fastpaths/eks-capabilities/**}" - name: Refresh AWS credentials if: always() uses: aws-actions/configure-aws-credentials@v4.3.1 diff --git a/.gitignore b/.gitignore index 5634411e7..2455e0dd0 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,16 @@ devenv.local.nix # direnv .direnv + +# Kiro .kiro/ +.claude/ + +# Terraform +*.tfstate +*.tfstate.* +*.tfplan +*.tfvars +!*.tfvars.example +.terraform/ +.terraform.lock.hcl diff --git a/.spelling b/.spelling index 046339f74..e42193727 100644 --- a/.spelling +++ b/.spelling @@ -58,6 +58,9 @@ apis versioned crds argocd +Argoadmin +preprovision +repoint dev webhooks gitops @@ -142,4 +145,7 @@ cni-tshoot resolv untolerated Workernodes -Gitea \ No newline at end of file +Gitea +kro +fastpath +fastpaths \ No newline at end of file diff --git a/hack/run-tests.sh b/hack/run-tests.sh index 45da55e80..3778a25ff 100755 --- a/hack/run-tests.sh +++ b/hack/run-tests.sh @@ -94,6 +94,7 @@ $CONTAINER_CLI run $background_args $dns_args \ -v $SCRIPT_DIR/../website/docs:/content \ -v $SCRIPT_DIR/../manifests:/eks-workshop/manifests \ -e 'EKS_CLUSTER_NAME' -e 'EKS_CLUSTER_AUTO_NAME' -e 'AWS_REGION' -e 'RESOURCES_PRECREATED' -e 'BASE_INBOUND_CIDRS' \ + -e 'ARGOCD_ADMIN_EMAIL' \ $aws_credential_args $container_image -g "${actual_glob}" --hook-timeout 3600 --timeout 3600 $output_args ${AWS_EKS_WORKSHOP_TEST_FLAGS} || exit_code=$? if [ $exit_code -eq 0 ]; then diff --git a/hack/shell.sh b/hack/shell.sh index 2e0f3fcae..0c3f11dff 100644 --- a/hack/shell.sh +++ b/hack/shell.sh @@ -44,5 +44,6 @@ $CONTAINER_CLI run --rm $interactive_args $dns_args \ -v $SCRIPT_DIR/../cluster:/cluster \ -e "RESET_NO_DELETE=true" \ -e 'EKS_CLUSTER_NAME' -e 'EKS_CLUSTER_AUTO_NAME' -e 'AWS_REGION' -e 'BASE_INBOUND_CIDRS' \ + -e 'ARGOCD_ADMIN_EMAIL' \ -p 8889:8889 \ $aws_credential_args $container_image $shell_command \ No newline at end of file diff --git a/lab/bin/reset-environment b/lab/bin/reset-environment index 4c29ca052..af97dd6ff 100644 --- a/lab/bin/reset-environment +++ b/lab/bin/reset-environment @@ -144,35 +144,45 @@ if [ ! -z "$module" ]; then logmessage "\nšŸ“¦ Deploying base application..." kubectl apply -k $base_path + # Stage the lab Terraform on disk every run so we can read outputs + # (or apply, if first run) against the kubernetes-secret backend. + rm -rf /eks-workshop/terraform + mkdir -p /eks-workshop/terraform + cp -R $manifests_path/.workshop/terraform/* /eks-workshop/terraform + rm -f /eks-workshop/terraform/lab-fastpaths.tf + + mkdir -p /eks-workshop/terraform/lab + cp -R $manifests_path/modules/fastpaths/developers/.workshop/terraform/* /eks-workshop/terraform/lab + cp $manifests_path/.workshop/terraform/lab-fastpaths.tf /eks-workshop/terraform/lab.tf + + mkdir -p /eks-workshop/terraform-data + export TF_DATA_DIR="/eks-workshop/terraform-data" + export TF_VAR_eks_cluster_id="$EKS_CLUSTER_NAME" + export TF_VAR_eks_cluster_auto_id="$EKS_CLUSTER_AUTO_NAME" + export TF_VAR_resources_precreated="false" + # eks-capabilities fast path (Lab 2) creates an IAM Identity Center user + + # group + membership in terraform; AWS sends an activation email to this + # address. Empty default is fine for other fastpaths; the precondition in + # argocd-capability.tf guards against an empty value when the + # eks-capabilities terraform actually runs. + export TF_VAR_argocd_admin_email="${ARGOCD_ADMIN_EMAIL:-}" + + tf_dir=$(realpath --relative-to="$PWD" '/eks-workshop/terraform') + # One-time preprovision: install KEDA, fluent-bit, external-secrets etc. TF_PID="" if [ "$RESOURCES_PRECREATED" != "true" ]; then logmessage "\nšŸ”§ First time setup: provisioning fastpaths infrastructure (this only runs once)..." - rm -rf /eks-workshop/terraform - mkdir -p /eks-workshop/terraform - cp -R $manifests_path/.workshop/terraform/* /eks-workshop/terraform - rm -f /eks-workshop/terraform/lab-fastpaths.tf - - # Copy lab files BEFORE destroy so it can clean up partial state - # from a previously interrupted apply - mkdir -p /eks-workshop/terraform/lab - cp -R $manifests_path/modules/fastpaths/developers/.workshop/terraform/* /eks-workshop/terraform/lab - cp $manifests_path/.workshop/terraform/lab-fastpaths.tf /eks-workshop/terraform/lab.tf - - mkdir -p /eks-workshop/terraform-data - export TF_DATA_DIR="/eks-workshop/terraform-data" - export TF_VAR_eks_cluster_id="$EKS_CLUSTER_NAME" - export TF_VAR_eks_cluster_auto_id="$EKS_CLUSTER_AUTO_NAME" - export TF_VAR_resources_precreated="false" - - tf_dir=$(realpath --relative-to="$PWD" '/eks-workshop/terraform') - terraform -chdir="$tf_dir" init -upgrade terraform -chdir="$tf_dir" destroy --auto-approve terraform -chdir="$tf_dir" apply --auto-approve & TF_PID=$! + else + # Already provisioned — just init so we can read outputs from the + # kubernetes-backed Terraform state. + terraform -chdir="$tf_dir" init -upgrade >/dev/null fi logmessage "\nā³ Waiting for application to become ready..." @@ -199,6 +209,16 @@ if [ ! -z "$module" ]; then logmessage "\nāœ… Fastpaths infrastructure provisioned!" fi + # Export the lab Terraform's `environment_variables` output into the IDE + # shell so labs and test hooks can reference values like + # EKS_CAP_ACK_CAPABILITY and EKS_CAP_DDB_TABLE. Mirrors the same write + # done at the bottom of this script for non-fastpaths labs. Runs whether + # or not preprovision just executed, so re-entries pick up the values. + terraform -chdir="$tf_dir" output -json \ + | jq -r '.environment.value | select(. != null)' \ + > ~/.bashrc.d/workshop-env.bash + echo "export INBOUND_CIDRS='${INBOUND_CIDRS}'" >> ~/.bashrc.d/workshop-env.bash + # Save cleanup hook for this module's path (developer or operator) rm -rf /eks-workshop/hooks # Map content path to manifests path (developer->developers, operator->operators) diff --git a/lab/iam/iam-role-cfn.yaml b/lab/iam/iam-role-cfn.yaml index f5f517f81..e7dcd20e4 100644 --- a/lab/iam/iam-role-cfn.yaml +++ b/lab/iam/iam-role-cfn.yaml @@ -93,6 +93,17 @@ Resources: PolicyDocument: file: ./iam/policies/labs4.yaml + EksWorkshopEksCapabilitiesPolicy: + Type: AWS::IAM::ManagedPolicy + DependsOn: + - EksWorkshopIdeRole + Properties: + Roles: + - !Ref EksWorkshopIdeRole + ManagedPolicyName: ${Env}-ide-eks-capabilities + PolicyDocument: + file: ./iam/policies/eks-capabilities.yaml + EksWorkshopTroubleshootPolicy: Type: AWS::IAM::ManagedPolicy DependsOn: diff --git a/lab/iam/policies/eks-capabilities.yaml b/lab/iam/policies/eks-capabilities.yaml new file mode 100644 index 000000000..0fe8e1c43 --- /dev/null +++ b/lab/iam/policies/eks-capabilities.yaml @@ -0,0 +1,102 @@ +Version: "2012-10-17" +Statement: + # IAM Identity Center lookup (preprovision data source) and + # workshop-scoped user/group lifecycle management for the Argo CD capability + # admin group + user the path provisions. The capability itself, when + # created, registers an SSO application against the IDC instance, so the + # IDE role (the principal calling CreateCapability) needs the application + # lifecycle actions too. + - Effect: Allow + Action: + - sso:ListInstances + - sso:DescribeInstance + - sso:GetApplication + - sso:ListApplications + - sso:CreateApplication + - sso:UpdateApplication + - sso:DeleteApplication + - sso:PutApplicationGrant + - sso:GetApplicationGrant + - sso:ListApplicationGrants + - sso:DeleteApplicationGrant + - sso:PutApplicationAuthenticationMethod + - sso:GetApplicationAuthenticationMethod + - sso:ListApplicationAuthenticationMethods + - sso:DeleteApplicationAuthenticationMethod + - sso:PutApplicationAccessScope + - sso:GetApplicationAccessScope + - sso:ListApplicationAccessScopes + - sso:DeleteApplicationAccessScope + - sso:PutApplicationAssignmentConfiguration + - sso:GetApplicationAssignmentConfiguration + - sso:CreateApplicationAssignment + - sso:DeleteApplicationAssignment + - sso:ListApplicationAssignments + - sso:TagResource + - sso:UntagResource + - sso:ListTagsForResource + Resource: ["*"] + - Effect: Allow + Action: + - identitystore:ListUsers + - identitystore:ListGroups + - identitystore:ListGroupMemberships + - identitystore:DescribeUser + - identitystore:DescribeGroup + - identitystore:DescribeGroupMembership + Resource: ["*"] + - Effect: Allow + Action: + - identitystore:CreateUser + - identitystore:CreateGroup + - identitystore:CreateGroupMembership + - identitystore:UpdateUser + - identitystore:UpdateGroup + - identitystore:DeleteUser + - identitystore:DeleteGroup + - identitystore:DeleteGroupMembership + Resource: ["*"] + + # CodeCommit Git data plane: the lab pages clone, push, and pull against + # the seeded repository using git-remote-codecommit. Creation/deletion + # is in labs2.yaml; this scope adds the per-commit data ops. + - Effect: Allow + Action: + - codecommit:GitPull + - codecommit:GitPush + - codecommit:GetBranch + - codecommit:GetCommit + - codecommit:GetRepository + - codecommit:GetFile + - codecommit:GetFolder + - codecommit:GetReferences + - codecommit:ListBranches + - codecommit:BatchGetCommits + - codecommit:BatchGetRepositories + - codecommit:CreateBranch + - codecommit:CreateCommit + - codecommit:UpdateDefaultBranch + Resource: + - !Sub arn:aws:codecommit:${AWS::Region}:${AWS::AccountId}:${Env}* + + # EKS Capabilities lifecycle. eks:* in base.yaml is tag-scoped to the + # cluster; capability operations work against capability ARNs that don't + # carry the same tag at create-time. Granting them explicitly here. + - Effect: Allow + Action: + - eks:CreateCapability + - eks:DeleteCapability + - eks:DescribeCapability + - eks:UpdateCapability + - eks:ListCapabilities + Resource: ["*"] + + # iam:PassRole for the IAM Capability Roles passed to CreateCapability. + - Effect: Allow + Action: + - iam:PassRole + Resource: + - !Sub arn:aws:iam::${AWS::AccountId}:role/${Env}*-cap-role + Condition: + StringEquals: + iam:PassedToService: capabilities.eks.amazonaws.com diff --git a/lab/scripts/installer.sh b/lab/scripts/installer.sh index f4f00254a..4af294885 100644 --- a/lab/scripts/installer.sh +++ b/lab/scripts/installer.sh @@ -120,6 +120,10 @@ rm -rf flux.tar.gz # git-remote pip install git-remote-s3 +# git-remote-codecommit (used by the EKS capabilities Argo CD fast path to clone +# CodeCommit repos with ambient AWS credentials via the codecommit:: helper) +pip install git-remote-codecommit==1.17 + # terraform download "https://releases.hashicorp.com/terraform/${terraform_version}/terraform_${terraform_version}_linux_${arch_name}.zip" "terraform.zip" unzip -o -q terraform.zip -d /tmp diff --git a/manifests/.workshop/terraform/base.tf b/manifests/.workshop/terraform/base.tf index f4b8d3386..d34a66481 100644 --- a/manifests/.workshop/terraform/base.tf +++ b/manifests/.workshop/terraform/base.tf @@ -59,6 +59,13 @@ variable "inbound_cidrs" { default = "0.0.0.0/0" } +# tflint-ignore: terraform_unused_declarations +variable "argocd_admin_email" { + description = "Optional email for the Argo CD workshop admin user (fastpaths/eks-capabilities Lab 2). The OTP-based activation flow ignores it; only set this if using email-link activation. Ignored elsewhere." + type = string + default = "" +} + data "aws_partition" "current" {} data "aws_caller_identity" "current" {} data "aws_region" "current" {} diff --git a/manifests/.workshop/terraform/lab-fastpaths.tf b/manifests/.workshop/terraform/lab-fastpaths.tf index d650b5072..888c012dc 100644 --- a/manifests/.workshop/terraform/lab-fastpaths.tf +++ b/manifests/.workshop/terraform/lab-fastpaths.tf @@ -14,6 +14,7 @@ module "lab" { tags = local.tags resources_precreated = var.resources_precreated inbound_cidrs = var.inbound_cidrs + argocd_admin_email = var.argocd_admin_email } locals { diff --git a/manifests/modules/fastpaths/developers/.workshop/terraform/main.tf b/manifests/modules/fastpaths/developers/.workshop/terraform/main.tf index f7d1c3e1e..c0a566f27 100644 --- a/manifests/modules/fastpaths/developers/.workshop/terraform/main.tf +++ b/manifests/modules/fastpaths/developers/.workshop/terraform/main.tf @@ -11,4 +11,10 @@ module "preprovision" { eks_cluster_auto_id = var.eks_cluster_auto_id tags = var.tags inbound_cidrs = var.inbound_cidrs + + # Empty string means "use the preprovision module's default placeholder". + # The preprovision module's default is a non-deliverable placeholder that's + # fine for the OTP activation flow; learners only need to override this if + # they prefer the email-link activation flow. + argocd_admin_email = var.argocd_admin_email != "" ? var.argocd_admin_email : "argocd-admin@example.com" } diff --git a/manifests/modules/fastpaths/developers/.workshop/terraform/outputs.tf b/manifests/modules/fastpaths/developers/.workshop/terraform/outputs.tf new file mode 100644 index 000000000..89747a03f --- /dev/null +++ b/manifests/modules/fastpaths/developers/.workshop/terraform/outputs.tf @@ -0,0 +1,4 @@ +output "environment_variables" { + description = "Environment variables to be added to the IDE shell" + value = try(module.preprovision[0].environment_variables, {}) +} diff --git a/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-capability.tf b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-capability.tf new file mode 100644 index 000000000..1fbad53c8 --- /dev/null +++ b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-capability.tf @@ -0,0 +1,289 @@ +# Argo CD EKS Capability provisioning ---------------------------------------- +# +# Enables the Argo CD EKS-managed capability on the shared Auto Mode cluster, +# federated with AWS IAM Identity Center for sign-in. Used by the +# `fastpaths/eks-capabilities` Lab 2 (Continuous delivery with Argo CD). +# +# Reference pattern: aws-samples/appmod-blueprints + the AWS docs +# https://docs.aws.amazon.com/eks/latest/userguide/create-argocd-capability.html +# +# Data sources (aws_caller_identity, aws_region, aws_partition, +# aws_eks_cluster.eks_cluster_auto) and the region preflight +# (null_resource.eks_cap_region_preflight) are declared in eks-auto.tf / +# eks-capabilities.tf and reused here. + +# --- IAM Identity Center preflight ------------------------------------------- +# +# Argo CD is the ONLY EKS capability that requires AWS IAM Identity Center — +# it is the sole authentication path (no local users, no admin password). We do +# NOT create an Identity Center instance for the learner: it is an account-wide, +# largely one-per-org resource. Instead we look it up and fail fast with an +# actionable message if it is missing, so learners don't wait minutes for an +# opaque downstream error. +# +# list-instances is a regional API, so this also enforces that Identity Center +# lives in the same region as the workshop cluster (required because we wire the +# instance region straight into the capability configuration below). +data "aws_ssoadmin_instances" "current" {} + +locals { + eks_cap_idc_present = length(data.aws_ssoadmin_instances.current.arns) > 0 + eks_cap_idc_instance_arn = local.eks_cap_idc_present ? tolist(data.aws_ssoadmin_instances.current.arns)[0] : "" + eks_cap_idc_identitystore = local.eks_cap_idc_present ? tolist(data.aws_ssoadmin_instances.current.identity_store_ids)[0] : "" + + eks_cap_argocd_capability_name = "${var.eks_cluster_auto_id}-argocd" + eks_cap_argocd_admin_user = "${var.eks_cluster_auto_id}-argocd-admin" + eks_cap_argocd_admin_group = "${var.eks_cluster_auto_id}-argocd-admins" + eks_cap_codecommit_repo_name = "${var.eks_cluster_auto_id}-catalog-gitops" + eks_cap_codecommit_repo_arn = "arn:${data.aws_partition.current.partition}:codecommit:${data.aws_region.current.id}:${data.aws_caller_identity.current.account_id}:${local.eks_cap_codecommit_repo_name}" + eks_cap_codecommit_repo_url = "https://git-codecommit.${data.aws_region.current.id}.amazonaws.com/v1/repos/${local.eks_cap_codecommit_repo_name}" +} + +resource "null_resource" "eks_cap_argocd_idc_preflight" { + lifecycle { + precondition { + condition = local.eks_cap_idc_present + error_message = "The Argo CD capability requires AWS IAM Identity Center, but no Identity Center instance was found in ${data.aws_region.current.id}. Enable IAM Identity Center in this region (https://console.aws.amazon.com/singlesignon/home) before running this fast path." + } + } +} + +# --- Identity Center user + group + membership ------------------------------ +# +# We create a workshop-scoped admin group and a single user inside the built-in +# Identity Store, then map the GROUP -> Argo CD ADMIN role in the capability +# config below. +# +# Activation flow (matches saas-on-eks-workshop-capabilities): +# 1. Admin disables MFA on the IDC instance once (Console only, no API). +# 2. Admin generates a one-time password for this user via the IDC Console +# (Users -> argo-admin -> Reset password -> "Generate a one-time password"). +# 3. Learner signs in to Argo CD with username + OTP, is forced to set a +# permanent password, then lands in Argo CD as ADMIN. +# +# This is why the email defaults to a non-deliverable placeholder — the OTP +# flow doesn't use it. To use the email-link activation flow instead, set +# TF_VAR_argocd_admin_email to a real address. +# +# Pattern adopted from: +# https://github.com/aws-samples/saas-on-eks-workshop-capabilities/blob/main/assetsSrc/terraform/identity-center.tf +# https://github.com/aws-samples/saas-on-eks-workshop-capabilities/blob/main/content/100-introduction/225-argocd-user-management.en.md +resource "aws_identitystore_user" "argocd_admin" { + identity_store_id = local.eks_cap_idc_identitystore + + user_name = local.eks_cap_argocd_admin_user + display_name = "Argo CD Workshop Admin" + + name { + given_name = "Argo CD" + family_name = "Workshop Admin" + } + + emails { + value = var.argocd_admin_email + primary = true + } + + depends_on = [null_resource.eks_cap_argocd_idc_preflight] +} + +resource "aws_identitystore_group" "argocd_admins" { + identity_store_id = local.eks_cap_idc_identitystore + display_name = local.eks_cap_argocd_admin_group + description = "Argo CD administrators for ${var.eks_cluster_auto_id} (EKS Workshop fast path)" + + depends_on = [null_resource.eks_cap_argocd_idc_preflight] +} + +resource "aws_identitystore_group_membership" "argocd_admin" { + identity_store_id = local.eks_cap_idc_identitystore + group_id = aws_identitystore_group.argocd_admins.group_id + member_id = aws_identitystore_user.argocd_admin.user_id +} + +# --- CodeCommit repository, seeded with the catalog manifests ---------------- +# +# Pre-provisioned and seeded so the lab can focus on the managed Argo CD +# capability rather than Git plumbing. The repo holds a complete, self-contained +# copy of the catalog stack (deployment, service, mysql, config) so Argo CD can +# deploy a healthy catalog from scratch. +resource "aws_codecommit_repository" "catalog_gitops" { + repository_name = local.eks_cap_codecommit_repo_name + description = "Catalog GitOps source for the EKS Workshop Argo CD capability fast path (${var.eks_cluster_auto_id})" + + tags = var.tags +} + +# Seed the repo with the catalog manifests via a single CodeCommit commit. +# Idempotent: re-running prepare-environment (which destroys + re-applies this +# module) recreates the repo, so we always seed on create. The trigger also +# re-seeds if the local seed manifests change. +resource "null_resource" "eks_cap_argocd_repo_seed" { + triggers = { + repository = aws_codecommit_repository.catalog_gitops.repository_name + region = data.aws_region.current.id + content_hash = sha1(join(",", [for f in fileset("${path.module}/argocd-seed/catalog", "**") : filesha1("${path.module}/argocd-seed/catalog/${f}")])) + } + + provisioner "local-exec" { + interpreter = ["/bin/bash", "-c"] + command = "${path.module}/argocd-seed/seed-repo.sh" + + environment = { + REPO_NAME = aws_codecommit_repository.catalog_gitops.repository_name + AWS_REGION = data.aws_region.current.id + SEED_DIR = "${path.module}/argocd-seed/catalog" + } + } + + depends_on = [aws_codecommit_repository.catalog_gitops] +} + +# --- IAM Capability Role for Argo CD ----------------------------------------- +# +# Assumed by the EKS capabilities service principal. The managed Argo CD +# (running in AWS-owned infrastructure) uses this role to pull the catalog +# manifests from CodeCommit. Scoped to GitPull on the single seeded repo — +# no account-wide managed policy. +resource "aws_iam_role" "eks_cap_argocd_capability" { + name = "${var.eks_cluster_auto_id}-argocd-cap-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "capabilities.eks.amazonaws.com" + } + Action = [ + "sts:AssumeRole", + "sts:TagSession", + ] + } + ] + }) + + tags = var.tags +} + +resource "aws_iam_role_policy" "eks_cap_argocd_codecommit" { + name = "argocd-capability-codecommit" + role = aws_iam_role.eks_cap_argocd_capability.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "PullCatalogGitOpsRepo" + Effect = "Allow" + Action = [ + "codecommit:GitPull", + "codecommit:GetRepository", + "codecommit:GetBranch", + "codecommit:GetFolder", + "codecommit:GetFile", + ] + Resource = local.eks_cap_codecommit_repo_arn + } + ] + }) +} + +# Wait for IAM eventual consistency before EKS validates the role's trust +# policy. Mirrors the ACK capability pattern in eks-capabilities.tf — a freshly +# created role frequently fails CreateCapability with an invalid-trust-policy +# error without this gap, and reset-environment recreates the role every run. +resource "time_sleep" "eks_cap_argocd_role_propagation" { + depends_on = [ + aws_iam_role.eks_cap_argocd_capability, + aws_iam_role_policy.eks_cap_argocd_codecommit, + ] + + create_duration = "30s" +} + +# Activate the Argo CD capability, federated with IAM Identity Center. +# +# The AWS Terraform provider models the capability configuration as native HCL +# nested blocks (NOT jsonencode). IAM Identity Center is required — the +# `aws_idc` block and a role mapping are mandatory for a usable capability. +resource "aws_eks_capability" "argocd" { + cluster_name = var.eks_cluster_auto_id + capability_name = local.eks_cap_argocd_capability_name + type = "ARGOCD" + role_arn = aws_iam_role.eks_cap_argocd_capability.arn + delete_propagation_policy = "RETAIN" + + configuration { + argo_cd { + namespace = "argocd" + + aws_idc { + idc_instance_arn = local.eks_cap_idc_instance_arn + idc_region = data.aws_region.current.id + } + + rbac_role_mapping { + role = "ADMIN" + + identity { + id = aws_identitystore_group.argocd_admins.group_id + type = "SSO_GROUP" + } + } + } + } + + tags = var.tags + + depends_on = [ + aws_iam_role_policy.eks_cap_argocd_codecommit, + aws_identitystore_group_membership.argocd_admin, + null_resource.eks_cap_region_preflight, + null_resource.eks_cap_argocd_idc_preflight, + time_sleep.eks_cap_argocd_role_propagation, + ] +} + +# Grant the capability's IAM role permission to deploy into THIS cluster. +# +# Unlike the ACK capability, the Argo CD capability AUTOMATICALLY creates the +# EKS access entry for its Capability Role during creation, and AWS auto-attaches +# AmazonEKSArgoCDPolicy (namespace-scoped to argocd) and AmazonEKSArgoCDClusterPolicy +# (cluster-wide). Those auto-attached policies are sufficient for the capability +# to bootstrap itself (create argocd namespace, install CRDs, etc.). +# +# We additionally bind AmazonEKSClusterAdminPolicy so the capability's controllers +# can sync user Applications to the local "in-cluster" deployment target the +# learner registers in Lab 2. +# +# IMPORTANT: do NOT make this depend on aws_eks_capability.argocd reaching ACTIVE. +# That creates a deadlock: terraform waits for the capability before attaching the +# admin policy, but the capability sits in CREATING with health +# `AccessDenied: Unauthorized` for the full 20-min timeout because its controllers +# can't write user Applications without the admin policy. By letting this resource +# apply in parallel with the capability create, the admin policy lands shortly +# after the auto-created access entry appears, and the capability completes its +# next health check successfully. +# +# We also DON'T create an aws_eks_access_entry here — the capability auto-creates +# one and our explicit declaration would collide (ResourceInUseException). +resource "aws_eks_access_policy_association" "argocd" { + cluster_name = var.eks_cluster_auto_id + policy_arn = "arn:${data.aws_partition.current.partition}:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy" + principal_arn = aws_iam_role.eks_cap_argocd_capability.arn + + access_scope { + type = "cluster" + } + + # Depend only on the IAM role + 30s propagation, NOT on the capability. + # The capability's auto-created access entry is what this association binds + # to; that entry exists from the moment the capability starts CREATING, so + # we don't need to wait for it to reach ACTIVE. + depends_on = [ + aws_iam_role.eks_cap_argocd_capability, + time_sleep.eks_cap_argocd_role_propagation, + ] +} diff --git a/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/configMap.yaml b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/configMap.yaml new file mode 100644 index 000000000..8aaa6ddb9 --- /dev/null +++ b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/configMap.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: catalog + namespace: catalog +data: + RETAIL_CATALOG_PERSISTENCE_PROVIDER: mysql + RETAIL_CATALOG_PERSISTENCE_ENDPOINT: catalog-mysql:3306 + RETAIL_CATALOG_PERSISTENCE_DB_NAME: catalog diff --git a/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/deployment.yaml b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/deployment.yaml new file mode 100644 index 000000000..1ca26e63b --- /dev/null +++ b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/deployment.yaml @@ -0,0 +1,75 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: catalog + namespace: catalog + labels: + app.kubernetes.io/created-by: eks-workshop + app.kubernetes.io/type: app +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: catalog + app.kubernetes.io/instance: catalog + app.kubernetes.io/component: service + template: + metadata: + annotations: + prometheus.io/path: /metrics + prometheus.io/port: "8080" + prometheus.io/scrape: "true" + labels: + app.kubernetes.io/name: catalog + app.kubernetes.io/instance: catalog + app.kubernetes.io/component: service + app.kubernetes.io/created-by: eks-workshop + spec: + serviceAccountName: catalog + securityContext: + fsGroup: 1000 + containers: + - name: catalog + envFrom: + - configMapRef: + name: catalog + - secretRef: + name: catalog-db + securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + image: "public.ecr.aws/aws-containers/retail-store-sample-catalog:1.2.1" + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + protocol: TCP + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 3 + readinessProbe: + httpGet: + path: /health + port: 8080 + successThreshold: 3 + periodSeconds: 5 + resources: + limits: + memory: 512Mi + requests: + cpu: 250m + memory: 512Mi + volumeMounts: + - mountPath: /tmp + name: tmp-volume + volumes: + - name: tmp-volume + emptyDir: + medium: Memory diff --git a/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/namespace.yaml b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/namespace.yaml new file mode 100644 index 000000000..34688bac0 --- /dev/null +++ b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/namespace.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: catalog + labels: + app.kubernetes.io/created-by: eks-workshop + annotations: + argocd.argoproj.io/sync-wave: "-1" diff --git a/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/secrets.yaml b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/secrets.yaml new file mode 100644 index 000000000..a5eb4b07b --- /dev/null +++ b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/secrets.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: catalog-db + namespace: catalog +data: + RETAIL_CATALOG_PERSISTENCE_USER: "Y2F0YWxvZw==" + RETAIL_CATALOG_PERSISTENCE_PASSWORD: "ZFltTmZXVjR1RXZUem9GdQ==" diff --git a/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/service-mysql.yaml b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/service-mysql.yaml new file mode 100644 index 000000000..afbd369a4 --- /dev/null +++ b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/service-mysql.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: catalog-mysql + namespace: catalog + labels: + app.kubernetes.io/created-by: eks-workshop +spec: + type: ClusterIP + ports: + - port: 3306 + targetPort: mysql + protocol: TCP + name: mysql + selector: + app.kubernetes.io/name: catalog + app.kubernetes.io/instance: catalog + app.kubernetes.io/component: mysql diff --git a/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/service.yaml b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/service.yaml new file mode 100644 index 000000000..18ee4e793 --- /dev/null +++ b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: catalog + namespace: catalog + labels: + app.kubernetes.io/created-by: eks-workshop +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: catalog + app.kubernetes.io/instance: catalog + app.kubernetes.io/component: service diff --git a/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/serviceAccount.yaml b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/serviceAccount.yaml new file mode 100644 index 000000000..4c6d66eee --- /dev/null +++ b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/serviceAccount.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: catalog + namespace: catalog diff --git a/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/statefulset-mysql.yaml b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/statefulset-mysql.yaml new file mode 100644 index 000000000..3bae481bf --- /dev/null +++ b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog/statefulset-mysql.yaml @@ -0,0 +1,54 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: catalog-mysql + namespace: catalog + labels: + app.kubernetes.io/created-by: eks-workshop + app.kubernetes.io/team: database +spec: + replicas: 1 + serviceName: catalog-mysql + selector: + matchLabels: + app.kubernetes.io/name: catalog + app.kubernetes.io/instance: catalog + app.kubernetes.io/component: mysql + template: + metadata: + labels: + app.kubernetes.io/name: catalog + app.kubernetes.io/instance: catalog + app.kubernetes.io/component: mysql + app.kubernetes.io/created-by: eks-workshop + app.kubernetes.io/team: database + spec: + containers: + - name: mysql + image: "public.ecr.aws/docker/library/mysql:8.0" + imagePullPolicy: IfNotPresent + env: + - name: MYSQL_ROOT_PASSWORD + value: my-secret-pw + - name: MYSQL_DATABASE + value: catalog + - name: MYSQL_USER + valueFrom: + secretKeyRef: + name: catalog-db + key: RETAIL_CATALOG_PERSISTENCE_USER + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: catalog-db + key: RETAIL_CATALOG_PERSISTENCE_PASSWORD + volumeMounts: + - name: data + mountPath: /var/lib/mysql + ports: + - name: mysql + containerPort: 3306 + protocol: TCP + volumes: + - name: data + emptyDir: {} diff --git a/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/seed-repo.sh b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/seed-repo.sh new file mode 100755 index 000000000..196f0b7d2 --- /dev/null +++ b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/seed-repo.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +# Seed the catalog GitOps CodeCommit repository with the catalog manifests. +# +# Invoked by null_resource.eks_cap_argocd_repo_seed in argocd-capability.tf. +# Expects REPO_NAME, AWS_REGION, and SEED_DIR in the environment. Uses +# git-remote-codecommit (codecommit:: remote helper) so it authenticates with +# the ambient AWS credentials — no SSH keys, no Git credential helper. +# +# Idempotent: clones the (possibly empty) repo, replaces the catalog/ tree with +# the seed manifests, and pushes only when the content differs. Re-running is a +# no-op when the repo already matches the seed. + +set -Eeuo pipefail + +: "${REPO_NAME:?REPO_NAME is required}" +: "${AWS_REGION:?AWS_REGION is required}" +: "${SEED_DIR:?SEED_DIR is required}" + +if ! command -v git-remote-codecommit >/dev/null 2>&1 && ! python3 -c "import git_remote_codecommit" >/dev/null 2>&1; then + echo "git-remote-codecommit is not installed; installing into a virtualenv..." >&2 + GRC_VENV="$(mktemp -d)/grc" + python3 -m venv "$GRC_VENV" + # shellcheck disable=SC1091 + source "$GRC_VENV/bin/activate" + pip install --quiet git-remote-codecommit +fi + +REMOTE="codecommit::${AWS_REGION}://${REPO_NAME}" +WORKDIR="$(mktemp -d)" +trap 'rm -rf "$WORKDIR"' EXIT + +git clone --quiet "$REMOTE" "$WORKDIR" 2>/dev/null || { + # Empty repository: git clone warns and returns non-zero. Initialize instead. + git -C "$WORKDIR" init --quiet + git -C "$WORKDIR" remote add origin "$REMOTE" +} + +git -C "$WORKDIR" config user.email "eks-workshop@amazon.com" +git -C "$WORKDIR" config user.name "EKS Workshop" + +# Replace the catalog tree wholesale so the seed is the source of truth. +rm -rf "${WORKDIR:?}/catalog" +mkdir -p "$WORKDIR/catalog" +cp -R "$SEED_DIR/." "$WORKDIR/catalog/" + +git -C "$WORKDIR" add -A + +if git -C "$WORKDIR" diff --cached --quiet 2>/dev/null; then + echo "CodeCommit repo ${REPO_NAME} already up to date; nothing to seed." + exit 0 +fi + +git -C "$WORKDIR" commit --quiet -m "Seed catalog manifests for Argo CD capability fast path" + +# Ensure we push to a branch named main regardless of the local default. +CURRENT_BRANCH="$(git -C "$WORKDIR" rev-parse --abbrev-ref HEAD)" +if [[ "$CURRENT_BRANCH" != "main" ]]; then + git -C "$WORKDIR" branch -M main +fi + +git -C "$WORKDIR" push --quiet origin main +echo "Seeded CodeCommit repo ${REPO_NAME} (branch main) with catalog manifests." diff --git a/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/eks-capabilities.tf b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/eks-capabilities.tf new file mode 100644 index 000000000..bb565daba --- /dev/null +++ b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/eks-capabilities.tf @@ -0,0 +1,217 @@ +# EKS Capabilities provisioning ----------------------------------------------- +# +# Enables the ACK EKS-managed capability on the shared Auto Mode cluster. +# Used by the `fastpaths/eks-capabilities` lab. Provisioned alongside the +# other fastpaths preprovision resources because the cluster is shared. +# +# Reference pattern: aws-samples/appmod-blueprints +# platform/infra/terraform/cluster/main.tf +# +# We rely on data sources already declared in eks-auto.tf +# (aws_caller_identity, aws_region, aws_partition). + +# --- Region preflight -------------------------------------------------------- +# +# EKS Capabilities are not available in AWS GovCloud or China regions per the +# GA announcement (Nov 2025). Fail fast with a clear message so learners don't +# wait several minutes for a downstream API error. +locals { + eks_cap_unsupported_region_prefixes = ["us-gov-", "cn-"] + eks_cap_region_supported = !anytrue([ + for prefix in local.eks_cap_unsupported_region_prefixes : + startswith(data.aws_region.current.id, prefix) + ]) +} + +resource "null_resource" "eks_cap_region_preflight" { + lifecycle { + precondition { + condition = local.eks_cap_region_supported + error_message = "EKS Capabilities are not available in ${data.aws_region.current.id}. Run this fast path from a commercial AWS region (not GovCloud or China)." + } + } +} + +locals { + eks_cap_ack_capability_name = "${var.eks_cluster_auto_id}-ack" + eks_cap_carts_table_name = "${var.eks_cluster_auto_id}-carts-fastpath" + eks_cap_carts_table_arn = "arn:${data.aws_partition.current.partition}:dynamodb:${data.aws_region.current.id}:${data.aws_caller_identity.current.account_id}:table/${local.eks_cap_carts_table_name}" + + # Lab 3 (kro) provisions a second carts-* table via an RGD instance. Use a + # wildcard ARN so the same carts Pod Identity policy and the same ACK + # capability role cover both Lab 1's `${cluster}-carts-fastpath` and any + # `${cluster}-carts-*` table a learner names in their CartsStack instance. + eks_cap_carts_tables_wildcard_arn = "arn:${data.aws_partition.current.partition}:dynamodb:${data.aws_region.current.id}:${data.aws_caller_identity.current.account_id}:table/${var.eks_cluster_auto_id}-carts-*" + eks_cap_kro_table_name = "${var.eks_cluster_auto_id}-carts-kro" +} + +# --- IAM Capability Role for ACK -------------------------------------------- +# +# Assumed by the EKS capabilities service principal. The ACK controllers +# (running in AWS-managed infra outside the cluster) use this role to call +# the AWS APIs needed to reconcile the Table custom resource. + +resource "aws_iam_role" "eks_cap_ack_capability" { + name = "${var.eks_cluster_auto_id}-ack-cap-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "capabilities.eks.amazonaws.com" + } + Action = [ + "sts:AssumeRole", + "sts:TagSession", + ] + } + ] + }) + + tags = var.tags +} + +resource "aws_iam_role_policy" "eks_cap_ack_capability_dynamodb" { + name = "ack-capability-dynamodb" + role = aws_iam_role.eks_cap_ack_capability.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "ManageCartsFastpathTables" + Effect = "Allow" + Action = [ + "dynamodb:CreateTable", + "dynamodb:DescribeTable", + "dynamodb:UpdateTable", + "dynamodb:DeleteTable", + "dynamodb:UpdateContinuousBackups", + "dynamodb:DescribeContinuousBackups", + "dynamodb:DescribeTimeToLive", + "dynamodb:UpdateTimeToLive", + "dynamodb:DescribeContributorInsights", + "dynamodb:UpdateContributorInsights", + "dynamodb:DescribeKinesisStreamingDestination", + "dynamodb:EnableKinesisStreamingDestination", + "dynamodb:DisableKinesisStreamingDestination", + "dynamodb:GetResourcePolicy", + "dynamodb:PutResourcePolicy", + "dynamodb:DeleteResourcePolicy", + "dynamodb:TagResource", + "dynamodb:UntagResource", + "dynamodb:ListTagsOfResource", + ] + Resource = [ + local.eks_cap_carts_tables_wildcard_arn, + "${local.eks_cap_carts_tables_wildcard_arn}/index/*", + ] + } + ] + }) +} + +# Wait for IAM eventual consistency before EKS validates the role's trust +# policy. Without this gap, CreateCapability frequently fails with +# `InvalidParameterException: The trust policy for the provided role is +# invalid` on a freshly-created role, even though the policy is correct. +# `reset-environment` runs `terraform destroy` then `apply`, so every +# preprovision run creates a brand-new role and re-encounters this race. +resource "time_sleep" "eks_cap_ack_capability_role_propagation" { + depends_on = [ + aws_iam_role.eks_cap_ack_capability, + aws_iam_role_policy.eks_cap_ack_capability_dynamodb, + ] + + create_duration = "30s" +} + +# Activate the ACK capability via the AWS provider's native resource. +resource "aws_eks_capability" "ack" { + cluster_name = var.eks_cluster_auto_id + capability_name = local.eks_cap_ack_capability_name + type = "ACK" + role_arn = aws_iam_role.eks_cap_ack_capability.arn + delete_propagation_policy = "RETAIN" + + tags = var.tags + + depends_on = [ + aws_iam_role_policy.eks_cap_ack_capability_dynamodb, + null_resource.eks_cap_region_preflight, + time_sleep.eks_cap_ack_capability_role_propagation, + time_sleep.eks_cap_ack_access_propagation, + ] +} + +# Bind the capability's IAM role to the cluster admin access policy so its +# controllers can reconcile inside the cluster (create CRDs, watch resources, +# etc.). Without this association the capability shows ACTIVE but its +# controllers cannot talk to the Kubernetes API. +# +# An aws_eks_access_entry is required before an aws_eks_access_policy_association +# can attach a policy to a principal — the entry establishes the principal's +# identity on the cluster, the association attaches policies to it. +# +# Both are created BEFORE aws_eks_capability.ack so the capability's controllers +# already have cluster API access the first time they reconcile. Without this +# ordering, the capability sits in CREATING with health +# `AccessDenied: Unauthorized`. +resource "aws_eks_access_entry" "ack" { + cluster_name = var.eks_cluster_auto_id + principal_arn = aws_iam_role.eks_cap_ack_capability.arn + type = "STANDARD" +} + +resource "aws_eks_access_policy_association" "ack" { + cluster_name = var.eks_cluster_auto_id + policy_arn = "arn:${data.aws_partition.current.partition}:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy" + principal_arn = aws_iam_role.eks_cap_ack_capability.arn + + access_scope { + type = "cluster" + } + + depends_on = [aws_eks_access_entry.ack] +} + +# Give the access entry + policy association time to propagate inside the +# cluster before the capability creates and its controllers try to authenticate. +# Without this gap, the capability frequently sits in CREATING with health +resource "time_sleep" "eks_cap_ack_access_propagation" { + depends_on = [aws_eks_access_policy_association.ack] + create_duration = "60s" +} + +# --- Extend the existing carts Pod Identity role ----------------------------- +# +# The fastpaths preprovision already creates a carts role + Pod Identity +# association in pod-identity.tf, scoped to the `${cluster}-carts` table. +# Add an inline policy granting access to the new `-carts-fastpath` table +# so the same carts ServiceAccount can read/write it after Lab 1's ConfigMap +# flip — no new ServiceAccount, no SA annotation patching needed. +resource "aws_iam_role_policy" "eks_cap_carts_fastpath_dynamodb" { + name = "carts-fastpath-dynamodb" + role = module.iam_assumable_role_carts.iam_role_name + + # Wildcard `${cluster}-carts-*` so the same carts ServiceAccount role + # covers both Lab 1's `-carts-fastpath` table and Lab 3's `-carts-kro` + # table created by the kro RGD instance. No per-instance policy edits + # needed when the learner names a new CartsStack. + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllAPIActionsOnCartFastpathTables" + Effect = "Allow" + Action = "dynamodb:*" + Resource = [ + local.eks_cap_carts_tables_wildcard_arn, + "${local.eks_cap_carts_tables_wildcard_arn}/index/*", + ] + } + ] + }) +} diff --git a/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/fluent-bit.tf b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/fluent-bit.tf index ac064fa9d..a8c1e8e68 100644 --- a/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/fluent-bit.tf +++ b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/fluent-bit.tf @@ -123,10 +123,3 @@ resource "helm_release" "aws_for_fluent_bit" { aws_eks_pod_identity_association.fluentbit ] } - -output "environment_variables" { - description = "Environment variables to be added to the IDE shell" - value = { - CLOUDWATCH_LOG_GROUP_NAME = aws_cloudwatch_log_group.fluentbit.name - } -} diff --git a/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/kro-capability.tf b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/kro-capability.tf new file mode 100644 index 000000000..9e46873ef --- /dev/null +++ b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/kro-capability.tf @@ -0,0 +1,134 @@ +# kro EKS Capability provisioning -------------------------------------------- +# +# Enables the kro EKS-managed capability on the shared Auto Mode cluster. +# Used by the `fastpaths/eks-capabilities` Lab 3 (Compose stacks with kro). +# +# kro is the EKS-managed form of the kro-run/kro project — a Kubernetes +# resource orchestrator that lets users define a single "schema" CR (e.g. +# `CartsStack`) which kro expands into a graph of underlying resources +# (Namespace, ACK Table, ConfigMap, ServiceAccount, ...). +# +# The capability runs in AWS-owned infrastructure outside the cluster. What +# you see inside the cluster are only the CRDs the capability registered +# (`resourcegraphdefinitions.kro.run` plus dynamically generated CRDs for +# each RGD a learner applies). +# +# Reference docs: +# https://docs.aws.amazon.com/eks/latest/userguide/kro.html +# https://docs.aws.amazon.com/eks/latest/userguide/create-kro-capability.html +# https://github.com/kro-run/kro +# +# Data sources (aws_caller_identity, aws_region, aws_partition, +# aws_eks_cluster.eks_cluster_auto) and the region preflight +# (null_resource.eks_cap_region_preflight) are declared in eks-auto.tf / +# eks-capabilities.tf and reused here. + +locals { + eks_cap_kro_capability_name = "${var.eks_cluster_auto_id}-kro" +} + +# --- IAM Capability Role for kro -------------------------------------------- +# +# kro itself does NOT call AWS APIs (per AWS docs: +# https://docs.aws.amazon.com/eks/latest/userguide/kro-permissions.html). It +# only reconciles Kubernetes resources. So the capability role's only job is +# being trusted by the EKS capabilities service principal — no AWS data-plane +# permissions needed. +resource "aws_iam_role" "eks_cap_kro_capability" { + name = "${var.eks_cluster_auto_id}-kro-cap-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "capabilities.eks.amazonaws.com" + } + Action = [ + "sts:AssumeRole", + "sts:TagSession", + ] + } + ] + }) + + tags = var.tags +} + +# Wait for IAM eventual consistency before EKS validates the role's trust +# policy. Mirrors the ACK + Argo CD capability patterns — without this gap a +# freshly-created role frequently fails CreateCapability with an +# `InvalidParameterException: invalid trust policy` error, even though the +# policy is correct. `reset-environment` runs `terraform destroy` then +# `apply`, so every preprovision run creates a brand-new role and re-encounters +# this race. +resource "time_sleep" "eks_cap_kro_role_propagation" { + depends_on = [aws_iam_role.eks_cap_kro_capability] + create_duration = "30s" +} + +# Activate the kro capability via the AWS provider's native resource. +# +# Unlike the Argo CD capability, the kro capability does NOT need +# `AmazonEKSClusterAdminPolicy` to reach ACTIVE — the auto-attached +# `AmazonEKSKROPolicy` is sufficient for kro's bootstrap (managing its own +# `resourcegraphdefinitions.kro.run` CRD). So the capability create has no +# dependency on the supplemental policy below — no deadlock risk, and no +# need to apply them in parallel. +resource "aws_eks_capability" "kro" { + cluster_name = var.eks_cluster_auto_id + capability_name = local.eks_cap_kro_capability_name + type = "KRO" + role_arn = aws_iam_role.eks_cap_kro_capability.arn + delete_propagation_policy = "RETAIN" + + tags = var.tags + + depends_on = [ + null_resource.eks_cap_region_preflight, + time_sleep.eks_cap_kro_role_propagation, + ] +} + +# Grant the kro capability's IAM role cluster-admin so it can reconcile the +# children that workshop RGDs expand into (Namespaces, ACK Tables, +# ConfigMaps, ...). +# +# The kro capability auto-creates an EKS access entry for its capability +# role during creation (per AWS docs — same as the Argo CD capability). AWS +# auto-attaches `AmazonEKSKROPolicy` to that auto-created entry, which is +# sufficient for kro to manage its own CRDs but NOT to create the children +# those RGDs expand into. +# +# We do NOT declare an aws_eks_access_entry — the capability auto-creates +# one and our explicit declaration would collide with +# `ResourceInUseException`. +# +# Unlike the Argo CD capability (Decision 2), we DO depend on the capability +# resource here. Reason: kro's capability reaches ACTIVE on its own; the +# Argo CD deadlock came from Argo CD needing cluster-admin to bootstrap user +# Applications, which kro doesn't need. Depending on the capability resource +# guarantees the auto-created access entry exists before AssociateAccessPolicy +# runs (avoids `ResourceNotFoundException: principalArn could not be found`). +resource "aws_eks_access_policy_association" "kro" { + cluster_name = var.eks_cluster_auto_id + policy_arn = "arn:${data.aws_partition.current.partition}:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy" + principal_arn = aws_iam_role.eks_cap_kro_capability.arn + + access_scope { + type = "cluster" + } + + depends_on = [ + aws_eks_capability.kro, + ] +} + +# Give the access policy association time to propagate inside the cluster +# before any RGD/instance gets applied by the lab. Same 60s gap as the ACK +# capability. +resource "time_sleep" "eks_cap_kro_access_propagation" { + depends_on = [aws_eks_access_policy_association.kro] + create_duration = "60s" +} diff --git a/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/outputs.tf b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/outputs.tf new file mode 100644 index 000000000..ccc82d773 --- /dev/null +++ b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/outputs.tf @@ -0,0 +1,22 @@ +output "environment_variables" { + description = "Environment variables exported into the IDE shell" + value = { + CLOUDWATCH_LOG_GROUP_NAME = aws_cloudwatch_log_group.fluentbit.name + EKS_CAP_DDB_TABLE = local.eks_cap_carts_table_name + EKS_CAP_ACK_CAPABILITY = aws_eks_capability.ack.capability_name + + # Argo CD capability (Lab 2) + EKS_CAP_ARGOCD_CAPABILITY = aws_eks_capability.argocd.capability_name + EKS_CAP_ARGOCD_URL = try(aws_eks_capability.argocd.configuration[0].argo_cd[0].server_url, "") + EKS_CAP_ARGOCD_ADMIN_GROUP = aws_identitystore_group.argocd_admins.display_name + EKS_CAP_ARGOCD_ADMIN_GROUP_ID = aws_identitystore_group.argocd_admins.group_id + EKS_CAP_ARGOCD_USER = aws_identitystore_user.argocd_admin.user_name + EKS_CAP_CODECOMMIT_REPO = aws_codecommit_repository.catalog_gitops.repository_name + EKS_CAP_CODECOMMIT_URL = local.eks_cap_codecommit_repo_url + EKS_CLUSTER_AUTO_ARN = data.aws_eks_cluster.eks_cluster_auto.arn + + # kro capability (Lab 3) + EKS_CAP_KRO_CAPABILITY = aws_eks_capability.kro.capability_name + EKS_CAP_DDB_TABLE_KRO = local.eks_cap_kro_table_name + } +} diff --git a/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/vars.tf b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/vars.tf index 5a6f1c9d0..b1650f590 100644 --- a/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/vars.tf +++ b/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/vars.tf @@ -23,3 +23,19 @@ variable "inbound_cidrs" { type = string default = "0.0.0.0/0" } + +# tflint-ignore: terraform_unused_declarations +variable "argocd_admin_email" { + description = <<-EOT + Email address attached to the Argo CD workshop admin user record. + Defaults to a non-deliverable placeholder because this fast path uses the + admin-generated OTP activation path (see setup-idc.md), not the + email-link path — so the email value is cosmetic and never needs to + receive mail. Override with a real address only if you specifically want + to use the email-link activation flow. + + Pattern adopted from https://github.com/aws-samples/saas-on-eks-workshop-capabilities. + EOT + type = string + default = "argocd-admin@example.com" +} diff --git a/manifests/modules/fastpaths/developers/.workshop/terraform/vars.tf b/manifests/modules/fastpaths/developers/.workshop/terraform/vars.tf index 28e5a0b00..2453354d9 100644 --- a/manifests/modules/fastpaths/developers/.workshop/terraform/vars.tf +++ b/manifests/modules/fastpaths/developers/.workshop/terraform/vars.tf @@ -46,3 +46,10 @@ variable "inbound_cidrs" { description = "CIDR range to allowlist for inbound traffic" type = string } + +# tflint-ignore: terraform_unused_declarations +variable "argocd_admin_email" { + description = "Email attached to the Argo CD workshop admin user (Lab 2). Optional — the OTP-based activation flow ignores it. See preprovision/vars.tf." + type = string + default = "" +} diff --git a/manifests/modules/fastpaths/eks-capabilities/.workshop/cleanup.sh b/manifests/modules/fastpaths/eks-capabilities/.workshop/cleanup.sh new file mode 100644 index 000000000..d9845bbe9 --- /dev/null +++ b/manifests/modules/fastpaths/eks-capabilities/.workshop/cleanup.sh @@ -0,0 +1,140 @@ +#!/bin/bash + +set -Eeuo pipefail + +# The EKS capability, IAM Capability Role, and DynamoDB IAM policies are +# tracked by the shared fastpaths preprovision Terraform and are torn down +# only when the entire fastpaths environment is destroyed. This script +# cleans up the per-lab resources the *learner* applied during the labs so +# the path can be entered/exited cleanly between sessions. + +# --- Lab 1 (ACK) ------------------------------------------------------------- + +logmessage "Deleting ACK Table custom resources..." +delete-all-if-crd-exists tables.dynamodb.services.k8s.aws + +logmessage "Removing carts Pod Identity association..." +for assoc in $(aws eks list-pod-identity-associations \ + --cluster-name "${EKS_CLUSTER_AUTO_NAME:-eks-workshop-auto}" \ + --namespace carts --service-account carts \ + --query 'associations[].associationId' --output text 2>/dev/null); do + aws eks delete-pod-identity-association \ + --cluster-name "${EKS_CLUSTER_AUTO_NAME:-eks-workshop-auto}" \ + --association-id "$assoc" >/dev/null 2>&1 || true +done + +logmessage "Restoring base-application carts ConfigMap..." +kubectl apply -k ~/environment/eks-workshop/base-application/carts >/dev/null 2>&1 || true + +# --- Lab 2 (Argo CD) --------------------------------------------------------- + +# Remove the catalog Application first so Argo CD stops reconciling, then let it +# prune the resources it owns (cascade=foreground waits for that to finish). +if kubectl get crd applications.argoproj.io >/dev/null 2>&1; then + if kubectl get application catalog -n argocd >/dev/null 2>&1; then + logmessage "Deleting Argo CD catalog Application..." + kubectl delete application catalog -n argocd \ + --cascade=foreground --timeout=180s >/dev/null 2>&1 || true + fi + + # Remove the cluster registration Secret the learner created. + if kubectl get secret in-cluster -n argocd >/dev/null 2>&1; then + logmessage "Removing Argo CD cluster registration..." + kubectl delete secret in-cluster -n argocd >/dev/null 2>&1 || true + fi +fi + +# The Application's prune may have removed the catalog namespace. Restore the +# base-application catalog so subsequent labs see a healthy retail store. +logmessage "Restoring base-application catalog..." +kubectl apply -k ~/environment/eks-workshop/base-application/catalog >/dev/null 2>&1 || true + +# Remove the cloned GitOps working copy from the IDE home, if present. +rm -rf ~/environment/catalog-gitops >/dev/null 2>&1 || true + +# Reset the CodeCommit repo back to the seeded state so the GitOps update step +# in Lab 2 always has the same starting tag (1.2.1) — without this, a re-run of +# `prepare-environment` finds a learner-pushed `1.2.2` commit still on main and +# the lab's `sed 1.2.1 → 1.2.2` becomes a no-op. +# +# This cleanup hook runs BEFORE the workshop-env.bash file is regenerated, so +# EKS_CAP_CODECOMMIT_REPO is not yet exported. The repo name is deterministic +# per the terraform: `${cluster_auto}-catalog-gitops`. Derive it. +CODECOMMIT_REPO="${EKS_CAP_CODECOMMIT_REPO:-${EKS_CLUSTER_AUTO_NAME:-eks-workshop-auto}-catalog-gitops}" + +if aws codecommit get-repository --repository-name "$CODECOMMIT_REPO" >/dev/null 2>&1; then + logmessage "Resetting CodeCommit repo ${CODECOMMIT_REPO} to seeded state..." + SEED_DIR="/eks-workshop/manifests/modules/fastpaths/developers/.workshop/terraform/preprovision/argocd-seed/catalog" + if [ -d "$SEED_DIR" ]; then + WORKDIR="$(mktemp -d)" + REMOTE="codecommit::${AWS_REGION}://${CODECOMMIT_REPO}" + if git clone --quiet "$REMOTE" "$WORKDIR" 2>/dev/null; then + git -C "$WORKDIR" config user.email "eks-workshop@amazon.com" + git -C "$WORKDIR" config user.name "EKS Workshop" + rm -rf "${WORKDIR:?}/catalog" + mkdir -p "$WORKDIR/catalog" + cp -R "$SEED_DIR/." "$WORKDIR/catalog/" + git -C "$WORKDIR" add -A + if ! git -C "$WORKDIR" diff --cached --quiet 2>/dev/null; then + git -C "$WORKDIR" commit --quiet -m "Reset catalog manifests to seed state" || true + git -C "$WORKDIR" push --quiet origin main >/dev/null 2>&1 || true + fi + fi + rm -rf "$WORKDIR" + fi +fi + +# After the repo reset, force Argo CD to re-reconcile so the catalog Application +# applies the freshly-reset 1.2.1 manifests against the cluster (otherwise the +# Application thinks it's already Synced because it reconciled to the previous +# state, and the lab page sees a stale Deployment image). +if kubectl get application catalog -n argocd >/dev/null 2>&1; then + logmessage "Forcing Argo CD catalog Application to re-sync..." + kubectl annotate application catalog -n argocd \ + argocd.argoproj.io/refresh=hard --overwrite >/dev/null 2>&1 || true +fi + +# --- Lab 3 (kro) ------------------------------------------------------------- + +# Order matters: delete the CartsStack instance first so kro prunes its +# children (Namespace, ACK Table, ConfigMap, ServiceAccount), then the RGD, +# then anything kro didn't manage. Without this order the RGD delete +# orphans the instance and the ACK Table never gets deleted. +if kubectl get crd cartsstacks.kro.run >/dev/null 2>&1; then + if kubectl get cartsstack carts-kro -n default >/dev/null 2>&1; then + logmessage "Deleting kro CartsStack instance carts-kro..." + # Generous timeout: kro must prune the carts Deployment (Pod terminates), + # the ACK Table (controller deletes the AWS DynamoDB table), and the + # rest of the namespace's child resources before the instance disappears. + kubectl delete cartsstack carts-kro -n default \ + --cascade=foreground --timeout=480s >/dev/null 2>&1 || true + fi +fi + +if kubectl get crd resourcegraphdefinitions.kro.run >/dev/null 2>&1; then + if kubectl get rgd cartsstack >/dev/null 2>&1; then + logmessage "Deleting kro ResourceGraphDefinition cartsstack..." + kubectl delete rgd cartsstack --timeout=120s >/dev/null 2>&1 || true + fi +fi + +# Belt-and-braces: if the instance pruning didn't drop the namespace (e.g. +# kro was in a degraded state), remove it explicitly. +logmessage "Removing carts-kro namespace if present..." +kubectl delete ns carts-kro --ignore-not-found --timeout=120s >/dev/null 2>&1 || true + +logmessage "Restoring ui Deployment carts endpoint (in case the optional UI demo overrode it)..." +# `kubectl set env -` removes the env var, restoring the Pod's compiled-in +# default (carts.carts:80). Idempotent — succeeds whether the override was +# applied or not. +kubectl -n ui set env deployment/ui RETAIL_UI_ENDPOINTS_CARTS_URL- >/dev/null 2>&1 || true + +logmessage "Removing carts-kro Pod Identity association..." +for assoc in $(aws eks list-pod-identity-associations \ + --cluster-name "${EKS_CLUSTER_AUTO_NAME:-eks-workshop-auto}" \ + --namespace carts-kro --service-account carts \ + --query 'associations[].associationId' --output text 2>/dev/null); do + aws eks delete-pod-identity-association \ + --cluster-name "${EKS_CLUSTER_AUTO_NAME:-eks-workshop-auto}" \ + --association-id "$assoc" >/dev/null 2>&1 || true +done diff --git a/manifests/modules/fastpaths/eks-capabilities/ack/carts/config.properties b/manifests/modules/fastpaths/eks-capabilities/ack/carts/config.properties new file mode 100644 index 000000000..b3e13a121 --- /dev/null +++ b/manifests/modules/fastpaths/eks-capabilities/ack/carts/config.properties @@ -0,0 +1,2 @@ +RETAIL_CART_PERSISTENCE_PROVIDER=dynamodb +RETAIL_CART_PERSISTENCE_DYNAMODB_TABLE_NAME=${EKS_CLUSTER_AUTO_NAME}-carts-fastpath diff --git a/manifests/modules/fastpaths/eks-capabilities/ack/carts/kustomization.yaml b/manifests/modules/fastpaths/eks-capabilities/ack/carts/kustomization.yaml new file mode 100644 index 000000000..3f604363e --- /dev/null +++ b/manifests/modules/fastpaths/eks-capabilities/ack/carts/kustomization.yaml @@ -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 diff --git a/manifests/modules/fastpaths/eks-capabilities/ack/dynamodb/kustomization.yaml b/manifests/modules/fastpaths/eks-capabilities/ack/dynamodb/kustomization.yaml new file mode 100644 index 000000000..a4fa7ac60 --- /dev/null +++ b/manifests/modules/fastpaths/eks-capabilities/ack/dynamodb/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - table.yaml diff --git a/manifests/modules/fastpaths/eks-capabilities/ack/dynamodb/table.yaml b/manifests/modules/fastpaths/eks-capabilities/ack/dynamodb/table.yaml new file mode 100644 index 000000000..47bf7c989 --- /dev/null +++ b/manifests/modules/fastpaths/eks-capabilities/ack/dynamodb/table.yaml @@ -0,0 +1,25 @@ +apiVersion: dynamodb.services.k8s.aws/v1alpha1 +kind: Table +metadata: + name: items + namespace: carts +spec: + tableName: ${EKS_CLUSTER_AUTO_NAME}-carts-fastpath + billingMode: PAY_PER_REQUEST + keySchema: + - attributeName: id + keyType: HASH + attributeDefinitions: + - attributeName: id + attributeType: "S" + - attributeName: customerId + attributeType: "S" + globalSecondaryIndexes: + - indexName: idx_global_customerId + keySchema: + - attributeName: customerId + keyType: HASH + - attributeName: id + keyType: RANGE + projection: + projectionType: "ALL" diff --git a/manifests/modules/fastpaths/eks-capabilities/argocd/application.yaml b/manifests/modules/fastpaths/eks-capabilities/argocd/application.yaml new file mode 100644 index 000000000..3466a24c3 --- /dev/null +++ b/manifests/modules/fastpaths/eks-capabilities/argocd/application.yaml @@ -0,0 +1,31 @@ +# Argo CD Application that delivers the catalog service via GitOps. +# +# The source is the pre-provisioned CodeCommit repository, referenced DIRECTLY +# by its HTTPS URL — the managed Argo CD capability authenticates to CodeCommit +# using its IAM Capability Role (codecommit:GitPull), so there is no repository +# Secret, no SSH key, and no Git credential helper. +# +# Destination uses the registered `in-cluster` target. Automated sync with +# prune + selfHeal turns every push to the repo into a reconciled rollout. +# +# Piped through envsubst before applying so $EKS_CAP_CODECOMMIT_URL is resolved. +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: catalog + namespace: argocd +spec: + project: default + source: + repoURL: ${EKS_CAP_CODECOMMIT_URL} + targetRevision: main + path: catalog + destination: + name: in-cluster + namespace: catalog + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/manifests/modules/fastpaths/eks-capabilities/argocd/cluster.yaml b/manifests/modules/fastpaths/eks-capabilities/argocd/cluster.yaml new file mode 100644 index 000000000..dc8eb03fd --- /dev/null +++ b/manifests/modules/fastpaths/eks-capabilities/argocd/cluster.yaml @@ -0,0 +1,19 @@ +# Registers the local EKS cluster as an Argo CD deployment target. +# +# The managed Argo CD capability does NOT register the local cluster +# automatically, and it identifies clusters by EKS cluster ARN (the usual +# https://kubernetes.default.svc is not supported). We name it `in-cluster` +# for parity with common Argo CD examples. +# +# Piped through envsubst before applying so $EKS_CLUSTER_AUTO_ARN is resolved. +apiVersion: v1 +kind: Secret +metadata: + name: in-cluster + namespace: argocd + labels: + argocd.argoproj.io/secret-type: cluster +stringData: + name: in-cluster + server: ${EKS_CLUSTER_AUTO_ARN} + project: default diff --git a/manifests/modules/fastpaths/eks-capabilities/kro/instance/cartsstack-instance.yaml b/manifests/modules/fastpaths/eks-capabilities/kro/instance/cartsstack-instance.yaml new file mode 100644 index 000000000..5078f0a1e --- /dev/null +++ b/manifests/modules/fastpaths/eks-capabilities/kro/instance/cartsstack-instance.yaml @@ -0,0 +1,8 @@ +apiVersion: kro.run/v1alpha1 +kind: CartsStack +metadata: + name: carts-kro + namespace: default +spec: + tableName: ${EKS_CLUSTER_AUTO_NAME}-carts-kro + namespace: carts-kro diff --git a/manifests/modules/fastpaths/eks-capabilities/kro/instance/kustomization.yaml b/manifests/modules/fastpaths/eks-capabilities/kro/instance/kustomization.yaml new file mode 100644 index 000000000..2f03751c1 --- /dev/null +++ b/manifests/modules/fastpaths/eks-capabilities/kro/instance/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - cartsstack-instance.yaml diff --git a/manifests/modules/fastpaths/eks-capabilities/kro/rgd/cartsstack-rgd.yaml b/manifests/modules/fastpaths/eks-capabilities/kro/rgd/cartsstack-rgd.yaml new file mode 100644 index 000000000..25f0dc01a --- /dev/null +++ b/manifests/modules/fastpaths/eks-capabilities/kro/rgd/cartsstack-rgd.yaml @@ -0,0 +1,161 @@ +apiVersion: kro.run/v1alpha1 +kind: ResourceGraphDefinition +metadata: + name: cartsstack +spec: + schema: + apiVersion: v1alpha1 + kind: CartsStack + group: kro.run + spec: + tableName: string | required=true + namespace: string | required=true + image: string | default="public.ecr.aws/aws-containers/retail-store-sample-cart:1.2.1" + replicas: integer | default=1 + status: + tableArn: ${table.status.ackResourceMetadata.arn} + resources: + - id: ns + template: + apiVersion: v1 + kind: Namespace + metadata: + name: ${schema.spec.namespace} + - id: table + template: + apiVersion: dynamodb.services.k8s.aws/v1alpha1 + kind: Table + metadata: + name: items + namespace: ${schema.spec.namespace} + spec: + tableName: ${schema.spec.tableName} + billingMode: PAY_PER_REQUEST + attributeDefinitions: + - attributeName: id + attributeType: "S" + - attributeName: customerId + attributeType: "S" + keySchema: + - attributeName: id + keyType: HASH + globalSecondaryIndexes: + - indexName: idx_global_customerId + keySchema: + - attributeName: customerId + keyType: HASH + - attributeName: id + keyType: RANGE + projection: + projectionType: "ALL" + - id: sa + template: + apiVersion: v1 + kind: ServiceAccount + metadata: + name: carts + namespace: ${schema.spec.namespace} + - id: config + template: + apiVersion: v1 + kind: ConfigMap + metadata: + name: carts + namespace: ${schema.spec.namespace} + data: + RETAIL_CART_PERSISTENCE_PROVIDER: dynamodb + RETAIL_CART_PERSISTENCE_DYNAMODB_TABLE_NAME: ${schema.spec.tableName} + - id: deployment + template: + apiVersion: apps/v1 + kind: Deployment + metadata: + name: carts + namespace: ${schema.spec.namespace} + labels: + app.kubernetes.io/created-by: eks-workshop + app.kubernetes.io/type: app + spec: + replicas: ${schema.spec.replicas} + selector: + matchLabels: + app.kubernetes.io/name: carts + app.kubernetes.io/instance: carts + app.kubernetes.io/component: service + template: + metadata: + labels: + app.kubernetes.io/name: carts + app.kubernetes.io/instance: carts + app.kubernetes.io/component: service + app.kubernetes.io/created-by: eks-workshop + spec: + serviceAccountName: ${sa.metadata.name} + securityContext: + fsGroup: 1000 + containers: + - name: carts + image: ${schema.spec.image} + imagePullPolicy: IfNotPresent + env: + - name: JAVA_OPTS + value: -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/urandom + envFrom: + - configMapRef: + name: ${config.metadata.name} + ports: + - name: http + containerPort: 8080 + protocol: TCP + readinessProbe: + httpGet: + path: /actuator/health/readiness + port: 8080 + initialDelaySeconds: 15 + periodSeconds: 3 + livenessProbe: + httpGet: + path: /actuator/health/liveness + port: 8080 + initialDelaySeconds: 45 + periodSeconds: 3 + resources: + limits: + memory: 1Gi + requests: + cpu: 250m + memory: 1Gi + securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + volumeMounts: + - mountPath: /tmp + name: tmp-volume + volumes: + - name: tmp-volume + emptyDir: + medium: Memory + - id: service + template: + apiVersion: v1 + kind: Service + metadata: + name: carts + namespace: ${schema.spec.namespace} + labels: + app.kubernetes.io/created-by: eks-workshop + spec: + type: ClusterIP + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: carts + app.kubernetes.io/instance: carts + app.kubernetes.io/component: service diff --git a/manifests/modules/fastpaths/eks-capabilities/kro/rgd/kustomization.yaml b/manifests/modules/fastpaths/eks-capabilities/kro/rgd/kustomization.yaml new file mode 100644 index 000000000..ca9378755 --- /dev/null +++ b/manifests/modules/fastpaths/eks-capabilities/kro/rgd/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - cartsstack-rgd.yaml diff --git a/website/docs/fastpaths/eks-capabilities/_category_.json b/website/docs/fastpaths/eks-capabilities/_category_.json new file mode 100644 index 000000000..4a3253737 --- /dev/null +++ b/website/docs/fastpaths/eks-capabilities/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "EKS Capabilities", + "position": 70 +} diff --git a/website/docs/fastpaths/eks-capabilities/ack/index.md b/website/docs/fastpaths/eks-capabilities/ack/index.md new file mode 100644 index 000000000..fc6312c19 --- /dev/null +++ b/website/docs/fastpaths/eks-capabilities/ack/index.md @@ -0,0 +1,24 @@ +--- +title: "Provision AWS resources with ACK" +sidebar_position: 10 +--- + +::required-time{estimatedLabExecutionTimeMinutes="10"} + +:::tip What's been set up for you + +- The **ACK EKS-managed capability** is enabled on the cluster, with the DynamoDB controller selected. The capability assumes an IAM Capability Role scoped to a single DynamoDB table named `${EKS_CLUSTER_AUTO_NAME}-carts-fastpath`. +- An **IAM role** for the `carts` ServiceAccount is pre-provisioned (`${EKS_CLUSTER_AUTO_NAME}-carts-dynamo`) so the application Pod can read and write the table via [EKS Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html). +- The base retail application is running with `carts` pointing at the in-cluster `carts-dynamodb` Pod. + +::: + +By default, the **carts** component in the sample application uses a [DynamoDB local](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html) instance running as a Pod called `carts-dynamodb`. In this lab we'll provision a real Amazon DynamoDB table using a Kubernetes manifest, then point the `carts` Deployment at the cloud-managed table. + +Unlike the [self-managed ACK lab](/docs/automation/controlplanes/ack), there's no `helm install ack-dynamodb-controller` step here. The DynamoDB controller is delivered by the **ACK EKS capability** — a fully managed control-plane component that lives in AWS-owned infrastructure and assumes an IAM Capability Role to act on AWS resources for the cluster. + +Throughout this lab, we will: + +1. Verify the ACK capability is `ACTIVE` and the DynamoDB CRDs are present in the cluster. +2. Provision a DynamoDB table by applying a Kubernetes `Table` custom resource. +3. Migrate the `carts` Deployment from the in-cluster DynamoDB Pod to the new AWS-managed table by patching its ConfigMap and ServiceAccount. diff --git a/website/docs/fastpaths/eks-capabilities/ack/migrate-carts.md b/website/docs/fastpaths/eks-capabilities/ack/migrate-carts.md new file mode 100644 index 000000000..db2a85d27 --- /dev/null +++ b/website/docs/fastpaths/eks-capabilities/ack/migrate-carts.md @@ -0,0 +1,64 @@ +--- +title: "Migrate the carts service" +sidebar_position: 40 +--- + +The DynamoDB table exists, but the `carts` Deployment is still pointed at the in-cluster `carts-dynamodb` Pod. Two changes flip it onto the AWS table: + +1. **ConfigMap** — replace `RETAIL_CART_PERSISTENCE_DYNAMODB_ENDPOINT` and remove the `_CREATE_TABLE` flag (the table already exists). +2. **EKS Pod Identity** — bind the `carts` ServiceAccount to a pre-provisioned IAM role so the Pod can call DynamoDB. The role and its policy are created during `prepare-environment`; we just need to associate it with the ServiceAccount. + +Inspect the kustomization that patches the ConfigMap: + +```kustomization +modules/fastpaths/eks-capabilities/ack/carts/kustomization.yaml +ConfigMap/carts +``` + +:::note +The base-application's local `carts-dynamodb` Pod and Service stay in place. We're only flipping the application's pointer at the database — cleanup will restore the original ConfigMap so other labs work normally. +::: + +Apply the kustomization: + +```bash +$ kubectl kustomize ~/environment/eks-workshop/modules/fastpaths/eks-capabilities/ack/carts \ + | envsubst | kubectl apply -f - +``` + +Bind the `carts` ServiceAccount to the IAM role via [EKS Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html). The role `${EKS_CLUSTER_AUTO_NAME}-carts-dynamo` was created by `prepare-environment` and already has access to both the `-carts` and `-carts-fastpath` tables: + +```bash wait=30 +$ aws eks create-pod-identity-association --cluster-name ${EKS_CLUSTER_AUTO_NAME} \ + --role-arn arn:aws:iam::${AWS_ACCOUNT_ID}:role/${EKS_CLUSTER_AUTO_NAME}-carts-dynamo \ + --namespace carts --service-account carts | jq . +``` + +Restart the `carts` Pod so it picks up the new ConfigMap and the Pod Identity binding: + +```bash timeout=120 +$ kubectl rollout restart -n carts deployment/carts +deployment.apps/carts restarted +$ kubectl rollout status -n carts deployment/carts --timeout=90s +deployment "carts" successfully rolled out +``` + +Confirm the Pod sees the new table name and has Pod Identity credentials available: + +```bash +$ kubectl exec -n carts deployment/carts -- env \ + | grep -E '^RETAIL_CART_PERSISTENCE_DYNAMODB_TABLE_NAME=' +RETAIL_CART_PERSISTENCE_DYNAMODB_TABLE_NAME=...-carts-fastpath +``` + +```bash +$ kubectl exec -n carts deployment/carts -- env \ + | grep AWS_CONTAINER_CREDENTIALS_FULL_URI +AWS_CONTAINER_CREDENTIALS_FULL_URI=http://... +``` + +The `AWS_CONTAINER_CREDENTIALS_FULL_URI` env var being present confirms Pod Identity is wiring the IAM role into the Pod. Every DynamoDB call the carts service makes will use the role's credentials, scoped to only the tables we provisioned. + +That's Lab 1 done. The retail app is now backed by a real, AWS-managed DynamoDB table, provisioned and reconciled entirely from the Kubernetes API by an EKS capability. + +Next, we'll deliver the `catalog` service via GitOps using the **Argo CD capability**. diff --git a/website/docs/fastpaths/eks-capabilities/ack/provision-table.md b/website/docs/fastpaths/eks-capabilities/ack/provision-table.md new file mode 100644 index 000000000..83aa42e90 --- /dev/null +++ b/website/docs/fastpaths/eks-capabilities/ack/provision-table.md @@ -0,0 +1,52 @@ +--- +title: "Provision a DynamoDB table" +sidebar_position: 30 +--- + +We can now define a real DynamoDB table as a Kubernetes resource. Take a look at the manifest: + +::yaml{file="manifests/modules/fastpaths/eks-capabilities/ack/dynamodb/table.yaml" paths="apiVersion,kind,spec.tableName,spec.billingMode,spec.keySchema,spec.attributeDefinitions,spec.globalSecondaryIndexes"} + +1. Uses the ACK DynamoDB controller's `Table` custom resource. +2. Names the table after the cluster (`${EKS_CLUSTER_AUTO_NAME}-carts-fastpath`) so parallel workshop runs don't collide. +3. Uses on-demand pricing. +4. Defines the partition key schema and a global secondary index on `customerId` — matching what the `carts` service expects. + +:::info +The YAML closely mirrors the [DynamoDB `CreateTable` API](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_CreateTable.html). Anything you can express through the API is expressible here. +::: + +Apply the manifest: + +```bash wait=10 +$ kubectl kustomize ~/environment/eks-workshop/modules/fastpaths/eks-capabilities/ack/dynamodb \ + | envsubst | kubectl apply -f - +table.dynamodb.services.k8s.aws/items created +``` + +The capability's DynamoDB controller picks up the new `Table` resource and provisions the corresponding AWS resource. Wait for the `ACK.ResourceSynced` condition — this is how every ACK resource signals it has reconciled successfully: + +```bash timeout=720 +$ kubectl wait table.dynamodb.services.k8s.aws items \ + -n carts --for=condition=ACK.ResourceSynced --timeout=10m +table.dynamodb.services.k8s.aws/items condition met +``` + +Inspect the resource status: + +```bash +$ kubectl get table.dynamodb.services.k8s.aws items -n carts \ + -o jsonpath='{.status.tableStatus}{"\n"}' +ACTIVE +``` + +Finally, confirm the table exists in AWS: + +```bash +$ aws dynamodb describe-table \ + --table-name "$EKS_CAP_DDB_TABLE" \ + --query 'Table.TableStatus' --output text +ACTIVE +``` + +We've created a real DynamoDB table without ever leaving the Kubernetes API. The capability handled both the controller infrastructure and the IAM permissions needed to call the DynamoDB API. diff --git a/website/docs/fastpaths/eks-capabilities/ack/tests/hook-suite.sh b/website/docs/fastpaths/eks-capabilities/ack/tests/hook-suite.sh new file mode 100644 index 000000000..f3b104872 --- /dev/null +++ b/website/docs/fastpaths/eks-capabilities/ack/tests/hook-suite.sh @@ -0,0 +1,34 @@ +set -Eeuo pipefail + +before() { + echo "Asserting ACK capability is ACTIVE before running Lab 1 tests..." + status=$(aws eks describe-capability \ + --cluster-name "$EKS_CLUSTER_AUTO_NAME" \ + --capability-name "$EKS_CAP_ACK_CAPABILITY" \ + --query 'capability.status' --output text) + if [[ "$status" != "ACTIVE" ]]; then + echo "ACK capability status is '$status', expected ACTIVE" >&2 + exit 1 + fi + + kubectl get crd tables.dynamodb.services.k8s.aws >/dev/null +} + +after() { + echo "Asserting Lab 1 end state..." + + # Table exists in AWS + aws dynamodb describe-table --table-name "$EKS_CAP_DDB_TABLE" \ + --query 'Table.TableStatus' --output text | grep -q ACTIVE + + # Pod Identity association for carts SA exists + aws eks list-pod-identity-associations --cluster-name "$EKS_CLUSTER_AUTO_NAME" \ + --namespace carts --service-account carts \ + --query 'associations[].associationId' --output text | grep -q . + + # carts Pod sees the new table name in its environment + kubectl exec -n carts deployment/carts -- env \ + | grep -q "^RETAIL_CART_PERSISTENCE_DYNAMODB_TABLE_NAME=${EKS_CAP_DDB_TABLE}$" +} + +"$@" diff --git a/website/docs/fastpaths/eks-capabilities/ack/verify-capability.md b/website/docs/fastpaths/eks-capabilities/ack/verify-capability.md new file mode 100644 index 000000000..be6b51136 --- /dev/null +++ b/website/docs/fastpaths/eks-capabilities/ack/verify-capability.md @@ -0,0 +1,40 @@ +--- +title: "Verify the ACK capability" +sidebar_position: 20 +--- + +The `prepare-environment` step has already enabled the ACK capability on the cluster. Before doing anything else, let's confirm it's `ACTIVE` and the DynamoDB CRDs are available. + +Inspect the capability resource directly: + +```bash +$ aws eks describe-capability \ + --cluster-name $EKS_CLUSTER_AUTO_NAME \ + --capability-name $EKS_CAP_ACK_CAPABILITY \ + --query 'capability.status' --output text +ACTIVE +``` + +A capability transitions through `CREATING → ACTIVE`. If the status here is anything other than `ACTIVE`, wait a moment and re-run the command — the capability may still be initializing. + +Now check that the DynamoDB controller's custom resources are registered in the cluster: + +```bash +$ kubectl get crd tables.dynamodb.services.k8s.aws \ + -o jsonpath='{.spec.names.kind}{"\n"}' +Table +``` + +```bash +$ kubectl api-resources --api-group=dynamodb.services.k8s.aws +NAME SHORTNAMES APIVERSION NAMESPACED KIND +backups dynamodb.services.k8s.aws/v1alpha1 true Backup +globaltables dynamodb.services.k8s.aws/v1alpha1 true GlobalTable +tables dynamodb.services.k8s.aws/v1alpha1 true Table +``` + +:::info +Notice we never installed a Helm chart, never created an `ack-system` namespace, and there's no DynamoDB controller Pod running on your worker nodes. The capability runs in AWS-managed infrastructure outside the cluster — what you see inside the cluster are only the CRDs the capability registered for you to apply. +::: + +With the capability `ACTIVE` and the CRDs in place, we're ready to provision a DynamoDB table from Kubernetes. diff --git a/website/docs/fastpaths/eks-capabilities/argocd/gitops-update.md b/website/docs/fastpaths/eks-capabilities/argocd/gitops-update.md new file mode 100644 index 000000000..ab037fb1d --- /dev/null +++ b/website/docs/fastpaths/eks-capabilities/argocd/gitops-update.md @@ -0,0 +1,92 @@ +--- +title: "Trigger a GitOps update" +sidebar_position: 50 +--- + +With automated sync enabled, the Git repository is now the source of truth for `catalog`. To roll out a change we don't touch the cluster at all — we change the manifests in CodeCommit and let Argo CD reconcile. Let's bump the `catalog` container image from `1.2.1` to `1.2.2` and watch it deploy. + +First, clone the CodeCommit repository into the IDE. `git-remote-codecommit` lets `git` authenticate to CodeCommit using your ambient AWS credentials through the `codecommit::` remote helper — no SSH keys, no Git credentials: + +```bash +$ rm -rf ~/environment/catalog-gitops +$ git clone codecommit::${AWS_REGION}://${EKS_CAP_CODECOMMIT_REPO} ~/environment/catalog-gitops +``` + +:::note +The `rm -rf` makes the step idempotent: if you ran the clone earlier or re-entered the lab via `prepare-environment`, any previous working copy is removed before cloning a fresh one. The cleanup hook between fastpath modules also removes this directory, so it may not be present from a previous session. +::: + +Confirm the current image tag in the repository: + +```bash +$ grep 'image:' ~/environment/catalog-gitops/catalog/deployment.yaml + image: "public.ecr.aws/aws-containers/retail-store-sample-catalog:1.2.1" +``` + +Update the image tag from `1.2.1` to `1.2.2`: + +```bash +$ sed -i 's|retail-store-sample-catalog:1.2.1|retail-store-sample-catalog:1.2.2|' \ + ~/environment/catalog-gitops/catalog/deployment.yaml +$ grep 'image:' ~/environment/catalog-gitops/catalog/deployment.yaml + image: "public.ecr.aws/aws-containers/retail-store-sample-catalog:1.2.2" +``` + +Commit and push the change. Run the whole block as one chained command — Git needs `user.email` and `user.name` set in this repository before the commit, and the chain (`&&`) guarantees the identity is in place before `git commit` runs: + +```bash +$ cd ~/environment/catalog-gitops && \ + git config user.email "you@eksworkshop.com" && \ + git config user.name "EKS Workshop" && \ + git add catalog/deployment.yaml && \ + (git diff --cached --quiet \ + || git commit -m "Bump catalog image to 1.2.2") && \ + git push origin main +``` + +:::note +If you split the block and run `git commit` standalone, you may see `fatal: unable to auto-detect email address` — that means the `git config` lines didn't run yet in the same shell. Just re-run the chained block above. +::: + +That's the entire change — a single commit to Git. Argo CD polls the repository on its own schedule and reconciles when it detects the new revision. To avoid waiting up to ~3 minutes for the next poll, ask Argo CD to refresh the Application now: + +```bash +$ kubectl annotate application catalog -n argocd \ + argocd.argoproj.io/refresh=hard --overwrite +application.argoproj.io/catalog annotated +``` + +Wait for the rollout to complete with the new image: + +```bash timeout=300 +$ kubectl rollout status -n catalog deployment/catalog --timeout=180s +deployment "catalog" successfully rolled out +``` + +Confirm the running Deployment is now on the new tag: + +```bash +$ kubectl get deployment catalog -n catalog \ + -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}' +public.ecr.aws/aws-containers/retail-store-sample-catalog:1.2.2 +``` + +You can also confirm Argo CD reconciled to the new revision and reports healthy: + +```bash +$ kubectl get application catalog -n argocd \ + -o jsonpath='{.status.sync.status}{"/"}{.status.health.status}{"\n"}' +Synced/Healthy +``` + +You can also see the rolled-out version on the Argo CD UI: + +![Argo CD UI after Identity Center sign-in](/img/fastpaths/eks-capabilities/argocd/argocd-ui-1.22-app.png) + +:::tip +Because `selfHeal` is enabled, try editing the Deployment directly — for example `kubectl scale -n catalog deployment/catalog --replicas=3`. Argo CD detects the drift from Git and reverts it, because Git, not the cluster, is the source of truth. +::: + +That's Lab 2 done. You delivered the `catalog` service through a fully managed GitOps pipeline: a push to CodeCommit became a reconciled rollout on the cluster, with no `kubectl apply` and no self-managed Argo CD to operate. + +Next, we'll use the **kro capability** to declare the complete `carts` stack as a single resource graph. diff --git a/website/docs/fastpaths/eks-capabilities/argocd/index.md b/website/docs/fastpaths/eks-capabilities/argocd/index.md new file mode 100644 index 000000000..73b1ece9b --- /dev/null +++ b/website/docs/fastpaths/eks-capabilities/argocd/index.md @@ -0,0 +1,30 @@ +--- +title: "Continuous delivery with Argo CD" +sidebar_position: 20 +--- + +::required-time + +:::tip What's been set up for you + +- The **Argo CD EKS-managed capability** is `ACTIVE` on the cluster and federated with **AWS IAM Identity Center** for sign-in. There are no local users and no admin password — Identity Center is the only authentication path. +- An IAM Identity Center group + admin user were created by Terraform (see [Sign in to Argo CD via Identity Center](./signin-argocd.md) for the user activation step). The group is mapped to the Argo CD **ADMIN** role on the capability. +- An **AWS CodeCommit repository** (`${EKS_CAP_CODECOMMIT_REPO}`) is pre-provisioned and seeded with the `catalog` Kubernetes manifests. +- An **IAM Capability Role** trusted by the Argo CD capability service principal grants `codecommit:GitPull` on that repository — so the managed Argo CD can read your manifests with no SSH keys and no Git credentials. +- `git-remote-codecommit` is pre-installed in the web IDE for cloning the repo. The lab drives Argo CD through `kubectl` against the `argoproj.io` custom resources, with the browser UI available for interactive exploration. + +::: + +In [Lab 1](../ack/) you provisioned an AWS resource from Kubernetes with the ACK capability. In this lab we'll change how an application gets _delivered_ to the cluster: instead of running `kubectl apply` by hand, we'll let the **Argo CD EKS capability** continuously reconcile the `catalog` service from a Git repository. + +Unlike the [self-managed Argo CD lab](/docs/automation/gitops/argocd), there's no `helm install argocd` step, no `argocd-server` LoadBalancer to wait on, and no initial admin secret to retrieve. The Argo CD control plane runs in AWS-managed infrastructure outside the cluster and assumes an IAM Capability Role to pull from CodeCommit and to deploy into the cluster. + +Throughout this lab, we will: + +1. Verify the Argo CD capability is `ACTIVE` and the Argo CD CRDs are present in the cluster. +2. Register the cluster as an Argo CD deployment target and create an `Application` that points at the seeded CodeCommit repository, with automated sync enabled. +3. Trigger a GitOps update by pushing an image tag change to CodeCommit and watching Argo CD roll it out automatically. + +:::info +Authentication to the Argo CD UI is brokered through AWS Identity Center, which involves an interactive browser sign-in. So this lab can be tested end to end, every step below drives Argo CD through the Kubernetes API (`kubectl` against the `argoproj.io` custom resources). The Identity Center sign-in is covered as an optional walkthrough so you can explore the UI on your own. +::: diff --git a/website/docs/fastpaths/eks-capabilities/argocd/register-and-deploy.md b/website/docs/fastpaths/eks-capabilities/argocd/register-and-deploy.md new file mode 100644 index 000000000..57bf2ca1f --- /dev/null +++ b/website/docs/fastpaths/eks-capabilities/argocd/register-and-deploy.md @@ -0,0 +1,125 @@ +--- +title: "Deliver catalog with GitOps" +sidebar_position: 40 +--- + +The `catalog` service is currently running as part of the base application, applied directly with `kubectl`. We'll now hand ownership of it to Argo CD so it's delivered from Git instead. Two declarative steps make that happen: register a deployment target, then create an `Application`. + +## Register the cluster as a deployment target + +The managed Argo CD capability doesn't deploy to the local cluster automatically — you register it explicitly, and it's identified by its **EKS cluster ARN** rather than the usual in-cluster API URL. We register it under the conventional name `in-cluster`. + +The capability auto-created an EKS access entry for its IAM Capability Role during `prepare-environment`, and the role is associated with the cluster-admin access policy, so Argo CD already has the Kubernetes permissions it needs to sync. + +```yaml +# manifests/modules/fastpaths/eks-capabilities/argocd/cluster.yaml +apiVersion: v1 +kind: Secret +metadata: + name: in-cluster + namespace: argocd + labels: + argocd.argoproj.io/secret-type: cluster +stringData: + name: in-cluster + server: $EKS_CLUSTER_AUTO_ARN + project: default +``` + +1. The `argocd.argoproj.io/secret-type: cluster` label tells Argo CD this Secret describes a deployment target. +2. The target is identified by the cluster ARN (`$EKS_CLUSTER_AUTO_ARN`), not `https://kubernetes.default.svc`. + +Apply it, resolving the cluster ARN with `envsubst`: + +```bash +$ cat ~/environment/eks-workshop/modules/fastpaths/eks-capabilities/argocd/cluster.yaml \ + | envsubst | kubectl apply -f - +secret/in-cluster created +``` + +## Create the catalog Application + +Now define an Argo CD `Application` that points at the seeded CodeCommit repository. Because the IAM Capability Role grants `codecommit:GitPull`, Argo CD reads the repository **directly** by its HTTPS URL — there's no repository Secret, no SSH key, and no Git credential helper to configure. + +```yaml +# manifests/modules/fastpaths/eks-capabilities/argocd/application.yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: catalog + namespace: argocd +spec: + project: default + source: + repoURL: $EKS_CAP_CODECOMMIT_URL + targetRevision: main + path: catalog + destination: + name: in-cluster + namespace: catalog + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true +``` + +1. `repoURL` is the CodeCommit HTTPS endpoint (`$EKS_CAP_CODECOMMIT_URL`); `path: catalog` selects the manifests directory in the repo. +2. `destination.name: in-cluster` matches the deployment target we just registered. +3. `syncPolicy.automated` with `prune` and `selfHeal` makes Argo CD continuously reconcile the cluster to match Git. + +Before Argo CD adopts `catalog`, remove the copy the base application applied with `kubectl` so there's a single owner of the namespace: + +```bash +$ kubectl delete namespace catalog --ignore-not-found +namespace "catalog" deleted +``` + +Apply the Application, resolving the repository URL with `envsubst`: + +```bash +$ kubectl delete application catalog -n argocd --ignore-not-found +$ cat ~/environment/eks-workshop/modules/fastpaths/eks-capabilities/argocd/application.yaml \ + | envsubst | kubectl apply -f - +application.argoproj.io/catalog created +``` + +Argo CD picks up the new `Application`, pulls the manifests from CodeCommit, creates the `catalog` namespace, and deploys the workloads. Trigger an immediate refresh so we don't wait on the default ~3-minute poll, then wait for it to report both `Synced` and `Healthy`: + +```bash timeout=600 +$ kubectl annotate application catalog -n argocd \ + argocd.argoproj.io/refresh=hard --overwrite +application.argoproj.io/catalog annotated +$ kubectl wait --for=jsonpath='{.status.sync.status}'=Synced \ + application/catalog -n argocd --timeout=300s +application.argoproj.io/catalog condition met +$ kubectl wait --for=jsonpath='{.status.health.status}'=Healthy \ + application/catalog -n argocd --timeout=300s +application.argoproj.io/catalog condition met +``` + +Inspect the Application's status: + +```bash +$ kubectl get application catalog -n argocd \ + -o jsonpath='{.status.sync.status}{"/"}{.status.health.status}{"\n"}' +Synced/Healthy +``` + +Confirm the workloads Argo CD deployed are running: + +```bash timeout=300 +$ kubectl rollout status -n catalog deployment/catalog --timeout=240s +deployment "catalog" successfully rolled out +$ kubectl get pods -n catalog +NAME READY STATUS RESTARTS AGE +catalog-7d9f4c5b8d-abcde 1/1 Running 0 90s +catalog-mysql-0 1/1 Running 0 90s +``` + +You can also see it on Argo CD UI + +![Argo CD UI after Identity Center sign-in](/img/fastpaths/eks-capabilities/argocd/argocd-ui-signed-in-app.png) + +The `catalog` service is now delivered by GitOps. Any change pushed to the CodeCommit repository will be reconciled to the cluster automatically — which we'll see next. diff --git a/website/docs/fastpaths/eks-capabilities/argocd/signin-argocd.md b/website/docs/fastpaths/eks-capabilities/argocd/signin-argocd.md new file mode 100644 index 000000000..1845fa7f9 --- /dev/null +++ b/website/docs/fastpaths/eks-capabilities/argocd/signin-argocd.md @@ -0,0 +1,114 @@ +--- +title: "Sign in to Argo CD via Identity Center" +sidebar_position: 15 +--- + +The managed Argo CD capability authenticates **only** through AWS IAM Identity Center. There is no local `admin` account and no auto-generated password — anyone who signs in does so with an Identity Center identity mapped to one of the three built-in Argo CD roles (`ADMIN`, `EDITOR`, `VIEWER`). + +This page walks the **one-time setup** plus the **first sign-in** to the Argo CD UI. After this, you reuse the password you set here for the rest of Lab 2. + +:::caution +Disabling MFA weakens security for **all** users in the IAM Identity Center instance, not just the workshop user. Acceptable for a personal/dev/test account; **do not** apply this in a production account or shared organization. +::: + +:::info +The rest of Lab 2 drives Argo CD entirely through the Kubernetes API so it stays fully testable. Signing in to the UI is **optional but recommended** — the visual graph of the catalog stack is the most engaging part of the lab. +::: + +### 1. Identity Center user and group + +Terraform pre-created the workshop user and group in AWS Identity Center. They were exported into your shell by `prepare-environment`: + +```bash test=false +$ echo $EKS_CAP_ARGOCD_USER +eks-workshop-...-argocd-admin +$ echo $EKS_CAP_ARGOCD_ADMIN_GROUP +eks-workshop-...-argocd-admins +$ echo $EKS_CAP_ARGOCD_URL +https://....eks-capabilities.us-west-2.amazonaws.com +``` + +Pre-created user + +- `$EKS_CAP_ARGOCD_USER`: administrative user mapped to the Argo CD `ADMIN` role. + +Pre-created group + +- `$EKS_CAP_ARGOCD_ADMIN_GROUP`: group with administrative privileges, associated with the Argo CD capability. + +### 2. Disabling MFA for Workshop + +To simplify the authentication experience during the workshop, we'll disable Multi-Factor Authentication (MFA) for Identity Center users. + +Steps to Disable MFA: + +1. Navigate to Identity Center Console + - Open AWS Console + - Search for "Identity Center" +1. Select **Configure MFA** + + ![Configure MFA](/img/fastpaths/eks-capabilities/argocd/sso_mfa_navigate.png) + +1. Disable MFA + - Select "Never (disabled)" in MFA Settings + - Save changes + + ![SSO MFA Disable](/img/fastpaths/eks-capabilities/argocd/sso_mfa_disable.png) + +### 3. Generate temporary password for the admin user + +New users in Identity Center require temporary passwords to be generated by administrators. + +1. Select User + - Navigate to Identity Center → Users + - Find and select `$EKS_CAP_ARGOCD_USER` + + ![Select Argoadmin](/img/fastpaths/eks-capabilities/argocd/argoadmin_select.png) + +1. Reset Password + - Click "Reset password" + - Choose "Generate a one-time password" + + ![Argoadmin Reset Password](/img/fastpaths/eks-capabilities/argocd/argoadmin_select_resetpassword.png) + +1. Copy Temporary Password + - Copy the generated password + - Password will be used to login to the Argo CD dashboard in the next step. + + ![Copy Reset Argoadmin Password](/img/fastpaths/eks-capabilities/argocd/argoadmin_copy_resetpassword.png) + +:::tip +This password generation process will be referenced in other chapters when logging in as different users. +::: + +### 4. First sign-in to Argo CD + +Open the Argo CD URL in a new browser tab: + +```bash test=false +$ echo $EKS_CAP_ARGOCD_URL +``` + +1. Click **Log in via AWS Identity Center**. +2. **Username:** the value of `$EKS_CAP_ARGOCD_USER`. Click **Next**. +3. **Password:** the one-time password you copied in step 3. Click **Sign in**. +4. Identity Center forces a **Set new password** screen on first sign-in. Choose any new password and confirm it. +5. After setting the new password you'll be redirected to the Argo CD **Applications** view as `ADMIN`. + +![Argo CD UI after Identity Center sign-in](/img/fastpaths/eks-capabilities/argocd/argocd-ui-signed-in.png) + +:::tip +You can also reach the UI from the **Amazon EKS console**: select your cluster, choose the **Capabilities** tab, choose **Argo CD**, then **Open Argo CD UI**. Both paths route through the same Identity Center sign-in. +::: + +You're now ready to walk through the rest of Lab 2. + + diff --git a/website/docs/fastpaths/eks-capabilities/argocd/tests/hook-suite.sh b/website/docs/fastpaths/eks-capabilities/argocd/tests/hook-suite.sh new file mode 100644 index 000000000..27b84cd7e --- /dev/null +++ b/website/docs/fastpaths/eks-capabilities/argocd/tests/hook-suite.sh @@ -0,0 +1,53 @@ +set -Eeuo pipefail + +before() { + echo "Asserting Argo CD capability is ACTIVE before running Lab 2 tests..." + status=$(aws eks describe-capability \ + --cluster-name "$EKS_CLUSTER_AUTO_NAME" \ + --capability-name "$EKS_CAP_ARGOCD_CAPABILITY" \ + --query 'capability.status' --output text) + if [[ "$status" != "ACTIVE" ]]; then + echo "Argo CD capability status is '$status', expected ACTIVE" >&2 + exit 1 + fi + + # Argo CD CRDs registered by the capability + kubectl get crd applications.argoproj.io >/dev/null + + # CodeCommit repo seeded with the catalog manifests + aws codecommit get-file \ + --repository-name "$EKS_CAP_CODECOMMIT_REPO" \ + --commit-specifier main \ + --file-path catalog/deployment.yaml >/dev/null +} + +after() { + echo "Asserting Lab 2 end state..." + + # Application exists and is Synced + Healthy + kubectl wait --for=jsonpath='{.status.sync.status}'=Synced \ + application/catalog -n argocd --timeout=180s + kubectl wait --for=jsonpath='{.status.health.status}'=Healthy \ + application/catalog -n argocd --timeout=180s + + # Cluster registered as an Argo CD deployment target + kubectl get secret -n argocd \ + -l argocd.argoproj.io/secret-type=cluster -o name | grep -q . + + # The GitOps update rolled out: running Deployment is on the bumped tag. + # Poll up to 2 minutes — Argo CD may show Synced+Healthy briefly before the + # Deployment's pod template observably reflects the latest revision. + for i in $(seq 1 24); do + image=$(kubectl get deployment catalog -n catalog \ + -o jsonpath='{.spec.template.spec.containers[0].image}' 2>/dev/null || true) + if [[ "$image" == *"retail-store-sample-catalog:1.2.2" ]]; then + return 0 + fi + sleep 5 + done + + echo "catalog image is '$image', expected the GitOps-updated 1.2.2 tag" >&2 + exit 1 +} + +"$@" diff --git a/website/docs/fastpaths/eks-capabilities/argocd/verify-capability.md b/website/docs/fastpaths/eks-capabilities/argocd/verify-capability.md new file mode 100644 index 000000000..c6a7c4400 --- /dev/null +++ b/website/docs/fastpaths/eks-capabilities/argocd/verify-capability.md @@ -0,0 +1,48 @@ +--- +title: "Verify the Argo CD capability" +sidebar_position: 20 +--- + +The `prepare-environment` step has already enabled the Argo CD capability on the cluster and federated it with AWS Identity Center. Before deploying anything, let's confirm it's `ACTIVE` and that the Argo CD custom resources are available. + +Inspect the capability resource directly: + +```bash +$ aws eks describe-capability \ + --cluster-name $EKS_CLUSTER_AUTO_NAME \ + --capability-name $EKS_CAP_ARGOCD_CAPABILITY \ + --query 'capability.status' --output text +ACTIVE +``` + +A capability transitions through `CREATING → ACTIVE`. If the status here is anything other than `ACTIVE`, wait a moment and re-run the command — the capability may still be initializing. + +The capability publishes the URL of its managed Argo CD server. It was exported into your shell as `$EKS_CAP_ARGOCD_URL`: + +```bash +$ echo $EKS_CAP_ARGOCD_URL +https://....argocd.eks.amazonaws.com +``` + +Now check that the Argo CD controller's custom resources are registered in the cluster: + +```bash +$ kubectl api-resources --api-group=argoproj.io +NAME SHORTNAMES APIVERSION NAMESPACED KIND +applications app,apps argoproj.io/v1alpha1 true Application +applicationsets appset,as argoproj.io/v1alpha1 true ApplicationSet +appprojects appproj,... argoproj.io/v1alpha1 true AppProject +``` + +:::info +Just like the ACK capability, we never installed a Helm chart and there's no `argocd-server` Pod running on your worker nodes. The Argo CD control plane runs in AWS-managed infrastructure outside the cluster — what you see inside the cluster are the CRDs the capability registered, which you'll use to declare Applications. +::: + +The capability is also federated with the Identity Center group (its UUID — see [Sign in to Argo CD via Identity Center](./signin-argocd.md)) that grants the Argo CD `ADMIN` role: + +```bash +$ echo $EKS_CAP_ARGOCD_ADMIN_GROUP_ID +########-####-####-####-############ +``` + +With the capability `ACTIVE` and the CRDs in place, we're ready to deliver the `catalog` service via GitOps. diff --git a/website/docs/fastpaths/eks-capabilities/index.md b/website/docs/fastpaths/eks-capabilities/index.md new file mode 100644 index 000000000..8d3fcc283 --- /dev/null +++ b/website/docs/fastpaths/eks-capabilities/index.md @@ -0,0 +1,75 @@ +--- +title: "EKS Capabilities" +sidebar_position: 70 +sidebar_custom_props: { "module": true } +--- + +::required-time{estimatedLabExecutionTimeMinutes="25"} + +Welcome to the **EKS Capabilities** fast path — a hands-on journey targeted at the platform engineer / DevOps persona, showcasing the capabilities that ship with [Amazon EKS Capabilities](https://aws.amazon.com/about-aws/whats-new/2025/11/amazon-eks-capabilities/) on a single coherent story over the retail sample application. + +Each capability is a **fully managed control-plane component** — the controllers run in AWS-owned infrastructure, not on your worker nodes. There's no Helm install, no controller Deployment to scale, and no Pod-level IRSA for the controllers — the capability itself assumes an IAM role to do its work. + +## What you'll build + +| Lab | Capability | What you'll do | +|---|---|---| +| **Lab 1** | **ACK** | Provision a real Amazon DynamoDB table from Kubernetes by applying a `Table` custom resource, then migrate the `carts` microservice from its in-cluster mock to the AWS-managed table via EKS Pod Identity. | +| **Lab 2** | **Argo CD** | Deliver the `catalog` microservice via GitOps from a pre-provisioned AWS CodeCommit repository. Sign in to the managed Argo CD UI through AWS IAM Identity Center, register the cluster as a deployment target, then trigger a real GitOps update by pushing an image-tag bump. | +| **Lab 3** | **kro** | Compose Lab 1's three apply steps into a single `CartsStack` custom resource. Define a `ResourceGraphDefinition` that bundles a Namespace, an ACK `Table`, a ConfigMap, and a ServiceAccount, then apply one instance and watch kro reconcile the whole graph. | + +## Before you start + +This fast path uses a dedicated Amazon EKS Auto Mode cluster. + +### One-time prerequisite (Lab 2 only) + +The Argo CD capability authenticates **only** through AWS IAM Identity Center — there is no local admin user and no auto-generated password. Terraform creates the IDC user, group, and group-membership for you, but you'll do two one-time admin actions in the AWS Console to complete sign-in: + +1. **Disable MFA** on the Identity Center instance (one-time, account-wide). +2. **Generate a one-time password** for the workshop's admin user. + +Walk through the [Sign in to Argo CD via Identity Center](./argocd/signin-argocd.md) page when you reach Lab 2 — it's a 5-minute Console walk that disables MFA, generates a one-time password, and signs you in to Argo CD. + +:::caution +Disabling MFA weakens security for **all** users in the IAM Identity Center instance. Acceptable for a personal/dev/test account; **do not** apply this in a production account or shared organization. +::: + +### Provision the lab infrastructure + +#### 1. Confirm Identity Center is enabled in this region + +```bash test=false +$ aws sso-admin list-instances --query 'Instances[].InstanceArn' --output text | head -1 +arn:aws:sso:::instance/ssoins-... +``` + +If that returns nothing, enable Identity Center once at the [IAM Identity Center console](https://console.aws.amazon.com/singlesignon/home) and re-run. + +#### 2. Run `prepare-environment` + +After completing the IDC prerequisite (or skip it if you only plan to do Lab 1): + +```bash timeout=1800 +$ prepare-environment fastpaths/eks-capabilities +``` + +The first run takes ~10 minutes — it provisions the shared fastpaths infrastructure (KEDA, fluent-bit, External Secrets, Pod Identity for `carts`) plus the EKS capabilities, IAM Capability Roles, the IAM Identity Center user/group/membership, and a seeded CodeCommit repository. Subsequent runs only re-deploy the base application. + +This is the only place `prepare-environment` is invoked. The same provisioning is reused across all labs. + +## What's pre-provisioned for you + +By the end of `prepare-environment`, your cluster has: + +- **ACK capability** — `ACTIVE`, with the DynamoDB controller's CRDs registered in the cluster. +- **Argo CD capability** — `ACTIVE`, federated with AWS IAM Identity Center for sign-in, with an admin group/user mapped to the Argo CD `ADMIN` role. +- **kro capability** — `ACTIVE`, with the `resourcegraphdefinitions.kro.run` CRD registered for use in Lab 3. +- **CodeCommit repository** — pre-seeded with the `catalog` Kubernetes manifests so Argo CD has something to reconcile from. +- **IAM Capability Roles** — one per capability, scoped to the AWS APIs each capability legitimately needs. +- **Pod Identity role for `carts`** — pre-provisioned and wildcard-scoped to `${EKS_CLUSTER_AUTO_NAME}-carts-*`, so the same role covers Lab 1's `-carts-fastpath` table and Lab 3's `-carts-kro` table without changes. +- **Shared fastpaths add-ons** — KEDA, fluent-bit, External Secrets (carried over from the developer/operator fastpaths preprovision). + +Each capability runs in AWS-managed infrastructure outside the cluster — what you see inside the cluster is only the CRDs and any managed namespace each capability registers for you to apply against. + +Let's get started. diff --git a/website/docs/fastpaths/eks-capabilities/kro/apply-instance.md b/website/docs/fastpaths/eks-capabilities/kro/apply-instance.md new file mode 100644 index 000000000..5dfa2c47f --- /dev/null +++ b/website/docs/fastpaths/eks-capabilities/kro/apply-instance.md @@ -0,0 +1,168 @@ +--- +title: "Apply a CartsStack instance" +sidebar_position: 40 +--- + +We're about to apply a single `CartsStack` CR and watch kro fan it out into six Kubernetes resources plus an AWS DynamoDB table. Before we do, there's one piece kro can't manage on its behalf: + +## Pre-bind the Pod Identity association + +The carts container is a Spring Boot service that calls AWS DynamoDB on startup — it needs IAM credentials and a region from the moment it boots. Pod Identity is an **EKS API**, not a Kubernetes API, so kro can't include it in the RGD. + +We solve this by **pre-creating** the Pod Identity association before the carts Pod ever exists. EKS allows registering an association for a namespace + ServiceAccount that doesn't exist yet — the binding becomes active the moment the SA appears in the cluster: + +```bash wait=10 +$ aws eks create-pod-identity-association --cluster-name ${EKS_CLUSTER_AUTO_NAME} \ + --role-arn arn:aws:iam::${AWS_ACCOUNT_ID}:role/${EKS_CLUSTER_AUTO_NAME}-carts-dynamo \ + --namespace carts-kro --service-account carts | jq '.association.associationId' +"a-..." +``` + +The IAM role `${EKS_CLUSTER_AUTO_NAME}-carts-dynamo` is the same role from Lab 1, wildcard-scoped to `${EKS_CLUSTER_AUTO_NAME}-carts-*` — it already covers the kro-managed table without changes. + +:::info +Pod Identity is an EKS API rather than a Kubernetes API, so kro can't manage it directly inside the RGD. The split is intentional: kro composes Kubernetes resources, and an external one-line `aws eks` call binds the AWS-side identity. By creating the association _before_ applying the CartsStack, the carts Pod's first boot has its IAM credentials already wired in. Adding the ACK eks controller would let the RGD include a `PodIdentityAssociation` CR and remove this pre-step — out of scope for this lab, but a natural extension for a platform team. +::: + +## Apply the CartsStack instance + +The instance manifest is small: + +```yaml +apiVersion: kro.run/v1alpha1 +kind: CartsStack +metadata: + name: carts-kro + namespace: default +spec: + tableName: ${EKS_CLUSTER_AUTO_NAME}-carts-kro + namespace: carts-kro +``` + +Apply it: + +```bash +$ kubectl kustomize ~/environment/eks-workshop/modules/fastpaths/eks-capabilities/kro/instance \ + | envsubst | kubectl apply -f - +cartsstack.kro.run/carts-kro created +``` + +:::tip +Compare with [Lab 1](../ack/migrate-carts.md): the same end state took three separate `kubectl apply` steps plus an `aws eks create-pod-identity-association` call plus a `kubectl rollout restart`. Here, one `CartsStack` CR drives the whole graph — and a platform team only writes the RGD once for everyone who needs a carts-shaped stack. +::: + +kro reconciles the six child resources in dependency order: it creates the `Namespace` first, then the `Table` (which the ACK DynamoDB controller starts provisioning in AWS), then the `ConfigMap` and `ServiceAccount` once the Namespace exists, and finally the `Deployment` and `Service` once the SA and ConfigMap they reference exist. The carts Pod boots with IAM credentials already injected by the Pod Identity association we created in the previous step. + +Wait for the instance to reach `ACTIVE`: + +```bash timeout=720 +$ kubectl wait cartsstack carts-kro --for=jsonpath='{.status.state}'=ACTIVE --timeout=10m +cartsstack.kro.run/carts-kro condition met +``` + +:::note +The wait timeout is generous because the bottleneck is the ACK DynamoDB controller actually creating the AWS DynamoDB table — a single instance creates one table from cold and the kro instance does not reach `ACTIVE` until every child resource it owns is healthy. +::: + +## Inspect what kro created + +The Namespace, ConfigMap, and ServiceAccount appeared in the new namespace: + +```bash +$ kubectl get ns carts-kro +NAME STATUS AGE +carts-kro Active ... +$ kubectl -n carts-kro get configmap/carts serviceaccount/carts +NAME DATA AGE +configmap/carts 2 ... + +NAME SECRETS AGE +serviceaccount/carts 0 ... +``` + +Wait for the carts Deployment to roll out — its readiness probe takes ~30 seconds after the Pod becomes ready: + +```bash timeout=180 +$ kubectl -n carts-kro rollout status deployment/carts --timeout=120s +deployment "carts" successfully rolled out +``` + +Inspect the Deployment and Service: + +```bash +$ kubectl -n carts-kro get deployment/carts service/carts +NAME READY UP-TO-DATE AVAILABLE AGE +deployment.apps/carts 1/1 1 1 ... + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/carts ClusterIP ... 80/TCP ... +``` + +The ACK `Table` is reconciled and `ACK.ResourceSynced=True`: + +```bash +$ kubectl -n carts-kro get table.dynamodb.services.k8s.aws items \ + -o jsonpath='{.status.tableStatus}{"\n"}' +ACTIVE +``` + +And the AWS-side DynamoDB table exists: + +```bash +$ aws dynamodb describe-table \ + --table-name "$EKS_CAP_DDB_TABLE_KRO" \ + --query 'Table.TableStatus' --output text +ACTIVE +``` + +A single `CartsStack` apply produced six Kubernetes resources, a real AWS DynamoDB table, and a running carts Pod — all without the learner having to think about ordering or status conditions. + +## Verify the Pod can reach DynamoDB + +The carts Pod has a ServiceAccount, a ConfigMap pointing at the new table, IAM credentials via Pod Identity, and a network path to DynamoDB. Confirm Pod Identity injected the role's credentials at Pod boot: + +```bash +$ kubectl exec -n carts-kro deployment/carts -- env \ + | grep AWS_CONTAINER_CREDENTIALS_FULL_URI +AWS_CONTAINER_CREDENTIALS_FULL_URI=http://... +``` + +The `AWS_CONTAINER_CREDENTIALS_FULL_URI` env var being present confirms Pod Identity injected the role's credentials when the Pod was scheduled. The Pod is wired to the AWS DynamoDB table via: + +- **ServiceAccount** → `aws eks create-pod-identity-association` +- → **IAM role `${EKS_CLUSTER_AUTO_NAME}-carts-dynamo`** +- → **AWS DynamoDB table `${EKS_CLUSTER_AUTO_NAME}-carts-kro`** + +That's Lab 3 done. You've defined a higher-level Kubernetes API (`CartsStack`) that composes ACK and native Kubernetes resources into a single CR — and a single instance produced a complete, running, IAM-bound carts service. + +## Optional: see Lab 3's carts in the retail store UI + +By default, the retail store's `ui` Pod talks to `carts.carts.svc.cluster.local` — Lab 1's namespace. To temporarily route the UI at the kro-managed carts service in `carts-kro` instead, repoint the env var and restart the UI: + +```bash test=false +$ kubectl -n ui set env deployment/ui RETAIL_UI_ENDPOINTS_CARTS_URL=http://carts.carts-kro:80 +$ kubectl -n ui rollout status deployment/ui --timeout=60s +deployment "ui" successfully rolled out +``` + +Now port-forward the UI Service so you can hit it from the IDE's browser preview: + +```bash test=false +$ kubectl port-forward -n ui svc/ui 8080:80 +``` + +In the workshop IDE, a popup appears showing forwarded ports — click to open `http://localhost:8080`. Add a couple of items to a cart in the browser; they'll land in the kro-managed `${EKS_CLUSTER_AUTO_NAME}-carts-kro` DynamoDB table: + +```bash test=false +$ aws dynamodb scan --table-name "$EKS_CAP_DDB_TABLE_KRO" \ + --query 'Count' --output text +``` + +Press `CTRL+C` to break the port-forward. + +To revert the UI back to Lab 1's carts namespace: + +```bash test=false +$ kubectl -n ui set env deployment/ui RETAIL_UI_ENDPOINTS_CARTS_URL- +$ kubectl -n ui rollout status deployment/ui --timeout=60s +``` diff --git a/website/docs/fastpaths/eks-capabilities/kro/define-rgd.md b/website/docs/fastpaths/eks-capabilities/kro/define-rgd.md new file mode 100644 index 000000000..3ad93453d --- /dev/null +++ b/website/docs/fastpaths/eks-capabilities/kro/define-rgd.md @@ -0,0 +1,108 @@ +--- +title: "Define the CartsStack RGD" +sidebar_position: 30 +--- + +A **ResourceGraphDefinition** is the heart of kro: it declares a new Kubernetes API kind and the graph of resources that get created whenever someone applies an instance of that kind. + +The `CartsStack` RGD we'll define in this lab takes a couple of simple inputs (`tableName`, `namespace`, plus optional `image` and `replicas`) and expands into the full carts stack — a `Namespace`, an ACK `Table`, a `ConfigMap`, a `ServiceAccount`, a `Deployment` running the carts container, and a `Service` so other components can reach it. Here's an excerpt of the manifest we'll apply: + +```yaml +apiVersion: kro.run/v1alpha1 +kind: ResourceGraphDefinition +metadata: + name: cartsstack +spec: + schema: + apiVersion: v1alpha1 + kind: CartsStack + group: kro.run + spec: + tableName: string | required=true + namespace: string | required=true + image: string | default="public.ecr.aws/aws-containers/retail-store-sample-cart:1.2.1" + replicas: integer | default=1 + status: + tableArn: ${table.status.ackResourceMetadata.arn} + resources: + - id: ns + template: + apiVersion: v1 + kind: Namespace + metadata: { name: ${schema.spec.namespace} } + - id: table + template: + apiVersion: dynamodb.services.k8s.aws/v1alpha1 + kind: Table + metadata: { name: items, namespace: ${schema.spec.namespace} } + spec: + tableName: ${schema.spec.tableName} + billingMode: PAY_PER_REQUEST + # ...key schema + GSI omitted for brevity + - id: sa + template: + apiVersion: v1 + kind: ServiceAccount + metadata: { name: carts, namespace: ${schema.spec.namespace} } + - id: config + template: + apiVersion: v1 + kind: ConfigMap + metadata: { name: carts, namespace: ${schema.spec.namespace} } + data: + RETAIL_CART_PERSISTENCE_PROVIDER: dynamodb + RETAIL_CART_PERSISTENCE_DYNAMODB_TABLE_NAME: ${schema.spec.tableName} + - id: deployment + template: + apiVersion: apps/v1 + kind: Deployment + metadata: { name: carts, namespace: ${schema.spec.namespace} } + spec: + replicas: ${schema.spec.replicas} + # ...selector, podSpec wired to the SA + ConfigMap above + - id: service + template: + apiVersion: v1 + kind: Service + metadata: { name: carts, namespace: ${schema.spec.namespace} } + spec: + type: ClusterIP + ports: [{ port: 80, targetPort: http, name: http }] +``` + +A few things to notice: + +- **`spec.schema`** declares the shape of the user-facing CR. kro uses a **SimpleSchema** syntax (the `string | required=true` form), not raw OpenAPI. Optional fields like `image` and `replicas` get sensible defaults so a minimal instance only has to set `tableName` and `namespace`. +- **`spec.resources`** is the graph kro will create. Each entry has an `id` (used to reference its outputs from other resources) and a `template` (the actual manifest). +- **`${schema.spec.X}`** references fields from the user's instance. **`${table.status.ackResourceMetadata.arn}`** references runtime status from another resource — kro orders the graph so `table` is created before anything that reads its status. +- **`${sa.metadata.name}`** and **`${config.metadata.name}`** in the Deployment template are how the Pod's `serviceAccountName` and `envFrom.configMapRef.name` get wired up — kro infers the dependency from these references, so the Deployment is created only after the SA and ConfigMap exist. + +Apply the RGD: + +```bash +$ kubectl apply -k ~/environment/eks-workshop/modules/fastpaths/eks-capabilities/kro/rgd +resourcegraphdefinition.kro.run/cartsstack created +``` + +kro validates the RGD synchronously: it type-checks every `${...}` expression against the actual Kubernetes schemas of the resources you reference, and detects circular dependencies. If anything is wrong, the apply fails immediately with a descriptive error. + +Wait for the RGD to reach `Active` — at this point kro has dynamically generated and registered the `CartsStack` CRD in the cluster: + +```bash timeout=120 +$ kubectl wait rgd cartsstack --for=jsonpath='{.status.state}'=Active --timeout=60s +resourcegraphdefinition.kro.run/cartsstack condition met +``` + +Confirm the new `CartsStack` kind is now a first-class Kubernetes API: + +```bash +$ kubectl api-resources --api-group=kro.run | grep -E 'NAME|cartsstacks' +NAME SHORTNAMES APIVERSION NAMESPACED KIND +cartsstacks kro.run/v1alpha1 true CartsStack +``` + +:::info +`cartsstacks` is **namespaced** even though the RGD that defines it is cluster-scoped. RGDs live cluster-wide; instances always live in a specific namespace. +::: + +The schema exists. Next we'll apply an instance. diff --git a/website/docs/fastpaths/eks-capabilities/kro/index.md b/website/docs/fastpaths/eks-capabilities/kro/index.md new file mode 100644 index 000000000..9a18611c1 --- /dev/null +++ b/website/docs/fastpaths/eks-capabilities/kro/index.md @@ -0,0 +1,49 @@ +--- +title: "Compose stacks with kro" +sidebar_position: 30 +--- + +::required-time{estimatedLabExecutionTimeMinutes="10"} + +:::tip What's been set up for you + +- The **kro EKS-managed capability** is `ACTIVE` on the cluster, with the `resourcegraphdefinitions.kro.run` CRD registered. +- The **ACK DynamoDB capability** from Lab 1 is still `ACTIVE` — kro will compose its `Table` custom resource alongside native Kubernetes objects. +- The pre-provisioned `${EKS_CLUSTER_AUTO_NAME}-carts-dynamo` IAM role is wildcard-scoped to `${EKS_CLUSTER_AUTO_NAME}-carts-*` tables, so the same role covers Lab 1's `-carts-fastpath` table and Lab 3's `-carts-kro` table without changes. + +::: + +In [Lab 1](../ack/) you applied **three separate manifests** — a `Table` custom resource, a ConfigMap override, and a Pod Identity association — to migrate `carts` onto an AWS-managed DynamoDB table. That's the right altitude for a one-off, but a platform team running this for many services would want **one user-facing CR** that captures the whole stack. + +In this lab you'll do exactly that with the **kro** EKS capability. kro lets you define a `ResourceGraphDefinition` (RGD) — a schema for a higher-level CR plus the graph of resources it expands into — and apply a single instance that bundles a Namespace, an ACK `Table`, a ConfigMap, and a ServiceAccount. The kro controllers run in AWS-owned infrastructure outside the cluster; you only see the CRDs they registered. + +Throughout this lab, we will: + +1. Verify the kro capability is `ACTIVE` and the `resourcegraphdefinitions.kro.run` CRD is present. +2. Apply a `CartsStack` ResourceGraphDefinition that composes Namespace + ACK Table + ConfigMap + ServiceAccount + Deployment + Service. +3. Apply a `CartsStack` instance, observe kro reconcile its child resources in order, then bind a Pod Identity association so the `carts` Pod can read and write the new table. + +A single `CartsStack` apply produces this graph: + +```text + you apply: + CartsStack/carts-kro (one CR, two required fields) + │ + ā–¼ kro reconciler expands the RGD + │ + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + ā–¼ ā–¼ ā–¼ ā–¼ ā–¼ ā–¼ +Namespace Table ConfigMap SA Deployment Service + │ │ + ā–¼ ā–¼ + ACK DynamoDB Pod (carts-…) + controller │ + │ │ + aws eks create-pod-identity-association + ā–¼ ā–¼ + AWS DynamoDB table AWS_CONTAINER_CREDENTIALS + (eks-workshop-…-carts-kro) via Pod Identity Agent +``` + +:::info +kro itself does not call AWS APIs — it only reconciles Kubernetes resources. Anything that needs to _create_ an AWS resource flows through a controller that does (in this lab, the ACK DynamoDB controller from Lab 1). Pod Identity is an EKS API rather than a Kubernetes API, so the binding step at the end of this lab is a one-line `aws eks` command, not part of the RGD. +::: diff --git a/website/docs/fastpaths/eks-capabilities/kro/tests/hook-suite.sh b/website/docs/fastpaths/eks-capabilities/kro/tests/hook-suite.sh new file mode 100644 index 000000000..337cdeac9 --- /dev/null +++ b/website/docs/fastpaths/eks-capabilities/kro/tests/hook-suite.sh @@ -0,0 +1,51 @@ +set -Eeuo pipefail + +before() { + echo "Asserting kro capability is ACTIVE before running Lab 3 tests..." + status=$(aws eks describe-capability \ + --cluster-name "$EKS_CLUSTER_AUTO_NAME" \ + --capability-name "$EKS_CAP_KRO_CAPABILITY" \ + --query 'capability.status' --output text) + if [[ "$status" != "ACTIVE" ]]; then + echo "kro capability status is '$status', expected ACTIVE" >&2 + exit 1 + fi + + kubectl get crd resourcegraphdefinitions.kro.run >/dev/null +} + +after() { + echo "Asserting Lab 3 end state..." + + # RGD reached Active + kubectl get rgd cartsstack \ + -o jsonpath='{.status.state}' | grep -q '^Active$' + + # Instance reached ACTIVE (kro uses uppercase for instance state, mixed + # case for RGD state — confirmed against kro v0.9.2 shipped by the EKS + # capability). + kubectl get cartsstack carts-kro \ + -o jsonpath='{.status.state}' | grep -q '^ACTIVE$' + + # ACK Table inside the carts-kro namespace synced + kubectl -n carts-kro get table.dynamodb.services.k8s.aws items \ + -o jsonpath='{.status.tableStatus}' | grep -q '^ACTIVE$' + + # AWS-side table exists + aws dynamodb describe-table --table-name "$EKS_CAP_DDB_TABLE_KRO" \ + --query 'Table.TableStatus' --output text | grep -q ACTIVE + + # Pod Identity association for carts-kro/carts SA exists + aws eks list-pod-identity-associations --cluster-name "$EKS_CLUSTER_AUTO_NAME" \ + --namespace carts-kro --service-account carts \ + --query 'associations[].associationId' --output text | grep -q . + + # carts Deployment from the RGD is ready + kubectl -n carts-kro rollout status deployment/carts --timeout=60s + + # Pod Identity creds are wired into the (post-restart) Pod + kubectl exec -n carts-kro deployment/carts -- env \ + | grep -q '^AWS_CONTAINER_CREDENTIALS_FULL_URI=' +} + +"$@" diff --git a/website/docs/fastpaths/eks-capabilities/kro/verify-capability.md b/website/docs/fastpaths/eks-capabilities/kro/verify-capability.md new file mode 100644 index 000000000..b800b31c8 --- /dev/null +++ b/website/docs/fastpaths/eks-capabilities/kro/verify-capability.md @@ -0,0 +1,38 @@ +--- +title: "Verify the kro capability" +sidebar_position: 20 +--- + +`prepare-environment` enabled the kro capability on the cluster. Confirm it's `ACTIVE` and that kro's CRDs are registered before applying anything. + +Inspect the capability resource directly: + +```bash +$ aws eks describe-capability \ + --cluster-name $EKS_CLUSTER_AUTO_NAME \ + --capability-name $EKS_CAP_KRO_CAPABILITY \ + --query 'capability.status' --output text +ACTIVE +``` + +A capability transitions through `CREATING → ACTIVE`. If the status is anything else, wait a moment and re-run. + +Check that kro's `ResourceGraphDefinition` CRD is registered: + +```bash +$ kubectl get crd resourcegraphdefinitions.kro.run \ + -o jsonpath='{.spec.names.kind}{"\n"}' +ResourceGraphDefinition +``` + +```bash +$ kubectl api-resources --api-group=kro.run +NAME SHORTNAMES APIVERSION NAMESPACED KIND +resourcegraphdefinitions rgd kro.run/v1alpha1 false ResourceGraphDefinition +``` + +:::info +The `ResourceGraphDefinition` CRD is **cluster-scoped** (`NAMESPACED: false`). RGDs you create later are always cluster-wide. The instances of those RGDs — for example a `CartsStack` resource — are namespaced, because they own resources in a specific namespace. +::: + +With the capability `ACTIVE` and the CRD in place, we're ready to define our first RGD. diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 2cd0e4548..2f4a336f3 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -62,6 +62,10 @@ const config = { }, { to: "/docs/fastpaths/developer", label: "Developer" }, { to: "/docs/fastpaths/operator", label: "Operator" }, + { + to: "/docs/fastpaths/eks-capabilities", + label: "EKS Capabilities", + }, ], }, }, diff --git a/website/static/img/fastpaths/eks-capabilities/argocd/argoadmin_copy_resetpassword.png b/website/static/img/fastpaths/eks-capabilities/argocd/argoadmin_copy_resetpassword.png new file mode 100644 index 000000000..12302f90f Binary files /dev/null and b/website/static/img/fastpaths/eks-capabilities/argocd/argoadmin_copy_resetpassword.png differ diff --git a/website/static/img/fastpaths/eks-capabilities/argocd/argoadmin_select.png b/website/static/img/fastpaths/eks-capabilities/argocd/argoadmin_select.png new file mode 100644 index 000000000..877e6bc6c Binary files /dev/null and b/website/static/img/fastpaths/eks-capabilities/argocd/argoadmin_select.png differ diff --git a/website/static/img/fastpaths/eks-capabilities/argocd/argoadmin_select_resetpassword.png b/website/static/img/fastpaths/eks-capabilities/argocd/argoadmin_select_resetpassword.png new file mode 100644 index 000000000..7c63afbf3 Binary files /dev/null and b/website/static/img/fastpaths/eks-capabilities/argocd/argoadmin_select_resetpassword.png differ diff --git a/website/static/img/fastpaths/eks-capabilities/argocd/argocd-ui-1.22-app.png b/website/static/img/fastpaths/eks-capabilities/argocd/argocd-ui-1.22-app.png new file mode 100644 index 000000000..ad6765f40 Binary files /dev/null and b/website/static/img/fastpaths/eks-capabilities/argocd/argocd-ui-1.22-app.png differ diff --git a/website/static/img/fastpaths/eks-capabilities/argocd/argocd-ui-signed-in-app.png b/website/static/img/fastpaths/eks-capabilities/argocd/argocd-ui-signed-in-app.png new file mode 100644 index 000000000..f88fbe396 Binary files /dev/null and b/website/static/img/fastpaths/eks-capabilities/argocd/argocd-ui-signed-in-app.png differ diff --git a/website/static/img/fastpaths/eks-capabilities/argocd/argocd-ui-signed-in.png b/website/static/img/fastpaths/eks-capabilities/argocd/argocd-ui-signed-in.png new file mode 100644 index 000000000..887fee638 Binary files /dev/null and b/website/static/img/fastpaths/eks-capabilities/argocd/argocd-ui-signed-in.png differ diff --git a/website/static/img/fastpaths/eks-capabilities/argocd/argocd-ui-sso.png b/website/static/img/fastpaths/eks-capabilities/argocd/argocd-ui-sso.png new file mode 100644 index 000000000..85d25915d Binary files /dev/null and b/website/static/img/fastpaths/eks-capabilities/argocd/argocd-ui-sso.png differ diff --git a/website/static/img/fastpaths/eks-capabilities/argocd/eks-capability.png b/website/static/img/fastpaths/eks-capabilities/argocd/eks-capability.png new file mode 100644 index 000000000..f444638a8 Binary files /dev/null and b/website/static/img/fastpaths/eks-capabilities/argocd/eks-capability.png differ diff --git a/website/static/img/fastpaths/eks-capabilities/argocd/sso_mfa_disable.png b/website/static/img/fastpaths/eks-capabilities/argocd/sso_mfa_disable.png new file mode 100644 index 000000000..8fc3f852c Binary files /dev/null and b/website/static/img/fastpaths/eks-capabilities/argocd/sso_mfa_disable.png differ diff --git a/website/static/img/fastpaths/eks-capabilities/argocd/sso_mfa_navigate.png b/website/static/img/fastpaths/eks-capabilities/argocd/sso_mfa_navigate.png new file mode 100644 index 000000000..d0adce19b Binary files /dev/null and b/website/static/img/fastpaths/eks-capabilities/argocd/sso_mfa_navigate.png differ diff --git a/website/test-durations.json b/website/test-durations.json index cc616e2d0..142e8a796 100644 --- a/website/test-durations.json +++ b/website/test-durations.json @@ -50,6 +50,20 @@ "/fastpaths/developer/pod-logging/fluent-bit-cloudwatch.md": 1, "/fastpaths/developer/pod-logging/fluentbit-setup.md": 1, "/fastpaths/developer/pod-logging/index.md": 1, + "/fastpaths/eks-capabilities/ack/index.md": 1, + "/fastpaths/eks-capabilities/ack/migrate-carts.md": 1, + "/fastpaths/eks-capabilities/ack/provision-table.md": 1, + "/fastpaths/eks-capabilities/ack/verify-capability.md": 1, + "/fastpaths/eks-capabilities/argocd/gitops-update.md": 1, + "/fastpaths/eks-capabilities/argocd/index.md": 1, + "/fastpaths/eks-capabilities/argocd/register-and-deploy.md": 1, + "/fastpaths/eks-capabilities/argocd/signin-argocd.md": 1, + "/fastpaths/eks-capabilities/argocd/verify-capability.md": 1, + "/fastpaths/eks-capabilities/index.md": 1, + "/fastpaths/eks-capabilities/kro/apply-instance.md": 1, + "/fastpaths/eks-capabilities/kro/define-rgd.md": 1, + "/fastpaths/eks-capabilities/kro/index.md": 1, + "/fastpaths/eks-capabilities/kro/verify-capability.md": 1, "/fastpaths/explore/index.md": 1, "/fastpaths/getting-started/about.md": 1, "/fastpaths/getting-started/finish.md": 1,