`fly deploy --remote-only` isn't reading Rails Credentials file from Github Actions, but works elsewhere

Hey guys, I’m not sure what issue I’m facing but when I deploy on my local computer the fly deploy --remote-only the deployment is flawless but when I do literally the same thing on github action it errors out on bundle exec rails assets:precompile step with the error of


 #23 [stage-4 5/5] RUN bundle exec rails assets:precompile
#23 3.850 rails aborted!
#23 3.851 NoMethodError: undefined method `access_key_id' for nil:NilClass
#23 3.851 
#23 3.851   access_key_id: Rails.application.credentials.aws.access_key_id,
#23 3.851                                                   ^^^^^^^^^^^^^^
#23 3.851 /app/config/initializers/shrine.rb:8:in `<main>'

Code that is causing the error out:

  • Now I’ve double-checked my credentials file and it’s in there as such,
  • The master key is set in fly, and in GitHub actions…

This is my credentials file layout:

  • I’ve ran Rails.application.credentials.aws.access_key_id in rails console and it is returning a value so the spacing and such is right on how I’ve written the credential file.

I’m not really sure what else to do here… It’s not making much sense.

And to show that it is working you can see it running here: https://climate-coolers.fly.dev/

Any help is appreciated!

Workflow file:

name: Fly Deploy
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
env:
  FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
  RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
jobs:
  deploy:
    name: Deploy app
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: superfly/flyctl-actions/setup-flyctl@master
      - run: flyctl deploy --remote-only

Dockerfile:

# syntax = docker/dockerfile:experimental
ARG RUBY_VERSION=3.1.2
ARG VARIANT=jemalloc-slim
FROM quay.io/evl.ms/fullstaq-ruby:${RUBY_VERSION}-${VARIANT} as base

ARG NODE_VERSION=16
ARG BUNDLER_VERSION=2.3.11

ARG RAILS_ENV=production
ENV RAILS_ENV=${RAILS_ENV}

ENV RAILS_SERVE_STATIC_FILES true
ENV RAILS_LOG_TO_STDOUT true

ARG BUNDLE_WITHOUT=development:test
ARG BUNDLE_PATH=vendor/bundle
ENV BUNDLE_PATH ${BUNDLE_PATH}
ENV BUNDLE_WITHOUT ${BUNDLE_WITHOUT}

RUN mkdir /app
WORKDIR /app
RUN mkdir -p tmp/pids

SHELL ["/bin/bash", "-c"]

RUN curl https://get.volta.sh | bash

ENV BASH_ENV ~/.bashrc
ENV VOLTA_HOME /root/.volta
ENV PATH $VOLTA_HOME/bin:/usr/local/bin:$PATH

RUN volta install node@${NODE_VERSION} && volta install yarn

FROM base as build_deps

ARG DEV_PACKAGES="git build-essential libpq-dev wget vim curl gzip xz-utils libsqlite3-dev"
ENV DEV_PACKAGES ${DEV_PACKAGES}

RUN --mount=type=cache,id=dev-apt-cache,sharing=locked,target=/var/cache/apt \
    --mount=type=cache,id=dev-apt-lib,sharing=locked,target=/var/lib/apt \
    apt-get update -qq && \
    apt-get install --no-install-recommends -y ${DEV_PACKAGES} \
    && rm -rf /var/lib/apt/lists /var/cache/apt/archives

FROM build_deps as gems

RUN gem install -N bundler -v ${BUNDLER_VERSION}

COPY Gemfile* ./
RUN bundle install &&  rm -rf vendor/bundle/ruby/*/cache

FROM build_deps as node_modules

COPY package*json ./
COPY yarn.* ./

RUN if [ -f "yarn.lock" ]; then \
    yarn install; \
    elif [ -f "package-lock.json" ]; then \
    npm install; \
    else \
    mkdir node_modules; \
    fi

FROM base

ARG PROD_PACKAGES="postgresql-client file vim curl gzip libsqlite3-0 git"
ENV PROD_PACKAGES=${PROD_PACKAGES}

RUN --mount=type=cache,id=prod-apt-cache,sharing=locked,target=/var/cache/apt \
    --mount=type=cache,id=prod-apt-lib,sharing=locked,target=/var/lib/apt \
    apt-get update -qq && \
    apt-get install --no-install-recommends -y \
    ${PROD_PACKAGES} \
    && rm -rf /var/lib/apt/lists /var/cache/apt/archives

COPY --from=gems /app /app
COPY --from=node_modules /app/node_modules /app/node_modules

ENV SECRET_KEY_BASE 1

COPY . .

RUN bundle exec rails assets:precompile

ENV PORT 8080

ARG SERVER_COMMAND="bundle exec puma -C config/puma.rb"
ENV SERVER_COMMAND ${SERVER_COMMAND}
CMD ${SERVER_COMMAND}

fly.toml

# fly.toml file generated for climate-coolers on 2022-05-28T14:59:34+02:00

app = "climate-coolers"

kill_signal = "SIGINT"
kill_timeout = 5
processes = []

[deploy]
  release_command = "bundle exec rails db:migrate"

[env]

[experimental]
  allowed_public_ports = []
  auto_rollback = true

[[services]]
  http_checks = []
  internal_port = 8080
  processes = ["app"]
  protocol = "tcp"
  script_checks = []

  [services.concurrency]
    hard_limit = 25
    soft_limit = 20
    type = "connections"

  [[services.ports]]
    force_https = true
    handlers = ["http"]
    port = 80

  [[services.ports]]
    handlers = ["tls", "http"]
    port = 443

  [[services.tcp_checks]]
    grace_period = "1s"
    interval = "15s"
    restart_limit = 0
    timeout = "2s"

I was able to do this successfully once with another project that I’ve literally copy-pasted the fly.toml and docker so this is even more baffeling.

1 Like

Hey, this fortunately has a simple solution.

The problem occurs because asset compilation will load your initialiser, and RAILS_MASTER_KEY is only available at runtime. ENV in the Dockerfile is only available at runtime. Locally it probably works because you have your master.key file on the filesystem.

A quick way to fix this would be to pass RAILS_MASTER_KEY as a build argument like:

fly deploy --remote-only --build-arg RAILS_MASTER_KEY=$(cat config/master.key)

That’s not ideal, though, as build arguments are stored in image history, unnecessarily exposing your credentials.

The correct way to make this work is to ensure that you initialiser is not run unless you need it in during asset compilation. You could do this by changing the asset compilation line to RUN bundle exec rails assets:precompile ASSET_COMPILE=1, then conditionally checking for ENV['ASSET_COMPILE'] in your initialiser(s).

4 Likes

Wow that was it! I don’t think I would ever have gotten that haha. I’ve got a lot to learn with devops…

Just for someone who needs a bit more direction on this I’ve added this to the codebase
In initializers/shrine.rb I wrapped all of the code with

if ENV['ASSET_COMPILE'].blank?
...
end

in routes.rb I’ve changed things to because it calls shrine from routes

 if ENV['ASSET_COMPILE'].blank?
    mount Shrine.presign_endpoint(:cache) => '/s3/params'
  end

And then as mentioned above I’ve replaced Dockerfile line of
RUN bundle exec rails assets:precompile
to
RUN bundle exec rails assets:precompile ASSET_COMPILE=1

Thanks so much!

2 Likes