Lightsail: CLIENT_UNAUTHORIZED [769]

事象

Lightsailコンソールからのログインでエラー。DistroUbuntu 18.04LTSを20.04LTSにアップグレードしたもの。

CLIENT_UNAUTHORIZED [769]

原因

新しいバージョンのSSH (8.2) から、SHA-1ハッシュのサポートが標準ではサポートされなくなっているため。 旧バージョンからアップグレードした場合、公開鍵の署名がSHA-1のままになっている。

Future deprecation notice

It is now possible[1] to perform chosen-prefix attacks against the SHA-1 hash algorithm for less than USD$50K. For this reason, we will be disabling the "ssh-rsa" public key signature algorithm that depends on SHA-1 by default in a near-future release.

This algorithm is unfortunately still used widely despite the existence of better alternatives, being the only remaining public key signature algorithm specified by the original SSH RFCs.

調査

Ubuntuの場合、 sshd のログは、 /var/log/auth.log にある。 中身を見てみると、以下のログを発見。

Jan  1 02:38:22 ip-172-26-5-22 sshd[1066128]: userauth_pubkey: certificate signature algorithm ssh-rsa: signature algorithm not supported [preauth]
Jan  1 02:38:22 ip-172-26-5-22 sshd[1066128]: Connection closed by authenticating user ubuntu 54.240.XXX.XXX port 2190 [preauth]

対応

いったん、SSH-RSAを許可。でも、クラックされるからサポートされなくなったわけで、本来は公開鍵を変えるべき。

# diff sshd_config sshd_config_backup-2021-01-01
125,126d124
<
< CASignatureAlgorithms ^ssh-rsa

参考情報

以下のAWS Developer Forumにある事象と同じと思われる。

[https://forums.aws.amazon.com/thread.jspa?threadID=307027:What causes a "CLIENT_UNAUTHORIZED [769]" login error?]

これはHashicorpの例だけど、同じ原因みたい。

sarunas-krisciukaitis.medium.com

Docker + WSL2

Docker + WSL2での開発ってできるの?という疑問を調べてみたメモ。

Dockerのコンテナを動かす部分と、イメージを格納する部分は、WSL2を使うようにできる。WSL2がある状態でDockerをインストールすると、勝手にそれを使うように設定してくれる。

インストール後にWSL2のDistroを見てみると、2つ追加されている。

PS C:\Users\choge> wsl -l -v
  NAME                   STATE           VERSION
* WLinux                 Running         2
  docker-desktop-data    Running         2
  docker-desktop         Running         2

この状態で何らかのコンテナを起動すると、WSL2上にイメージがダウンロードされて、WSL2のKernelでそのイメージが動くみたい。よって、やってることは基本的にはVM立ててその中でDockerを動かしているのと同じ。ただし、スピードはだいぶ速いらしい。

開発方法としては、大きく二つありそうな感じ:

  • あくまでも実行環境 (or ビルド環境) としてDockerを使う。
    • 例えばDocker Composeとかを使って、DB+APPをまとめて動かす、といった感じ。
    • Windows環境のIDEで作ったイメージをLinux環境で動かせる? 基本は必要なものをADDして、RUNでビルドすればよいと思うけど、ものによってはビルド用のコンテナが必要なのかも?
  • 開発環境自体としてDockerを使う。
    • この場合、OS+Programming言語+Middleware+依存ライブラリなどをイメージにしておいて、そのイメージをもとにしたコンテナでシェルを起動して開発する感じ。
    • VS Codeを使っているのであれば、なんかいい感じのオプションで開発できるっぽい。

よって、Javaのアプリの開発環境をDockerでまとめて配布、とかは若干無駄な感じがする。結局IDEとか必要だし。これとかこれも、HelloWorld動かしてるだけだし・・・

NodeJSとかPythonとか、VS Codeで十分開発までできちゃうスクリプト言語の開発にはいいかも。特に pip の挙動が微妙なPythonにはちょうど良いかも。

参考資料

大体が出来上がったものをコンテナで実行してる感じ。下のアメブロの記事は、VS Codeを使ってコンテナ内で開発までやってる。

Dockerで始める Java EE アプリケーション開発 for JJUG CCC 2017

『VSCodeとDockerでSpring Boot + PostgreSQL開発環境を作る(1)』

以下のはPythonとかの開発

IntelliJ+Dockerでlocalを汚さず開発 - Qiita

MacでAWSのCodeCommitをcloneできない

事象

git cloneすると、以下のエラーメッセージ。実際には当該レポジトリは存在している。

fatal: repository 'https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/sample/' not found

環境

  • OS: macOS High Sierra (10.13.3)
  • git: OS Xに付いてるやつ
  • AWS: 複数のアカウントあり
    • おそらく、複数のIAMユーザーだったり、profileを切り替えたりしても発生する。

原因

OS XのKeychainが勝手に最初に使った認証情報をキャッシュしているため。

対策

/Applications/Xcode.app/Contents/Developer/usr/share/git-core/gitconfig (OS Xのデフォルト) または、/usr/local/git/etc/gitconfig (brewで入れたやつ)を編集。

以下をコメントアウトするか、[credential "https://github.com"]のようにサイトを指定する。

[credential]
   helper = osxkeychain

参考資料

基本的に以下に書いてあることそのまま。エラーメッセージでググっても出てこないんだよね。

docs.aws.amazon.com

Spring + Vue.jsでページングする

Spring Dataを使うと、Pagingをいい感じにやってくれる。こいつとVue.jsを使って、いい感じにページングをしてみる。

API

SpringのRestControllerPageableが全てよしなに取り計らってくれる。

@RequestMapping("/api")
@RestController
public class APIController {
    
    @Autowired
    private ItemRepository repo;

    @GetMapping("/items")
    public Page<Item> allItems(Pageable pageable) {
        return repo.findAll(pageable);
    }
}

これで、以下のようにページングしやすい形でResponseを返してくれる。

{
  content: [ ... (省略) ...],
    pageable: {
      sort: {
        sorted: false,
          unsorted: true
      },
        offset: 200,
        pageSize: 20,
        pageNumber: 10,
        unpaged: false,
        paged: true
    },
    totalPages: 21,
    totalElements: 410,
    last: false,
    size: 20,
    numberOfElements: 20,
    first: false,
    sort: {
      sorted: false,
        unsorted: true
    },
    number: 10
}

英語だけど以下の記事がわかりやすい。HATEOAS 対応しようとしたけど、面倒くさそうなので後回し。

www.baeldung.com

画面

上記のAPIで返ってきた結果を表示する。Vue.js始めたてなので、もっとスマートなやり方はあるかも。

まずはページングするためのComponent。単純に全ページ分を表示する。20ページを超えたあたりからちょっと見づらくなるかも。

個人的にポイントだと思ったのは以下。

  • RestControllerが返すJSONのうち、content以外をとりあえずpagerというpropsにして渡す。その後の制御は大体これで事足りる。
  • JSONに含まれるページ番号は0始まりのインデクスだが、v-for="page in page.totalPages"でループするpageは1始まり。
  • 各ページがアクティブ(=現在ページ)かどうかを判定するために、インラインで{'active': page - 1 === pager.number}としている。
  • ページ遷移をするときは、propsで渡されたpager自身を更新する必要があるので、親Componentとやり取りする必要がある。調べた限りだと以下の方法がありそう。
    • 親Componentをpropsで子Componentにわたす
    • $emitでイベントを発行し、そいつを親Componentで捕まえる (ここの解説)
Vue.component('pager-block', {
    props: ['pager'],
    data: {},
    computed: {},
    methods: {
        changepage: function(page) {
            this.$emit('changepage', page);
        }
    },
    template: `
<nav aria-label="Page navigation example">
  <ul class="pagination">
    <li class="page-item" :class="{disabled: pager.first}">
        <a class="page-link" href="#">Previous</a>
    </li>
    <template v-for="page in pager.totalPages">
        <li class='page-item' :class="{'active': page - 1 === pager.number}">
            <a class="page-link" href="#" :class="page" @click="changepage(page - 1)">{{page}}</a>
        </li>
    </template>
    <li class="page-item" :class="{disabled: pager.last}">
        <a class="page-link" href="#">Next</a>
    </li>
  </ul>
</nav>
    `,
});

親Componentはこんな感じ。

  • methodsでページ読み込みの処理を作っておいて、mountedから呼び出す
  • GETリクエストのパラメーターはURLSearchParamsを使う。普通に文字列として追加してもよい。
  • 上の方ので発行したイベントを、@changepageでキャッチする。
  • propsで渡すpagerJSONの中身をぶち込むときに、普通にインデックスを使うとVue.jsが認識できない。this.$setを使う必要がある (ここ参照)
       <pager-block :pager="pager" @changepage="changepage"></pager-block>
var app = new Vue({
      el: '#app',
      data: function() {
          return {
            itemList: [],
            pager: {},
          };
      },
      methods: {
        loadPage: function(page) {
            var urlParams = new URLSearchParams();
            urlParams.set("page", page);
            fetch("/api/items?" + urlParams.toString())
                .then(response => {return response.json();})
                .then(data => {
                    console.log(data);
                    for (var k in data) {
                        if (k === 'content') {
                            continue;
                        }
                        this.$set(this.pager, k, data[k]);
                    }
                    for (var i = 0; i < data.content.length; i++) {
                        this.itemList.push(data.content[i]);
                    }
                });
        },
        changepage: function(page) {
            this.itemList.splice(0);
            this.loadPage(page);
        }
      },
      mounted: function() {
          this.loadPage(0);
      },
});

MongoDBで集計した上にガッチャンコするやつ

やりたいこと

MongoDBにざっくり以下のようなデータがある。

{ groupId: "aaa", lang: "en", data: "hoge" },
{ groupId: "aaa", lang: "ja", data: "ほげほげ" },
{ groupId: "bbb", lang: "en", data: "fuga" },
{ groupId: "bbb", lang: "fr", data: "titi" },
...

こいつをもとに、以下のように集計したデータを作りたい。

{ groupId: "aaa", langs: ["en", "ja"] },
{ groupId: "bbb", langs: ["en", "fr"] },
...

やりかた

以下の通り。$group$pushを使えば良い。

> db.hoge.aggregate([
...   { $group: { _id: "$groupId", langs: { $push: "$lang" } } },
...   { $sort: { groupId: 1 } }
... ])
{ groupId: "aaa", langs: ["en", "ja"] },
{ groupId: "bbb", langs: ["en", "fr"] },

参考資料

その他

例のDynamoDBガッツリ使いたい案件、ある程度柔軟に検索するのであればMongoDBも視野に入る???

DynamoDBでQuery

一つのパーティションの中に26万件データが入っている状態でQueryをいっぱい試してみたけど、1回4KBくらいの読み込みだと、0.01秒くらい。

大したことないな。

>>> sparse.item_count
263132
>>> times = []
>>> for i in range(100):
...     before = time.time()
...     res = sparse.query(
...         TableName='sparse-data',
...         KeyConditionExpression='sid=:sid AND unixtime BETWEEN :start_dt AND :end_dt',
...         ExpressionAttributeValues={
...             ':sid': 'ABC-1234567',
...             ':start_dt': decimal.Decimal(start_dt.timestamp()),
...             ':end_dt':   decimal.Decimal(end_dt.timestamp())
...         },
...         ReturnConsumedCapacity='TOTAL'
...     )
...     times.append(time.time() - before)
...     print('consumed capacity: {0:.2f}'.format(res['ConsumedCapacity']['CapacityUnits']))
...     time.sleep(0.5)
>>> sum(times) / len(times)
0.019462196826934813

AWS ことはじめ その5 (DynamoDB - Part3)

前回に続いて、今度はデータの読み出し。

choge.hatenadiary.com

やっぱりIDとタイムスタンプ以外でも読み出したいとか、貯めておくのは一旦1ヶ月でいいとか、色々要望がぶれ始めているのでDynamoDBを本当に使うのか雲行きが怪しくなってきている。IDとタイムスタンプ以外で、という件は、Secondary Indexである程度対応できる気はしている。逆に貯めるのを1ヶ月だけでよい、というのは、そもそもKVSを使うほどの容量なのか?という話になってくる。悩ましいところ。

データ格納イメージ

復習。パーティションキーをもとに分散してデータを格納して、同一パーティション内はソートキーで順番にデータを並べる。

f:id:ch0ge:20180523231346p:plain

1. Primary Keyでアクセス

DynamoDB での項目の操作 - Amazon DynamoDB

最も高速。RDB的に言うとUNIQUE INDEXでアクセスしているようなもの。 f:id:ch0ge:20180523225354p:plain

2. Queryでのアクセス:

クエリの操作 - Amazon DynamoDB

パーティションキーで範囲を絞込み、その中で検索を行う。キャパシティユニットは、結果的に返ってくる項目のサイズによる。つまり、たくさんの属性を持つ項目を読み取ると、1属性だけを返したとしても全属性分のキャパシティユニットが消費される。 f:id:ch0ge:20180523230936p:plain

>>> sparse.attribute_definitions
[{'AttributeName': 'sid', 'AttributeType': 'S'}, {'AttributeName': 'unixtime', 'AttributeType': 'N'}]

>>> sparse.key_schema
[{'AttributeName': 'sid', 'KeyType': 'HASH'}, {'AttributeName': 'unixtime', 'KeyType': 'RANGE'}]

>>> res = sparse.query(
...     TableName='sparse-data',
...     Limit=10,  # 取得する件数を制限(任意)
...     ReturnConsumedCapacity='TOTAL', # 消費したキャパシティユニットの総量を返す
...     KeyConditionExpression='sid = :val', # 条件式。値は":sid"のようなPlaceholderにする
...     ExpressionAttributeValues={   # ↑で指定した Placeholderに対する値を記載する
...         ':val': 'ABC-1234567',
...     },
...     ConsistentRead=False,  # (結果整合性ではなく)強い整合性を持った読み込みを行うか
...     ScanIndexForward=False  # ソートキーの最初から読み込む(True)か、最後から読み込む(False)か
... )

>>> res['Count'], res['ScannedCount'] # 'Count': 結果の件数、 'ScannedCount': 実際に読み込んだ件数。今回は特にフィルタリングしていないので一致する。
(10, 10)

>>> res['ConsumedCapacity']  # 消費したキャパシティユニット。1回の結果整合性のみの読み取り、かつ、1KB以内なので、消費されたRCUは0.5
{'TableName': 'sparse-data', 'CapacityUnits': 0.5}

>>> res['LastEvaluatedKey'] # クエリーが読み取った最後の項目。複数のオペレーションに分けるときは、この値をExclusiveStartKeyに含める。
{'sid': 'ABC-1234567', 'unixtime': Decimal('1684587390')}

>>> res2 = sparse.query(
...     TableName='sparse-data',
...     ReturnConsumedCapacity='TOTAL',
...     KeyConditionExpression='sid = :sid AND unixtime BETWEEN :start_dt AND :end_dt',
...     # ソートキーの条件を指定。条件は一つだけなので、〜以上〜以下のようなケースはBETWEENを使う
...     ExpressionAttributeValues={
...         ':sid': 'ABC-1234567',
...         ':start_dt': decimal.Decimal(start_dt.timestamp()),  # Decimal!
...         ':end_dt'  : decimal.Decimal(end_dt.timestamp())
...     }
... )

ちなみに、boto3の(ドキュメント)http://boto3.readthedocs.io/en/latest/reference/services/dynamodb.html#DynamoDB.Client.queryのExpressionAttributeValuesというセクションに条件で指定する値の書き方が書いてある。が、これ多分間違ってる。例が

    { ":avail":{"S":"Available"}, ":back":{"S":"Backordered"}, ":disc":{"S":"Discontinued"} }

となっているが、正しくは

     {":avail": "Available", ":back": "Backordered", ":disc": "Discontinued" }

のように、値部分はdictではなく単なる文字列とかを指定する。

3. Scanでのアクセス

テーブルをスキャンする - Amazon DynamoDB

テーブル全部を取得する。普通はこれをやる意味はない。全件取得しても大したことない小さなテーブルだとか、そもそもどこかにエクスポートするから全件取得したいとか、そういう特殊なケース向け。

f:id:ch0ge:20180523231139p:plain