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
--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!
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
--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)
I gave the finished product to git. qiita-sample-app
app //Any name is fine
├─docker-compose.yml 
├─front
|   ├─Dockerfile
└─back 
    ├─Dockerfile
    ├─Gemfile
    └─Gemfile.lock
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
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"]
back/Gemfile
source 'https://rubygems.org'
gem 'rails', '~>6'
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:
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)
./back/db.env
MYSQL_ROOT_PASSWORD=rootpassword
MYSQL_USER=username
MYSQL_PASSWORD=userpassword
./
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
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
./
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
** 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
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!

hello, Rails
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
./
#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.
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
http://localhost:3000/ Access to! If you see the familiar screen below, you are successful.

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.
./
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!
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.
Communication from nuxt to rails requires some preparation. Let's not rush.
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 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
  ],
...
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"
})
./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
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>
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>

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.
Now, let's set up and add an automated test!
./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
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.
Next, let CircleCI automatically test the test created above every time you push it with git!
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!

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

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.
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!

-[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.
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