Skip to content

DO-1743: pin third-party GitHub Actions to commit SHAs for security #6

DO-1743: pin third-party GitHub Actions to commit SHAs for security

DO-1743: pin third-party GitHub Actions to commit SHAs for security #6

name: 🐳 Docker ECR Deployment

Check failure on line 1 in .github/workflows/docker-ecr-deploy.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/docker-ecr-deploy.yml

Invalid workflow file

(Line: 431, Col: 13): Unrecognized named-value: 'secrets'. Located at position 34 within expression: inputs.enable-signing == true && secrets.container-signing-key != '', (Line: 435, Col: 13): Unrecognized named-value: 'secrets'. Located at position 34 within expression: inputs.enable-signing == true && secrets.container-signing-key != ''
on:
workflow_call:
inputs:
# Core Configuration
aws-region:
description: "AWS region for ECR registry"
type: string
required: false
default: "ap-southeast-2"
ecr-repository:
description: "ECR repository name (required)"
type: string
required: true
dockerfile-path:
description: "Path to Dockerfile"
type: string
required: false
default: "Dockerfile"
build-context:
description: "Docker build context path"
type: string
required: false
default: "."
# Platform and Build Configuration
platforms:
description: "Target platforms for multi-platform builds"
type: string
required: false
default: "linux/amd64,linux/arm64"
push-to-registry:
description: "Push built images to ECR registry"
type: boolean
required: false
default: true
# Security and Scanning
vulnerability-scan:
description: "Enable container vulnerability scanning"
type: boolean
required: false
default: true
security-threshold:
description: "Security vulnerability threshold (CRITICAL/HIGH/MEDIUM/LOW)"
type: string
required: false
default: "HIGH"
# Tagging Strategy
tag-strategy:
description: "Image tagging strategy (latest/semantic/branch/custom)"
type: string
required: false
default: "latest"
custom-tags:
description: "Custom tags (comma-separated) when using custom strategy"
type: string
required: false
default: ""
# Build Optimization
cache-from:
description: "Cache sources for build optimization (comma-separated)"
type: string
required: false
default: ""
build-args:
description: "Docker build arguments as JSON object"
type: string
required: false
default: "{}"
target-stage:
description: "Target build stage for multi-stage Dockerfiles"
type: string
required: false
default: ""
# Registry Management
cleanup-old-images:
description: "Clean up old images from ECR registry"
type: boolean
required: false
default: false
retention-count:
description: "Number of images to retain when cleaning up"
type: string
required: false
default: "10"
# Container Signing
enable-signing:
description: "Enable container image signing with cosign"
type: boolean
required: false
default: false
# Advanced Configuration
debug:
description: "Enable verbose logging and debug output"
type: boolean
required: false
default: false
secrets:
aws-access-key-id:
description: "AWS access key ID"
required: true
aws-secret-access-key:
description: "AWS secret access key"
required: true
container-signing-key:
description: "Private key for container signing (optional)"
required: false
outputs:
image-uri:
description: "Full URI of the built container image"
value: ${{ jobs.build.outputs.image-uri }}
image-digest:
description: "SHA256 digest of the built image"
value: ${{ jobs.build.outputs.image-digest }}
image-tags:
description: "Applied image tags as JSON array"
value: ${{ jobs.build.outputs.image-tags }}
vulnerability-report:
description: "Container vulnerability scan results"
value: ${{ jobs.scan.outputs.vulnerability-report }}
jobs:
# Validate inputs and prepare build configuration
prepare:
name: πŸ” Prepare Docker Build
runs-on: ubuntu-latest
outputs:
image-tags: ${{ steps.tag-config.outputs.tags }}
build-args: ${{ steps.build-config.outputs.args }}
cache-config: ${{ steps.cache-config.outputs.setup }}
platforms: ${{ steps.platform-config.outputs.platforms }}
security-scan: ${{ steps.security-config.outputs.enabled }}
steps:
- name: Validate required inputs
run: |
echo "πŸ” Validating Docker build configuration..."
if [ -z "${{ inputs.ecr-repository }}" ]; then
echo "❌ Error: ecr-repository is required"
exit 1
fi
# Validate security threshold
case "${{ inputs.security-threshold }}" in
CRITICAL|HIGH|MEDIUM|LOW)
echo "βœ… Security threshold: ${{ inputs.security-threshold }}"
;;
*)
echo "❌ Error: security-threshold must be one of: CRITICAL, HIGH, MEDIUM, LOW"
exit 1
;;
esac
# Validate tag strategy
case "${{ inputs.tag-strategy }}" in
latest|semantic|branch|custom)
echo "βœ… Tag strategy: ${{ inputs.tag-strategy }}"
;;
*)
echo "❌ Error: tag-strategy must be one of: latest, semantic, branch, custom"
exit 1
;;
esac
# Validate build args JSON if provided
if [ "${{ inputs.build-args }}" != "{}" ]; then
echo '${{ inputs.build-args }}' | jq . > /dev/null
if [ $? -ne 0 ]; then
echo "❌ Error: build-args must be valid JSON"
exit 1
fi
fi
echo "βœ… All inputs validated successfully"
- name: Configure image tags
id: tag-config
run: |
echo "🏷️ Configuring image tags..."
tags=""
case "${{ inputs.tag-strategy }}" in
latest)
tags="latest"
if [ "${{ github.ref_type }}" = "tag" ]; then
tags="$tags,${{ github.ref_name }}"
fi
;;
semantic)
if [ "${{ github.ref_type }}" = "tag" ]; then
tag_name="${{ github.ref_name }}"
tags="$tag_name"
# Extract semantic version components
if [[ $tag_name =~ ^v?([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then
major="${BASH_REMATCH[1]}"
minor="${BASH_REMATCH[2]}"
patch="${BASH_REMATCH[3]}"
tags="$tags,$major,$major.$minor,$major.$minor.$patch"
fi
else
tags="latest"
fi
;;
branch)
branch_name=$(echo "${{ github.ref_name }}" | sed 's/[^a-zA-Z0-9.-]/-/g')
tags="$branch_name"
if [ "${{ github.ref_name }}" = "main" ] || [ "${{ github.ref_name }}" = "master" ]; then
tags="$tags,latest"
fi
;;
custom)
if [ -n "${{ inputs.custom-tags }}" ]; then
tags="${{ inputs.custom-tags }}"
else
echo "❌ Error: custom-tags must be provided when using custom strategy"
exit 1
fi
;;
esac
# Add commit SHA tag
short_sha=$(echo "${{ github.sha }}" | cut -c1-7)
tags="$tags,sha-$short_sha"
echo "tags=$tags" >> $GITHUB_OUTPUT
echo "βœ… Tags configured: $tags"
- name: Configure build arguments
id: build-config
run: |
echo "βš™οΈ Configuring build arguments..."
build_args=""
# Add default build args
build_args="$build_args --build-arg BUILDKIT_INLINE_CACHE=1"
build_args="$build_args --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
build_args="$build_args --build-arg VCS_REF=${{ github.sha }}"
build_args="$build_args --build-arg VERSION=${{ github.ref_name }}"
# Add custom build args
if [ "${{ inputs.build-args }}" != "{}" ]; then
echo '${{ inputs.build-args }}' | jq -r 'to_entries[] | "--build-arg \(.key)=\(.value)"' | while read -r arg; do
build_args="$build_args $arg"
done
fi
echo "args=$build_args" >> $GITHUB_OUTPUT
echo "βœ… Build arguments configured"
- name: Configure cache settings
id: cache-config
run: |
echo "πŸ’Ύ Configuring build cache..."
cache_from=""
if [ -n "${{ inputs.cache-from }}" ]; then
# Convert comma-separated cache sources to buildx format
IFS=',' read -ra CACHE_SOURCES <<< "${{ inputs.cache-from }}"
for source in "${CACHE_SOURCES[@]}"; do
cache_from="$cache_from --cache-from type=registry,ref=$source"
done
fi
# Add ECR cache source
ecr_uri="${{ inputs.ecr-repository }}:buildcache"
cache_from="$cache_from --cache-from type=registry,ref=$ecr_uri"
cache_to="--cache-to type=registry,ref=$ecr_uri,mode=max"
echo "setup=$cache_from $cache_to" >> $GITHUB_OUTPUT
echo "βœ… Cache configuration prepared"
- name: Configure platforms
id: platform-config
run: |
echo "πŸ—οΈ Configuring build platforms..."
platforms="${{ inputs.platforms }}"
# Validate platforms
IFS=',' read -ra PLATFORM_LIST <<< "$platforms"
for platform in "${PLATFORM_LIST[@]}"; do
case "$platform" in
linux/amd64|linux/arm64|linux/arm/v7|linux/arm/v8)
echo "βœ… Platform supported: $platform"
;;
*)
echo "⚠️ Warning: Unusual platform: $platform"
;;
esac
done
echo "platforms=$platforms" >> $GITHUB_OUTPUT
echo "βœ… Platforms configured: $platforms"
- name: Configure security scanning
id: security-config
run: |
echo "πŸ”’ Configuring security settings..."
scan_enabled="${{ inputs.vulnerability-scan }}"
if [ "$scan_enabled" = "true" ]; then
echo "enabled=true" >> $GITHUB_OUTPUT
echo "βœ… Vulnerability scanning enabled (threshold: ${{ inputs.security-threshold }})"
else
echo "enabled=false" >> $GITHUB_OUTPUT
echo "ℹ️ Vulnerability scanning disabled"
fi
# Build and push Docker image
build:
name: πŸ—οΈ Build & Push Docker Image
runs-on: ubuntu-latest
needs: prepare
outputs:
image-uri: ${{ steps.build.outputs.image-uri }}
image-digest: ${{ steps.build.outputs.digest }}
image-tags: ${{ steps.build.outputs.tags }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: ${{ needs.prepare.outputs.platforms }}
driver-opts: |
network=host
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.aws-access-key-id }}
aws-secret-access-key: ${{ secrets.aws-secret-access-key }}
aws-region: ${{ inputs.aws-region }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
with:
mask-password: 'true'
- name: Create ECR repository if needed
run: |
echo "πŸ—„οΈ Ensuring ECR repository exists..."
aws ecr describe-repositories --repository-names ${{ inputs.ecr-repository }} --region ${{ inputs.aws-region }} 2>/dev/null || {
echo "πŸ“¦ Creating ECR repository: ${{ inputs.ecr-repository }}"
aws ecr create-repository \
--repository-name ${{ inputs.ecr-repository }} \
--region ${{ inputs.aws-region }} \
--image-scanning-configuration scanOnPush=true \
--encryption-configuration encryptionType=AES256
# Set lifecycle policy
aws ecr put-lifecycle-policy \
--repository-name ${{ inputs.ecr-repository }} \
--region ${{ inputs.aws-region }} \
--lifecycle-policy-text '{
"rules": [
{
"rulePriority": 1,
"description": "Keep last 30 images",
"selection": {
"tagStatus": "any",
"countType": "imageCountMoreThan",
"countNumber": 30
},
"action": {
"type": "expire"
}
}
]
}'
}
echo "βœ… ECR repository ready"
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v5
with:
context: ${{ inputs.build-context }}
file: ${{ inputs.dockerfile-path }}
platforms: ${{ needs.prepare.outputs.platforms }}
push: ${{ inputs.push-to-registry }}
tags: |
${{ steps.login-ecr.outputs.registry }}/${{ inputs.ecr-repository }}:${{ needs.prepare.outputs.image-tags }}
target: ${{ inputs.target-stage }}
cache-from: ${{ needs.prepare.outputs.cache-config }}
cache-to: type=inline
provenance: true
sbom: true
outputs: type=image,name=${{ steps.login-ecr.outputs.registry }}/${{ inputs.ecr-repository }},push=${{ inputs.push-to-registry }}
- name: Extract build metadata
id: metadata
run: |
echo "πŸ“‹ Extracting build metadata..."
registry="${{ steps.login-ecr.outputs.registry }}"
repository="${{ inputs.ecr-repository }}"
# Get image digest from build output
digest="${{ steps.build.outputs.digest }}"
# Construct image URI
image_uri="$registry/$repository@$digest"
# Format tags as JSON array
tags_array=$(echo "${{ needs.prepare.outputs.image-tags }}" | sed 's/,/","/g' | sed 's/^/["/' | sed 's/$/"]/')
echo "image-uri=$image_uri" >> $GITHUB_OUTPUT
echo "digest=$digest" >> $GITHUB_OUTPUT
echo "tags=$tags_array" >> $GITHUB_OUTPUT
echo "βœ… Image built: $image_uri"
echo "🏷️ Tags applied: ${{ needs.prepare.outputs.image-tags }}"
- name: Sign container image
if: inputs.enable-signing == true && secrets.container-signing-key != ''
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3
- name: Sign the container image
if: inputs.enable-signing == true && secrets.container-signing-key != ''
run: |
echo "✍️ Signing container image..."
echo "${{ secrets.container-signing-key }}" > cosign.key
cosign sign --key cosign.key \
${{ steps.metadata.outputs.image-uri }}
rm cosign.key
echo "βœ… Image signed successfully"
# Vulnerability scanning
scan:
name: πŸ” Security Vulnerability Scan
runs-on: ubuntu-latest
needs: [prepare, build]
if: needs.prepare.outputs.security-scan == 'true' && needs.build.result == 'success'
outputs:
vulnerability-report: ${{ steps.scan.outputs.report }}
security-status: ${{ steps.scan.outputs.status }}
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.aws-access-key-id }}
aws-secret-access-key: ${{ secrets.aws-secret-access-key }}
aws-region: ${{ inputs.aws-region }}
- name: Run Trivy vulnerability scanner
id: scan
uses: aquasecurity/trivy-action@77137e9dc3ab1b329b7c8a38c2eb7475850a14e8 # master
with:
image-ref: ${{ needs.build.outputs.image-uri }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH,MEDIUM,LOW'
- name: Analyze vulnerability results
id: analyze
run: |
echo "πŸ” Analyzing vulnerability scan results..."
# Parse SARIF results
critical_count=$(jq '[.runs[].results[] | select(.level == "error")] | length' trivy-results.sarif)
high_count=$(jq '[.runs[].results[] | select(.level == "warning")] | length' trivy-results.sarif)
medium_count=$(jq '[.runs[].results[] | select(.level == "note")] | length' trivy-results.sarif)
echo "πŸ”΄ Critical vulnerabilities: $critical_count"
echo "🟠 High vulnerabilities: $high_count"
echo "🟑 Medium vulnerabilities: $medium_count"
# Check against threshold
case "${{ inputs.security-threshold }}" in
CRITICAL)
if [ "$critical_count" -gt 0 ]; then
echo "❌ Critical vulnerabilities found - blocking deployment"
echo "status=failed" >> $GITHUB_OUTPUT
exit 1
fi
;;
HIGH)
if [ "$critical_count" -gt 0 ] || [ "$high_count" -gt 0 ]; then
echo "❌ High or critical vulnerabilities found - blocking deployment"
echo "status=failed" >> $GITHUB_OUTPUT
exit 1
fi
;;
MEDIUM)
if [ "$critical_count" -gt 0 ] || [ "$high_count" -gt 0 ] || [ "$medium_count" -gt 0 ]; then
echo "❌ Medium, high, or critical vulnerabilities found - blocking deployment"
echo "status=failed" >> $GITHUB_OUTPUT
exit 1
fi
;;
LOW)
echo "ℹ️ All vulnerabilities reported for awareness"
;;
esac
echo "status=passed" >> $GITHUB_OUTPUT
echo "βœ… Vulnerability scan passed security threshold"
# Create summary report
report=$(jq -c '{
critical: [.runs[].results[] | select(.level == "error")] | length,
high: [.runs[].results[] | select(.level == "warning")] | length,
medium: [.runs[].results[] | select(.level == "note")] | length,
threshold: "${{ inputs.security-threshold }}",
status: "passed"
}' trivy-results.sarif)
echo "report=$report" >> $GITHUB_OUTPUT
- name: Upload Trivy scan results
uses: actions/upload-artifact@v4
with:
name: trivy-results
path: trivy-results.sarif
retention-days: 30
- name: Upload to GitHub Security tab
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif
# Registry cleanup
cleanup:
name: 🧹 Registry Cleanup
runs-on: ubuntu-latest
needs: [prepare, build, scan]
if: inputs.cleanup-old-images == true && (needs.scan.result == 'success' || needs.scan.result == 'skipped') && needs.build.result == 'success'
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.aws-access-key-id }}
aws-secret-access-key: ${{ secrets.aws-secret-access-key }}
aws-region: ${{ inputs.aws-region }}
- name: Clean up old images
run: |
echo "🧹 Cleaning up old images from ECR..."
# Get list of images sorted by pushed date
images=$(aws ecr describe-images \
--repository-name ${{ inputs.ecr-repository }} \
--region ${{ inputs.aws-region }} \
--query 'sort_by(imageDetails, &imagePushedAt)[:-${{ inputs.retention-count }}].imageDigest' \
--output text)
if [ -n "$images" ] && [ "$images" != "None" ]; then
echo "πŸ—‘οΈ Found $(echo $images | wc -w) images to delete"
for digest in $images; do
echo "Deleting image: $digest"
aws ecr batch-delete-image \
--repository-name ${{ inputs.ecr-repository }} \
--region ${{ inputs.aws-region }} \
--image-ids imageDigest=$digest
done
echo "βœ… Cleanup completed"
else
echo "ℹ️ No images to clean up"
fi
# Deployment summary
summary:
name: πŸ“‹ Deployment Summary
runs-on: ubuntu-latest
needs: [prepare, build, scan, cleanup]
if: always() && needs.build.result == 'success'
steps:
- name: Display deployment summary
run: |
echo "πŸ“‹ Docker ECR Deployment Summary"
echo "=================================="
echo "Repository: ${{ inputs.ecr-repository }}"
echo "Region: ${{ inputs.aws-region }}"
echo "Platforms: ${{ needs.prepare.outputs.platforms }}"
echo "Image URI: ${{ needs.build.outputs.image-uri }}"
echo "Tags: ${{ needs.prepare.outputs.image-tags }}"
echo ""
if [ "${{ inputs.vulnerability-scan }}" = "true" ]; then
if [ "${{ needs.scan.result }}" = "success" ]; then
echo "πŸ”’ Security scan: βœ… PASSED"
echo "Security threshold: ${{ inputs.security-threshold }}"
else
echo "πŸ”’ Security scan: ❌ FAILED"
fi
else
echo "πŸ”’ Security scan: ⏭️ SKIPPED"
fi
if [ "${{ inputs.cleanup-old-images }}" = "true" ]; then
if [ "${{ needs.cleanup.result }}" = "success" ]; then
echo "🧹 Registry cleanup: βœ… COMPLETED"
else
echo "🧹 Registry cleanup: ❌ FAILED"
fi
else
echo "🧹 Registry cleanup: ⏭️ SKIPPED"
fi
if [ "${{ inputs.enable-signing }}" = "true" ]; then
echo "✍️ Image signing: βœ… ENABLED"
else
echo "✍️ Image signing: ⏭️ DISABLED"
fi
echo ""
echo "πŸŽ‰ Docker deployment completed successfully!"