From ba6b917e7e1f594cdda263ca06d7ba713eb28ddc Mon Sep 17 00:00:00 2001 From: Trey Pendragon Date: Thu, 12 Sep 2024 09:45:31 -0700 Subject: [PATCH 01/14] Create docker container. Commands run (no changes): 1. `bundle add dockerfile-rails --optimistic --group development` 1. `bin/rails generate dockerfile --nginx --parallel` 1. `rm -rf .yarn` 1. `rm .yarnrc` 1. Copy build+publish action from DPUL-C --- .dockerignore | 44 +++++++ .github/workflows/build-docker-image.yml | 147 +++++++++++++++++++++++ .node-version | 1 + Dockerfile | 134 +++++++++++++++++++++ Gemfile | 2 + Gemfile.lock | 6 + bin/docker-entrypoint | 8 ++ config/dockerfile.yml | 6 + package.json | 6 +- 9 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/build-docker-image.yml create mode 100644 .node-version create mode 100644 Dockerfile create mode 100755 bin/docker-entrypoint create mode 100644 config/dockerfile.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..a3797213 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,44 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. + +# Ignore git directory. +/.git/ + +# Ignore bundler config. +/.bundle + +# Ignore all environment files (except templates). +/.env* +!/.env*.erb + +# Ignore all default key files. +/config/master.key +/config/credentials/*.key + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/.keep + +# Ignore assets. +/node_modules/ +/app/assets/builds/* +!/app/assets/builds/.keep +/public/assets + +# Vite Ruby +/public/vite* +yarn-error.log +# Vite uses dotenv and suggests to ignore local-only env files. See +# https://vitejs.dev/guide/env-and-mode.html#env-files +*.local diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml new file mode 100644 index 00000000..37d28bd1 --- /dev/null +++ b/.github/workflows/build-docker-image.yml @@ -0,0 +1,147 @@ +name: Create and publish a Docker image + +on: + push: + branches: + - main + pull_request: + branches: + - main + +# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. +jobs: + build-and-push-image: + runs-on: ubuntu-latest + # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. + permissions: + contents: read + packages: write + # + steps: + - name: Checkout repository + uses: actions/checkout@v4 + # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha + env: + DOCKER_METADATA_PR_HEAD_SHA: true + # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. + # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. + # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. + - name: Build and push Docker image + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + file: Dockerfile + container-vuln-scan: + needs: build-and-push-image + runs-on: ubuntu-latest + if: + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha + env: + DOCKER_METADATA_PR_HEAD_SHA: true + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.20.0 + id: runscanner + continue-on-error: true + with: + image-ref: 'ghcr.io/pulibrary/imagecat-rails:${{ steps.meta.outputs.version }}' + format: 'table' + exit-code: '1' + ignore-unfixed: true + vuln-type: 'os,library' + severity: 'CRITICAL,HIGH' + output: 'vulnerabilities.table' + - name: Set variables + id: scanner + if: ${{ always() }} + run: | + EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) + echo "results<<$EOF" >> $GITHUB_OUTPUT + echo "$(cat vulnerabilities.table)" >> $GITHUB_OUTPUT + echo "$EOF" >> $GITHUB_OUTPUT + - name: Output variable + if: ${{ always() }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + SCANNER_OUTPUTS: ${{ steps.scanner.outputs.results }} + run: echo "${{ env.SCANNER_OUTPUTS }}" + - name: Find Comment for scan + if: github.event_name == 'pull_request' + uses: peter-evans/find-comment@v3 + id: fc + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-includes: 'Container Scanning Status: ' + - name: Create or update comment + if: github.event_name == 'pull_request' + uses: peter-evans/create-or-update-comment@v4 + env: + SCANNER_OUTPUTS: ${{ steps.scanner.outputs.results }} + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body: | + ## Container Scanning Status: ${{ steps.runscanner.outcome != 'success' && '❌ Failure' || '✅ Success' }} + ``` + ${{ env.SCANNER_OUTPUTS }} + ``` + edit-mode: replace + - name: Create issue + if: steps.runscanner.outcome != 'success' && github.event_name != 'pull_request' + uses: JasonEtco/create-an-issue@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + SCANNER_OUTPUTS: ${{ steps.scanner.outputs.results }} + with: + filename: .github/failed-vuln-check.md + update_existing: true + - name: Find existing security issue + id: issues + if: steps.runscanner.outcome == 'success' && github.event_name != 'pull_request' + uses: lee-dohm/select-matching-issues@v1 + with: + query: 'Container Vulnerability Scanner Failed is:open ' + token: ${{ secrets.GITHUB_TOKEN }} + - name: Close found issues + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + if: steps.runscanner.outcome == 'success' && github.event_name != 'pull_request' + run: cat ${{ steps.issues.outputs.path }} | xargs gh issue close -c 'Container Scan Passing on Merge to Main' diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..02c8b485 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +18.18.0 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..2495b273 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,134 @@ +# syntax = docker/dockerfile:1 + +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile +ARG RUBY_VERSION=3.2.0 +FROM ruby:$RUBY_VERSION-slim as base + +# Rails app lives here +WORKDIR /rails + +# Set production environment +ENV BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development:test" \ + RAILS_ENV="production" + +# Update gems and bundler +RUN gem update --system --no-document && \ + gem install -N bundler + + +# Throw-away build stages to reduce size of final image +FROM base as prebuild + +# Install packages needed to build gems and node modules +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential curl libpq-dev node-gyp pkg-config python-is-python3 + + +FROM prebuild as node + +# Install JavaScript dependencies +ARG NODE_VERSION=18.18.0 +ARG YARN_VERSION=1.22.21 +ENV PATH=/usr/local/node/bin:$PATH +RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \ + /tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \ + npm install -g yarn@$YARN_VERSION && \ + rm -rf /tmp/node-build-master + +# Install node modules +COPY --link package.json yarn.lock ./ +RUN yarn install --frozen-lockfile + + +FROM prebuild as build + +# Install application gems +COPY --link Gemfile Gemfile.lock ./ +RUN bundle install && \ + bundle exec bootsnap precompile --gemfile && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git + +# Copy node modules +COPY --from=node /rails/node_modules /rails/node_modules +COPY --from=node /usr/local/node /usr/local/node +ENV PATH=/usr/local/node/bin:$PATH + +# Copy application code +COPY --link . . + +# Precompile bootsnap code for faster boot times +RUN bundle exec bootsnap precompile app/ lib/ + +# Precompiling assets for production without requiring secret RAILS_MASTER_KEY +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile + + +# Final stage for app image +FROM base + +# Install packages needed for deployment +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl nginx postgresql-client ruby-foreman && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# configure nginx +RUN gem install foreman && \ + sed -i 's|pid /run|pid /rails/tmp/pids|' /etc/nginx/nginx.conf && \ + sed -i 's/access_log\s.*;/access_log \/dev\/stdout;/' /etc/nginx/nginx.conf && \ + sed -i 's/error_log\s.*;/error_log \/dev\/stderr info;/' /etc/nginx/nginx.conf + +COPY <<-"EOF" /etc/nginx/sites-available/default +server { + listen 3000 default_server; + listen [::]:3000 default_server; + access_log /dev/stdout; + + root /rails/public; + + location /cable { + proxy_pass http://localhost:8082/cable; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + } + + location / { + try_files $uri @backend; + } + + location @backend { + proxy_pass http://localhost:3001; + proxy_set_header Host $http_host; + } +} +EOF + +# Copy built artifacts: gems, application +COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --from=build /rails /rails + +# Run and own only the runtime files as a non-root user for security +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ + chown 1000:1000 /var/lib/nginx /var/log/nginx/* && \ + chown -R 1000:1000 db log storage tmp +USER 1000:1000 + +# Deployment options +ENV PORT="3001" + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Build a Procfile for production use +COPY <<-"EOF" /rails/Procfile.prod +nginx: /usr/sbin/nginx -g "daemon off;" +rails: ./bin/rails server -p 3001 +EOF + +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 +CMD ["foreman", "start", "--procfile=Procfile.prod"] diff --git a/Gemfile b/Gemfile index aac3544b..a1da74f2 100644 --- a/Gemfile +++ b/Gemfile @@ -88,3 +88,5 @@ group :development do # Speed up commands on slow machines / big apps [https://github.com/rails/spring] # gem "spring" end + +gem "dockerfile-rails", ">= 1.6", :group => :development diff --git a/Gemfile.lock b/Gemfile.lock index 2719cedd..39bf996b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -137,6 +137,8 @@ GEM reline (>= 0.3.8) diff-lcs (1.5.1) docile (1.4.1) + dockerfile-rails (1.6.17) + rails (>= 3.0.0) drb (2.2.1) dry-cli (1.1.0) ed25519 (1.3.0) @@ -206,6 +208,8 @@ GEM net-protocol net-ssh (7.2.3) nio4r (2.7.3) + nokogiri (1.16.7-aarch64-linux) + racc (~> 1.4) nokogiri (1.16.7-arm64-darwin) racc (~> 1.4) nokogiri (1.16.7-x86_64-darwin) @@ -384,6 +388,7 @@ GEM zeitwerk (2.6.17) PLATFORMS + aarch64-linux arm64-darwin-21 x86_64-darwin-20 x86_64-linux @@ -399,6 +404,7 @@ DEPENDENCIES capybara coveralls_reborn (~> 0.27.0) debug + dockerfile-rails (>= 1.6) ed25519 importmap-rails jbuilder diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint new file mode 100755 index 00000000..acec955d --- /dev/null +++ b/bin/docker-entrypoint @@ -0,0 +1,8 @@ +#!/bin/bash -e + +# If running the production procfile then create or migrate existing database +if [ "${*}" == "foreman start --procfile=Procfile.prod" ]; then + ./bin/rails db:prepare +fi + +exec "${@}" diff --git a/config/dockerfile.yml b/config/dockerfile.yml new file mode 100644 index 00000000..d4700c20 --- /dev/null +++ b/config/dockerfile.yml @@ -0,0 +1,6 @@ +# generated by dockerfile-rails + +--- +options: + nginx: true + parallel: true diff --git a/package.json b/package.json index b9024fa4..8a4b2fa6 100644 --- a/package.json +++ b/package.json @@ -10,5 +10,9 @@ "sass": "^1.69.5", "vue": "^3.4.0", "vue-loader": "^15.7.0" + }, + "packageManager": "yarn@1.22.21", + "scripts": { + "build": "vite build --outDir public" } -} +} \ No newline at end of file From 9cd37c03976d1a19dbcf8af91b316adc66a35d77 Mon Sep 17 00:00:00 2001 From: Trey Pendragon Date: Thu, 12 Sep 2024 09:47:52 -0700 Subject: [PATCH 02/14] Rubocop. --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index a1da74f2..f16ac329 100644 --- a/Gemfile +++ b/Gemfile @@ -89,4 +89,4 @@ group :development do # gem "spring" end -gem "dockerfile-rails", ">= 1.6", :group => :development +gem 'dockerfile-rails', '>= 1.6', group: :development From 186c9b6a6f09b262f0f265689b9608f61cd99efb Mon Sep 17 00:00:00 2001 From: Trey Pendragon Date: Thu, 12 Sep 2024 09:56:11 -0700 Subject: [PATCH 03/14] Too many vulns, use a more recent image. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2495b273..278cb40f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # syntax = docker/dockerfile:1 # Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile -ARG RUBY_VERSION=3.2.0 +ARG RUBY_VERSION=3.2 FROM ruby:$RUBY_VERSION-slim as base # Rails app lives here From 89b47a627e0b237e6f411570301a836916e188f2 Mon Sep 17 00:00:00 2001 From: Trey Pendragon Date: Thu, 12 Sep 2024 11:06:47 -0700 Subject: [PATCH 04/14] Only scan OS vulnerabilities We have dependabot for libraries. --- .github/workflows/build-docker-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index 37d28bd1..943da7e7 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -82,7 +82,7 @@ jobs: format: 'table' exit-code: '1' ignore-unfixed: true - vuln-type: 'os,library' + vuln-type: 'os' severity: 'CRITICAL,HIGH' output: 'vulnerabilities.table' - name: Set variables From df6f085198c65edb20f2557dbf78faf8d1340af9 Mon Sep 17 00:00:00 2001 From: Trey Pendragon Date: Thu, 12 Sep 2024 11:38:02 -0700 Subject: [PATCH 05/14] Add staging HCL --- bin/deploy | 66 +++++++++++++++++++++++++++++++++++++++ config/deploy/staging.hcl | 54 ++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100755 bin/deploy create mode 100644 config/deploy/staging.hcl diff --git a/bin/deploy b/bin/deploy new file mode 100755 index 00000000..df454b4f --- /dev/null +++ b/bin/deploy @@ -0,0 +1,66 @@ +#!/bin/bash +ENV=$1 +BRANCH_NAME="${BRANCH:-main}" +REPOSITORY="${REPO:-imagecat-rails}" +JOB_NAME="${JOBNAME:-imagecat}" + +# Make sure we're on VPN +if ! nslookup nomad-host-prod1.lib.princeton.edu 2>&1 > /dev/null +then + echo "Unable to connect to nomad-host-prod1. Ensure you're on VPN." + exit 1 +fi + +## Get Github Token +if ! command -v gh &> /dev/null +then + if [ -z "$GITHUB_TOKEN" ] + then + echo "gh must be installed or a token passed with GITHUB_TOKEN. Run 'brew install gh'." + exit 1 + fi +fi + +GH_TOKEN="${GITHUB_TOKEN:-$(gh auth token 2> /dev/null)}" + +if [ "$GH_TOKEN" = "" ] +then + echo "Github token not set. Run 'gh auth login' and follow the directions." + exit 1 +fi + +if [[ -z "${ENV}" ]]; +then + echo "Missing Environment. Command: 'BRANCH=main ./bin/deploy staging'." + exit +fi + +# Create Github Deployment +DEPLOY_OUTPUT=$(curl -s -X POST -H "Accept: application/vnd.github+json-H" -H "Content-Type: application/x-www-form-urlencoded" -H "Authorization: Bearer ${GH_TOKEN}" --data "{\"ref\":\"${BRANCH_NAME}\",\"description\":\"Deploy from Nomad script\", \"auto_merge\": false, \"environment\": \"${ENV}\", \"required_contexts\": [] }" "https://api.github.com/repos/pulibrary/${REPOSITORY}/deployments") +regex='"id": ([0-9]+),' +[[ $DEPLOY_OUTPUT =~ $regex ]] +DEPLOY_ID=${BASH_REMATCH[1]} + +if [[ -z "${DEPLOY_ID}" ]] +then + echo "Unable to fetch Deploy ID." + exit 1 +fi + +# Create "Started" Deployment Status +curl -s -X POST -H "Accept: application/vnd.github+json-H" -H "Content-Type: application/x-www-form-urlencoded" -H "Authorization: Bearer ${GH_TOKEN}" --data "{\"environment\":\"${ENV}\",\"state\":\"in_progress\",\"log_url\": \"https://nomad.lib.princeton.edu/ui/jobs/${JOB_NAME}-${ENV}\", \"description\":\"Deployment started.\"}" "https://api.github.com/repos/pulibrary/${REPOSITORY}/deployments/${DEPLOY_ID}/statuses" > /dev/null + +# Deploy using nomad-host-prod1, which has the nomad management key. +ssh deploy@nomad-host-prod1.lib.princeton.edu << EOF + curl -s "https://raw.githubusercontent.com/pulibrary/${REPOSITORY}/${BRANCH_NAME}/config/deploy/${ENV}.hcl" | nomad job run -var "branch_or_sha=sha-$(git ls-remote https://github.com/pulibrary/${REPOSITORY}.git ${BRANCH_NAME} | awk '{ print substr($1,1,7) }')" - +EOF +retcode=$? + +if [ $retcode -eq 0 ] +then + # Create "Completed Successfully" Deployment Status + curl -s -X POST -H "Accept: application/vnd.github+json-H" -H "Content-Type: application/x-www-form-urlencoded" -H "Authorization: Bearer ${GH_TOKEN}" --data "{\"environment\":\"${ENV}\",\"state\":\"success\",\"log_url\": \"https://nomad.lib.princeton.edu/ui/jobs/${JOB_NAME}-${ENV}\", \"description\":\"Deployment finished successfully.\"}" "https://api.github.com/repos/pulibrary/${REPOSITORY}/deployments/${DEPLOY_ID}/statuses" > /dev/null +else + # Create "Failed" Deployment Status + curl -s -X POST -H "Accept: application/vnd.github+json-H" -H "Content-Type: application/x-www-form-urlencoded" -H "Authorization: Bearer ${GH_TOKEN}" --data "{\"environment\":\"${ENV}\",\"state\":\"failure\",\"log_url\": \"https://nomad.lib.princeton.edu/ui/jobs/${JOB_NAME}-${ENV}\", \"description\":\"Deployment failed.\"}" "https://api.github.com/repos/pulibrary/${REPOSITORY}/deployments/${DEPLOY_ID}/statuses" > /dev/null +fi diff --git a/config/deploy/staging.hcl b/config/deploy/staging.hcl new file mode 100644 index 00000000..dde58db9 --- /dev/null +++ b/config/deploy/staging.hcl @@ -0,0 +1,54 @@ +variable "branch_or_sha" { + type = string + default = "main" +} +job "imagecat-staging" { + region = "global" + datacenters = ["dc1"] + node_pool = "staging" + type = "service" + group "web" { + count = 2 + network { + port "http" { to = 3000 } + } + service { + port = "http" + check { + type = "http" + port = "http" + path = "/" + interval = "10s" + timeout = "1s" + } + } + task "webserver" { + driver = "docker" + config { + image = "ghcr.io/pulibrary/imagecat-rails:${ var.branch_or_sha }" + ports = ["http"] + force_pull = true + } + template { + destination = "${NOMAD_SECRETS_DIR}/env.vars" + env = true + change_mode = "restart" + data = < Date: Thu, 12 Sep 2024 11:39:29 -0700 Subject: [PATCH 06/14] Podman. --- config/deploy/staging.hcl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/deploy/staging.hcl b/config/deploy/staging.hcl index dde58db9..609ce75e 100644 --- a/config/deploy/staging.hcl +++ b/config/deploy/staging.hcl @@ -23,7 +23,7 @@ job "imagecat-staging" { } } task "webserver" { - driver = "docker" + driver = "podman" config { image = "ghcr.io/pulibrary/imagecat-rails:${ var.branch_or_sha }" ports = ["http"] From 95ba1064444fc5752be69c1e954f26afcc8f3df2 Mon Sep 17 00:00:00 2001 From: Trey Pendragon Date: Thu, 12 Sep 2024 12:00:44 -0700 Subject: [PATCH 07/14] Add docker compose. There was a missing pids directory that was necessary for launching - this docker-compose file let me debug that. --- README.md | 8 ++++---- bin/deploy | 2 +- docker-compose.yml | 34 ++++++++++++++++++++++++++++++++++ tmp/pids/.keep | 0 4 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 docker-compose.yml create mode 100644 tmp/pids/.keep diff --git a/README.md b/README.md index 76c5d9c4..db254c75 100644 --- a/README.md +++ b/README.md @@ -35,13 +35,13 @@ Run the `bin/rails server` or `rails s` command, then in a browser connect to [l ## Deploying -You may use Capistrano on the command line. +You can deploy from the command line: -`BRANCH=branch_name bundle exec cap staging deploy` +`BRANCH=branch_name ./bin/deploy staging` -`BRANCH=branch_name bundle exec cap production deploy` +## Testing Production Install -Alternatively, you may deploy from [ansible-tower](https://ansible-tower.princeton.edu/). +You can test the docker container by running `docker compose up` and going to `http://localhost:3000` ## How to load data diff --git a/bin/deploy b/bin/deploy index df454b4f..44e8e334 100755 --- a/bin/deploy +++ b/bin/deploy @@ -52,7 +52,7 @@ curl -s -X POST -H "Accept: application/vnd.github+json-H" -H "Content-Type: app # Deploy using nomad-host-prod1, which has the nomad management key. ssh deploy@nomad-host-prod1.lib.princeton.edu << EOF - curl -s "https://raw.githubusercontent.com/pulibrary/${REPOSITORY}/${BRANCH_NAME}/config/deploy/${ENV}.hcl" | nomad job run -var "branch_or_sha=sha-$(git ls-remote https://github.com/pulibrary/${REPOSITORY}.git ${BRANCH_NAME} | awk '{ print substr($1,1,7) }')" - + curl -H 'Cache-Control: no-cache' -s "https://raw.githubusercontent.com/pulibrary/${REPOSITORY}/${BRANCH_NAME}/config/deploy/${ENV}.hcl" | nomad job run -var "branch_or_sha=sha-$(git ls-remote https://github.com/pulibrary/${REPOSITORY}.git ${BRANCH_NAME} | awk '{ print substr($1,1,7) }')" - EOF retcode=$? diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..f3cb749d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +version: "3.8" +services: + web: + build: + context: . + args: + UID: ${UID:-1000} + GID: ${GID:-${UID:-1000}} + ports: + - "3000:3000" + environment: + - SECRET_KEY_BASE=1111111 + - APP_DB_HOST=postgres-db + - APP_DB_USERNAME=root + - APP_DB_PASSWORD=password + - APP_DB=pulfalight + - RAILS_ENV=staging + depends_on: + postgres-db: + condition: service_healthy + postgres-db: + image: postgres + environment: + POSTGRES_USER: root + POSTGRES_PASSWORD: password + volumes: + - ./tmp/db:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: pg_isready + interval: 2s + timeout: 5s + retries: 30 diff --git a/tmp/pids/.keep b/tmp/pids/.keep new file mode 100644 index 00000000..e69de29b From 81357b4517d5c21efaf1fe25e260a32a18b2f7c0 Mon Sep 17 00:00:00 2001 From: Trey Pendragon Date: Thu, 12 Sep 2024 13:33:04 -0700 Subject: [PATCH 08/14] Go back to accurate ruby version. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 278cb40f..2495b273 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # syntax = docker/dockerfile:1 # Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile -ARG RUBY_VERSION=3.2 +ARG RUBY_VERSION=3.2.0 FROM ruby:$RUBY_VERSION-slim as base # Rails app lives here From deaa8a7ea8810e187bfe38408125da29be409f9e Mon Sep 17 00:00:00 2001 From: Trey Pendragon Date: Thu, 12 Sep 2024 13:43:33 -0700 Subject: [PATCH 09/14] Production HCL. --- config/deploy/production.hcl | 55 ++++++++++++++++++++++++++++++++++++ config/deploy/staging.hcl | 1 + 2 files changed, 56 insertions(+) create mode 100644 config/deploy/production.hcl diff --git a/config/deploy/production.hcl b/config/deploy/production.hcl new file mode 100644 index 00000000..4a8b30df --- /dev/null +++ b/config/deploy/production.hcl @@ -0,0 +1,55 @@ +variable "branch_or_sha" { + type = string + default = "main" +} +job "imagecat-production" { + region = "global" + datacenters = ["dc1"] + node_pool = "production" + type = "service" + group "web" { + count = 2 + network { + port "http" { to = 3000 } + } + service { + port = "http" + check { + type = "http" + port = "http" + path = "/" + interval = "10s" + timeout = "1s" + } + } + task "webserver" { + driver = "podman" + config { + image = "ghcr.io/pulibrary/imagecat-rails:${ var.branch_or_sha }" + ports = ["http"] + force_pull = true + } + template { + destination = "${NOMAD_SECRETS_DIR}/env.vars" + env = true + change_mode = "restart" + data = < Date: Thu, 12 Sep 2024 13:46:30 -0700 Subject: [PATCH 10/14] Only use env variable if necessary. --- .github/workflows/build-docker-image.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index 943da7e7..0de369d5 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -98,8 +98,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - SCANNER_OUTPUTS: ${{ steps.scanner.outputs.results }} - run: echo "${{ env.SCANNER_OUTPUTS }}" + run: echo "${{ steps.scanner.outputs.results }}" - name: Find Comment for scan if: github.event_name == 'pull_request' uses: peter-evans/find-comment@v3 @@ -111,15 +110,13 @@ jobs: - name: Create or update comment if: github.event_name == 'pull_request' uses: peter-evans/create-or-update-comment@v4 - env: - SCANNER_OUTPUTS: ${{ steps.scanner.outputs.results }} with: comment-id: ${{ steps.fc.outputs.comment-id }} issue-number: ${{ github.event.pull_request.number }} body: | ## Container Scanning Status: ${{ steps.runscanner.outcome != 'success' && '❌ Failure' || '✅ Success' }} ``` - ${{ env.SCANNER_OUTPUTS }} + ${{ steps.scanner.outputs.results }} ``` edit-mode: replace - name: Create issue From 14502a4efd5afede0f2b37bb44d8532998f71b35 Mon Sep 17 00:00:00 2001 From: Trey Pendragon Date: Thu, 12 Sep 2024 14:03:57 -0700 Subject: [PATCH 11/14] Update ruby. --- .circleci/config.yml | 2 +- .tool-versions | 2 +- Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2b637c0a..6cb4880d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -30,7 +30,7 @@ jobs: working_directory: ~/rails_template docker: - &ruby_docker - image: cimg/ruby:3.2.3-browsers + image: cimg/ruby:3.2.5-browsers environment: RAILS_ENV: test steps: diff --git a/.tool-versions b/.tool-versions index b56b315b..7a29313b 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,4 +1,4 @@ -ruby 3.2.0 +ruby 3.2.5 awscli 2.11.24 yarn 1.22.10 nodejs 18.18.0 diff --git a/Dockerfile b/Dockerfile index 2495b273..8cc4ad20 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # syntax = docker/dockerfile:1 # Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile -ARG RUBY_VERSION=3.2.0 +ARG RUBY_VERSION=3.2.5 FROM ruby:$RUBY_VERSION-slim as base # Rails app lives here From 17e231eae28eb8c168a9c6e2faf17b45bd1eb388 Mon Sep 17 00:00:00 2001 From: Trey Pendragon Date: Fri, 13 Sep 2024 09:25:09 -0700 Subject: [PATCH 12/14] Set resource requirements. --- config/deploy/production.hcl | 4 ++++ config/deploy/staging.hcl | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/config/deploy/production.hcl b/config/deploy/production.hcl index 4a8b30df..699f56fa 100644 --- a/config/deploy/production.hcl +++ b/config/deploy/production.hcl @@ -29,6 +29,10 @@ job "imagecat-production" { ports = ["http"] force_pull = true } + resources { + cpu = 1000 + memory = 500 + } template { destination = "${NOMAD_SECRETS_DIR}/env.vars" env = true diff --git a/config/deploy/staging.hcl b/config/deploy/staging.hcl index 6fcd9ada..0e829fa4 100644 --- a/config/deploy/staging.hcl +++ b/config/deploy/staging.hcl @@ -29,6 +29,10 @@ job "imagecat-staging" { ports = ["http"] force_pull = true } + resources { + cpu = 1000 + memory = 500 + } template { destination = "${NOMAD_SECRETS_DIR}/env.vars" env = true From 32b7a286b252c0c3f8c3cd8ec256886cfb50fbba Mon Sep 17 00:00:00 2001 From: Trey Pendragon Date: Fri, 20 Sep 2024 13:44:53 -0700 Subject: [PATCH 13/14] Add nightly vuln scan. --- .github/workflows/nightly-vuln-scanning.yml | 67 +++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/nightly-vuln-scanning.yml diff --git a/.github/workflows/nightly-vuln-scanning.yml b/.github/workflows/nightly-vuln-scanning.yml new file mode 100644 index 00000000..75fd30f5 --- /dev/null +++ b/.github/workflows/nightly-vuln-scanning.yml @@ -0,0 +1,67 @@ +name: Run nightly vulnerability check + +on: + schedule: + - cron: '0 0 * * *' + +# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. +jobs: + container-vuln-scan: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.20.0 + id: runscanner + continue-on-error: true + with: + image-ref: 'ghcr.io/pulibrary/imagecat-rails:main' + format: 'table' + exit-code: '1' + ignore-unfixed: true + vuln-type: 'os' + severity: 'CRITICAL,HIGH' + output: 'vulnerabilities.table' + - name: Set variables + id: scanner + if: job.steps.runscanner.status == failure() + run: | + EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) + echo "results<<$EOF" >> $GITHUB_OUTPUT + echo "$(cat vulnerabilities.table)" >> $GITHUB_OUTPUT + echo "$EOF" >> $GITHUB_OUTPUT + - name: Output variable + if: job.steps.runscanner.status == failure() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: echo "${{ steps.scanner.outputs.results }}" + - name: Create issue + if: steps.runscanner.outcome != 'success' + uses: JasonEtco/create-an-issue@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + SCANNER_OUTPUTS: ${{ steps.scanner.outputs.results }} + with: + filename: .github/failed-vuln-check.md + update_existing: true + - name: Find existing security issue + id: issues + if: steps.runscanner.outcome == 'success' + uses: lee-dohm/select-matching-issues@v1 + with: + query: 'Container Vulnerability Scanner Failed is:open ' + token: ${{ secrets.GITHUB_TOKEN }} + - name: Close found issues + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + if: steps.runscanner.outcome == 'success' + run: cat ${{ steps.issues.outputs.path }} | xargs gh issue close -c 'Container Scan Passing on Merge to Main' From 563a038ac07ce662f29dd79242cbff4e6ab9da27 Mon Sep 17 00:00:00 2001 From: Trey Pendragon Date: Fri, 20 Sep 2024 15:24:35 -0700 Subject: [PATCH 14/14] Add spreading. --- config/deploy/production.hcl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/deploy/production.hcl b/config/deploy/production.hcl index 699f56fa..63ecf088 100644 --- a/config/deploy/production.hcl +++ b/config/deploy/production.hcl @@ -7,6 +7,12 @@ job "imagecat-production" { datacenters = ["dc1"] node_pool = "production" type = "service" + # Spread all instances across hosts. + # By default Nomad uses as few resources as possible, but we want a host to be + # able to go down in prod. + spread { + attribute = "${node.unique.name}" + } group "web" { count = 2 network {