[Part 1] WEB service created with Rails + Nuxt + MySQL + Docker is automatically tested and deployed with ECS / ECR / CircleCI and converted to terraform.

In writing this article

[AWS? Docker?] Summarize the necessary knowledge in your portfolio in an easy-to-understand manner [terraform? CircleCI?]

CircleCI? ?? ?? Docker ??? People should look at this first. ↑

Studying for studying doesn't mean much ... So move your hands first! So this time I will write an article in hands-on format. I hope that those who read it will be able to quickly build a development environment and concentrate on development quickly.

I reproduced it in my environment about 3 times so as not to suffer from errors as much as possible.

I think it will probably be a trilogy ... I'm sorry it's long.

First: This article 2nd: [[Part 2] Automatically deploy WEB service created with Rails + Nuxt + MySQL + Docker with ECS / ECR / CircleCI and make it terraform](https://qiita.com/Shogo1222/items/ dcbc1e50f6fc83f48b44) Third: [Part 2] Automatically deploy WEB service created with Rails + Nuxt + MySQL + Docker with ECS / ECR / CircleCI and make it terraform

Goals for this article

--Hello World! Using Rails and Nuxt! --Launch Rails in API mode-Pull Rails data with Nuxt. --Create a user table in Rails and create a user in Nuxt. --Introduce rspec and run the test. --Try automatic testing in cooperation with CircleCI.

It's long, but let's do our best!

usage environment

MacOS Catalina 10.15.5 Rails 6.0.3.2 @vue/cli 4.4.4 @nuxt/cli v2.14.4 Vuetify Docker(forMac) version 19.03.8 docker-compose version 1.25.5

Premise

--Docker installed
Install Docker on Mac (Update: 2019/7/13) --CircleCI account created
I've just started CircleCI, so I've summarized it in an easy-to-understand manner --Created git account
[6.1 GitHub-Account Preparation and Setup](https://git-scm.com/book/ja/v2/GitHub-%E3%82%A2%E3%82%AB%E3%82%A6%E3% 83% B3% E3% 83% 88% E3% 81% AE% E6% BA% 96% E5% 82% 99% E3% 81% A8% E8% A8% AD% E5% AE% 9A)

Finished product

I gave the finished product to git. qiita-sample-app

Environmental development construction with Docker

Initial setting

This goal

app //Any name is fine
├─docker-compose.yml 
├─front
|   ├─Dockerfile
└─back 
    ├─Dockerfile
    ├─Gemfile
    └─Gemfile.lock

Create working directory

mkdir app
cd app //Go to app
mkdir front //front creation
mkdir back //back creation
touch ./back/Dockerfile
touch ./back/Gemfile
touch ./back/Gemfile.lock
mkdir ./back/environments
touch ./back/environments/db.env

touch ./front/Dockerfile

touch docker-compose.yml

Description of Dockerfile

Edit Dockerfile in / back

back/Dockerfile


#Specifying the image
FROM ruby:2.6.3-alpine3.10

#Download required packages
ENV RUNTIME_PACKAGES="linux-headers libxml2-dev make gcc libc-dev nodejs tzdata mysql-dev mysql-client yarn" \
    DEV_PACKAGES="build-base curl-dev" \
    HOME="/app" \
    LANG=C.UTF-8 \
    TZ=Asia/Tokyo

#Move to working directory
WORKDIR ${HOME}

#Copy the necessary files from the host (files on your computer) to Docker
ADD Gemfile ${HOME}/Gemfile
ADD Gemfile.lock ${HOME}/Gemfile.lock

RUN apk update && \
    apk upgrade && \
    apk add --update --no-cache ${RUNTIME_PACKAGES} && \
    apk add --update --virtual build-dependencies --no-cache ${DEV_PACKAGES} && \
    bundle install -j4 && \
    apk del build-dependencies && \
    rm -rf /usr/local/bundle/cache/* \
    /usr/local/share/.cache/* \
    /var/cache/* \
    /tmp/* \
    /usr/lib/mysqld* \
    /usr/bin/mysql*

#Copy the necessary files from the host (files on your computer) to Docker
ADD . ${HOME}

#Open port 3000
EXPOSE 3000

#Execute command
CMD ["bundle", "exec", "rails", "s", "puma", "-b", "0.0.0.0", "-p", "3000", "-e", "development"]

Edit Dockerfile in / front

front/Dockerfile


FROM node:12.5.0-alpine

ENV HOME="/app" \
    LANG=C.UTF-8 \
    TZ=Asia/Tokyo

ENV HOST 0.0.0.0

WORKDIR ${HOME}

RUN apk update && \
    apk upgrade && \
    npm install -g n && \
    yarn install &&\
    rm -rf /var/cache/apk/*

#I will remove it later(When using ECS)

#RUN yarn run build
#EXPOSE 3000
#CMD ["yarn", "dev"]

Description of Gemfile

back/Gemfile


source 'https://rubygems.org'
gem 'rails', '~>6'

Description of docker-compose.yml

yml:./docker-compose.yml


version: "3"

services:
  db:
    image: mysql:5.7
    env_file:
      - ./back/environments/db.env
    restart: always
    volumes:
      - db-data:/var/lib/mysql:cached

  back:
    build: back/
    # rm -f tmp/pids/server.Useful when you fail to erase the rails server with pid
    command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" 
    env_file:
      - ./back/environments/db.env
    volumes:
      - ./back:/app:cached
    depends_on:
      - db
    #Host computer port: Port in Docker
    ports:
      - 3000:3000

  front:
    build: front/
    command: yarn run dev
    volumes:
      - ./front:/app:cached
    ports:
      #Host computer port: Port in Docker
      - 8080:3000
    depends_on:
      - back
volumes:
  public-data:
  tmp-data:
  log-data:
  db-data:

Trivia

The network inside Docker is different from the network on the host machine. For example, the rails process started by Docker is started by localhost (127.0.0.1): 3000 in the virtual environment (container). It is different from localhost on the host machine.

However, it can be solved by port mapping in docker-compose.yml.

If you specify host port 3000 and container port 3000 and bind to 0.0.0.0, you can access with localhost as if you started it on the host machine. This is called port forwarding.

You can access the process built in the virtual environment normally by specifying the binding address and performing port forwarding.

reference: The meaning of option -b of rails s -b 0.0.0.0

So what does 8080: 3000 mean? This port-maps container port 3000 to host port 8080 (≈ port forwarding). In other words, the nuxt process built inside the Docker container uses port 3000, but when browsing on the host machine, make it visible on 8080. (If Rails is also 3000, it will be duplicated in the host machine, so an error will occur)


Let's set db.env

./back/db.env


MYSQL_ROOT_PASSWORD=rootpassword
MYSQL_USER=username
MYSQL_PASSWORD=userpassword

Let's build with Docker Compose

./


docker-compose build

It will take some time. You don't have to do build many times, just run it when you change Gemfile or Dockerfile! (I did it many times at the beginning and lost time.)

This is the docker-compose command that came out for the first time, but from now on I will use this command a lot.

hello, Nuxt

This goal

Create a nuxt app under front Hello World with Nuxt!

The final directory structure looks like this.

...Abbreviation
front
├─Dockerfile
...Abbreviation
├─components //Where to put vue components
├─layouts //Where index calls by default
├─pages //Add here when adding a page
├─plugins //Where to put the plugin configuration file to be added with yarn etc.
├─nuxt.config.js //nuxt itself configuration file
├─package.json //Where to set the package dependency of yarn / npm
...Abbreviation

Let's create a nuxt app

./


docker-compose build
docker-compose run front npx [email protected] 
#If it is version3, an error will occur unless it is an empty directory, so the version is specified.

#The following selection screen will appear.

 #I like the name of the app, so it's OK!It will be the title when opened on the browser
? Project name                   --> sample_app

 #I like the description of the app, so it's OK!It becomes the subtitle when opened on the browser
? Project description            --> sample_app

 #I like the creator of the app, so it's OK!
? Author name                    --> me

 #You can choose npm or yarn, but yarn seems to be faster, so select yarn
? Choose the package manager     --> Yarn

? Choose UI framework            --> Vuetify.js
? Choose custom server framework --> None
? Choose Nuxt.js modules         --> Axios
? Choose linting tools           --> -
? Choose test framework          --> None
? Choose rendering mode          --> SPA

Trivia

** docker-compose run **: means run the following command on your Docker machine ** front **: means run on a container with this name

When you want to type Rails command,

docker-compose run back rails db:create

By the way, if you want to go into a container and debug

docker-compose exec back sh

To


Hello nuxt

docker-compose up front

Launch the front image you just built. http://localhost:8080/

Access the above URL and when this screen is displayed, it is complete! nuxt helloworld.png

hello, Rails

This goal

Create a rails app under back Hello World on Rails! Yay!

...Abbreviation
front
├─Dockerfile
├─Gemfile
├─Gemfile.lock
...Abbreviation
├─app //Where controller and view are included
├─config //Where there is something that is read at startup
├─db //db table information etc.
├─environments //Environment variables for DB connection information
...Abbreviation

Creating a Rails app

./


#Create in API mode.--If you remove the api, things necessary for screen drawing such as view will also be installed.
#Select MySQL as the database.

docker-compose run back rails new . -f -d mysql --api
docker-compose build

In this state, the contents of the Gemfile have already been rewritten from the initial state. Please check it.

DB settings

yml:./back/config/database.yml


default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV.fetch('MYSQL_USER') { 'root' } %> #add.
  password: <%= ENV.fetch('MYSQL_PASSWORD') { 'password' } %> #add.
  host: db #change.

Grant the authority to the user described in db.env.

./


touch ./back/db/grant_user.sql

sql:./back/db/grant_user.sql


GRANT ALL PRIVILEGES ON *.* TO 'username'@'%';
FLUSH PRIVILEGES;

./


#Start the container.
docker-compose up

#Run the above SQL in the db container.
docker-compose exec db mysql -u root -p -e"$(cat back/db/grant_user.sql)"

#Creating a DB.
docker-compose run back rails db:create

Hello Rails

http://localhost:3000/ Access to! If you see the familiar screen below, you are successful.

rails helloworld.png

Get users using API between Nuxt-> Rails

From here, let's actually move on to implementation. As much as possible, I made it easy to add functions after that, so If you want to add another new function, please try it in the same way.

Let's create a Users table in the DB

./


docker-compose run back rails g scaffold user name:string
#It creates various files like this.
Running via Spring preloader in process 20
      invoke  active_record
      create    db/migrate/20200902105643_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      invoke  resource_route
       route    resources :users
      invoke  scaffold_controller
      create    app/controllers/users_controller.rb
      invoke    test_unit
      create      test/controllers/users_controller_test.rb

# db/Below migrate~create_users.It runs rb and creates a table in the DB.
#Once run, the same file will not be rerun.
# 2020~If the number part of is changed, it can be re-executed, but if the same table exists in the DB, an error will occur.
docker-compose run back rails db:migrate

Now you have a table called users in a DB called app_development. Easy!

Let's get the data of Users

http://localhost:3000/users/ Let's access to. Since it's in API mode, you should see an empty json on a blank screen.

Why does the screen look like this? that is, There is a secret in **./back/config/routes.rb **.

rb:./back/config/routes.rb


Rails.application.routes.draw do
  resources :users
end

There is a description of: users in resources. It links a controller called ./back/app/users_controller.rb with a path like ** / users **. Therefore, it is easy to think that the same procedure is used when adding functions.

./


#With this command, you can check which route is currently registered.
docker-compose run back rails routes

I will omit the details, but I would appreciate it if you could check it in the tutorial etc.

Let's refer to the Users data from Nuxt

Preparation

Communication from nuxt to rails requires some preparation. Let's not rush.

Install @ nuxtjs / axios

A plugin called axios is used for API communication.

./


docker-compose run front yarn 
docker-compose run front yarn add @nuxtjs/axios

Notice that the following has been added to ./front/package.json:

json:./front/package.json


...
    "@nuxtjs/axios": "^5.12.2"
...

Modify ./front/nuxt.config.js

Modify nuxt.config.js, which is a configuration file that is loaded when nuxt is started for the first time.

javascript:nuxt.config.js


...
  modules: [
    '@nuxtjs/axios' //add to
  ],
...

Add ./front/plugins/axios.js

Create a new file called axios.js under ./front/plugins/.

javascript:./front/plugins/axios.js


import axios from "axios"

export default axios.create({
  baseURL: "http://localhost:3000"
})

Added rack-cors to ./back/Gemfile

./back/Gemfile


...
gem 'rack-cors' #add to
...

./


docker-compose build

Add settings to ./back/config/initializers/cors.rb

rb:./back/config/initializers/cors.rb


Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'

    resource '*',
             headers: :any,
             methods: %i[get post put patch delete options head]
  end
end

./


docker-compose up

Screen development

Let's actually create a new screen. Create a new users.vue under ./front/pages.

vue:./front/pages/users.vue


<template>
  <v-container>
    <v-row align="center" justify="center">
      <v-col cols="12">
          <h1>Hello, Qiita! </h1>
        </v-col>
      </v-row>

      <v-card
        class="mx-auto"
        max-width="300"
        tile
      >
          <v-list rounded>
            <v-subheader>USERS</v-subheader>
            <v-list-item-group color="primary">
              <v-list-item
                v-for="user in users"
                :key="users.id"
                @click=""
              >
                <v-list-item-content>
                  <v-list-item-title v-text="user.name"></v-list-item-title>
                </v-list-item-content>
              </v-list-item>
            </v-list-item-group>
          </v-list>
      </v-card>
    </v-container>
</template>
<script>
import axios from "~/plugins/axios"

export default {
  data() {
    return {
      users: []
    }
  },
  created() {
    //Get users with axios
    axios.get("/users").then(res => {
      if (res.data) {
        this.users = res.data
      }
    })
  }
}
</script>

Let's add Users data from Nuxt

I got the Users data above ... but it shouldn't show anything because it doesn't contain anything. Now let's save the data to the DB with the API.

vue:./front/pages/users.vue


<template>
  <v-container>
    <v-row align="center" justify="center">
      <v-col cols="12">
        <v-text-field
        label="Username"
        v-model="name"
        prepend-icon=""
        type="text"
        />
        <v-btn color="primary" @click="createUser">ADD USER</v-btn>
      </v-col>
      <v-col cols="12">
          <h1>Hello, Qiita! </h1>
        </v-col>
      </v-row>

      <v-card
        class="mx-auto"
        max-width="300"
        tile
      >
          <v-list rounded>
            <v-subheader>USERS</v-subheader>
            <v-list-item-group color="primary">
              <v-list-item
                v-for="user in users"
                :key="users.id"
                @click=""
              >
                <v-list-item-content>
                  <v-list-item-title v-text="user.name"></v-list-item-title>
                </v-list-item-content>
              </v-list-item>
            </v-list-item-group>
          </v-list>
      </v-card>
    </v-container>
</template>

<script>
import axios from "~/plugins/axios"

export default {
  data() {
    return {
      name: "",
      users: []
    }
  },
  created() {
    //Get users with axios
    axios.get("/users").then(res => {
      if (res.data) {
          this.users = res.data
          }
        })
  },
  methods: {
    //Register users with axios
    createUser(){
      axios.post("/users", {name: this.name})
    .then(res => {
      if (res.data) {
          this.users.push(res.data)
          }
        })
      }
  }
}
</script>

nuxt add user.png

You can now register the user. Did it! By the way, the screen of nuxt becomes white when the dark theme in nuxt.config.js is set to false.

Automatic test

Now, let's set up and add an automated test!

Added rspec-rails and factory_bot_rails to ./back/Gemfile

./back/Gemfile


...
gem 'rspec-rails' #add to
gem 'factory_bot_rails' #add to
...

./


docker-compose down
docker-compose build back
docker-compose up
docker-compose exec back rails generate rspec:install

#The following files will be added
Running via Spring preloader in process 18
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

Let's write a test for Users

First, create a test file.

./


mkdir ./back/spec/models
mkdir ./back/spec/requests
mkdir ./back/spec/factories

touch ./back/spec/models/user_spec.rb
touch ./back/spec/requests/user_spec.rb
touch ./back/spec/factories/users.rb

rb:./back/spec/models/user_spec.rb


require 'rails_helper'

RSpec.describe User, type: :model do
  it 'Normal test' do
    @user = User.new(
      name: 'test'
    )
    expect(@user).to be_valid
  end
end

rb:./back/spec/factories/users.rb


# frozen_string_literal: true

FactoryBot.define do
  factory :user do
    name { 'testuser' }
  end
end

rb:./back/spec/requests/user_spec.rb


require 'rails_helper'

RSpec.describe User, type: :request do
  # frozen_string_literal: true

  require 'rails_helper'

  describe 'User' do
    before(:each) do
      @status_code_ok = 200
    end
    it 'Show user' do
      @user = FactoryBot.create(:user)
      get '/users/'
      @json = JSON.parse(response.body)
      #Judgment of availability of response
      expect(response.status).to eq(@status_code_ok)
    end
  end
end

./


docker-compose run back bundle exec rspec

..

Finished in 0.13418 seconds (files took 2.18 seconds to load)
2 examples, 0 failures

Did it. The test is successful. thank you for your hard work.

Automatic testing with CircleCI

Next, let CircleCI automatically test the test created above every time you push it with git!

Link CircleCI account to git repository

Have you created a CircleCI account? If your CircleCI account is linked to your git account, as shown in the image You will see a SetUp button for your repository, press it!

CircleCI 連携.png CircleCI 連携2.png

Press Add Config to create and build a pull request for this repository.

CircleCI プルリク.png

Merge it into master and pull it locally with the git pull command. After pulling, .circleci / config.yml was successfully generated locally. CircleCI edits this config.yml to control it for automatic testing and automatic deployment.

Have CircleCI run automated tests.

Add the following to the test part of database.yml.

yml:./back/config/database.yml


test:
  <<:         *default
  database:   app_test
  username:   root #add to
  password:   rootpassword #add to

Now it's time to get CircleCI to run the automated tests. Overwrite config.yml with the following description and push it to git.

yml:./circleci/config.yml


version:                     2.1

#Job to run
jobs:
  #job to build
  build:
    machine:
      image:                 circleci/classic:edge
    steps:
      - checkout
      - run:
          name:              docker-compose build
          command:           docker-compose build
  #job to test
  test:
    machine:
      image:                 circleci/classic:edge
    steps:
      - checkout
      - run:
          name:              docker-compose up -d
          command:           docker-compose up -d
      - run:                 sleep 30
      - run:
          name:              docker-compose run back rails db:create RAILS_ENV=test
          command:           docker-compose run back rails db:create RAILS_ENV=test
      - run:
          name:              docker-compose run back rails db:migrate RAILS_ENV=test
          command:           docker-compose run back rails db:migrate RAILS_ENV=test
      - run:
          name:              docker-compose run back bundle exec rspec spec
          command:           docker-compose run back bundle exec rspec spec
      - run:
          name:              docker-compose down
          command:           docker-compose down

#Workflow to control the order
workflows:
  build_and_test_and_deploy:
    jobs:
      - build
      - test:
          requires:
            - build

Did it! It's a success! CircleCI success.png

-[x] Hello World! Using Rails and Nuxt! -[x] Start Rails in API mode-Pull Rails data with Nuxt. --[x] Create a user table with Rails and create a user with Nuxt. -[x] Install rspec and run the test. -[x] Try automatic testing in cooperation with CircleCI.

That's all for the purpose of this article. Thank you for your hard work.

Finally

Thank you for your hard work. I'm sorry it's been so long. I wrote this article with the hope that there would be an article that would create an environment that could be easily developed immediately. Since it is linked to CircleCI, I think that test-driven development can be done immediately in this environment. These are just examples, so I think there is a better structure and writing style. In that case, try implementing it yourself, and if you think "this is better!", I would be grateful if you could comment.

If I have time, I think I'll write these up to production deployment using ECS in the AWS environment. After that, I think that I will finish writing up to IaC with terraform.

Recommended Posts

[Part 1] WEB service created with Rails + Nuxt + MySQL + Docker is automatically tested and deployed with ECS / ECR / CircleCI and converted to terraform.
[Part 2] Automatically test and deploy WEB services created with Rails + Nuxt + MySQL + Docker with ECS / ECR / CircleCI to make them terraform
[AWS] How to automatically deploy a Web application created with Rails 6 to ECR / ECS using CircleCI ① Preparation [Container deployment]
[Part 1] How to deploy Docker containers and static files with CircleCI + ECS + ECR + CloudFront
Wait for PostgreSQL to start with Docker and then start the WEB service
Deploy to heroku with Docker (Rails 6, MySQL)
[For super beginners (more screenshots)] Automatically deploy to AWS ECR / ECS with Ruby2.6 x Rails6 x CircleCi [Hands-on format]
[Rails] [Docker] Copy and paste is OK! How to build a Rails development environment with Docker