Skip to content

Latest commit

 

History

History
384 lines (289 loc) · 13 KB

TLDR.md

File metadata and controls

384 lines (289 loc) · 13 KB

TL;DR

The guide is quite long! If you want to just get a proof of concept running go ahead with these instructions.

Basic Up and Going

  1. Make sure you have docker, lucky, and up commands.

    docker -v # => 19.03.12
    lucky -v  # => 0.24.0
    up -v     # => 0.1.7
  2. Do the following

    mkdir lhd-test
    cd lhd-test
    git clone https://github.com/KCErb/lucky-hasura-docker
    lucky init.custom foo_bar --api
    cd lucky-hasura-docker
  3. Replace project variables. sed is different on Linux and macOS :(

    Linux

    git grep -l 'GITLAB_USER' | xargs sed -i 's/GITLAB_USER/kcerb/g'
    git grep -l 'GITLAB_REPO_NAME' | xargs sed -i 's/GITLAB_REPO_NAME/foo_bar/g'
    git grep -l 'PROJECT_NAME' | xargs sed -i 's/PROJECT_NAME/foo_bar/g'
    git grep -l 'SWARM_NAME' | xargs sed -i 's/SWARM_NAME/foo_bar/g'

    macOS

    git grep -l 'GITLAB_USER' | xargs sed -i '' -e 's/GITLAB_USER/kcerb/g'
    git grep -l 'GITLAB_REPO_NAME' | xargs sed -i '' -e 's/GITLAB_REPO_NAME/foo_bar/g'
    git grep -l 'PROJECT_NAME' | xargs sed -i '' -e 's/PROJECT_NAME/foo_bar/g'
    git grep -l 'SWARM_NAME' | xargs sed -i '' -e 's/SWARM_NAME/foo_bar/g'
  4. Do the following

    cd ..
    # modify foo_bar a bit
    rm -rf foo_bar/script
    rm foo_bar/Procfile
    rm foo_bar/Procfile.dev
    echo '\nup.cache' >> foo_bar/.gitignore
    # rsync contents of template dir into foo_bar
    rsync -avr lucky-hasura-docker/proj_template/ foo_bar
    cd foo_bar
  5. In config/server.cr you should see a line that starts with settings.secret_key_base = (line 17). Replace it with lucky_hasura_32_character_secret.

  6. Now you can start developing with

    script/up

Production Instructions

  1. Provision a production server somewhere.

    • Ensure that ports 80 and 443 are available to the public.

    • Save the SSL .cert and .key files as /etc/certs/cloudflare.cert and /etc/certs/cloudflare.key.

    • Create a non-root user (lhd for example) and make sure you can ssh into the server as that user.

  2. Provision two deploy tokens from Gitlab Settings > Repository. Name one gitlab-deploy-token, it will be used in CI so you don't need to save its username or password. Name the other whatever you like, give it at least both read_repository and read_registry scopes, and be sure to copy the username and password for step 4.

  3. Create an account/API key on SendGrid, copy the value for the next step.

  4. (Assuming you are on your production server as the lhd user) Create a .lhd-env file the home dir and place the Gitlab credentials and SendGrid API key (chmod 600 this file to add a layer of security):

    export DEPLOY_USERNAME='gitlab+deploy-token-170337'
    export DEPLOY_TOKEN='8yQesUWt4MaHJ8T4d6hc'
    export SEND_GRID_KEY='SG.ALd_3xkHTRioKaeQ.APYYxwUdr00BJypHuximcjNBmOxET1gV8Q'
  5. Add the following variables to your environment using your own values. You can use .lhd-env or .profile since the CI runner uses a login shell.

    export APP_DOMAIN='foobar.business'
    export IP_ADDRESS='198.211.113.94'
  6. On Gitlab also save that IP address as a variable under Settings > CI/CD with the key PRODUCTION_SERVER_IP.

  7. Generate a keypair (the following assumes you are doing this on your own compute NOT the production server).

    ssh-keygen -t ed25519 -C “gitlab-ci@foo_bar_production” -f ~/.ssh/gitlab-ci
  8. Add the private key to the CI env as GITLAB_PRODUCTION_KEY.

  9. Copy the public key to the server

    ssh-copy-id -i ~/.ssh/gitlab-ci [email protected]

First Push

  1. Add everything, commit, and push as below. The sub-deploy keyword runs the deploy script functions in the required order for the first push. This is all it takes to put your app online! For more details about what that includes see the full guide.

    git remote add origin [email protected]:KCErb/foo_bar.git
    git add .
    git commit -m "first commit [sub-deploy]"
    git push -u origin master
  2. Once the deploy stage has passed CI, you can log in to the server and see progress with docker service ls. You should see 1/1 for all replicas once everything is online. You can confirm this by visiting https://api.foobar.business/ where you'll see the Hello World message that ships with Lucky.

Security Note - You may want to rotate the join token after the first bootstrap since it is printed to the Gitlab CI history. Read more about join tokens.

Troubleshooting - If you get Invalid memory access at the crystal build step of the build stage you are bumping into a hard-to-reproduce issue that has cropped up variously across the crystal ecosystem. Just try triggering a new build and invalidating the Docker cache. Also please open an issue so that I can track the frequency of this issue.

Extras

Monitoring with Swarmprom

  1. (Optional) Add slack credentials for automatic slack alerts to .lhd-env.

    export SLACK_URL='https://hooks.slack.com/services/G11G430A7/AK9023U17/vaGCB6T6ZVF1HRng0WqTEaeX'
    export SLACK_CHANNEL='lhd-demo'
    export SLACK_USER='Prometheus'
  2. Start the swarm (be sure that APP_DOMAIN is defined via source ~/.profile if you haven't logged out yet):

    source ~/.lhd-env
    cd ~/GITLAB_REPO_NAME/Docker/prometheus-swarm
    docker stack deploy -c docker-compose.yml prometheus_swarm
  3. Once the services are up, log in and play around, visit grafana.foobar.business. The password was generated by the bootstrap script and is stored in ~/.lhd-env as ADMIN_PASSWORD. The username is PROJECT_NAME_admin.

Seed Database, Test Lucky, Test Hasura

  1. Add this to the call method of tasks/create_required_seeds (WARNING: if you don't want these in production add them to create_sample_seeds instead)

    %w{admin buzz}.each do |name|
      email = name + "@foobar.business"
      user = UserQuery.new.email(email).first?
      UserBox.create &.email(email) unless user
    end
  2. Replace payload variable in src/models/user_token.cr in the self.generate method with

    # (demo ONLY, not a good way to assign roles!!!!)
    allowed_roles = ["user"]
    default_role = "user"
    if user.email.includes?("admin")
      allowed_roles << "admin"
      default_role = "admin"
    end
    payload = {"user_id" => user.id,
      "https://hasura.io/jwt/claims" => {
        "x-hasura-allowed-roles" => allowed_roles,
        "x-hasura-default-role" => default_role,
        "x-hasura-user-id" => user.id.to_s,
      }
    }
  3. Add user role in Hasura dashboard localhost:9695 with permission to select their own email (screenshot in full guide).

  4. Add spec/requests/graphql/users/query_spec.cr file with contents

    require "../../../spec_helper"
    require "http/client"
    require "json"
    
    describe "GraphQL::Users::Query" do
      it "admin can see all users" do
        admin, user = make_test_users
        users = graphql_request(admin)
        users.size.should eq 2
      end
    
      it "user can see only self" do
        admin, user = make_test_users
        users = graphql_request(user)
        users.size.should eq 1
        users.first["email"].should eq "[email protected]"
      end
    end
    
    private def make_test_users
      admin = UserBox.create &.email("[email protected]")
      user = UserBox.create &.email("[email protected]")
      {admin, user}
    end
    
    # returns [{"email" => "[email protected]"}]
    private def graphql_request(user) : Array(JSON::Any)
      client = HTTP::Client.new("foo_bar_hasura_test", 8080)
      client.before_request do |request|
        request.headers["Authorization"] = "Bearer #{UserToken.generate(user)}"
      end
      query = %({"query": "{ users { email } }"})
      response = client.post("/v1/graphql", headers: HTTP::Headers{"Content-Type" => "application/json"}, body: query)
      json = JSON.parse response.body
      data = json["data"]?
      data = data.should_not be_nil
      data["users"].as_a
    end
  5. While we're messing with tests, this file is basically harmless but not needed, so delete

    rm spec/setup/setup_database.cr
  6. Run script/test to start a shell session in a special test environment.

CORS

When making an API call to the Lucky backend, you will probably need to setup a CORS middleware.

  1. Create a file called src/handlers/cors_handler.cr.

  2. Copy and paste the following code in. Adjust the ALLOWED_ORIGINS to your needs.

    class CORSHandler
      include HTTP::Handler
    
      # Origins that your API allows
      ALLOWED_ORIGINS = [
        # Allows for local development
        /\.lvh\.me/,
        /localhost/,
        /127\.0\.0\.1/,
    
        # Add your production domains here
        /foobar\.business/
      ]
    
      def call(context)
        request_origin = context.request.headers["Origin"]? || "localhost"
    
        # Setting the CORS specific headers.
        # Modify according to your apps needs.
        context.response.headers["Access-Control-Allow-Origin"] = allowed_origin?(request_origin) ? request_origin : ""
        context.response.headers["Access-Control-Allow-Credentials"] = "true"
        context.response.headers["Access-Control-Allow-Methods"] = "POST,GET,OPTIONS"
        context.response.headers["Access-Control-Allow-Headers"] = "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"
    
        # If this is an OPTIONS call, respond with just the needed headers.
        if context.request.method == "OPTIONS"
          context.response.status = HTTP::Status::NO_CONTENT
          context.response.headers["Access-Control-Max-Age"] = "#{20.days.total_seconds.to_i}"
          context.response.headers["Content-Type"] = "text/plain"
          context.response.headers["Content-Length"] = "0"
          context
        else
          call_next(context)
        end
      end
    
      private def allowed_origin?(request_origin)
        ALLOWED_ORIGINS.find(false) do |pattern|
          pattern === request_origin
        end
      end
    end
  3. Add CORSHandler.new to the middleware list in src/app_server.cr.

  4. Add require "./handlers/**" to src/app.cr.

Healthcheck

  1. Uncomment the healthcheck lines in docker-compose.swarm.yml under the lucky service.

  2. Add the version route for health checks by running the following in the lucky container.

    lucky gen.model Version
  3. Add a value column to the versions table by updating src/models/version.cr with:

    class Version < BaseModel
     table do
       column value : String
     end
    end
  4. Update the corresponding migration by adding 1 line in the create block add value : String. My file looks like this:

    class CreateVersions::V20201010215829 < Avram::Migrator::Migration::V1
      def migrate
        # Learn about migrations at: https://luckyframework.org/guides/database/migrations
        create table_for(Version) do
          primary_key id : Int64
          add_timestamps
          add value : String
        end
      end
    
      def rollback
        drop table_for(Version)
      end
    end
  5. Add a route to GET the current version. Put the following in src/actions/version/get.cr

    class Version::Get < ApiAction
      include Api::Auth::SkipRequireAuthToken
      get "/version" do
        last_version = VersionQuery.last?
        if last_version
          json({version: last_version.value})
        else
          json({error: "Unable to reach database"}, 503)
        end
      end
    end
  6. Add some logic to tasks/create_required_seeds.cr so that each time the required seeds are created we make sure the latest version number is provided:

    current_version = `git rev-parse --short=8 HEAD 2>&1`.rchop
    current_version = "pre-first-commit" unless $?.success?
    last_version = VersionQuery.last?
    version_is_same = last_version && last_version == current_version
    SaveVersion.create!(value: current_version) unless version_is_same
  7. Migrate and seed in the lucky container.

    lucky db.migrate && lucky db.create_required_seeds
  8. Test from the host

    curl localhost:5000/version

Rollback/Deploy to version

Rollback and Deploy scripts are provided. To rollback to a certain image you just need to provide the tag of the image (first 8 characters of commit sha where that image was built)

script/rollback 53c086ec

If you want to fast-forward to a later commit, you can give its short-sha (first 8 characters) to the deploy script

script/deploy 8d9b3d0c

In both cases the -s flag can be passed to signal subtractive mode as described in the guide.