Odooを読む②

公式ドキュメントを読む。 開発者向けチュートリアルがあったので、とりあえずそれをやっていく。

事前準備

Docker内でCustom Moduleを使ってOdooを立ち上げる

実行時に--addons-pathを付与することで、アドオンをディレクトリから追加できる。 Dockerイメージにhostのディレクトリをマウントすれば同じことができるはず

 ./odoo-bin --addons-path="addons/,../enterprise/,../technical-training-sandbox" -d rd-demo

参考: Chapter2

チュートリアル

Docker環境で、Chapter3を進める。
Docker Hubの記載によると、/mnt/extra-addonにマウントすればカスタムアドオンを使えるらしい。

この場合、compose.yamlは以下のようになる。

services:
  web:
    image: odoo:16.0
    container_name: odoo_server
    volumes:
      - ./technical-training-sandbox:/mnt/extra-addons
    depends_on:
      - db
    ports:
      - "8069:8069"
  db:
    image: postgres:15
    container_name: odoo_postgres
    command: postgres -c log_destination=stderr -c log_statement=all -c log_connections=on -c log_disconnections=on
    env_file:
      - .env
    ports:
      - "5433:5432"
    volumes:
      - odoo_data:/var/lib/postgresql/data
volumes:
  odoo_data:

あとはドキュメントの記載に倣って進め、./technical-training-sandbox以下にaddonのファイルを配置することで、estateのアドオンを追加できた。

Odooを読む

業務システムのバックエンドの作り方を知りたいので、Odooから勉強する。

  1. 基本的な構成を理解
  2. 個々の機能を理解
  3. 機能追加時のDBの変更を確認する

基本的な構成

OwlによるSPAだが、JSONRPCを使っていて一般的なJSON色付けではなさそう。

docker composeで立ち上げる

  • 5433でローカルから接続する
  • クエリのログを確認できるようにする
services:
  web:
    image: odoo:16.0
    container_name: odoo_server
    depends_on:
      - db
    ports:
      - "8069:8069"
  db:
    image: postgres:15
    container_name: odoo_postgres
    command: postgres -c log_destination=stderr -c log_statement=all -c log_connections=on -c log_disconnections=on
    env_file:
      - .env
    ports:
      - "5433:5432"
    volumes:
      - odoo_data:/var/lib/postgresql/data
volumes:
  odoo_data:

デフォルトのDBのテーブル定義書をA5:SQL Mk-2で出力

アドオンを追加して、テーブル定義書の差分をとることで、追加されたテーブルを確認する
在庫アドオンを追加すると、以下のテーブルが追加される

 diff default_entities/index.html stock_entities/index.html | grep -oE "(\">).*?(</a>)" | sed -e 's/\">//g' | sed -e 's/<\/a>//g'

追加されたテーブル

account_analytic_account
account_analytic_applicability
account_analytic_distribution_model
account_analytic_line
account_analytic_plan
barcode_nomenclature
barcode_rule
base_module_install_request
base_module_install_review
confirm_stock_sms
crm_tag
crm_team
crm_team_member
digest_digest
digest_digest_res_users_rel
digest_tip
digest_tip_res_users_rel
email_template_attachment_rel
fetchmail_server
iap_account
iap_account_res_company_rel
ir_act_server_res_partner_rel
lot_label_layout
lot_label_layout_stock_picking_rel
mail_activity
mail_activity_rel
mail_activity_type
mail_activity_type_mail_template_rel
mail_alias
mail_blacklist
mail_blacklist_remove
mail_channel
mail_channel_member
mail_channel_res_groups_rel
mail_channel_rtc_session
mail_compose_message
mail_compose_message_ir_attachments_rel
mail_compose_message_res_partner_rel
mail_followers
mail_followers_mail_message_subtype_rel
mail_gateway_allowed
mail_guest
mail_ice_server
mail_link_preview
mail_mail
mail_mail_res_partner_rel
mail_message
mail_message_reaction
mail_message_res_partner_rel
mail_message_res_partner_starred_rel
mail_message_schedule
mail_message_subtype
mail_notification
mail_notification_mail_resend_message_rel
mail_resend_message
mail_resend_partner
mail_shortcode
mail_template
mail_template_mail_template_reset_rel
mail_template_preview
mail_template_reset
mail_tracking_value
mail_wizard_invite
mail_wizard_invite_res_partner_rel
message_attachment_rel
payment_country_rel
payment_icon
payment_icon_payment_provider_rel
payment_link_wizard
payment_provider
payment_provider_onboarding_wizard
payment_token
payment_transaction
phone_blacklist
phone_blacklist_remove
picking_label_type
picking_label_type_stock_picking_rel
portal_share
portal_share_res_partner_rel
portal_wizard
portal_wizard_res_partner_rel
portal_wizard_user
privacy_log
privacy_lookup_wizard
privacy_lookup_wizard_line
procurement_group
product_attribute
product_attribute_custom_value
product_attribute_product_template_rel
product_attribute_value
product_attribute_value_product_template_attribute_line_rel
product_attr_exclusion_value_ids_rel
product_category
product_label_layout
product_label_layout_product_product_rel
product_label_layout_product_template_rel
product_label_layout_stock_move_line_rel
product_packaging
product_pricelist
product_pricelist_item
product_product
product_product_stock_track_confirmation_rel
product_removal
product_replenish
product_replenish_stock_route_rel
product_supplierinfo
product_tag
product_tag_product_product_rel
product_tag_product_template_rel
product_template
product_template_attribute_exclusion
product_template_attribute_line
product_template_attribute_value
product_variant_combination
report_stock_quantity
resource_calendar
resource_calendar_attendance
resource_calendar_leaves
resource_resource
res_country_group_pricelist_rel
res_groups_spreadsheet_dashboard_rel
res_partner_autocomplete_sync
res_users_settings
res_users_settings_volumes
sms_composer
sms_resend
sms_resend_recipient
sms_sms
sms_template
sms_template_preview
sms_template_reset
sms_template_sms_template_reset_rel
snailmail_letter
snailmail_letter_format_error
snailmail_letter_missing_required_fields
spreadsheet_dashboard
spreadsheet_dashboard_group
stock_assign_serial
stock_backorder_confirmation
stock_backorder_confirmation_line
stock_change_product_qty
stock_conflict_quant_rel
stock_immediate_transfer
stock_immediate_transfer_line
stock_inventory_adjustment_name
stock_inventory_adjustment_name_stock_quant_rel
stock_inventory_conflict
stock_inventory_conflict_stock_quant_rel
stock_inventory_warning
stock_inventory_warning_stock_quant_rel
stock_location
stock_lot
stock_move
stock_move_line
stock_move_line_consume_rel
stock_move_move_rel
stock_orderpoint_snooze
stock_orderpoint_snooze_stock_warehouse_orderpoint_rel
stock_package_destination
stock_package_level
stock_package_type
stock_package_type_stock_putaway_rule_rel
stock_picking
stock_picking_backorder_rel
stock_picking_sms_rel
stock_picking_transfer_rel
stock_picking_type
stock_putaway_rule
stock_quant
stock_quantity_history
stock_quant_package
stock_quant_stock_request_count_rel
stock_quant_stock_track_confirmation_rel
stock_replenishment_info
stock_replenishment_option
stock_request_count
stock_return_picking
stock_return_picking_line
stock_route
stock_route_categ
stock_route_move
stock_route_packaging
stock_route_product
stock_route_warehouse
stock_rule
stock_rules_report
stock_rules_report_stock_warehouse_rel
stock_scheduler_compute
stock_scrap
stock_storage_category
stock_storage_category_capacity
stock_traceability_report
stock_track_confirmation
stock_track_line
stock_warehouse
stock_warehouse_orderpoint
stock_warn_insufficient_qty_scrap
stock_wh_resupply_table
team_favorite_user_rel
uom_category
uom_uom
utm_campaign
utm_medium
utm_source
utm_stage
utm_tag
utm_tag_rel

DBのログからstock_pickingをgrepしたもの

2023-07-02 06:36:28.977 UTC [36] LOG:  statement: SELECT "stock_picking_type".id FROM "stock_picking_type" WHERE ("stock_picking_type"."active" = true) AND ("stock_picking_type"."company_id" in (1)) ORDER BY  "stock_picking_type"."sequence" ,"stock_picking_type"."id"   LIMIT 80
2023-07-02 06:36:28.978 UTC [36] LOG:  statement: SELECT "stock_picking_type"."id" AS "id", "stock_picking_type"."color" AS "color", "stock_picking_type"."code" AS "code", COALESCE("stock_picking_type"."name"->>'ja_JP', "stock_picking_type"."name"->>'en_US') AS "name" FROM "stock_picking_type" WHERE ("stock_picking_type"."company_id" in (1)) AND "stock_picking_type".id IN (1, 2, 6)
                    SELECT min("stock_picking".id) AS id, count("stock_picking".id) AS "picking_type_id_count" , "stock_picking"."picking_type_id" as "picking_type_id"
                    FROM "stock_picking"
                    WHERE ((("stock_picking"."state" = 'draft') AND (("stock_picking"."state" not in ('done', 'cancel')) OR "stock_picking"."state" IS NULL)) AND ("stock_picking"."picking_type_id" in (1, 2, 6))) AND ("stock_picking"."company_id" in (1))
                    GROUP BY "stock_picking"."picking_type_id"
                    SELECT min("stock_picking".id) AS id, count("stock_picking".id) AS "picking_type_id_count" , "stock_picking"."picking_type_id" as "picking_type_id"
                    FROM "stock_picking"
                    WHERE ((("stock_picking"."state" in ('confirmed', 'waiting')) AND (("stock_picking"."state" not in ('done', 'cancel')) OR "stock_picking"."state" IS NULL)) AND ("stock_picking"."picking_type_id" in (1, 2, 6))) AND ("stock_picking"."company_id" in (1))
                    GROUP BY "stock_picking"."picking_type_id"
                    SELECT min("stock_picking".id) AS id, count("stock_picking".id) AS "picking_type_id_count" , "stock_picking"."picking_type_id" as "picking_type_id"
                    FROM "stock_picking"
                    WHERE ((("stock_picking"."state" = 'assigned') AND (("stock_picking"."state" not in ('done', 'cancel')) OR "stock_picking"."state" IS NULL)) AND ("stock_picking"."picking_type_id" in (1, 2, 6))) AND ("stock_picking"."company_id" in (1))
                    GROUP BY "stock_picking"."picking_type_id"
                    SELECT min("stock_picking".id) AS id, count("stock_picking".id) AS "picking_type_id_count" , "stock_picking"."picking_type_id" as "picking_type_id"
                    FROM "stock_picking"
                    WHERE ((("stock_picking"."state" in ('assigned', 'waiting', 'confirmed')) AND (("stock_picking"."state" not in ('done', 'cancel')) OR "stock_picking"."state" IS NULL)) AND ("stock_picking"."picking_type_id" in (1, 2, 6))) AND ("stock_picking"."company_id" in (1))
                    GROUP BY "stock_picking"."picking_type_id"
                    SELECT min("stock_picking".id) AS id, count("stock_picking".id) AS "picking_type_id_count" , "stock_picking"."picking_type_id" as "picking_type_id"
                    FROM "stock_picking"
                    WHERE (((("stock_picking"."scheduled_date" < '2023-07-02 06:36:28') AND ("stock_picking"."state" in ('assigned', 'waiting', 'confirmed'))) AND (("stock_picking"."state" not in ('done', 'cancel')) OR "stock_picking"."state" IS NULL)) AND ("stock_picking"."picking_type_id" in (1, 2, 6))) AND ("stock_picking"."company_id" in (1))
                    GROUP BY "stock_picking"."picking_type_id"
                    SELECT min("stock_picking".id) AS id, count("stock_picking".id) AS "picking_type_id_count" , "stock_picking"."picking_type_id" as "picking_type_id"
                    FROM "stock_picking"
                    WHERE ((("stock_picking"."backorder_id" IS NOT NULL AND ("stock_picking"."state" in ('confirmed', 'assigned', 'waiting'))) AND (("stock_picking"."state" not in ('done', 'cancel')) OR "stock_picking"."state" IS NULL)) AND ("stock_picking"."picking_type_id" in (1, 2, 6))) AND ("stock_picking"."company_id" in (1))
                    GROUP BY "stock_picking"."picking_type_id"

クエリした結果をみると、数字が多く何を意味しているかよくわからない。

 id | picking_type_id_count | picking_type_id 
----+-----------------------+-----------------
  9 |                     3 |               1
  1 |                     3 |               2

サーバーへのリクエストを見てみる

/web/dataset/call_kwへのPOSTがデータを取得しているように見える。

2023-07-02 07:00:25,722 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:25] "GET /web HTTP/1.1" 200 - 10 0.008 0.014
2023-07-02 07:00:26,349 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:26] "POST /web/action/load HTTP/1.1" 200 - 9 0.007 0.011
2023-07-02 07:00:26,425 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:26] "POST /mail/init_messaging HTTP/1.1" 200 - 32 0.019 0.023
2023-07-02 07:00:26,708 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:26] "POST /web/dataset/call_kw/res.users/systray_get_activities HTTP/1.1" 200 - 2 0.001 0.003
2023-07-02 07:00:26,800 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:26] "POST /web/dataset/call_kw/stock.picking.type/get_views HTTP/1.1" 200 - 2 0.002 0.011
2023-07-02 07:00:26,823 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:26] "GET /web/image/res.company/1/favicon HTTP/1.1" 304 - 5 0.003 0.008
2023-07-02 07:00:26,846 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:26] "POST /mail/load_message_failures HTTP/1.1" 200 - 9 0.022 0.012
2023-07-02 07:00:26,849 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:26] "POST /web/dataset/call_kw/stock.picking.type/web_search_read HTTP/1.1" 200 - 9 0.012 0.012
2023-07-02 07:00:26,852 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:26] "GET /web/image/res.company/1/favicon HTTP/1.1" 304 - 5 0.007 0.012
2023-07-02 07:00:27,140 1 INFO ? werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:27] "GET /mail/static/src/audio/ting.ogg HTTP/1.1" 206 - 0 0.000 0.001
2023-07-02 07:00:27,146 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:27] "GET /web/image/res.company/1/favicon HTTP/1.1" 304 - 5 0.002 0.006
2023-07-02 07:00:27,185 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:27] "GET /web/image?model=res.users&field=avatar_128&id=2 HTTP/1.1" 304 - 9 0.007 0.010

コードを読む

odoo/addons/web/controllers/dataset.py

from odoo.api import call_kw

    @http.route(['/web/dataset/call_kw', '/web/dataset/call_kw/<path:path>'], type='json', auth="user")
    def call_kw(self, model, method, args, kwargs, path=None):
        return self._call_kw(model, method, args, kwargs)

    def _call_kw(self, model, method, args, kwargs):
        check_method_name(method)
        return call_kw(request.env[model], method, args, kwargs)

odoo/odoo/api.py

def call_kw(model, name, args, kwargs):
    """ Invoke the given method ``name`` on the recordset ``model``. """
    method = getattr(type(model), name)
    api = getattr(method, '_api', None)
    if api == 'model':
        result = _call_kw_model(method, model, args, kwargs)
    elif api == 'model_create':
        result = _call_kw_model_create(method, model, args, kwargs)
    else:
        result = _call_kw_multi(method, model, args, kwargs)
    model.env.flush_all()
    return result

リクエストのペイロードはdeveloper toolで確認できる。

{
  "id": 3,
  "jsonrpc": "2.0",
  "method": "call",
  "params": {
    "model": "stock.picking.type",
    "method": "get_views",
    "args": [],
    "kwargs": {
      "context": {
        "lang": "ja_JP",
        "tz": "Asia/Tokyo",
        "uid": 2,
        "allowed_company_ids": [
          1
        ],
        "params": {
          "action": 371,
          "model": "stock.picking.type",
          "view_type": "kanban",
          "cids": 1,
          "menu_id": 204
        }
      },
      "views": [
        [
          false,
          "kanban"
        ],
        [
          false,
          "form"
        ],
        [
          false,
          "search"
        ]
      ],
      "options": {
        "action_id": 371,
        "load_filters": true,
        "toolbar": true,
        "mobile": true
      }
    }
  }
}

rpcでほぼ同じパラメータでリクエストしてるのを発見

odoo/addons/web/static/src/legacy/js/services/data_manager.js

            this._cache.views[viewsKey] = rpc.query({
                args: [],
                kwargs: { context, options, views: views_descr },
                model,
                method: 'get_views',
            })

これは、web.rpcで定義されている

var rpc = require('web.rpc');

この実際の定義が見つからない。addons/web/static/src/legacy/js/core/rpc.jsにはあるが、legacyとあるのでこれを使っているのかがわからない。

次回、公式ドキュメントから学んでいく https://www.odoo.com/documentation/16.0/developer/tutorials/getting_started/01_architecture.html

SQSの動作を検証する①(配信遅延・可視性タイムアウト)

TL;DR

  • 配信遅延を設定すると、メッセージを送信してから受信できるようになるまで配信遅延分のラグを生じさせる
  • 可視性タイムアウトを設定すると、メッセージを受信してから同じメッセージを再度取得するまでのラグを生じさせる
    • 受信したメッセージの可視性タイムアウトを変更することもできる

検証

ドキュメントに従って、Go SDKを用いてキューの動作を検証する。

サンプルコードを実行すると、
1. QUEUE_URLで指定されたキューに対してメッセージを送信
2. QUEUE_URLで指定されたキューからメッセージを受信
される。
なお、2.で受信した際にまだキューにメッセージがない場合、その旨を標準出力に出力してリトライする。

➜  sqs-example git:(main) go run main.go

出力

{"Attributes":{"SentTimestamp":"1678598881871"},"Body":"Information about current NY Times fiction bestseller for week of 12/11/2016.","MD5OfBody":"xxx","MD5OfMessageAttributes":"xxx","MessageAttributes":{"Author":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"John Grisham"},"Title":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"The Whistler"},"WeeksOn":{"BinaryListValues":null,"BinaryValue":null,"DataType":"Number","StringListValues":null,"StringValue":"6"}},"MessageId":"xxx","ReceiptHandle":"xxx"}

無事に出力された。

配信遅延

配信遅延を5秒に設定してみる。

再実行

➜  sqs-example git:(main) go run main.go

出力

message is empty
message is empty
message is empty
message is empty
message is empty
{"Attributes":{"SentTimestamp":"1678599920508"},"Body":"Information about current NY Times fiction bestseller for week of 12/11/2016.","MD5OfBody":"xxx","MD5OfMessageAttributes":"xxx","MessageAttributes":{"Author":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"John Grisham"},"Title":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"The Whistler"},"WeeksOn":{"BinaryListValues":null,"BinaryValue":null,"DataType":"Number","StringListValues":null,"StringValue":"6"}},"MessageId":"xxx","ReceiptHandle":"xxx"}

1秒ごとにリトライするので、配信遅延を5秒に設定すると5回リトライし、6回目にメッセージを取得する。

可視性タイムアウト

メッセージを取得後に可視性タイムアウトが始まる。
可視性タイムアウトが時間切れになるまでの間に取得したメッセージを削除しなかった場合、該当のメッセージは再度取得できるようになる。

取得後のメッセージを削除しないように変更し、再実行。
なお、可視性タイムアウトは5秒に設定している

➜  sqs-example git:(feature-visibility-timeout-test) go run main.go

出力

{"Attributes":{"SentTimestamp":"1678662317827"},"Body":"Information about current NY Times fiction bestseller for week of 12/11/2016.","MD5OfBody":"xxx","MD5OfMessageAttributes":"xxx","MessageAttributes":{"Author":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"John Grisham"},"Title":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"The Whistler"},"WeeksOn":{"BinaryListValues":null,"BinaryValue":null,"DataType":"Number","StringListValues":null,"StringValue":"6"}},"MessageId":"xxx","ReceiptHandle":"xxx"}
message is empty // 1秒
message is empty // 2秒
message is empty // 3秒
message is empty // 4秒
message is empty // 5秒
message is empty // 6秒
{"Attributes":{"SentTimestamp":"1678662317827"},"Body":"Information about current NY Times fiction bestseller for week of 12/11/2016.","MD5OfBody":"xxx","MD5OfMessageAttributes":"xxx","MessageAttributes":{"Author":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"John Grisham"},"Title":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"The Whistler"},"WeeksOn":{"BinaryListValues":null,"BinaryValue":null,"DataType":"Number","StringListValues":null,"StringValue":"6"}},"MessageId":"xxx","ReceiptHandle":"xxx"}
message is empty // 1秒
message is empty // 2秒
message is empty // 3秒
message is empty // 4秒
message is empty // 5秒
message is empty // 6秒
message is empty // 7秒
{"Attributes":{"SentTimestamp":"1678662317827"},"Body":"Information about current NY Times fiction bestseller for week of 12/11/2016.","MD5OfBody":"xxx","MD5OfMessageAttributes":"xxx","MessageAttributes":{"Author":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"John Grisham"},"Title":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"The Whistler"},"WeeksOn":{"BinaryListValues":null,"BinaryValue":null,"DataType":"Number","StringListValues":null,"StringValue":"6"}},"MessageId":"xxx","ReceiptHandle":"xxx"}

少なくとも5秒間は間をあけないと、新しいメッセージを取得できないことがわかる。

さらに、ChangeMessageVisibilityを用いて受信したメッセージの可視性タイムアウトを5秒ずつ延ばしてみる

➜  sqs-example git:(feature-expand-visibility-timeout-by-execution) ✗ go run main.go

出力

{"Attributes":{"SentTimestamp":"1678663932591"},"Body":"Information about current NY Times fiction bestseller for week of 12/11/2016.","MD5OfBody":"xxx","MD5OfMessageAttributes":"xxx","MessageAttributes":{"Author":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"John Grisham"},"Title":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"The Whistler"},"WeeksOn":{"BinaryListValues":null,"BinaryValue":null,"DataType":"Number","StringListValues":null,"StringValue":"6"}},"MessageId":"xxx","ReceiptHandle":"xxx"}
message is empty // 1秒
message is empty // 2秒
message is empty // 3秒
message is empty // 4秒
message is empty // 5秒
message is empty // 6秒
message is empty // 7秒
{"Attributes":{"SentTimestamp":"1678663932591"},"Body":"Information about current NY Times fiction bestseller for week of 12/11/2016.","MD5OfBody":"xxx","MD5OfMessageAttributes":"xxx","MessageAttributes":{"Author":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"John Grisham"},"Title":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"The Whistler"},"WeeksOn":{"BinaryListValues":null,"BinaryValue":null,"DataType":"Number","StringListValues":null,"StringValue":"6"}},"MessageId":"xxx","ReceiptHandle":"xxx"}
message is empty // 1秒
message is empty // 2秒
message is empty // 3秒
message is empty // 4秒
message is empty // 5秒
message is empty // 6秒
message is empty // 7秒
message is empty // 8秒
message is empty // 9秒
message is empty // 10秒
message is empty // 11秒
{"Attributes":{"SentTimestamp":"1678663932591"},"Body":"Information about current NY Times fiction bestseller for week of 12/11/2016.","MD5OfBody":"xxx","MD5OfMessageAttributes":"xxx","MessageAttributes":{"Author":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"John Grisham"},"Title":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"The Whistler"},"WeeksOn":{"BinaryListValues":null,"BinaryValue":null,"DataType":"Number","StringListValues":null,"StringValue":"6"}},"MessageId":"xxx","ReceiptHandle":"xxx"}

1回目のメッセージ取得は0秒、2回目は少なくとも7(>5)秒、3回目は11(>10)秒のインターバルがある。
これにより、取得したメッセージの可視性タイムアウトを操作できることがわかる。

また、受信したメッセージの可視性タイムアウトを0に設定することもできる。
コードを変更し、実行

➜  sqs-example git:(feature-terminate-visibility-timeout) go run main.go

出力

{"Attributes":{"SentTimestamp":"1678664647087"},"Body":"Information about current NY Times fiction bestseller for week of 12/11/2016.","MD5OfBody":"xxx","MD5OfMessageAttributes":"xxx","MessageAttributes":{"Author":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"John Grisham"},"Title":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"The Whistler"},"WeeksOn":{"BinaryListValues":null,"BinaryValue":null,"DataType":"Number","StringListValues":null,"StringValue":"6"}},"MessageId":"xxx","ReceiptHandle":"xxx"}
{"Attributes":{"SentTimestamp":"1678664647087"},"Body":"Information about current NY Times fiction bestseller for week of 12/11/2016.","MD5OfBody":"xxx","MD5OfMessageAttributes":"xxx","MessageAttributes":{"Author":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"John Grisham"},"Title":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"The Whistler"},"WeeksOn":{"BinaryListValues":null,"BinaryValue":null,"DataType":"Number","StringListValues":null,"StringValue":"6"}},"MessageId":"xxx","ReceiptHandle":"xxx"}
message is empty
{"Attributes":{"SentTimestamp":"1678664647087"},"Body":"Information about current NY Times fiction bestseller for week of 12/11/2016.","MD5OfBody":"xxx","MD5OfMessageAttributes":"xxx","MessageAttributes":{"Author":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"John Grisham"},"Title":{"BinaryListValues":null,"BinaryValue":null,"DataType":"String","StringListValues":null,"StringValue":"The Whistler"},"WeeksOn":{"BinaryListValues":null,"BinaryValue":null,"DataType":"Number","StringListValues":null,"StringValue":"6"}},"MessageId":"xxx","ReceiptHandle":xxx"}

間髪入れずにメッセージを取得できることが確認できた。

GCPの基本(リソース階層・IAM)

GCPリソース階層

  1. 組織: 会社ごとに一つ。AWSではOrganization
  2. フォルダ: 組織に含まれる概念。AWSではOU
  3. プロジェクト: 課金単位。AWSではアカウント
  4. リソース

IAM

  • 組織、フォルダ、プロジェクト、リソースに対しての権限を付与

ロール: 権限の集合
ポリシー: プリンシパルへのロールの割り当て

ロールと権限を紐づける画面は以下のような感じ

基本ロール

  • Browser: フォルダ、組織、IAMポリシーなどのプロジェクトの階層の閲覧権限。リソースの表示権限はない
  • Viewer: 閲覧者
  • Editor: Viewer権限に加え、リソースの編集権限
  • Owner: すべてのEditor権限に加え、リソースの権限管理、課金情報設定が可能

権限の一つ一つは、以下のように表される

まとめ

  • プロジェクト単位: Browser, Viewer, Editor, Owner
  • サービス単位: Viewer, Editor, Owner
    • サービスの一部についての権限も付与できる
  • ↑に加え、Conditionで最小権限の原則を実現する
  • 権限を剥奪しても、最大で80秒程度アクセス可能
    • AWSでは即座に剥奪されるが、GCPは非同期

microservices-demoをAzureにデプロイする その1

結論

Azureにデプロイできた。k8sマニフェストは何の修正もいらなかった。

概要

前回の記事ではAWSにデプロイしたが、Azureにもmicroservices-demoをデプロイしたい。

Kubernetesリソースの差分

CSP固有のサービスを使う箇所はtype: LoadBalancerのみ。
GKEとAWSでは、それぞれ以下のタイプのLBだった。

GKE: 外部TCP/UDPロードバランサ
AWS: Internet-Facing NLB

AzureはおそらくパブリックAzure Load Balancerだろう。

実行

AKSクラスタの作成
ドキュメントに従って、クラスタを作成する。

以下のように設定する。
その他は、すべてデフォルトの値でデプロイする。

②kubectlの設定

az login
az account set --subscription "従量課金"
az aks get-credentials --resource-group rg-aks-poc --name poc-cluster
k get nodes

NAME                                STATUS   ROLES   AGE   VERSION
aks-agentpool-xxxxxxxxx-vmss000000   Ready    agent   20m   v1.24.9
aks-agentpool-xxxxxxxxx-vmss000001   Ready    agent   19m   v1.24.9

Nodeを確認できた。

yamlの修正
特になし!!何もせずにAKSにデプロイできるということになる

k8sリソース作成

kubectl apply -f ./release/kubernetes-manifests.yaml

Podの作成完了

k apply -fしてからPodがすべて立ち上がるまでが明らかに早い気がする。20秒くらいですべてデプロイし終わった。

デプロイも完了

作られたリソースの確認

NSGは以下の通り

Azure Load Balancer

フロントエンドIP

バックエンドプール

アウトバウンドルール
インバウンドルール

ワーカーノード

microservices-demoをAWSにデプロイする その1

結論

AWSにデプロイすることはできた。
しかしFargateが使えない分、GKEと比べるとやや面倒1

概要

microservices-demoを用いて、Kubernetesのアプリケーションの検証をしたい。
GKEのクラスタを作成後、以下を実行することで実現できる。

kubectl apply -f ./release/kubernetes-manifests.yaml

これをAWSでもデプロイできるようにしたい。
※EKS Fargateにアプリケーションがデプロイされるだけで、Pod間の通信制御などは行われない

Kubernetesリソースの差分

リポジトリをクローンしてきて、kubernetes-manifests.yaml を確認する。

Ingressリソースはないため、L7ロードバランサはない。その代わり、以下のLoadBalancerを作成した際にGKEではLoadBalancer Serviceが作成される

apiVersion: v1
kind: Service
metadata:
  name: frontend-external
spec:
  type: LoadBalancer
  selector:
    app: frontend
  ports:
  - name: http
    port: 80
    targetPort: 8080

インターネットからのアクセスが可能なので、作成されるのは外部 TCP / UDP ロードバランサに該当する。

LoadBalancer サービスを作成すると、GKE により、その特性がサービス マニフェストのパラメータに依存する Google Cloud のパススルー ロードバランサが構成されます。 https://cloud.google.com/kubernetes-engine/docs/concepts/service-load-balancer?hl=ja

外部 TCP / UDP ロードバランサはSSLを終端しないロードバランサ(NLBのようなもの)らしい。

パススルー ロードバランサでは、クライアント接続を終端しません。ロード バランシングされたパケットは、パケットの送信元、宛先、ポート情報(該当する場合)が変更されずにバックエンド VM によって受信されます。

したがって、Internet-facingなNLBを作成して、Podにリクエストするようにすればとりあえずデプロイはできる。

AWSでtypeがLoadBalancerのServiceを作成するとCLBかNLBが作成される。この方法は推奨されていないが、一旦はこの方法を用いる。

実行

①EKSクラスタの作成

ドキュメントに従って、クラスタを作成する。

②EKS Nodeの作成

マネージド型ノードグループを作成

yamlの修正

kubernetes-manifests.yamlを以下のように修正(service.beta.kubernetes.io/aws-load-balancer-type: nlbアノテーションに追加)

kind: Service
metadata:
  name: frontend-external
+  annotations:
+    service.beta.kubernetes.io/aws-load-balancer-type: nlb
spec:
  type: LoadBalancer
  selector:
    app: frontend
  ports:
  - name: http
    port: 80
    targetPort: 8080

k8sリソース作成

kubectl apply -f ./release/kubernetes-manifests.yaml

Podの作成完了

デプロイも完了


  1. Fargateを用いようとするとAWS Load Balancer Controller add-onを使う必要があるなど、差分が大きい

runcを読む その1

モチベーション

ある日、Youkiというコンテナランタイムが出てきて衝撃を受けた。
自分もコンテナが何か知りたくなったので、runcを読むことにした。

正攻法と挫折

OCIの仕様を読み込むところから始める。

…と思ったが、抽象的過ぎてイメージがつかめない。
ふんわりと分かったこと

①以下のOperationがある

  • state
  • create
  • start
  • kill
  • delete

②コンテナランタイムを起動するためにはbundleconfig.jsonが必要

runcの大体の動作を把握する

実際に動かしてみる。

ubuntu18.04からbundleを取得する

docker run --rm -d --name ubuntu ubuntu:18.04 tail -f /dev/null
docker export ubuntu > rootfs.tar
docker kill ubuntu
mkdir rootfs
tar xf rootfs.tar -C rootfs

runc は、runc specで適当なconfig.jsonを生成できる。

runc spec

準備完了。runcを実行する。 確かにUbuntu18.04LTSのイメージが実行されている

sudo runc run ubuntu
# cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.6 LTS (Bionic Beaver)"
ID=ubuntu
(中略)
# exit
cat /etc/os-release 
NAME="Ubuntu"
VERSION="20.04.5 LTS (Focal Fossa)"
ID=ubuntu
(中略)

runc runを実行できた。 次回から、runc のコードリーディングを本格的に進めていく。