Manjusaka

Manjusaka

IaCについて簡単に話しましょう:コードとしてのインフラストラクチャ

実際、IaC という概念はすでに長い間存在しているので、少し水文を書いて IaC の過去、現在、未来について簡単に話してみたいと思います。

IaC の過去#

実際、IaC の歴史は十分に長いものです。まず、IaC の核心的な特徴を見てみましょう。

  1. 最終的な産物は machine readable な産物です。コードであることもあれば、設定ファイルであることもあります。
  2. machine readable な産物に基づいて、既存の VCS システム(SVN、Git など)を利用してバージョン管理を行うことができます。
  3. machine readable な産物に基づいて、既存の CI/CD システム(Jenkins、Travis CI など)を利用して継続的インテグレーション / 継続的デリバリーを行うことができます。
  4. 状態の一貫性、または冪等性と呼ばれるものです。理論的には、同じコードと同じパラメータに基づいて構築された産物の最終的な動作は一貫しているべきです。

実際、IaC のこれらの核心的な特徴を通じて、私たちは現在 IaC が台頭した理由を理解することができます。IaC の実際の台頭の大背景は、ミレニアム以降、インターネットの世界の進化がますます加速していることです。この時期、従来の手作業によるメンテナンスは以下のいくつかの問題に直面しています。

  1. インタラクティブな変更によって引き起こされる人的要因が大きすぎて、変更の制御が不可能になります。
  2. 手動の変更はますます迅速なインフラの進化に追いつけません。
  3. インタラクティブな変更は管理を難しくし、バージョン管理などの手段が空論になってしまいます。

このような時代背景の中で、皆がより技術的で優雅な手段を用いてこれらの問題を解決しようとしています。そこで、IaC という概念が登場しました。

もし IaC をいくつかの段階に分けるとしたら、以下のように分けられると思います。

  1. 刀根火種の段階
  2. 現代的な IaC

前述のように、IaC は実際には自発的な推進力であり、不確実性に直面したときに、私たちはコードを用いて不確実性をできるだけ排除することを選択します(実際、この原則は現在まで貫かれています)。

最初の頃、人々は最も基本的なコードの形式を用いて IaC の作業を完了することを選びました。その特徴は、以前のさまざまなインタラクティブな手段の精密化、プログラム的な記述です。人々は直接 bash を使ってすべてを解決しようとするかもしれません(祖伝の来歴不明の bash スクリプト.jpg)、または Python Fabric のようなフレームワークに基づいて簡単なラッピングを行って必要なプログラム的記述の作業を完了するかもしれません。

しかし、この段階を振り返ると、いくつかの欠陥を直感的に感じることができます。

  1. コードの再利用性が低い。
  2. 各社がそれぞれの祖伝の IaC 基盤を持ち、統一された業界標準がないため、新人の参入障壁が高い。

このような問題に直面して、より現代的な IaC 施設が生まれました。その典型的な産物は以下の通りです。

  1. Ansible
  2. Chef
  3. Puppet

実際、これらのツールは設計上それぞれ異なる選択をしています(例えば、Pull/Push モデルの選択など)が、その核心的な特徴は変わりません。

  1. フレームワーク内部には、SSH 接続管理や複数機器の並行実行、auto retry などの一般的な機能が提供されています。
  2. 上記の基本機能に基づいて、DSL ラッピングが提供されます。これにより、開発者は IaC のロジックにより集中でき、基盤レベルの詳細に気を取られなくなります。
  3. オープンソースであり、充実したプラグインメカニズムが形成されています。コミュニティはこの基盤に基づいてより豊かなエコシステムを提供できます。例えば、SDN コミュニティは ANSIBLE に基づいてさまざまなスイッチの playbook を提供しています。

現在に至るまで、実際に IaC の発展は相対的に完備された程度に達しています。その中には多くのツールが今でも存在しています。

新生代の IaC#

2006 年 8 月 25 日、Amazon が正式に EC2 サービスを提供することを発表して以来、全体のインフラは急速にクラウド時代に向かっています。現在までに、各クラウドベンダーはさまざまなサービスを提供しています。10 年以上の進化を経て、IaaS、PaaS、DaaS、FaaS など、さまざまなサービスモデルが誕生しました。これらのサービスモデルは、私たちのインフラ構築をより簡単かつ迅速にしました。しかし、これらのサービスモデルは新たな問題も引き起こしました。

ここまで書いてきたことで、問題の所在に気づいた方もいるかもしれません:計算力やリソースを取得することがますます迅速になっている今、私たちはこれらのリソースをどう管理するのでしょうか?

この問題を解決するためには、コードや宣言的な設定を使ってこれらのリソースを管理する方法を考える必要があります。少し見覚えがありませんか?歴史は常に循環しています.jpg

初めの頃、私たちは各クラウドベンダーが提供する API や SDK を基に独自の IaC ツールをラッピングすることを選びました。前述のように、これにはいくつかの追加の問題が生じます。

  1. コードの再利用性が低い。
  2. 各社がそれぞれの祖伝の IaC 基盤を持ち、統一された業界標準がないため、新人の参入障壁が高い。

この時、クラウド時代に向けたクラウドリソース管理の新しい IaC ツールの需要がますます高まります。この時、Terraform のような新しいツールが登場しました。

Terraform では、EC2 インスタンスを起動するための定義は次のように短いものです。

resource "aws_vpc" "my_vpc" {
  cidr_block = "172.16.0.0/16"

  tags = {
    Name = "tf-example"
  }
}

resource "aws_subnet" "my_subnet" {
  vpc_id            = aws_vpc.my_vpc.id
  cidr_block        = "172.16.10.0/24"
  availability_zone = "us-west-2a"

  tags = {
    Name = "tf-example"
  }
}

resource "aws_network_interface" "foo" {
  subnet_id   = aws_subnet.my_subnet.id
  private_ips = ["172.16.10.100"]

  tags = {
    Name = "primary_network_interface"
  }
}

resource "aws_instance" "foo" {
  ami           = "ami-005e54dee72cc1d00" # us-west-2
  instance_type = "t2.micro"

  network_interface {
    network_interface_id = aws_network_interface.foo.id
    device_index         = 0
  }

  credit_specification {
    cpu_credits = "unlimited"
  }
}

この基盤の上に、私たちは Database、Redis、MQ などのインフラをコード化 / 記述的に設定することができ、リソースのメンテナンスの有効性を向上させることができます。

同時に、各社の SaaS の発展に伴い、開発者もこれらの SaaS サービスをコード化 / 記述的に設定しようと試みています。Terraform を例にとると、Terraform の Provider を通じて接続することができます。例えば、newrelicが提供するProviderBytebaseが提供するProviderなどです。

また、IaC ツールがインフラの記述の標準化を助けた後、私たちはその基盤の上でさらに面白いことをすることができます。例えば、Infracostを基に、リソース変更によるコストの変化を計算することができます。さらに、atlantisを基に集中型のリソース変更を行うなど、より高度な作業を行うことができます。

現在、私たちが持っている IaC 製品の選択肢は十分に多く、ほとんどのニーズを満たすことができます。しかし、IaC 全体の製品の発展は実際には相対的に完備された程度に達しているのでしょうか?答えは明らかに否定的です。

未来の IaC#

この章では、現在の IaC 製品が直面しているいくつかの問題と、私の未来に対する考えを話したいと思います。

欠陥一:既存の DSL に基づく文法体系の欠陥#

まず、例を見てみましょう。

locals {
  dns_records = {
    # "demo0" : 0,
    "demo1" : 1,
    "demo2" : 2,
    "demo3" : 3,
  }
  lb_listener_port  = 80
  instance_rpc_port = 9545

  default_target_group_attr = {
    backend_protocol     = "HTTP"
    backend_port         = 9545
    target_type          = "instance"
    deregistration_delay = 10
    protocol_version     = "HTTP1"
    health_check = {
      enabled             = true
      interval            = 15
      path                = "/status"
      port                = 9545
      healthy_threshold   = 3
      unhealthy_threshold = 3
      timeout             = 5
      protocol            = "HTTP"
      matcher             = "200-499"
    }
  }
}

module "alb" {
  source  = "terraform-aws-modules/alb/aws"
  version = "~> 6.0"

  name                       = "alb-demo-internal-rpc"
  load_balancer_type         = "application"
  internal                   = true
  enable_deletion_protection = true

  http_tcp_listeners = [
    {
      protocol           = "HTTP"
      port               = local.lb_listener_port
      target_group_index = 0
      action_type        = "forward"
    }
  ]

  http_tcp_listener_rules = concat([
    for rec, pos in local.dns_records : {
      http_tcp_listener_index = 0
      priority                = 105 + tonumber(pos)
      actions = [
        {
          type               = "forward"
          target_group_index = tonumber(pos)
        }
      ]
      conditions = [
        {
          host_headers = ["${rec}.manjusaka.me"]
        }
      ]
    }
    ], [{
      http_tcp_listener_index = 0
      priority                = 120
      actions = [
        {
          type = "weighted-forward"
          target_groups = [
            {
              target_group_index = 0
              weight             = 95
            },
            {
              target_group_index = 5
              weight             = 4
            },
          ]
        }
      ]
      conditions = [
        {
          host_headers = ["demo0.manjusaka.me"]
        }
      ]
  }])

  target_groups = [
    merge(
      {
        name_prefix = "demo0"
        targets = {
          "demo0-${module.ec2_instance_demo[0].tags_all["Name"]}" = {
            target_id = module.ec2_instance_demo[0].id
            port      = local.instance_rpc_port
          }
        }
      },
      local.default_target_group_attr,
    ),
    merge(
      {
        name_prefix = "demo1"
        targets = {
          "demo1-${module.ec2_instance_demo[0].tags_all["Name"]}" = {
            target_id = module.ec2_instance_demo[0].id
            port      = local.instance_rpc_port
          }
        }
      },
      local.default_target_group_attr,
    ),
    merge(
      {
        name_prefix = "demo2"
        targets = {
          "demo2-${module.ec2_family_c[0].tags_all["Name"]}" = {
            target_id = module.ec2_family_c[0].id
            port      = local.instance_rpc_port
          },
        }
      },
      local.default_target_group_attr,
    ),
    merge(
      {
        name_prefix = "demo3"
        targets = {
          "demo3-${module.ec2_family_d[0].tags_all["Name"]}" = {
            target_id = module.ec2_family_d[0].id
            port      = local.instance_rpc_port
          },
        }
      },
      local.default_target_group_attr,
    ), # target_group_index_3
    merge(
      {
        name_prefix = "demonew"
        targets = {
          "demo0-${module.ec2_instance_reader[0].tags_all["Name"]}" = {
            target_id = module.ec2_instance_reader[0].id
            port      = local.instance_rpc_port
          }
        }
      },
      local.default_target_group_attr,
    ),
  ]
}

この TF 設定の記述は長く見えますが、実際には非常にシンプルなことを行っています。異なるドメイン名*.manjusaka.meに基づいてトラフィックを異なるインスタンスに転送します。そして、demo0.manjusaka.meというドメイン名に対しては、個別のトラフィックのグレースケール処理を行います。

私たちは、Terraform のこのような DSL の解決策が、動的で柔軟なシーンにおいて表現力に大きな制限があることを認識できます。

コミュニティもこの問題を十分に認識しています。したがって、Python/Lua/Go/TS などの完全なプログラミング言語に基づく IaC 製品、例えば Pulumi のようなものが登場しました。例えば、Pulumi + Python を使って上記の例を書き換えると(ここでは ChatGPT が技術的なサポートを提供しています)。

from pulumi_aws import alb

dns_records = {
    # "demo0" : 0,
    "demo1": 1,
    "demo2": 2,
    "demo3": 3,
}
lb_listener_port = 80
instance_rpc_port = 9545

default_target_group_attr = {
    "backend_protocol": "HTTP",
    "backend_port": 9545,
    "target_type": "instance",
    "deregistration_delay": 10,
    "protocol_version": "HTTP1",
    "health_check": {
        "enabled": True,
        "interval": 15,
        "path": "/status",
        "port": 9545,
        "healthy_threshold": 3,
        "unhealthy_threshold": 3,
        "timeout": 5,
        "protocol": "HTTP",
        "matcher": "200-499",
    },
}

alb_module = alb.ApplicationLoadBalancer(
    "alb",
    name="alb-demo-internal-rpc",
    load_balancer_type="application",
    internal=True,
    enable_deletion_protection=True,
    http_tcp_listeners=[
        {
            "protocol": "HTTP",
            "port": lb_listener_port,
            "target_group_index": 0,
            "action_type": "forward",
        }
    ],
    http_tcp_listener_rules=[
        {
            "http_tcp_listener_index": 0,
            "priority": 105 + pos,
            "actions": [
                {
                    "type": "forward",
                    "target_group_index": pos,
                }
            ],
            "conditions": [
                {
                    "host_headers": [f"{rec}.manjusaka.me"],
                }
            ],
        }
        for rec, pos in dns_records.items()
    ]
    + [
        {
            "http_tcp_listener_index": 0,
            "priority": 120,
            "actions": [
                {
                    "type": "weighted-forward",
                    "target_groups": [
                        {"target_group_index": 0, "weight": 95},
                        {"target_group_index": 5, "weight": 4},
                    ],
                }
            ],
            "conditions": [{"host_headers": ["demo0.manjusaka.me"]}],
        }
    ],
    target_groups=[
        alb.TargetGroup(
            f"demo0-{module.ec2_instance_demo[0].tags_all['Name'].apply(lambda x: x)}",
            name_prefix="demo0",
            targets=[
                {
                    "target_id": module.ec2_instance_demo[0].id,
                    "port": instance_rpc_port,
                }
            ],
            **default_target_group_attr,
        ),
        alb.TargetGroup(
            f"demo1-{module.ec2_instance_demo[0].tags_all['Name'].apply(lambda x: x)}",
            name_prefix="demo1",
            targets=[
                {
                    "target_id": module.ec2_instance_demo[0].id,
                    "port": instance_rpc_port,
                }
            ],
            **default_target_group_attr,
        ),
        alb.TargetGroup(
            f"demo2-{module.ec2_family_c[0].tags_all['Name'].apply(lambda x: x)}",
            name_prefix="demo2",
            targets=[
                {
                    "target_id": module.ec2_family_c[0].id,
                    "port": instance_rpc_port,
                }
            ],
            **default_target_group_attr,
        ),
        alb.TargetGroup(
            f"demo3-{module.ec2_family_d[0].tags_all['Name'].apply(lambda x: x)}",
            name_prefix="demo3",
            targets=[
                {
                    "target_id": module.ec2_family_d[0].id,
                    "port": instance_rpc_port,
                }
            ],
            **default_target_group_attr,
        ),
        alb.TargetGroup(
            f"demo0-{module.ec2_instance_reader[0].tags_all['Name'].apply(lambda x: x)}",
            name_prefix="demonew",
            targets=[
                {
                    "target_id": module.ec2_instance_reader[0].id,
                    "port": instance_rpc_port,
                }
            ],
            **default_target_group_attr,
        ),
    ],
)

全体的な使い方は、私たちの使用習慣により近く、表現力も向上しています。

欠陥二:ビジネスニーズとのギャップ#

実際、クラウド時代の IaC ツールは、主にインフラの存在性の問題を解決することに焦点を当てています。しかし、既存のインフラの編成とより合理的な利用には、実際にはかなりのギャップがあります。私たちは、これらの基礎リソースにアプリケーションをデプロイするにはどうすればよいのでしょうか。これらのリソースをどのように調整するのでしょうか。実際、これは非常に興味深い問題です。

実際、意外かもしれませんが、Kubernetes/Nomad は実際にこの問題を解決しようとしています。誰かが考えているかもしれません、「何?これも IaC ツールとして数えられるの?」間違いなくそうです。前述の IaC のいくつかの核心的な特徴を照らし合わせてみてください。

  1. 最終的な産物は machine readable な産物です。コードであることもあれば、設定ファイル(YAML エンジニアが認める)であることもあります。
  2. machine readable な産物に基づいて、既存の VCS システム(SVN、Git など)を利用してバージョン管理を行うことができます(マニフェストはリポジトリに従います)。
  3. machine readable な産物に基づいて、既存の CI/CD システム(Jenkins、Travis CI など)を利用して継続的インテグレーション / 継続的デリバリーを行うことができます(argocd などのプラットフォームがさらなるサポートを提供しています)。

また、対応する設定ファイルの中で、必要な CPU/Mem、必要なディスク / リモートディスク、必要なゲートウェイなどを宣言することができます。同時に、このフレームワークは計算インフラを相対的に一般的な抽象化を行い、ビジネスの 80%のシーンでは、基盤インフラの詳細を考慮する必要がありません。

しかし、実際にはこの既存のソリューションにもいくつかの問題があります。例えば、複雑さの急増、自己ホスト型の運用コスト、抽象化の漏れによる問題などです。

欠陥三:品質の偏差#

クラウド時代に新たに生まれた IaC は、その範囲が従来の Ansible などの IaC ツールよりも広く、野心も大きいです。その副作用として、品質の偏差が生じます。この話題は二つの側面から語ることができます。

第一に、Terraform のような IaC ツールは、公式に提供された Provider を通じて AWS/Azure/GCP などのプラットフォームをサポートしています。しかし、公式サポートであっても、その Provider に設計されたロジックとプラットフォーム側のインタラクティブなインターフェースでの設計ロジックは一致しません。例えば、以前に「Aurora DB インスタンスの削除保護はコンソールで作成する際にデフォルトでオンになっているが、TF ではデフォルトでオフになっている」と不満を述べたことがあります。これは実際に使用する際に、開発者に追加のメンタル負担をもたらします。

第二に、IaC ツールはコミュニティに極度に依存しています(ここでのコミュニティはオープンソースコミュニティやさまざまな商業会社を含みます)。Ansible などの古い先輩とは異なり、その周辺施設の品質は比較的安定しています。Terraform などの新生代の IaC の周辺の品質は言うに及ばずです。例えば、国内の福報云、華為云、騰訊云などのベンダーが提供する Provider は常に批判されています。また、多くの大規模な開発者向け SaaS プラットフォームには公式に提供された Provider がありません(例えば Newrelic)。

同時に、クラウドベンダーが提供するいくつかの機能は、一般的な IaC ツールと衝突します。例えば、AWS の WAF ツールには、IPSet に基づいて遮断する機能があります。この場合、IPSet が非常に大きいと、一般的な IaC ツールでの記述は災害的な存在となります。この場合、類似のシーンでは、クラウドベンダー自身の SDK を基にラッピングするしかありません。クラウドベンダーが提供する SDK の品質が合格であれば良いですが、福報云のような奇妙な SDK 設計であれば、ただ祈るしかありません。

欠陥四:開発者体験の不足#

開発者体験は、現在比較的ホットな話題です。誰も自分の貴重な時間を無駄にしたくはありません。現時点での主要な IaC ツールは、プロダクションサーバー向けであり、開発者体験向けではないため、使用する際の体験は非常に一般的です。

例えば、AWS 上で開発者のために一括で EC2 インスタンスを開設する必要があるシーンを考えてみましょう。どのようにして開発者がこれらのマシンをすぐに使えるようにするかは、大きな問題です。

事前に作成したイメージなどを通じて比較的一様な環境を提供することはできますが、環境をさらに微調整する必要がある場合は、非常に面倒です。

このようなシーンに対して、古いものでは Nix、新しいものではenvdがこれらの問題を解決しようとしています。しかし、現時点では、既存の IaC 製品との間にいくつかのギャップがあります。今後の接続方法は非常に興味深い話題になるかもしれません。

欠陥五:新しい技術スタックに対する不足#

最も典型的なのは Serverless のシーンです。例えば、簡単な要件があるとします。Lambda を使って簡単な SSR のレンダリングを実現したいのです。

export default function BlogPosts({ posts }) {
  return posts.map(post => <BlogPost key={post.id} post={post} />)
}

export async function getServerSideProps() {
  const posts = await getBlogPosts();
  return {
    props: { posts }
  }
}

関数自体は非常にシンプルですが、この関数をプロダクション環境にデプロイするためには、かなり面倒なことになります。例えば、今このシンプルな関数のためにどのようなインフラを準備する必要があるか考えてみましょう。

  1. 一つの Lambda インスタンス
  2. 一つの S3 バケット
  3. 一つの APIGateway とルーティングルール
  4. CDN の接続(オプション)
  5. DNS の準備

IaC マニフェストとビジネスコードが分離されている場合、私たちの変更やリソースの管理は大きな問題となります。Vercel は最近のブログFramework-defined infrastructureでこの問題を説明しています。私たちがどのようにして Domain Code as Infrastructure に発展できるかは、未来の大きな挑戦となるでしょう。

まとめ#

この記事は二日間かけて書きました。IaC という事物についての私のいくつかの考えをまとめたものです(Terraform のチュートリアルではありません!(逃。皆さんが楽しんで読んでいただけることを願っています。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。