チュートリアルJul 29, 202017 分 READ

パート3:CI/CD によって Kubernetes デプロイを自動化する方法

Angel Rivera

デベロッパー アドボケイト

Developer C sits at a desk working on an intermediate-level project.

このシリーズでは、コードとしてのインスフラストラクチャ (IaC) を導入する方法について説明していきます。開発者の皆さんに IaC のコンセプトをしっかりご理解いただけるよう、チュートリアルやコード サンプルを交えて作成しました。各記事では以下のトピックについて取り上げます。

今回の記事では、Terraform の IaC デプロイを自動化する継続的インテグレーション & 継続的デプロイメント (CI/CD) のパイプラインの作成方法について説明します。IaC デプロイについては、本シリーズのパート 1パート 2 で詳しく説明しています。この記事では以下の手順を行います。

事前にパート 1 の前提条件セクションに記載されている手順をすべて完了する必要があります。前提条件の手順が完了したら、こちらのコードリポジトリに含まれる CircleCI .config.yml ファイルから見ていきましょう。

継続的インテグレーションと継続的デプロイメント

開発者やチームは、CI/CD パイプラインを採用することで、ビルドとテストのプロセスを自動化できます。これにより、ソフトウェア開発プロセスのステータスをほぼリアルタイムで把握できる貴重なフィードバック ループが確立されます。また、プロセス実行の一貫性や結果の精度を向上させるだけでなく、プロセスの最適化やスピードアップにも貢献します。CI/CD による開発作業の合理化は、一般的なアプローチとして多くの開発チームに浸透しつつあります。価値の高い CI/CD パイプラインを構築するには、何度も繰り返すタスクをどのように統合、自動化できるのかを理解しておくことが重要です。

パート 1パート 2 では、Terraform を使用して新しい GKE クラスタを作成すると共に、アプリケーションをデプロイし、実行し、サービスを提供するための関連 Kubernetes オブジェクトを作成しました。そのための Terraform コマンドは、ターミナルから手動で実行しました。Terraform コードの開発中や変更中は手動でもかまいませんが、Terraform コマンドの実行は自動化したほうが便利です。自動化にはさまざまな方法がありますが、ここでは CI/CD パイプライン内で自動化する方法に注目していきます。

CircleCI パイプライン

CircleCI パイプラインは、プロジェクトで作業をトリガーするときに実行されるプロセス全体を指す言葉です。パイプラインにはワークフローが含まれており、ワークフローはジョブのオーケストレーションを担います。この仕組みはすべてプロジェクトの設定ファイルに定義されています。次のセクションでは、実際に CI/CD パイプラインを定義してプロジェクトを構築していきます。

CircleCI でプロジェクトをセットアップする

このプロジェクトで使用する config.yml ファイルを作成する前に、このプロジェクトを CircleCI に追加する必要があります。これについては CircleCI の入門ガイドを参照してください。CircleCI のセットアップ セクションに記載された手順を完了できたら、プロジェクトレベルの環境変数を構成します。

プロジェクト レベルの環境変数

このパイプライン内の一部のジョブは、ターゲット サービスでコマンドを実行するために認証情報にアクセスする必要があります。このセクションでは、これらのジョブに必要な認証情報を定義し、プロジェクトレベルの環境変数として CircleCI に入力する方法について説明します。CircleCI ダッシュボードで作成する必要がある環境変数名の一覧を以下にまとめます。この一覧を参考にしながら、Name フィールドに EnVar Name: の値を、Value フィールドにそれぞれの認証情報を入力してください。

上記のすべての環境変数を入力できたら、config.yml ファイル内でパイプラインの構築に取り掛かります。

CircleCI config.yml

config.yml は、CI/CD 関連ジョブの処理と実行について定義するためのファイルです。このセクションでは、パイプラインのジョブとワークフローを定義していきます。エディターで .circleci/.config.yml ファイルを開き、中身を削除して以下の内容をペーストします。

version: 2.1
jobs:

version: キーには、このパイプラインの実行中に使用するプラットフォーム機能を指定します。jobs: キーには、このパイプライン用に定義するジョブの一覧を指定します。パイプライン内で実行するジョブを作成しましょう。

ジョブ - run_tests:

CircleCI のプラットフォームをスムーズに利用できるよう、CircleCI のリファレンス ドキュメントで、特別なキーや機能について確認しておくことをお勧めします。今回取り上げるジョブに含まれているキーについては、以下に簡単にご説明します。

  • docker: - ジョブを実行するランタイムを表すキー
    • image: - このジョブで使用する Docker コンテナを表すキー
  • steps: - ジョブの実行中に実行される実行可能コマンドのリストまたはコレクションを表すキー
    • checkout: - ソース コードを構成済みのパスにチェックアウトするために使用する特別なステップのキー
    • run: - すべてのコマンドライン プログラムを呼び出すために使用するキー
      • name: - CircleCI UI に表示されるステップのタイトルを表すキー
      • command: - シェルで実行されるコマンドを表すキー
    • store_test_results: - ビルドのテスト結果をアップロードおよび保存するための特別なステップを表すキー
      • path: - JUnit XML または Cucumber JSON のテスト メタデータ ファイルが格納されたサブディレクトリを含むディレクトリへのパス (絶対パス、または working_directory からの相対パス)
    • store_artifacts: - Web アプリまたは API からアクセスできるアーティファクト (ログ、バイナリなど) を格納するステップを表すキー
      • path: - ジョブ アーティファクトの保存に使用するプライマリ コンテナ内のディレクトリのパス

CI/CD のメリットは、新しく記述されたコードのテストを自動で実行できるという点です。コードを変更するたびにテストを実行することで、コード内の既知または未知のバグを検知できるようになります。config.yml ファイル内に新しいジョブを定義してみましょう。以下の内容をファイルにペーストしてください。コード ブロック内で何が行われるかについて詳しく説明していきます。

  run_tests:
    docker:
      - image: circleci/node:12
    steps:
      - checkout
      - run:
          name: Install npm dependencies
          command: |
            npm install --save
      - run:
          name: Run Unit Tests
          command: |
            ./node_modules/mocha/bin/mocha test/ --reporter mochawesome --reporter-options reportDir=test-results,reportFilename=test-results
      - store_test_results:
          path: test-results
  • docker: キーと image: キー - このジョブで使用する Executor と Docker イメージを指定する
  • command: npm install --save キー - アプリ内で使用するアプリ依存関係をインストールする
  • name: Run Unit Tests - 自動化されたテストを実行し、test-results/ という名前のローカル ディレクトリに保存する
  • store_test_results: - test-results/ ディレクトリに結果を保存し、CircleCI 内のビルドにピン留めする特別なコマンド

このジョブは単体テストとして機能し、コード内のエラーを特定するのに役立ちます。いずれかのテストでエラーが発生すると、パイプライン全体の構築が失敗し、開発者にエラーの修正を求めるメッセージが表示されます。目標となるのは、すべてのテストとジョブをエラーなしで成功させることです。次に、Docker イメージを作成して Docker Hub レジストリにプッシュするジョブを構築します。

ジョブ - build_docker_image

本シリーズのパート 2 では、Docker イメージを手動で作成し、Docker Hub レジストリにプッシュしました。このジョブは、同じことを自動で行います。以下にbuild_docker_image:ジョブの例を示します。このコードブロックを config.ymlファイルに追加したうえで、各要素について詳しく見ていきましょう。

  build_docker_image:
    docker:
      - image: circleci/node:12
    steps:
      - checkout
      - setup_remote_docker:
          docker_layer_caching: false
      - run:
          name: Build Docker image
          command: |
            export TAG=0.2.<< pipeline.number >>
            export IMAGE_NAME=$CIRCLE_PROJECT_REPONAME            
            docker build -t $DOCKER_LOGIN/$IMAGE_NAME -t $DOCKER_LOGIN/$IMAGE_NAME:$TAG .
            echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
            docker push $DOCKER_LOGIN/$IMAGE_NAME

build_docker_image ジョブはきわめてシンプルなジョブです。皆さんは既に、ここに含まれるほとんどの CircleCI YAML キーを目にしたことがあると思うので、ここでは詳しい説明を省いて name: Build Docker Image コマンド ブロックに着目していきます。

export TAG=0.2.<< pipeline.number >> の行では、pipeline.number の値を使用して Dockerタグ値を実行対象のパイプライン番号に関連付けるローカル環境変数を定義しています。export IMAGE_NAME=$CIRCLE_PROJECT_REPONAMEDocker イメージの命名で使用する変数を定義しています。

docker build -t $DOCKER_LOGIN/$IMAGE_NAME -t $DOCKER_LOGIN/$IMAGE_NAME:$TAG .は、前の手順で設定したプロジェクト レベルの変数と、先ほど指定したローカル環境変数を組み合わせてDocker build コマンドを実行します。

echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin では、Docker Hub認証情報を認証してプラットフォームにアクセスします。最後に、docker push $DOCKER_LOGIN/$IMAGE_NAME では、新しい Docker イメージを Docker Hub レジストリにアップロードします。

パート 2 において手動で実行したコマンドに環境変数を追加しただけなので、この動作については馴染みがあるはずです。次のセクションでは、GKE クラスタを作成する Terraform コードを実行するジョブを構築します。

ジョブ - gke_create_cluster

このジョブでは、part03/iac_gke_cluster/ ディレクトリにある Terraform コードの実行を自動化します。config.yml ファイルに以下のコードブロックを追加して保存します。

  gke_create_cluster:
    docker:
      - image: ariv3ra/terraform-gcp:latest
    environment:
      CLOUDSDK_CORE_PROJECT: cicd-workshops
    steps:
      - checkout
      - run:
          name: Create GKE Cluster
          command: |
            echo $TF_CLOUD_TOKEN | base64 -d > $HOME/.terraformrc
            echo $GOOGLE_CLOUD_KEYS | base64 -d > $HOME/gcloud_keys
            gcloud auth activate-service-account --key-file ${HOME}/gcloud_keys
            cd part03/iac_gke_cluster/
            terraform init
            terraform plan -var credentials=$HOME/gcloud_keys -out=plan.txt
            terraform apply plan.txt

このジョブ コードブロックの重要な要素を確認していきましょう。まず注目していただきたいのが、Executor Docker イメージであるimage: ariv3ra/terraform-gcp:latest です。これは先ほど作成したイメージで、Google SDKTerraform CLI の両方がインストールされています。このイメージを使用しない場合は、このジョブにインストール ステップを追加する必要があります。すると、ジョブ実行のたびにツールのインストールと構成が行われるようになります。次に注目したいのは、environment: CLOUDSDK_CORE_PROJECT: cicd-workshops のキーです。後で実行する gcloud cli コマンドに必要な環境変数の値を設定しています。

echo $TF_CLOUD_TOKEN | base64 -d > $HOME/.terraformrc は、$TF_CLOUD_TOKEN 値をデコードするコマンドです。Terraform が各 Terraform クラウドワークスペース上のステート データにアクセスするために必要となる ./terraformrc ファイルを作成します。

echo $GOOGLE_CLOUD_KEYS | base64 -d > $HOME/gcloud_keys$GOOGLE_CLOUD_KEYS 値をデコードするコマンドです。glcoud cli が GCP にアクセスするために使用する gcloud_keys ファイルを作成します。

gcloud auth activate-service-account --key-file ${HOME}/gcloud_keys は、先ほどデコードして生成した gcloud_keys ファイルを使用して GCP へのアクセスを認証するコマンドです。

残りは、terraform cli コマンドと -var パラメーターです。Terraform の variables.tf ファイルに定義された個々の変数の default 値を指定したりオーバーライドしたりします。terraform apply plan.txt を実行すると、このジョブにより新しい GKE クラスタが作成されます。

ジョブ - gke_deploy_app

このジョブでは、part03/iac_kubernetes_app/ ディレクトリにある Terraform コードの実行を自動化します。config.yml ファイルに以下のコードブロックを追加して保存します。

  gke_deploy_app:
    docker:
      - image: ariv3ra/terraform-gcp:latest
    environment:
      CLOUDSDK_CORE_PROJECT: cicd-workshops
    steps:
      - checkout
      - run:
          name: Deploy App to GKE
          command: |
            export CLUSTER_NAME="cicd-workshops"
            export TAG=0.2.<< pipeline.number >>
            export DOCKER_IMAGE="docker-image=${DOCKER_LOGIN}/${CIRCLE_PROJECT_REPONAME}:$TAG"
            echo $TF_CLOUD_TOKEN | base64 -d > $HOME/.terraformrc
            echo $GOOGLE_CLOUD_KEYS | base64 -d > $HOME/gcloud_keys
            gcloud auth activate-service-account --key-file ${HOME}/gcloud_keys
            gcloud container clusters get-credentials $CLUSTER_NAME --zone="us-east1-d"
            cd part03/iac_kubernetes_app
            terraform init
            terraform plan -var $DOCKER_IMAGE -out=plan.txt
            terraform apply plan.txt
            export ENDPOINT="$(terraform output endpoint)"
            mkdir -p /tmp/gke/ && echo 'export ENDPOINT='${ENDPOINT} > /tmp/gke/gke-endpoint
      - persist_to_workspace:
          root: /tmp/gke
          paths:
            - "*"

このジョブ コードブロックの重要な要素と、先ほど取り上げなかった新しい要素について見ていきましょう。

export CLUSTER_NAME="cicd-workshops" は、デプロイ先の GCP プロジェクトの名前を格納する変数を定義します。

gcloud container clusters get-credentials $CLUSTER_NAME --zone="us-east1-d" は、先ほどのジョブで作成した GKEクラスタから kubeconfig データを取得するコマンドです。

terraform plan -var $DOCKER_IMAGE -out=plan.txt は、Terraform の variables.tf ファイルに定義された個々の変数の default 値をオーバーライドするコマンドです。

export ENDPOINT="$(terraform output endpoint)" は、Terraform コマンドで生成された出力 endpoint 値をローカル環境変数に割り当てます。このローカル環境変数はファイルに保存され、CircleCI ワークスペースに保管されます。後でこの値を取得して、アタッチされた CircleCI ワークスペースからアタッチし、必要に応じてフォローアップ ジョブで使用することが可能です。

Job - gke_destroy_cluster

このパイプライン用に作成する最後のジョブです。ここまでの CI/CD ジョブで構築したすべてのリソースとインフラストラクチャを破棄します。destroyコマンドを実行するジョブは、スモークテスト、結合テスト、パフォーマンス テストといったさまざまなテスト形式に使用可能なエフェメラルなリソースに最適で、役目を終えた構成要素を破棄できます。

このジョブでは、part03/iac_kubernetes_app/ ディレクトリにある Terraform コードの実行を自動化します。config.yml ファイルに以下のコードブロックを追加して保存します。

  gke_destroy_cluster:
    docker:
      - image: ariv3ra/terraform-gcp:latest
    environment:
      CLOUDSDK_CORE_PROJECT: cicd-workshops
    steps:
      - checkout
      - run:
          name: Destroy GKE Cluster
          command: |
            export CLUSTER_NAME="cicd-workshops"
            export TAG=0.2.<< pipeline.number >>
            export DOCKER_IMAGE="docker-image=${DOCKER_LOGIN}/${CIRCLE_PROJECT_REPONAME}:$TAG"            
            echo $TF_CLOUD_TOKEN | base64 -d > $HOME/.terraformrc
            echo $GOOGLE_CLOUD_KEYS | base64 -d > $HOME/gcloud_keys
            gcloud auth activate-service-account --key-file ${HOME}/gcloud_keys
            cd part03/iac_kubernetes_app
            terraform init
            gcloud container clusters get-credentials $CLUSTER_NAME --zone="us-east1-d"            
            terraform destroy -var $DOCKER_IMAGE --auto-approve
            cd ../iac_gke_cluster/
            terraform init
            terraform destroy -var credentials=$HOME/gcloud_keys --auto-approve

このジョブ コードブロックの重要な要素と、先ほど取り上げなかった新しい要素について見ていきましょう。

terraform destroy -var credentials=$HOME/gcloud_keys --auto-approve は、part03/iac_gke_cluster ディレクトリと part03/iac_kubernetes_app/ ディレクトリ内の Terraformコードで作成されたすべてのリソースを破棄する、Terraformの destroy コマンドを実行するコマンドです。

これで、パイプライン内に必要なジョブをすべて定義できました。この後は、パイプライン内でのジョブの実行、処理をオーケストレーションする CircleCI ワークフローを作成します。

CircleCI ワークフロー

このパイプライン内のすべてのジョブを作成できたので、次にジョブの実行方法と処理方法を定義するワークフローを作成します。ワークフローはジョブの指示書のようなものです。個々のジョブをどのタイミングで、どのようにして実行するかを指定します。以下は、パイプライン内で使用するワークフロー ブロックです。以下のワークフロー コードブロックを config.yml ファイルに追加します。

workflows:
  build_test:
    jobs:
      - run_tests
      - build_docker_image
      - gke_create_cluster
      - gke_deploy_app:
          requires:
            - run_tests
            - build_docker_image
            - gke_create_cluster
      - approve-destroy:
          type: approval
          requires:
            - gke_create_cluster
            - gke_deploy_app
      - gke_destroy_cluster:
          requires:
            - approve-destroy

上記のコードブロックは、パイプラインのワークフローの定義です。このブロックで何が行われるか見ていきましょう。workflows: キーは、ワークフローの要素を指定します。build_test: はこのワークフローの名前と識別子の組み合わせです。

jobs: キーは、config.yml ファイル内に定義された実行ジョブの一覧です。パイプラインで実行するジョブをここで指定します。今回は以下のジョブが指定されています。

      - run_tests
      - build_docker_image
      - gke_create_cluster
      - gke_deploy_app:
          requires:
            - run_tests
            - build_docker_image
            - gke_create_cluster
      - approve-destroy:
          type: approval
          requires:
            - gke_create_cluster
            - gke_deploy_app
      - gke_destroy_cluster:
          requires:
            - approve-destroy

run_testsbuild_docker_imagegke_create_clusterのワークフロージョブは、requires: キーを持つ gke_deploy_app: アイテムとは異なり、すべて並列実行されます。ジョブは既定で並列に実行されるため、依存関係がある場合は、指定されたジョブを開始する前に完了しておく必要のあるジョブの一覧と共に、requires: キーを使用し、ジョブ名に基づいて依存関係を明示的に要求する必要があります。requires: キーは、他のジョブの成功に対する依存関係を構築します。これにより、パイプラインの実行をセグメント化し、制御することができます。

approve-destroy: アイテムには、手動の承認ステップを必要とするジョブを指定します。ワークフロー ジョブ リスト内の次のジョブの実行に承認が求められる場合、ユーザーの介入が必要になります。次の gke_destroy_cluster: ジョブは、approval-destroy: ジョブが完了してから実行され、パイプライン内で以前に実行したジョブによって作成されたすべてのリソースを破棄します。

完成した .config.yml ファイル

以下は完成した config.yml ファイルの例です。プロジェクト コード レポジトリ.circleci/ ディレクトリにも同じファイルが収められています。

version: 2.1
jobs:
  run_tests:
    docker:
      - image: circleci/node:12
    steps:
      - checkout
      - run:
          name: Install npm dependencies
          command: |
            npm install --save
      - run:
          name: Run Unit Tests
          command: |
            ./node_modules/mocha/bin/mocha test/ --reporter mochawesome --reporter-options reportDir=test-results,reportFilename=test-results
      - store_test_results:
          path: test-results
      - store_artifacts:
          path: test-results
  build_docker_image:
    docker:
      - image: circleci/node:12
    steps:
      - checkout
      - setup_remote_docker:
          docker_layer_caching: false
      - run:
          name: Build Docker image
          command: |
            export TAG=0.2.<< pipeline.number >>
            export IMAGE_NAME=$CIRCLE_PROJECT_REPONAME
            docker build -t $DOCKER_LOGIN/$IMAGE_NAME -t $DOCKER_LOGIN/$IMAGE_NAME:$TAG .
            echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
            docker push $DOCKER_LOGIN/$IMAGE_NAME
  gke_create_cluster:
    docker:
      - image: ariv3ra/terraform-gcp:latest
    environment:
      CLOUDSDK_CORE_PROJECT: cicd-workshops
    steps:
      - checkout
      - run:
          name: Create GKE Cluster
          command: |
            echo $TF_CLOUD_TOKEN | base64 -d > $HOME/.terraformrc
            echo $GOOGLE_CLOUD_KEYS | base64 -d > $HOME/gcloud_keys
            gcloud auth activate-service-account --key-file ${HOME}/gcloud_keys
            cd part03/iac_gke_cluster/
            terraform init
            terraform plan -var credentials=$HOME/gcloud_keys -out=plan.txt
            terraform apply plan.txt
  gke_deploy_app:
    docker:
      - image: ariv3ra/terraform-gcp:latest
    environment:
      CLOUDSDK_CORE_PROJECT: cicd-workshops
    steps:
      - checkout
      - run:
          name: Deploy App to GKE
          command: |
            export CLUSTER_NAME="cicd-workshops"
            export TAG=0.2.<< pipeline.number >>
            export DOCKER_IMAGE="docker-image=${DOCKER_LOGIN}/${CIRCLE_PROJECT_REPONAME}:$TAG"
            echo $TF_CLOUD_TOKEN | base64 -d > $HOME/.terraformrc
            echo $GOOGLE_CLOUD_KEYS | base64 -d > $HOME/gcloud_keys
            gcloud auth activate-service-account --key-file ${HOME}/gcloud_keys
            gcloud container clusters get-credentials $CLUSTER_NAME --zone="us-east1-d"
            cd part03/iac_kubernetes_app
            terraform init
            terraform plan -var $DOCKER_IMAGE -out=plan.txt
            terraform apply plan.txt
            export ENDPOINT="$(terraform output endpoint)"
            mkdir -p /tmp/gke/
            echo 'export ENDPOINT='${ENDPOINT} > /tmp/gke/gke-endpoint
      - persist_to_workspace:
          root: /tmp/gke
          paths:
            - "*"
  gke_destroy_cluster:
    docker:
      - image: ariv3ra/terraform-gcp:latest
    environment:
      CLOUDSDK_CORE_PROJECT: cicd-workshops
    steps:
      - checkout
      - run:
          name: Destroy GKE Cluster
          command: |
            export CLUSTER_NAME="cicd-workshops"
            export TAG=0.2.<< pipeline.number >>
            export DOCKER_IMAGE="docker-image=${DOCKER_LOGIN}/${CIRCLE_PROJECT_REPONAME}:$TAG"            
            echo $TF_CLOUD_TOKEN | base64 -d > $HOME/.terraformrc
            echo $GOOGLE_CLOUD_KEYS | base64 -d > $HOME/gcloud_keys
            gcloud auth activate-service-account --key-file ${HOME}/gcloud_keys
            cd part03/iac_kubernetes_app
            terraform init
            gcloud container clusters get-credentials $CLUSTER_NAME --zone="us-east1-d"            
            terraform destroy -var $DOCKER_IMAGE --auto-approve
            cd ../iac_gke_cluster/
            terraform init
            terraform destroy -var credentials=$HOME/gcloud_keys --auto-approve
workflows:
  build_test:
    jobs:
      - run_tests
      - build_docker_image
      - gke_create_cluster
      - gke_deploy_app:
          requires:
            - run_tests
            - build_docker_image
            - gke_create_cluster
      - approve-destroy:
          type: approval
          requires:
            - gke_create_cluster
            - gke_deploy_app
      - gke_destroy_cluster:
          requires:
            - approve-destroy

まとめ

お疲れさまでした。本シリーズのパート3はこれで以上です。今回は、Terraform を使用して IaC リソースを実行する config.yml ファイルを新しく作成しました。さらに config.yml の重要な要素や、CircleCI プラットフォームに関連する内部のコンセプトについても解説しました。

本シリーズでは、Docker、GCP、Kubernetes、Terraform、CircleCIといった、さまざまなコンセプトとテクノロジーについて、具体的な手順を交えながら紹介してきました。複数のプロジェクトをつなぎ合わせて CircleCI を使用する方法や、Terraformコードを活用してターゲットのデプロイ環境でアプリケーションをテストする方法についても取り上げました。本シリーズは、DevOpsの重要なコンセプトやテクノロジー、そしてそれらを組み合わせる方法について理解を深めていただけるように作成されています。

コードの変更、新しいTerraformプロバイダーの追加、CI/CD ジョブやパイプラインの再構成を、ご自身の手でお試しになることをお勧めします。ここで学んだ知識と、頭の中にあるアイデアを組み合わせて、リリースの目標を達成しましょう。ブログ記事を読むだけでなく実際に試してみることで、学びがさらに深まります。

最後までお読みいただきありがとうございました。皆さんのお役に立てたなら幸いです。ご意見やご感想がございましたら、Twitter でお気軽に @punkdata宛にメンションしてください。

さらに詳しく知りたい方は以下の資料を参照してください。

クリップボードにコピー