1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その3、Nuxt.js編

Last updated at Posted at 2023-06-05

はじめに

RailsとNuxt3でtodoリストの作り方を
初めから丁寧に説明したいと思います。

使用pcはmacを想定しています。

完成した構成図は以下の通りです。

aws_structure.png

また、githubレポジトリはこちらです。

各シリーズは以下の通りです。

RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その1、Rails基本設定編
RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その2、Rails API編
RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その3、Nuxt.js編
RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その4、TerraformECS前編
RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その5、TerraformECS後編
RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その6、Blue/Greenデプロイ前編
RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その7、Blue/Greenデプロイ後編

Nuxt.jsをインストールする

webserver/Dockerfile
node:18.15-buster-slim

WORKDIR /usr/src/app

コンテナの中に入り、Nuxt.jsをインストールします。

docker compose exec webserver bash

myappはアプリケーション名です。
お好きな名前にしましょう。

npx nuxi init myapp
cd myapp
npm install

Screenshot 2023-03-28 at 18.46.28.png

Dockerの構成を見直す

理由は以下の通りです。

  • railsとport番号が衝突しているので回避する
  • コンテナデプロイができるように、コンテナ起動時にアプリを立ち上げたい
  • Nuxt.jsでホットリロードをしたい
  • ロードバランサーを導入しCORS対策にリバースプロキシをしたい
  • 本番環境用と練習環境用で分けたい

apserver側(rails)

本番環境では、レッドチームにリバースシェル攻撃を受けた際、パッケージのインストールができないように、ユーザーを変えておきます。

apserver/Dockerfile
# For development
FROM ruby:3.1.3-slim-buster as development
WORKDIR /usr/src

ENV HOST dbserver
ENV DBNAME todoproject
ENV USERNAME rubyMySql1
ENV PASSWORD rubyMySql1

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
    g++=4:8.3.0-1 make=4.2.1-1.2 \
    libmariadb-dev=1:10.3.38-0+deb10u1 \
    mariadb-client=1:10.3.38-0+deb10u1 \
    git=1:2.20.1-2+deb10u8 \
    && apt-get -y clean \
    && rm -rf /var/lib/apt/lists/*

COPY . .
WORKDIR /usr/src/app
RUN bundle install

# For staging
FROM development as staging

RUN groupadd -r rails && useradd -r -g rails rails
RUN chown -R rails:rails /usr/src/app
USER rails
EXPOSE 8080

CMD ["rails", "s", "-b", "0.0.0.0","-p", "8080"]
apserver/.dockerignore
.git
*Dockerfile*
*docker-compose*
apserver/docker-entrypoint
#!/usr/bin/env bash

set -euo pipefail

function tmp_file_exits() {
  if [ -e /usr/src/app/tmp/pids/server.pid ]; then
    rm -f /usr/src/app/tmp/pids/server.pid
    echo "delete successful server.pid file"
  else
    echo "file doesn't exits"
  fi
}

function main() {
  tmp_file_exits
  cd /usr/src/app/app
  bundle exec rails s -p 8080 -b '0.0.0.0'
}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  main "$@"
fi

webserver側(Nuxt.js)

WATCHPACK_POLLING=trueはホットリロードを許容しています。

apserver/Dockerfile
# For development
FROM node:18.15-buster-slim as development

COPY ./docker-entrypoint /docker-entrypoint
WORKDIR /usr/src/app/myapp
ENV WATCHPACK_POLLING=true
ENV NODE_ENV development

COPY ./myapp/package*.json ./
RUN npm install
COPY ./myapp .

ENTRYPOINT [ "/docker-entrypoint" ]

# For build
FROM node:18.15-buster-slim as build
WORKDIR /app
COPY ./myapp/package*.json ./
RUN npm ci
COPY ./myapp .
RUN npm run build

# For staging
FROM gcr.io/distroless/nodejs18-debian11:nonroot as staging
WORKDIR /app
ENV NODE_ENV production
ENV HOST 0.0.0.0

COPY --from=build --chown=nonroot:nonroot /app/.output /app/.output

USER nonroot
EXPOSE 3000

CMD [ "/app/.output/server/index.mjs" ]
apserver/.dockerignore
.git
*Dockerfile*
*docker-compose*
node_modules

/tmp/nitro/worker-30-1.sockが存在するため、エラーが出て
立ち上がらないので、コンテナ起動時に削除しています。

webserver/docker-entrypoint
#!/usr/bin/env bash

set -euo pipefail

function tmp_file_exits() {
  if [ "$(ls -A /tmp/nitro/)" ]; then
    rm /tmp/nitro/*
    echo "delete successful tmp file"
  else
    echo "file doesn't exits"
  fi
}

function main() {
  tmp_file_exits
  npm run dev
}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  main "$@"
fi

loadbalancer側

loadbalancer/nginx.conf
server {
    listen 80;
    listen [::]:80;
    server_name localhost;

    location / {
        proxy_pass http://webserver:3000;
    }
    location /api/ {
        proxy_pass http://apserver:8080;
    }
}

docker-compose

24678はNuxt.jsのホットリロード時の待ち受けportです。

docker-compose.yml
version: "3"
services:
  apserver:
    build:
      context: ./apserver
      target: development
    container_name: "ruby"
    tty: true
    entrypoint: /usr/src/docker-entrypoint
    volumes:
      - ./apserver:/usr/src
    ports:
      - "8080:8080"
  webserver:
    build:
      context: ./webserver
      target: development
    volumes:
      - ./webserver/myapp:/usr/src/app/myapp
      - /usr/src/app/myapp/node_modules
    ports:
      - "3000:3000"
      - '24678:24678'
  loadbalancer:
    image: nginx:1.22-bullseye
    ports:
      - "80:80"
    volumes:
      - ./loadbalancer:/etc/nginx/conf.d:ro
  dbserver:
    build: ./dbserver
    container_name: mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: todoproject
      MYSQL_USER: rubyMySql1
      MYSQL_PASSWORD: rubyMySql1
      TZ: 'Asia/Tokyo'
    command: mysqld
    volumes:
      - ./dbserver/conf/my.cnf:/etc/mysql/conf.d/my.cnf
    ports:
      - 3306:3306
    cap_add:
      - SYS_NICE

Nuxt.jsを作ろう

railsを少しいじる

出力をselectする。

apserver/app/app/controllers/api/todos_controller.rb
def index
    @todos = Todo.all.select(:id, :content)
    render status: :ok, json: @todos #200

共通部分を作る

外部apiからデータを受け取りたいので、ssr: falseする。
また、cssフレームワークPrimer CSSをインポートします。

webserver/myapp/nuxt.config.ts
export default defineNuxtConfig({
    ssr: false,
    app: {
        head: {
            title: "Todolist",
            meta: [
                { charset: "utf-8" },
                { name: "viewport", content: "width=device-width, initial-scale=1" },
            ],
            link: [
                { rel: "icon", type: "image/x-icon", href: "/favicon.ico" },
                { rel: "stylesheet", href: "https://cdn.jsdelivr.net/npm/@primer/css@20.8.3/dist/primer.min.css" }
            ],
            script: [
                { src: "https://cdn.jsdelivr.net/npm/@primer/css@20.8.3/dist/primer.min.js" }
            ],
        },
    }

})

全体のtemplate設定

webserver/myapp/app.vue
<template>
  <div>
    <NuxtLayout>
      <NuxtPage />
    </NuxtLayout>
  </div>
</template>

header、hooterなど共通レイアウトを設定できます。

webserver/myapp/layouts/default.vue
<template>
    <div>
        <Header />
        <slot />
    </div>
</template>
webserver/myapp/pages/index.vue
<script>
import Tables from '~~/components/Tables.vue';
import Forms from '~~/components/Forms.vue';
</script>
<template>
    <div class="d-flex flex-justify-center flex-items-center">
        <div class="container mt-3">
            <div class="columns mb-1">
                <Forms />
            </div>
            <br>
            <div class="columns mb-1">
                <Tables />
            </div>
        </div>
    </div>
</template>

<style>
p {
    text-align: center;
}
</style>
webserver/myapp/composables/useFlagStore.ts
import { Ref } from "nuxt/dist/app/compat/capi";

export const useFlagStore = () => {
    const flag = useState("flag", () => (false))
    return {
        flag: readonly(flag),
        setFlag: setFlag(flag),
        deleteTodo: deleteTodo(flag)
    };
};

const setFlag = (flag: Ref<boolean>) => (val: boolean) => { flag.value = val };

const deleteTodo = (flag: Ref<boolean>) => (num: string) => {
    const currentUrl = window.location.href
    const pending = useFetch(currentUrl + 'api/todos/' + num, { method: "delete", })
    flag.value = true
};

comportnentを作る

webserver/myapp/components/Forms.vue
<script setup lang="ts">

const { setFlag } = useFlagStore();
const task = ref('');

const handleClick = () => {
    const currentUrl = window.location.href;
    useFetch(currentUrl + 'api/todos', {
        method: 'post',
        body: { "content": task.value },
    });
    task.value = '';
    setFlag(true);
};
</script>

<template>
    <div>
        <form>
            <div class="input-group">
                <input class="form-control" type="text" placeholder="write your something to do" v-model="task">
                <span class="input-group-button">
                    <button class="btn" type="button" aria-label="Copy to clipboard" @click="handleClick">
                        confirm
                    </button>
                </span>
            </div>
        </form>
    </div>
</template>
webserver/myapp/components/Header.vue
<template>
    <h1 class="header">todolist</h1>
</template>
<style>
.header {
    z-index: 999;
    top: 0;
    left: 0;
    width: 100%;
    padding: 20px 40px;
    background: #eee;
    box-sizing: border-box;
}
</style>
webserver/myapp/components/Tables.vue
<script setup lang="ts">
const currentUrl = window.location.href;
const { data, pending, error, refresh } = await useFetch(currentUrl + 'api/todos', { method: "get", });

const { flag, setFlag, deleteTodo } = useFlagStore();
watch(flag, () => {
    if (flag) {
        const startMsec = new Date();
        while (new Date() - startMsec < 100);
        refresh();
        setFlag(false);
    }
})
</script>

<template>
    <div>
        <p v-if="error">{{ error }}</p>
        <table border="1" width="500">
            <tbody>
                <tr v-for="todo in data" :key="todo.id">
                    <td class="content">{{ todo.content }}</td>
                    <td class="button">
                        <button class="btn-mktg btn-signup-mktg full-button" type="button"
                            @click="deleteTodo(todo.id)">Delete</button>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</template>

<style>
.full-button {
    width: 100%;
    height: 100%;
}

.content {
    width: 80%;
}

.button {
    width: 20%;
}
</style>
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?