/** * WPML compatibility functions * * @global array $duplicated_posts Array to store the posts being duplicated. * * @package Yoast\WP\Duplicate_Post * @since 3.2 */ add_action( 'admin_init', 'duplicate_post_wpml_init' ); /** * Add handlers for WPML compatibility. */ function duplicate_post_wpml_init() { if ( defined( 'ICL_SITEPRESS_VERSION' ) ) { add_action( 'dp_duplicate_page', 'duplicate_post_wpml_copy_translations', 10, 3 ); add_action( 'dp_duplicate_post', 'duplicate_post_wpml_copy_translations', 10, 3 ); add_action( 'shutdown', 'duplicate_wpml_string_packages', 11 ); } } global $duplicated_posts; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: Renaming a global variable is a BC break. $duplicated_posts = []; /** * Copy post translations. * * @global SitePress $sitepress Instance of the Main WPML class. * @global array $duplicated_posts Array of duplicated posts. * * @param int $post_id ID of the copy. * @param WP_Post $post Original post object. * @param string $status Status of the new post. */ function duplicate_post_wpml_copy_translations( $post_id, $post, $status = '' ) { global $sitepress; global $duplicated_posts; remove_action( 'dp_duplicate_page', 'duplicate_post_wpml_copy_translations', 10 ); remove_action( 'dp_duplicate_post', 'duplicate_post_wpml_copy_translations', 10 ); $current_language = $sitepress->get_current_language(); $trid = $sitepress->get_element_trid( $post->ID ); if ( ! empty( $trid ) ) { $translations = $sitepress->get_element_translations( $trid ); $new_trid = $sitepress->get_element_trid( $post_id ); foreach ( $translations as $code => $details ) { if ( $code !== $current_language ) { if ( $details->element_id ) { $translation = get_post( $details->element_id ); if ( ! $translation ) { continue; } $new_post_id = duplicate_post_create_duplicate( $translation, $status ); if ( ! is_wp_error( $new_post_id ) ) { $sitepress->set_element_language_details( $new_post_id, 'post_' . $translation->post_type, $new_trid, $code, $current_language ); } } } } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: see above. $duplicated_posts[ $post->ID ] = $post_id; } } /** * Duplicate string packages. * * @global array() $duplicated_posts Array of duplicated posts. */ function duplicate_wpml_string_packages() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: renaming the function would be a BC-break. global $duplicated_posts; foreach ( $duplicated_posts as $original_post_id => $duplicate_post_id ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. $original_string_packages = apply_filters( 'wpml_st_get_post_string_packages', false, $original_post_id ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. $new_string_packages = apply_filters( 'wpml_st_get_post_string_packages', false, $duplicate_post_id ); if ( is_array( $original_string_packages ) ) { foreach ( $original_string_packages as $original_string_package ) { $translated_original_strings = $original_string_package->get_translated_strings( [] ); foreach ( $new_string_packages as $new_string_package ) { $cache = new WPML_WP_Cache( 'WPML_Package' ); $cache->flush_group_cache(); $new_strings = $new_string_package->get_package_strings(); foreach ( $new_strings as $new_string ) { if ( isset( $translated_original_strings[ $new_string->name ] ) ) { foreach ( $translated_original_strings[ $new_string->name ] as $language => $translated_string ) { do_action( // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. 'wpml_add_string_translation', $new_string->id, $language, $translated_string['value'], $translated_string['status'] ); } } } } } } } } Odoo ExternalNameを使ったFreee接続 – Raqqa

Odoo ExternalNameを使ったFreee接続

ExternalName Service 定義例(Freee用)

apiVersion: v1
kind: Service
metadata:
  name: freee-api
spec:
  type: ExternalName
  externalName: api.freee.co.jp

これで、Kubernetes内のアプリは以下のように書けます:

import requests

response = requests.get("https://freee-api.default.svc.cluster.local/v1/partners", headers=headers)


→ 実際は DNS で api.freee.co.jp に変換され、通常通りAPIが呼ばれます。

ExternalName の利用メリット(外部API一般に共通)

メリット内容
✅ 接続先を抽象化SaaSホストを svc名.namespace.svc.cluster.local で管理できる(コードにホスト名をハードコーディングしない)
✅ マルチ環境対応staging → prod で externalName のみ変更すればOK(コード変更不要)
✅ Kubernetes的に統一他のServiceと同じ形式で扱えるので CI/CD も一貫性あり
✅ Helmで管理可能values.yaml で外部APIホスト名を切り替え可能にできる

🔐 注意点と補足

ポイント説明
🔒 TLSはそのまま利用可能ExternalName単なるDNS CNAMEなので https:// アクセスも問題なし(証明書はFreeeが持っている)
❌ ポート定義不可ports: の指定はできない(エラーになります)
⚠ ネットワーク制御できないPodから直接インターネットへ出る構成になるので、Egress制限やFW制御が必要な場合は別途対策が必要
📊 死活監視やロードバランスは不可ExternalName はService Discoveryの置き換えではないので、リトライやフォールバックはアプリ側実装が必要です

✅ 他の外部APIにも応用できる例

SaaS/APIExternalName例
Freee会計api.freee.co.jpfreee-api.svc.cluster.local
Stripeapi.stripe.comstripe-api.svc.cluster.local
Salesforcelogin.salesforce.comsfdc-login.svc.cluster.local
OpenAI APIapi.openai.comopenai-api.svc.cluster.local

🧭 外部API接続の全体像(技術スタック)

技術役割
アプリケーション層Python/Node.js などAPI呼び出し処理を実装(requests/axiosなど)
Service抽象化層ExternalName接続先ホストの抽象化・環境分離
セキュリティ層Kubernetes Secret / EnvAPIキー、OAuth2トークンの保護
通信制御層NetworkPolicy / Egress制御SaaSアクセス許可/通信制限管理
運用層Helm / ConfigMap接続先切替や環境ごとの設定注入

複数のアカウントを扱う場合は下記が良い

✅ ゴール:

  • Freeeのアカウント(=OAuth2トークン + company_id)を安全に・動的に・容易に管理
  • 新しいアカウントが増えてもコード・構成を極力変えずに連携対象を追加できる

🧩 方法別比較(アカウント追加に対する管理性)

方法管理方法アカウント追加の作業負荷備考
PodごとにJob/Secretを個別作成Job・Secretを1セットずつ✖:増えるごとにYAML追加・CI変更保守コスト大
環境変数にベタ書きFREEE_ACCESS_TOKEN=xxx✖:アプリ再ビルドが必要セキュリティ・柔軟性低い
⚠️ ConfigMapに一覧管理1つのYAMLで全アカウントを管理△:巨大化・マウントが複雑に読み込みは簡単だが運用は微妙
Secret+IDベース動的読み込み(推奨)各アカウントをSecretで個別に管理し、ランタイムでID指定◎:増やすだけで済む最小限の追加、ロジック変更不要
外部ストレージ(DBやVault)で動的管理DBにアカウント情報を保持◎◎:Webで登録可、完全自動化本格運用向き。構成は複雑化するが最強

✅ ベストプラクティス:Secretをアカウント単位で作成+ランタイム動的参照

🎯 なぜこれが良いのか?

  • HelmやDeployment YAMLは変更不要
  • Secretだけ追加すればよい
  • 複数Podで共有できる
  • SECRET_NAME=freee-account-1234 のように アカウントIDで切替可能

シングルアカウント(=1社分のFreeeアカウント)に固定でアクセスする用途であれば、ExternalName を使うのは「有効かつシンプル」な選択肢です。

ただし、ExternalNameはホスト名の抽象化だけを行うものであり、Freeeの認証トークン(OAuth2)などは別途管理が必要です。


✅ 結論:

利用ケースExternalNameの利用価値
✅ シングルアカウント◎ 有効。ホスト名をK8s内で抽象化できて便利
❌ マルチアカウント✖ 意味がない。APIホストは同じ、切り替えるのはトークンだから

🧠 ExternalNameを使うと何が嬉しいか

例:

yamlコピーする編集するapiVersion: v1
kind: Service
metadata:
  name: freee-api
spec:
  type: ExternalName
  externalName: api.freee.co.jp

アプリはこう呼び出します:

pythonコピーする編集するrequests.get("https://freee-api.default.svc.cluster.local/v1/partners", headers={...})

✅ メリット

項目説明
コードからホスト名を隠せるapi.freee.co.jp を直接書かない
環境ごとの切り替えが簡単externalName を dev/prod 切り替えるだけ(Helm valuesで管理可能)
他のKubernetes Serviceと同じ形で統一保守性が高まる・CI/CD管理が楽

🔐 トークン(認証)はどうするの?

ExternalName は DNS だけなので、以下のような認証トークンは必ず別で管理が必要です:

  • OAuth2の access_token(FreeeのAPI認証)
  • client_id, client_secret(もしリフレッシュ対応もするなら)

管理方法:

方法適切かコメント
Kubernetes Secret に格納✅ 安全・推奨
環境変数で注入△ 実行時は使えるがセキュリティ注意
ConfigMap(平文)❌ セキュリティNG

✅ おすすめ構成(シングルアカウント)

構成要素内容
ServiceExternalNamefreee-api.svc.cluster.local にする
Secretaccess_token を保持、アプリPodにマウント or 環境変数で注入
Deployment固定トークンでFreee APIにアクセス(会社IDは指定可)
Helm valuesトークン/host名を環境別に定義し切り替え可能にしておくと便利

✅ まとめ

質問回答
シングルアカウントならExternalNameで十分?はい。ホスト名抽象化に有効で、運用が簡単になります。
認証トークンはどうする?🔑 Secret や環境変数で別途安全に管理
他と組み合わせるべきものは?DeploymentCronJob からFreee APIを呼び出すスクリプトと併用


Comments

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です