# Advanced buildspec.yml: Python, Go, ECR Push, EKS Deploy

- URL: https://karandeepsingh.ca/posts/aws-codebuild-advanced-buildspec-guide/
- Author: Karandeep Singh
- Published: 2021-09-23
- Updated: 2026-05-04
- Tags: containers, codebuild, eks
- Summary: This article covers the buildspec.yaml file in AWS CodeBuild with practical examples for Node.js, Python, and Go projects. Includes the critical ECR login fix and EKS deployment patterns. 

![Server room with dark cables representing cloud build infrastructure](https://karandeepsingh.ca/images/posts/aws-codebuild-buildspec.jpg)

## Introduction

If you've worked with AWS CodeBuild, you know the buildspec file is where everything comes together. It's a YAML file that tells CodeBuild what to do at each step. Get it right and your pipeline runs smoothly. Get it wrong and you'll spend hours digging through logs.

This guide covers the basics and some commonly useful patterns. The examples focus on Node.js, Python, and Go.

## Basic Structure

Quick reference for what goes into a buildspec:

- **Location**: Root directory of your source (or specify a different path in CodeBuild settings)
- **Version**: Use 0.2 (runs all commands in the same shell instance)
- **env**: Environment variables, Parameter Store refs, Secrets Manager refs
- **phases**: Commands that run at each stage
  - **install**: Set up dependencies and runtimes
  - **pre_build**: Prep work before building
  - **build**: Main build commands
  - **post_build**: Cleanup, push artifacts, notifications
- **artifacts**: What files to output
- **reports**: Test report configuration
- **cache**: Paths to cache between builds

## Core Components

### Version

Always use 0.2. It runs commands in the same shell, so environment variables persist between commands.

```yaml
version: 0.2
```

### Environment Variables

Three ways to define them:

```yaml
env:
  variables:
    NODE_ENV: "production"
  parameter-store:
    DB_HOST: "/app/prod/db-host"
  secrets-manager:
    DB_PASSWORD: "prod/db:password"
```

### Phases

Each phase runs in sequence. If one fails, subsequent phases don't run (unless you configure otherwise).

### Artifacts

Defines build output:

```yaml
artifacts:
  files:
    - 'dist/**/*'
  name: build-$CODEBUILD_BUILD_NUMBER
```

### Cache

Speeds up builds by reusing dependencies:

```yaml
cache:
  paths:
    - 'node_modules/**/*'
    - '/root/.npm/**/*'
```

---

## Critical Fix: ECR Login Command

If you're pushing Docker images to ECR, you need to know about this. The old login command was removed in AWS CLI v2 and will break your builds.

### Old Command (Deprecated)

```yaml
# DEPRECATED - removed in AWS CLI v2
# DO NOT USE - keeping for reference only
- $(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION)
```

This fails silently or throws "unknown command" errors on CodeBuild images 5.0+.

### Current Command (Use This)

```yaml
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
```

The new command pipes the password directly to docker login, which is more secure since credentials don't appear in shell history or logs.

### Why This Matters

- CodeBuild image 4.0 = AWS CLI v1 (old command works)
- CodeBuild image 5.0+ = AWS CLI v2 (only new command works)

If your builds suddenly fail after an image upgrade, this is probably why.

---

## Practical Examples

### Example 1: Node.js Application

Basic Node.js build with npm:

```yaml
version: 0.2

phases:
  install:
    runtime-versions:
      nodejs: 20
    commands:
      - echo "Installing dependencies"
      - npm ci
  pre_build:
    commands:
      - echo "Running lint"
      - npm run lint
  build:
    commands:
      - echo "Running tests"
      - npm test
      - echo "Building"
      - npm run build
  post_build:
    commands:
      - echo "Build complete"

artifacts:
  files:
    - 'dist/**/*'
    - 'package.json'
  base-directory: '.'

cache:
  paths:
    - 'node_modules/**/*'
```

### Example 2: Node.js with ECR Push

Building a Docker image and pushing to ECR:

```yaml
version: 0.2

env:
  variables:
    IMAGE_REPO_NAME: "my-node-app"
    IMAGE_TAG: "latest"

phases:
  install:
    runtime-versions:
      nodejs: 20
  pre_build:
    commands:
      - echo "Logging in to ECR"
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
      - IMAGE_TAG=${COMMIT_HASH:=latest}
  build:
    commands:
      - echo "Building Docker image"
      - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
  post_build:
    commands:
      - echo "Pushing to ECR"
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      - printf '[{"name":"app","imageUri":"%s"}]' $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG > imagedefinitions.json

artifacts:
  files:
    - imagedefinitions.json
```

Note that `docker` is not a valid `runtime-versions` key on the managed CodeBuild images (`aws/codebuild/standard:5.0` and newer only accept android, dotnet, golang, java, nodejs, php, python, and ruby). Docker is already preinstalled on those images, so you do not declare it as a runtime. To run Docker commands like `docker build`, enable the project's **Privileged** mode instead — adding `docker` under `runtime-versions` just causes the install phase to fail.

### Example 3: Python Script/Lambda Package

Python without frameworks, packaging for Lambda or general use:

```yaml
version: 0.2

phases:
  install:
    runtime-versions:
      python: 3.12
    commands:
      - echo "Installing dependencies"
      - pip install -r requirements.txt -t ./package
  pre_build:
    commands:
      - echo "Running linter"
      - pip install flake8
      - flake8 src/
  build:
    commands:
      - echo "Running tests"
      - pip install pytest
      - pytest tests/
      - echo "Creating deployment package"
      - cd package && zip -r ../function.zip . && cd ..
      - zip -g function.zip -r src/
  post_build:
    commands:
      - echo "Package created"

artifacts:
  files:
    - function.zip

cache:
  paths:
    - '/root/.cache/pip/**/*'
```

### Example 4: Go Application

Basic Go build:

```yaml
version: 0.2

env:
  variables:
    GO111MODULE: "on"

phases:
  install:
    runtime-versions:
      golang: 1.21
  pre_build:
    commands:
      - echo "Downloading dependencies"
      - go mod download
      - echo "Running vet"
      - go vet ./...
  build:
    commands:
      - echo "Running tests"
      - go test ./...
      - echo "Building binary"
      - CGO_ENABLED=0 GOOS=linux go build -o app ./cmd/main.go
  post_build:
    commands:
      - echo "Build complete"

artifacts:
  files:
    - app

cache:
  paths:
    - '/go/pkg/mod/**/*'
```

### Example 5: EKS Deployment

Building and deploying to EKS:

```yaml
version: 0.2

env:
  variables:
    EKS_CLUSTER_NAME: "my-cluster"
    IMAGE_REPO_NAME: "my-app"
  parameter-store:
    KUBECONFIG_DATA: "/eks/kubeconfig"

phases:
  install:
    commands:
      - echo "Installing kubectl"
      - curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
      - chmod +x kubectl
      - mv kubectl /usr/local/bin/
  pre_build:
    commands:
      - echo "Logging in to ECR"
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
      - IMAGE_TAG=${COMMIT_HASH:=latest}
      - echo "Building image"
      - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
  build:
    commands:
      - echo "Configuring kubectl"
      - aws eks update-kubeconfig --name $EKS_CLUSTER_NAME --region $AWS_DEFAULT_REGION
      - echo "Updating deployment"
      - kubectl set image deployment/my-app app=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      - kubectl rollout status deployment/my-app --timeout=300s
  post_build:
    commands:
      - echo "Deployment complete"

artifacts:
  files:
    - imagedefinitions.json
```

### Example 6: Conditional Builds

Different behavior based on branch:

```yaml
version: 0.2

phases:
  install:
    runtime-versions:
      nodejs: 20
    commands:
      - npm ci
  build:
    commands:
      - |
        if [ "$CODEBUILD_WEBHOOK_TRIGGER" = "branch/main" ]; then
          echo "Main branch - running full build"
          npm run test:all
          npm run build:prod
        else
          echo "Feature branch - quick build"
          npm run test
          npm run build
        fi
  post_build:
    commands:
      - echo "Done"

artifacts:
  files:
    - 'dist/**/*'
```

---

## Best Practices

### Use npm ci Instead of npm install

`npm ci` is faster and more reliable for CI environments. It installs exact versions from package-lock.json and deletes node_modules first.

### Cache Dependencies

This can cut build times significantly:

```yaml
cache:
  paths:
    - 'node_modules/**/*'      # Node
    - '/root/.npm/**/*'        # npm global cache
    - '/root/.cache/pip/**/*'  # Python
    - '/go/pkg/mod/**/*'       # Go modules
```

### Handle Secrets Properly

Never hardcode credentials:

```yaml
env:
  secrets-manager:
    API_KEY: "prod/api:key"
  parameter-store:
    DB_HOST: "/app/db-host"
```

### Check Build Status in post_build

The post_build phase runs even if build fails. Check the status:

```yaml
post_build:
  commands:
    - |
      if [ "$CODEBUILD_BUILD_SUCCEEDING" = "1" ]; then
        echo "Build succeeded, pushing artifacts"
        # push to S3, notify slack, etc
      else
        echo "Build failed"
        # send alert
      fi
```

### Use Specific Runtime Versions

Don't rely on defaults. Specify versions explicitly:

```yaml
install:
  runtime-versions:
    nodejs: 20
    python: 3.12
```

---

## Troubleshooting

### ECR Login Fails

Check which AWS CLI version your image uses:

```yaml
pre_build:
  commands:
    - aws --version
```

If it's v2, use the `get-login-password` command.

### Build Works Locally But Fails in CodeBuild

Common causes:
- Different Node/Python version
- Missing environment variables
- File permissions
- Dependencies not in package-lock.json

### Artifacts Not Found

Make sure your paths are correct. Use `ls -la` to debug:

```yaml
post_build:
  commands:
    - ls -la
    - ls -la dist/
```

---
