diadia

興味があることをやってみる。自分のメモを残しておきます。

djangoのcontextをjavascriptに渡す際に困る場合がある件

大した内容ではないので問題点と対処法を軽く書く。

djangoのcontextに格納したデータをjavascriptにわたす方法

  1. 最初にviewにてデータをcontextに格納する
  2. テンプレートにてjavascriptを走らせ、contextのデータを受け取る
def example(self, request, *args, **kwargs):
    context = {}
    context["data"] = "my_data"
    return render(request, "hoe/hoge.html", context)

テンプレートのscriptタグ内でdataをを取り出す。

<script>
var data_of_django = "{{ data }}";
console.log("{{ data }}")
</script>

こんな感じでscriptタグ内でdjangoのcontextデータを取り出し加工したりして使っていくのが基本。

問題となるパータン

モデルの属性としてTextAreaフィールドを設定している場合がある。 TextAreaは改行もデータとして保存できるが、この改行がjavascriptではエラーの原因になってしまう。

対処法

javascriptに渡すデータをjsonに加工すること、またテンプレートでは"|safe"を使って取り出すこと。この2点で対処できる。

import json
def example(self, request, *args, **kwargs):
    context = {}
    context["data"] = json.dumps("my_data") #ココ
    return render(request, "hoe/hoge.html", context)
<script>
var data_of_django = "{{ data|safe }}";
console.log("{{ data|safe }}")
</script>

leafletを使って以下が表示される。"This content should also be served over HTTPS."

問題は何なのか

leafletを使っていて以下の内容がconsoleに表示される事になった。

Mixed Content: The page at '<URL>' was loaded over HTTPS, but requested an insecure image '<URL>'. 
This content should also be served over HTTPS.

これは何を示しているかというと、表示されたページがHTTPS通信でページが表示されているにも関わらず、安全ではないHTTP通信で通信されている。 この部分についてもHTTPSで配信するべきだ。って旨である。
で、該当するURLは"http://c.tile.osm.org/13/2014/3754.png"であった。
osm.orgからの配信内容である。 自分の場合leafletを使ってopen street mapを配信するときにこれが表示される事がわかった。

これについてhttps化してosmを配信する資料があった。

osmhttps化して配信する資料

Streaming Leaflet Tiles over HTTPS / SSL · Issue #3186 · Leaflet/Leaflet · GitHub

資料の概要

L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
    attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);

上のコードを下のコードに変更する。

L.tileLayer('https://{s}.tile.osm.org/{z}/{x}/{y}.png', {
    attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);

要するに"http://{s}.tile.osm.org/{z}/{x}/{y}.png"の部分を'https://{s}.tile.osm.org/{z}/{x}/{y}.png'に変更するだけといったもの。

自分のコードを直してみる

自分のコードを確認してみると、、、

var map = L.map('map_leaflet', { center: [lat, lon],zoom: 13})
                        .addLayer(L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png'));

確かにhttpから配信する仕組みになっている。これをhttpsに変更してみようと思う。

'https://{s}.tile.osm.org/{z}/{x}/{y}.png'に変更したところうまく行った。

他にも 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png'で成功している人もいるみたいだけれども自分の場合はhttps://{s}.tile.osm.org/{z}/{x}/{y}.png'でうまく行った。

またleafletのcss,javascriptを直接ダウンロードしてnginxから直接配信する方法でもhttps化できるという意見もあった。

supervisorについてメモ

ドキュメント

Supervisor: A Process Control System — Supervisor 4.2.0 documentation

supervisorは何なのか?

これはプロセス管理のツールである。

fabricを使った自動デプロイスクリプトを書いては見たもののgunicornのプロセスを切ってから新たに立ち上げることにたいして思うように制御することができなかった。そこでsupervisorを使う事になった。

supervisorのイメージ

supervisorの構成はドキュメントでは以下の構成とされている。

supervisord
supervisorctl
Web Server
XML-RPC Interface

supervisor-components

まだよく分かっていない状態でこれらの役割を敢えていうとすると、supervisordはクライアントから命令を受けるためのサーバーでありsupervisorctlはそのサーバーに命令を送るためのクライアントである。このクライアントはshellのようなインターフェースをもって命令を送ることになる。Web Serverはwebのコントロールパネルから制御するものなのかと仮設を立てているが使っていないのでよくわからない。XML-RPC Interfaceは全くわからない。
自分が言いたいことはsupervisordとsupervisorctlさえ使えばdjangoのアプリケーションのプロセス管理ができるということだ。

djangoのプロセス制御をどのように行ったか

上で説明したようにsupervisordとsupervisorctlさえ使えばプロセス管理ができる。両者を使うには諸設定ファイルが必要なのはもちろん、linuxでは標準実装されていないのでインストールが必要になる。またsupervisorの立ち上げ、supervisorctlを使ってプロセスをkill、アプリケーションの立ち上げを行う。これらをできるだけ順を追って再現できるように振り返りたい。

0. 前提

  • supervisor 4.2.0
  • centos7

1. インストール

supervisorをインストールする。これはpipを使う方法とepelからyum intastallみたいな形でインストールすることもできるようだ。自分はpipからインストールした。 popのほうがより新しいバージョンのsupervisorを使うことができるからだ。

pip install supervisor

installing

2. 設定ファイルを作成

supervisordとsupervisorctlは設定ファイルによって制御されている。この設定ファイルは同一のファイルから両者を制御することもできるし、supervisorctlのコントロールしたいプロセス毎に作成し、それとsupervisordの設定ファイルということもできる。詳しいことは設定ファイルの編集で説明する。

pipでインストールしただけでは設定ファイルが存在しないので設定ファイルを作成する。
設定ファイルは雛形が存在しており、コマンドを使ってその雛形を呼び出し設定ファイルとする。

#supervisorの設定ファイルを/etc配下に作成する
sudo touch  /etc/supervisord.conf
sudo echo_supervisord_conf > /etc/supervisord.conf

echo_supervisord_confのコマンドはsupervisorをインストールすると使えるようになっており、この中に設定ファイルの雛形が書いてある。この中身を設定ファイルにコピーする流れになる。

また設定ファイル名は上記のようにsupervisord.confが望ましいし、当該ファイルの配置場所も/etc/以下に配置するのが望ましい。 それは基本的に-cオプション(設定ファイルの指定)を付さないでコマンドを入力すると、supervisorはsupervisord.confという設定ファイルを読み込もうとする仕様であるからだ。また、設定ファイルの配置する場所も-cオプションがなければ以下のディレクトリから設定ファイルを探す仕様になっているからだ。

../etc/supervisord.conf (Relative to the executable)
../supervisord.conf (Relative to the executable)
$CWD/supervisord.conf
$CWD/etc/supervisord.conf
/etc/supervisord.conf
/etc/supervisor/supervisord.conf (since Supervisor 3.3.0)

configuration-file

3. 設定ファイルの編集

作成した/etc/supervisord.confに手を加えるとsupervisordとsupervisorctlが使えるようになる。試してはないので確信が持てないがsupervisord.confにsupervisordとsupervisorctlの両方の内容を記述して動かすことができる。
自分の場合はsupervisordについては/etc/supervisord.confに記述し、supervisorctlで管理したいプロセスは別途設定ファイルを作成し、 /etc/supervisord.confから当該設定ファイルの読み込みませる仕組みにした。したがっていまから行うのは/etc/supervisord.confの編集とsupervisorctlで管理するプロセスに関わる設定ファイルの作成である。

ⅰ. /etc/supervisord.confの編集

supervisordに関する設定内容。ここは設定すると言ってるが全く触らなくてよかった。雛形のママである。
このファイルだとlogfileやpidfileが/tmp以下に作成されるので何らかのエラーが生じた場合は/tmp以下のlogを閲覧することになる。ココは任意に変更すると良いだろう。

[supervisord]
logfile=/tmp/supervisord.log ; main log file; default $CWD/supervisord.log
logfile_maxbytes=50MB        ; max main logfile bytes b4 rotation; default 50MB
logfile_backups=10           ; # of main logfile backups; 0 means none, default 10
loglevel=info                ; log level; default info; others: debug,warn,trace
pidfile=/tmp/supervisord.pid ; supervisord pidfile; default supervisord.pid
nodaemon=false               ; start in foreground if true; default false
silent=false                 ; no logs to stdout if true; default false
minfds=1024                  ; min. avail startup file descriptors; default 1024
minprocs=200                 ; min. avail process descriptors;default 200
;umask=022                   ; process file creation umask; default 022
;user=supervisord            ; setuid to this UNIX account at startup; recommended if root
;identifier=supervisor       ; supervisord identifier, default is 'supervisor'
;directory=/tmp              ; default is not to cd during start
;nocleanup=true              ; don't clean up tempfiles at start; default false
;childlogdir=/tmp            ; 'AUTO' child log dir, default $TEMP
;environment=KEY="value"     ; key value pairs to add to environment
;strip_ansi=false            ; strip ansi escape codes in logs; def. false

; The rpcinterface:supervisor section must remain in the config file for
; RPC (supervisorctl/web interface) to work.  Additional interfaces may be
; added by defining them in separate [rpcinterface:x] sections.

同ファイルの最下部に[include]がある。これを編集(追記)する。

[include]
files = /etc/supervisord.d/*.conf ;(追記) 起動するプロセスのconfファイルの配置場所
;files = relative/directory/*.ini

include-section-settings

ⅱ. プロセスのconfファイルを作成

プロセスのconfファイルを配置するディレクトリを作成。

sudo mkdir /etc/supervisord.d

プロセスのconfファイルを作成

sudo touch /etc/supervisord.d/django_project.conf

プロセスのconfファイルの編集

sudo vi /etc/supervisord.d/django_project.conf

で以下のように記述する。

[program:django_project] ;この値を引数としてsupervisorctlを使う
directory=/home/user_name/src
command=gunicorn config.wsgi --bind localhost:8000 --env DJANGO_SETTINGS_MODULE=config.prod_settings --daemon config.wsgi:application
numprocs=1
autostart=true
autorestart=true
user=user_name ;デプロイするユーザー名
redirect_stderr=true

4. supervisordの起動とプロセスの実行

supervisorの起動する
supervisord
設定ファイルの読み込み
supervisorctl reread
プロセスのリスタート
supervisorctl restart django_project

一発でうまく起動することが自分はできなかった。自分の場合はすでに8000番ポートを使ってアプリケーションを起動していた。うまくイカない場合はすでに起動しているdjangoのプロジェクトのプロセスをkillしてから実行してみるとよい。

sudo lsof -i:8000

pidの番号をkillする

kill -9 番号

Djangoのformをより良く見せるために(改訂版)

Djangoにおいてformウィジェットの理解を深めない限り、forms.ModelFormやforms.Formの利用したとて、フォームを使った画面が残念な結果になってしまう。

見た目の良いフォーム画面を作るには

これらの方法が考えられるがメリット、デメリットが存在する。 これらのメリット・デメリットを考えてみたい。

そもそもウィジェットとは?

現在自分の中ではウィジェットとはformだったりボタン等のユーザーインターフェースのことを指すと考えている。

htmlファイルを自ら作成して好みのformを作成する方法

これはテンプレートにformタグと共にインプットタグ等を書いていく方法で、class属性を思うままに書くことができるので自由に作ることができる。
しかしながらdjangoに備わっているインプットデータのバリデーション機能を享受できなくなるデメリットがある。自分でバリデーション機能も実装するのは手間なので避けるべき方法だと思う。

ModelFormやFormのウィジェットをforms.pyにて編集する方法

関連ドキュメント

https://docs.djangoproject.com/ja/2.2/topics/forms/
https://docs.djangoproject.com/ja/2.2/ref/forms/widgets/

例えばBootstrap4のform-controlを反映させたい場合以下のようになる。

class CommentModelForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ("name","url","comment")
        
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            self.field["name"].widget.attrs["class"] = "form-control"
            self.field["url"].widget.attrs["class"] = "form-control"
            self.field["comment"].widget.attrs["class"] = "form-control"
    

その他ウィジェットを編集する方法は以下のようになる。なお、参考資料は以下である。 https://docs.djangoproject.com/ja/2.2/ref/forms/api/#accessing-the-fields-from-the-form

def __init__(self, *args, **kwargs):
   super().__init__(*args, **kwargs)
   for field in self.fields.values():
        self.field["name"].required = True  #必須項目のフォームに変更する
        self.field["url"].label = "URL"  #ユーザ視点でフォームの入力名が変わる。
        self.field["comment"].widget.attrs["placeholder"] = "コメントを書いてください。"

forms.pyのinitwidgetを編集する方法である。これを使うとformのwidgetの属性値を管理するのが楽になる。forms.pyを直接いじれない場面に遭遇するといじるために手間がかかる。またファイルは違えどviews.pyで編集する方法もこの方法に含まれる。

Javascriptを使ってformを改善させる方法

上記の方法はforms.pyにおいてformのインスタンス初期化時に属性をセットする方法である。 サードパーティのライブラリを使う場合に問題が顕在化する。例えばdjango-allauthではログイン等の画面も準備してくれるが、そのフォームを編集する際にforms.pyをいじることが多少難しくなることだ。実際いじることはできるが少し複雑になってしまうので構成を共有して理解するには難しいと思われる。
こちらの記事の一部にformの編集の仕方を書いている。
django-allauth : テンプレートのカスタマイズ - diadia

このような場合にはjavascriptを使ってDOMを操作することが良い思う。 共通のformを利用する場合にはincludeを挿入すればよいだろうし、コンポーネント化できるので積極的に使って良い方法だと思われる。問題点はjavascriptの知識が要求されることである。
Vue.jsとか他のフレームワークを使っている場合DOM操作がバッティングする恐れがあり、それらがどのように反応するかは検証する必要がある。自分の場合それがどのように反応するかは分かっていないのでそこをはっきりさせるのにコストがかかる。

window.addEventListener('load', function() {
    var email_form = document.getElementById("id_email")
    email_form.setAttribute('class', 'form-control');
})

サードパーティライブラリを使用する方法

django-widget-tweaksというライブラリが存在する。
https://python.keicode.com/django/form-add-class.php これを使えばテンプレートタグに追加したい属性値を直接記述できるようになる。サードパーティライブラリなのでメンテナンスされるかが外部に依存する事になってしまう。

{% load widget_tweaks %}
{{form.username|add_class:"form-control"}}

結論

個人的にはforms.pyのインスタンスの初期化で各ウィジェットを調整し、それが難しい場合にはjavascriptを使って調整する方法を取るのが望ましいと思う。

django ページネーションについて(改訂版)

ページネーションについて

ページネーションは一覧表示させるオブジェクトを1ページ内にどれだけ分割して表示させるか。それに関わる技術のことのようだ。

関連ドキュメント

ページネーション | Django ドキュメント | Django

メモ

djangoにおけるページネーションに関わるオブジェクトは2つある。

  1. Paginator オブジェクト
  2. Page オブジェクト

このオブジェクトの役割は具体的にはなんなのか?
Paginator オブジェクトはどんなページネーションにするかを定めるために使うのと、そのオブジェクトを使ってページを表示することができる(もちろん他にも)。
Page オブジェクトはあるページに該当するデータを参照したり、その隣(前のページや後のページ)が存在するか、その隣のページを参照するような役割をもつ。

1ページ当たりいくつのオブジェクトを描画するかを定めるのがPaginatorの役割で、それはsettings.pyでページネーションを定めるのではなくviewsの各ロジックに組み込んで使用する。 またPageオブジェクトはフロントエンド側で使いやすいと思われる。具体的には隣のページがあればページを繰るアイコンを表示させるとかクリックすることで該当ページの内容を表示させるとか。 もちろんPageオブジェクトをテンプレートで使いたいのでviewsでPageオブジェクトを生成しcontextに格納することを想定される。両オブジェクトはともにviewsで使用されることか2つのオブジェクトを組み合わせた関数を作ってしまうと便利かも。

以前の記事

ページネーションの必要素は2要素のようだ。表示するべきリストオブジェクトの作成とのページを繰る関係のオブジェクトである。で前者はPageクラスインスタンスにobject_list属性で表現できるようだ。

ページネーション | Django ドキュメント | Django

後者については同じくPageクラスインスタンスに属性やメソッド使ったりすると表現できるようだ 。

ページネーション | Django ドキュメント | Django

page_obj.object_listは普段object_listをhtml上に表現するように扱えばいいので特に問題はないだろう。page_obj.numberについて考える。まずpage_obj.numberはどこに使うか?という問題である。言うまでもなくviews.pyのrequest.GETに関するコードの流れの最終着地点はcontextに追加し、render関数を介してhtml上に表示させることだ。だからpage_obj.numberはhtmlに表現される。htmlの下部の"前へ","1","2","3","次へ"というものだ。page_obj.numberはここに使われる。またpage_objには以下のメソッドがある。これらを使うと前へ、とか"3"を表現できるようになりそうだ。

Page オブジェクトメソッド 動作
page_obj.has_next() 次のページが存在する時Trueを返す。
page_obj.has_previous() 前のページが存在する時Trueを返す。
page_obj.has__other_pages() 次のページまたは前のページが存在する時Trueを返す。
page_obj.has_next_number() 次のページ数を返す。次のページが存在しない場合はInvalidPage例外を起こす。
page_obj.start_index() ページ上の最初のオブジェクトに対する、1から始まるインデックスを返します。これは、ページネータのリストに含まれる全オブジェクトに対するインデックスです。たとえば、5個のオブジェクトのリストを各ページ2オブジェクトでページネーションしている場合、2ページ目の start_index() は 3 を返すでしょう。
page_obj.end_index() ページ上の最後のオブジェクトに対する、1から始まるインデックスを返します。これは、ページネータのリストに含まれる全オブジェクトに対するインデックスです。たとえば、5個のオブジェクトのリストを各ページ2オブジェクトでページネーションしている場合、2ページ目の end_index() は 4 を返すでしょう。

だからhtmlではpage_obj.object_listの使い方としては、

{% for obj in page_obj.object_list %}

{{ obj }}

{% endfor %}

ページを繰る関係で言えばhtmlでは以下のように表現できる。

{%for n in  page_obj.paginator.page_range %}

<a href="?page={{ n }}">{{ n }}</a>

{% endfor %}

page_objの作り方

page_objはPageクラスのインスタンスである。しかしながらpage_obj=Page()と作るわけではないようだ。作り方はpage_obj = pagenator_obj.page(number)で作る。pagenator_obj=Pagenator()で作成する。

cdnでvuetifyを使うために

軽く要点を残す。

djangoでincludeを使ってコンポーネント化できたのでテンプレートの見通しが良くなった。
cdnを使ってコンポーネント化するとvuecliを入れる前にどんな感じでvuetifyや単一ファイルコンポーネントが使えるか確認できる。(もちろん全く同じではない。)

完成形イメージ

<body>
<my_template/>

{% include "my_template_vuejs.html" %}
</body>

"my_template_vuejs.html"でmy_templateタグの定義を行うとともに、vueの初期化でmy_templateタグを使えるように書く。そしてmy_templateタグを表示させたいところに配置する。

必要なこと

1 vue.jsが起動するテンプレートタグを設定する。

<body>
<my_template/> #これ
</body>

2 "my_template"タグの定義を書く

cdnでテンプレートを定義するときに注意しなければならないことがある。それは必ずscriptタグから書き始めること、templateタグを内部で書かないことである。特にtemplateタグを書いてしまうとエラーとなってしまうので注意が必要。 またこのような書き方はインラインテンプレート記法と呼ぶようだ。Vue.js初心者向け:インラインテンプレートから単一ファイルコンポーネントの使い方まで - Qiita

touch hoge/my_template_vuejs.html
<script type="text/x-template" id="m_temp">

<p>自分の書きたいコンポーネントを書く。
ここにvuetifyのテンプレートを直接書き始めても正常に読み込まれる。</p>

</script>

3 同一ファイル内でvueの初期化を実施する

<script>

Vue.component("my_template", #呼び出したいテンプレート名
{ 
    template: "#m_temp", #上で定めたコンポーネントのid

});

new Vue({
    el: "m_temp",
    vuetify: new Vuetify() #Vuetifyを使う場合これを追加する
})
</script>

あとはインクルードで呼び出せば使える。

cdnコンポーネント化する資料はココにもあった。こっちのが書き方が明快かも CDNで始めるVue.jsコンポーネント③ - Qiita

nginxで403が返されるときに対応したこと

サーバー上に新しくディレクトリを作成し、そこを起点にgithubからpullを行いデプロイした際にうまく作動しなかった。

環境

centos7
nginx
porstgresql
django

症状:

  1. home画面のhtmlはサーバーから配信される。
  2. home画面の静的ファイルの配信がうまく行っていない。
  3. 配信がうまく行かなかった静的ファイルに直接アクセスする(新しいタブで画像を開く)場合403が返される。

判断:

home画面のhtmlはサーバーから配信される。 -->
webサーバーとWebApplicationSerer(django)は正常に作動していると可能性が高い。

home画面の静的ファイルの配信がうまく行っていない。 -->
webサーバー(nginx)から静的ファイルを送信する際に問題がある。

(新しいタブで画像を開く)場合403が返される。-->
nginxが静的ファイルにアクセスするが権限に関して問題がある可能性を示唆している。

対応したこと

  1. "nginx 403"で検索をかけ、以下の記事を探し当てた。 Nginx で 403 Forbidden エラーが出るときのチェック項目|まくろぐ
  2. ドキュメントルートまでに実行権限があるかチェックした。
  3. うまく配信されなかった静的ファイルの読み取り権限があるかチェックした。

なお、権限チェックに関してはlsコマンドを使った。

ls -l (対象ディレクトリ)

ドキュメントルート(静的ファイルを配信するために静的ファイルを集めたディレクトリ)までの実行権限を調べたところ見事に実行権限がなかった。具体的にはドキュメントリート自体には実行権限xが存在していたが、親のディレクトリとその親ディレクトリにはxの権限が欠如していた。 rootからドキュメントルートまで下記のコマンドを実行した。

chmod +x (対象のディレクトリ)

問題のあった静的ファイルの読み取り権限rはあったのでこれに関して特に行うことはなかった。

そして再度nginxの起動を行った。

systemctl restart nginx

結果として正常に静的ファイルを表示させることができた。

この件から得られた知見

nginxがファイルを配信する前提としてドキュメントルートまでの実行権限が必要なこと、配信する静的ファイルに読み取り権限が必要なことを学んだ。またnginxで403が出る場合とその他のエラーが出る場合を経験したけど、そもそもhome画面が表示されない場面に出くわした。このときは静的ファイルがドキュメントルートに存在していない場合があった。

だからnginxの配信するファイルの存在があるかないか ?
無い --> 画面自体が表示されない。
ある --> 権限に問題がある。

今の所こんな感じの原因を推測できる

AWS学習メモ

知識の整理の方法をよく考えたい。

実現したい実装に対して、概念による実装方法があり、具体的な実装方法がある。

具体的な実装方法はコンソールによる具体的な実装方法とteraformによる実装方法である。これらの棲み分けを意識して分けてメモをしていきたい。

VPC: Virtual Private Cloud

ネットワークの基盤を作るサービス

awsマネジメントコンソールの場合

VPCを作成するときにはVPCサービスから選択してVPCを作成する。

Terraformの場合

resource"aws_vpc"
"example"{
  cidr_block="10.0.0.0/16"
  enable_dns_support=true
  enable_dns_hostnames=true
  
  tags={
    Name="example"
  }
}

VPC内にサブネットを作成するけれども、これはどのようにするか?サブネットはVPCサービスの項目の一つであるので、VPCダッシュボードから作成することになる。サブネット作成時の留意点は、どのVPCと紐付けるかを確認することと、サブネットのCIDRの値を定めることである。

ルーティングの設定 1. 大きく分けて2つある。インターネットゲートウェイを作成し、それをVPCにアタッチすること。 1. ルートテーブルを作成し、パブリックサブネットに紐付ける。

インターネットゲートウェイもルートテーブルもVPCダッシュボードの項目の一つとして存在している。各項目を選んで各々作成する運びとなる。

EBS

EC2のストレージ扱いっぽい。EC2を停止していてもこの部分に関しては課金され続ける。

セキュリティグループ

セキュリティグループってのはファイアウォールに当たる言葉のようだ。

EC2接続方法

macの場合はssh接続を行う。ダウンロードしたキーを使ってssh接続を行う。 注意点はログインユーザー名をec2-userとしてログインを実行する。

chmod 600 ~/Desktop/aws-keypair.pem

ssh -i ~/Desktop/aws-keypair.pem ec2-user@54.245.191.191 -p 22

アパッチのインストール、セッティング

インストール
sudo yum update -y

sudo yum install httpd -y
起動
sudo systemctl start httpd
sudo systemctl enable httpd
sudo systemctl status httpd

RDSについて

これもAWSのサービスの一つ。データベース関連のものでpostgresql, mysql, mariadb, oracle, sql server等を使うことができる。 DBをセッティングする方法としてオンプレミス、onEC2 、RDSの3通りが考えられる。onEC2とは、さくらVPCのサーバー上にDBをインストールさせるのと同じようにEC2インスタンス上にDBを設けることだ。

RDSを始める

まずRDSは可用性を高めるためにサブネットを2つ準備しなくてはならない。したがって、VPCのメニューからサブネットを2つ作成する。 次にRDSに接続するためのセキュリティグループをRDS作成時に求められる。これも作成しなければならない。 あとwebサーバーとDBサーバーを分ける場合にはRDSに接続するためにwebサーバー側にDBのクライアントをインストールする必要がある。

またRDSもEC2と同様従量課金制なので使用しないときはRDSを停止することが望ましい。
[速報] RDSインスタンスの起動/停止 | Developers.IO

MYSQL

インストール

sudo yum -y install mysql

接続

mysql -h (endpoint: RDSに書いてある) -u (username: RDS作成時に定めた値) -p

このあとパスワードを入力して認証されれば、RDSに接続できたことになる。 またssh接続する事はできない。

ユーザー作成

CREATE USER '(ユーザー名)'@'%' IDENTIFIED BY 'password';

作成したユーザーにDBへの権限を付与する

GRANT ALL ON (DB名).*  TO '(ユーザー名)'@'%';

与えた権限を反映させる

FLUSH PRIVILEGES;
SELECT user, host FROM mysql.user;

terraformのリソースタイプ(確認している限り)

  1. resource"aws_instance"
  2. resource"aws_security_group"
  3. resource"aws_vpc"
  4. resource"aws_subnet"
  5. resource"aws_internet_gateway"
  6. resource"aws_route_table"
  7. resource"aws_route"
  8. resource"aws_route_table_association"
  9. resource"aws_eip"

fabricでデプロイするために得られた知見

まずfabricでできることはなにか?

fabricでできることはshellスクリプトでもできる。シェルスクリプトが書ければfabricをあえて学習する必要ないと思われるが、fabricを使うのはpythonを使うので学習コストが低いし、pythonで書くのでコードの見通しが良くなりメンテナンス性が向上する。
そもそも現状の自分ではシェルスクリプトを満足に書くことができない。

fabricを使ってdocker-composeコマンドでデプロイするか、デプロイに必要な各コマンドを叩くかの選択肢がある。

自分の場合すでに稼働させているサーバーに対して各コマンドを叩いてデプロイを実施する。

fabricバージョン2と1がある。使い方がぜんぜん違うようだ。 旧使い方と新使い方でどのような違いがあるかチェックしたい。

fab オプションと引数 — Fabric ドキュメント

基本的な使い方

fabricのスクリプトをfabfile.pyとして記述する。fabfileというディレクトリを作成するパターンも見られル。
fabricをインストールするとfabコマンドを使うことができる。

fab fabfileの関数

これを使ってデプロイを実施していく。

fabfileの中で行うことは以下である。

  1. githubまたはbitbucketからコードをpullする。
  2. pullしたコードの中にあるrequirements.txtを読み込み、依存関係のあるパッケージをインストールする
  3. python manage.py makemigrationsを行う
  4. python manage.py migrateを行う
  5. python manage.py collectstaticを行う
  6. サービスを新たにする。

今までコードのpull, cloneはhttps通信を使っていた。この問題点はリポジトリにアクセスするとパスワードを入力される。この要求をパスしないとデプロイができないのでパスワードを聞かれないような方法でpullを実行するように変更しなければならない。 これについてはgithubやbitbucketにssh接続するとパスワードを要求されなくなることがわかった。

ではssh接続するためには何が必要か?

  1. pullを行うサーバー側においてリモートリポジトリの登録をsshバージョンで登録する
  2. サーバーにおいてssh接続を行うための秘密の鍵、公開鍵を生成する
  3. 上で生成した公開鍵をgithub, bitbucketに登録する

これをするとパスワードを要求されずにpullを実行する事ができる様になる。

軽くメモをすると、 git remote -vで表示されるリモートリポジトリの表示をsshバージョンに変更するには以下のコマンドを実行すれば良い。

git remote set-url origin git@github.com:[ユーザー名]/[リポジトリ名].git

githubで公開鍵を登録する際にはリポジトリごとに登録すると考えていたが、アカウント毎に登録する仕様になっていることが分かった。だからいくらリポジトリからsshキーの登録を探そうとしても見つからない。。。 以下にアクセスすること。
https://github.com/settings/keys

参考:
git パスワード を毎回聞かれる問題の解決方法 - Qiita
GitHubでssh接続する手順~公開鍵・秘密鍵の生成から~ - Qiita

ディレクトリやファイルが存在するかどうかをチェックする場合について

fabricのrun関数の引数としてshellスクリプトの断片を書きまくれば良いと考えていたが、そうではなかった。fabricを使ってディレクトリの存在を確認するには以下の関数をインポートして使うようだ。そしてこれはshellスクリプトを書かないで済むからメンテナンスしやすくなってありがたい。。。

from fabric.contrib.files import exists

    def is_exist_dir(self):
        with cd("/"):
            if exists('/home/user/tet_dir'):
                print("存在する")
            else:
                print("存在しない")
fabricディレクトリの存在のチェックするには?の資料

python - Check If Path Exists Using Fabric - Stack Overflow

あと今思うのはcircleciを使わなくてもfabricだけで完結したほうがスッキリするし、circleciを利用する意義について理由を探さなければならないと思うくらい便利。

例えばgit pushを行うのはすべてfabric経由で実行する。 fabric内でテストを実行する。テストで失敗すればそのままデプロイがされないし、テスト成功したらデプロイ実行させる。
これで十分なんじゃないか...?

fabricでgunicorn のdaemonオプションが使えない時

--daemonをつけないと適切にgunicornが実行されるのに--daemonをつけたときだけうまく作動しない症状に出くわした。 これについては以下が参考になった。 Fabricでgunicornのデーモン化ができなかったアレ - Qiita

結論から言うとpty=Falseを付け加える。

run("/usr/local/bin/gunicorn --daemon --env DJANGO_SETTINGS_MODULE=config.settings.prod_settings config.wsgi:application -c {}".format(GUNICORN_CONF), pty=False)

circleciのsshキーを登録する

circleciのコンソールにsshキー(秘密の鍵)を登録する事ができる。

しかしこの秘密の鍵はpem形式でなければコンソールでエラーが出てしまう。したがってpem形式のキーを別途作成し登録する必要があった。

手順; 1. pem形式の鍵を作成する 1. サーバーに公開鍵を登録する 1. 秘密鍵をcircleciに登録する

ssh-keygen -f hoge -m PEM

参考資料
SSHの鍵 - ふなWiki

CircleCIのメモ

CircleCI環境変数について

circleciの環境変数の定め方は2通りある。 一つはconfig.ymlで環境変数をハードコーディングで定める方法である。
もう一つはcircleciのコンソールで環境変数をキーとバリュー形式で登録しておき、config.ymlでキーを呼び出すことで環境変数を読み込む方法である。

config.ymlでハードコーディングする方法は例として以下のようになる。

version: 2
jobs:
  build-job:
    docker:
      - image: circleci/python:3.7.3
      - image: circleci/postgres:10.5-postgis
    environment:
      DATABASE_URL: postgres://postgres:@localhost/circle_test
      SECRET_KEY: aaq1sjddijfv*d5j%$ssyji+@!2bqur_hynpls_fwozwbs9rkj
      DJANGO_READ_DOT_ENV_FILE: True
      DJANGO_SETTINGS_MODULE: config.settings.dev_settings
    working_directory: ~/app

またシェルコマンドで環境変数を設定することもできるが、単純に

export PATH=/path/to/foo/bin:$PATH

を使えばよいというわけではない。この方法では環境変数を設定する事ができないからだ。

echo 'export PATH=/path/to/foo/bin:$PATH' >> $BASH_ENV

BASH_ENVを使う必要があることに留意すること。

シェルコマンドによる環境変数の設定の資料

シェルコマンドで環境変数を設定する

今の所config.ymlから.envファイルを読み込む方法が使えるのか確認できていない。 ※ docker-compose.ymlではenvironmentで環境変数を定めることができるに加えて、env_fileで.envファイルを読み込むことができた。分かったら後に書き加える。

キャッシュについて

依存関係のキャッシュ
キャッシュを使うとビルドにかかる時間を短縮できる -> circleciで利用できる無料利用分を有効に使うことができる。

どんな仕組みでキャッシュを更新するのか?? どう使うのか?? config.ymlで記述するのはstepsでrestore_cacheやsave_cacheを使う。
取得済みのデータが有ればrestore_cacheを宣言してキャッシュ済みのキーをリスト形式で記述すればキャッシュの読み込みができる。

    steps:
      - checkout
      - restore_cache:
          key: pip-{{ checksum "requirements.txt" }}

checksumはキャッシュが更新するかを確認するためのもの。checksumの値が変更されていれば後述するsave_cacheでキャッシュを更新する。

      - save_cache:
          key: pip-{{ checksum "app/requirements.txt" }}
          paths:
            - 'venv'

workflowsについて

ジョブの実行を Workflow で制御する - CircleCI

よく使うコマンド

circleci local execute --job ジョブ名

circleciコンソールの見方

circleciの実行を止める種類は2つある。
workflowsそのものをとめる方法と実行中のjobを止める方法が存在する。 workflowsを止める方法はプロジェクトを選んだ状態の画面から"Return"というドロップダウンメニューから"Cancel workflow"を押すことで止める事ができる。
一方jobを止める場合はworkflowsからjobをクリックした画面(jobの詳細実行画面)にある"Return"というドロップダウンメニューに"Cancel job"がある。これを押すことでjobを止めることができる。 ssh接続ができる状態にしてからどうやってssh接続できる状態を止めるようにするかはまだあやふや。これをきっちり止めないと新規に新たな実行ができないままになってしまう。無料枠だから??

CircleCIで実行サーバーにssh接続する際に困ったこと

ビルドしてテストして問題がなかったらデプロイに移るわけだけれども、ssh接続できずにずっと苦しい思いをした。 ココについて解決できたので改めて整理してみたいと思う。

必要なこと。

githubからソースコードを取得するのは2つの場面がある。一つはgithubにpushすることでcircleciサーバーが起動し取得する場面である。2つ目はテストが完了し実行サーバーでデプロイする際にコードを取得する場面である。 circleciサーバーがコードをpullする際はsshkeyが必要になるが、特にcircleci側にsshkeyを登録する必要はない。
サーバー側でデプロイするときにはhttps通信でpullを実行する場合とssh通信でpullを実行する場合がある。httpsでpullを実行する場合には認証するためにpasswordをいんてラクティブな形式で入力することになる。これをcircleciでデプロイする場合にはpasswordを入力する事ができず、デプロイ作業中に止まってしまう。passwordを聞かれずにpullさえできればデプロイ作業が継続される。ssh接続でpullを実行するとpasswordを聞かれることがない。 したがって実行サーバーがpullを行うときはssh通信を行うことが必須となる事がわかった。これを実現するには以下2件の作業が必要になる。

  1. sshのkeyをgithubに登録する。
  2. 実行サーバーのgit remoteではhttpsではなくssh形式でリモートリポジトリを登録する。

実行サーバーがpullしてきたものに対しgithubがpullを許容するために、別の言い方をすれば実行サーバーからgithubssh通信を実行するためには、通信を開始する側は秘密の鍵が必要になるし、通信を受ける側であるgithubでは秘密の鍵が必要になる。 したがって実行サーバーで秘密の鍵、公開鍵を作成し、公開鍵の情報はgithubに登録する作業が必要になる。

またgit remote -v で表示される形式がhttps形式であれば実行サーバ-に秘密の鍵があろうとまたgithubに公開鍵があろうとhttps通信が行えわれ結果としてpasswordが聞かれてしまう。そこでssh通信を行う宣言をするためにもssh形式にあったリモートの登録が必要になる。 この辺のことはいかにうまくまとめられている。
GitHubでssh接続する手順~公開鍵・秘密鍵の生成から~ - Qiita

これでcircleciからgithubの接続と実行サーバーからgithubの接続ができるようになった。 circleciでpullを行いビルド、テストを実行する。 circleciサーバーから実行サーバーにssh接続を行い、実行サーバーがgithubからpullを実施する。ここでまだcircleciサーバーから実行サーバーに接続する設定が終わっていない。

circleciサーバーから実行サーバーに接続するにはやはりsshの鍵が必要でそれに関連した作業がある。

  1. pem形式のsshkeyを作成し、circleciサーバー側に秘密の鍵を登録する
  2. ssh接続の際にオプションを付け加える。

sshkey-genコマンドを実行する際にpem形式の鍵を作成しないとcircleciで秘密の鍵を登録する事ができない。
したがって-m pemオプションをつけて実行する。

sshkey-gen -m pem

ssh接続の時新たなサーバーに接続する場合にはsshクライアント側で以下のような質問が標準出力される。

ECDSA key fingerprint is SHA256:IcFXfHgh42HvcZiG/zQmle+IjD4lrUu4XV+469JheJE.
Are you sure you want to continue connecting (yes/no)? 

この出力に対し、yesを入力しなければssh接続が開始されない。これはそもそも接続先として誤ったhostに接続した場合に誤ったホストはsshクライアントが入力する内容を盗聴する事ができる。このような場合にクライアントがsudo コマンドでpasswordを入力したものなら悪用されることに怯えなければならない。したがって以前通信したことがないホストと通信する際に上記の標準出力が表示されるのはあなたが接続しようとしているホストは間違っている可能性もあるので注意してねって感じの注意喚起の役割を果たしていることが分かった。

この標準出力を出さない方法として

ssh -p 22 -o 'StrictHostKeyChecking no' username@host

がある。これを実行すればssh接続できる。

circleci+fabricの資料

これがとても良くわかりやすくベースはこれを使って実装した Django and CircleCI 2.0 | Timmy O'Mahony

vuexのコンポーネント間のデータ受け渡し方法

vuexのデータの受け渡しはvuexのドキュメントを読めばだいたいのことがわかる。

vuexはVue.js アプリケーションのための 状態管理パターン + ライブラリである。

重要なファイル 最初はApp.vueとstore/index.jsだけで済む。

<template>
  <v-app>
    <v-app-bar
      app
      color="primary"
      dark
    >
      <div class="d-flex align-center">
        <v-img
          alt="Vuetify Logo"
          class="shrink mr-2"
          contain
          src="https://cdn.vuetifyjs.com/images/logos/vuetify-logo-dark.png"
          transition="scale-transition"
          width="40"
        />

        <v-img
          alt="Vuetify Name"
          class="shrink mt-1 hidden-sm-and-down"
          contain
          min-width="100"
          src="https://cdn.vuetifyjs.com/images/logos/vuetify-name-dark.png"
          width="100"
        />
      </div>

      <v-spacer></v-spacer>

      <v-btn
        href="https://github.com/vuetifyjs/vuetify/releases/latest"
        target="_blank"
        text
      >
        <span class="mr-2">Latest Release</span>
        <v-icon>mdi-open-in-new</v-icon>
      </v-btn>
    </v-app-bar>

    <v-content>
      <HelloWorld/>
    </v-content>
    <form>
    <input type="text" v-on:input="updateMessage" ><br />
    <button>submit</button>
  </form>
  <span>{{ obj }}</span>
  </v-app>
  
</template>

<script>
import { mapState } from 'vuex' 
import HelloWorld from './components/HelloWorld';

export default {
  name: 'App',

  components: {
    HelloWorld,
  },

  data: () => ({
    //
  }),
  methods:{
    updateMessage: function(e){
      console.log("update")
      this.$store.dispatch('updateTest', {message: e.target.value})
    }
  },
  computed:{
    ...mapState({
    obj: state => state.obj
  })
  }
};
</script>

どのようにstateにデータを保存するか。これはmethodsにupdateMessageメソッドが存在する。

 this.$store.dispatch('updateTest', {message: e.target.value})

this.$store.dispatch()はactionsを呼び出す。第一引数の文字列はstore/index.jsのうちactionsに含まれているもののうち何のプロパティを呼び出すかを示している。
また第2引数は{message: e.target.value}みたいな感じで渡さないとstore/index.jsでデータをきちんと受け取ってくれない現象に陥る。(具体的にはundefinedになる。) そしてこのメソッドはtemplateタグ内のinputタグでv-on:inputメソッドの引数として使われている。 入力される-> this.$store.dispatch()でupdateTestが実行される。

また基本的にstateのデータはcomputedを通じてstateのデータを取得するようだ。

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    obj: "hhh"
  },
  mutations: {
    updateMessage (state, message) {
      state.obj = message
    }
  },
  actions: {
    updateTest: function({commit}, {message}){
      commit("updateMessage", message)
    }
  },
  modules: {
  }
})

このファイルはvuexのデータフローの仕組みをまとめるものである。actionsで何かが呼ばれればcommitによってmutationsが呼ばれる。mutationsによってstateの内容を変更する。こんな感じの流れになっている。

GitHub - chiaki1990/vuex_sample_first

dockerのvolumeがホストのどこにあるのか

Dockerのvolumeがホストのどこに存在するか知りたい。

docker-compose.yml内で以下のように記述することに関していままで自分が勘違いしていた。

  db:
    image: postgres

    volumes:
        - postgres_data:/var/lib/postgresql/data <-コレ

    environment:
      - POSTGRES_DB=docker_test_db
      - POSTGRES_USER=docker_test_user
      - POSTGRES_PASSWORD=docker_pass
      - DATABASE=postgres

    ports:
      - 5432:5432

volumes:
  postgres_data:

postgres_dataはいかなるホスト(OS)でも存在し、そしてdocker-compose.ymlではパスで表示している。ひょっとするとdocker-compose.ymlのカレントディレクトリに隠しファイルとして作成されるのか、とも思っていた。

こんなふうに考えていたが、少し確かめたことがありその結果は以下になる。まずpostgres_dataはすべてのホストで存在している事実はない。volumesで宣言することで新たにpostgres_dataというディレクトリもしくはファイルが作成されることはなかった。次にdocker-compose.ymlでpostgres_dataはパスとして書いていない。 そしてカレントディレクトリの隠しファイルとして新たにファイルが作成されることもなかった。

ではどこにvolumesの値が保存されるのかという話だ。
どこにデータが存在するか確かめたい。本当にホストに存在しているのか確かめたいと思った。

Volumeのパスは以下のコマンドで確認することができると分かった。

docker inspect コンテナID
Volumeのパスを参照するコマンドについての資料

Dockerで特定のコンテナのVolumeのパスを確認する方法 - Qiita

結果としてホストのある部分にvolumeに該当する領域を確認することができた。

"Mounts": [
{
"Type": "volume",
"Name": "mydockertest_postgres_data",
"Source": "/var/lib/docker/volumes/mydockertest_postgres_data/_data",
"Destination": "/var/lib/postgresql/data",
"Driver": "local",
"Mode": "rw",
"RW": true,
"Propagation": ""
}
],

またvolumeは自分で削除しない限りずっと残り続けてしまうので確認が必要だと認識した。 ちなみにvolumeのリスト表示は以下のコマンドで表示することができる。

docker volume ls

これでコンテナを使っていないのにvolumeだけ残っているものを発見したら削除できる

Volumeの削除は以下のようにして実行する。 未使用のvolumeを一括削除する

docker volume rm $(docker volume ls -qf dangling=true)

指定して削除するコマンドは以下の通り。

docker volume rm project_db-data
volumeの一括削除に関する資料

Docker volumeの削除 - Qiita

volumeの削除コマンドについての資料

未使用のコンテナ、volumeなどを一括削除 - Qiita