Skip to content

Commit 4589f4c

Browse files
detti456arangatang
andauthored
feat: build and publish container image (#126)
* chore: build and publish container image * test: test and fix workflow and Dockerfile for private ECR * fix: disable test workflow * fix: switch to Amazon Linux and generate SBOM * fix: save SBOM in csv format * refactor: move SBOM generation to build step * chore: add sbom attestation to container image * fix: change repository and role names, clean up unused variables * fix: fix version in uv.lock * fix: remove test ECR publish workflow * fix: build docker image from pypi and separate ecr publish workflow * fix: set build permission to read --------- Co-authored-by: Leonardo Araneda Freccero <arangatang@users.noreply.github.com>
1 parent 1b770ee commit 4589f4c

File tree

7 files changed

+395
-55
lines changed

7 files changed

+395
-55
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
---
2+
# This local action builds an image and pushes it to registries
3+
name: "Build and push image"
4+
author: "MCP Proxy for AWS"
5+
description: "Builds an image and pushes it to registries"
6+
7+
inputs:
8+
image:
9+
description: 'The image'
10+
type: string
11+
required: true
12+
version:
13+
default: ''
14+
description: 'The version to associate to the image'
15+
type: string
16+
required: false
17+
public-ecr-role-to-assume:
18+
description: 'The public ECR role to use to push the image'
19+
type: string
20+
required: true
21+
public-ecr-registry-alias:
22+
description: 'The registry alias'
23+
type: string
24+
required: true
25+
public-ecr-aws-region:
26+
default: 'us-east-1'
27+
description: 'The region to login'
28+
type: string
29+
required: false
30+
31+
runs:
32+
using: "composite"
33+
steps:
34+
- name: Docker meta
35+
id: meta
36+
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
37+
with:
38+
images: |
39+
public.ecr.aws/${{ inputs.public-ecr-registry-alias }}/${{ inputs.image }}
40+
# Disable all but the raw and sha
41+
tags: |
42+
type=schedule,enable=false
43+
type=semver,pattern={{raw}},enable=false
44+
type=pep440,pattern={{raw}},enable=false
45+
type=match,pattern=(.*),group=1,enable=false
46+
type=edge,enable=false
47+
type=ref,event=branch,enable=false
48+
type=ref,event=tag,enable=false
49+
type=ref,event=pr,enable=false
50+
type=sha,format=long,enable=true
51+
type=raw,value=latest,enable=true
52+
type=raw,value=${{ inputs.version || github.sha }},enable=${{ (inputs.version && true) || 'false' }}
53+
labels: |
54+
maintainer=MCP Proxy for AWS
55+
org.opencontainers.image.description=MCP Proxy for AWS
56+
org.opencontainers.image.source=https://github.com/${{ github.repository_owner }}/${{ inputs.image }}
57+
org.opencontainers.image.title=aws.${{ inputs.image }}
58+
org.opencontainers.image.url=https://github.com/${{ github.repository_owner }}/${{ inputs.image }}
59+
org.opencontainers.image.version=${{ inputs.version || github.sha }}
60+
org.opencontainers.image.vendor=Amazon Web Services, Inc.
61+
62+
- name: Setup AWS Credentials
63+
id: setup-aws-credentials
64+
uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0
65+
with:
66+
role-to-assume: ${{ inputs.public-ecr-role-to-assume }}
67+
aws-region: ${{ inputs.public-ecr-aws-region }}
68+
role-duration-seconds: 7200
69+
role-session-name: GitHubActions${{ github.run_id }}
70+
mask-aws-account-id: true
71+
72+
- name: Login to Public ECR
73+
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
74+
with:
75+
registry: public.ecr.aws
76+
77+
- name: Set up QEMU
78+
id: setup-qemu
79+
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
80+
81+
- name: Set up Docker Buildx
82+
id: setup-buildx
83+
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
84+
with:
85+
buildkitd-flags: --debug
86+
87+
- name: Build and push by digest
88+
id: build
89+
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
90+
with:
91+
platforms: 'linux/amd64,linux/arm64'
92+
labels: ${{ steps.meta.outputs.labels }}
93+
tags: public.ecr.aws/${{ inputs.public-ecr-registry-alias }}/${{ inputs.image }}
94+
context: .
95+
file: ./Dockerfile
96+
push: true
97+
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
98+
cache-from: type=gha
99+
cache-to: type=gha,mode=max
100+
sbom: true
101+
102+
- name: Export digest
103+
run: |
104+
mkdir -p ${{ runner.temp }}/digests/${{ inputs.image }}
105+
digest="${{ steps.build.outputs.digest }}"
106+
touch "${{ runner.temp }}/digests/${{ inputs.image }}/${digest#sha256:}"
107+
shell: bash
108+
109+
- name: Create manifest list and push
110+
working-directory: ${{ runner.temp }}/digests/${{ inputs.image }}
111+
env:
112+
IMAGE: ${{ inputs.image }}
113+
ALIAS: ${{ inputs.public-ecr-registry-alias }}
114+
DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }}
115+
run: |
116+
echo "DOCKER_METADATA_OUTPUT_JSON=$DOCKER_METADATA_OUTPUT_JSON"
117+
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
118+
$(printf 'public.ecr.aws/'$ALIAS'/'$IMAGE'@sha256:%s ' *)
119+
shell: bash
120+
121+
- name: Inspect image
122+
env:
123+
IMAGE: ${{ inputs.image }}
124+
ALIAS: ${{ inputs.public-ecr-registry-alias }}
125+
VERSION: ${{ steps.meta.outputs.version }}
126+
run: |
127+
docker buildx imagetools inspect public.ecr.aws/$ALIAS/$IMAGE:$VERSION
128+
shell: bash
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
name: Publish to ECR
2+
3+
on:
4+
workflow_call:
5+
workflow_dispatch:
6+
7+
permissions: {}
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: read
14+
outputs:
15+
version: ${{ steps.get-package-version.outputs.version }}
16+
steps:
17+
- name: Checkout code
18+
uses: actions/checkout@v4
19+
with:
20+
persist-credentials: false
21+
22+
- name: Set up uv
23+
uses: astral-sh/setup-uv@v4
24+
25+
- name: Get version from package
26+
id: get-package-version
27+
run: |
28+
set -euo pipefail
29+
30+
# Get version from uv
31+
VERSION="$(uv tree 2>/dev/null | grep mcp-proxy-for-aws | sed -e 's/^.*[[:space:]]v\(.*\)/\1/g' | head -1)"
32+
33+
if [[ -z "$VERSION" ]]; then
34+
echo "::error::Failed to extract version from package" >&2
35+
exit 1
36+
fi
37+
38+
# Validate version format
39+
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
40+
echo "::error::Invalid version format: $VERSION" >&2
41+
exit 1
42+
fi
43+
44+
echo "version=$VERSION" >> $GITHUB_OUTPUT
45+
echo "::debug::Package version: $VERSION"
46+
47+
- name: Set up Docker Buildx
48+
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
49+
50+
- name: Build and export to Docker
51+
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
52+
with:
53+
context: .
54+
file: ./Dockerfile
55+
load: true
56+
tags: mcp-proxy-for-aws:${{ steps.get-package-version.outputs.version }}
57+
cache-from: type=gha
58+
cache-to: type=gha,mode=max
59+
60+
- name: Generate CycloneDX SBOM with Syft
61+
uses: anchore/sbom-action@v0
62+
with:
63+
image: mcp-proxy-for-aws:${{ steps.get-package-version.outputs.version }}
64+
format: cyclonedx-json
65+
output-file: sbom.cyclonedx.json
66+
67+
- name: Install CycloneDX CLI
68+
run: |
69+
wget -q https://github.com/CycloneDX/cyclonedx-cli/releases/latest/download/cyclonedx-linux-x64 -O cyclonedx
70+
chmod +x cyclonedx
71+
sudo mv cyclonedx /usr/local/bin/
72+
73+
- name: Convert SBOM to CSV
74+
run: |
75+
cyclonedx convert --input-file sbom.cyclonedx.json --input-format json --output-format csv --output-file SBOM-${{ steps.get-package-version.outputs.version }}.csv
76+
77+
- name: Upload SBOM artifact
78+
uses: actions/upload-artifact@v4
79+
with:
80+
name: sbom-${{ steps.get-package-version.outputs.version }}
81+
path: SBOM-${{ steps.get-package-version.outputs.version }}.csv
82+
retention-days: 90
83+
84+
deploy:
85+
needs: build
86+
runs-on: ubuntu-latest
87+
environment:
88+
name: ecr
89+
url: https://gallery.ecr.aws/mcp-proxy-for-aws/mcp-proxy-for-aws
90+
permissions:
91+
id-token: write
92+
contents: read
93+
steps:
94+
- name: Checkout code
95+
uses: actions/checkout@v4
96+
with:
97+
persist-credentials: false
98+
99+
- name: Build and Publish Container
100+
id: build-and-publish
101+
uses: ./.github/actions/build-and-push-container-image
102+
with:
103+
image: mcp-proxy-for-aws
104+
version: ${{ needs.build.outputs.version }}
105+
public-ecr-role-to-assume: ${{ secrets.PublishPublicECRArn }}
106+
public-ecr-registry-alias: mcp-proxy-for-aws
107+
public-ecr-aws-region: us-east-1

.github/workflows/pypi-publish-on-release.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ jobs:
5959
url: https://pypi.org/p/mcp-proxy-for-aws
6060
permissions:
6161
id-token: write
62+
contents: read
6263
steps:
6364
- name: Download distribution packages
6465
uses: actions/download-artifact@v5
@@ -71,3 +72,10 @@ jobs:
7172

7273
- name: Publish to PyPI
7374
run: uv publish
75+
76+
trigger-ecr-publish:
77+
needs: [build, deploy]
78+
permissions:
79+
contents: read
80+
uses: ./.github/workflows/ecr-publish-on-release.yml
81+
secrets: inherit

Dockerfile

Lines changed: 27 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -12,66 +12,38 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
# dependabot should continue to update this to the latest hash.
16-
FROM public.ecr.aws/docker/library/python:3.14.0-alpine3.22@sha256:8373231e1e906ddfb457748bfc032c4c06ada8c759b7b62d9c73ec2a3c56e710 AS uv
17-
18-
# Install the project into `/app`
19-
WORKDIR /app
20-
21-
# Enable bytecode compilation
22-
ENV UV_COMPILE_BYTECODE=1
23-
24-
# Copy from the cache instead of linking since it's a mounted volume
25-
ENV UV_LINK_MODE=copy
26-
27-
# Prefer the system python
28-
ENV UV_PYTHON_PREFERENCE=only-system
29-
30-
# Run without updating the uv.lock file like running with `--frozen`
31-
ENV UV_FROZEN=true
32-
33-
# Copy the required files first
34-
COPY pyproject.toml uv.lock ./
35-
36-
# Install the project's dependencies using the lockfile and settings
37-
RUN --mount=type=cache,target=/root/.cache/uv \
38-
pip install uv && \
39-
uv sync --frozen --no-install-project --no-dev --no-editable
40-
41-
# Then, add the rest of the project source code and install it
42-
# Installing separately from its dependencies allows optimal layer caching
43-
COPY . /app
44-
RUN --mount=type=cache,target=/root/.cache/uv \
45-
uv sync --frozen --no-dev --no-editable
46-
47-
# Make the directory just in case it doesn't exist
48-
RUN mkdir -p /root/.local
49-
50-
# dependabot should continue to update this to the latest hash.
51-
FROM public.ecr.aws/docker/library/python:3.14.0-alpine3.22@sha256:8373231e1e906ddfb457748bfc032c4c06ada8c759b7b62d9c73ec2a3c56e710
52-
53-
# Place executables in the environment at the front of the path and include other binaries
54-
ENV PATH="/app/.venv/bin:$PATH:/usr/sbin"
55-
56-
# Install lsof for the healthcheck
57-
# Install other tools as needed for the MCP server
58-
# Add non-root user and ability to change directory into /root
59-
RUN apk update && \
60-
apk --no-cache add lsof && \
61-
addgroup -S app && \
62-
adduser -S app -G app -h /app && \
63-
chmod o+x /root
64-
65-
# Get the project from the uv layer
66-
COPY --from=uv --chown=app:app /root/.local /root/.local
67-
COPY --from=uv --chown=app:app /app/.venv /app/.venv
15+
# Using Amazon Linux for consistency and compliance
16+
FROM public.ecr.aws/amazonlinux/amazonlinux:latest
17+
18+
# Python optimization
19+
ENV PYTHONUNBUFFERED=1 \
20+
PIP_NO_CACHE_DIR=1 \
21+
PIP_DISABLE_PIP_VERSION_CHECK=1
22+
23+
# Install runtime dependencies and create application user
24+
RUN yum update -y && \
25+
yum install -y \
26+
python3.13 \
27+
python3.13-pip \
28+
ca-certificates \
29+
shadow-utils \
30+
lsof && \
31+
yum clean all && \
32+
update-ca-trust && \
33+
groupadd -r app && \
34+
useradd -r -g app -d /app app
35+
36+
# Install mcp-proxy-for-aws from PyPI
37+
RUN python3.13 -m pip install mcp-proxy-for-aws
6838

6939
# Get healthcheck script
7040
COPY ./docker-healthcheck.sh /usr/local/bin/docker-healthcheck.sh
7141

7242
# Run as non-root
7343
USER app
7444

75-
# When running the container, add --db-path and a bind mount to the host's db file
76-
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "docker-healthcheck.sh" ]
45+
# Health check to monitor container status
46+
HEALTHCHECK --interval=60s --timeout=10s --start-period=10s --retries=3 CMD ["docker-healthcheck.sh"]
47+
48+
# Application entrypoint
7749
ENTRYPOINT ["mcp-proxy-for-aws"]

0 commit comments

Comments
 (0)