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=1andDOTNET_CLI_TELEMETRY_OPTOUT=1in 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
reportssection. 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 β