コンテンツにスキップ

Home

Route53 + ALB + EC2でWebサーバを公開する

EC2 に Web サーバ立てて https でアクセスするように設定しようと思ったら完全に忘れてしまってたので残しておく。 キャプチャとか残すのはめんどうなので、大雑把な手順のみ記載する。

ところどころワードに誤りがあるかもしれません。いい感じに読み替えましょう。

前提

  • ドメイン取得済み
  • Route53 の DNS 設定とか済んでいる

手順

  1. ACM で証明書を発行する
  2. EC2 インスタンスを作成して Web サーバを立てる
  3. EC2 に設定しているセキュリティグループのインバウンドに Web サーバのポートを開ける
  4. ターゲットグループを作成する
  5. プロトコル: http
  6. ポート: Web サーバと同じ番号を指定する
  7. ALB を作成する
  8. プロトコル: https
  9. ポート: 443
  10. ターゲット: 4で作成したもの
  11. デフォルトの証明書: 1 で作成したやつ
  12. Route53 でレコードを作成する
  13. レコード名: ドメイン名
  14. レコードタイプ: A
  15. エイリアス: On
  16. トラフィックのルーティング先: 5の ALB

Google Apps ScriptでScrapboxのフィードからsitemap.xmlを生成した

Scrapbox の public プロジェクトを検索エンジンにのせたくて、sitemap.xml を作成してみました。

Google Apps Script をあまり使ったことがないので、ChatGPT に聞いたら実装してくれました。少しだけ手直ししてますが、90%ぐらいは ChatGPT が書いたものです。Google Apps Script で実行すると Google Drive にsitemap.xmlというファイルが作成されます

function myFunction() {
  // RSS feed URL
  const feedUrl = "https://scrapbox.io/api/feed/mahs-note";

  // Base URL of the site
  const baseUrl = "https://scrapbox.io/mahs-note";

  // Fetch RSS feed
  const feed = UrlFetchApp.fetch(feedUrl).getContentText();
  const document = XmlService.parse(feed);
  const root = document.getRootElement();
  const channel = root.getChild("channel");
  const items = channel.getChildren("item");

  // Create sitemap.xml
  var sitemap = '<?xml version="1.0" encoding="UTF-8"?>\n';
  sitemap += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';

  // Add home page to sitemap
  sitemap += "<url>\n";
  sitemap += "<loc>" + baseUrl + "</loc>\n";
  sitemap += "<changefreq>daily</changefreq>\n";
  sitemap += "<priority>1.0</priority>\n";
  sitemap += "</url>\n";

  // Add entries to sitemap
  sitemap = items.reduce((accumulator, item) => {
    const link = item.getChildText("link");
    const updated = item.getChildText("pubDate");
    const updatedDate = new Date(updated);
    const updatedDateString = updatedDate.toISOString();

    accumulator += "<url>\n";
    accumulator += "<loc>" + link + "</loc>\n";
    accumulator += "<lastmod>" + updatedDateString + "</lastmod>\n";
    accumulator += "<changefreq>daily</changefreq>\n";
    accumulator += "<priority>0.8</priority>\n";
    accumulator += "</url>\n";
    return accumulator;
  }, sitemap);

  sitemap += "</urlset>";

  // Save sitemap.xml to Google Drive
  DriveApp.createFile("sitemap.xml", sitemap, "application/xml");
  Logger.log("Sitemap.xml created");
}

Python3.9 + AWS LambdaでRuntime.ImportModuleError

起きたこと

API Gateway + Lambda の構成でアプリを動かしていたら、API アクセス時にエラーとなってしまいました。

[ERROR] Runtime.ImportModuleError: Unable to import module 'app': cannot import name 'DEFAULT_CIPHERS' from 'urllib3.util.ssl_' (/var/task/urllib3/util/ssl_.py)
Traceback (most recent call last):

なんで?

urllib3 は標準ライブラリのため requirements.txt に記載していません。そのため、おそらく AWS 側で何かあったと考えられます。urllib3 のChangelogを見ると、最新バージョンが v1.x から v2.0 になっています。よくわかんないけど、おそらく AWS 側も v2.0 になったのが原因だと推測しました。

やったこと

requirements.txtに urllib3 を記載しました。

urllib3==1.26.15

これでとりあえず直りました。 そのうち Lambda で v2.0 で動くと思うので、定期的にチェックしていこうと思います。

Next.jsでYoutubeやTwitterのリンクを埋め込む

リンクだけあっても味気ないので、埋め込み対応をしてみました。

前提

  • remark, remark-rehype, rehype-stringify がインストール済みであること
  • npm i --save-dev remark remark-rehype rehype-stringify

対応方法

@remark-embedder/transformer-oembedを使って変換します。

設定方法

@remark-embedder/transformer-oembedをインストールします。

npm i --save-dev @remark-embedder/transformer-oembed

コンポーネント markdownToHtml.js が以下のようなものだとします:

```ts:markdownToHtml.js import { remark } from "remark"; import remarkRehype from "remark-rehype"; import rehypeStringify from "rehype-stringify"; import rehypeExternalLinks from "rehype-external-links"; import remarkEmbedder from "@remark-embedder/core"; import type { Config } from "@remark-embedder/transformer-oembed"; import oembedTransformer from "@remark-embedder/transformer-oembed";

export const markdownToHtml = async (markdown: string) => { const result = await remark() .use(remarkEmbedder, { transformers: [ [ oembedTransformer, { params: { maxwidth: 550, omit_script: true, lang: "ja", dnt: true, }, } as Config, ], ], }) .use(remarkRehype, { allowDangerousHtml: true }) .use(rehypeStringify) .process(markdown); return result.toString(); };

markdownToHtml.js を使ってページを表示します。

```ts:index.tsx
import { InferGetStaticPropsType, NextPage } from "next";
import { markdownToHtml } from "../../lib/markdownToHtml";

type Props = InferGetStaticPropsType<typeof getStaticProps>;

const Home: NextPage<Props> = ({ html }) => {
  return (
    <div
      dangerouslySetInnerHTML={{
        __html: html,
      }}
    ></div>
  );
};

export const getStaticProps = async () => {
  return {
    props: {
      html: await markdownToHtml("https://www.youtube.com/watch?v=dQw4w9WgXcQ"),
    },
  };
};

export default Home;

出力結果

YouTube の場合
<iframe
  width="356"
  height="200"
  src="https://www.youtube.com/embed/dQw4w9WgXcQ?feature=oembed"
  frameborder="0"
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
  allowfullscreen
  title="Rick Astley - Never Gonna Give You Up (Official Music Video)"
></iframe>
Twitter の場合
<blockquote
  class="twitter-tweet"
  data-width="550"
  data-lang="ja"
  data-dnt="true"
>
  <p lang="en" dir="ltr">
    Creators can now sign up and earn a living directly on Twitter in the EU,
    UK, and EEA.<br /><br />Tap on “Monetization” in settings to apply today.<br /><br />For
    a full list of available countries see our Help Center:
    <a href="https://t.co/YbBw0EVKqJ">https://t.co/YbBw0EVKqJ</a>
  </p>
  — Twitter (@Twitter)
  <a
    href="https://twitter.com/Twitter/status/1649507477325488131?ref_src=twsrc%5Etfw"
    >2023年4月21日</a
  >
</blockquote>

ツイートを以下のような埋め込みにする場合は、widgets.js を使って<iframe>に変換します。

https://twitter.com/Twitter/status/1649507477325488131

```ts:index.tsx import { useEffect } from "react"; import { InferGetStaticPropsType, NextPage } from "next"; import { markdownToHtml } from "../../lib/markdownToHtml";

type Props = InferGetStaticPropsType;

const Home: NextPage = ({ html }) => { useTweetEmbed(); return (

); };

export const getStaticProps = async () => { return { props: { html: await markdownToHtml( "https://twitter.com/Twitter/status/1649507477325488131" ), }, }; };

export const useTweetEmbed = () => { useEffect(() => { const script = document.createElement("script"); script.src = "https://platform.twitter.com/widgets.js"; document.body.appendChild(script);

return () => {
  document.body.removeChild(script);
};

}, []); };

export default Home; ```

Markdownのリンクを新規タブで開く

前提

  • remark, remark-rehype, rehype-stringify がインストール済みであること
  • npm i --save-dev remark remark-rehype rehype-stringify

対応方法

rehype-external-linksを使って target 属性を指定します

設定方法

rehype-external-linksをインストールします。

npm i --save-dev rehype-external-links

コンポーネント markdownToHtml.js が以下のようなものだとします:

import { remark } from "remark";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypeExternalLinks from "rehype-external-links";

export const markdownToHtml = async (markdown: string) => {
  const result = await remark()
    .use(remarkRehype, { allowDangerousHtml: true })
    .use(rehypeExternalLinks, {
      target: "_blank",
      rel: ["noopener noreferrer"],
    })
    .use(rehypeStringify)
    .process(markdown);
  return result.toString();
};

markdownToHtml.js を使ってページを表示します。

import { InferGetStaticPropsType, NextPage } from "next";
import { markdownToHtml } from "../../lib/markdownToHtml";

type Props = InferGetStaticPropsType<typeof getStaticProps>;

const Home: NextPage<Props> = ({ html }) => {
  return (
    <div
      dangerouslySetInnerHTML={{
        __html: html,
      }}
    ></div>
  );
};

export const getStaticProps = async () => {
  return {
    props: {
      html: await markdownToHtml(
        "[rehype](https://github.com/rehypejs/rehype)"
      ),
    },
  };
};

export default Home;

このように出力されます。

<div>
  <p>
    <a
      href="https://github.com/rehypejs/rehype"
      target="_blank"
      rel="noopener noreferrer"
    >
      rehype
    </a>
  </p>
</div>

Chaliceでリクエストパラメータベースのオーソライザーを作成する

まず、Chaliceの定義から。

Chalice は、python でサーバーレスアプリを書くためのフレームワークです。AWS Lambda を利用したアプリケーションを素早く作成し、デプロイすることができます。提供するものです。

  • アプリの作成、デプロイ、管理のためのコマンドラインツール
  • Amazon API Gateway、Amazon S3、Amazon SNS、Amazon SQS、およびその他の AWS サービスと統合するためのデコレーターベースの API。
  • IAM ポリシーの自動生成

Chalice のオーソライザーはトークンベースのオーソライザーしか用意されていません。リクエストパラメータベースで使いたい場合は、自分で拡張する必要があります。検索しまくっても全然出てこないので、サンプルコードを残しておきます。

from typing import Any, Dict, List, Optional

from chalice import Chalice
from chalice.app import Authorizer

app = Chalice(app_name="custom-request-base-authorizer")


class CustomAuthorizer(Authorizer):

    _AUTH_TYPE = "custom"

    def __init__(
        self,
        name: str,
        authorizer_uri: str,
        ttl_seconds: int = 300,
        header: str = "Authorization",
        invoke_role_arn: Optional[str] = None,
        scopes: Optional[List[str]] = None,
        identity_sources: Optional[List[str]] = [],
    ) -> None:
        self.name = name
        self._header = header
        self._authorizer_uri = authorizer_uri
        self._ttl_seconds = ttl_seconds
        self._invoke_role_arn = invoke_role_arn
        self.scopes = scopes or []
        self._identity_sources = identity_sources

    def to_swagger(self) -> Dict[str, Any]:
        # パラメータ名を変換
        # ヘッダーにしか対応してません
        # クエリパラメータなどに対応したい場合は、自分で実装してください
        identity_source = ",".join(
            [
                f"method.request.header.{identity_source}"
                for identity_source in self._identity_sources
            ]
        )

        swagger: Dict[str, Any] = {
            "in": "header",
            "type": "apiKey",
            "name": self._header,
            "x-amazon-apigateway-authtype": self._AUTH_TYPE,
            "x-amazon-apigateway-authorizer": {
                "type": "request",
                "identitySource": identity_source,
                "authorizerUri": self._authorizer_uri,
                "authorizerResultTtlInSeconds": self._ttl_seconds,
            },
        }
        if self._invoke_role_arn is not None:
            swagger["x-amazon-apigateway-authorizer"][
                "authorizerCredentials"
            ] = self._invoke_role_arn
        return swagger


region = "ap-northeast-1"   # リージョンを指定
lambda_arn = "" # LambdaオーソライザーのARNを指定
authorizer_uri = f"arn:aws:apigateway:{region}:lambda:path/2015-03-31/functions/{lambda_arn}/invocations"

authorizer = CustomAuthorizer(
    "CustomRequestBaseAuthorizer",
    authorizer_uri=authorizer_uri,
    identity_sources=["Authorization", "X-Request-Id"],
)


@app.route("/private", authorizer=authorizer)
def private():
    return {"hello": "world"}

さらに細かい設定をしたい場合は、x-amazon-apigateway-authorizer オブジェクトで確認してください。

参考

docusaurus + CloudFront + S3で直リンクOKにする

docusaurusCloudFront + S3でホスティングしたとき、直リンでアクセスできるパスが制限されています。

  • NG: https://example.com/foo
  • OK: https://example.com/foo/index.html
  • OK: https://example.com
  • ※デフォルトルートを設定したときのみ OK

https://example.com/fooを参照したい場合は、まずhttps://example.comにアクセスしてページ内リンクからhttps://example.com/fooへ移動しないといけません。

これが地味に使いにくい。URL をチームに共有するとき、アドレスバーをコピペするとhttps://example.com/fooとなるので、アクセスするときに手間がかかります。

Lambda@edgeを使って解決します。

手順

  1. Lambda を作成する
  2. Lambda のバージョンを作成する
  3. IAM ロール周りを修正する
  4. CloudFront をトリガーに設定する

1. Lambdaを作成する

us-east-1リージョンに Lambda を作成します。 ランタイムはNode.js 16.xを選択しました。 ロールは基本的な Lambda アクセス権限で新しいロールを作成で作成しました。

export const handler = async (event, context, callback) => {
  const request = event.Records[0].cf.request;

  const olduri = request.uri;

  if (isExistExtension(oldurl)) {
    // 拡張子があれば、そのまま返す
    return callback(null, request);
  }

  // 末尾にindex.htmlを付与する
  // - https://example.com/foo → https://example.com/foo/index.html
  // - https://example.com/foo/ → https://example.com/foo/index.html
  const newuri = (olduri.endsWith("/") ? olduri : olduri + "/") + "index.html";
  request.uri = newuri;

  return callback(null, request);
};

/**
 * 拡張子存在チェック
 */
function isExistExtension(url) {
  const ext = url.split(".").pop().trim();

  // 拡張子がない場合は、ext = url となる
  return ext !== url;
}

2. Lambdaのバージョンを作成する

右上にある「アクション」プルダウンや「バージョン」タブから「新しいバージョンを発行」をする。 入力項目は適当でよいです。

3. IAM ロール周りを修正する

1. Lambdaを作成するで作成したロールに対して色々やります。

ポリシーの追加

新たに以下のポリシーを追加します。[lambdaの関数名]には1. Lambda を作成する作成時の関数名を入力します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iam:CreateServiceLinkedRole",
        "cloudfront:UpdateDistribution"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": ["lambda:GetFunction", "lambda:EnableReplication"],
      "Resource": "arn:aws:lambda:us-east-1:371422377734:function:[lambdaの関数名]:*"
    }
  ]
}
信頼関係の変更

「信頼関係」タブがあるので、edgelambda.amazonaws.comを追加します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": ["lambda.amazonaws.com", "edgelambda.amazonaws.com"]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

4. CloudFront をトリガーに設定する

右上にある「アクション」プルダウンから「Lambda@Edge」へのデプロイを選択します。

item value
Distribution 対象の CloudFront
Cache behavior *
CloudFront event Origin request
Confirm deploy to Lambda@Edge On

これでデプロイすれば完了です。

参考

2023年の目標

今年の目標を立てました。最近気持ちが追いつかないので、頑張りすぎない程度の目標です。

  • CODE COMPLETE 上を買う
  • CODE COMPLETE 上を流し読みする
  • 転職に向けて何か動く
  • 月1は書籍を読む
  • 月1はブログを更新する
  • お酒をできるだけ控える

SQLAlchemyのSessionとin_transactionを調べた

SQLAlchemy でトランザクション管理をするにあたり、SQLAlchemy であつかう Session について理解する必要があったので検証しました。また、SQLAlchemy 1.4 Documentationに書いてあったin_transactionの挙動を確認したかったので、ついでに動作検証しました。

ソースコード

https://github.com/mt2m10/session-check-code-in-SQLAlchemy

検証内容

  • sessionmakerscoped_sessionの Session 生成の違い
  • in_transactionの挙動

検証環境

  • Windows 11
  • WSL2 (Ubuntu)

結果

SQLite を使って確認しました。

(session-check-code-in-SQLAlchemy) mah@DESKTOP-TQ41EI2:~/dev/session-check-code-in-SQLAlchemy$ python main.py
------------------------------------
main 開始
------------------------------------

トランザクションの確認
[$<sqlalchemy.orm.session.Session object at 0x7f0e284e5330>] transaction status: False
[$<sqlalchemy.orm.session.Session object at 0x7f0e284e6920>] transaction status: False

[$<sqlalchemy.orm.session.Session object at 0x7f0e284e5330>] transaction開始
[$<sqlalchemy.orm.session.Session object at 0x7f0e284e5330>] トランザクションを開始します

トランザクションの確認
[$<sqlalchemy.orm.session.Session object at 0x7f0e284e5330>] transaction status: True
[$<sqlalchemy.orm.session.Session object at 0x7f0e284e6920>] transaction status: False

[$<sqlalchemy.orm.session.Session object at 0x7f0e284e6920>] transaction開始
[$<sqlalchemy.orm.session.Session object at 0x7f0e284e6920>] トランザクションを開始します

トランザクションの確認
[$<sqlalchemy.orm.session.Session object at 0x7f0e284e5330>] transaction status: True
[$<sqlalchemy.orm.session.Session object at 0x7f0e284e6920>] transaction status: True

------------------------------------
main 終了
------------------------------------

------------------------------------
main 開始
------------------------------------

トランザクションの確認
[$<sqlalchemy.orm.session.Session object at 0x7f0e284e5600>] transaction status: False
[$<sqlalchemy.orm.session.Session object at 0x7f0e284e5600>] transaction status: False

[$<sqlalchemy.orm.session.Session object at 0x7f0e284e5600>] transaction開始
[$<sqlalchemy.orm.session.Session object at 0x7f0e284e5600>] トランザクションを開始します

トランザクションの確認
[$<sqlalchemy.orm.session.Session object at 0x7f0e284e5600>] transaction status: True
[$<sqlalchemy.orm.session.Session object at 0x7f0e284e5600>] transaction status: True

[$<sqlalchemy.orm.session.Session object at 0x7f0e284e5600>] transaction開始
[$<sqlalchemy.orm.session.Session object at 0x7f0e284e5600>] 既にトランザクションが開始されてます

トランザクションの確認
[$<sqlalchemy.orm.session.Session object at 0x7f0e284e5600>] transaction status: True
[$<sqlalchemy.orm.session.Session object at 0x7f0e284e5600>] transaction status: True

------------------------------------
main 終了
------------------------------------
  • sessionmakerは、sessionmaker()()のたびに新しいセッションを生成する
  • scoped_sessionは、複数回セッション生成しても同じセッションが返ってくる
  • in_transactionはトランザクション中ならTrue、トランザクション外ならFalseが返ってくる

pyenvでのinstall中にエラーが発生するときに対処したこと

対処方法

下記パッケージをインストールする。

sudo apt install -y build-essential zlib1g-dev libssl-dev libbz2-dev libreadline-dev libsqlite3-dev

環境

  • Windows 11
  • WSL2 (Ubuntu 20.04)
  • pyenv 2.3.4-2-g23559ee6

エラー内容

pyenvコマンドを使って Python をインストールすると、途中でエラーが発生しました。

$ pyenv install 3.10.6
Downloading Python-3.10.6.tar.xz...
-> https://www.python.org/ftp/python/3.10.6/Python-3.10.6.tar.xz
Installing Python-3.10.6...

BUILD FAILED (Ubuntu 20.04 using python-build 2.3.4-2-g23559ee6)

Inspect or clean up the working tree at /tmp/python-build.20220906122257.2953
Results logged to /tmp/python-build.20220906122257.2953.log

Last 10 log lines:
checking for python3... python3
checking for --enable-universalsdk... no
checking for --with-universal-archs... no
checking MACHDEP... "linux"
checking for gcc... no
checking for cc... no
checking for cl.exe... no
configure: error: in `/tmp/python-build.20220906122257.2953/Python-3.10.6':
configure: error: no acceptable C compiler found in $PATH
See `config.log' for more details

このエラーはbuild-essentialをインストールすると解決できるのですが、次に別のエラーが発生します。

すべてのエラーをここに載せるのもめんどうです。
とりあえず、対処方法のコマンドを実行すればエラーが解消できるのでお試しください。

参考

  • https://pouhon.net/pyenv-error/2009/