shibatch's journey

日々考えていることをつらつら書くだけです

2026年 フィンランド・バルト三国旅行

リトアニアで乗った気球

今年も夏に海外旅行へ行ってきました。去年より長く、10泊11日の旅です。

日程

  • フィンランド🇫🇮(ヘルシンキ) 8/12 ~ 8/13
    • ↓フェリーで移動🚢
  • エストニア🇪🇪(タリン) 8/13 ~ 8/15
    • ↓高速バス(Lux Express)で移動🚌
  • ラトビア🇱🇻 (リガ / ユルマラ) 8/15 ~8/18
    • ↓電車(vivi→LTG)で移動🚃
  • リトアニア🇱🇹(ヴィリニュス / トラカイ) 8/18 ~ 8/21

位置的にはこのへん

国ごとの特色

今回フィンランドは1泊2日で駆け足での旅行でしたが、エストニア、ラトビア、リトアニアの三国は一応2泊以上は取るスケジュールとしました。日本ではバルト三国の違いはピンとこないけれども、実際行ってみるとかなり国民性の違いがあることを感じました。

まずエストニアのタリンはフィンランドが近いこともあり、割と北欧気質というかゆったりしているけれども、観光客にも優しい丁寧な国という感じ。ラトビアのリガでは行ったタイミングがちょうどリガ市の生誕825年祭(Riga825)の真っ最中で町中が大盛り上がりで開放的で、昨年行ったスウェーデンのストックホルムを思い出すような陽気さを感じました。リトアニアは街中工事していて、経済発展の勢いを感じた反面、あまり観光客慣れしていないというか、接客もこなれていない感じでイメージしている東欧の雰囲気がありました。三者三様で面白かったです。

ダークツーリズム

昨年北欧3ヶ国(デンマーク、ノルウェー、スウェーデン)を旅行した際に感じましたが、北欧はウクライナ支援の姿勢を明確にしています。それは街中でもウクライナと自国の旗と一緒に飾られていたりすることが特別ではなく、日常の風景として溶け込んでいたからです。

どの行政機関もだいたいウクライナの旗が立ってる

折しもロシア大統領が択捉島を訪問したと言うニュースが報じられたタイミングで、歴史的にソ連に蹂躙された過去を持つバルト三国はどういう姿勢なのか、3ヶ国とも批判の声明を出していたが、どのような背景があるのかを知る事は今回貴重な経験になると思い、ラトビアでは占領博物館、リトアニアでは占領と自由闘士の博物館を見てみました。こういった人類の負の歴史を学ぶような旅行をダークツーリズムと呼ぶそうです。

リガの占領博物館

リガの占領博物館では、第二次世界大戦時にソ連に占領されラトビアの人がどのようにどのような扱いを受けたかを数字を交えて、データとして事実を並べることに重点を置いた展示に見えました。だからといって凄惨さがないわけではなく、ラトビアの人がどのような苦難を受けたか、そして展示の最後には戦争によってラトビアを出て行かなければならなかった人の人数や命を落とした人数が飛び込んでくる展示になっており、考えさせられるものがありました。

リガの占領博物館

ヴィリニュスの占領と自由闘士の博物館

ヴィリニュスの占領と自由投資の博物館では、リガよりも直接的な展示がなされていました。ソ連に反抗したパルチザンのエピソードやシベリアに送られたリトアニアの人がいかにして、自国の文化を苦労して守っていったかということが強いメッセージとして出されていました。特に地下は実際にKGBが使用していた刑務所がそのまま公開されており、生々しい雑居房や拷問の部屋も公開されており、生産な様子が伝わってきて、ちょっと長く滞在するには体調が悪くなるほど。

地上は説明の展示、地下はKGBの施設をそのまま公開してる

リガとヴィリニュスはひとつ大きな違いがあって、リガはあくまで「ソ連」を非難する内容なのに対して、ヴィリニュスはソ連を非難しつつも、今のロシアも直接非難する内容になっていてスタンスが違う。

今のウクライナ侵略を直接非難する内容

ラトビアはロシア系住人も多い(らしい)ので現状の避難は抑制的で、リトアニアはそうでもないので現状のロシアに直接届くメッセージになっていました。そういえば択捉島訪問に対するメッセージもリトアニアがバルト三国で一番早かった。こういうところも色が違うなと思いました。

どのバスでもVilnius❤️Ukrainaと表示してある

初めてやることをやる

今まで大体の旅行は観光名所回っておいしいごはん食べておわり、だったんだけれど今回はアクティビティを織り交ぜてみました。たとえば……

フィンランド: サウナ

人気のサウナ施設Löyly へ行ってまいりました

www.loylyhelsinki.fi

ここ予約取るのが大変で、行ける日が埋まってていったん諦めたんだけれど直前でぽろぽろキャンセルが出たので運良く入れました

次行くときが来るのかよくわからないけれど、一旦メモしておくと

  • 欧州のサウナ文化として水着は必須(日本から持ってった)
  • サンダルはなくても困らないがあった方が動きやすい
  • バスタオルを1枚貸してもらえるけれども、自分でセームタオルを持っていっていると、さっと体をふけて便利
  • お水はちゃんとサービスがあるので、持っていかなくてもOK

4種類位のサウナがあって、それぞれ特色があって楽しかったです。日本でサウナ入るよりもよりサウナが好きになりました。

飛行機の長旅疲れにちょうどよかった

ラトビア: オペラ鑑賞

リガはオペラが有名と地球の歩き方に書いてあったので、ギリギリだったけれども席を予約してみてみることに。10ユーロから50ユーロ位まであって、真ん中位の30ユーロ位の席にした。

www.opera.lv

上の黒いディスプレイに字幕が出る

滞在2日間で違う演目でどちらをみるか迷ったんだけれど、初めての鑑賞なので古典的名作(とAIが言っていた)椿姫を鑑賞してみました。さすがに全く予習なしで見るのは無謀なので、内容をかじってから鑑賞。セリフはフランス語で上の字幕にラトビア語と英語が書いてある形式でした。英語もそんなに難しいものではなかったので、所々意味をつまみながらと演者の表示や歌の調子といったもので、鑑賞を終了。初めて見たけれども、内容の悲恋さに加え、演者の息のあった歌唱や煌びやかさにめちゃめちゃ引き込まれてしまいました。最後カーテンコールの時は心から拍手してしまうほど。今までなじみがなかったけれども、食わず嫌いは良くないなぁと思いました。これぞ海外で「新しいことをやってみる」価値かなと。細かいところは追えていない部分もあるので日本でも見たい。

ラトビア: ケメリ国立公園 / ユルマラリゾート

ラトビアでは首都のリガの他にも、そこから電車で一時間、そこから徒歩50分のケメリ国立公園へ行ってまいりました。ケメリ国立公園は広大な湿地帯で、栄養分が少ないため低木が地平線近くまで広がっている圧巻の景色でございました。ただ駅からめちゃ遠かったな!Bolt(ライドシェア)も捕まらなくて行くのも帰るのも大変。ただめちゃよい景色だった

ケメリ国立公園

その後リゾート地のユルマラでリゾートホテル体験。位置的には [リガ] <--- 電車30分 --> [ユルマラ] <-- 電車30分 --> [ケメリ] って感じ。ほとんどAirBnBに泊まっているので、旅の途中でホテルに泊まるのを挟むことでリフレッシュできました。

www.hoteljurmala.com

このリゾートホテルすごいと思ったのはオンラインチェックインしていたらフロントに行く必要なくて、スマホが部屋の鍵になるってところ。日本にはないシステムでした…! サウナやジャグジーもあって、連泊することを想定しているような感じ。欧米の人はこういうところでリフレッシュするんだ〜と良い体験でございました。

ユルマラ

リトアニア: 熱気球体験

リトアニアは欧州で数少ない都市で気球に乗れる都市らしく、じゃぁ乗ったことないし乗ってみるべ!と旅行開始後に予約をしてみました。

balloon.lt

連絡はWhatsAppでやりとりするので英語苦手でも問題なし。料金は相乗りで149ユーロ。ちょこっと調べたけれどトルコも同じくらいの価格で、日本で乗る場合はもっと高いかな、という感じ。

気球は7人乗りで、ギリシャから来た人やイングランドから来た親子とカタコト英語でちょっと話しました。ライブでよくある「このメンバーでやることは二度とない」が熱気球でもありましたね!全員で撮影した写真はよい思い出になりました

降りた後にセレモニーで乾杯があるの知らなかった、楽しい

いったんまとめ

他にもごはん、特に地元の特産品でのごはん美味しかったし、市場巡りも楽しかったです。もう一本、今回の旅行、大きな失敗はなかったけれど良いところ悪かったところがあるので自分用のメモを書きたい。

2025年 北欧旅行総括と知見メモ

ストックホルムの夜景

旅行の概要

旅行予約について

  • 航空券は出発8日前に取得。今まででいちばんギリギリ
  • その他、ホテルや国間移動のバスや列車は出発3~4日前にまとめて取った
    • 相当ギリギリ
  • 航空券はいつも通りskyscanner
  • コペンハーゲンストックホルムはhotels.com、オスロAirbnbの民泊を取った

今までの個人旅行の予約日の比較。今回がかなりギリギリなことがわかる。取れはしたけれどもっと早く取ったほうが安かったかも?

個人旅行の比較

生成AI大活躍

  • 今回場所選定から日によっての動き方までAI(Gemini)に相談しながらやった
  • 「8泊9日で北欧3カ国を歴訪するならどのようなスケジュールにすればよい?」で国と都市とスケジュールを大まかに絞る
  • 飛行機の時刻、予約したホテルと鉄道、バスの時刻を伝えて詳細なスケジュールを立ててもらう
  • 土日は空いている時間が短い施設なども考慮した観光先を出力してくれる
  • 希望を伝えると都度修正してくれる(コペンハーゲンではチボリ公園で遊ぶ時間を長くとりたい、など)
  • 割と余裕のあるスケジュールを組むので前倒しして動くこともけっこうできる

Gemini相談

  • 旅先でわからないものを撮影してこれは何?と伝えるとめっちゃ詳しく教えてくれる
    これは何

esim助かる

  • 出国前にholaflyという海外で使えるesimを契約した。自分が契約した区間だとデータ使い放題で5000円ほど。
  • これがめちゃくちゃ体験が良かった。出国前にesimをアクティベートして現地着いたらONにするだけ。
  • 特に途切れるなど不満はなく、国をまたいでもローミングし直してくれる。
  • 海外で他の日本人の方で他のesimサービスを使っている方はうまくアクティベートできていなかったので、holaflyは安定して使えたので他人に勧められる。 Buy eSIM for International Travel | Holafly

細かいメモ

  • 航空会社アプリでのモバイルチェックインがめちゃくちゃ便利
    • アプリでパスポートの表面写真撮ってRFIDスマホで読み込めば完了
    • 当日カウンターへ行かなくてもそのまま検査場へ行ける(預け入れ荷物がない場合)
  • 飛行機の座席は窓際がよいか通路側がよいか
    • 今回行きは窓際、帰りは通路側を取って比較してみた
    • 精神的には誰にも気にせずトイレへ行ける通路側の方がラクかも
    • ただし窓際の人がトイレへ出るときに移動する必要があるのがデメリット
    • つまりこうすると良い
      • 寝ることを主体にする場合は窓側
      • 起きておくことを主体にする場合は通路側
    • 寝る主体か起きる主体かはいく先の時差ぼけ調整を基準に考えると良い
  • 移動中のスマホ充電について
    • 飛行機はUSB-Aしか端子がなかったためスマホの充電はモバイルバッテリー頼りだった。ただ特に困ったことはなかったし、今後はよりUSB-Cに統一されていくだろうから今後気をつける必要はなさそう。なお、長距離バスはUSB-Cとコンセント共についていて、長距離電車はコンセントのみついていた。

総額

  • 今回8泊9日で71万円ほど
  • 歴代の個人旅行で過去最高額になった(これでも食費を控えるなどはしたが…)
  • 他の北欧歴訪したブログを見ても取り立てて高いわけではない。つまり北欧自体が高い。
  • ギリギリで航空券を取ったので(これが往復35万円した)、これが数ヶ月前に取れると安くできるかも…ただなかなかねー、難しい

総括

ものすごくいろいろな場所へ行ったので内容については省くけれども、天気に恵まれたし大きなトラブルもなくめちゃ良い旅行になった。毎年行ける値段ではないけれど。

きみもAWS LoadBalancer Controller芸人になろう

AWSKubernetesingressコントローラーとしてAWS LoadbBanacer Controllerがあります。 これ、使うとALBが自動で起動されて使うことができます。つまり、ALBの仕様に完全に依存する形のIngress Controllerです。

ALBは高機能な反面、ingressとして使うと少々クセがある感じがしたので覚え書きとしてまとめていきます。 仕様に制限があるのでその制限を回避する方法も載せていきます。

なお、現時点での最新版、v2.7での情報です。

1. リスナールールを「条件」「アクション」に分けてアノテーションで記載する

ALBにはリスナールールという仕組みがあり、AWS LoadBalancer Controllerではこの仕組みを制御できます。

Application Load Balancer のリスナールール - Elastic Load Balancing

リスナールールはアクセスを「条件」と「アクション」に分けて記述する機能です。 AWS LoadBalancer Controllerを使うとIngressでこのリスナールールを表現することができます。

条件

条件はalb.ingress.kubernetes.io/conditions.${conditions-name} で、ここにSourceIPやらリクエストメソッドはGETやらの条件をつけることができます。

アクション

アクションはalb.ingress.kubernetes.io/actions.${action-name}で、ここにリダイレクトするなり、特定のKuernetes のservice resourceに転送するなりのアクションをつけることができます。

これは実際におこなった設定なのですが「password変更のRESTful APIを塞ぎたい」場合、以下のようなアノテーションingressに設定することになります。

    alb.ingress.kubernetes.io/conditions.deny-password-login: >
      [{"Field":"http-request-method", "httpRequestMethodConfig":{"Values":["POST"]}}]
    alb.ingress.kubernetes.io/actions.deny-password-login: >
      {"Type": "fixed-response", "fixedResponseConfig":{"statusCode":"403"}}

conditionでPOST、actionで403のレスポンスを返す、としています。conditionとactionで名前を合わせることで一対の設定となります。 これをingressのruleに設定します。以下のようにすれば、example.com/api/session に対して上記のアノテーションを設定することができます。

  rules:
  - host: example.com
    http:
      paths:
      - backend:
          service:
            name: deny-password-login  👈annotationで設定した名前を指定する
            port:
              name: use-annotation
        path: /api/session
        pathType: Exact

2.「条件」の個数制限に注意する

ALBの仕様として、「条件」は5つまでしか設定できません。

Application Load Balancer のクォータ - Elastic Load Balancing

この個数を超えるとingressを設定してもALBには設定は反映されません。AWS LoadbBanacer Controllerのログにはエラーが出力されますが、Ingress ControllerのPodやIngressは見かけ上異常なく見えるので注意が必要です。

3. pathTypeの仕様に注意する

IngressではpathTypeによって完全一致か前方一致かを指定できます。

Ingress | Kubernetes

  • ImplementationSpecific(実装に特有): このパスタイプでは、パスとの一致はIngressClassに依存します。Ingressの実装はこれを独立したpathTypeと扱うことも、PrefixExactと同一のパスタイプと扱うこともできます。
  • Exact: 大文字小文字を区別して完全に一致するURLパスと一致します。
  • Prefix: /で分割されたURLと前方一致で一致します。大文字小文字は区別され、パスの要素対要素で比較されます。パス要素は/で分割されたパスの中のラベルのリストを参照します。リクエストがパス p に一致するのは、Ingressのパス p がリクエストパス p と要素単位で前方一致する場合です。

簡単に言えばExactは完全一致、Prefixは前方一致で、AWS LoadbBanacer Controllerを使った場合でもその仕様は変わりません。

ただし、pathTypeはALBの「条件」に設定されるため、前述の個数制限は留意する必要があります。

たとえば、先ほど出した例は Exactを使いましたが、

  rules:
  - host: example.com
    http:
      paths:
      - backend:
          service:
            name: deny-password-login 
            port:
              name: use-annotation
        path: /api/session
        pathType: Exact  👈これね

この場合は自動的に2つ「条件」を使うことになります。

  • HTTPホストヘッダーが example.com
  • パスパターンが/api/session

そのため、他の条件(リクエストメソッドやソースIPなど)は3つつけることができます。

さて、Prefixはどうなるのでしょう?

  rules:
  - host: example.com
    http:
      paths:
      - backend:
          service:
            name: deny-password-login 
            port:
              name: use-annotation
        path: /api/session
        pathType: Prefix

この場合は3つ「条件」を使います。

  • HTTPホストヘッダーが example.com
  • パスパターンが/api/session
  • パスパターンが/api/session/*

そのため、他の条件(リクエストメソッドやソースIPなど)は2つになります。

いちばんわかりにくいImplementationSpecificの場合はどうなるのでしょうか? これは指定したそのままがALBに設定されます。

        path: /api/session
        pathType: ImplementationSpecific

↑たとえばこの場合はExactと同じように、2つ「条件」を使うことになります。example.com/api/session には適用されますが example.com/api/session/hoge には適用されない設定です。

        path: /api/session/*
        pathType: ImplementationSpecific

これはExactでもPrefixでもない指定になって、2つ「条件」を使うことになります。 example.com/api/session には適用されず、example.com/api/session/hoge には適用されます。

こういう感じなので、AWS LoadbBanacer Controller を使う場合は設定が思った通りに反映されているかALBを確認しながらやると良いです。

4. 制限は回避できる

ここまで条件の指定は5つまでしかない、ということを口すっぱく言ってきましたが、この仕様は回避することができます。 たとえばソースIPをたくさん条件に指定したい場合は以下のように複数アノテーションを作ればよいです。

    alb.ingress.kubernetes.io/actions.allow-from-certain-ips1: >
      {"Type": "forward", "ForwardConfig": {"TargetGroups": [{"ServiceName": "exampleservice", "ServicePort": "80"}]}}
    alb.ingress.kubernetes.io/conditions.allow-from-certain-ips1: >
      [{"Field":"source-ip", "sourceIpConfig": {"values":["8.8.4.0/24", "8.8.8.0/24", "8.34.208.0/20"]}}]
    alb.ingress.kubernetes.io/actions.allow-from-certain-ips2: >
      {"Type": "forward", "ForwardConfig": {"TargetGroups": [{"ServiceName": "exampleservice", "ServicePort": "80"}]}}
    alb.ingress.kubernetes.io/conditions.allow-from-certain-ips2: >
      [{"Field":"source-ip", "sourceIpConfig": {"values":["8.35.192.0/20", "23.236.48.0/20", "23.251.128.0/19"]}}]
  - host: example.jp
    http:
      paths:
      - backend:
          service:
            name: allow-from-certain-ips1
            port: 
              name: use-annotation
        path: /example/*
        pathType: ImplementationSpecific
  - host: example.jp
    http:
      paths:
      - backend:
          service:
            name: allow-from-certain-ips2
            port: 
              name: use-annotation
        path: /example/*
        pathType: ImplementationSpecific

これは特定のソースIPからexample.jp/example/*にアクセスした場合はそのままexampleserviceに通すことを意味しています。


どうでしょう?AWS LoadBalancer Controllerの深淵を少しは覗くことができたでしょうか??

なんだか難しそうには見えますが、慣れてしまうと割と高機能なALBの機能を活用できるので便利です。

誰か困っている人に届きますように……

2023年ふりかえり

今年を雑に振り返りたい

今年、業務面を振り返ると自律して動けるようになって信頼を積み上げることができたという感じ。去年まではなんだかんだ「人の歩いてきた道」をなぞるところもあったと思うが今年は自社内の課題を見出しそれに対して解決していく動きが強くできたと思う。反面ちょっと技術力でなんとかする、みたいな動きにはちょっと乏しかったかもしれない。

組織やチームがダイナミックに変わった年でもあった。退職した人もいるし新しく入った人もいるし、今まであまり話さなかった人とも話すようになった。社内にいながら変化を感じられるのは刺激になるしとても良い。あと6年経つとチーム内でもだいぶ古株になってきているところにちょっと戸惑いがある。でもそれも信頼を積み上げてこられた証拠なのかもしれない。

あまり年の初めに抱負を立てることはないのだけれど、来年はもうちょっと技術力で〜した、という場面が増えると良い。登壇する機会も増やせたらな

今年も良い方々に恵まれました。来年もよろしくお願いいたします。

Herokuのデータベースを(Kubernetes Podを経由して)Amazon Auroraにデータ移行してみた

この記事はGMOペパボエンジニア Advent Calendar 2023の10日目になります🎄

最近PaaSであるHerokuにあるサービスをAWS(Amazon EKS on Fargate)で動かそうとしています。まだ検証段階ではあるものの、動作するところまではこぎつけました。HerokuとAWSを以下の組み合わせで移設しようとしています。

どれもいざやってみるとさほど困難さはなかったのですが(最初にEKSを構築してIngressを動かすほうがよほど手間がかかった)、Herokuで採用しているデータベースであるPostgreSQLに関しては知識ゼロから始めたもので学びが多かったのでやったことを書いていきます。雰囲気だけでも感じていただけたら嬉しいです!

なお、移行して動いた、という段階なのでパフォーマンスがどうなったといった視点は今回はないです…ご了承ください。

Auroraの構築については省略します😃他にも解説記事はたくさんあるかと思いますので

下準備

※私は作業PCがMacなのでMacの場合です

Herokuでデータベースのバックアップを取得するためにはHerokuのコマンドを実行することになるのですが、内部的にPostgreSQLCLIコマンド─psql─をKickするため、最初にpsqlコマンドを利用できるようにする必要があります。psqlコマンドを使うためには以下からHerokuに構築しているPostgreSQLのバージョンに合ったものをインストールします。

postgresapp.com

インストールが終わったら、psqlコマンドを使うためにパスを通してあげます。自分は雑にzshrcに以下を入れました。

% tail -3 ~/.zshrc

# psql PATH
export PATH=${PATH}:/Applications/Postgres.app/Contents/Versions/latest/bin

psqlコマンドが打てるようになったら準備完了です😎

% which psql
/Applications/Postgres.app/Contents/Versions/latest/bin/psql

Heroku PostgreSQLのデータベースを覗いてみる

ではHerokuのPostgreSQLをちょこっと覗いてみましょう。ログインはheroku pg:psql です。オプションを指定しない場合はHerokuでサービスが参照しているデータベースの所有者ユーザーになります。

% heroku pg:psql
--> Connecting to postgresql-example-98765
psql (13.13, server 13.12 (Ubuntu 13.12-1.pgdg20.04+1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

heroku-database::DATABASE=>

PostgreSQLではメタコマンドと言って短いコマンドでデータベースの内容をわかりやすく出力してくれる仕組みがあります。データベース一覧とその所有者を表示する\lを打ってみます。

heroku-database::DATABASE=> \l
                                                     List of databases
      Name      |     Owner      | Encoding |   Collate   |    Ctype    |                Access privileges
----------------+----------------+----------+-------------+-------------+--------------------------------------------------
 ma72h7wq4Z4uhlk | bkreikhp6iwaly | UTF8     | en_US.UTF-8 | en_US.UTF-8 | bkreikhp6iwaly=CTc/bkreikhp6iwaly               +
                |                |          |             |             | internal_utility_u1jk56dcsvf7q8=c/bkreikhp6iwaly
 postgres       | postgres       | UTF8     | en_US.UTF-8 | en_US.UTF-8 | postgres=CTc/postgres
 template0      | postgres       | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres                                     +
                |                |          |             |             | postgres=CTc/postgres
 template1      | postgres       | UTF8     | en_US.UTF-8 | en_US.UTF-8 | postgres=CTc/postgres
(4 rows)

※データベース名、ユーザー名は架空のものです

いちばん上に表示されているma72h7wq4Z4uhlkが実際に使用しているデータベース、つまり今回データ移行するものです。HerokuのPostgreSQLのデータベース名はランダム文字列なんですね。右隣に表示されているOwnerの名前もランダム文字列です。 このデータベース名は後ほど使うので控えておきます。

バックアップ(スナップショット)をとる

それではリストア元のデータベースのバックアップを取得します。一般的な話になるのですが、データベースのバックアップをバックアップは2種類に大別できまして、コールドバックアップとホットバックアップがあります。コールドバックアップはデータベースのソフトウェア(今回の場合はPostgreSQL)を停止させてデータのバックアップを取得するものですが、PaaSであるHerokuはコールドバックアップは想定していないようです。

そこでホットバックアップを取得するのですが、これはとても簡単で以下のコマンドを実行するだけです。

% heroku pg:backups:capture

これは内部的にはPostgreSQLpg_dump​コマンドを実行し、先ほど確認したma72h7wq4Z4uhlkデータベースの論理バックアップを取得しています。バックアップファイルはHeroku内に保管されます。

コマンド実行後Webコンソールでみても取得されていることがわかります。

Heroku backup

※今回の場合は2MBと極小のデータですが、20GB以上のデータだとタイムアウトする可能性があるからデータベースのブランチ(耳慣れない表現ですがデータベースの書き込みがないレプリカ)を作成することを推奨しています。

バックアップファイルを取得する

Herokuに格納されたバックアップデータをローカルPCにダウンロードします。出力ファイル名を指定しない場合はlatest.dumpという名前のファイルがダウンロードされます。

% heroku pg:backups:download
Getting backup from ⬢... done, #1
Downloading latest.dump... ████████████████████████▏  100% 00:00 1.94MB
% ls -l latest.dump
-rw-r--r--  1 shibatch  staff  2030927 11 29 19:04 latest.dump
shibatch@PM-GMXJX5060K sandbox-eks % ls -lh latest.dump

リストア用のPodを立てる

今回、EKSとAuroraはサブネットを分けている構成にしています。

構成図
今回はEKSにHerokuのWeb Dynosに相当するPodを構築するため、EKSのあるサブネットからAuroraのあるサブネットに疎通する必要があります。

そこで疎通のテストも兼ねて、EKS上にAurora(PostgreSQL)をCLI操作するためのPodを立てて、そのPodにバックアップデータを転送してリストアすることにしました。 EKSに以下の通り、PostgreSQLが使えるPodを立てます。このコンテナイメージは内部でPostgreSQLを起動するためパスワードの環境変数指定が必要ですが、今回psqlコマンドを打ちたいだけなのでパスワードはなんでもよいです。

% kubectl run postgres-cli --image=postgres:13-alpine --env="POSTGRES_PASSWORD=test" 
% kubectl get pod
NAME            READY   STATUS    RESTARTS   AGE
postgres-cli   1/1     Running   0          2m49s

Podにリストアしたデータを転送する

KubernetesにはPodに対してファイルを転送するコマンドがあるので今回はそのコマンドで転送します(こんなコマンドあるの知らなかった)。 転送完了したらPodにログインしてファイルの存在確認します。

% kubectl cp ./latest.dump postgres-cli:/tmp/sandbox-pg.dump
% k exec -it postgres-cli -- /bin/bash
postgres-cli:/# ls -l /tmp
total 1984
-rw-r--r--    1 502      dialout    2030927 Nov 29 10:12 sandbox-pg.dump

カンのいい読者なら察したでしょうが、このコピーは検証用データベースの2MBのバックアップデータだからシュッとできたものの、本番用のGB単位のデータだったら転送に時間がかかる&Podのtmp領域の空き容量の関係でこんなうまくいかない可能性があります。この問題はこれから考えます😃

Auroraにデータベースのあれこれを作成する

あとはリストアしていくだけ…と言いたいところですが、Auroraにあらかじめリストアするデータベースを作成しておかないといけないようでした。 まずはログインしたPodを踏み台にしてAuroraのプライマリインスタンスにログインします。初回ログインではスーパーユーザーになるでしょう。下記ではデフォルトのpostgresユーザーとしています。

postgres-cli:/# psql --host=aurora-postgres.cluster-bkreikhp6iwaly.ap-northeast-1.rds.amazonaws.com --port=5432 --username=postgres --password -d postgres
Password:
psql (13.13, server 13.12)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

とにもかくにもHeroku PostgreSQLにあったデータベース名でデータベースを作ってみます。

postgres=> CREATE DATABASE ma72h7wq4Z4uhlk;
CREATE DATABASE
postgres=> \l
                                                List of databases
      Name      |      Owner       | Encoding |   Collate   |    Ctype    |           Access privileges
----------------+------------------+----------+-------------+-------------+---------------------------------------
 ma72h7wq4Z4uhlk | bkreikhp6iwaly | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
<snip>

スーパーユーザーで運用するのはセキュリティ上望ましくないので、運用のためのロールとユーザーを作成します。 まずはroleから。今回はappというロール名にしました。

postgres=> CREATE ROLE app;
CREATE ROLE

作成したapproleにデータベースへのアクセス権を付与していきます。

postgres=> GRANT CONNECT ON DATABASE ma72h7wq4Z4uhlk TO app;
GRANT
postgres=> GRANT ALL PRIVILEGES ON DATABASE ma72h7wq4Z4uhlk TO app;
GRANT

ログインユーザーを作成します。今回はapp_userとしました。

postgres=> CREATE ROLE app_user LOGIN PASSWORD 'なにがし';
CREATE ROLE

app_userapproleの権限を付与します。

postgres=> GRANT app TO app_user;
GRANT ROLE

一旦postgresのコンソールを抜けて、ma72h7wq4Z4uhlk databaseへログインしなおします。

postgres-cli:/# psql --host=aurora-postgres.cluster-bkreikhp6iwaly.ap-northeast-1.rds.amazonaws.com --port=5432 --username=postgres --password -d ma72h7wq4Z4uhlk

app roleに対して、publicスキーマのテーブル、シーケンスに対する権限を付与します。

ma72h7wq4Z4uhlk=> GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO app;
GRANT
ma72h7wq4Z4uhlk=> GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO app;
GRANT
ma72h7wq4Z4uhlk=> ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO app;
GRANT

リストアを実施する

Podログインした状態からバックアップファイルを指定してリストアします!リストア先は先ほど作成したデータベース名です。

postgres-test:/# pg_restore --verbose --clean --no-acl --no-owner -h aurora-postgres.cluster-bkreikhp6iwaly.ap-northeast-1.rds.amazonaws.com -U postgres -d ma72h7wq4Z4uhlk /tmp/sandbox-pg.dump
<snip>

これでリストアは完了ですね!

この状態でPodのコンテナに設定するプライマリデータベース接続用の環境変数(Herokuからそのまま使う場合はDATABASE_URL)をAuroraのものに変更するとちゃんと接続してPodがRunningになってくれました。

DATABASE_URL:  postgres://app_user:password@aurora-postgres.cluster-bkreikhp6iwaly.ap-northeast-1.rds.amazonaws.com:5432/ma72h7wq4Z4uhlk primary

今後の課題

というわけで、これでHeroku PostgreSQL→Auroraにデータ移行できたよ〜というものを紹介してみました。ここにある手順ですがまだ荒削りなところがあります。具体的には先ほど言及したもっと大きなサイズのデータ移行の場合どうするかということと、あと移行後のデータの完全性をちゃんと知りたいなぁと感じており、今後の課題です。

意外にすんなりできた、というのが感想です。本気でデータ移行する場合は書き込みできない状態にして実施することになるなどもっと細かい点を詰めなければなりませんが、何かしらの参考になれば幸いです😃

メンテナンスしやすいTerraformコードを書くために気をつけていること

TerraformというIaCのためのツールがあります。割とディレクトリ構成から管理の仕方までいろいろと自由がきくツールであります。

最近これでAWSVPCやEKS、RDS、ElastiCacheをコードにしたのだけれど、個人的に今まで他の方が書いたコードを読み解いたりもしてきた経験から、なんとなく「こう書いた方が他人には優しいだろう」「将来的なメンテナンスコストが抑えられるだろう」と思いつつ書いていることに気づいたので、自分がどういうことに気を付けているかを書き留めてみます。   …といってもそんなに奇をてらったようなテクニックを使っているわけではないので(むしろそういうことをするとメンテナンスしにくくなるので🙅‍♀️)そんなの常識じゃん、みたいなこともありそうだけれどとりあえず書いてみます。

1. module化は避ける

いきなり好みが分かれそうですが、自分がTerraformでコードを書くときはmodule化しません。module化は便利ですがTerraformのコードの階層が深くなりがちで、その分何が生成されるのかが他人から見たときに直感的に把握しにくくなると感じています。共通化はworkspaceの使用、for_eachやforを使った利用に極力とどめておくべきです。

2.tfstateはなるべく小さく

tfstateを小さくすることは何個かメリットがあります。ひとつはterraform plan / applyの実行時間が短くなること。もうひとつは複数人で作業していても更新した箇所とは無関係な箇所でterraformの差分が現れてこれ何だろう〜で時間を食うようなこと(これが本当によくある…)が防げます。 どの程度の範囲で単位にするのが良さそうかというと例えばRDSだとそのRDSを使うためのIAMやSGまで、VPCだとSubnetとInternetGatewayとNatGatewayくらいまでくらいが妥当じゃないでしょうか。

3.workspace、for_eachの積極活用

環境を分けるときはworkspaceを使う、複数台作成するときはfor_eachを使う。これで大体の場合は対応できるというか、逆にこの方法でコードにできないような構成を避ける、環境の方をTerraformで管理しやすい方に寄せるまであります。

RDS(Aurora)を例にとると、

resource "aws_rds_cluster" "main_db" {
  cluster_identifier              = "main_db-${terraform.workspace}"
  engine                          = "aurora-postgresql"
  engine_version                  = var.main_db_engine_version
  db_subnet_group_name            = aws_db_subnet_group.main.name
  vpc_security_group_ids          = [aws_security_group.main_db.id]
  db_cluster_parameter_group_name = aws_rds_cluster_parameter_group.main_db.name
}

resource "aws_rds_cluster_instance" "main_db" {
  for_each           = var.main_db_instances
  identifier         = "${each.key}-${each.value.az}"
  cluster_identifier = aws_rds_cluster.main_db.id
  instance_class     = each.value.instance_class
  engine             = "aurora-postgresql"
  availability_zone  = each.value.az
  promotion_tier     = each.value.promotion_tier
}

このようにworkspaceでクラスタ名を分けつつfor_eachを使ってクラスタインスタンスをそれぞれ設定できるようにします。 workspace(環境)によって分けるtfvarsは以下のようにします。

# staging
main_db_engine_version = "13.12"

main_db_instances = {
  "user01" = {
    az = "ap-northeast-1c"
    instance_class = "db.t4g.medium"
    promotion_tier  = 0
  }
}
# production
main_db_engine_version = "13.12"

main_db_instances = {
  "user01" = {
    az = "ap-northeast-1a"
    instance_class = "db.r6g.xlarge"
    promotion_tier  = 0
  }
  "user02" = {
    az = "ap-northeast-1c"
    instance_class = "db.r6g.xlarge"
    promotion_tier  = 0
  }
  "user03" = {
    az = "ap-northeast-1d"
    instance_class = "db.r6g.xlarge"
    promotion_tier  = 0
  }
  "internal01" = {
    az = "ap-northeast-1a"
    instance_class = "db.r6g.large"
    promotion_tier  = 100 //writerの対象にしない
  }
}

こうするとstagingはインスタンス1台だけ、productionは4台作って1台はフェイルオーバ対象から除外する… といったことが柔軟にできますね!1台だけインスタンスクラスを変える、といったこともできます。

4. 作成後にterraform importよりは構築時からTerraformで作成する

めんどくさくてWebコンソールから作成してからコードにしがちですが、構築時からTerraform使ったほうがよいと考えてます。Terraform ImportだとImport漏れが起こりがちで、コードから同じ環境がきちんと再現できるかが微妙に怪しいからです。せっかくコード化するならちゃんと運用時に使えるコードにしたいものです。

こんなところでしょうか?ほかにも無意識にやってることはありそうだけれども思いつく限りでこのあたりは気を付けて使ってます。TerraformのコツはやりたいことをTerraformに落とし込むよりTerraformコードにしやすい構成にすることだと思っております……ではでは

Datadogのログ管理機能のコストを削減する

Datadogにはログ管理という機能があり、ログを一挙に取得してそれを良い感じに判別する機能を持っています。 ただDatadogはAPMが優秀でよく使うのですが、その分ログ管理機能は(自分は)あまり使っていなく、その割に全ログ取得するとなるとかなりコストがかかっている状態でした。

そこでDatadogのログ監視は基本的には使わず、何か障害があったときに参考にする程度でよいかな、と思い、Agentが取得するログをerr / warn / crit とつくものだけに限定することにしました。 ドキュメントにも方法は載っているのですがAgent側でDD_LOGS_CONFIG_PROCESSING_RULESを使うとできます。

こんな感じ。

- name: DD_LOGS_CONFIG_PROCESSING_RULES
  value: '[{"type":"include_at_match","name":"include_error_warn_critical_logs","pattern":"(?i)(err|warn|crit)"}]'

これで大文字小文字関係なくerr / warn / critとつくログのみを収集するようになります。 実際やってみると取得されるログが激減して良い感じ。

Datadog

もともとのDatadogの思想とはそぐわないかもしれませんが、そんなこと言ったって無駄なログまで取得されて課金されていくのをみすみす見逃していくわけにはいかなかったのです。次の支払い明細が楽しみ。ではでは。