RareJob Tech Blog

レアジョブテクノロジーズのエンジニア・デザイナーによる技術ブログです

Nuxtで環境に応じた環境変数をいい感じに設定する

APP/UXチームに所属しております、フロントエンドエンジニアの田原です。
前回の記事から時間が空きましたが、弊社ブログ2回目の登場です。よろしくお願いします。

(隙あらば自分語り) 私は映画が好きなのでよく映画館に観に行くのですが、
最近はコロナの影響もあり外出を極力控えているので、そうこうしている間に観たい映画の劇場公開が終わってしまうのだろうなという寂しさで胸がいっぱいになる日々を過ごしております。 (直近ではミッドサマーを観に行こうと思っていましたが、、、諦めました)

ちなみに誰も興味がないとは思うのですが一番好きな映画はインファナル・アフェア - Wikipedia(3部作)とダークナイト - Wikipediaです。(一番と言っておきながら4本も上げていますが、どうしても順位がつけられないので)

インファナル・アフェア好き!という方と中々巡り合った事がないので、これを機に好きになる方が増えると嬉しいです。

さて、前置きが長くなってしましたが、今回の本題に入らせて頂きたいと思います。
過去の記事でも紹介しておりますが弊社ではフロントエンドフレームワークにVue.js(以下、Vue)を採用し、開発をおこなっております。

Vueを使った開発経験ある点もあって、最近、新たにスタートしたProjectにおいてはNuxt.js(以下、Nuxt)を採用する運びとなりました。
※前回の記事は↓↓ rarejob-tech-dept.hatenablog.com

今回はNuxtで開発をおこなっていくにあたり、先に設定しておくと楽だなと思ったProject内での環境変数の取り扱いについてご紹介させて頂きます。

目次

環境変数の扱いについて

Nuxtを利用してProject(App)の準備を行うにあたり、create-nuxt-appコマンドで環境の準備をおこないますが、その際に.envファイルが自動で生成されるので、 このファイル内でApp内で利用する環境変数を定義し読み込めるようにすることが多いと思います。

とはいえ、開発環境が多岐にわたってくると(local/dev/prod)初期設定時からの.envファイルのみでは扱いが少し面倒になってくるかと思います。

実現したかった事

  • 環境毎対応する.envファイルを定義しておき、Nuxt Appのdeployやbuildのscriptコマンドに応じて、それぞれの環境変数がApp内で参照出来るようにしたい。
  • 環境毎の設定については可読性や拡張性についても考慮した作りとしたい。
  • SSR対応のProject(App)ではない為、クライアントサイドからの参照ができれば良い。

対応について

1. dotenv(@nuxtjs/dotenv)をProject Appに追加

create-nuxt-appで生成したApp(Project)に対して、dotenv(@nuxtjs/dotenv)を追加します。 github.com

※dotenvとは カレントディレクトリに置かれた.envファイルを読み込み、そこに記述されたキー&バリューのペアをprocess.env 経由で参照できるようするnpm moduleです。 Nuxtで生成されたAppに対しては以下のコマンドでnuxt専用のものを追加します。

yarn add --dev @ nuxtjs / dotenv #またはnpm install --save-dev @ nuxtjs / dotenv
2. nuxt.config.jsに設定を記載

追加したdotenv moduleを利用する為にnuxt.config.jsのmoduleの項目に@nuxtjs/dotenvを追記します。

nuxt.config.js

︙
modules: [
  '@nuxtjs/dotenv'
 ︙
]
3. 各環境に対応するenvファイルを設定

Project Rootに各環境毎に対応する.envファイルを設置します。

config/

config
├── .env.dev
├── .env.local
└── .env.prod

各々の.envファイルにApp内で利用したい環境変数を記載します。

e.g. env.local

BASE_API_ENDPOINT_URL="https://hogehoge/local"

e.g. env.dev

BASE_API_ENDPOINT_URL="https://hogehoge/dev"

e.g. env.prod

BASE_API_ENDPOINT_URL="https://hogehoge/prod"

※変数は複数定義してOK

4. package.jsonのscriptsでENVに各々の環境の文字列を渡すように指定する

package.json

"scripts": {
    "dev": "ENV=local HOST=0.0.0.0 PORT=3000 nuxt",
    "build-local": "ENV=local nuxt build",
    "build-dev": "ENV=dev nuxt build",
    "build-prod": "ENV=prod nuxt build",
  },

※ENVに.envとして指定したファイル名(今回の例ではlocal/dev/prod)を渡している。

5. nuxt.config.jsに設定を追加し、config/配下の.envファイルが読み込まれるように対応する

nuxt.config.js

const envPath = `config/.env.${process.env.ENV}||'local'`
require('dotenv').config({ path: envPath })


export default {
  ︙
  dotenv: {
    filename: envPath
  }
}

【補足説明】 * 4で設定したscriptsでENVに文字列を渡している為、nuxt.config.js内においてprocess.env.〇〇で文字列を取得できる。 * envPathに各.envファイルへのPathが代入され(config/.env.local等)、これによりdotenvを使って3で定義した各.envファイルへの参照が可能に * dotenvのpathにenvPathを指定する。(ファイル名をオーバーライドする為の設定)

6. 型エラー解消

1〜5までの対応が終わっていれば、App内(.vue/.ts等)の各ファイルからprocess.env.〇〇で.envファイルに定義した環境変数に対して参照を行うことができます。

︙
created() {
  process.env.BASE_API_ENDPOINT_URL // scriptsで渡した引数の環境に応じた値が参照可能
}

ただし、process.env.〇〇という形ではApp内のどこからでも呼べてしまうのは統一性が無い&参照の際のprocess.envに型を入れたいので型の指定とplugin化を以下の様に行います。(型指定を行わない場合、Nuxt側で持っているdefaultの型がprocess.envにあたる為、想定した型で参照できない場合があります)

// environments.ts等(plugin file)

export type export type EnvironmentVariables = {
  // nuxt default property ------------
  NODE_ENV: string
  browser: boolean
  client: boolean
  mode: 'spa' | 'universal'
  modern: boolean
  server: boolean
  static: boolean
  APP_NAME: string
  // nuxt custom propety --------------
  BASE_API_ENDPOINT_URL: string
}

export cont environments: EnvironmentVariables = {
  // nuxt default property ------------
  NODE_ENV: process.env.NODE_ENV!,
  browser: process.browser!,
  client: process.client!,
  mode: process.mode!,
  modern: process.modern!,
  server: process.server!,
  static: process.static!,
  APP_NAME: process.env.APP_NAME!,
  // nuxt custom propety --------------
  BASE_API_ENDPOINT_URL: process.env.BASE_API_ENDPOINT_URL!
}

export defalut (context: any, inject:(name: string, v: any) => any) => {
  inject('environments', environments)
}

nuxt.config.js

︙
plugins: ['@/plugins/environments']

準備したplugin file内で適当な名前でinjectしnuxt.config.jsにもpluginの宣言を行います。 また、TypeScript側で環境変数が設定されていることを検知させるためにtypes/enviroments.d.tsを設定します。

import Vue from 'vue'
import { EnvironmentVariables } from '@/plugins/environments'

declare module 'vue/types/vue' {
  interface Vue {
    $environments: EnvironmentVariables
  }
}

最終的にApp内でthis.$enviroments.process.env.〇〇という様に指定した環境変数を参照することができます。

参考にさせて頂いたサイト

dotenvについて
process.envの型指定について

まとめ

APIに対してのRequest basePathが環境によって異なることに気づいたタイミングでこの対応を行いましたが、最初にこの辺りについて対応しておいた方が楽だったなぁっと思い、備忘録も兼ねて内容を残しました。

editor formatのチーム間の設定やaxios-modulesとの付き合い方やstoreの設計、その他にもCSS-moduleやstorybookの設定周辺等、Nuxtで環境を作っていく上でこんな感じで作っていくといいかも思える様な設定などについても、改めてご紹介できればと思っております。

最後まで読んで頂きありがとうございました。

Ansible の cron モジュールで環境差分を減らしてみる

ご無沙汰してます

先日、大好きな女性アーティストである aiko の楽曲がサブスクリプションで提供されて狂喜乱舞しております DevOps チームのおくさんです
好きな aiko の曲は「ジェット」の再録音 ver. です
これまでライブには 3 回程しか行けていませんが、一度もライブで「ジェット」を聴いたことはないです・・・

さて、前回記事を書いてから約半年経過し、久々の執筆となります

前回は AWS の CloudWatch に関する記事を書きましたが、今回は Ansible の利用において便利だなと思った「cron モジュール」について書いてみようと思います

rarejob-tech-dept.hatenablog.com

目次

Ansible の cron モジュール

Ansible では、サーバやネットワーク機器等の設定に関する構成管理を行うことができますが、行う操作によってはモジュールという形で提供されています
たとえば、yum でパッケージをインストールするには「yum モジュール」を利用して各種パッケージのインストール等を行います

「cron モジュール」は文字通り cron 設定を管理するためのモジュールで、以下のドキュメントに利用方法が記載されています
こちらのモジュールを利用することにより、cron 設定の管理を Ansible にて行うことが可能になります

docs.ansible.com

やってみよう

cron モジュールのドキュメントに Examples がありますので、これを参考にしつつ動かしてみましょう

今回はサンプルとして、以下のような Playbook を作成してみました
設定内容は、毎日 0:00 にroot ユーザで date コマンドを実行するものとなります

test_cron.yml

---
- hosts: test
  become: yes
  tasks:
    - name: Test
      cron:
        name: 'Test'
        minute: '0'
        hour: '0'
        day: "*"
        month: "*"
        weekday: "*"
        job: 'date'
        state: 'present'
        user: 'root'

なお、今回利用する Ansible のバージョンは以下です

/work/ansible # ansible --version
ansible 2.9.2

では、作成した Playbook を実行してみましょう

/work/ansible # ansible-playbook -i inventory.ini test_cron.yml

PLAY [test] ********************************************************************************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************************************************************************
ok: [x.x.x.x]

TASK [Test] ********************************************************************************************************************************************************************************************************
changed: [x.x.x.x]

PLAY RECAP *********************************************************************************************************************************************************************************************************
x.x.x.x              : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

サーバの crontab を確認すると、無事に root ユーザで cron の設定が行われたことを確認できます

[root@x.x.x.x ~]# crontab -l -u root
#Ansible: Test
0 0 * * * date

環境にあわせて cron の有効無効を変更する

さて、本題です

Ansible を利用する際には、本番環境や開発環境でなるべく同じ Playbook を利用したい場合があると思います
しかし、「本番環境では cron を動かしたいが、開発環境では cron を動かしたくない」といったように、環境によって行うべき挙動に差が生まれることも多々あると思います

その場合、cron モジュールにおいては、「disabled パラメータ」を利用することにより環境の差分を吸収することが可能です
disabled パラメータはドキュメントに以下のように記載されています

disabled 
boolean

Choices:
no ←
yes

If the job should be disabled (commented out) in the crontab.
Only has effect if state=present.

したがって、本番環境で cron を有効にする場合は「disabled: no」、開発環境で cron を無効にする場合は「disabled: yes」とすることにより、開発環境のみ cron のコメントアウトを行うことができます
これにより、同一の Playbook を利用して環境によって挙動を変更することができます

せっかくなので、上述で利用した Playbook を変更して試してみましょう
本番環境で cron を動かしたいときは以下のような Task になります

- hosts: test
  become: yes
  tasks:
    - name: Test
      cron:
        name: 'Test'
        minute: '0'
        hour: '0'
        day: "*"
        month: "*"
        weekday: "*"
        job: 'date'
        state: 'present'
        user: 'root'
        disabled: 'no'

また、開発環境で cron を動かしたくないときは、以下のような Task になります

- hosts: test
  become: yes
  tasks:
    - name: Test
      cron:
        name: 'Test'
        minute: '0'
        hour: '0'
        day: "*"
        month: "*"
        weekday: "*"
        job: 'date'
        state: 'present'
        user: 'root'
        disabled: 'yes'

違いは disabled パラメータだけであり、このままだと冗長になってしまうので、変数を利用して切り替えましょう
変数を利用することにより、1 つのタスクで動作の切り替えが可能になります

- hosts: test
  become: yes
  tasks:
    - name: Test
      cron:
        name: 'Test'
        minute: '0'
        hour: '0'
        day: "*"
        month: "*"
        weekday: "*"
        job: 'date'
        state: 'present'
        user: 'root'
        disabled: "{{ DISABLED }}"

それでは、変更した Task を利用して Playbook を実行してみましょう

今回はサンプルなので、extra-vars でサクッと変数に値を代入して Playbook を実行します
まずは DISABLED 変数に yes を代入します

/work/ansible # ansible-playbook -i inventory.ini test_cron.yml -e DISABLED=yes

PLAY [test] ********************************************************************************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************************************************************************
ok: [x.x.x.x]

TASK [Test] ********************************************************************************************************************************************************************************************************
changed: [x.x.x.x]

PLAY RECAP *********************************************************************************************************************************************************************************************************
x.x.x.x              : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

実行した結果、cron をコメントアウトができました

[root@x.x.x.x ~]# crontab -l -u root
#Ansible: Test
#0 0 * * * date
[root@x.x.x.x ~]#

今度は、DISABLED 変数に no を代入します

/work/ansible # ansible-playbook -i inventory.ini test_cron.yml -e DISABLED=no

PLAY [test] ********************************************************************************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************************************************************************
ok: [x.x.x.x]

TASK [Test] ********************************************************************************************************************************************************************************************************
changed: [x.x.x.x]

PLAY RECAP *********************************************************************************************************************************************************************************************************
x.x.x.x              : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

実行した結果、cron 有効にすることができました

[root@x.x.x.x ~]# crontab -l -u root
#Ansible: Test
0 0 * * * date

まとめ

ジョブについては各種 OSSクラウドベンダーのサービスを利用して管理することも多いと思いますが、まだまだ各サーバ内で cron として実行している環境も多いと思います
Ansible を利用することでも cron の管理は可能ですので、 この記事がどなたかのお役に立ちますと幸いです

また、Ansible で提供されているモジュールは非常に便利なので、各種ドキュメントをしっかり読みながら、今後もいろいろ探して導入検証をしてみようと思います

レイヤードアーキテクチャを導入した開発

バックエンドエンジニアをしております。久住です。
「レアジョブ英会話 スマートメソッド®コース」ではLaravelとVue.jsを使ったSPAの開発をしておりバックエンドではDDDを意識した実装を行なってます。
その中の「レイヤードアーキテクチャ」について今回は書かせていただきます。

なぜレイヤードアーキテクチャを採用しようとしたのか

よくあるMVCの問題としては以下のようなものがあると思っております。

  • Controllerにビジネスロジックを書くなど、FAT Controllerになる
  • ModelにビジネスロジックとDBアクセスが混在して、メンテナンス性が悪くなる
  • 共通のModel間で自由にコールし合い、互いの依存性が高くなる
  • 共通のModelに、特定のControllerからしか使われないような汎用性の低いメソッドができる
  • 共通のModelに、同じような役割のメソッドが複数できる

他のMVCを採用したプロダクトではこれらの問題がたびたびエンジニアのミーティングでも課題として挙げられる事がありました。

  • 修正時に影響範囲がわかりにくい
  • どこに何を実装すべきかチーム間で齟齬が出る 等

作成したアーキテクチャ

以下のような実装イメージをチームで統一しております。

  • ソースをいくつかの層に分離し、実装すべき箇所を明確にする
  • 各層の依存関係を限定的にする

f:id:daisuke6666:20200227162137p:plain

  • 各層の役割について
    • Controller・・・Requestから入力値を取得し、Domain.Serviceに処理を移譲、結果をResponseに渡す
    • Request・・・アクセス認証、入力値のvalidation、入力値のデータ型キャストを行う
    • Response・・・APIレスポンスの構築、Viewファイル
    • Domain・・・ビジネスロジックがここに集約される
      • Service・・・Controllerからデータを受け取り、Entity、ValueObjectの生成、RepositoryへEntityの取得、作成、更新、削除を移譲する
      • Entity・・・一意に識別できるIDを持ち、その属性が変化しても同じものだと判定する、もしくは同じ属性でも別のものだと判定する場合があるもの
        例:ユーザー → 名前が変わっても同じ人である、同姓同名の人がいても別の人の場合がある
      • ValueObject・・・その属性やふるまいだけを表現し、それが「どの」人なのか、「どの」ものなのかは気にしないもの
        どれかを識別するIDは持っていない。また、オブジェクト自体は不変であり、setterは絶対にない
    • Infrastructure・・・DB、他サービスAPI、Platformなどの外部アクセスを行う(Platformとはデータ基盤の事です)

f:id:daisuke6666:20200227161905p:plain

例えばDBから取得しているデータをAPIから取得するようにしたい。
となった場合infrastructureを修正すれば良いとすぐにわかります。

導入してわかったこと

  • メリット
    • どこかを修正した時に上位層を修正すれば良いので、影響範囲がわかりやすく、これが最大のメリットに感じています。
    • おかげで開発者間での実装の相談のしやすさや見積もりの精度も上がりました。
    • コードが増えてくるとどこに何をするものがあるのか不明確になる事が多いと思いますが、役割を明確にする事によって解決し変化に強いシステムが作れます。
  • デメリット
    • 開発メンバーの入れ替わり等があると共有認識を作るのが大変かもしれません。ドキュメントやサンプルコードを見る事でチーム間で共有しています。
    • 初期段階の仕組み作りは少し大変かもしれません。(Laravelのディレクトリ構造も変えたような記憶があります)

今後もメリットを増やせるように仕組みをアップデートしていこうと思います。

ちょっとしたデザインの経験談

こんにちは、デザインチームのキョウと申します。
最近はスパイスとカレーにはまっていて、毎日寝る前にインスタで美味しそうなカレーの投稿写真をひたすら眺めることで、日々の癒やしの時間として過ごしています。
そろそろスパイスマスターの資格を取ろうかなとも考えております(・∀・)

さてさて、本題に入ります。

web業界の流行りのビジュアルデザインとUIデザインの変革をずっと見てきましたが、自分がデザインする時によくやっていることや、使っているツールについて、ちょっと話をしようと思います。


- ひたすら参考サイトを見る

デザイナーとして働いてもうすぐ9年になりますが、新人として働き始めた頃は、ひたすら参考サイトを探して、いいと思ったデザインをひたすら真似して作るという経験は、今でも生かしています。
特にデザインのアイディアがどうしても浮かばない時や、複雑なUIはどうやって作っていくのかは悩んでいる時に、とにかく参考書のようにいろいろなサイトを見ていました。

優れたものをひたすら眺めて、考えて、いざ手を動かして作ってみると、徐々に自分のものになっていく感じがとても実感できるようになります。
そしてこの過程をやっているうちに、自分なりのアレンジもできるようになり、最終的にデザイン感性の磨きとスキルアップには繋がったと、私はそう感じています。

最近では、作る前にもう既に頭の中にイメージが湧いたり、こういう場合はこういうUIだー、この改修ならこのサイトのUIを参考にすればいいよー、みたいなことができるようになりました。

私がよく参考にしているサイトはこちらになります。

webサイト系ならここらへん:
https://muuuuu.org/
https://bookma.torch.blue/
https://1guu.jp/

バナー、チラシ系ならここらへん:
https://retrobanner.net/
http://bannermatome.com/

配色に悩んだら:
https://colorhunt.co/
https://colors.muz.li/
https://coolors.co/

そしてみんなご存知のピンタレスト(何でも参考になります):
https://www.pinterest.jp/


- このデザインは難しい、どうしたらいいかは分からない時は

最初の一歩が難しく、なかなか手を付けられないと感じているデザイナーはいると思いますが、そんな時に、とにかくめっちゃラフな状態でもいいので、紙にワイヤーを書いたり、FigmaやSketch、Adobe UXなどのUIが作りやすいツールで簡単なレイアウトを作ったり、コンテンツの整理から始まるといいと思いますね(σ・∀・)σ

もちろん会社にちゃんとしたディレクターがいて、ディレクターがキレイなワイヤフレームを書いてくれるケースも多いと思いますが、うちの会社の場合は、正式なディレクターという役割を持っている人はいないので、大きなコンテンツ改修や、新規ページを作成する際に、デザイナーはディレクターの役割として働かないといけない時は結構あります。
そういう時、やはり作業依頼者に要望を聞きながら、まず最初はコンテンツと情報の整理から、ワイヤフレームを作っていくというケースが多いです。

ワイヤフレームができたら、自分も作業依頼者も頭の中でイメージができるようになり、フィードバックのやりとりもしやすくなりますね。
そして最初からデザインガイドラインが決まっていれば、わりとデザインにすんなり入れるようになります。

もし最初から色もデザインのテイストも決まってないであれば、やはり先程申し上げた通り、参考サイトをとにかく真似して作ってみよう、そして細かいデザイン調整や、難しいパーツは後回しにしよう、と私はよくこのやり方でやっています。
そしたら、デザインのスピードもかなり上げられて、最初のデザインから何回もブラッシュアップして行くうちに、どんどんいいデザインが出来上がっていきます。


- 常識にとらわれない、時には変則も必要

デザインガイドラインは既に決まっていて、サイトで使う色やフォントも決まっている案件は多いと思いますが(特に自社の保守案件の場合)
明らかにここはこの色を使うと変だよー、でもデザインガイドライン上ではこのボタンの場合はこの色を使えと書かれているよー、この部分はデザインガイドラインに載せた色と文字サイズだとすごく見づらいよー、みたいな時は結構あると思います。
そういう時、きっちりデザインガイドラインを沿って作るより、やはりユーザー目線から見て、ユーザー層を考えた上で、見やすい、使いやすいデザインにするのが一番大事だと、私は思いますね。

もちろん、デザインガイドラインや、サイト全体の統一感からすごく離れたテイストや色を使うのは駄目だと思います。
でも、同じ色トーン、似たような構成であれば、ちょっとした変化を出したり、色を鮮やかにしたり暗くしたり、文字サイズもちょっと上げたり下げたりするなら、全然ありだと思いますね(σ・∀・)σ

そして世の中のデザインルールでは、同じサイトでは最大3-4種類のフォントスタイルと色を使わないという傾向があると感じてますが、もちろん統一感とスッキリ度は大事だと思います、自分もごちゃごちゃしたデザインより、スッキリしたデザインの方が好きです。
でも時には、どうしてもユーザーに見てもらいたい、どうしても特別感を出したい、どうしてもスペシャルなコンテンツにしたい時ってありますよね?
そういう場合、きちんとルールを守るより、時にはちょっと外れたデザインをするのもありだと思いますけどねヽ(^o^)丿


以上。
ちょっとした自分なりの経験談でした。
参考になれたら幸いでございます。

そして、スパイスの力でみんなが健康な体に作れたらいいなと、願います(`・ω・´)ノ

AWS CodePipelineを用いたデプロイ

DevOps チームの うすい と申します。よろしくお願いします。 昨年11月にハーフマラソンに出てから膝に矢を受けてしまった状態が続いています。困っています。

今回は最近作ったシステムにおけるデプロイについてお話したいと思います。

前提

弊社の主だった環境は AWS 上にあります。また、ソースコード管理には GitLab を使用していて、GitLab CI も元気に稼働しています。 本システムは小規模で、リリース後のデプロイは多くないことが想定されています。

デプロイフロー

フローとしては下記のようになっています。

  1. master マージ
  2. GitLab CI で成果物を S3 にアップロード
  3. S3 にファイルが作成されたことをトリガーとして CodePipeline がスタート
  4. ステージング環境にて CodeDeploy でデプロイ
  5. 本番環境デプロイ承認のためのメール送信と Slack 通知
  6. 承認権限を持った人が CodePipeline の中で承認
  7. 本番環境にデプロイ

少し細かく見ていきましょう。

GitLab CI

GitLab CI の中でcomposer installnpm installをしています。各ツールのバージョンを簡単に固定したかったため、今回は PHP 用のイメージの中で npm をインストールしたりするのではなく、バージョンを指定したイメージを使用し作成した成果物を次のジョブに引き継ぐ方式をとりました。 .gitlab-ci.yml の概要です。

stages:
  - npm
  - composer
  - upload

npm:
  stage: npm
  image: node:12.14.1-alpine3.11
  script:
    - npm install
    - npm audit fix
    - npm run production
    - tar czf node_modules.tar.gz node_modules
  artifacts:
    paths:
      - node_modules.tar.gz

composer:
  stage: composer
  image: composer:1.9
  script:
    - composer install
    - zip -r ./${CI_PIPELINE_ID}.zip .
  artifacts:
    paths:
      - ./${CI_PIPELINE_ID}.zip

s3upload:
  stage: upload
  image: alpine:latest
  before_script:
    - apk add --no-cache python3
    - pip3 install awscli
  script:
    - aws s3 cp ./${CI_PIPELINE_ID}.zip s3://${S3BUCKET}/${APP}.zip

実際にはbefore_scriptと呼ばれる文字通りscriptの前に実行されるセクションで環境変数ファイルに秘匿情報を埋め込んだりもしています。それらの秘匿情報は GitLab のSecretに保存しています。 このようにして作成された成果物を最終的に S3 にアップロードしています。

また、これまでは nginx や php-fpm の設定ファイルは DevOps チームの Ansible のリポジトリで管理されていることが多く、開発チームで変更したい場合には

依頼&マージリクエスト作成(開発チーム) → マージ&反映(DevOps チーム) → 確認(開発チーム)

となっていましたが、開発チームで使用しているリポジトリに必要なファイルをすべて含め、CodeDeploy の中で反映を行うようにしました(後述)。

CodePipeline

CodePipeline は S3 にファイルが置かれたことをトリガーとしてスタートします。特に込み入ったことはしておらず、シンプルなパイプラインとなっています。

イメージ図

f:id:goode:20200210174116p:plain
CodePipeline

SNS との連携も上記画像の真ん中の上にあるNotifyから簡単に作成することができて、今回はパイプラインの成功スタート失敗、そして承認時に SNS に通知を送って Lambda を起動し Slack の Incoming Webhooks で通知、といったことをしていますが、AWS Chatbot でも検証中です(下記図)。Lambda 内でメッセージをパースする必要もないので、こだわりがなければ AWS Chatbot で良いかと思います。 承認部分に関してはメールでの通知も行っています。

Chatbot

CodeDeploy

今回は AutoScaling と EC2 を組み合わせたシステム構成になっているので、コンピューティングプラットフォームEC2/オンプレミス、またデプロイ設定はリリース後の修正が多くないこと、また利用される時間があらかじめ把握することができることから、時間の節約のためデプロイタイプインプレースにしました。

CodeDeployはイベントという形で順々に処理を実行していきます。

イベント

こちらのAfterInstallの中で設定ファイルのアップデートや nginx の再起動などを行っています。

また、余談ですがインスタンス起動時のユーザーデータに登録したスクリプトで CloudWatch Alarm の作成を行い、ステージング環境など夜間休日にインスタンスを停止する環境において残り続けてしまう CloudWatch Alarm は AWS Batch で削除しています。 AWS Batch のコンテナイメージはこんな感じで作って ECR に push しています。

delete-insufficient-cloudwatch-alarm.sh

#!/usr/bin/env bash

aws cloudwatch describe-alarms --state-value INSUFFICIENT_DATA --region ap-northeast-1 | grep AlarmName | awk '{print $2}' > ./result.txt

sed -i -e 's/"//g' ./result.txt
sed -i -e 's/,$//g' ./result.txt

for name in $(cat ./result.txt)
do
  aws cloudwatch delete-alarms --alarm-names ${name} --region ap-northeast-1
done

Dockerfile

FROM amazonlinux:latest
 
ENV LANG C.UTF-8
RUN yum -y install unzip aws-cli
ADD delete-insufficient-cloudwatch-alarm.sh /usr/local/bin/delete-insufficient-cloudwatch-alarm.sh
 
ENTRYPOINT ["/usr/local/bin/delete-insufficient-cloudwatch-alarm.sh"]

まとめ

今回は EC2 を選択しましたが、別のプロジェクトでは ECS(Fargate)を使用していたり他にも IaC や CI / CD の強化といったこともしています。DevOps チームのメンバーは現在絶賛募集中ですので、興味を持たれた方はご連絡いただければと思います。また、もっといいやり方や改善点を指摘してくれる方もカジュアル面談でお越しいただければと思います。

Composer対応していないパッケージをComposerで定義する

こんにちは。
厳しい寒さが続いておりますが、いかがお過ごしでしょうか。
サービス開発チームのすずきです。

みなさんComposerは使っていますか?(何をいまさら)
弊社の英会話システムはPHPで作られており、パッケージの依存管理にはComposerを利用しています。
新しいパッケージを追加したいときは

$ composer require monolog/monolog

みたいな感じでPackagistから取得することが多いと思うのですが、
今回やりたいこととしてはS3バケットに上がっているパッケージを追加し利用できるようにする ことです。

元々は、弊社オンプレのGitlab上に配置されており、以下の様にcomposer.jsonに記載することで利用しておりました。

{
    "repositories": [
        {
            "type": "vcs",
            "url": "git@bitbucket.org:vendor/my-private-repo.git"
        }
    ],
    "require": {
        "vendor/my-private-repo": "dev-master"
    }
}

上記のサンプルはこちらから拝借🙏

今回は、パッケージをZipしたものがS3にアップロードされている状態で、Composerを利用してパッケージ追加したいと思います。
そのためには自分でpackageを定義する必要があり、私たちの場合はpackageに対して、元々パッケージが定義していたcomposer.jsonの内容を記載することで対応しました。

パッケージが定義していたcomposer.json

{
    "name": "vendor/my-private-repo",
    "description": "this is a private package",
    "type": "library",
    "require": {
        "php": "^7.2.0",
        "ext-json": "*",
    },
    "require-dev": {
        "phpunit/phpunit": "< 7.2",
    },
    "autoload": {
        "psr-4": {
            "Vendor\\Private\\": "src/private"
        }
    }
}

パッケージを利用する側のcomposer.json

{
    "repositories": [
        {
            "type": "package",
            "package": {
                "name": "vendor/my-private-repo",
                "version": "3.1.7",
                "dist": {
                    "url": "https://path-to-aws-s3-bucket/my-private-repo-v.3.1.7.zip",
                    "type": "zip"
                },
                "require": {
                    "php": "^7.2.0",
                    "ext-json": "*",
                },
                "autoload": {
                    "psr-4": {
                        "Vendor\\Private\\": "src/private"
                    }
                }
            }
        }
    ],
    "require": {
        "vendor/my-private-repo": "3.1.*"
    }
}

パッケージが依存している他のパッケージをrequireに記載することでComposerが依存関係を解決してくれます。 また、パッケージを利用する側で不要なrequire-devは削除しています。

参考

getcomposer.org

寒い日が続きますが、来週には春の暖かさがくるようです🤗

Androidのダークテーマ

こんにちは、ネイティブアプリ開発を担当している杉山です。

今回は、Android 10 から利用可能となった「ダークテーマ」のお話です。

ダークテーマとは

Android 10 から利用可能となった黒基調の UI です。

ダークテーマのメリット

  • 電力消費を節約!
  • 視覚障がいのある方、明るい光が苦手な方にとって見やすさ向上!

ダークテーマを使う

1. アプリのテーマに DayNight Theme を継承させる

/res/values/styles.xml にて以下の設定を行う。

<style name="AppTheme" parent="Theme.AppCompat.DayNight">

MaterialComponents のダークテーマも利用可能です。

<style name="AppTheme" parent="Theme.MaterialComponents.DayNight">

2. リソースディレクトリを切り分ける

修飾子として、日中用には notnight を付け、夜間用には night を付けて、リソースを管理。( notnight が付いていない場合、通常の values からリソースを取り出す。)

- res
  - values-night
  - values-notnight
  - values
  - drawable-night
  - drawable

3. アプリ内に適用するモードを設定

アプリケーションクラスを継承させたクラスの中で以下の設定を行う。

AppCompatDelegate.setDefaultNightMode([設定するモード])

モード一覧

  • MODE_NIGHT_YES ... 常に夜間モードを使用。リソースはnight修飾子の付いたものを使用。

  • MODE_NIGHT_NO ... 夜間モードは使用しない。リソースはnotnight修飾子の付いたものを使用。

  • MODE_NIGHT_FOLLOW_SYSTEM ... システムの判断に従って判別

  • MODE_NIGHT_AUTO ... 自動で判別。リソースは日中の場合 notnight修飾子の付いたものを、夜間の場合は night修飾子の付いたものを使用。

モードが切り替わったことをハンドリング

AndroidManifest.xml 内で以下の設定を行う。

<activity
    android:name=".Activity"
    android:configChanges="uiMode" />

onConfigurationChanged を用いて、設定されているテーマを確認する。

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)

    val isDarkTheme = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK
    when (isDarkTheme) {
        Configuration.UI_MODE_NIGHT_NO -> Log.d("Mode : ", "通常モード")
        Configuration.UI_MODE_NIGHT_YES -> Log.d("Mode : ", "ダークテーマ")
    }
}

おまけ

ダークテーマに対応しない

styles.xml で、DayNight Theme を継承しない

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">

ダークテーマをレイアウトから確認

  1. レイアウトファイルを開き、Design タブに切り替える。

  2. 画像のようなアイコンが、左上にあるのでクリック。

  3. Night Mode をNightに設定。( Not Night = 日中、Night = 夜間 )

f:id:r_sugiyama:20200130123302p:plain

まとめ

ダークテーマの対応は意外と簡単にできますが、イメージなどのリソースを作る対応が入ると大変だなと感じました。