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 製品が直面しているいくつかの問題と、私の未来に対する考えを話したいと思います。

欠陥 1:既存の 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,
        ),
    ],
)

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

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

実際、クラウド時代の 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% のシナリオでは、基盤の詳細を考慮する必要がありません。

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

欠陥 3:品質の偏差#

クラウド時代に新たに生まれた IaC は、従来の Ansible などの IaC ツールに比べて範囲が広く、野心も大きいです。その副作用として、品質の偏差が生じます。このトピックは 2 つの側面から説明できます。

第一に、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 設計の場合は、自分の運を信じるしかありません。

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

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

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

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

このようなシナリオに対して、古いものでは Nix、新しいものではenvdがこのような問題を解決しようとしています。しかし、現時点では、既存の IaC 製品との間にいくつかのギャップがあります。今後、どのように接続するかは非常に興味深いトピックになるでしょう。

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

最も典型的な例はサーバーレスのシナリオです。例えば、私は今、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. 1 つの Lambda インスタンス
  2. 1 つの S3 バケット
  3. 1 つの APIGateway およびルーティングルール
  4. CDN の接続(オプション)
  5. DNS の準備

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

まとめ#

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

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