diff --git a/.gitignore b/.gitignore index 9a6f694990..61acc7d8bc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ bin/ obj/ .vs/ node_modules/ +SESSION_NOTES.MD \ No newline at end of file diff --git a/.kiro/specs/voting-app-aws/spec.md b/.kiro/specs/voting-app-aws/spec.md new file mode 100644 index 0000000000..9bbb548e0a --- /dev/null +++ b/.kiro/specs/voting-app-aws/spec.md @@ -0,0 +1,66 @@ +# Voting App -- AWS EKS Deployment Spec + +## Overview +Deploy the dockersamples/example-voting-app on AWS EKS using DevOps best practices. The app consists of 5 services: vote (Python), result (Node.js), worker (.NET), redis, and postgres. + +## Architecture +``` +Internet + | + |---> ALB ---> vote Service (port 80) -- "Vote: Cats vs Dogs" + '---> ALB ---> result Service (port 80) -- "Live Results" + | + +------+-------+ + v v + Redis Postgres + | | + '---- Worker --+ +``` + +## AWS Resources +- **EKS Cluster**: `voting-app-cluster` -- t3.medium x2 nodes, ap-southeast-2 +- **ECR**: 3 private repos -- vote, result, worker (redis/postgres use public images) +- **CodeBuild**: builds images, pushes to ECR, deploys to EKS +- **ALB**: via AWS Load Balancer Controller -- exposes vote and result +- **IAM**: least-privilege roles for CodeBuild and EKS nodes +- **SNS**: deployment notifications to fahadkhalid695@gmail.com +- **CloudWatch**: Container Insights enabled on EKS cluster + +## Requirements + +### Requirement 1 -- ECR Repositories +- [ ] Create ECR repos: `voting-app/vote`, `voting-app/result`, `voting-app/worker` +- [ ] Enable image scanning on push +- [ ] Lifecycle policy: keep last 10 images + +### Requirement 2 -- EKS Cluster +- [ ] Create EKS cluster `voting-app-cluster` in ap-southeast-2 +- [ ] Managed node group: t3.medium, min=2 max=4, desired=2 +- [ ] Enable CloudWatch Container Insights +- [ ] Install AWS Load Balancer Controller via Helm +- [ ] Configure OIDC provider for IRSA + +### Requirement 3 -- IAM Roles +- [ ] CodeBuild role: ECR push, EKS describe/update, SNS publish +- [ ] EKS node role: ECR pull, CloudWatch, SSM +- [ ] ALB Controller role (IRSA): elasticloadbalancing:*, ec2:Describe* + +### Requirement 4 -- CodeBuild Pipeline +- [ ] Source: GitHub repo (dockersamples/example-voting-app) +- [ ] Build: docker build vote, result, worker -- push to ECR +- [ ] Deploy: kubectl apply k8s manifests with ECR image URIs +- [ ] Notify: SNS on success/failure + +### Requirement 5 -- Kubernetes Manifests +- [ ] Namespace: `voting` +- [ ] Deployments: vote, result, worker, redis, db (postgres) +- [ ] Services: ClusterIP for redis/db/worker, LoadBalancer for vote/result +- [ ] ConfigMap for postgres credentials +- [ ] Resource limits on all containers +- [ ] Liveness and readiness probes + +### Requirement 6 -- Verification +- [ ] Vote app accessible via ALB URL on port 80 +- [ ] Result app accessible via ALB URL on port 80 +- [ ] Cast a vote -- appears in result within 5 seconds +- [ ] SNS deployment notification received diff --git a/.tools/kubectl.exe b/.tools/kubectl.exe new file mode 100644 index 0000000000..eee2498ee3 Binary files /dev/null and b/.tools/kubectl.exe differ diff --git a/aws/buildspec.yml b/aws/buildspec.yml new file mode 100644 index 0000000000..2a25e59acf --- /dev/null +++ b/aws/buildspec.yml @@ -0,0 +1,89 @@ +version: 0.2 + +env: + variables: + AWS_REGION: "ap-southeast-2" + # ACCOUNT_ID and ECR_BASE are injected by CodeBuild project environment variables + # (set via deploy.ps1 when the project is created -- no hardcoded values needed here) + ACCOUNT_ID: "YOUR_AWS_ACCOUNT_ID" + CLUSTER_NAME: "voting-app-cluster" + ECR_BASE: "YOUR_AWS_ACCOUNT_ID.dkr.ecr.ap-southeast-2.amazonaws.com" + +phases: + install: + runtime-versions: + python: 3.11 + commands: + - echo Installing kubectl + - curl -LO "https://dl.k8s.io/release/$(curl -Ls https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + - chmod +x kubectl && mv kubectl /usr/local/bin/ + - kubectl version --client + + pre_build: + commands: + - echo ECR Login + - aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $ECR_BASE + + - echo Configure kubeconfig + - aws eks update-kubeconfig --region $AWS_REGION --name $CLUSTER_NAME + + - export IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c1-8) + - echo IMAGE_TAG=$IMAGE_TAG + + # ── AWS Load Balancer Controller setup ────────────────────────────── + # ── CloudWatch Container Insights ────────────────────────────────── + build: + commands: + - echo Building Docker images + - docker build -t $ECR_BASE/voting-app/vote:$IMAGE_TAG -t $ECR_BASE/voting-app/vote:latest ./vote + - docker build -t $ECR_BASE/voting-app/result:$IMAGE_TAG -t $ECR_BASE/voting-app/result:latest ./result + - docker build -t $ECR_BASE/voting-app/worker:$IMAGE_TAG -t $ECR_BASE/voting-app/worker:latest ./worker + + post_build: + commands: + - echo Pushing images to ECR + - docker push $ECR_BASE/voting-app/vote:$IMAGE_TAG + - docker push $ECR_BASE/voting-app/vote:latest + - docker push $ECR_BASE/voting-app/result:$IMAGE_TAG + - docker push $ECR_BASE/voting-app/result:latest + - docker push $ECR_BASE/voting-app/worker:$IMAGE_TAG + - docker push $ECR_BASE/voting-app/worker:latest + + - echo Deploying to EKS + - kubectl apply -f aws/k8s/postgres-secret.yaml + - kubectl apply -f aws/k8s/configmap.yaml + - kubectl apply -f aws/k8s/redis-deployment.yaml + - kubectl apply -f aws/k8s/redis-service.yaml + - kubectl apply -f aws/k8s/db-pvc.yaml + - kubectl apply -f aws/k8s/db-deployment.yaml + - kubectl apply -f aws/k8s/db-service.yaml + - kubectl apply -f aws/k8s/vote-deployment.yaml + - kubectl apply -f aws/k8s/vote-service.yaml + - kubectl apply -f aws/k8s/result-deployment.yaml + - kubectl apply -f aws/k8s/result-service.yaml + - kubectl apply -f aws/k8s/worker-deployment.yaml + + - echo Waiting for rollouts + - kubectl rollout status deployment/vote -n voting --timeout=300s + - kubectl rollout status deployment/result -n voting --timeout=300s + - kubectl rollout status deployment/worker -n voting --timeout=300s + + - echo Fetching service URLs + - kubectl get svc -n voting + - | + VOTE_URL=$(kubectl get svc vote -n voting \ + -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 2>/dev/null || echo "pending") + RESULT_URL=$(kubectl get svc result -n voting \ + -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 2>/dev/null || echo "pending") + echo "Vote App -- http://$VOTE_URL" + echo "Result App -- http://$RESULT_URL" + + if [ -n "$SNS_ARN" ] && [ "$SNS_ARN" != "None" ]; then + aws sns publish \ + --topic-arn "$SNS_ARN" \ + --subject "DEPLOYED - Voting App" \ + --message "Deployment complete. Tag: $IMAGE_TAG | Vote: http://$VOTE_URL | Result: http://$RESULT_URL" \ + --region $AWS_REGION + else + echo "SNS_ARN not set -- skipping notification" + fi diff --git a/aws/deploy.ps1 b/aws/deploy.ps1 new file mode 100644 index 0000000000..77b91bfd83 --- /dev/null +++ b/aws/deploy.ps1 @@ -0,0 +1,313 @@ +# Voting App - AWS EKS Full Deployment Script +# Run from the repo root: .\aws\deploy.ps1 +# +# BEFORE RUNNING -- update the variables below to match your environment: +# $ACCOUNT_ID -- your 12-digit AWS account ID +# $SNS_EMAIL -- email address for deployment notifications +# $GITHUB_REPO_URL -- your forked repo URL (if you have customised the app) +# +# NOTE: This script contains no credentials or secrets. +# AWS access is provided by the credentials in your current AWS CLI session. + +$ErrorActionPreference = "Stop" + +$REGION = "ap-southeast-2" +$ACCOUNT_ID = "YOUR_AWS_ACCOUNT_ID" # <-- replace with your account ID +$CLUSTER_NAME = "voting-app-cluster" +$ECR_BASE = "$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com" +$SNS_EMAIL = "YOUR_EMAIL@example.com" # <-- replace with your email +$CODEBUILD_PROJECT = "voting-app-build" +$ROLE_CODEBUILD = "VotingAppCodeBuildRole" +$ROLE_EKS_NODE = "VotingAppEKSNodeRole" +# Update this to your forked repo URL if you've customised the app +$GITHUB_REPO_URL = "https://github.com/dockersamples/example-voting-app.git" + +function Log($msg) { Write-Host "$(Get-Date -Format 'HH:mm:ss') $msg" -ForegroundColor Cyan } +function OK($msg) { Write-Host " OK -- $msg" -ForegroundColor Green } +function Warn($msg) { Write-Host " WARN -- $msg" -ForegroundColor Yellow } + +Log "================================================" +Log " VOTING APP -- AWS EKS Deployment" +Log "================================================" +Log " Account : $ACCOUNT_ID" +Log " Region : $REGION" +Log " Cluster : $CLUSTER_NAME" +Log "================================================" + +# ── STEP 1: SNS Topic ────────────────────────────── +Log "[1/8] Creating SNS notification topic..." +$SNS_ARN = (aws sns create-topic --name "VotingApp-Notifications" --region $REGION --query "TopicArn" --output text) +aws sns subscribe --topic-arn $SNS_ARN --protocol email --notification-endpoint $SNS_EMAIL --region $REGION | Out-Null +OK "SNS: $SNS_ARN" +OK "Subscription pending -- check $SNS_EMAIL" + +# ── STEP 2: ECR Repositories ────────────────────── +Log "[2/8] Creating ECR repositories..." +foreach ($repo in @("voting-app/vote","voting-app/result","voting-app/worker")) { + $exists = aws ecr describe-repositories --repository-names $repo --region $REGION 2>&1 + if ($LASTEXITCODE -ne 0) { + aws ecr create-repository --repository-name $repo --region $REGION ` + --image-scanning-configuration scanOnPush=true ` + --encryption-configuration encryptionType=AES256 | Out-Null + # Lifecycle policy -- keep last 10 images + $lifecycle = '{"rules":[{"rulePriority":1,"description":"Keep last 10 images","selection":{"tagStatus":"any","countType":"imageCountMoreThan","countNumber":10},"action":{"type":"expire"}}]}' + aws ecr put-lifecycle-policy --repository-name $repo --lifecycle-policy-text $lifecycle --region $REGION | Out-Null + OK "Created ECR: $repo" + } else { + OK "ECR already exists: $repo" + } +} + +# ── STEP 3: IAM Roles ───────────────────────────── +Log "[3/8] Creating IAM roles..." + +# CodeBuild trust policy +$cbTrust = '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"codebuild.amazonaws.com"},"Action":"sts:AssumeRole"}]}' + +$cbExists = aws iam get-role --role-name $ROLE_CODEBUILD 2>&1 +if ($LASTEXITCODE -ne 0) { + aws iam create-role --role-name $ROLE_CODEBUILD ` + --assume-role-policy-document $cbTrust ` + --description "CodeBuild role for Voting App -- ECR push and EKS deploy" | Out-Null + + $cbPolicy = "{`"Version`":`"2012-10-17`",`"Statement`":[{`"Effect`":`"Allow`",`"Action`":[`"ecr:GetAuthorizationToken`",`"ecr:BatchCheckLayerAvailability`",`"ecr:GetDownloadUrlForLayer`",`"ecr:BatchGetImage`",`"ecr:PutImage`",`"ecr:InitiateLayerUpload`",`"ecr:UploadLayerPart`",`"ecr:CompleteLayerUpload`"],`"Resource`":`"*`"},{`"Effect`":`"Allow`",`"Action`":[`"eks:DescribeCluster`",`"eks:ListClusters`",`"eks:AccessKubernetesApi`"],`"Resource`":`"*`"},{`"Effect`":`"Allow`",`"Action`":[`"logs:CreateLogGroup`",`"logs:CreateLogStream`",`"logs:PutLogEvents`"],`"Resource`":`"*`"},{`"Effect`":`"Allow`",`"Action`":[`"sns:Publish`"],`"Resource`":`"$SNS_ARN`"},{`"Effect`":`"Allow`",`"Action`":[`"s3:GetObject`",`"s3:PutObject`",`"s3:GetBucketAcl`",`"s3:GetBucketLocation`"],`"Resource`":`"*`"}]}" + + aws iam put-role-policy --role-name $ROLE_CODEBUILD ` + --policy-name "VotingAppCodeBuildPolicy" ` + --policy-document $cbPolicy | Out-Null + OK "CodeBuild role created: $ROLE_CODEBUILD" +} else { + OK "CodeBuild role already exists" +} + +# EKS Node trust policy +$eksTrust = '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}' + +$eksExists = aws iam get-role --role-name $ROLE_EKS_NODE 2>&1 +if ($LASTEXITCODE -ne 0) { + aws iam create-role --role-name $ROLE_EKS_NODE ` + --assume-role-policy-document $eksTrust ` + --description "EKS Node role for Voting App" | Out-Null + aws iam attach-role-policy --role-name $ROLE_EKS_NODE --policy-arn arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy | Out-Null + aws iam attach-role-policy --role-name $ROLE_EKS_NODE --policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly | Out-Null + aws iam attach-role-policy --role-name $ROLE_EKS_NODE --policy-arn arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy | Out-Null + aws iam attach-role-policy --role-name $ROLE_EKS_NODE --policy-arn arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy | Out-Null + aws iam attach-role-policy --role-name $ROLE_EKS_NODE --policy-arn arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy | Out-Null + OK "EKS node role created: $ROLE_EKS_NODE" +} else { + OK "EKS node role already exists" +} +aws iam attach-role-policy --role-name $ROLE_EKS_NODE --policy-arn arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy | Out-Null + +# ── STEP 4: EKS Cluster ─────────────────────────── +Log "[4/8] Creating EKS cluster (this takes 15-20 minutes)..." +$clusterExists = aws eks describe-cluster --name $CLUSTER_NAME --region $REGION 2>&1 +if ($LASTEXITCODE -ne 0) { + # EKS control-plane role -- uses the existing AccountFullAccessRole + # Ensure this role has a trust policy allowing eks.amazonaws.com to assume it + $EKS_CONTROL_PLANE_ROLE = "arn:aws:iam::${ACCOUNT_ID}:role/AccountFullAccessRole" + + $SUBNET_IDS_CLUSTER = (aws ec2 describe-subnets ` + --filters "Name=default-for-az,Values=true" ` + --region $REGION ` + --query "Subnets[*].SubnetId" ` + --output text) -replace '\s+', ',' + + aws eks create-cluster ` + --name $CLUSTER_NAME ` + --region $REGION ` + --kubernetes-version "1.31" ` + --role-arn $EKS_CONTROL_PLANE_ROLE ` + --resources-vpc-config "subnetIds=${SUBNET_IDS_CLUSTER},endpointPublicAccess=true,endpointPrivateAccess=false" ` + --logging '{"clusterLogging":[{"types":["api","audit","authenticator","controllerManager","scheduler"],"enabled":true}]}' | Out-Null + Log " Waiting for cluster to become ACTIVE..." + aws eks wait cluster-active --name $CLUSTER_NAME --region $REGION + OK "EKS cluster active!" +} else { + OK "EKS cluster already exists" +} + +# ── STEP 5: Node Group ──────────────────────────── +Log "[5/8] Creating managed node group..." +$nodeGroupExists = aws eks describe-nodegroup --cluster-name $CLUSTER_NAME --nodegroup-name "voting-app-nodes" --region $REGION 2>&1 +if ($LASTEXITCODE -ne 0) { + $NODE_ROLE_ARN = (aws iam get-role --role-name $ROLE_EKS_NODE --query "Role.Arn" --output text) + $SUBNET_IDS = (aws ec2 describe-subnets --filters "Name=default-for-az,Values=true" --region $REGION --query "Subnets[*].SubnetId" --output text) -replace '\s+',',' + + aws eks create-nodegroup ` + --cluster-name $CLUSTER_NAME ` + --nodegroup-name "voting-app-nodes" ` + --region $REGION ` + --node-role $NODE_ROLE_ARN ` + --subnets ($SUBNET_IDS -split ',') ` + --instance-types "t3.micro" ` + --scaling-config "minSize=3,maxSize=4,desiredSize=3" ` + --disk-size 20 ` + --ami-type AL2_x86_64 | Out-Null + + Log " Waiting for node group to become ACTIVE (5-10 min)..." + aws eks wait nodegroup-active --cluster-name $CLUSTER_NAME --nodegroup-name "voting-app-nodes" --region $REGION + OK "Node group active!" +} else { + OK "Node group already exists" +} + +# ── STEP 6: CloudWatch Container Insights ──────── +Log "[6/9] Enabling CloudWatch Container Insights on EKS..." +# Ensure the node role has CloudWatch permissions (already attached above) +# Enable via EKS managed addon (amazon-cloudwatch-observability) +$cwAddon = aws eks describe-addon --cluster-name $CLUSTER_NAME --addon-name amazon-cloudwatch-observability --region $REGION 2>&1 +if ($LASTEXITCODE -ne 0) { + aws eks create-addon ` + --cluster-name $CLUSTER_NAME ` + --addon-name amazon-cloudwatch-observability ` + --region $REGION | Out-Null + OK "CloudWatch Container Insights addon created" +} else { + OK "CloudWatch Container Insights addon already exists" +} + +# EBS CSI driver provisions the persistent volume used by Postgres. +Log "[6b/9] Enabling EBS CSI driver..." +$ebsCsiAddon = aws eks describe-addon --cluster-name $CLUSTER_NAME --addon-name aws-ebs-csi-driver --region $REGION 2>&1 +if ($LASTEXITCODE -ne 0) { + aws eks create-addon ` + --cluster-name $CLUSTER_NAME ` + --addon-name aws-ebs-csi-driver ` + --resolve-conflicts OVERWRITE ` + --region $REGION | Out-Null + OK "EBS CSI driver addon created" +} else { + OK "EBS CSI driver addon already exists" +} + +# ── STEP 7: ALB Controller IAM Policy and Role ─── +Log "[7/9] Creating ALB Controller IAM policy and role..." +$LBC_POLICY_NAME = "AWSLoadBalancerControllerIAMPolicy" +$LBC_ROLE_NAME = "AmazonEKSLoadBalancerControllerRole" + +$policyExists = aws iam get-policy --policy-arn "arn:aws:iam::${ACCOUNT_ID}:policy/$LBC_POLICY_NAME" 2>&1 +if ($LASTEXITCODE -ne 0) { + $policyDoc = (Invoke-WebRequest -Uri "https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.8.1/docs/install/iam_policy.json" -UseBasicParsing).Content + $policyDoc | Out-File -FilePath "$env:TEMP\lbc-policy.json" -Encoding utf8 + aws iam create-policy ` + --policy-name $LBC_POLICY_NAME ` + --policy-document "file://$env:TEMP\lbc-policy.json" ` + --region $REGION | Out-Null + OK "LBC IAM policy created" +} else { + OK "LBC IAM policy already exists" +} + +# OIDC provider (needed for IRSA) +$OIDC_URL = (aws eks describe-cluster --name $CLUSTER_NAME --region $REGION --query "cluster.identity.oidc.issuer" --output text) +$OIDC_ID = $OIDC_URL -replace "https://", "" -replace ".*/", "" +$oidcExists = aws iam list-open-id-connect-providers --query "OpenIDConnectProviderList[?ends_with(Arn,'$OIDC_ID')].Arn" --output text +if (-not $oidcExists) { + # eksctl must be available or use AWS CLI equivalent + # eksctl utils associate-iam-oidc-provider --cluster $CLUSTER_NAME --region $REGION --approve + # Alternatively the buildspec handles this -- flag for manual step + Warn "OIDC provider not yet associated -- the buildspec will handle this on first build." + Warn "Or run: eksctl utils associate-iam-oidc-provider --cluster $CLUSTER_NAME --region $REGION --approve" +} else { + OK "OIDC provider already associated: $oidcExists" +} + +$lbcRoleExists = aws iam get-role --role-name $LBC_ROLE_NAME 2>&1 +if ($LASTEXITCODE -ne 0) { + $OIDC_PROVIDER = ($OIDC_URL -replace "https://", "") + $lbcTrust = "{`"Version`":`"2012-10-17`",`"Statement`":[{`"Effect`":`"Allow`",`"Principal`":{`"Federated`":`"arn:aws:iam::${ACCOUNT_ID}:oidc-provider/${OIDC_PROVIDER}`"},`"Action`":`"sts:AssumeRoleWithWebIdentity`",`"Condition`":{`"StringEquals`":{`"${OIDC_PROVIDER}:aud`":`"sts.amazonaws.com`",`"${OIDC_PROVIDER}:sub`":`"system:serviceaccount:kube-system:aws-load-balancer-controller`"}}}]}" + aws iam create-role ` + --role-name $LBC_ROLE_NAME ` + --assume-role-policy-document $lbcTrust ` + --description "IRSA role for AWS Load Balancer Controller" | Out-Null + aws iam attach-role-policy ` + --role-name $LBC_ROLE_NAME ` + --policy-arn "arn:aws:iam::${ACCOUNT_ID}:policy/$LBC_POLICY_NAME" | Out-Null + OK "LBC IAM role created: $LBC_ROLE_NAME" +} else { + OK "LBC IAM role already exists" +} + +# ── STEP 8: CodeBuild Artifacts S3 Bucket ──────── +Log "[8/9] Creating CodeBuild artifacts bucket..." +$ARTIFACTS_BUCKET = "voting-app-artifacts-$ACCOUNT_ID" +$bucketExists = aws s3api head-bucket --bucket $ARTIFACTS_BUCKET --region $REGION 2>&1 +if ($LASTEXITCODE -ne 0) { + aws s3api create-bucket --bucket $ARTIFACTS_BUCKET --region $REGION ` + --create-bucket-configuration LocationConstraint=$REGION | Out-Null + OK "S3 artifacts bucket created: $ARTIFACTS_BUCKET" +} else { + OK "S3 bucket already exists" +} + +# ── STEP 9: CodeBuild Project ───────────────────── +Log "[9/9] Creating CodeBuild project..." +$CB_ROLE_ARN = (aws iam get-role --role-name $ROLE_CODEBUILD --query "Role.Arn" --output text) + +$projectExists = aws codebuild batch-get-projects --names $CODEBUILD_PROJECT --region $REGION --query "projects[0].name" --output text 2>&1 +if ($projectExists -ne $CODEBUILD_PROJECT) { + $projectJson = @" +{ + "name": "$CODEBUILD_PROJECT", + "description": "Build and deploy Voting App to EKS", + "source": { + "type": "GITHUB", + "location": "$GITHUB_REPO_URL", + "buildspec": "aws/buildspec.yml", + "gitCloneDepth": 1 + }, + "artifacts": { + "type": "NO_ARTIFACTS" + }, + "environment": { + "type": "LINUX_CONTAINER", + "image": "aws/codebuild/standard:7.0", + "computeType": "BUILD_GENERAL1_MEDIUM", + "privilegedMode": true, + "environmentVariables": [ + {"name": "AWS_REGION", "value": "$REGION"}, + {"name": "ACCOUNT_ID", "value": "$ACCOUNT_ID"}, + {"name": "CLUSTER_NAME", "value": "$CLUSTER_NAME"}, + {"name": "ECR_BASE", "value": "$ECR_BASE"}, + {"name": "SNS_ARN", "value": "$SNS_ARN"} + ] + }, + "serviceRole": "$CB_ROLE_ARN", + "logsConfig": { + "cloudWatchLogs": { + "status": "ENABLED", + "groupName": "/aws/codebuild/voting-app-build" + } + } +} +"@ + $projectJson | Out-File -FilePath "$env:TEMP\cb-project.json" -Encoding utf8 + aws codebuild create-project --cli-input-json "file://$env:TEMP\cb-project.json" --region $REGION | Out-Null + OK "CodeBuild project created: $CODEBUILD_PROJECT" +} else { + OK "CodeBuild project already exists" +} + +# ── Trigger Build ───────────────────────────────── +Log "[Build] Triggering first build..." +$BUILD_ID = (aws codebuild start-build --project-name $CODEBUILD_PROJECT --region $REGION --query "build.id" --output text) +OK "Build started: $BUILD_ID" + +# Summary +Write-Host "" +Write-Host "================================================" -ForegroundColor Cyan +Write-Host " DEPLOYMENT INITIATED!" -ForegroundColor Green +Write-Host "================================================" -ForegroundColor Cyan +Write-Host " EKS Cluster : $CLUSTER_NAME" -ForegroundColor White +Write-Host " CodeBuild : $CODEBUILD_PROJECT" -ForegroundColor White +Write-Host " Build ID : $BUILD_ID" -ForegroundColor White +Write-Host " ECR Base : $ECR_BASE" -ForegroundColor White +Write-Host " SNS Alerts : $SNS_EMAIL" -ForegroundColor White +Write-Host "" +Write-Host " Monitor build:" -ForegroundColor Yellow +Write-Host " https://$REGION.console.aws.amazon.com/codesuite/codebuild/projects/$CODEBUILD_PROJECT/history" -ForegroundColor Yellow +Write-Host "" +Write-Host " NOTE: Build takes ~10 min. Vote and Result URLs" -ForegroundColor Magenta +Write-Host " will be printed in the build logs when ready." -ForegroundColor Magenta +Write-Host " Check $SNS_EMAIL for deployment notification." -ForegroundColor Magenta diff --git a/aws/k8s/configmap.yaml b/aws/k8s/configmap.yaml new file mode 100644 index 0000000000..dbed625c6e --- /dev/null +++ b/aws/k8s/configmap.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config + namespace: voting +data: + # Non-sensitive application config + # Database credentials are stored in postgres-secret (Secret) + REDIS_HOST: "redis" + DB_HOST: "db" diff --git a/aws/k8s/db-deployment.yaml b/aws/k8s/db-deployment.yaml new file mode 100644 index 0000000000..135e7e9b32 --- /dev/null +++ b/aws/k8s/db-deployment.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: db + namespace: voting +spec: + replicas: 1 + selector: + matchLabels: + app: db + template: + metadata: + labels: + app: db + spec: + containers: + - name: postgres + image: postgres:15-alpine + ports: + - containerPort: 5432 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_DB + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + subPath: postgres + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + livenessProbe: + exec: + command: ["pg_isready", "-U", "postgres"] + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + exec: + command: ["pg_isready", "-U", "postgres"] + initialDelaySeconds: 10 + periodSeconds: 5 + volumes: + - name: postgres-data + persistentVolumeClaim: + claimName: postgres-pvc diff --git a/aws/k8s/db-pvc.yaml b/aws/k8s/db-pvc.yaml new file mode 100644 index 0000000000..4750207ac0 --- /dev/null +++ b/aws/k8s/db-pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgres-pvc + namespace: voting +spec: + accessModes: + - ReadWriteOnce + storageClassName: gp2 + resources: + requests: + storage: 5Gi diff --git a/aws/k8s/db-service.yaml b/aws/k8s/db-service.yaml new file mode 100644 index 0000000000..9b9768ca06 --- /dev/null +++ b/aws/k8s/db-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: db + namespace: voting +spec: + type: ClusterIP + ports: + - port: 5432 + targetPort: 5432 + selector: + app: db \ No newline at end of file diff --git a/aws/k8s/namespace.yaml b/aws/k8s/namespace.yaml new file mode 100644 index 0000000000..720292e65d --- /dev/null +++ b/aws/k8s/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: voting + labels: + app: voting-app diff --git a/aws/k8s/postgres-secret.yaml b/aws/k8s/postgres-secret.yaml new file mode 100644 index 0000000000..43f3241d35 --- /dev/null +++ b/aws/k8s/postgres-secret.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Secret +metadata: + name: postgres-secret + namespace: voting +type: Opaque +# Values are base64-encoded. +# These are intentional demo defaults (postgres/postgres/votes) suitable for +# development and testing. For production, replace with strong credentials +# and consider using AWS Secrets Manager with the Secrets Store CSI driver. +# To regenerate: echo -n "yourvalue" | base64 +data: + POSTGRES_USER: cG9zdGdyZXM= + POSTGRES_PASSWORD: cG9zdGdyZXM= + POSTGRES_DB: dm90ZXM= diff --git a/aws/k8s/redis-deployment.yaml b/aws/k8s/redis-deployment.yaml new file mode 100644 index 0000000000..f7e0b1ebde --- /dev/null +++ b/aws/k8s/redis-deployment.yaml @@ -0,0 +1,37 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + namespace: voting +spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:7-alpine + ports: + - containerPort: 6379 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + livenessProbe: + tcpSocket: + port: 6379 + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + tcpSocket: + port: 6379 + initialDelaySeconds: 5 + periodSeconds: 5 \ No newline at end of file diff --git a/aws/k8s/redis-service.yaml b/aws/k8s/redis-service.yaml new file mode 100644 index 0000000000..32cced2873 --- /dev/null +++ b/aws/k8s/redis-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: voting +spec: + type: ClusterIP + ports: + - port: 6379 + targetPort: 6379 + selector: + app: redis \ No newline at end of file diff --git a/aws/k8s/result-deployment.yaml b/aws/k8s/result-deployment.yaml new file mode 100644 index 0000000000..37582b9663 --- /dev/null +++ b/aws/k8s/result-deployment.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: result + namespace: voting +spec: + replicas: 2 + selector: + matchLabels: + app: result + template: + metadata: + labels: + app: result + spec: + containers: + - name: result + image: 888635225506.dkr.ecr.ap-southeast-2.amazonaws.com/voting-app/result:latest + ports: + - containerPort: 80 + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 15 + periodSeconds: 15 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 5 \ No newline at end of file diff --git a/aws/k8s/result-service.yaml b/aws/k8s/result-service.yaml new file mode 100644 index 0000000000..7572898a78 --- /dev/null +++ b/aws/k8s/result-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: result + namespace: voting + annotations: + service.beta.kubernetes.io/aws-load-balancer-type: "nlb" +spec: + type: LoadBalancer + ports: + - port: 80 + targetPort: 80 + selector: + app: result diff --git a/aws/k8s/vote-deployment.yaml b/aws/k8s/vote-deployment.yaml new file mode 100644 index 0000000000..8b6390f99d --- /dev/null +++ b/aws/k8s/vote-deployment.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vote + namespace: voting +spec: + replicas: 2 + selector: + matchLabels: + app: vote + template: + metadata: + labels: + app: vote + spec: + containers: + - name: vote + image: 888635225506.dkr.ecr.ap-southeast-2.amazonaws.com/voting-app/vote:latest + ports: + - containerPort: 80 + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 15 + periodSeconds: 15 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 5 \ No newline at end of file diff --git a/aws/k8s/vote-service.yaml b/aws/k8s/vote-service.yaml new file mode 100644 index 0000000000..117e4c2383 --- /dev/null +++ b/aws/k8s/vote-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: vote + namespace: voting + annotations: + service.beta.kubernetes.io/aws-load-balancer-type: "nlb" +spec: + type: LoadBalancer + ports: + - port: 80 + targetPort: 80 + selector: + app: vote diff --git a/aws/k8s/worker-deployment.yaml b/aws/k8s/worker-deployment.yaml new file mode 100644 index 0000000000..830f6fa706 --- /dev/null +++ b/aws/k8s/worker-deployment.yaml @@ -0,0 +1,25 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: worker + namespace: voting +spec: + replicas: 1 + selector: + matchLabels: + app: worker + template: + metadata: + labels: + app: worker + spec: + containers: + - name: worker + image: 888635225506.dkr.ecr.ap-southeast-2.amazonaws.com/voting-app/worker:latest + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi \ No newline at end of file diff --git a/aws/poll-build.ps1 b/aws/poll-build.ps1 new file mode 100644 index 0000000000..5ec55d0744 --- /dev/null +++ b/aws/poll-build.ps1 @@ -0,0 +1,29 @@ +# Poll CodeBuild status and print URLs when done +param([string]$BuildId) + +$REGION = "ap-southeast-2" + +if (-not $BuildId) { + $BuildId = (aws codebuild list-builds-for-project --project-name "voting-app-build" --region $REGION --query "ids[0]" --output text) + Write-Host "Polling latest build: $BuildId" +} + +$maxAttempts = 40 +$attempt = 1 +while ($attempt -le $maxAttempts) { + $status = (aws codebuild batch-get-builds --ids $BuildId --region $REGION --query "builds[0].buildStatus" --output text) + $phase = (aws codebuild batch-get-builds --ids $BuildId --region $REGION --query "builds[0].currentPhase" --output text) + Write-Host "[$attempt/$maxAttempts] $((Get-Date).ToString('HH:mm:ss')) Status: $status | Phase: $phase" + if ($status -eq "SUCCEEDED") { + Write-Host "BUILD SUCCEEDED!" -ForegroundColor Green + Write-Host "Check build logs for Vote and Result URLs." + Write-Host "https://$REGION.console.aws.amazon.com/codesuite/codebuild/projects/voting-app-build/history" + break + } elseif ($status -eq "FAILED" -or $status -eq "STOPPED") { + Write-Host "BUILD $status -- check logs:" -ForegroundColor Red + Write-Host "https://$REGION.console.aws.amazon.com/codesuite/codebuild/projects/voting-app-build/history" + break + } + $attempt++ + Start-Sleep -Seconds 30 +} \ No newline at end of file