Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions .github/workflows/build-and-push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# Build and Push to ECR
# This workflow builds the Docker image and pushes it to AWS ECR
# Triggered on pushes to stage branch (tag-based releases can be enabled later)
#
# On pull_request: build-only (validates Dockerfile, no AWS auth required)
# On push to stage: build + push to ECR (requires OIDC role below)
#
# Authentication: GitHub OIDC
# Prerequisites:
# 1. AWS OIDC provider for token.actions.githubusercontent.com (already exists)
# 2. IAM role with trust policy condition:
# "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" }
# "StringLike": { "token.actions.githubusercontent.com:sub": "repo:thunderbird/addons-server:ref:refs/heads/stage" }
# 3. Repository variable: AWS_ROLE_ARN (role ARN from step 2)
# Note: Can later be moved to an environment for stricter controls
# See: https://tinyurl.com/ghAwsOidc
#
# Publishing is gated on BOTH:
# - Event type (push, not pull_request)
# - vars.AWS_ROLE_ARN is set
# If either condition fails, then build succeeds but publish is skipped
#
# Required IAM permissions for the OIDC role:
# - ecr:GetAuthorizationToken
# - ecr:BatchCheckLayerAvailability
# - ecr:BatchGetImage
# - ecr:CompleteLayerUpload
# - ecr:DescribeImages
# - ecr:InitiateLayerUpload
# - ecr:GetDownloadUrlForLayer
# - ecr:ListImages
# - ecr:UploadLayerPart
# - ecr:PutImage

name: Build and Push to ECR

on:
push:
branches:
- stage
# tags:
# - 'v*' # Uncomment when tag-based releases are defined
pull_request:
branches:
- stage
- master

env:
AWS_REGION: us-west-2
ECR_REPOSITORY: atn-stage-addons-server
AWS_ACCOUNT_ID: "768512802988"

jobs:
# Build job: always runs, validates Dockerfile, no AWS permissions needed
build:
name: Build
runs-on: ubuntu-latest
permissions:
contents: read
# Note: no id-token here - minimum privilege for PR/build-only scenarios

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_REPOSITORY }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix=
# type=semver,pattern={{version}} # Enable when tag triggers are added
# type=semver,pattern={{major}}.{{minor}}

- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.ecs
push: false
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
OLYMPIA_UID=9500
OLYMPIA_GID=9500

# Informational job: shows why publishing skipped when not configured role
publish-disabled:
name: Publish (skipped - AWS_ROLE_ARN not set)
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/stage' && vars.AWS_ROLE_ARN == ''
steps:
- name: Publishing not configured
run: |
echo "::notice::Publish skipped: AWS_ROLE_ARN repo variable not set (OIDC role not configured yet)"
echo "See workflow header comments for IAM role setup instructions"

# Publish job: only runs on push to stage when OIDC role is configured
publish:
name: Publish to ECR
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/stage' && vars.AWS_ROLE_ARN != ''
concurrency:
group: ecr-stage-publish
cancel-in-progress: true
permissions:
contents: read
id-token: write # Required for OIDC authn - only granted when actually publishing

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}

- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2

- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_REPOSITORY }}
tags: |
type=ref,event=branch
type=sha,prefix=
type=raw,value=stage-latest

- name: Build and push Docker image
id: build-image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.ecs
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
OLYMPIA_UID=9500
OLYMPIA_GID=9500

# Generate build metadata (future: bake into image or upload to S3 for traceability)
- name: Generate version.json
run: |
echo '{
"commit": "${{ github.sha }}",
"version": "${{ github.ref_name }}",
"source": "https://github.com/${{ github.repository }}",
"build": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}' > version.json
cat version.json

- name: Image digest
run: echo "Image pushed with digest ${{ steps.build-image.outputs.digest }}"

# Deploy to ECS (optional - we would uncomment this when ready, or move to separate deploy.yml)
# deploy:
# name: Deploy to ECS
# needs: publish
# runs-on: ubuntu-latest
# permissions:
# contents: read
# id-token: write
#
# steps:
# - name: Configure AWS credentials (OIDC)
# uses: aws-actions/configure-aws-credentials@v4
# with:
# role-to-assume: ${{ vars.AWS_ROLE_ARN }}
# aws-region: ${{ env.AWS_REGION }}
#
# - name: Update ECS services
# run: |
# for service in web worker versioncheck; do
# aws ecs update-service \
# --cluster thunderbird-addons-stage-${service}-cluster \
# --service thunderbird-addons-stage-${service}-service \
# --force-new-deployment
# done
196 changes: 196 additions & 0 deletions Dockerfile.ecs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# Dockerfile.ecs
# ECS Fargate-optimised Dockerfile for Thunderbird Add-ons Server
#
# This Dockerfile is intended for production deployment on AWS ECS Fargate
# Key features:
# - Non-root user execution (olympia)
# - Environment-driven configuration
# - Built-in health check
# - Predictable entrypoint supporting multiple service modes
#
# Service modes (via ENTRYPOINT):
# web - Django application via uWSGI (default)
# worker - Celery background workers
# versioncheck - Versioncheck API service
# scheduler - Celery beat scheduler
# manage - Django management commands
#
# Required environment variables:
# DJANGO_SETTINGS_MODULE - Django settings module (default: settings)
# DATABASE_URL - MySQL connection string
# CELERY_BROKER_URL - RabbitMQ connection string
# CELERY_RESULT_BACKEND - Redis connection string
# ELASTICSEARCH_LOCATION - OpenSearch/Elasticsearch host:port
# MEMCACHE_LOCATION - Memcached host:port
#
# Optional environment variables:
# UWSGI_PROCESSES - Number of uWSGI worker processes (default: 4)
# UWSGI_THREADS - Threads per process (default: 4)
# UWSGI_PORT - HTTP port (default: 8000)
# CELERY_CONCURRENCY - Celery worker concurrency (default: 4)
# CELERY_QUEUES - Comma-separated queue names
# CELERY_LOGLEVEL - Celery log level (default: info)
# SENTRY_DSN - Sentry error reporting DSN
#
# Build:
# docker build -f Dockerfile.ecs -t addons-server:latest .
#
# Run examples:
# docker run -e DATABASE_URL=... addons-server:latest web
# docker run -e DATABASE_URL=... addons-server:latest worker
# docker run -e DATABASE_URL=... addons-server:latest versioncheck

FROM python:3.6-slim-stretch

LABEL maintainer="Thunderbird Team"
LABEL org.opencontainers.image.title="Thunderbird Add-ons Server"
LABEL org.opencontainers.image.description="ECS Fargate deployment image for addons.thunderbird.net"

# Build arguments
ARG OLYMPIA_UID=9500
ARG OLYMPIA_GID=9500

ENV PYTHON_VERSION_MAJOR=3
ENV SWIG_FEATURES="-D__x86_64__"
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV LANG=en_US.UTF-8
ENV LC_ALL=en_US.UTF-8

# Default runtime environment (can be overridden at container start)
ENV DJANGO_SETTINGS_MODULE=settings
ENV UWSGI_PROCESSES=4
ENV UWSGI_THREADS=4
ENV UWSGI_PORT=8000
ENV CELERY_CONCURRENCY=4
ENV CELERY_LOGLEVEL=info

# Create olympia user and group (non-root execution)
RUN groupadd -g ${OLYMPIA_GID} olympia && \
useradd -u ${OLYMPIA_UID} -g ${OLYMPIA_GID} -s /bin/bash -m olympia

# Update the main repositories to the archived repository (Stretch is EOL)
RUN echo "deb http://archive.debian.org/debian stretch main contrib non-free" > /etc/apt/sources.list

# Add nodesource repository and requirements
ADD docker/nodesource.gpg.key /etc/pki/gpg/GPG-KEY-nodesource
RUN apt-get update && apt-get install -y \
apt-transport-https \
gnupg2 \
&& rm -rf /var/lib/apt/lists/*
RUN cat /etc/pki/gpg/GPG-KEY-nodesource | apt-key add -
ADD docker/debian-stretch-nodesource-repo /etc/apt/sources.list.d/nodesource.list
ADD docker/debian-stretch-backports-repo /etc/apt/sources.list.d/backports.list

# Install system dependencies
# Note: Debian Stretch is EOL, some packages have dependency issues
# We skip libssl-dev as it has broken deps in the archive; cryptography is pre-built in pip
RUN apt-get update && apt-get install -y \
# General dependencies
bash-completion \
build-essential \
curl \
libjpeg-dev \
libsasl2-dev \
libxml2-dev \
libxslt-dev \
locales \
zlib1g-dev \
libffi-dev \
libmagic-dev \
nodejs \
# Git for git-checkout dependencies
git \
# MySQL client and development headers
mysql-client \
default-libmysqlclient-dev \
swig \
gettext \
# SVG rendering for theme previews
librsvg2-bin \
# PNG optimisation for uploaded images
pngcrush \
# Makefile and UI tests require uuid
uuid \
# GeoIP lookups
libmaxminddb0 \
libmaxminddb-dev \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean

# Install tini for proper signal handling in containers
# tini is not available in Debian Stretch apt repos, so we download from GitHub
ARG TINI_VERSION=v0.19.0
RUN curl -fsSL https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini -o /usr/bin/tini \
&& chmod +x /usr/bin/tini

# Compile required locale
RUN localedef -i en_US -f UTF-8 en_US.UTF-8

# Create application directories with correct ownership
RUN mkdir -p /data/olympia /app /var/log/olympia /var/run/olympia && \
chown -R olympia:olympia /data /app /var/log/olympia /var/run/olympia

# Copy version.json first (for cache invalidation)
COPY --chown=olympia:olympia version.json /app/version.json

# Copy application code
COPY --chown=olympia:olympia . /data/olympia
WORKDIR /data/olympia

# Install Python dependencies (as root for system packages then we fix ownership)
RUN pip3 install --no-cache-dir --exists-action=w --no-deps -r requirements/system.txt && \
pip3 install --no-cache-dir --exists-action=w --no-deps -r requirements/prod_py3.txt && \
pip3 install --no-cache-dir --exists-action=w --no-deps -e .

# Link uwsgi to expected paths
RUN ln -s /usr/local/bin/uwsgi /usr/bin/uwsgi && \
ln -s /usr/bin/uwsgi /usr/sbin/uwsgi

# Install uwsgi dogstatsd plugin for metrics
WORKDIR /usr/lib/uwsgi/plugins
RUN uwsgi --build-plugin https://github.com/Datadog/uwsgi-dogstatsd && \
rm -rf uwsgi-dogstatsd

# Build static assets
WORKDIR /data/olympia
RUN echo "from olympia.lib.settings_base import *\n\
LESS_BIN = 'node_modules/less/bin/lessc'\n\
CLEANCSS_BIN = 'node_modules/clean-css-cli/bin/cleancss'\n\
UGLIFY_BIN = 'node_modules/uglify-js/bin/uglifyjs'\n\
FXA_CONFIG = {'default': {}, 'internal': {}}\n"\
> settings_local.py

RUN DJANGO_SETTINGS_MODULE='settings_local' locale/compile-mo.sh locale

RUN npm install \
&& make -f Makefile-docker copy_node_js \
&& DJANGO_SETTINGS_MODULE='settings_local' python manage.py compress_assets \
&& DJANGO_SETTINGS_MODULE='settings_local' python manage.py generate_jsi18n_files \
&& DJANGO_SETTINGS_MODULE='settings_local' python manage.py collectstatic --noinput

# Clean up build-time files
RUN rm -f settings_local.py settings_local.pyc && \
rm -rf /root/.npm /root/.cache && \
find /data/olympia -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true && \
find /data/olympia -type f -name "*.pyc" -delete 2>/dev/null || true

# Make entrypoint executable
RUN chmod +x /data/olympia/docker/docker-entrypoint.sh

# Switch to non-root user
USER olympia

# Expose the application port
EXPOSE 8000

# Health check - calls the Django monitor endpoint
# Adjust interval/timeout based on application startup time
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:${UWSGI_PORT}/services/monitor.json || exit 1

# Use tini as init system for proper signal handling
ENTRYPOINT ["/usr/bin/tini", "--", "/data/olympia/docker/docker-entrypoint.sh"]

# Default command: run web server
CMD ["web"]
Loading