Home β€Ί .NET on AWS β€Ί Building .NET with CodeBuild

Building .NET with CodeBuild

CI/CD for .NET on AWS: buildspec files, caching NuGet packages, Native AoT builds, and CDK pipelines.

CodeBuild for .NET

AWS CodeBuild is the managed build service. Spin up a container, run your build commands, produce artifacts. No Jenkins servers to maintain, no build agents to patch. You pay per minute of build time.

The challenge with .NET on CodeBuild: AWS's managed build images don't always include the latest .NET SDK, the build times for AoT compilation are slow without caching, and the documentation heavily favors TypeScript/Python examples.

Buildspec Basics

Simple .NET build

version: 0.2

phases:
  install:
    runtime-versions:
      dotnet: 8.0
  
  pre_build:
    commands:
      - dotnet restore
  
  build:
    commands:
      - dotnet build -c Release --no-restore
      - dotnet test -c Release --no-build --no-restore
  
  post_build:
    commands:
      - dotnet publish src/MyApi -c Release -o publish --no-build

artifacts:
  files:
    - "publish/**/*"
  base-directory: "."

Native AoT build

AoT compilation needs a Linux environment and takes significantly longer. Use a larger build instance:

version: 0.2

env:
  variables:
    DOTNET_CLI_TELEMETRY_OPTOUT: "1"
    DOTNET_NOLOGO: "1"

phases:
  install:
    runtime-versions:
      dotnet: 8.0
    commands:
      # AoT needs clang and build tools
      - yum install -y clang zlib-devel

  pre_build:
    commands:
      - dotnet restore -r linux-arm64

  build:
    commands:
      - dotnet publish src/MyApi -c Release -r linux-arm64 --self-contained -o publish
      # Verify it's a native binary
      - file publish/bootstrap
      - ls -la publish/bootstrap

artifacts:
  files:
    - "publish/bootstrap"
  base-directory: "."

Important: Use arm64 compute type in CodeBuild when building for Graviton Lambda. Cross-compilation from x86 to arm64 AoT is not supported. You need the target architecture.

Multi-project solution with tests

version: 0.2

env:
  variables:
    DOTNET_CLI_TELEMETRY_OPTOUT: "1"
    CONFIGURATION: "Release"

phases:
  install:
    runtime-versions:
      dotnet: 8.0

  pre_build:
    commands:
      - dotnet restore MySolution.sln

  build:
    commands:
      - dotnet build MySolution.sln -c $CONFIGURATION --no-restore
      - dotnet test MySolution.sln -c $CONFIGURATION --no-build --no-restore --logger "trx;LogFileName=test-results.trx" --results-directory ./test-results

  post_build:
    commands:
      - dotnet publish src/OrderApi -c $CONFIGURATION -o artifacts/order-api --no-build
      - dotnet publish src/PaymentProcessor -c $CONFIGURATION -o artifacts/payment-processor --no-build

reports:
  test-report:
    files:
      - "**/*.trx"
    base-directory: "test-results"
    file-format: "VISUALSTUDIOTRX"

artifacts:
  files:
    - "artifacts/**/*"
  secondary-artifacts:
    order-api:
      files:
        - "**/*"
      base-directory: "artifacts/order-api"
    payment-processor:
      files:
        - "**/*"
      base-directory: "artifacts/payment-processor"

NuGet Caching

NuGet restore can add 30-60 seconds to every build. Use CodeBuild's caching to avoid downloading packages on every run:

version: 0.2

cache:
  paths:
    - "/root/.nuget/packages/**/*"

phases:
  pre_build:
    commands:
      - dotnet restore

For S3-based caching (persists across build projects):

cache:
  type: S3
  location: my-build-cache-bucket/nuget-cache
  paths:
    - "/root/.nuget/packages/**/*"

CodeArtifact for Private NuGet Packages

If you have internal NuGet packages, CodeArtifact hosts them on AWS instead of running a private NuGet server:

Configure NuGet source in buildspec

phases:
  pre_build:
    commands:
      # Get auth token
      - export CODEARTIFACT_AUTH_TOKEN=$(aws codeartifact get-authorization-token --domain my-org --domain-owner 123456789012 --query authorizationToken --output text)
      # Add source
      - dotnet nuget add source "https://my-org-123456789012.d.codeartifact.us-east-1.amazonaws.com/nuget/my-packages/v3/index.json" --name CodeArtifact --username aws --password $CODEARTIFACT_AUTH_TOKEN --store-password-in-clear-text
      - dotnet restore

Publishing packages to CodeArtifact

phases:
  post_build:
    commands:
      - export CODEARTIFACT_AUTH_TOKEN=$(aws codeartifact get-authorization-token --domain my-org --domain-owner 123456789012 --query authorizationToken --output text)
      - dotnet pack src/MyLibrary -c Release -o packages
      - dotnet nuget push packages/*.nupkg --source "https://my-org-123456789012.d.codeartifact.us-east-1.amazonaws.com/nuget/my-packages/v3/index.json" --api-key $CODEARTIFACT_AUTH_TOKEN

CDK Pipeline Setup (C#)

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

// Build project
var buildProject = new PipelineProject(this, "DotNetBuild", new PipelineProjectProps
{
    Environment = new BuildEnvironment
    {
        BuildImage = LinuxArmBuildImage.AMAZON_LINUX_2_STANDARD_3_0, // arm64 for Graviton
        ComputeType = ComputeType.LARGE, // needed for AoT compilation
    },
    Cache = Cache.Local(LocalCacheMode.DOCKER_LAYER, LocalCacheMode.CUSTOM),
    BuildSpec = BuildSpec.FromSourceFilename("buildspec.yml"),
    Timeout = Duration.Minutes(15),
});

// Grant CodeArtifact access
buildProject.AddToRolePolicy(new PolicyStatement(new PolicyStatementProps
{
    Actions = new[]
    {
        "codeartifact:GetAuthorizationToken",
        "codeartifact:GetRepositoryEndpoint",
        "codeartifact:ReadFromRepository",
    },
    Resources = new[] { "*" },
}));
buildProject.AddToRolePolicy(new PolicyStatement(new PolicyStatementProps
{
    Actions = new[] { "sts:GetServiceBearerToken" },
    Resources = new[] { "*" },
    Conditions = new Dictionary<string, object>
    {
        ["StringEquals"] = new Dictionary<string, string>
        {
            ["sts:AWSServiceName"] = "codeartifact.amazonaws.com",
        },
    },
}));

Full CDK Pipeline

using Amazon.CDK.Pipelines;

var pipeline = new CodePipeline(this, "AppPipeline", new CodePipelineProps
{
    PipelineName = "MyApp",
    Synth = new ShellStep("Synth", new ShellStepProps
    {
        Input = CodePipelineSource.GitHub("myorg/myrepo", "main"),
        Commands = new[]
        {
            "npm ci",
            "npx cdk synth",
        },
        PrimaryOutputDirectory = "cdk.out",
    }),
    CodeBuildDefaults = new CodeBuildOptions
    {
        BuildEnvironment = new BuildEnvironment
        {
            ComputeType = ComputeType.LARGE,
        },
        Cache = Cache.Local(LocalCacheMode.CUSTOM),
    },
});

Build Performance Tips

Optimization Impact
NuGet caching -30-60s per build
--no-restore on subsequent steps -10-20s
Larger compute type for AoT 2-3x faster AoT compilation
arm64 compute type Native builds, no cross-compilation
DOTNET_CLI_TELEMETRY_OPTOUT=1 Minor, avoids telemetry overhead
Parallel test execution Varies. Can halve test time

Tips

  • Use arm64 compute type if you're deploying to Graviton Lambda. AoT doesn't support cross-compilation, so you need to build on the same architecture.
  • Cache aggressively. NuGet packages and Docker layers are the biggest time sinks.
  • Separate build and test stages in your pipeline so test failures don't require re-building.
  • Set DOTNET_NOLOGO=1 and DOTNET_CLI_TELEMETRY_OPTOUT=1 in environment variables. Minor speedup and cleaner logs.
  • For monorepos with multiple services, use CodeBuild batch builds to build them in parallel rather than sequentially.
  • Report test results using the reports section. It gives you test history and failure tracking in the CodeBuild console.

Further Reading

Looking for hands-on help? View my .NET on AWS services β†’

Setting up CI/CD for .NET on AWS?

Drop me a message β€” I typically respond within one business day.