😅

[2023年4月版]TerraformからPythonのCDKへの移行もChatGPTでなんとか

2023/04/26に公開

Python の AWS CDK に Terraform のコードを移行させてみた

Terrformのコード、とっ散らかっておりまして…
もうちょっと整理したいけどどうしようもないので CDK を試してみたいけどAPIのリファレンス見てみるとめっちゃハードル高そうじゃん…
と、腰が引けておりましたが、ChatGPTのコード変換、やらせられないか試してみました。

今回も GPT-4 使っております。

そもそもTerrformとかCDK知ってる?

そもそもですが、知識があるかちょっと聞いてみました。

Python版CDK AWS や Terraformについての知識はありますか?

はい、Python版のAWS Cloud Development Kit (CDK)やTerraformに関する知識があります。これらはどちらもインフラストラクチャをコードで定義し、構築および管理するためのツールです。

  1. AWS Cloud Development Kit (CDK):
    AWS CDKは、Amazon Web Services (AWS)のリソースをコードで定義し、プロビジョニングするためのオープンソースソフトウェア開発フレームワークです。Pythonを含む複数のプログラミング言語をサポートしています。AWS CDKを使用すると、AWSのリソースをモジュール化して再利用しやすくなり、継続的インテグレーション/継続的デプロイメント (CI/CD) パイプラインと統合することができます。

CDKの基本概念は、スタック、アプリ、およびコンストラクトです。スタックは、AWSリソースのコレクションであり、アプリは複数のスタックを含むことができます。コンストラクトは、リソースのカプセル化された定義であり、再利用やカスタマイズが容易です。

  1. Terraform:
    Terraformは、HashiCorpによって開発されたインフラストラクチャアズコード (IaC) ツールであり、複数のクラウドプロバイダーやサービスと連携してリソースをプロビジョニングおよび管理することができます。Terraformは、HCL(HashiCorp Configuration Language)という専用の言語で記述された設定ファイルを使用してリソース定義を行います。

Terraformの基本概念には、プロバイダー、リソース、データソース、およびモジュールがあります。プロバイダーは、クラウドプロバイダーやサービスとの連携を行うプラグインであり、リソースはインフラストラクチャ内の個々のコンポーネントを表します。データソースは、リソース間で情報を共有するために使用され、モジュールは再利用可能なコンポーネントのセットです。

AWS CDKとTerraformはそれぞれ異なる目的やユースケースに適したツールですが、両方ともインフラストラクチャをコードで管理し、自動化や効率化を促進することができます。

ほう、ちゃんと学習モデルに入っているようですね!

というわけで…

イケるか聞いてみた

可能かどうか…

ChatGPTでTerrfomで書かれたAWSのIaCコードをCDKのPythonに書き換えることは可能ですか?

ChatGPTは、Terraformで書かれたAWSのIaCコードをCDKのPythonに書き換える際のガイダンスを提供することができますが、自動的には書き換えられません。ただし、手順を説明し、それに従って手動でコードを変換できます。

以下は、TerraformからAWS CDK Pythonにコードを変換する一般的な手順です:

  1. 必要なモジュールのインポート: ます、AWS CDKアプリケーションで使用されるPythonモジュールをインポートします。これには、aws_cdkとaws_cdk.aws_xxx(xxxは使

消極的なYESが返ってきたので、"STOP"しました。
とりあえすやらせてみればいいんです。

お題: Github Actions とOIDC連携するためのもろもろを作成する

Github Actions から AWS にデプロイとかいろいろさせたいので、OIDC経由でログイン?させたいのですが…
そのためのOIDCプロバイダーやロールなどを作成しておきたいので、ここをなんとかしておきたいのです。
とはいえさすがに手元の環境の.tfファイルをChatGPTに張り付けるわけには行かないので、以下の記事のtfファイルを参考にさせていただきました。

https://zenn.dev/nameless_gyoza/articles/github-actions-aws-oidc-by-terraform

TF → Python CDK に変換させてみる

ここに共有いただいている Terrform のコードを以下のプロンプトでChatGPTに食わせます。

以下のTerraformコードをCDKに書き換えてください
---
data "tls_certificate" "github_actions" {
url = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"
}

resource "aws_iam_openid_connect_provider" "github_actions" {
url            = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [data.tls_certificate.github_actions.certificates[0].sha1_fingerprint]
}
...

すると、

以下は、提供されたTerraformコードをAWS CDK (Python)に書き換えたものです。この例では、AWS CDKの構成要素と機能を活用して、TerraformコードをPythonコードに変換しています。

from aws_cdk import (
    aws_iam as iam,
    core,
)
from requests import get
from OpenSSL import crypto


class GithubActionsOidcStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # Get the GitHub Actions OIDC thumbprint
        github_actions_oidc_url = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"
        cert_url = get(github_actions_oidc_url).json()["jwks_uri"]
        cert_pem = get(cert_url).content
        cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
        thumbprint = cert.digest("sha1").decode()

        # Create the OIDC provider
        github_actions_oidc_provider = iam.OpenIdConnectProvider(
            self,
            "GithubActions",
            url="https://token.actions.githubusercontent.com",
            client_ids=["sts.amazonaws.com"],
            thumbprint_list=[thumbprint]
        )

        # Define the allowed GitHub repositories and paths
        allowed_github_repositories = ["foo", "bar"]
        github_org_name = "my-organization"
        full_paths = [f"repo:{github_org_name}/{repo}:*" for repo in allowed_github_repositories]

        # Create the IAM Role
        github_actions_role = iam.Role(
            self,
            "GithubActionsRole",
            assumed_by=iam.FederatedPrincipal(
                federated=github_actions_oidc_provider.arn,
                conditions={
                    "StringLike": {
                        "token.actions.githubusercontent.com:sub": full_paths
                    }
                },
                assume_role_action="sts:AssumeRoleWithWebIdentity"
            ),
            description="IAM Role for GitHub Actions OIDC"
        )

        # Attach the AdministratorAccess policy to the role
        iam.RolePolicyAttachment(
            self,
            "AdminPolicy",
            policy=iam.ManagedPolicy.from_aws_managed_policy_name("AdministratorAccess"),
            role=github_actions_role
        )


app = core.App()
GithubActionsOidcStack(app, "GithubActionsOidcStack")
app.synth()

さらっと出してきました。やるじゃない…

Terraformでありがたいのは data.tls_certificate.github_actions.certificates[0].sha1_fingerprint みたいなややこしい箇所を割と頑張ってアクセスしやすくしてくれているところでございます。
こういった箇所は CDK だとPythonやnode.jsの生の言語機能を使って自分で実装しろってところがハードモードなんすよね…
それをPythonのちゃんとした(それっぽい)コードに置き換えてくれるのは大変ありがたいですね!

とはいえ、上記のコードはちょっとダメなところがありまして、

        cert_pem = get(cert_url).content
        cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
        thumbprint = cert.digest("sha1").decode()

はダメ。requestsはGETじゃSSL証明書は取れないんだよ…[1]

第1回ダメ出し

というわけで低レベルなAPIでSSL証明書のフィンガープリント計算させてみます。
プロンプトは以下のように。

cert_pem = get(cert_url).contentではSSL証明書は取得できません。
socketとssl,hashlibを使った低レベルAPIを使用する実装に置き換えてください

すると。

失礼しました。以下に、socket、ssl、およびhashlibを使用した低レベルAPIを使用した実装に置き換えたコードを示します。

import socket
import ssl
import hashlib
from aws_cdk import (
    aws_iam as iam,
    core,
)
from urllib.parse import urlparse


class GithubActionsOidcStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # Get the GitHub Actions OIDC thumbprint
        github_actions_oidc_url = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"

        # Extract the hostname from the OIDC URL
        parsed_url = urlparse(github_actions_oidc_url)
        hostname = parsed_url.hostname

        # Connect to the host and retrieve the certificate
        context = ssl.create_default_context()
        with socket.create_connection((hostname, 443)) as sock:
            with context.wrap_socket(sock, server_hostname=hostname) as ssock:
                cert_der = ssock.getpeercert(binary_form=True)

        # Compute the thumbprint
        thumbprint = hashlib.sha1(cert_der).hexdigest()
...

これは良さげです。しかもWithとか使っててGithub Copilotのサジェストしたコードより洗練されている雰囲気。
とはいえ、他にも明らかにダメな個所がありますので指摘します。

第2回ダメ出し

次のダメ出しは以下のようなプロンプトです。

RolePolicyAttachmentはありません。iam.Policy クラスとPolicyクラスのattach_to_roleメソッドを使用してください。

はい、iam.RolePolicyAttachment(なんてしれっと使ってましたがそんなものありません。[2]
代わりにPolicyクラスのattach_to_roleメソッドを使用するように指摘します。[3]
するとちゃんと訂正してくれました。

すみません。指摘された通り、RolePolicyAttachment は誤りでした。iam.Policy クラスと Policy クラスの attach_to_role メソッドを使用して、ポリシーをIAMロールにアタッチする方法を以下に示します。

...
        # Create the IAM Role
        github_actions_role = iam.Role(
            self,
            "GithubActionsRole",
            assumed_by=iam.FederatedPrincipal(
                federated=github_actions_oidc_provider.arn,
                conditions={
                    "StringLike": {
                        "token.actions.githubusercontent.com:sub": full_paths
                    }
                },
                assume_role_action="sts:AssumeRoleWithWebIdentity"
            ),
            description="IAM Role for GitHub Actions OIDC"
        )

        # Create and attach the AdministratorAccess policy to the role
        admin_policy = iam.ManagedPolicy.from_aws_managed_policy_name("AdministratorAccess")
        admin_policy.attach_to_role(github_actions_role)

ン~…
そうじゃないんだけど、まぁいいか。

いったん、cdk synth してみます。

$ cdk synth
...
    raise AttributeError(f"'%s' object has no attribute '%s'" % (type_info, name))
AttributeError: '<class 'aws_cdk.aws_iam._IManagedPolicyProxy'>' object has no attribute 'attach_to_role'

ですね、ManagedPolicyattach_to_roleないって。

第3回ダメ出し

というわけでこの部分を再度、指摘してみます。
今回は修正方針を示すのがめんどいのでエラーメッセージだけを与えて対策を考えさせます。

AttributeError: '<class 'aws_cdk.aws_iam._IManagedPolicyProxy'>' object has no attribute 'attach_to_role'

すみません、再度誤りがありました。attach_to_role メソッドは iam.Policy クラスではなく、iam.Role クラスで使用できます。以下の例では、add_managed_policy メソッドを使用して、github_actions_role に AdministratorAccess ポリシーをアタッチしています。

...
        # Create the IAM Role
        github_actions_role = iam.Role(
            self,
            "GithubActionsRole",
            assumed_by=iam.FederatedPrincipal(
                federated=github_actions_oidc_provider.arn,
                conditions={
                    "StringLike": {
                        "token.actions.githubusercontent.com:sub": full_paths
                    }
                },
                assume_role_action="sts:AssumeRoleWithWebIdentity"
            ),
            description="IAM Role for GitHub Actions OIDC"
        )

        # Attach the AdministratorAccess policy to the role
        admin_policy = iam.ManagedPolicy.from_aws_managed_policy_name("AdministratorAccess")
        github_actions_role.add_managed_policy(admin_policy)

ちょっとまた思ってたのと違うけど、まぁ、いいか…
このようにこちらで具体的な修正方法を与えなくても自力でどうにかする場合もあります。
当然、打率は中くらい?ですが。

このようなやり取りを cdk synthでエラーがなくなるまで続けて…

なんとなく収束

synthの結果、エラーが出なくなりました!

$ cdk synth
Resources:
  GithubActionsDEF66140:
    Type: Custom::AWSCDKOpenIdConnectProvider
    Properties:
      ServiceToken:
        Fn::GetAtt:
          - CustomAWSCDKOpenIdConnectProviderCustomResourceProviderHandlerF2C543E0
          - Arn
      ClientIDList:
        - sts.amazonaws.com
      ThumbprintList:
        - f879abce0008e4eb126e0097e46620f5aaae26ad
      Url: https://token.actions.githubusercontent.com
      CodeHash: ffa3d5f3876afed62c60735ced8dafb4db2ccbd70a8b2f6b0d9e6eaa5823d631
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
    Metadata:
      aws:cdk:path: OidcStack/GithubActions/Resource/Default
  CustomAWSCDKOpenIdConnectProviderCustomResourceProviderRole517FED65:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
      ManagedPolicyArns:
        - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: Inline
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
...

とりあえず CloudFormationに変換するところまでは通りました。
ここまででだいたい満足です。CloudFormationにできれば後は目視で頑張れます。(違)

いったんまとめ

Terraformの便利機能があっさりなくなるのでCDKへの移行は絶望していましたが、ChatGPTのおかげで割と光がさしてきました。
最初は粗々ですが、なんとなくの方向性を出してくれるのがありがたいです。
AWSのリソースは膨大なので、まっさらからリソースを探し使い方を調べて…とやるより、そこそこのコード出してくれて、エラーをつぶしながらのほうがとっかかりがあるだけ全然違います。

自然言語やプログラミング言語の翻訳だけでなく、Terrformのような絶妙にプログラムじゃないコードもプログラムに変換できてよかったです。

とはいえ今回はcdk synth叩けば副作用ゼロで試せましたが、このように短いイテレーションサイクルでバンバン修正できることもなかなかないです。
生成されたコードの正確性や修正方針など、そこそここちらに勘がないとなかなか難しいですね…

今回はここまでといたします。お疲れさまでした。

脚注
  1. https://requests-docs-ja.readthedocs.io/en/latest/api/#requests.get ↩︎

  2. https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_iam.html ↩︎

  3. https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_iam/Policy.html#aws_cdk.aws_iam.Policy.attach_to_role ↩︎

Discussion