yikegaya’s blog

仕事関連(Webエンジニア)と資産運用について書いてます

Nuxt.jsとPython FastAPIで作ったブログサービスを無料デプロイした

NuxtとPython FastAPI、Postgresqlでブログサービスをサクッと作ってVercelとHerokuで無料デプロイしてみた

作ったもの

blog-app-frontend-red.vercel.app

ソースコード

github.com

github.com

作った動機

ブログサービスは昔Railsで作ったことあるんだけど、VueかReact使ってSPAでまた作りたいなーとぼんやり思ってたので。 あとPython FastAPIが最近評判良さそうで使ってみたかった。

2021/8/29現在実装した機能

  • ユーザ登録、ログイン
  • 記事投稿
  • 記事の詳細確認

まあ、とりあえず基本機能ができた程度の進捗。

デプロイ手順

FastAPIのherokuへのデプロイ

FastAPI用のDockerイメージを開発環境用に使ってたんでそのイメージを使い回してHerokuにデプロイしたいんだけど、ただ開発環境では開発用のシェルで実行していたんで本番環境ではそこを外したイメージを用意した。

FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7

RUN /usr/local/bin/python -m pip install --upgrade pip

COPY ./app /app
RUN pip install -r requirements.txt

↓本番用のDockerではこれを外す
CMD /start-reload.sh

heroku.yml

build:
  docker:
    web: Dockerfile.production

heroku.ymlを用意したらFastAPIのプロジェクトフォルダで以下のコマンドを実行するとデプロイできる

# herokuにログイン
heroku login
# アプリケーション作成
heroku create
# Docekrコンテナでデプロイするよう設定
heroku stack:set container
# デプロイ実行
git push heroku main

また、Postgreのデータベースを作成してテーブルを作成する(テーブル作成用のプログラムは現状作ってないので手動のSQLで作っておく)

## 無料のデータベース作成
heroku addons:create heroku-postgresql:hobby-dev
# herokuのコマンドプロンプトを立ち上げてそこからSQL実行
heroku pg:psql

Herokuの注意点

  • Heroku上でアプリケーションのポートを保持するPORTという環境変数がデフォルトで作られているのでプログラム上それを使用するようにしておく(自分で80とか3000とかポート指定できないっぽい)
  • PostgreのURLが設定されているDATABASE_URLという環境変数も作られているのでPostgreへの接続にはその変数を使用するようにしておく

NuxtのVercelへのデプロイ

VercelでのデプロイはNuxtの場合簡単。

作業としてやることは

  1. vercel.jsonを用意してNuxtのルートディレクトリに置いておく
  2. GithubにpushしてVercelの管理画面上でレポジトリ選択してデプロイ実行
  3. 環境変数でバックエンドのURLを指定しているのでVercelの管理画面上からPython FastAPIをデプロイしたherokuのURLを設定する
  4. デプロイ後、VercelのURLが発行されるのでFastAPIのCORSでそのURLを許可する

vercel.jsonはこんな感じ

{
  "builds": [
    {
      "src": "nuxt.config.js",
      "use": "@nuxtjs/vercel-builder",
      "config": {}
    }
  ]
}

Vercelの注意点

ドメインが複数発行されるようなので全てCORSに追加しといた方がいいかも。1つしか使わないなら1つだけ指定でもいいけど

今後やりたいこと

  • ソーシャルログイン
  • 記事のタグ付け
  • ユーザのフォロー機能
  • マークダウン対応
  • ユーザ登録はセッション周りはちゃんと作ってないので改善する?
  • 記事へのタグ付け
  • 画像や動画の埋め込み対応 。。。

などなどやろうと思えばいくらでもあるけどまあ、基本機能の実装とデプロイまでやったら割と気が済んでしまったな。。NuxtとFastAPIでちょっと遊びたかっただけだし

また気が向いた時に作るか

久しぶりにrails newしたので設定作業メモ

最近仕事でrails newを実行するとこからAPIサーバ2つ作った。その際の設定作業メモ

使った技術やバージョンざっくり

key val
Dockerのbaseイメージ ruby:3.0.2-alpine3.14
Railsのバージョン 6.1.4
DB 片方がPostgresql、もう一方がMySQL

注意点

  • rails newする時にオプションをつけないと初期設定が面倒
  • rubyのイメージがalpineの場合、postgre、mysqlへの接続用ライブラリをdockerイメージ内で追加する必要あり
  • 別のdockerコンテナで立ち上がってるサービスにアクセスするにはconfigでdockerのDNSからのアクセスを許可する必要あり

rails newする時にオプションをつけないと初期設定が面倒

デフォルトだと要件に合わないのでオプションをつけてrails newすると初期設定が大分楽になる

  • デフォルトだとDBがSQLite→今回はPostgreSQLMySQLを使いたい
  • デフォルトだとテストフレームワークがminitest→rspecを使いたい
  • デフォルトだとerbやアセットパイプラインなどフロントフロントエンド開発に必要なライブラリもインストールされる→今回APIサーバが欲しいだけなのでいれたくない

実行したコマンド

rails new samle-api-app --api -d postgresql --skip-test

-dオプションでDB接続に必要なライブラリの追加、--skip-testでminitestのインストールスキップ、apiオプションでフロントエンドライブラリのインストールをスキップしてくれる。

rubyのイメージがalpineの場合、postgre、mysqlへの接続用ライブラリをdockerイメージ内で追加する必要あり

Dockerfileに以下追加が必要。alpineの場合これがないとDBに接続できない

# PostgreSQLの場合
RUN apk update \
    && apk add \
    postgresql-client \
    postgresql \
    postgresql-dev

# MySQLの場合
RUN apk update \
    && apk add \
    mysql-client \
    mysql-dev

別のdockerコンテナで立ち上がってるサービスにアクセスするにはconfigでdockerのDNSからのアクセスを許可する必要あり

別のDockerコンテナにhost.docker.internalというDocker内のDNSで接続したかったんだけどやってみると403エラーになった。 調べたところRails6からconfigに設定追加しないと接続できないらしい。

config/environments/development.rbに以下追記

config.hosts << "host.docker.internal"

これでこんな感じでHTTPリクエスト送ったりできる

Faraday.get("http://host.docker.internal:3000/api/v1/sample")

参考

qiita.com

「CPUの創りかた」を読んだので軽く感想

積読になっていた「CPUの創りかた」を読んだので軽く感想書いてみる

book.mynavi.jp

どんな本か

  • LEDを光らせることができる4bit CPUを作成する手順を書いた本。C言語のプログラムを実行できます。。みたいなところまではやらない。
  • 入門本なので読みやすい。CPUの仕組みっていうテーマは難しめだけど、表紙や挿絵が萌え絵だったり文体が緩かったりで緊張感なく読める
  • 配線作業など電子工作の「実際の製作方法」は書いていない。実際に作らずに読んだだけだけどそれでも満足感はあった。
  • 書かれた年は2003年らしく割と古いけどあんまり気にならない
  • クロックとかプログラムカウンタとか命令デコーダとかレジスタとかなんとなく存在は知ってたけどよくわかってなかった仕組みが物理レベルでどう動いているかわかる
  • CPUが直接実行できる機械語論理回路で表現する方法がわかる

感想

CPU設計の仕事とかしたことないし、CPUに近いシステムプログラミング、組み込みプログラミングの分野でもないんだけどブラックボックスだった部分の知識を学ぶのは自信につながってくる気がする。

あと実益よりも単純に好奇心満たせて面白い。っていうのがあるんで読む理由はそれで十分かな。。

機械語をCPUがどう扱ってるのかはぼんやりわかったけど、C言語みたいな高級言語機械語にする部分とかも気になってきたな。。それはまた別の話でコンパイラの本とか読まないとわからなそう。

ECS環境で504Gatewayエラーに軽くハマった

ECS環境構築用のterraformの設定値を変数に切り出す作業をしてたんだけど、504Gatewayエラーが出るようになって軽くハマったのでメモ。

調べたところこの記事の内容とほぼ同じ事象だったらしくVPCのCIDRブロックを変数に切り出した際にセキュリティグループの更新を忘れていたらしい。。

yoshinorin.net

気づけばなんてことない内容なんだけど、ECSのイベントやCloudwatchにそれらしいエラーが何も出ないので気づきにくいな。。504だけしか情報がないと対応辛い。

resource "aws_security_group_rule" "hoge-ecs" {
  security_group_id = aws_security_group.hoge-ecs.id

  type = "ingress"
  from_port = 80
  to_port   = 80
  protocol  = "tcp"

  # ここを変数に書き換えるのを忘れてた。。
  cidr_blocks = [var.vpc_cidr_block]
}

Rails+ECS環境でのassets:precompileのタイミング

ECS環境でRailsを動かす時にassets:precompileを実行するタイミングとして、最初とりあえずコンテナ起動時に実行してたのを、Dockerイメージをbuildする時に実行するように変更したんだけど割と面倒だったのでメモ。

configパス以下を読んでいるらしくconfig以下の環境変数がないと落ちる。

例えばこういうのがないと実行できない↓(database.yml)

  username: <%= ENV.fetch("DATABASE_USERNAME") %>
  password: <%= ENV.fetch("DATABASE_PASSWORD") %>

環境変数はS3からpullする構成にしていたので、コンテナ起動時に実行すると環境変数が読み込まれているんだけど、build時には環境変数をファイルから読むことができない。

Dockerfileはgitに入れたいしに機密情報書くのやだなー。。と思ってたけど、assets:precompileってフロントエンドのアセット作ってるだけで、DBの認証情報とか必要ないのでbuild時は適当な文字列入れておけば問題なく動いた。

以下Dockerfileに追記

ENV DATABASE_USERNAME dummy
ENV DATABASE_PASSWORD dummy

VOLUMEの指定方法を間違えてコンパイルしたアセットが消える

assets:precompileの結果をnginxのコンテナと共有するためにDockerfileの中でVOLUME指定してたんだけど、VOLUMEはassets:precompileの後に指定しないとホストの内容が反映されてしまうらしくコンパイル後のassetが残らない。

# OK
RUN bundle exec rake assets:precompile RAILS_ENV=production
VOLUME /app/rails/public

# NG
VOLUME /app/rails/public
RUN bundle exec rake assets:precompile RAILS_ENV=production

これもコンテナ実行時にassets:precompileするとassetが消えずに残るので正常に起動できるが、build時だと消える。

地味に嫌だったなこれ。

AWS Internet Gatewayが原因でterraform destroyが終わらない時の対応メモ

terraformで構築したAWSWebサービス実行環境をterraform destroyコマンドで削除しようとするとInternet Gatewayを削除しようとするところでStill destroying...のメッセージがコンソールに表示され続けて実行が終わらなくなった。

対応

以下の対応で直りました。

terraformの修正

Nat GatewayとElasticIPにInternet Gatewayへの依存関係を追加するとうまくいく

resource "aws_nat_gateway" "movie-backend-nat-1a" {
  subnet_id     = aws_subnet.movie-backend-public-1a.id
  allocation_id = aws_eip.movie-backend-nat-1a.id

  depends_on = [aws_internet_gateway.movie-backend]

  tags = {
    Name = "movie-backend-1a"
  }
}
resource "aws_eip" "hoge-nat-1a" {
  vpc = true

  depends_on = [aws_internet_gateway.hoge]

  tags = {
    Name = "hoge-natgw-1a"
  }
}

手動で対応する場合

AWSの管理画面からInternet GatewayVPCからデタッチさせてもう一度terraform destroy実行。

「Go言語で作るインタプリタ」を読んだので内容整理

「Go言語で作るインタプリタ」を読んだ。親切な内容の本ではあるけどやっぱりインタプリタを作る。ってテーマ自体が難しく、読むのに苦戦したので内容書きながら整理してみる。

O'Reilly Japan - Go言語でつくるインタプリタ

ざっくり全体の流れ

  • プログラムを標準入力に渡す
  • 入力したプログラムを字句解析器(lexer)でトークンに分割する
  • 分割したトークンから抽象構文木(ast)をつくる
  • 抽象構文器を評価(eval)して結果を出力する

もう少し噛み砕いてみる

プログラムを構成するパッケージ

repl

「Read(読み込み)、Eval(評価)、Print(出力)、Loop(繰り返し)」の略。main関数からまず呼ばれるところ

token

ソースコードを分解して意味ごとにまとめたようなもの。「let a = 5」だったら「let」は変数宣言のletというキーワードで「a」は識別子で「=」はイコール分で「5」は整数。といった風に空白区切りで文字列を作ってそれぞれに意味を持たせる

lexer

字句解析器。ソースコードを受け取ってソースコードを表現するトークンを返す

parser

構文解析器。構文解析器とは入力データを受け取ってデータ構造を返すもの。今回の場合はプログラムを受け取って木構造のastを返却する。

ast

replで受け取った文字列を木構造にする。例えば「1 + 2」だったら「+」の左下に「1」がぶら下がっていて右下に「2」がぶら下がっているようなイメージ。ほとんどのコンパイラインタプリタの内部表現がこのデータ構造らしい。

まず木構造っていうのはこういうの👇(wikipediaja.wikipedia.org

evaluator

astを評価する。つまりここでastのデータ構造を元にプログラムとしての実行結果を導き出す

object

evalの評価結果。この本ではeval構造体中の評価関数に使われるinterfaceとして実装される

その他用語整理

プログラム中に出てくる英単語の意味を忘れがちで混乱したので書き出して整理

Node

astで出力した木構造の要素。式(Statement)か文(Expression)のどちらかで構成される。式は値を生成し、文はしない。

Identifier

識別子のこと。識別子は式の1種でもある

prefix

5 + 5とか、5 * 5*みたいな中置演算子

infix

-5-とか!foobarの!みたいな前置演算子

enviroment

変数に値を保存するために使う環境と呼ばれるもの。 プログラム内では文字列とobjectを関連付けるハッシュマップとして実装されていて、objectパッケージの中に定義されている構造体でlet文を評価する際に更新される。

上記の用語を踏まえてプログラムの流れを追ってみる

  • 標準入力を受け取る
  • 入力した文字列を使ってlexer構造体を初期化する
  • 初期化したlexer構造体を元にparser構造体を初期化する。
  • parse構造体のParseProgramに標準入力を読み込ませる
  • parse構造体の初期化メソッド(New)の中でlexer構造体のnextToken関数を呼び出してtoken構造体を初期化していく
  • ParseProgram内でparseStatement関数を呼び出しastの構造体を作成する。
  • parseStatement関数内で呼び出されるastを作成する。parseStatement関数はトークンのタイプをcase文で判定してlet式、return式、それ以外のいずれかが呼ばれる
  • ParseProgramを実行するとastの配列が返却される
  • 返却されたast(抽象構文木)の構造体をEvalメソッドで評価していく
  • Evalメソッドの中ではnodeの種別によって実行プログラムが分岐する。nodeの種別がLetstatementの場合はenviromentを更新して変数の値を保持する
  • evalでの評価が終わったら結果を標準出力に書き出して実行終了

よくわからないところ掘り下げてみる

抽象構文木の構造について

最終的にEvalメソッドに抽象構文木(ast構造体)を渡して結果を受け取るところでこのプログラムの処理は完結するんだけど、該当のプログラムを読むと以下のように書かれている

program := p.ParseProgram()
if len(p.Errors()) != 0 {
    printParserErrors(out, p.Errors())
    continue
}

evaluated := evaluator.Eval(program, env)
if evaluated != nil {
    io.WriteString(out, evaluated.Inspect())
    io.WriteString(out, "\n")
}

parsePrigramメソッドでast構造体(program)を作ってEvalメソッドに渡している。

Evalメソッドはastとenviromentを受け取ってオブジェクトを返す

func Eval(node ast.Node, env *object.Environment) object.Object

で、そのastの構造は例えば以下のようになっている(この本の内容に含まれているastのテストコードから抜粋)

これはlet myVar = anotherVar;という文字列を

   program := &Program{
        Statements: []Statement{
            &LetStatement{
                Token: token.Token{Type: token.LET, Literal: "let"},
                Name: &Identifier{
                    Token: token.Token{Type: token.IDENT, Literal: "myVar"},
                    Value: "myVar",
                },
                Value: &Identifier{
                    Token: token.Token{Type: token.IDENT, Literal: "anotherVar"},
                    Value: "anotherVar",
                },
            },
        },
    }

つまり頂点にStatements(式を複数保持する配列)があってその下にその式の種別が定義されている。式は種別ごとに異なった値を保持する構造体。上記の場合はlet式の構造体でトークンと変数名、変数の値を定義している。

Statementsというnodeが頂点にあり、そのNodeがLetStatementというnodeを持っていてそのLetStatementがToken、Name、Valueというnodeを持っている。という感じ。

enviromentについて

例えば以下のように入力を評価していく場合、前回の受付で更新した変数の状態を参照するのに必要となる。この場合、変数「a」、「b」、「c」、「d」それぞれの名前に何が保存されているかenviromentに記録される

let a = 5;

let b = a > 3;

let c = a * 99;

if (b) { 10 } else { 1 };

let d = if (c > a) { 99 } else { 100 };

d * c * a

enviromentがないと複数回入力した場合に以前の変数の格納結果を参照できない。

その他

計算の際のトークンの優先度はどう決める?

例えば1 + 2 * 2という式だと2 * 2を先に計算してから1を足す必要がある。これをどうするのか?

→優先度を定義した連想配列をparser構造体の中に定義していてそれを参照してEvalメソッドが適切な順序で評価できるようにastを出力する。

var precedences = map[token.TokenType]int{
    token.EQ:       EQUALS,
    token.NOT_EQ:   EQUALS,
    token.LT:       LESSGREATER,
    token.GT:       LESSGREATER,
    token.PLUS:     SUM,
    token.MINUS:    SUM,
    token.SLASH:    PRODUCT,
    token.ASTERISK: PRODUCT,
    token.LPAREN:   CALL,
    token.LBRACKET: INDEX,
}

それぞれの意味はtoken構造体に定義されている

const (
    ILLEGAL = "ILLEGAL"
    EOF     = "EOF"

    // Identifiers + literals
    IDENT  = "IDENT"  // add, foobar, x, y, ...
    INT    = "INT"    // 1343456
    STRING = "STRING" // "foobar"

    // Operators
    ASSIGN   = "="
    PLUS     = "+"
    MINUS    = "-"
    BANG     = "!"
    ASTERISK = "*"
    SLASH    = "/"

    LT = "<"
    GT = ">"

    EQ     = "=="
    NOT_EQ = "!="

    // Delimiters
    COMMA     = ","
    SEMICOLON = ";"
    COLON     = ":"

    LPAREN   = "("
    RPAREN   = ")"
    LBRACE   = "{"
    RBRACE   = "}"
    LBRACKET = "["
    RBRACKET = "]"

    // Keywords
    FUNCTION = "FUNCTION"
    LET      = "LET"
    TRUE     = "TRUE"
    FALSE    = "FALSE"
    IF       = "IF"
    ELSE     = "ELSE"
    RETURN   = "RETURN"
)

precedencesは下に行くほど優先度が高い。抽象構文木の中で優先度が高いものを評価の上位に割り当てることでEVal関数が適切な順序で処理できる。

Evalメソッドでやってること

Evalメソッドの中ではNodeのタイプごとに対応する関数を作ってそれぞれnodeを評価している。この1つ1つの分岐の中身を実装していく必要がある。

func Eval(node ast.Node, env *object.Environment) object.Object {
    switch node := node.(type) {

    // Statements
    case *ast.Program:
        return evalProgram(node, env)

    case *ast.BlockStatement:
        return evalBlockStatement(node, env)

    case *ast.ExpressionStatement:
        return Eval(node.Expression, env)

流石に1つ1つこの記事に書くのは辛いので割愛するが、例えば中置演算子の式計算は以下のような感じ。

func evalIntegerInfixExpression(
    operator string,
    left, right object.Object,
) object.Object {
    leftVal := left.(*object.Integer).Value
    rightVal := right.(*object.Integer).Value

    switch operator {
    case "+":
        return &object.Integer{Value: leftVal + rightVal}
    case "-":
        return &object.Integer{Value: leftVal - rightVal}
    case "*":
        return &object.Integer{Value: leftVal * rightVal}
    case "/":
        return &object.Integer{Value: leftVal / rightVal}
    case "<":
        return nativeBoolToBooleanObject(leftVal < rightVal)
    case ">":
        return nativeBoolToBooleanObject(leftVal > rightVal)
    case "==":
        return nativeBoolToBooleanObject(leftVal == rightVal)
    case "!=":
        return nativeBoolToBooleanObject(leftVal != rightVal)
    default:
        return newError("unknown operator: %s %s %s",
            left.Type(), operator, right.Type())
    }
}

左下と右下の値を引数に渡して演算していく。こういった評価関数を「式」と「文」のそれぞれの種別ごとに用意する必要がある。

以上