20200213のdockerに関する記事は12件です。

Go + echo + Docker + VSCode + Remote Containers で開発環境の構築 (ホットリロードとステップ実行対応)

背景

  • Go + echo で api サーバーを実装するにあたり開発環境を構築した際のメモ。
    • Docker で動かしたい。
    • VSCode の Remote Containers を試したい。
    • ホットリロード欲しい。
    • ステップ実行したい。

成果物

ホスト

  • Docker 19.03.1
  • docker-compose 1.24.1
  • VSCode 1.42.0
  • ms-vscode-remote.remote-containers 0.101.0

コンテナ内

  • go 1.13.7
  • realize 2.1
  • delve 1.4.0
  • echo 4.1.14

Dockerfile + docker-compose.yml 作成

Dockerfile
FROM golang:1.13.7

WORKDIR /go/src/app

ENV GO111MODULE=on
ENV GOPATH /go

RUN apt-get update \
    && apt-get install -y git \
    && go get -v \
        gopkg.in/urfave/cli.v2@master \
        github.com/oxequa/realize \
        github.com/go-delve/delve/cmd/dlv@latest \
        github.com/rogpeppe/godef@latest \
        golang.org/x/tools/gopls@latest \
        golang.org/x/tools/cmd/goimports@latest \
        github.com/ramya-rao-a/go-outline@latest  \
    && apt-get autoremove -y \
    && apt-get clean -y \
    && rm -rf /var/lib/apt/lists/*
  • ホットリロード用に realize 、デバッグ用に delve をインストール。
  • その他には VSCode 拡張の ms-vscode.go で後にレコメンドされるツール類をイントール。
docker-compose.yml
version: '3.7'

services:
  app:
    build: .
    command: /bin/sh -c "while sleep 1000; do :; done"
    ports:
      - 8080:8080
    volumes:
      - .:/go/src/app:cached
  • command: /bin/sh -c "while sleep 1000; do :; done" で Remote Container が立ち上がるまで待機しておく。

Remote Containers

エクステンションをインストールして起動準備

  • ms-vscode-remote.remote-containers を検索してインストール。
  • VSCode 左下に以下のアイコンが表示されるのでクリック。
    1_remote-containers.png

  • コマンドパレットが開くので Remote-Containers: Add Development Container Configuration Files... を選択。
    2_remote-containers.png

  • From 'docker-compose.yml' を選択。
    3_remote-containers.png

  • .devcontainer ディレクトリに設定ファイルが作成される。

.devcontainer
├── devcontainer.json
└── docker-compose.yml
  • .devcontainer/docker-compose.yml は使わず、事前に作っておいた docker-compose.yml を使うので削除しておく。

  • .devcontainer/devcontainer.json の変更。

.devcontainer/devcontainer.json
{
    "name": "go",

    "dockerComposeFile": [
        "../docker-compose.yml"
    ],

    "service": "app",
    "workspaceFolder": "/go/src/app",

    "settings": { 
        "terminal.integrated.shell.linux": "/bin/bash",
        "go.gopath": "/go"
    },

    "extensions": [
        "ms-vscode.go",
    ],

    "shutdownAction": "stopCompose"
}

Remote Container 起動

  • 左下のアイコンからコマンドパレットを開いて、 Remote-Containers: Reopen in Container を選択。
    4_remote-containers.png

  • コンテナがビルドされて Remote Container 内に入った状態になる。
    5_remote-containers.png

  • VSCode のターミナルを開くとコンテナ内のワーキングディレクトリ /go/src/app が開く。

  • 以降はこの Remote Container 内で作業していく。

go modules の利用準備

$ go mod init

Makefile 作っておく

  • 開発中に色々 make コマンド足していくので、とりあえず作っておく。
.PHONY: run
run:
    realize start --run

realize でのホットリロード設定

  • $ make run を実行すると .realize.yml が作成されるのでこれを以下のように修正。
.realize.yml
settings:
  legacy:
    force: false
    interval: 0s
schema:
  - name: app
    path: .
    commands:
      build:
        status: true
        method: go build -o bin/main
      run:
        status: true
        method: bin/main
    watcher:
      extensions:
        - go
      paths:
        - /
      ignored_paths:
        - .git
        - .realize
        - vendor

動作確認用プログラム

  • echo で Hello, World! を返すだけのやつ。
main.go
package main

import (
    "net/http"

    "github.com/labstack/echo/v4"
)

func main() {
    e := echo.New()
    e.GET("/", func(c echo.Context) error {
        return c.String(http.StatusOK, "Hello, World!")
    })

    e.Logger.Fatal(e.Start(":8080"))
}

起動

$ make run
realize start --run
[13:38:28][APP] : Watching 1 file/s 18 folder/s
[13:38:28][APP] : Build started
[13:38:28][APP] : Build completed in 0.206 s
[13:38:28][APP] : Running..
[13:38:28][APP] :    ____    __
[13:38:28][APP] :   / __/___/ /  ___
[13:38:28][APP] :  / _// __/ _ \/ _ \
[13:38:28][APP] : /___/\__/_//_/\___/ v4.1.14
[13:38:28][APP] : High performance, minimalist Go web framework
[13:38:28][APP] : https://echo.labstack.com
[13:38:28][APP] : ____________________________________O/_______
[13:38:28][APP] :                                     O\
[13:38:28][APP] : ⇨ http server started on [::]:8080
  • 別ターミナルで...
$ curl localhost:8080
Hello, World!
  • ホットリロードが効いているのを確認するため以下の変更をして保存。
main.go
e.GET("/", func(c echo.Context) error {
-    return c.String(http.StatusOK, "Hello, World!")
+    return c.String(http.StatusOK, "Good Bye!")
})
  • 確認
$ curl localhost:8080
Good Bye!
  • ctrl + c で終了。

デバッガを起動してステップ実行

  • デバッグ構成ファイルを作成。
.vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "debug",
      "type": "go",
      "request": "launch",
      "mode": "debug",
      "program": "${workspaceFolder}",
      "showLog": true
    }
  ]
}
  • ブレイクポイントを貼る。
    6_breakpoint.png

  • F5 でデバッガ起動。

    • make run で起動したままではポートがぶつかるので、そちらは終了しておくこと。
  • ブレイクポイントで止まる。

$ curl localhost:8080

7_debug.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerでfailed to get keyring during saving token, operation not permittedが出たら

  1. keyctlがインストールされていない。apt install keyutils
  2. dockerの初期設定でkeyutilsが無効になっている。docker run --rm -it --security-opt seccomp=unconfined debian:jessie \ unshare --map-root-user --user sh -c whoami

参考
https://docs.docker.com/engine/security/seccomp/
https://github.com/Azure/azure-storage-azcopy/issues/193

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【備忘】ubuntu+apache2+php7(docker) on centOS6

前提

vagrant+censos6+docker installは済

ubuntuのインストール

docker pull ubuntu:latest

ubuntuの起動

docker run -it -p 8080:80 --name SOMETHING_NAME ubuntu /bin/bash

ubuntuの初期設定

apt update
# vim
apt install vim

# 日本語化
apt install language-pack-ja-base language-pack-ja
locale-gen
echo export LANG=ja_JP.UTF-8 >> ~/.profile
source ~/.profile

# php
apt install software-properties-common #これないとadd-apt-repoが使えない
add-apt-repository ppa:ondrej/php
apt update
apt install php7.2 php7.2-common php7.2-cli php7.2-fpm php7.2-mysql php7.2-dev php7.2-mbstring php7.2-zip

# apache
apt install apache2
apt install libapache2-mod-php7.2 #これないとapache+phpが使えない

# redis
apt install redis-server
apt install php-redis

virtualHostの設定

touch /etc/apache2/sites-available/vhost_XXX.conf

<VirtualHost *:80>
    ServerName 192.168.0.11:8080
    ServerAdmin webmaster@virtual.host
    DocumentRoot /var/www/html_XXX
    ErrorLog /var/log/apache2/virtual.host.error.log
    CustomLog /var/log/apache2/virtual.host.access.log combined
    LogLevel warn
</VirtualHost>

あとは/var/www/html_xxx/info.phpとかでphpInfoでも出してみる

詰まりどころめも

vagrantの中のdockerの中のapacheにアクセスしたいがどうしたら

dockerのポートフォワードで解決

vimが文字化け

デフォで日本語使えるとか甘え

apache起動したもののphpが生テキストになる

apache2に拡張モジュールとしてphp7モジュールが必要だった

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FluentdとDockerとKinesis Firehose その1(Dockerイメージ作成編)

はじめに

公式のFluentdのDockerイメージからKinesis Data Firehoseまでの連携までを頑張る作業ログであります。

Dockerfile

こちらが今回作ったDockerfile

FROM fluent/fluentd:v1.9-1

USER root

COPY ./fluent.conf /etc/fluent/

# install plugin
RUN apk add --update-cache --virtual .build-deps sudo build-base ruby-dev \
    && gem install fluent-plugin-kinesis -v 3.0.0 --no-document \
    && gem sources --clear-all \
    && apk del .build-deps \
    && rm -rf /var/cache/apk/* \
    /home/fluent/.gem/ruby/*/cache/*.gem

# set timezone (Alpine)
RUN apk --update-cache add tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    apk del tzdata && \
    rm -rf /var/cache/apk/*

使った公式のFluentdのイメージはこちら
スクリーンショット 2020-02-13 15.17.59.png

alpineを使います。
alpineを使う理由としては不必要なものがインストールされていないので非常に軽量なイメージであるという点で、ゆくゆくはECS + Fragateに載せるときにちゃんと乗るようにという意味で使っています。

必要なパッケージとしては

apk add --update-cache --virtual .build-deps ...
三つのパッケージをbuild-depsという名前で一括インストしますという意味
(delするときに一括で消せたりする)

kinesis data firehoseのプラグインに関しては
AWSlabの公式プラグイン
を使っています。現在はv3なので、

gem install fluent-plugin-kinesis -v 3.0.0 --no-document

このようにバージョンを指定しております。
--no-ducumentをつけている理由はこちらを参考にしました。

プラグインインストール後はパッケージが残るので消しておくとimageの軽量化に繋がります。

Timezoneに関してはここを参照にしました。
confはまだ動くのを確認してないので下記にはあげないですが、buildは成功しました

$ docker build . -t fluent/fluentd:v1.9-1
Sending build context to Docker daemon  4.096kB
Step 1/5 : FROM fluent/fluentd:v1.9-1
v1.9-1: Pulling from fluent/fluentd
...

Step 3/5 : COPY ./fluent.conf /fluentd/etc/
 ---> a6c007f06146
Step 4/5 : RUN apk add --update-cache --virtual .build-deps sudo build-base ruby-dev     && gem install fluent-plugin-kinesis -v 3.0.0 --no-document     && gem sources --clear-all     && apk del .build-deps     && rm -rf /var/cache/apk/*     /home/fluent/.gem/ruby/*/cache/*.gem
 ---> Running in e1f69fd761c1
fetch http://dl-cdn.alpinelinux.org/alpine/v3.9/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.9/community/x86_64/APKINDEX.tar.gz
(1/23) Installing sudo (1.8.25_p1-r3)
(2/23) Installing binutils (2.31.1-r2)
(3/23) Installing libmagic (5.36-r1)
(4/23) Installing file (5.36-r1)
(5/23) Installing isl (0.18-r0)
(6/23) Installing libgomp (8.3.0-r0)
(7/23) Installing libatomic (8.3.0-r0)
...
Executing busybox-1.29.3-r10.trigger
OK: 189 MiB in 50 packages
Successfully installed jmespath-1.4.0
Successfully installed aws-partitions-1.272.0
Successfully installed aws-eventstream-1.0.3
Successfully installed aws-sigv4-1.1.0
Successfully installed aws-sdk-core-3.90.0
Successfully installed aws-sdk-kinesis-1.20.0
Successfully installed aws-sdk-firehose-1.24.0
Building native extensions. This could take a while...
Successfully installed google-protobuf-3.11.3
Successfully installed fluent-plugin-kinesis-3.0.0
9 gems installed
*** Removed specs cache ***
(1/23) Purging .build-deps (0)
(2/23) Purging sudo (1.8.25_p1-r3)
...
(12/23) Purging musl-dev (1.1.20-r5)
...
 ---> 4af7de5e802e
Step 5/5 : RUN apk --update-cache add tzdata &&     cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime &&     apk del tzdata &&     rm -rf /var/cache/apk/*
 ---> Running in 1dc29757e0be
fetch http://dl-cdn.alpinelinux.org/alpine/v3.9/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.9/community/x86_64/APKINDEX.tar.gz
(1/1) Installing tzdata (2019c-r0)
Executing busybox-1.29.3-r10.trigger
OK: 31 MiB in 28 packages
(1/1) Purging tzdata (2019c-r0)
Executing busybox-1.29.3-r10.trigger
OK: 28 MiB in 27 packages
Removing intermediate container 1dc29757e0be
 ---> f84414ed4cbc
Successfully built f84414ed4cbc
Successfully tagged fluent/fluentd:v1.9-1

buildしたら、確認

$ docker images
REPOSITORY                                                                                 TAG                 IMAGE ID            CREATED             SIZE
fluent/fluentd                                                                             v1.9-1              f84414ed4cbc        9 seconds ago       50.7MB

50.7MBは結構軽いと思います

runしてみる
※runに関しては公式の「How to use this image」を参考
https://hub.docker.com/r/fluent/fluentd/

$ docker run -d -p 24224:24224 -v /Users/a-honda/recochoku/learn-fluentd/fluentd-data/:/fluentd/log fluent/fluentd:v1.9-1
a1b12b666ea56c77ee2e17ace9b7eb820628da6af44a7127903a9f38f9e8d15d

ポートは24224:24224になっているのは
公式のfluent.confがそうなっているからである
https://github.com/fluent/fluentd-docker-image/blob/master/v1.9/alpine/fluent.conf

ちなみにこんな感じ、また-vでマウントしているのはsymlinkで
/fluentd/log/をマウントしているから.

<source>
  @type  forward
  @id    input1
  @label @mainstream
  port  24224
</source>

<filter **>
  @type stdout
</filter>

<label @mainstream>
  <match docker.**>
    @type file
    @id   output_docker1
    path         /fluentd/log/docker.*.log
    symlink_path /fluentd/log/docker.log
    append       true
    time_slice_format %Y%m%d
    time_slice_wait   1m
    time_format       %Y%m%dT%H%M%S%z
  </match>
  <match **>
    @type file
    @id   output1
    path         /fluentd/log/data.*.log
    symlink_path /fluentd/log/data.log
    append       true
    time_slice_format %Y%m%d
    time_slice_wait   10m
    time_format       %Y%m%dT%H%M%S%z
  </match>
</label>

psはこんな感じ

$ docker ps
CONTAINER ID        IMAGE                   COMMAND                  CREATED             STATUS              PORTS                                NAMES
a1b12b666ea5        fluent/fluentd:v1.9-1   "tini -- /bin/entryp…"   5 seconds ago       Up 4 seconds        5140/tcp, 0.0.0.0:24224->24224/tcp   intelligent_yonath

続きます。
ちなみになんでこんなことしてるかっていうと
firelensのfluentdのカスタムイメージがないから
https://hub.docker.com/r/amazon/aws-for-fluent-bit
↑はあるのに...

つづく

参考ページ

2020年02月14日追記

Dockerfileのところ,
COPY ./fluent.conf /fluentd/etc/
こう書いてたのですが、よくみたら、confを参照するパスが間違っていて
Not Such file fluent.confになっていたので下記に修正します

COPY ./fluent.conf /etc/fluent/

/etc下のfluentでした〜汗
dockerfileのところも書き直しておきます。

そうしたあともういっかいビルドしてdocker exec -it コンテナID /bin/.bashではいって、fluentdコマンドをうつと
スクリーンショット 2020-02-14 10.45.30.png

こんな感じで読み込んでくれてます。
最初のinfoでfluent-plugin-kinesisを読み込んでくれているのがわかります!

この状態で別タブを開いて、docker exec -it コンテナID /bin/.bashで入って、
# echo '{"key":"value"}' | fluent-cat docker.test
と打つと、デフォルトのconfファイルのdocker.**にマッチして'{"key":"value"}'がfluentd-data/に書き込まれているはず

/fluentd/log # ls
docker.b59e7f686c2e2f0d81aedbd8a0a6c887e.log
docker.b59e7f686c2e2f0d81aedbd8a0a6c887e.log.meta

/fluentd/log # cat docker.b59e7f686c2e2f0d81aedbd8a0a6c887e.log
2020-02-14T10:48:53+09:00   docker.test {"key":"value"}

ちゃんとマウントしたホスト側にも同じデータがあればいいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker上のOACISからホストマシンにジョブを投げる手順

oacis_dockerのイメージを使ってdocker上OACISを動かしつつ、dockerを起動しているホストを計算ホストとして利用する手順についてまとめる。
ここでは手元のマシンがmacであることを前提とする。
たとえば手元のマシンのjupyter notebookとかで開発しつつ、OACISの環境構築はdockerで行う場合に有用。

以後、dockerを起動しているマシンをホストマシン、docker上で起動しているコンテナを仮想マシンと呼ぶ。

手順

  • ホストマシン上でsshdを起動する。
    • mac OSの場合、"System Preferences" -> "Sharing" -> "Remote Login" でsshdを起動できる。
  • ホストマシン上でxsubをインストールする。
    • git clone https://github.com/crest-cassia/xsub.git
    • echo 'export PATH="$HOME/xsub/bin:$PATH"' >> ~/.bash_profile
    • echo 'export XSUB_TYPE="none"' >> ~/.bash_profile
    • 詳細は https://github.com/crest-cassia/xsub を参照。
  • docker上でoacisを起動する
    • docker run --name my_oacis -p 127.0.0.1:3000:3000 -dt oacis/oacis
  • ホストマシン上に仮想マシンの公開鍵をコピーする。
    • docker exec -it -u oacis my_oacis bash -lc "ssh-copy-id $USER@host.docker.internal -p 22"
      • ホストマシンにログインするためのパスワードが要求されるので入力する。
  • 仮想マシン上の".ssh/config"にホストマシンにログインするための情報を記載する。
    • docker exec -it -u oacis my_oacis bash -lc "printf \"Host internal\n HostName host.docker.internal\n User $USER\n Port 22\n\" >> ~/.ssh/config"
  • http://localhost:3000 にアクセスしてOACISのページを開き "Hosts"->"New Host" から新しいホストを登録する。
    • Name: internal として登録する。他はデフォルト値でOK。
  • simulatorをOACIS上で登録する。その際にexecutable_oninternalにチェックを入れる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

dockerの課題

docker 利用を進めてきて現在の作業上の課題または課題の候補を記録する。

32bit CPUで、古いWindowsで動作しない

docker for windowsがうなく導入または起動できていない。
直接まだ確かめていない。

PC用OS(windows, macOS, linux)側のdockerの版管理

どのOSのどの版ではdockerのどの版が安定して動作するか。

dockerのcomponent管理

古いcomponentでないと動作しない場合があるかどうか。
直接まだ確かめていない。

X windowsとの連携

Eclipse cheなどX Windowsを利用するソフトウェアなどの、Windows, mac OSでの動作のさせ方

docker hubに登録したソフトの管理

apt update, apt -y upgrade
をしてもよいか、しない方がよいかなどの指針。

security対策

どの水準で、どういうsecurity対策をするとよいか。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Elasticsearch & Kibana をLet's EncryptでTLS対応させる

やりたいこと

Elasticsearch 7.6 と Kibana 7.6 に Security を有効化してDockerで起動する(コピペ) 」をやったものの、最終的にやりたかったFastlyからのLoggingの宛先にするにはTLS周りが厳しかったので、Let's Encryptで証明書を手配することにした。
むしろこっちの方が簡単だったので作業の履歴メモとして残しておく。

前提

  • Dockerが使える
  • ホスト側がRAM 4GB以上で、Elasticsearchに2GB、Kibanaに1GBを割当てられる(Amazon Lightsail あるいは MacBook Pro)
  • ドメインを持っていてDNSがいじれる(仮に example.com とする)
  • Let's Encrypt はDNS認証でワイルドカード証明書を発行する
  • Elasticsearchを elasticsearch.example.com で運用するようDNSが構成済み
  • Kibanaを kibana.example.com で運用するようDNSが構成済み

ほぼコピペでイケる一式

1. Elasticsearchのデータを保存したり証明書一式を保存する場所を作る

  • Elasticsearchのコンテナにボリュームを割当てるときにパーミッションエラーが出たら要調整
mkdir -p ~/Development/Docker/Elasticsearch/data ~/Development/Docker/Elasticsearch/cert

2. Let's Encryptで証明書を手配する

  • certbotのDockerイメージを使うことでAmazon Linux上で、ホストを汚さずに作業する
  • 途中、認証用にDNSへTXTレコードを追加するように指示があるので、DNSにレコードを追加してしばらくしてから継続する
docker run -it --rm \
--name certbot \
-v ~/Development/Docker/Elasticsearch/cert:/etc/letsencrypt \
certbot/certbot certonly \
--manual \
-d *.example.com \
-m info@example.com \
--agree-tos \
--manual-public-ip-logging-ok \
--preferred-challenges dns-01 \
--server https://acme-v02.api.letsencrypt.org/directory

3. ElasticsearchとKibanaに証明書を組み込むDockerfileを用意する

  • 作業しやすい場所に移動
cd ~/Development/Docker/Elasticsearch
  • dockerfile-es というファイル名でElasticsearch用Dockerfileを作る
FROM docker.elastic.co/elasticsearch/elasticsearch:7.6.0

RUN \
elasticsearch-plugin install --batch analysis-icu && \
elasticsearch-plugin install --batch analysis-kuromoji

RUN mkdir /usr/share/elasticsearch/config/cert
ADD "./cert/archive/example.com" /usr/share/elasticsearch/config/cert
RUN chown -R root:elasticsearch /usr/share/elasticsearch/config/cert
  • dockerfile-kibana というファイル名でKibana用Dockerfileを作る
FROM docker.elastic.co/kibana/kibana:7.6.0

ADD "./cert/archive/example" /etc/kibana/cert

4. Dockerイメージをビルドする

docker build -f dockerfile-es -t elasticsearch-tls .
docker build -f dockerfile-kibana -t kibana-tls .

5. Elasticsearchを起動する

docker run --name Elasticsearch -d \
--restart=always \
-m 2048m \
-p 9200:9200 \
-p 9300:9300 \
-e cluster.name=ES \
-e discovery.type=single-node \
-e network.host=0.0.0.0 \
-e xpack.security.enabled=true \
-e xpack.monitoring.collection.enabled=true \
-e "ES_JAVA_OPTS=-Xms1024m -Xmx1024m" \
-e "ELASTIC_PASSWORD=iY69DxipKifV7utYA4t6jgxT" \
-e xpack.security.transport.ssl.enabled=true \
-e xpack.security.transport.ssl.key=/usr/share/elasticsearch/config/cert/privkey1.pem \
-e xpack.security.transport.ssl.certificate=/usr/share/elasticsearch/config/cert/fullchain1.pem \
-e xpack.security.http.ssl.enabled=true \
-e xpack.security.http.ssl.key=/usr/share/elasticsearch/config/cert/privkey1.pem \
-e xpack.security.http.ssl.certificate=/usr/share/elasticsearch/config/cert/fullchain1.pem \
-v ~/Development/Docker/Elasticsearch/data:/usr/share/elasticsearch/data \
--ulimit nproc=4096:4096 \
--ulimit memlock=256000:256000 \
--ulimit nofile=65536:65536 \
elasticsearch-tls

6. Kibana用ユーザーのパスワードを設定する

  • 前提にある通り、この時点で elasticsearch.example.com でElasticsearchにアクセスできることが必要
curl -XPUT --user elastic:iY69DxipKifV7utYA4t6jgxT 'https://elasticsearch.example.com:9200/_xpack/security/user/kibana/_password' -H "Content-Type: application/json" -d '{
  "password" : "6ezji8D5jvceXUsTsvg8mAY4"
}'

7. Kibanaを起動する

  • Elasticsearchとの通信はDocker内で es-internal.example.com で行うように構成する。ドメインを合わせないと証明書が機能しないため。
docker run --name Kibana -d \
--restart=always \
--link Elasticsearch:es-internal.example.com \
-m 1280m \
-p 443:5601 \
-e "ELASTICSEARCH_HOSTS=https://es-internal.example.com:9200" \
-e "ELASTICSEARCH_USERNAME=kibana" \
-e "ELASTICSEARCH_PASSWORD=6ezji8D5jvceXUsTsvg8mAY4" \
-e "SERVER_SSL_ENABLED=true" \
-e "SERVER_SSL_CERTIFICATE=/etc/kibana/cert/fullchain1.pem" \
-e "SERVER_SSL_KEY=/etc/kibana/cert/privkey1.pem" \
--ulimit nproc=4096:4096 \
--ulimit memlock=256000:256000 \
--ulimit nofile=65536:65536 \
kibana-tls

8. 必要な設定をする

  • https://kibana.example.com/ にアクセスして elastic ユーザーとしてログイン
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【 Docker+Nginx+Django+RDS】WEBアプリができるまで⑨身長体重を記録する@一括削除機能つき

前置き

独学で、子供の成長アプリを作った時のことを、記録として残していきます。
間違っているところなどあれば、ご連絡お願いします。
 ①Djangoのようこそページへたどり着くまで
 ②NginxでDjangoのようこそページへたどり着くまで
 ③カスタムユーザーを作ってadminにたどり着く
 ④ログインログアウトをしよう
 ⑤ユーザー登録(サインイン)機能を作ろう
 ⑥ユーザーごとのデータ登録できるようにする〜CRU編
 ⑦ユーザーごとのデータ登録できるようにする〜削除編
 ⑧画像ファイルのアップロード
 ⑨身長体重を記録する@一括削除機能つき <--ここです
 ⑩成長曲線グラフを描いてみよう
 ⑪本番環境へデプロイ+色々手直し

Goal

身長と体重を記録できるようにする。(次の章でグラフ化するデータを登録)

やることとポイントのまとめ

この章で実装するのは、基本的に⑧で作ったもの構成と同じ。
1つ違うのは、レコードの一括削除機能をつけているところ。

Template→Viewへ、削除対象レコードのIDを渡すところが前回と違う。

あとは、Djangoの基本に則って、、、
①新しいAppを作る→setting.pyとurls.pyにAppを作ったことを知らせる
②Modelを作ろう→Migrationする。admin.pyにmodelを追加する
③Templateを作ろう→urls.pyでURLの体系を指定しよう
④Viewを作ろう→Modelを扱いやすいようFormも要るね

①APP作成〜プロジェクト内の指定

docker-compose run web python ./manage.py startapp phys
setting.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'users.apps.UsersConfig',
    'shoes.apps.ShoesConfig',
    'phys.apps.PhysConfig', #追加
    'bootstrap4',
    'bootstrap_datepicker_plus',
    'django_cleanup.apps.CleanupConfig',
]
mysite/urls.py
from django.contrib import admin
from django.urls import path, include
from . import settings
from django.contrib.staticfiles.urls import static
from django.contrib.staticfiles.urls import staticfiles_urlpatterns

urlpatterns = [
    path('admin/', admin.site.urls),
    path('',include('users.urls')),
    path('',include('shoes.urls')),
    path('',include('phys.urls')), #追加
]

urlpatterns += staticfiles_urlpatterns()
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

②Model作成

phys/models.py
from django.db import models
from django.conf import settings
from django.utils import timezone
from users.models import KidsProfile

class PhysData(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    kidsProfile = models.ForeignKey(KidsProfile, on_delete=models.CASCADE)
    weight = models.DecimalField(
                max_digits=6,
                decimal_places=3
             )
    height = models.DecimalField(
                max_digits=5,
                decimal_places=2
             )
    date   = models.DateField(default=timezone.now)

    def __str__(self):
        return self.kidsProfile.name + str(self.date)
phys/admin.py
from django.contrib import admin
from .models import PhysData

admin.site.register(PhysData)

③テンプレート〜URL設計

phys_data_addとphys_data_editは省略。
前回の章で作ったものと、変わらないです。

phys/phys_data_list.html
{% extends 'base.html' %}

{% block content %}
<div class="row">
    <div class="col-md-12 col-lg-2">
        <div class="list-group">
          {% for kidsProfile in kidsProfiles %}
            <a href="/phys/list/{{kidsProfile.id}}" class="list-group-item list-group-item-action">{{kidsProfile.name}}</a>
          {% endfor %}
        </div>
    </div>
    <div class="col-md-12 col-lg-10 overflow-auto">
        <form method="post" action="{% url 'phys:phys_data_delete' %}">
        <table class="table table-hover">
          <thead class="thead-dark">
            <tr>
              <th>名前</th>
              <th>日付</th>
              <th>体重(kg)</th>
              <th>身長(cm)</th>
              <th>修正</th>
              <th>削除</th>
            </tr>
          </thead>
      {% for data_post in data_posts %}
          <tr>
            <td class="text-center">{{data_post.kidsProfile.name }}</td class="text-center">
            <td class="text-center">{{data_post.date }}</td>
            <td class="text-center">{{data_post.weight }}</td>
            <td class="text-center">{{data_post.height }}</td>
            <td class="text-center"><a href="{% url 'phys:phys_data_edit' data_post.pk %}" class="btn btn-success btn-sm" role="button">編集</a></td>
            <td class="text-center"><input type="checkbox" name="delete_ids" value="{{ data_post.pk }}"></input></td>
          </tr>
          {% endfor %}
          <tr>
            <td>
                <a href="{% url 'phys:phys_data_add' %}" role="button" class="btn btn-primary mr-auto">追加</a>
            </td>
            <td></td>
            <td></td>
            <td></td>
            <td></td>
            <td class="text-center">
              {% csrf_token %}
              <input type="submit" value="一括削除" class="btn btn-danger float-right">
            </td>
          </tr>
        </table>
        </form>
    </div>
{% endblock %}
phys/urls.py
from django.urls import path
from . import views

app_name = 'phys'

urlpatterns = [
    path('phys/list/', views.phys_data_list, name='phys_data_list'),
    path('phys/list/<kidsProfileId>', views.phys_data_list, name='phys_data_list'),
    path('phys/data_add/', views.phys_data_add, name='phys_data_add'),
    path('phys/data_edit/<DataPostId>', views.phys_data_edit, name='phys_data_edit'),
    path('phys/data_delete/', views.phys_data_delete, name='phys_data_delete'),
]

④ViewとModel

phys/views.py
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from .models import PhysData
from users.models import KidsProfile
from .forms import PhysDataForm, PhysDataEditForm



#身長体重データを昇順で返却
@login_required
def phys_data_list(request, **kwargs):
    user_name = request.user
    if len(kwargs) > 0:
        kidsProfileId = kwargs["kidsProfileId"]
    else:
        kidsProfileId = KidsProfile.objects.filter(user=user_name).order_by('id')[0]

    kids_profiles = KidsProfile.objects.filter(user=user_name)

    data_posts = PhysData.objects.filter(user=user_name, kidsProfile=kidsProfileId).order_by('date')
    params = {
        'data_posts': data_posts,
        'kidsProfiles' : kids_profiles,
    }

    return render(request, 'phys/phys_data_list.html', params)

#身長体重データを新規登録する
@login_required
def phys_data_add(request):
    user_name = request.user
    if request.method == 'POST':
      form = PhysDataForm(request.POST, user = user_name)
      if form.is_valid():
        data = form.save(commit=False)
        data.user = user_name
        data.save()
        return redirect('phys:phys_data_list')
    else:
      form = PhysDataForm(user = user_name)
    return render(request, 'phys/phys_data_add.html', {'form': form})

#身長体重データを編集する
@login_required
def phys_data_edit(request, DataPostId):
    physData = PhysData.objects.get(pk=DataPostId)
    if request.method == 'POST':
      form = PhysDataEditForm(request.POST)
      if form.is_valid():
        data = form.save(commit=False)
        data.user = request.user
        data.kidsProfile = physData.kidsProfile
        data.id = physData.id
        data.save()
        return redirect('phys:phys_data_list')
    else:
      form = PhysDataEditForm(
        {
          'weight' : physData.weight ,
          'height' : physData.height ,
          'date' : physData.date
        },
        user = request.user
      )
    return render(request, 'phys/phys_data_edit.html', {'form': form})

#身長体重データを削除する
@login_required
def phys_data_delete(request):
    delete_ids = request.POST.getlist('delete_ids')
    PhysData.objects.filter(pk__in=delete_ids).delete()
    return redirect('phys:phys_data_list')
phys/forms.py
from django import forms
from .models import PhysData
from users.models import KidsProfile
import bootstrap_datepicker_plus as datetimepicker



class PhysDataForm(forms.ModelForm):
    class Meta:
        model = PhysData
        fields = ('kidsProfile', 'weight', 'height', 'date',)
        widgets = {
            'date': datetimepicker.DatePickerInput(
                           format='%Y-%m-%d'),
        }

    def __init__(self, *args, **kwargs):
        user = kwargs.pop('user','')
        super(PhysDataForm, self).__init__(*args, **kwargs)
        self.fields['kidsProfile'] = forms.ModelChoiceField(queryset=KidsProfile.objects.filter(user=user))

class PhysDataEditForm(forms.ModelForm):
    class Meta:
        model = PhysData
        fields = ('weight', 'height', 'date',)
        widgets = {
            'date': datetimepicker.DatePickerInput(
              format='%Y-%m-%d',
              ),
        }

    def __init__(self, *args, **kwargs):
        user = kwargs.pop('user','')
        kidsProfileId = kwargs.pop('kidsProfileId','')
        super(PhysDataEditForm, self).__init__(*args, **kwargs)

できあがり

こんな形になります。削除レコードはチェックボックス形式で纏めて指定&削除可能。
スクリーンショット 2020-02-13 01.34.59.png
登録画面は、普通。編集画面も、ほぼ同様。(子供の名前を選ばない程度)
スクリーンショット 2020-02-13 01.35.10.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

dockerが占有しているディスク容量を開放するコマンド

docker system prune

自分の個人用Linux環境では現在root(/)に100Gのディスクをマウントしているのだけれど、最近ディスクスペースがないぞと警告がでてしまった。

調べるとdockerのimageやらなんやらで25GBもぶんどられていたので、ここを削減すべく調査。

調査結果

以下のコマンドでいい感じに使っていないものを削除してくれる。

$ docker system prune -af

14GBも削除できた。
とりあえず満足。

$ docker system prune -af
...                                                     
deleted: sha256:324e4d3a7dea2e55f7d62c69ed5c2c75e0498830d33f1bdf91ac3b53caaa3a4f
deleted: sha256:4500a5ee8f0d643c3f75655827ea893133976c034aaf02e2d362c2d74b3f2553
deleted: sha256:7f097c1fef1a1ccb97a17ae6e82f72096db3337655f3e4e3b192bdd61e9ed858
deleted: sha256:3feae1c6f465840e3ea3658175c7667ba6fc29dee110de2a690e751ac0d36962
deleted: sha256:7a73e56f893cf6284a6ad2be795dfe627da1b354e95befc61e49011a84e3f846
deleted: sha256:f38296c87e1266afe76efb0b909646b2898d50450a03679d505e9ee220ad3a61
deleted: sha256:a78b5195ddd832f217b5255707d9767efd74c43a3d373e7fcdff4176998d87a4
deleted: sha256:65e53d7a0ba16f3c4c6382176d26a23af5d669e0354910e7f3cb23c1ac2b35d1
deleted: sha256:7d54cb4e40b6e9f887c39adcc92656626fd8eb5bcd3472622bcdd03a7b63bbb7
deleted: sha256:db45b38d6bbd6cab848d4958e172201e041dfbc0abbacdfe55937081e809979b
deleted: sha256:05340d226ca3ff0041553d3d732257a8f3182e11612e47ac15cde17fa8a5e568
deleted: sha256:9cbb0103d6c58071dae6b3b5e5a471e954785875803067ab570008cd50e3db13
deleted: sha256:f7c732c81f969b4677ef4312ca2f70e5e3368e5e3a2244809d064a9533e0576d
deleted: sha256:ebb9ae013834b54e76c8d7dfde0ca9018f6bb3495740356a8f1dc655a8552130

Total reclaimed space: 14.1GB

volumeも削除したい場合には--volumes フラグをつける。

$ docker system prune -f --volumes
...
4290242096f39a7b48c1ed736b93395a917b682369717cbb020cacda4d3ded5a
4607556efe8c2d4450f5452f74260bd421acda8ded47f4e5b854881c08f318e5
579a2fe1ddd4dea14d8ad8320daa7c48c0fc4bc2c1c754dd7ab9f167f3ab2c4a
21bb1876436790dd3c1b1cf5696f925834f32b951d1a2aa6644d316b947722a3
50c9a60c8228b7d86a610f4a582cba2d344c9c3fc6a6f73c8ecb1e6b61f9be08
93ab608312285d3158c468ea6f8f143bb61ffc0fc813c6bc1a0179dee22565e9
a10442a600e0ef690ed8357ee82ca42c70254ace5a47ba899fb4ce81badab39c
164c8cc1a4e287ab0b5d55709e784c2cd0e73181dccefebbbb6be1f14cd8aabe
4200fd127cffb155c2c153257897755597498ce1a333f47cc3153827bc10c737
76c0c21bfdd76e2b9b8aebd77a37b0e4bbb09a5590671d7b2f327bc5fcc970f6

Total reclaimed space: 681.9MB
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

dockerが占有しているディスクを開放するコマンド

docker system prune

自分の個人用Linux環境では現在root(/)に100Gのディスクをマウントしているのだけれど、最近ディスクスペースがないぞと警告がでてしまった。

調べるとdockerのimageやらなんやらで25GBもぶんどられていたので、ここを削減すべく調査。

調査結果

以下のコマンドでいい感じに使っていないものを削除してくれる。

$ docker system prune -af

14GBも削除できた。
とりあえず満足。

$ docker system prune -af
...                                                     
deleted: sha256:324e4d3a7dea2e55f7d62c69ed5c2c75e0498830d33f1bdf91ac3b53caaa3a4f
deleted: sha256:4500a5ee8f0d643c3f75655827ea893133976c034aaf02e2d362c2d74b3f2553
deleted: sha256:7f097c1fef1a1ccb97a17ae6e82f72096db3337655f3e4e3b192bdd61e9ed858
deleted: sha256:3feae1c6f465840e3ea3658175c7667ba6fc29dee110de2a690e751ac0d36962
deleted: sha256:7a73e56f893cf6284a6ad2be795dfe627da1b354e95befc61e49011a84e3f846
deleted: sha256:f38296c87e1266afe76efb0b909646b2898d50450a03679d505e9ee220ad3a61
deleted: sha256:a78b5195ddd832f217b5255707d9767efd74c43a3d373e7fcdff4176998d87a4
deleted: sha256:65e53d7a0ba16f3c4c6382176d26a23af5d669e0354910e7f3cb23c1ac2b35d1
deleted: sha256:7d54cb4e40b6e9f887c39adcc92656626fd8eb5bcd3472622bcdd03a7b63bbb7
deleted: sha256:db45b38d6bbd6cab848d4958e172201e041dfbc0abbacdfe55937081e809979b
deleted: sha256:05340d226ca3ff0041553d3d732257a8f3182e11612e47ac15cde17fa8a5e568
deleted: sha256:9cbb0103d6c58071dae6b3b5e5a471e954785875803067ab570008cd50e3db13
deleted: sha256:f7c732c81f969b4677ef4312ca2f70e5e3368e5e3a2244809d064a9533e0576d
deleted: sha256:ebb9ae013834b54e76c8d7dfde0ca9018f6bb3495740356a8f1dc655a8552130

Total reclaimed space: 14.1GB

volumeも削除したい場合には--volumes フラグをつける。

$ docker system prune -f --volumes
...
4290242096f39a7b48c1ed736b93395a917b682369717cbb020cacda4d3ded5a
4607556efe8c2d4450f5452f74260bd421acda8ded47f4e5b854881c08f318e5
579a2fe1ddd4dea14d8ad8320daa7c48c0fc4bc2c1c754dd7ab9f167f3ab2c4a
21bb1876436790dd3c1b1cf5696f925834f32b951d1a2aa6644d316b947722a3
50c9a60c8228b7d86a610f4a582cba2d344c9c3fc6a6f73c8ecb1e6b61f9be08
93ab608312285d3158c468ea6f8f143bb61ffc0fc813c6bc1a0179dee22565e9
a10442a600e0ef690ed8357ee82ca42c70254ace5a47ba899fb4ce81badab39c
164c8cc1a4e287ab0b5d55709e784c2cd0e73181dccefebbbb6be1f14cd8aabe
4200fd127cffb155c2c153257897755597498ce1a333f47cc3153827bc10c737
76c0c21bfdd76e2b9b8aebd77a37b0e4bbb09a5590671d7b2f327bc5fcc970f6

Total reclaimed space: 681.9MB
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【 Docker+Nginx+Django+RDS】WEBアプリができるまで⑧画像ファイルのアップロード

前置き

独学で、子供の成長アプリを作った時のことを、記録として残していきます。
間違っているところなどあれば、ご連絡お願いします。
 ①Djangoのようこそページへたどり着くまで
 ②NginxでDjangoのようこそページへたどり着くまで
 ③カスタムユーザーを作ってadminにたどり着く
 ④ログインログアウトをしよう
 ⑤ユーザー登録(サインイン)機能を作ろう
 ⑥ユーザーごとのデータ登録できるようにする〜CRU編
 ⑦ユーザーごとのデータ登録できるようにする〜削除編
 ⑧画像ファイルのアップロード <--ここです
 ⑨身長体重を記録する@一括削除機能つき
 ⑩成長曲線グラフを描いてみよう
 ⑪本番環境へデプロイ+色々手直し

Goal

子供の靴のサイズなどを記録できる機能を作ります。
このへんから、実際のサービスっぽくなります。

作るもの

子供達の靴の情報を登録・管理する機能を、アプリ分割します。

docker-compose run web python ./manage.py startapp shoes
.
├── docker-compose.yml
├── nginx
│   ├── conf
│   │   └── mysite_nginx.conf
│   └── uwsgi_params
├── src
│   ├── manage.py
│   ├── media
│   │   └── image
│   │       └── noimage.png
│   ├── mysite
│   │   ├── __init__.py
│   │   ├── __pycache__
│   │   │   └── 略
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   ├── shoes
│   │   ├── __init__.py
│   │   ├── __pycache__
│   │   │   └── 略
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── forms.py
│   │   ├── migrations
│   │   │   └── 略
│   │   ├── models.py
│   │   ├── tests.py
│   │   ├── urls.py
│   │   └── views.py
│   ├── static
│   │   └── 略
│   ├── templates
│   │   ├── base.html
│   │   ├── shoes
│   │   │   ├── shoes_data_add.html
│   │   │   ├── shoes_data_edit.html
│   │   │   └── shoes_data_list.html
│   │   └── users
│   │       ├── kids_profile_add.html
│   │       ├── kids_profile_edit.html
│   │       ├── login.html
│   │       ├── mypage.html
│   │       └── signup.html
│   └── users
│       ├── __init__.py
│       ├── __pycache__
│       │   └── 略
│       ├── admin.py
│       ├── apps.py
│       ├── forms.py
│       ├── migrations
│       │   └── 略
│       ├── models.py
│       ├── tests.py
│       ├── urls.py
│       └── views.py
├── static
│       └── 略
└── web
    ├── Dockerfile
    └── requirements.txt

Model

靴情報は子供ごとに持つので、userとKidsProfileのモデルを外部参照。

靴のサイズは一般的に0.5ずつ上がるものなので、range関数を使って選択肢を生成。
ただしrange関数は整数しか扱えないので、うまいことやってあげる必要あり。

そして靴の画像を撮らないと、なんの靴のことを記録してるのかわからないので
画像をアップロードできるようにします。ImageField。ただしアップロードしない
人もいるかもしれないので、そんなときのためにdefaultの画像はこちらで用意。

shoes/models.py
from django.db import models
from django.conf import settings
from django.utils import timezone
from users.models import KidsProfile

#靴データ
class ShoesData(models.Model):

    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,null=True)
    kidsProfile = models.ForeignKey(KidsProfile, on_delete=models.CASCADE,null=True)
    buy_date = models.DateField(default=timezone.now)
    shoes_size = models.DecimalField(
                max_digits=3,
                decimal_places=1,
                choices = ((x/2, str(x/2)) for x in range(16,50,1)),
                blank = False
                )
    shoes_memo = models.TextField(blank=True, null=True)
    shoes_image = models.ImageField(upload_to="image/", blank=True, default="image/noimage.png")

    def __str__(self):
        return self.kidsProfile.name + str(self.buy_date)

Form

モデルに合わせてFormも作ります。
追加するときや参照するときは子供の情報が必要なのですが、
編集するときは「どの子の情報を直すか」は分かっているので、Edito用のFormも作ります。
なんだか冗長な気がしましたが、これ以上のやり方もわからず。

forms.py
from django import forms
from .models import ShoesData
from users.models import KidsProfile
import bootstrap_datepicker_plus as datetimepicker


class ShoesDataForm(forms.ModelForm):
    class Meta:
        model = ShoesData
        fields = ('kidsProfile', 'buy_date', 'shoes_size', 'shoes_memo', 'shoes_image')
        widgets = {
            'buy_date': datetimepicker.DatePickerInput(
                           format='%Y-%m-%d'),
        }

    def __init__(self, *args, **kwargs):
        user = kwargs.pop('user','')
        super(ShoesDataForm, self).__init__(*args, **kwargs)
        self.fields['kidsProfile'] = forms.ModelChoiceField(queryset=KidsProfile.objects.filter(user=user))

class ShoesDataEditForm(forms.ModelForm):
    class Meta:
        model = ShoesData
        fields = ('buy_date', 'shoes_size', 'shoes_memo', 'shoes_image')
        widgets = {
            'buy_date': datetimepicker.DatePickerInput(
                           format='%Y-%m-%d'),
        }

    def __init__(self, *args, **kwargs):
        user = kwargs.pop('user','')
        kidsProfileId = kwargs.pop('kidsProfileId','')
        super(ShoesDataEditForm, self).__init__(*args, **kwargs)

画像を扱おう(準備)

扱うために3つほど導入します。

・pillow
ImageFieldを扱うためのは、pillowというライブラリが必要です。

・django-cleanup
ImageFieldは、あくまでImageFieldオブジェクトなので、実際のjpegとかpngを管理はしてくれません。
つまり、ImageFieldを削除しても、画像ファイルは削除してくれないのです。(ゴミが残り続ける!)
そんなときのために、django-cleanupというライブラリを使います。

・Lightbox
画像をぽわーんとそれっぽく表示するやつです。
必要なCSSとJSは、STATICフォルダに格納しておいてください。

requirement.txt
Django==2.2.2
psycopg2==2.8.4
uwsgi==2.0.17
django-bootstrap4==1.1.1
django-bootstrap-datepicker-plus==3.0.5
pillow==7.0.0 #追加
django-cleanup==4.0.0 #追加

ImageFieldで指定したupload_toは、setting.pyで指定したMEDIAフォルダに行きます。
setting.pyにも、いろいろ追加しましょう。

setting.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'users.apps.UsersConfig',
    'shoes.apps.ShoesConfig', #追加
    'bootstrap4',
    'bootstrap_datepicker_plus',
    'django_cleanup.apps.CleanupConfig', #追加
]

(略)

MEDIA_ROOT = os.path.join(BASE_DIR, 'media') #追加
MEDIA_URL = '/media/' #追加

HTML/CSS

一覧表示するTemplate。モーダルを使って削除情報出すのは、前回と同様。
ポイント
(shoes_data_list)
・子供情報はページ左部にlist-group形式で並べて、選択するとそのIDを持たせて再表示。
 urls.py(とview.py)では、IDがくるときと、IDがこないときで待ち受け。
・画像ファイルは、登録がなかったらno-imageを表示させる。
・画像ファイルはbackground-imageで扱うことで、レスポンシブ対応に。
 画像サイズのレスポンシブ設定はCSSにて。

(shoes_data_list)
・Lightboxとdatepicker用のブロックコメントの順番は、これじゃないとダメ。
 datepickerのポップアップが機能しなくなります。(結構ハマった)
・メニューもちゃんとリンクを貼るようにしてみた。
 アプリを分けているので、ちゃんと名前指定(shoes:みたいに)しないとダメ。

(shoes_data_add,shoes_data_edit)
ひねりなし。言うこともなし。

shoes/shoes_data_list.html
{% extends 'base.html' %}

{% block extra_js %}
<script>
$(function() {
  $('.del_confirm').on('click', function () {
     $("#del_pk").text($(this).data("pk"));
     $('#del_url').attr('href', $(this).data("url"));

     $(this).attr('href', href);
  });
});
</script>
{% endblock extra_js %}

{% block content %}
<div class="row">
    <div class="col-md-12 col-lg-2">
        <div class="list-group">
          {% for kidsProfile in kidsProfiles %}
            <a href="/shoes/list/{{kidsProfile.id}}" class="list-group-item list-group-item-action">{{kidsProfile.name}}</a>
          {% endfor %}
        </div>
    </div>
    <div class="col-md-12 col-lg-10 overflow-auto">

        <div class="card">
            <div class="card-header">
                {{ kidsName }}
            </div>

            <div class="card-body">
                <div class="container-fluid">
                    {% for shoes_data_post in shoes_data_posts %}
                    <div class="row">
                        {% if shoes_data_post.shoes_image %}
                            <a href="{{ shoes_data_post.shoes_image.url }}"
                               style="background-image: url({{ shoes_data_post.shoes_image.url }})"
                               class="col-4 col-lg-2 border shoes_img" data-lightbox="demo"></a>
                        {% else %}
                            <a href="/media/image/noimage.png"
                               style="background-image: url(/media/image/noimage.png)"
                               class="col-4 col-lg-2 border shoes_img" data-lightbox="demo"></a>
                        {% endif %}
                        <div class="col-8 col-lg-10 pl-lg-5">
                            <div class="row">
                                <div class="col-md-12 col-lg-2 mb-2 rounded bg-secondary">名前</div>
                                <div class="col-md-12 col-lg-3 mb-2">{{ shoes_data_post.kidsProfile }}</div>
                                <div class="col-md-0  col-lg-7 "></div>
                                <div class="col-md-12 col-lg-2 mb-2 rounded bg-secondary">購入日</div>
                                <div class="col-md-12 col-lg-3 mb-2">{{ shoes_data_post.buy_date }}</div>
                                <div class="col-md-0  col-lg-1 "></div>
                                <div class="col-md-12 col-lg-2 mb-2 rounded bg-secondary">サイズ</div>
                                <div class="col-md-12 col-lg-3 mb-2">{{ shoes_data_post.shoes_size }}cm</div>
                                <div class="col-md-0  col-lg-1 "></div>
                                <div class="w-100"></div>
                                <div class="col-lg-2 rounded bg-secondary">メモ</div>
                                <div class="col-lg-10">{{ shoes_data_post.shoes_memo }}</div>
                                <div class="col-12 text-right mt-2">
                                    <a href="{% url 'shoes:shoes_data_edit' shoes_data_post.id %}" role="button" class="btn btn-success btn-sm">編集</a>
                                    <button type="button" class="btn btn-danger btn-sm del_confirm" data-toggle="modal" data-target="#delete_modal"
                                        data-pk="{{ shoes_data_post.pk }}" data-url="{% url 'shoes:shoes_data_delete' shoes_data_post.id %}">削除</button>
                                </div>
                            </div>
                        </div>
                    </div>
                    <hr>
                    {% endfor %}
                </div>
            </div>
        </div>
        <a href="{% url 'shoes:shoes_data_add' %}" role="button" class="btn btn-primary mr-auto">追加</a>


        <div class="modal fade" id="delete_modal" tabindex="-1" role="dialog" aria-labelledby="label1" aria-hidden="true">
            <div class="modal-dialog modal-dialog-centered">
                <div class="modal-content">
                    <div class="modal-body">
                        削除しても良いですか?
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">キャンセル</button>
                        <a href="#" id="del_url" class="btn btn-danger">削除</a>
                    </div>
                 </div>
             </div>
        </div>
    </div>
{% endblock %}

base.html
{% load static %}

<html>
    <head>
        <title>kids Growth</title>

        <link rel="stylesheet" href="{% static 'css/kidsGrowth.css' %}">
        <link rel="stylesheet" href="{% static 'css/lightbox.min.css' %}">
        <script src="{% static 'js/lightbox-plus-jquery.min.js' %}"></script>
        {% block extra_js %}{% endblock %}
        {% load bootstrap4 %}
        {% bootstrap_css %}
        {% bootstrap_javascript %}
    </head>
    <body>
        <!-- Navigation -->
        <nav class="navbar navbar-expand-sm navbar-dark bg-dark mt-3 mb-3">
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav4" aria-controls="navbarNav4" aria-expanded="false" aria-label="Toggle navigation">
              <span class="sr-only">メニュー</span>  
              <span class="navbar-toggler-icon"></span>
            </button>
            <a class="navbar-brand" href="">Kids Growth</a>
            <div class="collapse navbar-collapse" id="navbarNav4">
                <ul class="navbar-nav">
                  <li class="nav-item">
                      <a class="nav-link" href="">メニュー1</a>
                  </li>
                  <li class="nav-item">
                      <a class="nav-link" href="">メニュー2</a>
                  </li>
                  <li class="nav-item">
                      <a class="nav-link" href="{% url 'shoes:shoes_data_list'%}">シューズ一覧</a>
                  </li>
                  <li class="nav-item">
                      <a class="nav-link" href="{% url 'users:mypage'%}">マイページ</a>
                  </li>
                  {% if user.is_authenticated %}
                    <li class="nav-item">
                        <a class="nav-link" href="{% url 'users:logout'%}">ログアウト</a>
                    </li>
                  {% else %}
                    <li class="nav-item">
                        <a class="nav-link" href="{% url 'users:login'%}">ログイン</a>
                    </li>
                  {% endif %}
                </ul>
            </div>
        </nav>

        <div class="content container">
          {% block content %}
          {% endblock %}
        </div>
    </body>
</html>
shoes/shoes_data_add.html
{% extends 'base.html' %}

{% block content %}

<!--日付入力フォーマット用-->
{{ form.media }}

<div class="col-md-12 col-lg-5">
    <h2>New data</h2>
    <form method="POST" class="post-form"  enctype="multipart/form-data">{% csrf_token %}
        {% bootstrap_form form %}
        <button type="submit" class="save btn btn-primary">Save</button>
    </form>
</div>
{% endblock %}
shoes/shoes_data_edit.html
{% extends 'base.html' %}

{% block content %}

<!--日付入力フォーマット用-->
{{ form.media }}

<div class="col-md-12 col-lg-5">
    <h2>Data Edit</h2>
    <form method="POST" class="post-form"  enctype="multipart/form-data">{% csrf_token %}
        {% bootstrap_form form %}
        <button type="submit" class="save btn btn-primary">Save</button>
    </form>
</div>
{% endblock %}
kidsGrowth.css
.shoes_img {
  background-size: cover;
  background-position: center center;
}
shoes/urls.py
from django.urls import path
from . import views

app_name = 'shoes'

urlpatterns = [
    path('shoes/list/', views.shoes_data_list, name='shoes_data_list'),
    path('shoes/list/<kidsProfileId>', views.shoes_data_list, name='shoes_data_list'),
    path('shoes/data_add/', views.shoes_data_add, name='shoes_data_add'),
    path('shoes/data_edit/<shoesDataId>', views.shoes_data_edit, name='shoes_data_edit'),
    path('shoes/data_delete/<shoesDataId>', views.shoes_data_delete, name='shoes_data_delete'),
]

View

最後にView。なんか、もっと上手く書けるような気がしてならないですが。。

(shoes_data_list)
・初期表示の際に、最初に登録された子供(IDが一番若い)の情報を出すように。

(shoes_data_add)
・画像ファイルはrequest.FILES.get()で受け取らないといけないらしい。
 アップロードが面倒な人もいるかもしれないので、Validation回避。

(shoes_data_edit)
・django-cleanupを動かすために、変更前のレコードはdelete()している。
 UpdateではなくCreate&Deleteすることで、紐付く画像ファイル自体を自動で削除。

sheoes/views.py
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from .models import ShoesData
from users.models import KidsProfile
from .forms import ShoesDataForm, ShoesDataEditForm


#靴リスト
@login_required
def shoes_data_list(request, **kwargs):
    user_name = request.user
    if len(kwargs) > 0:
        kidsProfileId = kwargs["kidsProfileId"]
    else:
        kidsProfileId = KidsProfile.objects.filter(user=user_name).order_by('id')[0].id

    kids_profiles = KidsProfile.objects.filter(user=user_name) #子供情報選択用
    kids_profile  = KidsProfile.objects.filter(id=kidsProfileId)
    shoes_data_posts = ShoesData.objects.filter(user=user_name, kidsProfile=kidsProfileId).order_by('buy_date')

    params = {
        'shoes_data_posts' : shoes_data_posts,
        'kidsProfiles' : kids_profiles,
        'kidsName' : kids_profile[0].name,
    }
    return render(request, 'shoes/shoes_data_list.html', params)

#靴データを新規追加
@login_required
def shoes_data_add(request):
    user_name = request.user
    if request.method == 'POST':
      form = ShoesDataForm(request.POST, user = user_name)
      if form.is_valid():
        data = form.save(commit=False)
        if request.FILES.get('shoes_image') is None:
            pass
        else:
            data.shoes_image = request.FILES.get('shoes_image')
        data.user = user_name
        data.save()
        return redirect('shoes:shoes_data_list')
    else:
      form = ShoesDataForm(user = user_name)
    return render(request, 'shoes/shoes_data_add.html', {'form': form})

#靴データを編集
@login_required
def shoes_data_edit(request, shoesDataId):
    shoes_data = ShoesData.objects.get(pk=shoesDataId)
    if request.method == 'POST':
      form = ShoesDataEditForm(request.POST)
      if form.is_valid():
        data = form.save(commit=False)
        data.user = request.user
        data.kidsProfile = shoes_data.kidsProfile
        data.shoes_image = request.FILES.get('shoes_image')
        data.save()
        shoes_data.delete()
        return redirect('shoes:shoes_data_list')
    else:
      form = ShoesDataEditForm(
        {
          'buy_date' : shoes_data.buy_date ,
          'shoes_size' : shoes_data.shoes_size ,
          'shoes_memo' : shoes_data.shoes_memo ,
          'shoes_image' : shoes_data.shoes_image
        },
        user = request.user
      )
    return render(request, 'shoes/shoes_data_edit.html', {'form': form})

#靴データを削除
@login_required
def shoes_data_delete(request, shoesDataId):
    ShoesData.objects.get(id=shoesDataId).delete()
    return redirect('shoes:shoes_data_list')

動かすよ〜

画像をアップしなかったものと、アップしたもの。
スクリーンショット 2020-02-13 00.20.38.png
登録画面はシンプル。
スクリーンショット 2020-02-13 00.21.00.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Raspberry Pi上のDockerで動くイメージのCIでのビルド方法

発端

x86_64アーキテクチャ上でビルドしたDockerイメージが、Raspberry Pi上で動かない。
(Kubernetesクラスター on Raspberry Piでうまく動かなかった)

エラー内容

standard_init_linux.go:211: exec user process caused "exec format error"

理由

ビルドしたマシンと、Raspberry PiとでCPUアーキテクチャが異なるため。

# 普段使ってるLinux
$ uname -m
x86_64

# Raspberry Pi
$ uname -m
armv7l

対策1

Raspberry Pi上でイメージをビルドする。
この時、FROM nodeではなく、FROM arm32v7/node等、arm用のベースイメージに変更する必要がある(場合もある)。

対策2

対策1には実機が必要になる。
CIでイメージを作成したい場合等には利用できない。(ほとんどのCIはx86_64アーキテクチャなので)

そこで、multiarch/qemu-user-staticを利用することで、x86_64アーキテクチャ上でarm用イメージをビルドできるようになる。

name: main

on: [push, pull_request]

jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: setup docker for arm
        run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
      - name: build image
        run: docker build -f Dockerfile.arm -t owner/name:arm .
      - name: push image
        run: |
          docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASS }}
          docker push owner/name:arm
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む