20210917のvue.jsに関する記事は7件です。

laravel 419エラー

laravelでvue.jsを使っていて localstorageからデータを読み取って自動ログインするようにしているが、 なぜか 419エラーが頻発してログアウトしちゃう。 csrfが悪さしてるみたい。 その場合の対象方法。 36行目らへんの以下の行をコメントアウトすればOK app/Http/Kernel.php protected $middlewareGroups = [ 'web' => [ // \App\Http\Middleware\VerifyCsrfToken::class, ], ]; 以上
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Alpine.jsを始めよう!

はじめに みなさま、Alpine.jsをご存知でしょうか? まあ、ものすごく簡単に言うと かんたんなVue.js です。 ただでさえJSフレームワークの中では比較的簡単目なVue.jsですが、さらにかんたんです。 どんぐらいかんたんかというと jQueryと同じかそれ以上 ってレベルです。 しゅごい! 今回はこれを紹介していこうと思います。 JSフレームワークとは SPAとかJSでなんかすごいことやりたいときに入れるやつ 、くらいの雑な認識で大丈夫です。 いわゆる3大フレームワークと言われているのがお互い影響し合いつつ、競い合っている感じです。 概ねデータバインディング(JS内の変数の値をそのまま出したり、formから書き込んだり)がしたいときに使われることが多いかと思います。 Angular おそらく一番初めに出たJSフレームワーク Google製で、Google内で燃えに燃えている案件を鎮火させる過程で生まれたリアル銀の弾丸 ver1の頃は「AngularJS」だったが、2以降ほぼ別物になったので「JS」が名前から外れた React Facebook製 プログラマライクな作りなおかげか、おそらく最も人気がある ふだんJSを書かない人には死ぬほど敷居が高い Vue.js GoogleでAngularを使っていたエヴェン・ヨーによって開発された 感覚的にはAngularとReactのいいとこ取りって感じ .vueファイルにhtmlもjsもcssもまとめて書いちゃう Vue.jsと違うところ、同じところ v2の日本語版ドキュメントでも白状されていますが、 このツールのシンタックスは、ほぼ完全に Vue(それと、Angular による拡張)から借用しています。 ということで、結構シンタックスが似ています。 普段Vue.jsを使っている、あるいは挫折してしまったという人には朗報な気がします。 違うところ マスタッシュ記法 {{ }} でデータバインディングをしない Vue.jsでは hoge.vue <template> <div> <p>{{ hoge }}</p> </div> </template> <script> export default { data() { return {hoge: 'ほげ'}; } } </script> みたいに書くと ほげ って表示されます。 script の data のところで返している hoge を template の {{ hoge }} のところに当てはめています。 これがいわゆるマスタッシュ記法なんですが、これがAlpine.jsだとできません。 ちなみにAlpine.jsで同じことやると hoge.blade.php <div x-data="{hoge: 'ほげ'}"> <p x-text="hoge"></p> </div> だけでいいのです。すごくないですか? ディレクティブのprefixが違う 上の例でもチラチラ出てますが、 x-data とか x-text ってところですね。 Vue.jsに v-data はないのですが、 v-text はありますよね。 そう、Vue.jsで v- にしていたところが x- になっています。 Vue.js脳の人はそのままxにしていただければいいってことですね。 なんでXなのかは知りません。エェーックス!! .vueファイルを作らなくていい Vue.jsでも.vueファイル作ることが必須ではないのですが、作らずにコンポーネントを作ろうとすると大きな困難に立ち向かうことになるので、だいたい作ると思います。 Alpine.jsはhtmlソースに直接ガンガン書けるので大体のケースでは外部ファイルでわざわざJS書く必要すらありません。 同じところ シンタックスシュガーがほぼそのまま使える まあ、とくに on と bind のところですね。 こいつらは @ と : でいけます。おかげさまでVue.js書いてる感が出て楽しいです。 タブインターフェースを作ってみよう! Alpine.jsをつかってよくあるタブインターフェースを作ってみましょう! Alpine.jsは今流行りの tailwindcss とセットで語られることが多いので、それも使ってみましょう。 土台のHTMLをつくる tailwindcssのドキュメントに HTML starter template とかちょうど良さそうなのがあるので、これを引っ張って、そこにtailwindcssとAlpine.jsをCDNから引っ張ってくるコードを足してみました。 index.html <!doctype html> <html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet"> <script defer src="https://unpkg.com/alpinejs@3.3.4/dist/cdn.min.js"></script> <title>Alpine tab sample</title> </head> <body> </body> </html> 次に tailwindcomponents というサイトに良さげなタブの作例があるのでこちらを参考にさせてもらいます。今回はこちらを使用させていただきました。 https://tailwindcomponents.com/component/simple-tab これらを使って桃太郎と浦島太郎を切り替えるタブを作ってみました。 htmlだけだとこんな感じです。 index.html <!doctype html> <html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <script defer src="https://unpkg.com/alpinejs@3.3.4/dist/cdn.min.js"></script> <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet"> <title>Alpine tab sample</title> </head> <body> <div class="p-8"> <div style='border-bottom: 2px solid #eaeaea'> <ul class='flex cursor-pointer'> <li class='py-2 px-6 bg-white rounded-t-lg'>桃太郎</li> <li class='py-2 px-6 bg-white rounded-t-lg text-gray-500 bg-gray-200'>うらしまたろう</li> </ul> </div> <div> むかし、むかし、あるところに、おじいさんとおばあさんがありました。まいにち、おじいさんは山へしば刈かりに、おばあさんは川へ洗濯せんたくに行きました。  ある日、おばあさんが、川のそばで、せっせと洗濯せんたくをしていますと、川上かわかみから、大きな桃ももが一つ、 「ドンブラコッコ、スッコッコ。 ドンブラコッコ、スッコッコ。」  と流ながれて来きました。 </div> <div> むかし、むかし、あるところに浦島太郎という心やさしい漁師が住んでいました。 ある日のこと、浜辺を歩いていると一匹の亀が子供達にいじめられているのを見ました。 「これこれ、かめをいじめたらかわいそうだよ。はなしておやり」 そう言って浦島太郎は子供たちから亀を助けてやりました。 </div> </div> </body> </html> 現状話が混ざってしまってますね。ここにAlpine.jsのロジックを入れていきましょう。 Alpine.jsの仕掛けをいれる まずは x-data ディレクティブを入れます。 これは一番てっぺんのdivに入れてあげます。 x-data は とっておきたいデータを入れておく場所 です。 この辺に入れます <body> <div class="p-8" x-data="{open: 1}"> x-data の値のところはまんまjsのobjectですね。 次にイベントハンドラを入れていきましょう。 タブのところですね。 <div style='border-bottom: 2px solid #eaeaea'> <ul class='flex cursor-pointer'> <li class='py-2 px-6 bg-white rounded-t-lg' @click="open = 1">桃太郎</li> <li class='py-2 px-6 bg-white rounded-t-lg' @click="open = 2">うらしまたろう</li> </ul> </div> @click ってところがイベントハンドラです。clickって言っているくらいなので ここをクリックしたら ってところです。 値のところはどんな動作をさせるか、なんですが、ここでは x-data に入れている open の値を変えているよーっていう動作を入れています。 これでタブをクリックすれば内容が切り替わるようになったのですが、タブの色が切り替わらないので、切り替わるロジックを入れてやります。 <div style='border-bottom: 2px solid #eaeaea'> <ul class='flex cursor-pointer'> <li class='py-2 px-6 bg-white rounded-t-lg' @click="open = 1" :class="{'text-gray-500 bg-gray-200': open !== 1}">桃太郎</li> <li class='py-2 px-6 bg-white rounded-t-lg' @click="open = 2" :class="{'text-gray-500 bg-gray-200': open !== 2}">うらしまたろう</li> </ul> </div> :class ってところが ロジックで適用するクラスを切り替えているところ になります。 値のobjectのvalueは判定式になっていて、そこが true のときにkeyに書いてあるクラスが適用されます。 できあがり 最終的なソースはこんな感じになります。 index.php <!doctype html> <html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <script defer src="https://unpkg.com/alpinejs@3.3.4/dist/cdn.min.js"></script> <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet"> <title>Alpine tab sample</title> </head> <body> <div class="p-8" x-data="{open: 1}"> <div style='border-bottom: 2px solid #eaeaea'> <ul class='flex cursor-pointer'> <li class='py-2 px-6 bg-white rounded-t-lg' @click="open = 1" :class="{'text-gray-500 bg-gray-200': open !== 1}">桃太郎</li> <li class='py-2 px-6 bg-white rounded-t-lg' @click="open = 2" :class="{'text-gray-500 bg-gray-200': open !== 2}">うらしまたろう</li> </ul> </div> <div x-show="open === 1"> むかし、むかし、あるところに、おじいさんとおばあさんがありました。まいにち、おじいさんは山へしば刈かりに、おばあさんは川へ洗濯せんたくに行きました。  ある日、おばあさんが、川のそばで、せっせと洗濯せんたくをしていますと、川上かわかみから、大きな桃ももが一つ、 「ドンブラコッコ、スッコッコ。 ドンブラコッコ、スッコッコ。」  と流ながれて来きました。 </div> <div x-show="open === 2"> むかし、むかし、あるところに浦島太郎という心やさしい漁師が住んでいました。 ある日のこと、浜辺を歩いていると一匹の亀が子供達にいじめられているのを見ました。 「これこれ、かめをいじめたらかわいそうだよ。はなしておやり」 そう言って浦島太郎は子供たちから亀を助けてやりました。 </div> </div> </body> </html> 桃太郎時 うらしまたろう時 まとめ 以上、Alpine.jsのしょうかいでした! こんな恐ろしく簡単に、サクッととんでもないことできちゃうの、熱くないですか?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js 基本的なディレクティブ v-bind編

はじめに Vue .jsを学習し始めたのでアウトプットしていきます ディレクティブとは v-on で始まる特別な属性のこと  Vue.jsになんらかなの指示を行う仕組み マスタッシュ構文とは {{ 変数名 }} とすることで、Vueに定義したdataの変数をコンポーネントやテンプレートに埋め込むことができる。 v-bind マスタッシュ構文はtextコンテンツのための記法、属性には使用できない 属性へのデータバインディングにはv-bindディレクティブを使用 <input type="text" value="{{プロパティ名}}"/> ←❌ <input type="text" v-bind:value="プロパティ名"/> ← ⭕️ 例 index.html <div id="app"> <input type="text" v-bind:value="message"/> </div> app.js var app = new Vue({ el: '#app', data: { message: 'Hello Vue.js!' } }) 上記のように記述すると と表示される。 v-bindはこんな感じで使ってください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Nuxt.js / Vue.js】グローバル定数を定義して、v-for内の算出プロパティで使用する

はじめに ステータス管理をしたいな~となったときに、DBでは大抵数字とかで管理するのではないでしょうか。 1だったら進行中で~2だったら完了で~など各ページのdata内に書いたりしても間違いではないと思いますが、グローバル定数をいい感じで管理したいなと思って実践したのでその備忘録です。 間違いや、もっとこうしたほうがいいよ~とか、こんなやり方もあるよ~など何かありましたらコメントお願いします! グローバル定数管理をする jsファイルの作成 まずは定数を定義するファイルを~/pluginの直下に作成します。今回はconstants.jsとし、以下のようにしました。 constants.js const SAMPLE = 'sampleですよ' const PROJECT_STATE = { 1: '開始前', 2: '進行中', 3: '終了', } export default (context, inject) => { inject('SAMPLE', SAMPLE) inject('PROJECT_STATE', PROJECT_STATE) } 文字列を返すSAMPLEと、ステータスの情報を持ったPROJECT_STATEの2つを定義してみました。 定義した2つをinjectすることで呼び出すことができます。 injectとは Nuxt.jsのプラグインの機能です。 このinjectを使うことで、Nuxt.jsのアプリケーションで、いろいろな場所から共通で利用したい関数や値を呼び出すことができます。 なので題名と反しますが厳密にはグローバル変数を定義している、というわけではないです。ただinjectしてどこからでもアクセスできるようにした状態ということです(ややこしい) 今回のようなパターンでは、グローバル変数定義してimportで読み込むのでもいいのかなと思いますが、それは今度別記事でまとめます(覚えていれば) nuxt.config.jsで設定 nuxt.config.jsに記述を追加して先ほど定義したものを読み込むようにします。 pluginsに以下のように追加しました。 nuxt.config.js plugins: [ { src: '~/plugins/constants.js', ssr: false} ], 実際に使用する <script> ~~略~~ computed: { sample() { return this.$SAMPLE } } this.$定数名で呼ぶことができます。 v-for内の算出プロパティで使用する たとえばこんなデータがあるとします。 <script> export default { data() { return { projectList: [ { projectName: "sample1", projectId: "1", about: "sample1のプロジェクトです。", state: 1 }, { projectName: "sample2", projectId: "2", about: "sample2のプロジェクトです。", state: 2 }, { projectName: "sample3", projectId: "3", about: "sample3のプロジェクトです。", state: 3 } ] } } } これをv-forで表示し、かつステータスは数字をもとに先ほど記述したPROJECT_STATEを参照します。 <template> <v-app> <template v-for="project in projectList"> <div> {{ project.projectName }} </div> <div> {{ projectState(project.state) }} </div> <div> {{ project.about }} </div> </template> </v-app> </template> <script> export default { computed: { projectState: function() { return function(state) { return this.$PROJECT_STATE[state] } } } } </script> methodsに書いてもよかったのですが、何度も再描画は避けたいなと思ったのでcomputedに書いてみました。 参考にしたもの Vue.jsでのグローバル定数管理を工夫してみた話 Nuxt.jsで異なるコンポーネントから共通で利用できる関数を定義する(inject編) Nuxt.jsのinjectを使ってDIする さいごに 今回の定数はすべてのページで使用するというわけではないので、普通に別ファイルで定義してimportでもいいのかな、と思いましたが、共通関数とか書くときはこの書き方がいいのかな、と思います。 書き方は他にもいろいろあるので調べてみてください。 まだまだ初心者なのでうまい使い分けが難しいものです。 最後まで読んでいただきありがとうございます!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue.js ブラウザ戻るボタン

vue.js で戻るボタンを作りたい <a @click="$router.back()">戻る</a> たったこれだけ♪
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelばかりやってた人間がVue.jsでSPAを作るまでの学習記録

現状の成果物 Laravel+Vueのリポジトリ(https://github.com/nawawa/spa-tutorial-laravel) Vue単体SPAのリポジトリ(https://github.com/nawawa/spa-tutorial-vue) 筆者の背景 7ヶ月間のエンジニアインターンでは、ほとんどの間Laravelによるバックエンドの実装をさせていただきました。 フロントエンドも多少触らせていただきましたが、jQueryをメインに使用するコンポーネント化を一切行わない中での業務となっていたため少し面倒な部分も多く、学習しやすいといわれるVue.jsにずっと興味を持っていました。 業務を行うに当たっての補助的な学習に追われていましたが、インターン期間が終了したことで時間を確保できたこともあり、本格的に学習してみました。 記事の趣旨 Vue.jsをゼロから学習し、いわゆるSPAでWebアプリケーションを作成するまでの学習記録です。 SPAとは? 一枚のHTMLページの中で、JavaScriptから生成したHTMLを入れ替えることで擬似的にページ遷移を行うアーキテクチャです。アクセスした段階でHTMLを生成するので初期表示が遅くなるほか、DOMをGoogleのクローラーが解析できないのでSEOに弱いとも言われています。 ただデプロイが容易であることと、一回描画が済んでしまえば全体の読み込みが発生しないため、かなり高速に動作させられるのがメリットです。 学習その1:CDN+書籍 こちらのサイトに掲載されているチュートリアルを見ながら、環境構築はしないままハンズオン的に触ってみました。LaravelのBladeテンプレートに慣れていたので、Vue.js自体の基本的なディレクティブ等の機能は抵抗なく理解することができました。 上記サイトは『基礎から学ぶ Vue.js』という書籍のサポートサイトとなっており、最初のチュートリアルを終えた後は実際に書籍にて学習を継続しています。 学習その2:Laravelと組み合わせたチュートリアル 書籍での学習に飽きてきたこともあり、実際に動くものを作ってみようということで、@minato-naka様のこちらのチュートリアルにて開発を体験しました。 最初からバックエンドから切り離された形で作るのはハードルが高く感じられたので、LaravelのBladeテンプレートと組み合わせてSPA化するこちらの内容はとてもちょうどよかったように思います。 実務ではjQueryとES6構文で実装したソースをLaravel-mixでトランスパイルしていたこともあり、いい比較になりました。 学習その3:バックエンドを流用し、別で単独SPAを制作 Laravelで実装したAPIをそのまま使い、フロントエンド部分だけを切り離して独立したVueアプリケーションを作りました。 以下、実践した内容です。 vue-cli+docker-composeを使った環境構築 いちど使い始めてからはずっとコンテナ至上主義です…tet0h様による記事(Zenn)に従ってローカルを構築しました。ただDockerfileだけこのようにすることで、npmのアップデートを行う際のバグを回避する必要があるようです。 FROM node:15.11.0-alpine WORKDIR /app RUN apk update && \\ npm install && \\ npm install -g agentkeepalive --save \\ npm install -g npm@latest && \\ npm install -g @vue/cli 学習メモ:vue-cliは、Vue.js公式の開発支援ツールです。自前で一から用意するとめんどくさいような色々を揃えてくれます。 ESLint(構文チェッカー)の導入 単一ファイルコンポーネントをビルドする機能 テストツール(Jestとか)の導入 トランスパイラの導入…など バージョンを指定していないので、この場合は最新版(2021年9月現在は4系)がインストールされます。 Vueアプリケーション立ち上げ ビルドしたコンテナに入り、vue init webpack アプリ名を実行することでVue2系アプリケーションの雛形が作られます。 コンテナ内でバージョン確認 /app # npm list -g vue /usr/local/lib `-- @vue/cli@4.5.13 `-- vue@2.6.14 学習メモ:新しいアプリケーション作成時のコマンドにはvue createとvue initがありますが、vue-cliのバージョン2系まではinit、3系以降はcreateが使えるようになったそうです。(@teraco様の記事より) ディレクトリ構成に違いがあるもののどちらかでしかできないことはなく、筆者はこれを知らないまま記事をなぞってvue init webpackで作りました。公式としてはvue createが推奨のようで、実際vue initで作成する場合と比較して、細部が効率化できるとのことです。 Vue.jsによるSPAの構造 このようにして作られたVueアプリケーションは、以下のようなディレクトリ構成となっています。 ./ ├─build/ │    ├─build.js │    ├─check-versions.js │    ├─logo.png │    ├─utils.js │    ├─vue-loader.conf.js │    ├─webpack.base.conf.js │    ├─webpack.dev.conf.js │    ├─webpack.prod.conf.js │    └─webpack.test.conf.js //※2 │ ├─config/ │    ├─dev.env.js │    ├─index.js │    ├─prod.env.js │    └─test.env.js │ ├─src/ │    ├─assets/ │ └─logo.png │    ├─components/ │ └─HelloWorld.Vue │    ├─router/ │    ├─App.vue │    └─main.js │ ├─static/ │    └─.gitkeep │ ├─test/ //※2 │    ├─e2e/ │    └─unit/ │ ├─index.html ├─package.json ├─README.md ├─.editorconfig ├─.eslintignore //※1 ├─.eslintrc.js //※1 ├─.gitignore └─.postcssrc.js これらファイルからどのようにアプリが形成されるかというと… 一部のファイルに絞って見てみます。 ./ ├─src/ │    ├─assets/ │    ├─components/ │ └─HelloWorld.Vue │    ├─router/ │    ├─App.vue │    └─main.js ├─index.html npm run devでアプリをビルドする際index.htmlが参照され、SPAの基盤となるHTMLが生成されます。 なので一度Ctrl Cでストップしたあと<title>を書き換え、もう一度ビルドすると、ちゃんとブラウザタブに表示されているタイトルが変わっていることがわかります。 では、ページの中身はどのように定義されているのか。 index.jsを見てみると、このようなソースがあります。 new Vue({ el: '#app', router, components: { App }, template: '<App/>' }) Vueインスタンスのローカルに、Appコンポーネントが登録されています。 つまり「アプリ全体からAppコンポーネントへのアクセスができる」ということで、今のところはAppコンポーネントがアプリの全体像そのものとなっています。 Appコンポーネントはこのようになっています。 <template> <div id="app"> <img src="./assets/logo.png"> <router-view/> </div> </template> <script> export default { name: 'App' } </script> <style> #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style> 一番下にはCSS、その上のscriptはAppという名前でエクスポートしているだけ。 一番上のテンプレートでは<router-view/>というタグがあります。これがつまり、このSPAの切り替わる部分であり、各ルートごとのコンポーネントが表示される部分ということになります。 ルートはindex.jsに記載されています。 export default new Router({ routes: [ { path: '/', name: 'HelloWorld', component: HelloWorld } ] }) 定義されているのはトップページのルートのみ。 トップページにアクセスするとHelloWorldコンポーネントのテンプレートが描画されるようになっています。他のページを追加する際は、HelloWorldコンポーネントの位置に違うコンポーネントを指定することで遷移をさせることになります。 チュートリアルで作ったLaravelフロントエンドを完全SPA化 以上をおさえつつ、全く同じ構成のSPAを構築していきました。 やったことは以下の通りです。 トップページ用コンポーネント追加 前回のチュートリアルでは特にトップページを作り込むようなことはありませんでしたが、どうせならと空のコンポーネントを追加。 bootstrap-vueの追加 先のチュートリアルに合わせ、スタイルはBootstrapを使用。 参考記事:Vue.jsにBootstrapを適用させる方法 axiosの追加 環境構築ではインストールしなかったので、後から追加しています。 参考記事:vue-cliでaxiosを使用する(設定から使用方法まで) ルートからハッシュ記号を除くため、routerの設定を変更 参考記事:Vue.jsでURLの#(シャープ)を取り除く方法 以上により、まったく同じSPAを作ることができました。 今後の学習について 一からVue.jsを勉強し始めて、単独のSPAを作るまでの学習内容をまとめました。 今後は取得したデータを永続化する等の目的でVuexを使ってみたり、BootstrapではなくVuetifyでスタイル付与・レスポンシブ対応をしてみたり、その後はNuxt.jsでSSRやSSGなどもできるようになりたいと考えています。 最終的にはRails+NuxtでSSRするWebアプリを作ることをいったんの目標としていますが、どの程度の時間がかかるかわかりません。急がず進めていこうと思います。 その他参考記事 Vue-cli(webpack)解剖 ーディレクトリ構成ー Vue.js を vue-cli を使ってシンプルにはじめてみる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ECSへのデプロイ時間を4分の1まで短縮した話

はじめに 最近、業務でECS、GitLab CI/CDを用いてCI/CDパイプラインを構築しました。 しかし、構築当初、非常に大きな問題に直面していました。それは、デプロイまでの時間が非常に遅いことです。 当初は、デプロイまでにかかる時間がテスト(CI)を除いても30分以上かかっており、非常に遅かったです。 しかし現在、改良に改良を重ね、ビルド、デプロイまでにかかる時間が7分ほどまでに短縮することができました。この記事では、具体的にどういった方法で早くしたのかを紹介します。 アプリケーションの構成とデプロイの流れ まず初めに、アプリケーションのざっくりとした構成と、デプロイまでの一連の流れを説明します。 アプリケーションの構成は主に、フロントエンドがVue.js、バックエンドはRuby on Railsで作られています。 デプロイまでの流れは以下の通りです。以下は非常に時間が遅かった、構築当初の流れです。 技術的にはGitLab CI/CD, Docker, シェルスクリプト, AWS ECS + Fargateを使い構築しました。 テスト実行(RSpec) イメージのビルドとプッシュ デプロイ実行(ECSタスク定義更新 & ECSサービス更新) どのように早くしたのか、紹介します。 [問題点①] assets:precompileが非常に遅い まず、デプロイする上で一番のボトルネックになっていたのがassets:precompileでした。 こちらは、jsやCSSなどをコンパイルするコマンドですが、当初、コンパイルが完了するまで19分ほどかかっていました。なので、デプロイの半分以上の時間がコンパイルに費やされていました。 [対応策①] コンパイルした内容をローカルにcacheとして保存しておく まずはじめにやったこととしては、前回コンパイルした内容をローカルに保存しておき、次にコンパイルを走らせる時にそのcacheを使わせるようにすることです。 Railsでは、コンパイルした内容はpublic配下に保存されるので、buildしたイメージからdocker cpでpublicの内容を抜き出し、次回のイメージのビルドの際のCOPY句でcacheをコピーするように改良しました。 id=$(docker create ビルドしたイメージ) docker cp $id:/app/public キャッシュの保存先(ホストのパス) docker rm -v $id 次回コンパイル時には、COPY句でソースコードを持ってくる際に、前回コンパイルされたpublicも一緒に持ってくるようにします。 (少し横道に逸れますが、publicフォルダは.dockerignoreで除外していたので、一度sedを使ってdockerignoreからpublicを消してbuildするようにしていました。) ただ、この方法だけだと速度の改善としては微妙でした。 そこで色々調べてみると、publicだけじゃなく、tmp/cacheというフォルダにもコンパイルされたcacheが保存されているらしいということを知り、こちらも保存するように変更したところ、コンパイルが結構早くなりました。 id=$(docker create ビルドしたイメージ) docker cp $id:/app/public CI/CDを動かしてるサーバのローカル docker cp $id:/app/tmp/cache CI/CDを動かしてるサーバのローカル docker rm -v $id しかしこれでもまだ問題がありました。それは、この方法では、前回のコンパイルから今回のコンパイルにかけて、Vue.jsのファイルやassets以下のファイルに変更があった際に、コンパイルが走ってしまい、遅くなることがあったことです。 つまり、この方法だと、前回と今回でコンパイル内容に差異がない場合は、非常に早くコンパイルが終わるが、差異がある場合はコンパイルが以前と比べ少し早くなったかな?というぐらいで全体的には遅いことに変わりないということです。 [対応策②] webpackを使用してコンパイルするように変更 そこで、railsのrakeタスクでコンパイルするのを止めて、yarn run webpackを使うように変更してみました。 すると、コンパイルが高速で終わるようになり、数十秒で終わるようになりました。正直これが一番効果がありました。 yarn run webpack --config config/webpacker/${RAILS_ENV}.js [問題点②] イメージのサイズが大きい 次の問題点はイメージのサイズが大きかったことです。 当初Dockerでローカルの開発環境を構築した際には、railsを動かすイメージのサイズは5GBを超えていました。リポジトリに大容量のデータファイルがあったことも原因の一つでしたが、Dockerfileの書き方にも問題がありました。 イメージのサイズが大きいと、以下のような問題が生じました。 ECRにプッシュするまでに時間がかかる。 ECSでタスクを動かした時にイメージをpullしてくるのに時間がかかる。 イメージのサイズを小さくするのに行った対処を紹介します。 [対応策①] RUNの数を最小限に まず、RUNの数を最小にすることでした。 当初は、コマンド一つにつきRUNを一つという感じで書いていました。(コードは適当です。) RUN apt-get update RUN apt-get install -y sudo ... RUN mkdir -p /app RUN bundle install RUNの数を最小にして、レイヤを減らした結果、数百MBぐらいは削減できたと思います。 この方法はデメリットもあるのですが、その一つがbuild時のエラーでどこでエラーになったか分かりづらいことです。また、Dockerfileの可読性も悪くなるので、イメージサイズとトレードオフになるかなと思います。 RUN apt-get update && \ apt-get install -y sudo ... && \ mkdir -p /app && \ bundle install ただ、サイズを軽くする上で一番手軽な方法でもあると思うので、一度やってみることは非常に有効だと思います。 [対応策②] 必要なパッケージの見直しとキャッシュの削除 まず、現状で必要なパッケージの見直しを行いました。 具体的には、環境を構築する上でインストールする必要がないパッケージを順次削除していきました。結果的にいくつかのパッケージが不要なことがわかり、数MBほどですが、サイズが小さくなりました。 次に、apt-getしたときのキャッシュの削除を行いました。 こちらは、公式のDockerfileベストプラクティスで紹介されていた内容を用いているのですが、apt-get cleanとrm -rf /var/lib/apt/lists/*を加えました。また、apt-get install時に--no-install-recommendsオプションを付け加えました。 RUN apt-get update && apt-get install -y --no-install-recommends \ sudo \ # (中略) && apt-get clean \ && rm -rf /var/lib/apt/lists/* こちらもごくわずか(数十MBぐらい?)ですが、イメージを軽くできました。 [対応策③] multi stage buildを利用 イメージのサイズを軽量化する上で一番効果があったのがmulti stage buildを使うことでした。 具体的にはbundle installとyarn installをインストールするステージを分けました。最新版のdockerを使っていると、ステージごとに並列に処理してくれるので、buildも大分早くなりました。 また、bundle install用にインストールしていたg++やqt4-qmakeなどは、bundle installのステージのみでインストールするように変更してイメージを軽くすることができました。 具体的なコードは以下の通りです。 FROM ruby:2.7.2 AS gem_installer RUN apt-get update && \ apt-get install -y --no-install-recommends g++ qt4-qmake libqtwebkit-dev && \ mkdir -p /app COPY ["./workspace/Gemfile", "./workspace/Gemfile.lock", "/app/"] WORKDIR /app RUN bundle install FROM node:10.24.0 AS webpack_builder RUN mkdir /app WORKDIR /app ENV NODE_ENV development COPY ["./workspace/app/package.json", "./workspace/app/yarn.lock", "/app/"] RUN yarn install FROM ruby:2.7.2 ENV QMAKE="/usr/bin/qmake-qt4" ENV BUNDLER_VERSION=2.1.4 RUN apt-get update && \ apt-get install -y --no-install-recommends nodejs default-mysql-client sudo npm vim fonts-ipa* ssh cron && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* && \ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - && \ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list && \ sudo apt update && \ sudo apt -y install --no-install-recommends yarn && \ npm i npm@latest -g && \ npm install webpack-dev-server -g && \ gem update --system && \ echo "gem: --no-rdoc --no-ri" > ~/.gemrc && \ gem install 'bundler:2.1.4' 'rails:5.2.5' 'mailcatcher' 'git' 'spring' && \ mkdir -p /run/sshd /tools && \ ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime WORKDIR /app COPY ./web/git.rb /tools/ COPY --from=gem_installer /usr/local/bundle /usr/local/bundle COPY --from=webpack_builder /app/node_modules /app/node_modules [対応策④] alpineイメージを使用する こちらは実際にはまだ対応できていませんが、イメージを軽くする上では非常に効果が高いと思いますので紹介いたします。 alpine linuxという非常に軽量なOSをベースにして作られたイメージを使用すると、800MBほど軽量化ができました。 ただ、一度入れようかとチャレンジしたのですが、apkという独自のパッケージ管理を使用しており、結構取っ付きづらかったです。さらに、必要最小限のパッケージのみに絞られているため、shしか入っておらず、触りはじめの頃は大分苦戦しました。 結局、残念ながらbundle installのcapybaraをインストールするところで必ずエラーになり、導入には至れませんでした。 FROM ruby:2.7.2-alpine プロジェクトの初期などにイメージを構築する際には、alpineを使用した構築をおすすめいたします。 9/17 追記 alpineを安易に使用するのはやめておいた方が良いという記事もあるようです? 記事ではDistoressイメージというイメージの利用をおすすめしているようです。代わりに、こちらの方を使ってみても良いかもしれないですね。(私は全く知りませんでした。。) パイプラインの最適化 並列で処理 こちらはマシンのリソース的な問題でまだできていませんが、CI/CDパイプラインで並列に実行できる箇所を今後並列化してゆきたいと思っています。具体的には、「イメージのビルド後、ビルドした内容をホストにキャッシュ」する部分と「イメージをECRにプッシュする処理」は、互に独立しているので、並列で実行ができます。これが実現できれば、さらにCI/CDの時間を短縮することができそうです。 結果的にパイプラインの流れは以下のようになりました。 終わりに こんな感じの対応を行い、デプロイの時間を30分超→8分ぐらいまで短縮できました。非常に長い苦労を伴いましたが、ここまで短縮できてよかったです。 是非参考にしてみてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む