Skip to content
CI/CD for .NET on AWS: CodePipeline, CodeBuild, and Blue-Green Deployments on ECS

CI/CD for .NET on AWS: CodePipeline, CodeBuild, and Blue-Green Deployments on ECS

1 CI/CD for .NET on AWS: CodePipeline, CodeBuild, and Blue-Green Deployments on ECS

Modern .NET delivery has moved far beyond copying build outputs to IIS servers. A production-grade CI/CD pipeline for .NET on AWS should build consistently, test early, scan aggressively, publish immutable artifacts, and deploy with controlled traffic shifting. For teams running ASP.NET Core APIs, worker services, or microservices, the common target is now Amazon ECS with AWS Fargate, Amazon ECR, AWS CodeBuild, AWS CodePipeline, and AWS CodeDeploy.

This article covers sections 1–3 of the requested outline: the architecture blueprint, container foundation, and CodeBuild-based DevSecOps build engine. The structure follows the provided execution brief and outline.

1.1 The Evolution of .NET Deployment: From Web Deploy to Containerized Microservices

Traditional .NET deployment usually followed a familiar path: build in Visual Studio or a build server, package with MSBuild or Web Deploy, copy to IIS, update configuration, recycle the application pool, and hope the deployment window was quiet. That model worked for many internal applications, but it had predictable problems:

  • Servers drifted over time.
  • Rollbacks were manual.
  • Environment-specific configuration lived too close to the application.
  • Deployment success depended on machine state.
  • Scaling required more VM-level operations.
  • Release windows became operational events instead of routine engineering work.

Modern ASP.NET Core changed the deployment model. The application can run cross-platform, package cleanly into containers, and expose health endpoints that orchestrators can understand. Instead of treating servers as long-lived deployment targets, the pipeline produces a versioned container image and the runtime platform replaces tasks safely.

A typical modern flow looks like this:

Git commit
  -> restore, build, test
  -> static analysis and dependency scanning
  -> Docker image build
  -> container scan
  -> push image to Amazon ECR
  -> update ECS task definition
  -> deploy through CodeDeploy blue-green strategy
  -> shift traffic through ALB
  -> validate health and rollback if needed

The key improvement is immutability. The same image that passes tests is the image deployed to staging and production. Environment differences move into infrastructure, secrets, runtime configuration, and deployment parameters.

1.2 Why AWS for .NET? Evaluating the Synergy between .NET 9+ and AWS Graviton

.NET is no longer a Windows-only platform. ASP.NET Core runs well on Linux containers, and modern .NET applications can target both x64 and Arm64. That matters on AWS because ECS and Fargate can run container workloads on AWS Graviton-based infrastructure.

AWS positions Graviton processors as Arm-based processors designed for strong price-performance across cloud workloads, and Graviton4 is the current high-end generation for several EC2 families. AWS has published performance claims such as up to 30% better performance for Graviton4 R8g compared with Graviton3 R7g in memory-optimized EC2 instances, and RDS Graviton4 benchmark posts cite up to 40% performance improvement and up to 29% better price-performance versus Graviton3 in tested database scenarios. Those numbers are workload-specific, so architects should benchmark their own .NET services before making a fleet-wide decision.

For .NET teams, the practical point is simple: build multi-architecture images and keep the deployment platform flexible. A service can run on AMD64 today and move to ARM64 later without changing application code, assuming dependencies support Arm64.

The trade-off is operational testing. Native libraries, third-party agents, image scanning tools, and APM extensions must be validated on both architectures. For plain ASP.NET Core APIs, the transition is often smooth. For applications using native PDF libraries, image processing packages, legacy ODBC drivers, or proprietary security agents, test carefully.

1.3 Architecture Overview: Decoupling Build, Deploy, and Infrastructure-as-Code

A clean .NET CI/CD architecture separates three concerns:

Application build
  Produces tested binaries and container images.

Deployment orchestration
  Promotes a known image into a target environment.

Infrastructure-as-Code
  Defines ECS clusters, services, ALB listeners, IAM roles, ECR repositories, alarms, and pipeline resources.

Avoid mixing these concerns in one large script. A common mistake is allowing the build job to directly mutate production infrastructure. That works initially, but it becomes hard to audit and harder to roll back.

A better architecture uses AWS CDK for .NET or another IaC tool to define infrastructure. The pipeline can then reference existing resources: ECR repositories, ECS services, CodeDeploy applications, deployment groups, CloudWatch alarms, and IAM roles.

Recommended separation:

infra/
  Defines network, ECS, ALB, ECR, CodePipeline, CodeBuild, CodeDeploy.

src/
  Contains .NET application code and Dockerfile.

pipelines/
  Contains buildspec.yml, deployment templates, validation scripts.

tests/
  Contains unit, integration, contract, and smoke tests.

This layout keeps infrastructure evolution independent from application releases. Infrastructure changes can still flow through CI/CD, but they should be reviewed and promoted deliberately.

1.4 The Decision Matrix: ECS Fargate vs. EKS for .NET Workloads

For most .NET teams starting containerized delivery on AWS, ECS Fargate is the simpler default. It removes node management, integrates cleanly with ALB, CloudWatch, IAM, ECR, CodeDeploy, and supports blue-green deployments through AWS-native tooling.

EKS is a better fit when the organization already has Kubernetes platform engineering maturity or needs Kubernetes-specific capabilities such as custom controllers, service mesh patterns, Kubernetes-native operators, or multi-cloud portability.

A practical decision matrix:

FactorECS FargateEKS
Operational complexityLowerHigher
Kubernetes expertise requiredNoYes
AWS-native integrationStrongStrong, but more components
Blue-green with CodeDeployStraightforwardUsually handled through Kubernetes tooling
Platform flexibilityAWS-focusedKubernetes ecosystem
Best fit.NET APIs, workers, microservices with AWS-native operationsOrganizations standardizing on Kubernetes

Use ECS Fargate when the goal is reliable .NET delivery with low operational overhead. Use EKS when Kubernetes itself is a strategic platform requirement.

1.5 Strategic Objectives: Achieving Zero-Downtime and 15-Minute Lead Times

A strong CI/CD pipeline should optimize for lead time, deployment safety, and recovery speed. “Zero downtime” does not mean “no risk.” It means the system is designed so that a new version can be introduced without interrupting active traffic, and bad versions can be removed quickly.

For a practical .NET-on-ECS pipeline, target these objectives:

Commit to deployable artifact: under 10 minutes
Artifact to staging: under 5 minutes
Staging validation: automated
Production promotion: controlled approval
Rollback: automated through CodeDeploy and CloudWatch alarms

The 15-minute lead-time target depends on discipline:

  • Keep Docker layers cacheable.
  • Run fast unit tests before slow integration tests.
  • Fail early on formatting, build, and dependency issues.
  • Use parallel test execution where possible.
  • Avoid rebuilding the same artifact for each environment.
  • Promote image digests, not mutable tags.

The deployment goal is not only speed. It is repeatability. A slower pipeline that is deterministic is better than a fast pipeline that occasionally deploys unverified code.


2 The Foundation: Containerizing .NET Microservices for AWS

Containers are the contract between the build system and the runtime platform. A weak Dockerfile creates slow builds, large images, avoidable CVEs, and inconsistent behavior. A strong Dockerfile makes the pipeline faster and production safer.

2.1 Crafting Production-Ready Dockerfiles for ASP.NET Core

A production Dockerfile for ASP.NET Core should do five things well:

  • Restore dependencies efficiently.
  • Build and publish from a clean SDK image.
  • Run from a smaller runtime image.
  • Avoid root where possible.
  • Expose only what the application needs.

Recommended example:

# syntax=docker/dockerfile:1

FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src

COPY Directory.Packages.props ./
COPY src/Orders.Api/Orders.Api.csproj src/Orders.Api/
COPY tests/Orders.Api.Tests/Orders.Api.Tests.csproj tests/Orders.Api.Tests/

RUN dotnet restore src/Orders.Api/Orders.Api.csproj

COPY . .
RUN dotnet test tests/Orders.Api.Tests/Orders.Api.Tests.csproj \
    --configuration Release \
    --no-restore

RUN dotnet publish src/Orders.Api/Orders.Api.csproj \
    --configuration Release \
    --no-restore \
    --output /app/publish \
    /p:UseAppHost=false

FROM mcr.microsoft.com/dotnet/aspnet:9.0-noble-chiseled AS runtime
WORKDIR /app

COPY --from=build /app/publish .

ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080

USER $APP_UID

ENTRYPOINT ["dotnet", "Orders.Api.dll"]

This structure keeps build tools out of the runtime image. It also improves Docker layer reuse because project files are copied before the full source tree.

2.1.1 Multi-stage Builds for Minimal Attack Surface

A common incorrect pattern is using the SDK image in production:

# Incorrect
FROM mcr.microsoft.com/dotnet/sdk:9.0
WORKDIR /app
COPY . .
RUN dotnet publish -c Release -o out
ENTRYPOINT ["dotnet", "out/Orders.Api.dll"]

This image includes unnecessary build tooling. It is larger, slower to pull, and has more packages to patch.

A better pattern separates build and runtime:

# Recommended
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish src/Orders.Api/Orders.Api.csproj -c Release -o /out

FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
WORKDIR /app
COPY --from=build /out .
ENTRYPOINT ["dotnet", "Orders.Api.dll"]

The runtime image should contain only the published application and runtime dependencies.

2.1.2 Using Chiseled Ubuntu Images for Enhanced Security

Microsoft and Canonical introduced .NET chiseled Ubuntu images for production use. These images are designed to be smaller and more locked down than full Ubuntu runtime images. Microsoft’s container registry documentation also describes .NET distroless-style images, including Ubuntu Chiseled images, as having a minimal package set, no package manager, no shell, and non-root defaults in supported variants.

The benefit is a smaller attack surface. The trade-off is troubleshooting. Since chiseled images do not include a shell or package manager, you cannot exec into the container and run ad hoc Linux commands in the same way. Production teams should compensate with:

  • structured application logs,
  • health endpoints,
  • OpenTelemetry traces,
  • diagnostic sidecars where appropriate,
  • separate debug images for controlled troubleshooting.

Use chiseled images for production APIs when dependencies support them. Use standard runtime images during migration if you need shell-level debugging.

2.2 Configuration Management: Environment Variables vs. AWS AppConfig

Environment variables are the simplest runtime configuration mechanism for ECS tasks. They work well for stable settings:

{
  "ASPNETCORE_ENVIRONMENT": "Production",
  "Logging__LogLevel__Default": "Information",
  "FeatureFlags__EnableNewRouting": "false"
}

For secrets, do not use plain environment variables stored directly in task definitions. Use AWS Secrets Manager or SSM Parameter Store references.

Environment variables are best for:

  • ASP.NET Core environment name,
  • log level defaults,
  • endpoint names,
  • non-secret feature toggles,
  • static runtime settings.

AWS AppConfig is better when configuration changes need validation, staged rollout, and rollback. For example, a fraud scoring threshold or feature flag can be rolled out gradually without building a new container image.

A useful rule:

If the value changes only during deployment, use ECS task configuration.
If the value may change operationally after deployment, consider AWS AppConfig.
If the value is sensitive, use Secrets Manager or secure parameter references.

2.3 Local Development Parity: Using Testcontainers for .NET and LocalStack

Local parity does not mean recreating AWS exactly on a laptop. It means giving developers enough realistic dependencies to catch integration issues before CodeBuild.

Testcontainers for .NET is useful for integration tests that need real infrastructure-like dependencies:

using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Containers;

public sealed class PostgresFixture : IAsyncLifetime
{
    private readonly IContainer _postgres = new ContainerBuilder()
        .WithImage("postgres:16")
        .WithEnvironment("POSTGRES_USER", "app")
        .WithEnvironment("POSTGRES_PASSWORD", "app")
        .WithEnvironment("POSTGRES_DB", "orders")
        .WithPortBinding(5432, true)
        .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(5432))
        .Build();

    public string ConnectionString =>
        $"Host=localhost;Port={_postgres.GetMappedPublicPort(5432)};Database=orders;Username=app;Password=app";

    public Task InitializeAsync() => _postgres.StartAsync();

    public Task DisposeAsync() => _postgres.DisposeAsync().AsTask();
}

For AWS service simulation, LocalStack can help test interactions with services such as SQS, SNS, or S3. Use it for developer feedback, not as a replacement for staging tests against real AWS services.

The better testing model is layered:

Unit tests
  Fast, no external dependencies.

Integration tests
  Testcontainers for databases and messaging dependencies.

Contract tests
  Validate API contracts and event schemas.

Staging smoke tests
  Run against real AWS-deployed services.

2.4 Handling Large-Scale .NET Dependencies and NuGet Caching Strategies in Containers

Large enterprise .NET solutions often suffer from slow restores. The problem is usually not only NuGet download time. It is poor Docker layer structure.

Incorrect:

COPY . .
RUN dotnet restore

Any source change invalidates the restore layer.

Better:

COPY Directory.Packages.props ./
COPY NuGet.config ./
COPY src/Orders.Api/Orders.Api.csproj src/Orders.Api/
COPY src/Orders.Domain/Orders.Domain.csproj src/Orders.Domain/
RUN dotnet restore src/Orders.Api/Orders.Api.csproj

COPY . .
RUN dotnet publish src/Orders.Api/Orders.Api.csproj -c Release -o /out --no-restore

For CodeBuild, combine good Docker layering with caching. AWS CodeBuild supports S3 caching and local caching modes, including source cache, Docker layer cache, and custom cache. Docker layer cache is specifically intended for projects that build or pull large Docker images, but it requires privileged mode for Linux container builds.


3 The Build Engine: AWS CodeBuild and DevSecOps Integration

CodeBuild is where the pipeline turns source code into a verified artifact. Treat it as more than a compiler. It should test, scan, package, tag, and publish.

3.1 Designing the buildspec.yml: Parallelism and Custom Build Environments

A practical buildspec.yml for .NET should be explicit about phases, fail early, and produce deployment artifacts for CodePipeline.

version: 0.2

env:
  variables:
    DOTNET_CLI_TELEMETRY_OPTOUT: "1"
    PROJECT_PATH: "src/Orders.Api/Orders.Api.csproj"
    IMAGE_REPO_NAME: "orders-api"
  exported-variables:
    - IMAGE_TAG
    - IMAGE_URI

phases:
  install:
    runtime-versions:
      dotnet: 9.0
    commands:
      - dotnet --info
      - echo "Installing security tools"
      - curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin

  pre_build:
    commands:
      - IMAGE_TAG=${CODEBUILD_RESOLVED_SOURCE_VERSION:0:12}
      - ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
      - REGION=${AWS_DEFAULT_REGION}
      - IMAGE_URI=$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      - aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com
      - dotnet restore $PROJECT_PATH

  build:
    commands:
      - dotnet build $PROJECT_PATH --configuration Release --no-restore
      - dotnet test --configuration Release --no-build --logger trx
      - docker build -t $IMAGE_URI .

  post_build:
    commands:
      - trivy image --severity HIGH,CRITICAL --exit-code 1 $IMAGE_URI
      - docker push $IMAGE_URI
      - printf '[{"name":"orders-api","imageUri":"%s"}]' $IMAGE_URI > imagedefinitions.json

artifacts:
  files:
    - imagedefinitions.json

For larger systems, split work into multiple CodeBuild actions or batch builds:

Build job 1: compile and unit tests
Build job 2: integration tests
Build job 3: container build and scan
Build job 4: IaC synth/validate

This makes failures easier to isolate.

3.2 Integration of Open-Source Security Tools

Security checks should run before images reach production. The goal is not to block every low-risk issue. The goal is to stop known dangerous vulnerabilities, leaked secrets, and unsafe dependencies before deployment.

A reasonable baseline:

Gitleaks
  Secret scanning

dotnet list package --vulnerable
  NuGet dependency vulnerability visibility

SonarQube or SonarCloud
  Static analysis and code quality gates

Trivy or Snyk
  Container image and OS package scanning

3.2.1 Static Analysis: SonarQube or SonarCloud Integration

For SonarQube, a .NET build usually wraps build and test commands:

dotnet tool install --global dotnet-sonarscanner

dotnet sonarscanner begin \
  /k:"orders-api" \
  /d:sonar.host.url="$SONAR_HOST_URL" \
  /d:sonar.token="$SONAR_TOKEN" \
  /d:sonar.cs.vstest.reportsPaths="**/*.trx"

dotnet build src/Orders.Api/Orders.Api.csproj --configuration Release

dotnet test tests/Orders.Api.Tests/Orders.Api.Tests.csproj \
  --configuration Release \
  --logger trx

dotnet sonarscanner end /d:sonar.token="$SONAR_TOKEN"

Store tokens in Secrets Manager or CodeBuild secure environment variables, not in buildspec.yml.

3.2.2 Container Scanning: Implementing Trivy or Snyk within CodeBuild

Trivy is commonly used because it is easy to run in CI:

trivy image \
  --severity HIGH,CRITICAL \
  --ignore-unfixed \
  --exit-code 1 \
  "$IMAGE_URI"

Be careful with --ignore-unfixed. It reduces noise when no patch exists, but some organizations prefer visibility over pass/fail behavior. A practical approach is:

Dev branch:
  Report high and critical issues.

Main branch:
  Fail on critical issues.

Release branch:
  Fail on high and critical issues unless approved exception exists.

This avoids teaching teams to ignore scanners because they are too noisy.

3.2.3 Software Composition Analysis (SCA): Identifying Vulnerable NuGet Packages

.NET has built-in package vulnerability reporting:

dotnet list src/Orders.Api/Orders.Api.csproj package --vulnerable --include-transitive

Use it as a fast early check. For enterprise governance, pair it with SCA platforms that provide policy management, exception workflows, and license checks.

Recommended package governance:

Use Central Package Management.
Pin versions deliberately.
Avoid floating production dependencies.
Review transitive dependency changes during pull requests.
Fail builds for known critical vulnerabilities.

3.3 Pushing to Amazon ECR: Multi-Architecture ARM64/AMD64 Image Support

Amazon ECR supports multi-architecture images through Docker manifest lists. A manifest list lets one image reference point to variants for different CPU architectures, such as AMD64 and ARM64. This is useful when ECS services may run on Graviton-based infrastructure or mixed environments.

Example with Docker Buildx:

docker buildx create --use

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t "$IMAGE_URI" \
  --push \
  .

Before adopting this, confirm that all runtime dependencies support ARM64. The most common failures appear in native dependencies, monitoring agents, and older Linux packages.

3.4 Artifact Versioning: SemVer 2.0 Implementation in the Pipeline

Container tags should be useful, but deployments should ultimately track immutable image digests. A practical tagging model uses both semantic and traceable tags:

orders-api:1.8.0
orders-api:1.8.0-build.247
orders-api:git-a1b2c3d4e5f6
orders-api:prod

Avoid deploying only latest. It is convenient but weak for audit and rollback.

Example version generation:

BASE_VERSION=$(cat VERSION)          # 1.8.0
SHORT_SHA=${CODEBUILD_RESOLVED_SOURCE_VERSION:0:12}
BUILD_NUMBER=${CODEBUILD_BUILD_NUMBER}

IMAGE_TAG="$BASE_VERSION-build.$BUILD_NUMBER-git.$SHORT_SHA"

Recommended promotion model:

Build once:
  orders-api:1.8.0-build.247-git.a1b2c3d4e5f6

Deploy to dev:
  same image digest

Promote to stage:
  same image digest

Promote to prod:
  same image digest

This removes an entire class of release problems where staging and production were built from the same commit but not from the same artifact.

Takeaways:

  • Use containers to make .NET deployments immutable and repeatable.
  • Prefer ECS Fargate for AWS-native .NET workloads unless Kubernetes is a strategic requirement.
  • Build production images with multi-stage Dockerfiles and minimal runtime bases.
  • Use CodeBuild as a DevSecOps gate, not only as a compiler.
  • Push versioned, scanned images to ECR.
  • Prepare for Graviton by supporting multi-architecture images, but benchmark before committing production workloads.

4 Orchestrating the Flow: Multi-Stage Pipelines with AWS CodePipeline

A good pipeline does not only run builds. It controls promotion. After the image is built, scanned, tagged, and pushed to Amazon ECR, AWS CodePipeline becomes the release coordinator across environments. The pipeline should make it clear which artifact is moving, which environment receives it, who approved it, and what validation happened before production. This section continues the same implementation path from the earlier build and container foundation.

4.1 Source Integration: Webhooks with GitHub/GitLab vs. Native CodeCommit

Most .NET teams using AWS today keep source code in GitHub, GitLab, Azure DevOps, or Bitbucket rather than AWS CodeCommit. CodePipeline supports third-party source providers through AWS CodeConnections, which can start a pipeline from repository events and retrieve source revisions through a managed connection. AWS documents GitHub and GitLab.com source actions through these connection resources, including GitHub App-based connections and GitLab provider connections.

A practical source stage should capture the exact commit and pass it forward as metadata. That commit should become part of the Docker tag, deployment record, and release notes.

{
  "SourceAction": {
    "Provider": "CodeStarSourceConnection",
    "RepositoryName": "platform/orders-api",
    "BranchName": "main",
    "DetectChanges": true,
    "OutputArtifact": "SourceOutput"
  }
}

Use webhook-triggered source actions for normal CI/CD. Use manual pipeline execution for hotfix replay, rollback testing, or controlled release branches. For enterprise teams, the important control is not the source provider itself. The important control is that pull requests, branch protection, signed commits, and required checks happen before CodePipeline receives a deployable revision.

4.2 Multi-Environment Design: The Dev → Stage → Prod Promotion Flow

The mistake to avoid is rebuilding for each environment. Dev, stage, and prod should receive the same image digest. If each environment rebuilds from the same commit, the team has three artifacts that appear equivalent but may differ because of package feed timing, Docker base image changes, or restore behavior.

A cleaner promotion model looks like this:

Source
  -> BuildAndScan
  -> DeployDev
  -> SmokeTestDev
  -> DeployStage
  -> IntegrationTestStage
  -> Approval
  -> DeployProd
  -> PostDeployValidation

Environment-specific values should be injected through ECS task definitions, Secrets Manager references, AppConfig, or parameterized IaC. The image should not know whether it is running in dev or production.

For example, the same container image can use different runtime configuration:

{
  "containerDefinitions": [
    {
      "name": "orders-api",
      "image": "111122223333.dkr.ecr.us-east-1.amazonaws.com/orders-api@sha256:abc123",
      "environment": [
        {
          "name": "ASPNETCORE_ENVIRONMENT",
          "value": "Stage"
        }
      ],
      "secrets": [
        {
          "name": "ConnectionStrings__OrdersDb",
          "valueFrom": "arn:aws:secretsmanager:us-east-1:111122223333:secret:stage/orders-db"
        }
      ]
    }
  ]
}

This keeps promotion honest. The artifact that passed staging is the artifact that enters production.

4.3 Manual Approval Gates: Implementing Slack/Microsoft Teams Notifications via AWS Lambda

Manual approvals should be used sparingly. They are useful before production, before regulated data changes, or before high-risk releases. They should not become a replacement for automated testing.

A common pattern is CodePipeline approval action plus Amazon SNS plus Lambda. CodePipeline pauses at the approval step. SNS triggers a Lambda function. Lambda sends a formatted message to Slack or Microsoft Teams with the commit, image tag, environment, release notes, and approval link.

import json
import os
import urllib.request

def handler(event, context):
    record = event["Records"][0]
    message = json.loads(record["Sns"]["Message"])

    approval = message.get("approval", {})
    pipeline = message.get("approval", {}).get("pipelineName", "unknown")
    stage = approval.get("stageName", "unknown")
    action = approval.get("actionName", "unknown")
    token = approval.get("token", "")

    text = {
        "text": (
            f"Production approval required\n"
            f"Pipeline: {pipeline}\n"
            f"Stage: {stage}\n"
            f"Action: {action}\n"
            f"Token: {token[:8]}..."
        )
    }

    req = urllib.request.Request(
        os.environ["WEBHOOK_URL"],
        data=json.dumps(text).encode("utf-8"),
        headers={"Content-Type": "application/json"}
    )

    urllib.request.urlopen(req)
    return {"statusCode": 200}

Do not place approval tokens, webhook URLs, or release secrets in code. Store them in Secrets Manager or encrypted Lambda environment variables. Also make the approval message useful. “Approve production?” is weak. “Approve orders-api 1.8.0-build.247 from commit a1b2c3d, tested in stage at 14:05 UTC” is much better.

4.4 Cross-Account Deployments: Managing Secrets and IAM Roles for Enterprise Scales

Enterprise AWS environments often separate accounts by function: shared services, dev, stage, prod, security, and logging. This reduces blast radius and gives security teams stronger control. It also means CodePipeline must assume roles across accounts.

A simple model is:

Tooling account
  Owns CodePipeline and CodeBuild.

Dev account
  Owns dev ECS service and dev secrets.

Stage account
  Owns stage ECS service and stage secrets.

Prod account
  Owns production ECS service, production ALB, and production secrets.

The pipeline role in the tooling account assumes deployment roles in each target account. The target role grants only the actions required for that environment.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111122223333:role/CodePipelineServiceRole"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Keep secrets account-local where possible. A production database password should not be readable by the dev deployment role. The pipeline can pass an image digest and task definition template, but production secrets should resolve inside the production account at runtime.

4.5 Pipeline-as-Code: Provisioning the Pipeline using AWS CDK for .NET

The pipeline should be defined as code for the same reason application infrastructure is defined as code: reviewability, repeatability, and recovery. AWS CDK for .NET lets teams write infrastructure in C#, which fits naturally for .NET teams.

A minimal CDK pipeline definition may look like this:

using Amazon.CDK;
using Amazon.CDK.AWS.CodeBuild;
using Amazon.CDK.AWS.CodePipeline;
using Amazon.CDK.AWS.CodePipeline.Actions;
using Constructs;

public class DeliveryPipelineStack : Stack
{
    public DeliveryPipelineStack(Construct scope, string id, IStackProps props = null)
        : base(scope, id, props)
    {
        var sourceOutput = new Artifact_("SourceOutput");
        var buildOutput = new Artifact_("BuildOutput");

        var buildProject = new PipelineProject(this, "OrdersApiBuild", new PipelineProjectProps
        {
            Environment = new BuildEnvironment
            {
                BuildImage = LinuxBuildImage.STANDARD_7_0,
                Privileged = true
            }
        });

        new Pipeline(this, "OrdersApiPipeline", new PipelineProps
        {
            Stages = new[]
            {
                new StageProps
                {
                    StageName = "Source",
                    Actions = new IAction[]
                    {
                        new CodeStarConnectionsSourceAction(new CodeStarConnectionsSourceActionProps
                        {
                            ActionName = "GitHub",
                            Owner = "platform",
                            Repo = "orders-api",
                            Branch = "main",
                            ConnectionArn = "arn:aws:codestar-connections:us-east-1:111122223333:connection/example",
                            Output = sourceOutput
                        })
                    }
                },
                new StageProps
                {
                    StageName = "Build",
                    Actions = new IAction[]
                    {
                        new CodeBuildAction(new CodeBuildActionProps
                        {
                            ActionName = "BuildAndScan",
                            Project = buildProject,
                            Input = sourceOutput,
                            Outputs = new[] { buildOutput }
                        })
                    }
                }
            }
        });
    }
}

The production version should include artifact encryption, cross-account roles, approval actions, deployment actions, and CloudWatch alarms. But even this small example shows the pattern: the delivery system becomes versioned infrastructure.


5 Mastering Blue-Green and Canary Deployments on ECS

Blue-green deployment on ECS introduces a second task set, validates it, and shifts traffic through the load balancer. This avoids replacing live tasks in place. The original task set remains available during the deployment window, which gives CodeDeploy a safe rollback target.

5.1 Deep Dive into AWS CodeDeploy for ECS

With ECS blue-green deployments using CodeDeploy, the ECS service uses CodeDeploy as its deployment controller. CodeDeploy creates a replacement task set, wires it to the load balancer target group, shifts traffic according to the deployment configuration, and terminates the old task set after success. AWS ECS documentation describes predefined all-at-once, linear, and canary deployment configurations for this model, with the option to create custom configurations.

The pipeline should provide two key files to the deployment action:

taskdef.json
  ECS task definition with the new image.

appspec.yaml
  CodeDeploy instructions for the ECS service and container.

5.1.1 The Role of the AppSpec File and Task Definitions

The task definition describes what to run. The AppSpec file tells CodeDeploy how to deploy it.

version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: "<TASK_DEFINITION>"
        LoadBalancerInfo:
          ContainerName: "orders-api"
          ContainerPort: 8080

The task definition should stay environment-aware but artifact-stable. The image digest changes per release. CPU, memory, port mappings, secrets, log configuration, and health check settings are usually managed through environment-specific templates.

5.1.2 Target Group Switching: Managing the Primary and Replacement Listener

For ECS blue-green, the Application Load Balancer uses target groups to control where traffic goes. The blue task set receives production traffic. The green task set is registered behind the replacement target group. CodeDeploy shifts traffic from blue to green based on the configured strategy.

A common ALB setup:

Production listener: 443
  Routes customer traffic.

Test listener: 8443
  Routes validation traffic to the green task set before production cutover.

Blue target group:
  Current production tasks.

Green target group:
  Replacement tasks.

The test listener is useful when you want smoke tests to hit the new version before any real users do. It also helps validate routing, authentication, health checks, and application startup behavior.

5.2 Blue-Green Implementation: Traffic Shifting via Application Load Balancer

A deployment group controls traffic shifting and rollback behavior. For lower-risk internal services, all-at-once may be acceptable. For customer-facing APIs, canary or linear traffic shifting is safer.

All-at-once:
  Fastest deployment, highest blast radius.

Canary:
  Small initial percentage, observation window, then full shift.

Linear:
  Gradual traffic movement in equal increments.

AWS documents CodeDeploy deployment configurations that support canary, linear, and all-at-once traffic shifting. For ECS specifically, AWS provides predefined deployment configurations such as all-at-once and canary/linear options, and custom configurations can be created for more controlled rollout patterns.

5.3 Advanced Canary Patterns: Linear vs. All-at-Once Traffic Shifting

A canary is useful when production behavior is hard to fully reproduce in staging. For example, payment callbacks, long-tail customer data, browser-specific behavior, and real traffic concurrency can expose issues that tests miss.

A practical production rollout might use:

10% traffic for 10 minutes
  Watch 5xx errors, latency, CPU, memory, business metrics.

100% traffic after validation
  Continue monitoring during termination wait time.

Linear deployment is better when you want steady exposure:

10% every 5 minutes
  Useful for APIs with broad traffic volume and measurable error rates.

The trade-off is time. Canary and linear deployments reduce risk but extend the deployment window. For services with database migrations or external provider dependencies, the longer window must be planned carefully.

5.4 Automating Database Migrations (Entity Framework Core) in Blue-Green Scenarios

Database changes are often the hardest part of blue-green deployments. The old and new task sets may run at the same time. That means the database schema must support both versions during the traffic shift.

Avoid running destructive EF Core migrations inside application startup. If the service starts five tasks, five tasks may try to migrate at once. Instead, run migrations as a controlled pipeline step or one-off ECS task.

EF Core migration bundles are a practical option:

dotnet ef migrations bundle \
  --project src/Orders.Infrastructure \
  --startup-project src/Orders.Api \
  --configuration Release \
  --output artifacts/migrate-orders

The pipeline can run the bundle before the green deployment if the change is backward compatible:

chmod +x artifacts/migrate-orders

./artifacts/migrate-orders \
  --connection "$ORDERS_DB_CONNECTION"

For production, the migration step should have its own IAM permissions, timeout, logging, and approval rules. Treat it as a deployment operation, not as application boot logic.

5.4.1 The “Expand and Contract” Pattern for Zero-Downtime Schema Changes

The expand-and-contract pattern avoids breaking either version during deployment.

Expand:
  Add nullable column, new table, or new index.

Deploy:
  New application version writes to both old and new shape if required.

Backfill:
  Move historical data safely.

Switch:
  Read from the new shape after validation.

Contract:
  Remove old column or table in a later release.

Example:

ALTER TABLE orders
ADD normalized_status varchar(50) NULL;

The new application can write both status and normalized_status. The old application continues using status. After the green version is stable and data is backfilled, a later release removes the old dependency. The cost is extra code for one or two releases. The benefit is deployment safety.


6 Resilience, Rollbacks, and Automated Health Checks

A deployment pipeline is incomplete without a clear definition of failure. The system needs to know when a release is unhealthy, how to stop it, and how to restore service quickly.

6.1 Defining Success: Custom Health Checks in ASP.NET Core using Microsoft.Extensions.Diagnostics.HealthChecks

A basic /health endpoint that always returns 200 is not enough. ECS and CodeDeploy need health checks that reflect whether the service can actually handle traffic.

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddHealthChecks()
    .AddSqlServer(
        builder.Configuration.GetConnectionString("OrdersDb")!,
        name: "orders-db",
        timeout: TimeSpan.FromSeconds(3))
    .AddCheck("self", () => HealthCheckResult.Healthy());

var app = builder.Build();

app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = check => check.Name == "self"
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = _ => true
});

app.Run();

Use liveness to answer “is the process alive?” Use readiness to answer “should this task receive traffic?” During deployment, readiness matters more.

6.2 Automated Rollbacks: CloudWatch Alarms for 5xx Errors and Latency Spikes

CodeDeploy can roll back automatically when deployment failure conditions are met. AWS documents that CodeDeploy rollbacks redeploy a previous revision as a new deployment, and rollbacks can be manual or automatic.

CloudWatch alarms should measure symptoms that users feel:

ALB 5xx count
Target response time p95/p99
ECS task restarts
Application error rate
Dependency timeout rate

Example alarm logic:

{
  "AlarmName": "orders-api-prod-5xx-high",
  "MetricName": "HTTPCode_Target_5XX_Count",
  "Namespace": "AWS/ApplicationELB",
  "Statistic": "Sum",
  "Period": 60,
  "EvaluationPeriods": 3,
  "Threshold": 10,
  "ComparisonOperator": "GreaterThanThreshold"
}

Do not make alarms too sensitive. A single transient 502 during task registration should not automatically fail a release. Use evaluation periods and thresholds that match real production traffic.

6.3 Post-Deployment Validation: Running Integration Tests via Playwright or Selenium in the Pipeline

After traffic shifts, run smoke tests against the production endpoint. These tests should be short, deterministic, and safe. They should not create real payments, send customer emails, or mutate irreversible business state.

A Playwright example can validate the public API and UI shell:

using Microsoft.Playwright;
using Xunit;

public class ProductionSmokeTests
{
    [Fact]
    public async Task OrdersPage_LoadsSuccessfully()
    {
        using var playwright = await Playwright.CreateAsync();

        await using var browser = await playwright.Chromium.LaunchAsync(
            new BrowserTypeLaunchOptions { Headless = true });

        var page = await browser.NewPageAsync();
        var response = await page.GotoAsync("https://orders.example.com/health/ready");

        Assert.NotNull(response);
        Assert.True(response!.Ok);
    }
}

Run these tests as a separate CodeBuild action after deployment. Keep full regression suites in lower environments. Production smoke tests should confirm that the release is reachable, authenticated paths work, and critical dependencies are alive.

6.4 Managing “Sticky Sessions” and WebSocket Connections during Deployment Swaps

Blue-green deployment is easier for stateless APIs. Sticky sessions and WebSockets require extra care because active connections may remain pinned to old tasks while new traffic moves to green.

For ASP.NET Core applications, avoid in-memory session state for anything important. Use distributed stores such as Redis or DynamoDB-backed patterns where appropriate. For SignalR-style workloads, consider managed backplanes or design reconnection behavior explicitly.

Practical rules:

Keep session state outside the container.
Set graceful shutdown timeouts.
Handle SIGTERM in background workers.
Design clients to reconnect safely.
Avoid long-running deployment swaps during peak traffic.

For worker services, handle cancellation cleanly:

public sealed class OrderWorker : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await ProcessNextMessage(stoppingToken);
        }
    }

    private static async Task ProcessNextMessage(CancellationToken token)
    {
        await Task.Delay(TimeSpan.FromSeconds(1), token);
    }
}

The goal is not only a successful deployment. The goal is a deployment that users barely notice, operators can audit, and engineers can roll back without panic.


7 Observability and Governance for .NET Pipelines

A deployment that cannot be observed is not really controlled. Once the pipeline promotes a .NET service into production, the team needs evidence: logs, traces, metrics, audit events, secret rotation status, and cost signals. This is where CI/CD becomes an operating model instead of only an automation script.

7.1 Centralized Logging: Integrating Serilog with AWS CloudWatch and Insights

For ASP.NET Core on ECS, the simplest production logging pattern is structured JSON logs written to standard output. ECS sends container logs to Amazon CloudWatch Logs through the awslogs log driver. Serilog then gives the application consistent event structure, correlation IDs, request context, and error details.

using Serilog;
using Serilog.Formatting.Compact;

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseSerilog((context, services, configuration) =>
{
    configuration
        .ReadFrom.Configuration(context.Configuration)
        .Enrich.FromLogContext()
        .Enrich.WithProperty("Application", "orders-api")
        .WriteTo.Console(new RenderedCompactJsonFormatter());
});

var app = builder.Build();

app.MapGet("/orders/{id}", (string id, ILogger<Program> logger) =>
{
    logger.LogInformation("Order lookup requested for {OrderId}", id);
    return Results.Ok(new { id, status = "Accepted" });
});

app.Run();

The ECS task definition should route logs to a predictable log group per service and environment.

{
  "logConfiguration": {
    "logDriver": "awslogs",
    "options": {
      "awslogs-group": "/ecs/prod/orders-api",
      "awslogs-region": "us-east-1",
      "awslogs-stream-prefix": "ecs"
    }
  }
}

CloudWatch Logs Insights then becomes useful for release validation. For example, after a deployment, operators can compare error volume by image tag, task revision, or correlation ID.

fields @timestamp, Application, RequestPath, StatusCode, @message
| filter Application = "orders-api"
| filter StatusCode >= 500
| sort @timestamp desc
| limit 50

7.2 Distributed Tracing: Implementing OpenTelemetry (OTel) for .NET on AWS X-Ray

Logs show what happened inside one process. Traces show how a request moved across services. For .NET microservices on AWS, OpenTelemetry is the better long-term instrumentation choice because it avoids hard-coding the application to one tracing backend.

AWS provides OpenTelemetry guidance through the AWS Distro for OpenTelemetry, and .NET applications can export traces through the OpenTelemetry Protocol collector path used with AWS X-Ray. AWS describes ADOT as a secure, production-ready AWS-supported distribution of OpenTelemetry.

A minimal ASP.NET Core setup can start with HTTP and ASP.NET Core instrumentation:

using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource =>
    {
        resource.AddService(
            serviceName: "orders-api",
            serviceVersion: Environment.GetEnvironmentVariable("IMAGE_TAG") ?? "unknown");
    })
    .WithTracing(tracing =>
    {
        tracing
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddOtlpExporter();
    });

var app = builder.Build();

app.MapGet("/health/ready", () => Results.Ok("ready"));

app.Run();

In ECS, run the OpenTelemetry collector as a sidecar or use a platform-level collector pattern. The application sends OTLP traces locally, and the collector exports to AWS X-Ray. This keeps tracing configuration outside application code and makes backend changes less disruptive.

7.3 Secret Management: Rotating DB Strings and API Keys via AWS Secrets Manager

Secrets should not be stored in source code, pipeline variables, Docker images, or plain task definition fields. For ECS, the task definition can reference AWS Secrets Manager, and the application receives the value at runtime. Rotation should be planned based on how the dependency behaves.

Secrets Manager supports automatic rotation for secrets, including managed rotation for supported services and Lambda-based rotation for custom workflows. AWS also documents single-user and alternating-users rotation strategies, with alternating users helping maintain availability because one credential remains usable while the other is updated.

A practical .NET configuration pattern reads the connection string normally while AWS injects it securely:

var connectionString = builder.Configuration.GetConnectionString("OrdersDb");

builder.Services.AddDbContext<OrdersDbContext>(options =>
{
    options.UseSqlServer(connectionString);
});

The ECS task definition references the secret:

{
  "secrets": [
    {
      "name": "ConnectionStrings__OrdersDb",
      "valueFrom": "arn:aws:secretsmanager:us-east-1:111122223333:secret:prod/orders-db"
    }
  ]
}

The important operational detail is connection lifetime. If a database password rotates but the application keeps old pooled connections indefinitely, failures may appear later and look random. Configure sensible connection pool lifetime, test rotation in staging, and make sure the application can restart safely during a rotation window.

7.4 Compliance and Audit: Tracking Pipeline Changes with AWS Config and CloudTrail

CI/CD governance should answer four questions: who changed the pipeline, what changed, which artifact was deployed, and who approved production. AWS CloudTrail records AWS API activity, while AWS Config can track configuration changes for supported resources. Together, they provide an audit trail for pipeline resources, IAM roles, CodeBuild projects, ECS services, and related infrastructure.

For pipeline approvals, keep the approval action tied to identity. Avoid shared accounts. The approval record should include the release version, commit, image digest, environment, and business justification.

A release metadata file can be generated during build and stored as an artifact:

{
  "service": "orders-api",
  "version": "1.8.0-build.247",
  "commit": "a1b2c3d4e5f6",
  "imageDigest": "sha256:abc123",
  "pipelineExecutionId": "example-execution-id",
  "approvedBy": "captured-by-codepipeline",
  "changeTicket": "CHG-10482"
}

This small file is useful during audits because it connects source control, build, deployment, and approval history.

7.5 Cost Governance: Utilizing AWS Budgets to Monitor Pipeline and Container Costs

CI/CD cost is usually small compared with production compute, but it can grow quietly. Long CodeBuild jobs, frequent multi-architecture image builds, large ECR storage, verbose logs, NAT Gateway traffic, and oversized Fargate tasks all contribute.

Track cost by tags:

Application = orders-api
Environment = prod
Owner = platform-team
CostCenter = ecommerce

Use AWS Budgets for account-level and project-level alerts. For engineering visibility, publish build duration, deployment duration, and image size trends. A pipeline that grows from 8 minutes to 28 minutes is not only slower; it is usually more expensive and less trusted.


8 Advanced Optimization and Future-Proofing

After the pipeline is reliable, optimization becomes worthwhile. The goal is not to add complexity. The goal is to reduce lead time, improve cost efficiency, and keep the delivery platform adaptable.

8.1 Leveraging AWS Graviton4: Significant Price-Performance Gains for .NET 9+

Graviton adoption should be treated as an engineering decision, not a checkbox. The earlier build process already supports multi-architecture images, so the next step is benchmarking representative workloads. For .NET services, test startup time, request latency, CPU under load, garbage collection behavior, and dependency compatibility.

Run a controlled comparison:

Service: orders-api
Image: same commit, multi-arch manifest
Runtime A: ECS Fargate x86_64
Runtime B: ECS Fargate ARM64
Traffic: same k6 profile
Metrics: p95 latency, p99 latency, CPU, memory, error rate, cost per 1M requests

For many ASP.NET Core APIs, Arm64 is a realistic production target. But validate native dependencies first. PDF generation, cryptography providers, browser automation dependencies, and proprietary monitoring agents are common places where architecture assumptions appear.

8.2 Accelerating Build Times: Docker Layer Caching and CodeBuild VPC Endpoints

Build acceleration should start with measurement. Capture restore time, test time, Docker build time, image push time, and deployment time separately. Then optimize the slowest stage.

CodeBuild supports local caching modes, including Docker layer caching for Linux builds. AWS notes that Docker layer cache is useful for projects that build or pull large Docker images, but it requires privileged mode.

A CDK configuration can enable local Docker layer caching:

var project = new PipelineProject(this, "OrdersApiBuild", new PipelineProjectProps
{
    Environment = new BuildEnvironment
    {
        BuildImage = LinuxBuildImage.STANDARD_7_0,
        Privileged = true
    },
    Cache = Cache.Local(LocalCacheMode.DOCKER_LAYER, LocalCacheMode.CUSTOM)
});

For private builds inside a VPC, add VPC endpoints for services such as ECR, S3, CloudWatch Logs, Secrets Manager, and CodeBuild where appropriate. AWS documents interface VPC endpoints for CodeBuild, which help private networking scenarios avoid unnecessary public internet routing.

8.3 AI-Driven DevOps: Using Amazon Q Developer for Pipeline Debugging

Amazon Q Developer is the successor path for CodeWhisperer capabilities. AWS states that CodeWhisperer features are moving into Amazon Q Developer, and Amazon Q Developer can help developers understand, build, extend, and operate AWS applications.

Use it as an assistant, not as an approver. Good use cases include explaining failed IAM permissions, generating first-draft CDK constructs, reviewing CloudWatch error patterns, and suggesting buildspec improvements. Do not paste secrets, production credentials, or sensitive customer data into prompts.

A useful internal prompt pattern:

We are deploying an ASP.NET Core service to ECS Fargate through CodePipeline.
The CodeBuild phase fails during docker push with AccessDeniedException.
Review this IAM policy and suggest the minimum ECR permissions needed.
Do not broaden permissions beyond this repository.

The guardrail is simple: AI can speed investigation, but production changes still need code review, test execution, and human accountability.

8.4 Serverless CI/CD: Evaluating AWS Step Functions for Custom Orchestration Logic

CodePipeline is the right default for standard source-build-approve-deploy flows. Step Functions becomes useful when the release process has complex branching, retries, wait states, or external system coordination.

Use Step Functions when the workflow looks like this:

Start release
  -> Create change ticket
  -> Wait for external CAB approval
  -> Run migration task
  -> Deploy ECS service
  -> Run smoke tests
  -> Notify release channel
  -> Close change ticket

A simplified state machine can call Lambda functions for release tasks:

{
  "StartAt": "RunSmokeTests",
  "States": {
    "RunSmokeTests": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:111122223333:function:run-smoke-tests",
      "Next": "EvaluateResult"
    },
    "EvaluateResult": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.passed",
          "BooleanEquals": true,
          "Next": "NotifySuccess"
        }
      ],
      "Default": "NotifyFailure"
    },
    "NotifySuccess": {
      "Type": "Succeed"
    },
    "NotifyFailure": {
      "Type": "Fail"
    }
  }
}

The trade-off is ownership. Custom orchestration gives flexibility but creates more platform code to maintain.

8.5 Final Architect’s Checklist: Readiness for High-Availability and Disaster Recovery

Before treating the pipeline as production-ready, verify the release path under failure conditions.

Artifact integrity:
  Production deploys the same image digest tested in staging.

Rollback:
  CodeDeploy rollback has been tested, not only configured.

Secrets:
  Rotation is tested in staging and documented for production.

Database:
  Schema changes follow expand-and-contract.

Observability:
  Logs, traces, metrics, and alarms identify the failed version quickly.

Access:
  Cross-account roles follow least privilege.

Cost:
  Build duration, image size, ECR storage, and Fargate usage are monitored.

Disaster recovery:
  IaC can recreate pipeline and service infrastructure in a target region.

The mature state is boring in the best way. Developers commit code, the pipeline builds one artifact, environments receive controlled promotions, deployments shift traffic safely, and failures produce clear signals. That is the real value of CI/CD for .NET on AWS: faster delivery with less guesswork.

Advertisement