AWS CloudFormation 実践 第5回 ChatGPTでコードレビュー

GitHub ActionsのMarketplaceを見ていたところ、気になるものがありました。

なんと「AIがコードレビューしてくれる」です。

今回はこちらについて、CI/CDを絡めた実装方法の手順説明と、実施結果の評価をやっていきたいと思います。

Cloud Formationから、やや脱線気味ですが・・・w

前提条件

構造としては、Gitでプルリクに対してChatGPTにレビューを行ってもらい、結果をレビューコメントとして、受け取ります(図の上部)これにより、メインブランチへのマージ前に、コード誤り・改善の気づきを得ることができます。

  • 補足
    • このActionsでは、ChatGPTのエンジンとして「ChatGPT 3.5」「4」「4 Turbo」等を指定できます。
    • このレビュー機能は3.5でも使用可能ですが、上位バージョンを使用することで、レビュー精度は高くなります。
    • 現在、4以降を使用するには、有償クレジットの購入(最低 5$)等が必要となります。

実装方法解説

GitHub Actionsワークフローに関する実装要素の細かいところなどは、前回の記事を参照ください。

camelrush.hatenablog.com

GitHub Actionsのstepで、「anc95/ChatGPT-CodeReview@main」を使用します。

1. ChatGPTのAPI KEYを取得

  • API KEYの取得手順は割愛します、こちらのサイト等を参考にしてみてください。
    qiita.com

  • 高度なレビューを行いたい場合は、Credit(最低5$)を購入することで、使用可能となります。注意点を以下に挙げます。

    • ここで紹介するレビューは、無償版の「gpt-3.5-turbo」でも可能です。
    • 「ChatGPT」と「ChatGPT API」は別製品です。「Chat GPT Plus」で有償サブスクリプション(20$/月)を払っていても、ChatGPT APIの高度AIが使用できるわけではありません。別途、Chat GPT APIのCreditを購入する必要があります。
    • Credit購入前のAPI KEYを使用すると、ChatGPT4が使用できないことがあるようです。購入後、改めてAPI KEYを払い出しし、そちらを使用してください。

2. GitHubレポジトリにAPI KEYを設定

  • GitHubで、レビューするコードのレポジトリを開き、サインインします。
  • 上部の「Settings」メニューを開き、サイドメニューから「Secrets and variables」-「Actions」を選択します。
  • 「New repository secret」ボタンをクリックします。
  • 「Name」に「OPENAI_API_KEY」、「Secret」に取得したAPI KEYを設定して「Add secret」ボタンをクリックします。

3. Actionsワークフローを作成

  • 対象のレポジトリに対し、ルート下にワークフローファイルを追加します(ファイルの作り方は、前回のコラムを参考にしてください)
  • 作成するワークフローyamlは、次のとおりです(ほぼ、公式のサンプルのままです)
name: Code Review

permissions:
  contents: read
  pull-requests: write

on:
  pull_request:
    types: [opened, reopened, synchronize]

jobs:
  review:
    # if: ${{ contains(github.event.*.labels.*.name, 'gpt review') }} # Optional; to run only when a label is attached
    runs-on: ubuntu-latest
    steps:
      - uses: anc95/ChatGPT-CodeReview@main
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          # Optional
          LANGUAGE: Japanese
          OPENAI_API_ENDPOINT: https://api.openai.com/v1
          MODEL: gpt-3.5-turbo # https://platform.openai.com/docs/models
          PROMPT: # example: Please check if there are any confusions or irregularities in the following code diff:
          top_p: 1 # https://platform.openai.com/docs/api-reference/chat/create#chat/create-top_p
          temperature: 1 # https://platform.openai.com/docs/api-reference/chat/create#chat/create-temperature
          max_tokens: 4096
          MAX_PATCH_LENGTH: 10000 # if the patch/diff length is large than MAX_PATCH_LENGTH, will be ignored and won't review. By default, with no MAX_PATCH_LENGTH set, there is also no limit for the patch/diff length.
  • on: pull_requestとあるとおり、このワークフローは、プルリクエストの操作時に実行されます。
  • MODEL: gpt-3.5-turboの部分が、レビューエンジンの指定です。高度なAIを使用されたい方は、gpt-4-turboに変更してください(ただし、こちらを使用するには、前述のとおり、最低 5$のCredit購入が必要です)
  • max_tokens: 4096は、無償版を想定した場合の最大値です。有償版であればもっと大きい値が指定可能です。
  • 作成したワークフローファイルを、Master(Main)ブランチにCommit、Pushします。

4. レビュー依頼(Pull Request)を作成

  • レビューして欲しいコードを、ワークブランチにPushします。
    • レビューは、Pull Requestが示す変更の差分に対してだけ行われます。そのため、ソース全部をレビューさせるには、一旦ソースファイル自体をレポジトリから削除して、再度ファイルをPushしなおした方がよいです。
  • GitHubのPull Requestメニューから、「New pull request」ボタンをクリックします。
  • 上部でMaster(main)ブランチへのマージを指定して、「Create pull request」ボタンを押します。
     
  • 次画面で「Create pull request」ボタンを押すと、ワークフローが開始されます。

コードレビューが終わると、プルリクへのコメントとしてレビュー結果が応答されます(メールでも配信されてきました)

以上が実施手順となります。

それではやってみましょう!

レビューのお題

レビュー対象は、前回までのCloud Formationテンプレートファイルとしました。

内容を、以下に再掲します。

▼(テンプレート全文はここをクリックしてください)

AWSTemplateFormatVersion: 2010-09-09

Parameters:
  # My IP Address CIDR(Required)
  cftMyIpAddressCidr:
    Type: String
    Description: (Required)Mapping to Inbound rule on ec2'security group.

  # Environment variable
  cftEnv:
    Type: String
    Default: Dev
    AllowedValues:
    - Prd
    - Stg
    - Dev
    Description: Enter Environment Prd, Stg or Dev. Default is Dev.

Mappings:
  # Environment Mapping
  cftEnvMap:
    Prd:
      Suffix: prd
      InstanceType: t3.small
      VpcCidr: 10.2.0.0/21
      PublicSubnetCidr: 10.2.1.0/24
      PrivateSubnetCidr: 10.2.2.0/24
    Stg:
      Suffix: stg
      InstanceType: t3.small
      VpcCidr: 10.1.0.0/21
      PublicSubnetCidr: 10.1.1.0/24
      PrivateSubnetCidr: 10.1.2.0/24
    Dev:
      Suffix: dev
      InstanceType: t3.micro
      VpcCidr: 10.0.0.0/21
      PublicSubnetCidr: 10.0.1.0/24
      PrivateSubnetCidr: 10.0.2.0/24

Resources:
  # VPC
  cftVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !FindInMap [cftEnvMap, !Ref cftEnv, VpcCidr]
      EnableDnsSupport: true
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-vpc-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  # Internet Gateway
  cftIgw:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-igw-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix]}
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref cftVpc
      InternetGatewayId: !Ref cftIgw

  # Subnet (public)  
  cftPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: true
      VpcId: !Ref cftVpc
      CidrBlock: !FindInMap [cftEnvMap, !Ref cftEnv, PublicSubnetCidr]
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-public-subnet-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  cftPublicRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref cftVpc
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-public-rtb-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  cftPublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref cftPublicRtb
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref cftIgw

  cftPublicRtAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref cftPublicSubnet
      RouteTableId: !Ref cftPublicRtb

  # Subnet (private) 
  cftPrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: false
      VpcId: !Ref cftVpc
      CidrBlock: !FindInMap [cftEnvMap, !Ref cftEnv, PrivateSubnetCidr]
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-private-subnet-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  cftPrivateRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref cftVpc
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-private-rtb-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  cftPrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref cftPrivateRtb
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref cftNatgw

  cftPrivateRtAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref cftPrivateSubnet
      RouteTableId: !Ref cftPrivateRtb

  # NAT Gateway  
  cftNatgw:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt cftNatgwEip.AllocationId
      SubnetId: !Ref cftPublicSubnet
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-natgw-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  cftNatgwEip:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

  # EC2(public)
  cftPublicEc2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: cf-tutorial-ec2-key
      DisableApiTermination: false
      ImageId: ami-020283e959651b381
      InstanceType: !FindInMap [cftEnvMap, !Ref cftEnv, InstanceType]
      SubnetId: !Ref cftPublicSubnet
      Monitoring: false
      SecurityGroupIds:
      - !Ref cftPublicEc2Sg
      UserData: !Base64 |
        #!/bin/bash -ex
        # put your script here
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-public-ec2-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  # SecurityGroup(public EC2)
  cftPublicEc2Sg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub
      - cf-tutorial-public-ec2-sg-${env}
      - {env: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix]}
      GroupDescription: Allow SSH Access.
      VpcId: !Ref cftVpc
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 22
        ToPort: 22
        CidrIp: !Sub
        - ${MyIpCidr}
        - {MyIpCidr: !Ref cftMyIpAddressCidr}
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-public-ec2-sg-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  # ElasicIp for public EC2
  cftPublicEc2Eip:
    Type: AWS::EC2::EIP
    Properties:
      InstanceId: !Ref cftPublicEc2

  # EC2(private)
  cftPrivateEc2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: cf-tutorial-ec2-key
      DisableApiTermination: false
      ImageId: ami-020283e959651b381
      InstanceType: !FindInMap [cftEnvMap, !Ref cftEnv, InstanceType]
      SubnetId: !Ref cftPrivateSubnet
      Monitoring: false
      SecurityGroupIds:
      - !Ref cftPrivateEc2Sg
      UserData: !Base64 |
        #!/bin/bash -ex
        # put your script here
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-private-ec2-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  # SecurityGroup(private EC2)
  cftPrivateEc2Sg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub
      - cf-tutorial-private-ec2-sg-${env}
      - {env: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix]}
      GroupDescription: Allow SSH Access from Public EC2 Only.
      VpcId: !Ref cftVpc
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 22
        ToPort: 22
        CidrIp: !Sub
        - ${privateIp}/32
        - {privateIp: !GetAtt cftPublicEc2.PrivateIp}
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-private-ec2-sg-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

# ======================
#  Outputs Statement
# ======================
# IP Address Allocated to Public EC2
Outputs:
  cftPublicEc2Eip:
    Value: !GetAtt cftPublicEc2Eip.PublicIp

実行結果

上記をAIでレビューしてもらった結果、次のとおり回答されました。

ChatGPTらしく、すごい精度で回答が出てきました。

いくつか、ピックアップしてみます。

  • 「ポジティブな点」で、いくつかほめてくれててうれしい^^。「3. リソースタグ付け」は「タグ付けしているからわかりやすいね」だ。ふむふむ。
  • 「2. EIPリソースの属性」は「宣言が上下逆だ」と言っている。問題はないので、不要な指摘っぽい。
  • 「3. パラメータの検証」は「CIDR形式かどうかチェックが必要だ」と言っている。さらに、「セキュリティリスクになる可能性がある」と警告しており、Ingress記述と併せて、この指定がネットワークのエンドポイントになっていることが理解できているのがすごい!
  • 「4. 可用性ゾーンのハードコーディング」は「AZを固定しているからParameterやMappingで指定するようにしては?」と言ってる。ごもっとも。

まとめ

今回、NGの指摘ポイントを作り忘れたので、明確なNG指摘はありませんでしたが、それでもこれだけ改善提案を出してくれるのはありがたいです。実際にレビューアに提出する前に指摘を上げてくれるので、本人の気づきにもなりますし、開発の運用効率も上がりますね。

AWS CloudFormation 実践 第4回 CI/CD編(GitHub Actions)

今回は、前回までに作成したCloud Formationテンプレートを、GitHub Actionsを使ってデプロイする方法についてアウトプットしていきます。

事前知識

GitHub Actions


GitHub Actionsは、GitHubが提供するワークフロー自動化サービスで、GitHub Actionsを使うことで、CI/CD、デプロイ、テスト、パッケージングなど、作業を効率的に実行できます。ワークフローの定義ファイルを作成し、GitHub上で設定することで、自動化された処理を実行できます。

CI/CD

CI/CD(継続的インテグレーションと継続的デリバリー)は、ソフトウェア開発プロセスの一部であり、開発プロセスを効率化し、品質を向上させるための重要な手法です。

  • 継続的インテグレーション (CI):
    • 開発者がコードを共有リポジトリにプッシュするたびに、自動的にビルド、テスト、およびコードの品質チェックが行われます。 バグや問題を早期に検出し、コードの品質を向上させます。 CIツール(例:Jenkins、Travis CI、CircleCI)を使用して実現されます。
  • 継続的デリバリー (CD):
    • CIの成果物を自動的にステージング環境にデプロイします。ステージング環境でさらなるテストや検証を行い、問題がないことを確認します。成果物が本番環境にデプロイできる状態になったら、自動的に本番環境にデプロイされます。CDツール(例:GitHub Actions、AWS CodePipeline、GitLab CI/CD)を使用して実現されます。

全体構成

今回の作業を実装した後の構成は下図のとおりとなります。AWS VPC内部の構造は、Cloud Formationテンプレートで記述したものであり、前回と同様です。

処理の流れ

Actionsが動作する大まかな流れは、次の通りとなります。

  1. (開発者)GitHubにテンプレートファイルとワークフローyamlをプッシュする。
  2. GitHub Actions)プッシュを受けて、AWSにCloudFormationをDeployする
  3. AWSが)Cloud Formationに基づいてスタックを構成する

【ポイント】手順2で、ActionsからCloudFormationを操作する際、AWSへの接続・認証には、AWS IAMの「IDプロバイダ」で指定するOIDC(OpenID Connect)を使用します。詳細は後述します。

対応手順

それでは、GitHub Actionsの構成作業に入ります。作業手順はおおまかに以下の流れで実施していきます。

  1. CloudFormationテンプレートの作成
  2. AWS IDプロバイダの作成
  3. AWS IAMロールの作成
  4. GitHubレポジトリ設定(シークレットの登録)
  5. GitHub Actionsワークフローの作成
  6. GitHubへのPush
  7. 動作確認

CloudFormationテンプレートの作成

こちらは前回までの内容をそのまま使用します。テンプレートは、以下のGitHubにある「cf.yaml」を使用します。

github.com

AWS IDプロバイダの作成

こちらの手順を始める前に、GitHub Actionsが行うAWS認証について、少し説明します。

OIDCについて

OpenID Connect(OIDC)は、OAuth2.0メカニズムを使用した認証プロトコルのひとつです。GitHubから、AWS CloudFormationにDeployを指示する際、AWSへの接続に使用します。

この方法として、以前は、IAMユーザのアクセスキーとシークレットキーを使用してデプロイする手段もあったのですが(現在も使用可能ですが)、昨今では、よりセキュアなアプローチとして、今回のIDプロバイダを使用する方法が主流となっているようです。

シークレットアクセスキーのような固有のキーを発行、管理するのではなく、AWSにプロバイダ(今回の場合、GitHub)を登録し、これを信頼するロールを定義することでAWSに、特定のGitHubからのAPIリクエストを受け入れることができるようになります。またロールの信頼ポリシーに、対象リポジトリを指定することで、受け入れる対象を限定できます。下図にイメージを示します。

なお、本手順についてはGitHubのドキュメントに詳細がありますので、詳しくは以下をご確認ください。

docs.github.com

IDプロバイダの作成

ここでは、GitHub ActionsをIDプロバイダとして登録します。

  • AWSのマネジメントコンソールで、IAMサービスを開きます。
  • サイドメニューから「アクセス管理」-「IDプロバイダ」を選択し、「プロバイダを追加」ボタンをクリックします。
  • 画面には、次の内容を登録します。

    • プロバイダのタイプ:OpenID Connect
    • プロバイダ名:「https://token.actions.githubusercontent.com
      入力後「サムプリントを取得」をクリックします。
    • 対象者:「sts.amazonaws.com」
      ※ 上記「プロバイダ名」のURLや対象者の値は、GitHub固有の値であり、固有値です(前述のドキュメントに記載があります)

  • 入力が終わりましたら、右下の「プロバイダを追加」ボタンをクリックします。

  • 作成後、IDプロバイダ画面にプロバイダtoken.actions.githubusercontent.comが表示されます。こちらをクリックして表示される、ARNの値 arn:aws:iam::xxxxxxxxxxxx:oidc-provider/token.actions.githubusercontent.com を控えておいてください。

AWS IAMロールの作成

ここでは、GitHub Actionsを受け入れるためのロールを作成します。

  • AWSのマネジメントコンソールで、IAMサービスを開きます。
  • サイドメニューから「アクセス管理」-「ロール」を選択し、「ロールを作成」ボタンをクリックします。
  • 「信頼されたエンティティを選択」画面では、GitHub Actionsのトリガとなるレポジトリの情報を指定します。例として、レポジトリ https://github.com/camelrush/aws_cloudformation_tutorial の master ブランチへのpushをトリガにする場合、以下のように登録します。入力したら「次へ」をクリックします。
  • 「許可を追加」画面では、Cloud Formationの動作に必要なリソースへのアクセス権限を設定します。本来はAWSのWell Architected に従い、必要最低限の権限を指定すべきですが、今回は暫定的に「AdministratorAccess」を設定します。設定したら、「次へ」をクリックします。
  • 「名前、確認、および作成」画面で任意のロール名(cf-tutorial-dev-githubactionsiam-roleとしました)、説明を登録し、「ロールを作成」ボタンをクリックします。
  • 作成されたロールの「信頼関係」を確認すると、下図のようにポリシーが定義されています。

以上で、AWS側の設定は終わりです。

GitHubレポジトリ設定(シークレットの登録)

次に、上記で作成したIAMロールを、GitHubレポジトリに登録します。

  • WebブラウザGitHubレポジトリを開き、サインインします。
  • 上部メニューの「Settings」をクリックし、再度メニューから「Secrets and variables」-「Actions」をクリックします。

  • 「New repository secret」ボタンをクリックし、表示される画面に、次の情報を登録して「Add secret」ボタンをクリックします。

    • Name:「AWS_IAM_ROLE_ARN」
    • Secret:(前述で作成したロール名(例では、cf-tutorial-dev-githubactionsiam-role ))

以上で、GitHubに対して、AWSアクセスのためのロール名を設定できました。

GitHub Actionsワークフローの作成

ここでは、GitHub Actionsのワークフローファイルを作成していきます。フォーマットについても、簡単な解説を挟みます。

ディレクトリ構成

対象となるGitリポジトリのルート配下に、ディレクトリ「.github」「workflows」を作成し、その下にymlファイルを作成してください。ymlファイルの名前は任意です。

 .github(dir)
  └ workflows(dir)
    └ deploy-on-push.yml

ワークフローファイルymlファイルの記述

最初に、作成するパラメタファイルの全体を示します。

name: "CloudFormation Deploy on Push Event"

on:
  push:
    branches: master

env:
  AWS_REGION: ap-northeast-1
  CF_ENVIRONMENT: Dev
  CF_SOURCEIP: 0.0.0.0

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    name: deploy for CloudFormation
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Source.
        uses: actions/checkout@v4
      - name: Configure AWS Credential
        uses: aws-actions/configure-aws-credentials@v4
        with: 
          aws-region: ${{ env.AWS_REGION }}
          role-to-assume: ${{ secrets.AWS_IAM_ROLE_ARN }}
      - name: deployCloudformation
        uses: aws-actions/aws-cloudformation-github-deploy@master
        with:
          name: cf-tutorial-stack
          template: cf.yaml
          no-fail-on-empty-changeset: "1"
          parameter-overrides: >-
            cftMyIpAddress=${{ env.CF_SOURCEIP }},
            cftEnv=${{ env.CF_ENVIRONMENT }}

以下、ブロックごとに詳細を説明します。

on(トリガ)
on:
  push:
    branches: master

ワークフローを実行するトリガーを記載します。上記の場合、masterブランチへのpushがトリガーとなります。これ以外に、pull_requestという記述でプルリクをトリガーとしたり、path指定で特定の階層への操作をトリガ―に指定することもできます。

env(環境変数
env:
  AWS_REGION: ap-northeast-1
  CF_ENVIRONMENT: Dev
  CF_SOURCEIP: 0.0.0.0

ワークフロー内で環境変数を使用する場合、ここで任意の変数を定義します。定義した変数は、後述のjobs等で使用します。今回定義した内容は次の通りです。

  • AWS_REGION: ap-northeast-1:後述のOIDC接続で使用するパラメタ(リージョン)
  • CF_ENVIRONMENT: Dev:CloudFormationテンプレートに引き渡すパラメタ1(環境)
  • CF_SOURCEIP: 0.0.0.0:CloudFormationテンプレートに引き渡すパラメタ2(SSH接続元IP)
permission(権限)
permissions:
  id-token: write
  contents: read

GitHub ActionsがOIDCを使用してワークフローを実行するために必要な権限を指定します。上記2つが必要となりますので、必ず設定してください(この指定は、前述で紹介したGitHub Actionsのドキュメントに記載されています

jobs(ワークフロー内容)① runs-on迄
jobs:
  deploy:
    name: deploy for CloudFormation
    runs-on: ubuntu-latest

jobsには、ワークフローの処理内容を記述します。上記3行の設定内容を以下に示します。

  • deploy:jobの識別名です。値は任意ですので、なんでも構いません。
  • name:こちらも任意ですのでなんでもかいませんが、後ほどGitHub画面で進捗を確認する際、画面に表示されます。
  • runs-on: ワークフローを実行する環境(ジョブランナー)を指定します。大きく分けて、Githubがホストしている固有の環境と、自身が提供するセルフホストが指定できます。今回は、Githubホストのubuntuの最新バージョンを指定しました。ほかに、Windowsや、MacOSも指定が可能です。指定するや、詳細を知りたい方はこちらを参照ください。
jobs(ワークフロー内容)② steps

jobsの内訳として、stepsで複数の実行内容を定義していきます。今回は以下の3ステップを実行しています。

  1. Checkout Source:リポジトリから、ジョブランナーにソースをチェックアウト(取得)する。
  2. Configure AWS Credential:AWS接続のためのクレデンシャルをセットする
  3. deployCloudformation:Cloud Formationテンプレートをデプロイする。
jobs(ワークフロー内容)② step 1. ソースをチェックアウト(取得)する
    steps:
      - name: Checkout Source.
        uses: actions/checkout@v4

ここで、上記のuses: actions/checkout@v4 について、マーケットプレイスActionsについて説明します。

Actions実行内容 uses とマーケットストアについて

stepsで実行する内容は、さまざまあるのですが、代表的なものは runまたはusesの2つです。

runは、ジョブランナー環境で実行するスクリプト命令を直接記述します。ファイルにコピーや移動など、ShellやBatで実行するような命令があれば、このタイプを宣言します。

    steps:
      - name: File Create
        # ↓ 複数以上のコマンドは run | と記載します
        run: |                         
           touch sample.txt
           cat sample.txt
           pwd

usesは、他で定義されたActionを参照して使用します。定義済のActionには、GitHub Marketplaceで公開されているアクションを活用することができます。下の例では、AWSのOIDC接続で必要となる認証Actionを使用しています。

    steps:
      - name: Configure AWS Credential
        # ↓ Marketplace Actionの「aws-actions/configure-aws-credentials@v4」 を実行します
        uses: aws-actions/configure-aws-credentials@v4   
        # ↓ 上記Actionに対するパラメタを指定しています
        with: 
          aws-region: ${{ env.AWS_REGION }}
          role-to-assume: ${{ secrets.AWS_IAM_ROLE_ARN }}   

GitHub Marketplaceは、GitHubのサイドバーから「Marketplace」を選択することで参照でき、誰でも自由に使用できます。多様なActionが定義されており、中には「プルリクされたソースを、ChatGPTがコードレビューしてくれる」といったものもあります。

使用するActionのリンクを参照することで、具体的な使用方法や、指定できるパラメタの説明が書かれています。

今回のJobでは、usesを使用した3つの定義済ActionをStep実行します。

ソースをチェックアウト(取得)する

話を戻しまして、Step1 のソースチェックアウトです。

    steps:
      - name: Checkout Source.
        uses: actions/checkout@v4

ここでは、MarketplaceのActions actions/checkout@v4 を使用して、ジョブランナー環境にレポジトリのソースを取得しています。

jobs(ワークフロー内容)② step 2. AWS接続のためのクレデンシャルをセットする
      - name: Configure AWS Credential
        uses: aws-actions/configure-aws-credentials@v4
        with: 
          aws-region: ${{ env.AWS_REGION }}
          role-to-assume: ${{ secrets.AWS_IAM_ROLE_ARN }}

ここでは、Actions aws-actions/configure-aws-credentials@v4 を使用して、AWS接続のためのクレデンシャルを設定しています。with以下にあるのがActionのためのパラメタであり、リージョンと、ロールを指定しています。

変数の使用について

withの内容にある ${{ env.AWS_REGION }}${{ secrets.AWS_IAM_ROLE_ARN }}について解説します。

このようにsecretsを使用することで、外部に公開したくない情報を、定義ファイルの外で管理することができます。

jobs(ワークフロー内容)② step 3. Cloud Formationテンプレートをデプロイする
      - name: deployCloudformation
        uses: aws-actions/aws-cloudformation-github-deploy@master
        with:
          name: cf-tutorial-stack
          template: cf.yaml
          no-fail-on-empty-changeset: "1"
          parameter-overrides: >-
            cftMyIpAddress=${{ env.CF_SOURCEIP }},
            cftEnv=${{ env.CF_ENVIRONMENT }}

Cloud Formationをコールして、テンプレートファイルをAWS環境にデプロイします。Actionには「aws-actions/aws-cloudformation-github-deploy@master」を使用します。パラメタには、次の内容を指定しています。

  • name:Cloud Formationのスタック名
  • template:テンプレートファイル名
  • no-fail-on-empty-changeset:"1"を指定することでChange Set(変更点)が存在しない場合であっても、正常終了となります。
  • parameter-overrides:Cloud Formationのparameterに渡す値を指定。

parameter-overridesはテンプレートファイル内に定義している内容ですので、具体的な内容は、前回までのコラムを参照してください。なお設定しているCF_SOURCEIPCF_ENVIRONMENTは、envとして本ファイル上部に設定済みです。

以上で、ymlファイルの解説は終わりです。

GitHubへのPush

  • 最初に、上記で作成したワークフローyamlファイルを、GitHubにPushします。
  • 次に、Cloud Formationのテンプレートファイル「cf.yaml」を更新して、GitHubにPushします。
  • Push後、ブラウザでGitHubのActionタブを参照すると、定義したワークフローに従ってStepが実行されている進捗状況が表示されます。

  • ちょうど、上の図ではCloud Formationを呼び出しているところなので、別のブラウザでを使ってAWSマネージメントコンソールを開き、Cloud Formationを確認します。下図のとおり、スタックが実行中であることがわかります。

  • しばらくすると、ステータスが緑色(Complete)となり、デプロイが完了します。

動作確認

前述のワークフロー動作によって、下図のVPC内構成が作成されました。

SSHで接続し、動作確認を行います。

無事接続できました。

まとめ

CI/CD構成を作ることが目的でしたが、GitHub Actionsでできることが幅広くあり、Marketplace等を含めて知ることができたことはよかったです。今度、プルリクをChatGPTがコードレビューしてくれるActionなどは、試してみたいですね。

IaCについては、Terraformの使用や、ステート管理(実際の構成との差分)など、実運用に載せるにはまだまだ奥が深そうですが、精進して学んでいきたいと思います。

AWS CloudFormation 実践 第3回 Parameters、Mappingsの活用

前回まで

CloudFormation連載の続きです。

前回は、CloudFormationのテンプレートを使って、AWSインフラ環境に下図のEC2環境を構築しました。

今回はテンプレートファイルの補足として、Parameters、Mappingsを追記することで、一つのテンプレートファイルから複数の条件に対応した環境を構築します。

CloudFormationの基礎知識や、テンプレートの基礎、実行方法は前回の記事をご確認ください。

<テンプレートファイル フォーマット>

AWSTemplateFormatVersion: "version date"

Description:
  String

Metadata:
  template metadata

Parameters:            # ← 今回はここの話
  set of parameters

Rules:
  set of rules

Mappings:              # ← 今回はここの話
  set of mappings

Conditions:
  set of conditions

Transform:
  set of transforms

Resources:
  set of resources

Outputs:
  set of outputs

Parameters とは

Parametersは、Cloud Formationテンプレートに記載するセクションの一つです。

実行時 (スタックを作成または更新するとき) にテンプレートに渡す値を定義するものであり、渡された値はテンプレートの Resources および Outputs セクションで使用することができます。

具体的な例を挙げてみましょう。 前回のテンプレートファイルを見直してみると、これにはVCPのIPアドレスがテンプレート内に固定値(10.0.0.0/21)で書かれています。この状態ですと、IPアドレスの変更要件がある都度、テンプレート自体を書き換える必要があるため、コード保守性が下がってしまいます。

Parametersでは、このような値をテンプレートファイルへのパラメタ(引数)として定義し、スタック作成時にコマンドから指定することができます。

具体的には次のようなコードになります。

VPCIPアドレスをパラメタ化したテンプレート>

AWSTemplateFormatVersion: 2010-09-09

Parameters:
  # VPC Cidr(Required)
  cftVpcCidr:
    Type: String
    Description: (Required) Enter VPC IP Address.

Resources:
  # VPC
  cftVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref cftVpcCidr    # ここで上のParameterを使う
      EnableDnsSupport: true

実行時には、デプロイコマンドにオプション --parameter-overrides を指定してパラメタに設定する値を指定します。

# デプロイコマンド全文
$ aws cloudformation deploy --template-file ./cf.yaml --stack-name cf-tutorial-prd --parameter-overrides cftVpcCidr=10.0.0.0/21

ただ実際の使われかたとして、上のように設定値をParametersで指定することは少なく、次のMappingと合わせて使うことが多いと思います。

Mappings とは

Mappingsも、Cloud Formationテンプレートに記載するセクションの一つであり、キーと値の関係を持つテーブル表の情報です。具体的なマッピングデータを以下に示します。

<環境 Map>

キー VpcCidr SubnetCidr Suffix
Dev(開発) 10.0.0.0/21 10.0.1.0/24 dev
Stg(検証) 10.1.0.0/21 10.1.1.0/24 stg
Prd(本番) 10.2.0.0/21 10.2.1.0/24 prd

上記は、環境名のキーごとに、VPC・サブネットのIPアドレスと、タグ名に使用されるサフィックスを表にまとめたものです。Cloud FormationのMappingsには上表を定義しておき、実行時には前述Parametersに定義したキーを指定することで、テンプレート内の各項目の値を実行時に決定することができます。ルックアップテーブルのようなものですね。

下にMappingsを使用したテンプレートファイルを記載します。

Parameters:
  # Environment
  cftEnv:
    Type: String
    Default: Dev      # ← Defaultは"Dev"(開発)
    AllowedValues:    # ← 下のいずれかから選択
    - Prd
    - Stg
    - Dev
    Description: Enter Prd, Stg or Dev. Default is Dev.

Mappings:
  # Environment Mapping
  cftEnvMap:
    Prd:
      VpcCidr: 10.2.0.0/21
      PublicSubnetCidr: 10.2.1.0/24
      PrivateSubnetCidr: 10.2.2.0/24
      Suffix: prd
    Stg:
      VpcCidr: 10.1.0.0/21
      PublicSubnetCidr: 10.1.1.0/24
      PrivateSubnetCidr: 10.1.2.0/24
      Suffix: stg
    Dev:
      VpcCidr: 10.0.0.0/21
      PublicSubnetCidr: 10.0.1.0/24
      PrivateSubnetCidr: 10.0.2.0/24
      Suffix: dev

Resources:
  # VPC
  cftVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !FindInMap [cftEnvMap, !Ref cftEnv, VpcCidr] 
        #              ↑ !FindInMap関数でMappingsからVpcCidr値を取得する
      EnableDnsSupport: true
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-vpc-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

Mappingsを使用することによって、Parametersで入力された値に応じて、一つのテンプレートファイルで異なる環境の構築を指示することができるようになります。

EC2環境の構築

前回まで作成したテンプレートファイルを、Prd、Stg環境でも使用できるように書き換えました。

▼(テンプレート全文はここをクリックしてください)

AWSTemplateFormatVersion: 2010-09-09

Parameters:
  # My IP Address(Required)
  cftMyIpAddress:
    Type: String
    Description: (Required)Mapping to Inbound rule on ec2'security group.

  # Environment
  cftEnv:
    Type: String
    Default: Dev
    AllowedValues:
    - Prd
    - Stg
    - Dev
    Description: Enter Prd, Stg or Dev. Default is Dev.

Mappings:
  # Environment Mapping
  cftEnvMap:
    Prd:
      Suffix: prd
      InstanceType: t3.small
      VpcCidr: 10.2.0.0/21
      PublicSubnetCidr: 10.2.1.0/24
      PrivateSubnetCidr: 10.2.2.0/24
    Stg:
      Suffix: stg
      InstanceType: t3.small
      VpcCidr: 10.1.0.0/21
      PublicSubnetCidr: 10.1.1.0/24
      PrivateSubnetCidr: 10.1.2.0/24
    Dev:
      Suffix: dev
      InstanceType: t3.micro
      VpcCidr: 10.0.0.0/21
      PublicSubnetCidr: 10.0.1.0/24
      PrivateSubnetCidr: 10.0.2.0/24

Resources:
  # VPC
  cftVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !FindInMap [cftEnvMap, !Ref cåftEnv, VpcCidr]
      EnableDnsSupport: true
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-vpc-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  # Internet Gateway
  cftIgw:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-igw-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix]}
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref cftVpc
      InternetGatewayId: !Ref cftIgw

  # Subnet (public)  
  cftPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: true
      VpcId: !Ref cftVpc
      CidrBlock: !FindInMap [cftEnvMap, !Ref cftEnv, PublicSubnetCidr]
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-public-subnet-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  cftPublicRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref cftVpc
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-public-rtb-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  cftPublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref cftPublicRtb
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref cftIgw

  cftPublicRtAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref cftPublicSubnet
      RouteTableId: !Ref cftPublicRtb

  # Subnet (private)  
  cftPrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: false
      VpcId: !Ref cftVpc
      CidrBlock: !FindInMap [cftEnvMap, !Ref cftEnv, PrivateSubnetCidr]
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-private-subnet-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  cftPrivateRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref cftVpc
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-private-rtb-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  cftPrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref cftPrivateRtb
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref cftNatgw

  cftPrivateRtAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref cftPrivateSubnet
      RouteTableId: !Ref cftPrivateRtb

  # NAT Gateway  
  cftNatgw:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt cftNatgwEip.AllocationId
      SubnetId: !Ref cftPublicSubnet
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-natgw-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  cftNatgwEip:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

  # EC2(public)
  cftPublicEc2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: cf-tutorial-ec2-key
      DisableApiTermination: false
      ImageId: ami-020283e959651b381
      InstanceType: !FindInMap [cftEnvMap, !Ref cftEnv, InstanceType]
      SubnetId: !Ref cftPublicSubnet
      Monitoring: false
      SecurityGroupIds:
      - !Ref cftPublicEc2Sg
      UserData: !Base64 |
        #!/bin/bash -ex
        # put your script here
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-public-ec2-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  # SecurityGroup(public EC2)
  cftPublicEc2Sg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub
      - cf-tutorial-public-ec2-sg-${env}
      - {env: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix]}
      GroupDescription: Allow SSH Access.
      VpcId: !Ref cftVpc
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 22
        ToPort: 22
        CidrIp: !Sub
        - ${privateIp}/32
        - {privateIp: !Ref cftMyIpAddress}
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-public-ec2-sg-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  # ElasicIp for public EC2
  cftPublicEc2Eip:
    Type: AWS::EC2::EIP
    Properties:
      InstanceId: !Ref cftPublicEc2

  # EC2(private)
  cftPrivateEc2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: cf-tutorial-ec2-key
      DisableApiTermination: false
      ImageId: ami-020283e959651b381
      InstanceType: !FindInMap [cftEnvMap, !Ref cftEnv, InstanceType]
      SubnetId: !Ref cftPrivateSubnet
      Monitoring: false
      SecurityGroupIds:
      - !Ref cftPrivateEc2Sg
      UserData: !Base64 |
        #!/bin/bash -ex
        # put your script here
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-private-ec2-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  # SecurityGroup(private EC2)
  cftPrivateEc2Sg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub
      - cf-tutorial-private-ec2-sg-${env}
      - {env: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix]}
      GroupDescription: Allow SSH Access from Public EC2 Only.
      VpcId: !Ref cftVpc
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 22
        ToPort: 22
        CidrIp: !Sub
        - ${privateIp}/32
        - {privateIp: !GetAtt cftPublicEc2.PrivateIp}
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-private-ec2-sg-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

# ======================
#  Outputs Statement
# ======================
# IP Address Allocated to Public EC2
Outputs:
  cftPublicEc2Eip:
    Value: !GetAtt cftPublicEc2Eip.PublicIp

Parametersに定義した内容は以下の2つです。

  • cftMyIpAddress:SSH接続で接続元となる自端末のグローバルIPアドレスです。
  • cftEnv:デプロイする環境です。Dev、Stg、Envのいずれかを指定します。

上記のテンプレートファイルを以下の通り実行することで、環境に応じたVPCがそれぞれ構築されます。(実行時、スタック名がそれぞれ違うものになるように注意してください。同じ名前で実行すると、環境を置き換えてしまうので。)

# 開発(Dev)環境 スタック
aws cloudformation deploy --template-file ./cf.yaml --stack-name cf-tutorial-dev --parameter-overrides cftMyIpAddress=xxx.xxx.xxx.xxx cftEnv=Dev

# 検証(Stg)環境 スタック
aws cloudformation deploy --template-file ./cf.yaml --stack-name cf-tutorial-stg --parameter-overrides cftMyIpAddress=xxx.xxx.xxx.xxx cftEnv=Stg

# 本番(Prd)環境 スタック
aws cloudformation deploy --template-file ./cf.yaml --stack-name cf-tutorial-prd --parameter-overrides cftMyIpAddress=xxx.xxx.xxx.xxx cftEnv=Prd

<実行後のEC2一覧>

まとめ

以上の通り、Parameters と Mappings を使用することによって、よりIaCらしいテンプレートファイルを作ることができました。環境ごとに手動で構築すると、同じ構成を作っているつもりでも作業ミスが発生することもあリます。

作業効率だけではなく、品質維持のためにも IaC は重宝しますね。

なお、この結果については、以下のGitHubに掲載していますのでご確認ください。 github.com

AWS CloudFormation 実践 第2回 EC2編

前回まで

CloudFormation連載の続きです。

前回は、CloudFormationのテンプレートを使って、AWSインフラ環境にVPC、Subnet、InternetGateway、NATGatewayを構築しました。

今回はPublic/PrivateそれぞれのSubnet内にEC2を二つたてて、ローカル環境からSSHを使ってPubilc EC2→ Private EC2 の順でログインします。

CloudFormationの基礎知識や、テンプレートの基礎、実行方法は前回の記事をご確認ください。

事前準備. キーペアの作成

CloudFormationの作業に入る前に、マネージメントコンソールでEC2にログインするための鍵ファイルを作成します。

  • 補足.調べたところ、キーペアはCloudFormationで動的に作成することもできるそうなのですが、手順が複雑化するので、本連載の後半で、余力があればトライすることにしようと思います。 qiita.com

AWSマネージメントコンソールでEC2サービスを開き、左メニューから「キーペア」を選択し、以下のように鍵を作成します。この時「名前」に入力した値は、後ほどテンプレートファイルで指定することになります。

「キーペアを作成」ボタンをクリックすると、ローカルディスクにpemファイルがダウンロードされますので、ログインのために保存しておきます。

テンプレートファイルに追記

前回まで記述したテンプレートファイルに加え、以下の内容を加筆していきます。

(Public)EC2とセキュリティグループ

早速、EC2の内容を追記していきます。最初は、Public Subnet 内に配置される、外部からSSHアクセス可能なEC2です。SSHを通すには、ポート22を許可するセキュリティグループをEC2に適用する必要がありますので、セキュリティグループも併せて作成します。

# EC2(public)
  cftPublicEc2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: cf-tutorial-ec2-key
      DisableApiTermination: false
      ImageId: ami-020283e959651b381
      InstanceType: t2.micro
      SubnetId: !Ref cftPublicSubnet
      Monitoring: false
      SecurityGroupIds:
        - !Ref cftPublicEc2Sg
      UserData: !Base64 |
        #!/bin/bash -ex
        # put your script here
      Tags:
        - Key: Name
          Value: cf-tutorial-public-ec2

# SecurityGroup(public EC2)
  cftPublicEc2Sg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: cf-tutorial-public-ec2-sg
      GroupDescription: Allow SSH Access.
      VpcId: !Ref cftVpc
      SecurityGroupIngress:
        - 
          IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 60.111.xxx.xxx/32  # ← 許可ソースするIPに発信元IP(自PCのIP)を設定
      Tags:
        - Key: Name
          Value: cf-tutorial-public-ec2-sg

EC2のリソースでは、ImageIdInstanceType で対象のEC2を決定します。ImageIdは、AMIのイメージIDとなります。標準のカタログからも選択が可能であり、上記テンプレートで指定した「ami-020283e959651b381」は、以下の画面で確認した内容を指定しました。

InstanceType は、リファレンスの一覧で確認し、そこから選択します。今回は、最小レベルの種類である「t2.micro」を指定しました。

KeyNameには、事前準備したキーペアの「名前」cf-tutorial-ec2-key を指定します。

セキュリティグループリソースでは、VpcId に 該当するVPCを指定し、SecurityGroupIngress に インバウンドのルールを設定します。SecurityGroupIngress には許可するポート番号と、CidrIp(許可するソースIP)を設定します。

なお、アウトバウンドに指定を行う場合は、これに加え SecurityGroupEgress を設定します。

ちなみに、なぜかわかりませんが GroupDescription が必須だったりします。任意でよいので、今回は「Allow SSH Access.」と設定しました。

(Public)EC2にElastic IPアドレスを割り当てる

せっかくなので、PublicのEC2には、固定のグローバルIPアドレスを割当しましょう。前回NAT Gatewayでもやりましたが、AWSのElastic IPサービスを使って固有のIPを取得します。

# ElasicIp for public EC2
  cftPublicEc2Eip:
    Type: AWS::EC2::EIP
    Properties:
      InstanceId: !Ref cftPublicEc2

記述自体はそれほど難しいことはなく、InstanceId に、割り当てるEC2インスタンス!Ref で指定するだけです。これにより、Ipが自動割当てされます。

ただ、IPアドレスは自動割り当てとしているため、割り当てられたIPがいくつになるか知りたくなりますね。

割り当てられた Elastic IP をOutputする

せっかくなので、ここでテンプレートファイルの Outputs セクションを使って割当られたIPアドレスを出力してみましょう。

# IP Address Allocated to Public EC2
Outputs:
  cftPublicEc2Eip:
    Value: !GetAtt cftPublicEc2Eip.PublicIp

上記の内容は、Resources セクションと同列のインデントに記載することに注意して下さい。

cftPublicEc2EipPublicIp!GetAtt 組込み関数を使って取得し、リソース名 cftPublicEc2Eip に対する値(Value )として設定します。

!GetAttでどのような値が取得できるかは、各リソースのリファレンスの「Return Values」(EIPのResult Valuesはこちら)に書かれています。参考にしてみましょう。

<Elastic IP の Return Values>

(Private)EC2とセキュリティグループ

Publicと同様にPrivateEC2を構築します。PublicEC2との違いのポイントは以下です。

「PublicEC2のプライベートIPアドレス」 は、CloudFormationに自動割り振りされていますが、このIPはどのように取得するのでしょうか。

こちらも、前述の !GetAtt 組込み関数で取得することができます。

<EC2::Instance の Return Values>

これを踏まえ、テンプレートファイルを以下のとおり記載します。

# EC2(private)
  cftPrivateEc2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: cf-tutorial-ec2-key
      DisableApiTermination: false
      ImageId: ami-020283e959651b381
      InstanceType: t2.micro
      SubnetId: !Ref cftPrivateSubnet
      Monitoring: false
      SecurityGroupIds:
        - !Ref cftPrivateEc2Sg
      UserData: !Base64 |
        #!/bin/bash -ex
        # put your script here
      Tags:
        - Key: Name
          Value: cf-tutorial-private-ec2

# SecurityGroup(private EC2)
  cftPrivateEc2Sg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: cf-tutorial-prviate-ec2-sg
      GroupDescription: Allow SSH Access from Public EC2 Only.
      VpcId: !Ref cftVpc
      SecurityGroupIngress:
        - 
          IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 
            !Sub
              - ${privateIp}/32
              - { privateIp : !GetAtt cftPublicEc2.PrivateIp }
      Tags:
        - Key: Name
          Value: cf-tutorial-prviate-ec2-sg
  • SecurityGroupIngress の CidrIp(送信元)には、前述のとおり「PublicEC2のプライベートIPアドレス」を指定したいわけですが、 CIDR表記とする必要があります。そこで今回は !Sub関数を使いました。
  • !Sub は文字列の置換関数です。${privateIp}の部分に、その下にある「privateIp」の設定内容が置換されます。設定内容には、!GetAtt cftPublicEc2.PrivateIp からは「10.0.1.55」のようなIPアドレスが返却されるので、!Sub関数のテンプレ―ト ${privateIp}/32 の結果は「10.0.1.55/32」(CIDR表記) となります。

なお、この内容でデプロイした結果をマネージメントコンソールで見ると、このEC2にはパブリックIPが割当されていません。これは前回、Private Subnetでは、IPアドレスの割り当てを false に設定したためです。これによって、外部からアクセスはできないことになります。

テンプレートファイル全文

ここまでの記述内容をまとめると次のようになります。

(このまま使用する場合は、cftPublicEc2SgSecurityGroupIngressCidrには、自PCのグローバルIPアドレスを設定してください)

▼(テンプレート全文はここをクリックしてください)

AWSTemplateFormatVersion: 2010-09-09

Resources: 
# VPC
  cftVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/21
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: cf-tutorial-vpc
  
# Internet Gateway
  cftIgw:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: cf-tutorial-igw
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref cftVpc
      InternetGatewayId: !Ref cftIgw

# Subnet (public)  
  cftPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: true
      VpcId: !Ref cftVpc
      CidrBlock: 10.0.1.0/24
      Tags:
        - Key: Name
          Value: cf-tutorial-public-subnet
  
  cftPublicRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref cftVpc
      Tags:
        - Key: Name
          Value: cf-tutorial-public-rtb
  
  cftPublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref cftPublicRtb
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref cftIgw

  cftPublicRtAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref cftPublicSubnet
      RouteTableId: !Ref cftPublicRtb  

# Subnet (private)  
  cftPrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: false
      VpcId: !Ref cftVpc
      CidrBlock: 10.0.2.0/24
      Tags:
        - Key: Name
          Value: cf-tutorial-private-subnet
  
  cftPrivateRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref cftVpc
      Tags:
        - Key: Name
          Value: cf-tutorial-private-rtb
  
  cftPrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref cftPrivateRtb
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref cftNatgw

  cftPrivateRtAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref cftPrivateSubnet
      RouteTableId: !Ref cftPrivateRtb  

# NAT Gateway  
  cftNatgw:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt cftNatgwEip.AllocationId
      SubnetId: !Ref cftPublicSubnet
      Tags:
        - Key: Name
          Value: cf-tutorial-natgw

  cftNatgwEip:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

# EC2(public)
  cftPublicEc2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: cf-tutorial-ec2-key
      DisableApiTermination: false
      ImageId: ami-020283e959651b381
      InstanceType: t2.micro
      SubnetId: !Ref cftPublicSubnet
      Monitoring: false
      SecurityGroupIds:
        - !Ref cftPublicEc2Sg
      UserData: !Base64 |
        #!/bin/bash -ex
        # put your script here
      Tags:
        - Key: Name
          Value: cf-tutorial-public-ec2

# SecurityGroup(public EC2)
  cftPublicEc2Sg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: cf-tutorial-public-ec2-sg
      GroupDescription: Allow SSH Access.
      VpcId: !Ref cftVpc
      SecurityGroupIngress:
        - 
          IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 60.111.xxx.xxx/32  # ← 許可ソースするIPに発信元IP(自PCのIP)を設定
      Tags:
        - Key: Name
          Value: cf-tutorial-public-ec2-sg

# ElasicIp for public EC2
  cftPublicEc2Eip:
    Type: AWS::EC2::EIP
    Properties:
      InstanceId: !Ref cftPublicEc2

# EC2(private)
  cftPrivateEc2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: cf-tutorial-ec2-key
      DisableApiTermination: false
      ImageId: ami-020283e959651b381
      InstanceType: t2.micro
      SubnetId: !Ref cftPrivateSubnet
      Monitoring: false
      SecurityGroupIds:
        - !Ref cftPrivateEc2Sg
      UserData: !Base64 |
        #!/bin/bash -ex
        # put your script here
      Tags:
        - Key: Name
          Value: cf-tutorial-private-ec2

# SecurityGroup(private EC2)
  cftPrivateEc2Sg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: cf-tutorial-prviate-ec2-sg
      GroupDescription: Allow SSH Access from Public EC2 Only.
      VpcId: !Ref cftVpc
      SecurityGroupIngress:
        - 
          IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 
            !Sub
              - ${privateIp}/32
              - { privateIp : !GetAtt cftPublicEc2.PrivateIp }
      Tags:
        - Key: Name
          Value: cf-tutorial-prviate-ec2-sg

# ======================
#  Outputs Statement
# ======================
# IP Address Allocated to Public EC2
Outputs:
  cftPublicEc2Eip:
    Value: !GetAtt cftPublicEc2Eip.PublicIp

デプロイを実施

上記のテンプレートファイル(cf.yaml)を、AWS環境に対してデプロイします。

デプロイは次のコマンドで実行します。

> aws cloudformation deploy --template-file ./cf.yaml --stack-name cf-tutorial

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - cf-tutorial

CloudFormationが正常終了すれば、作業終了です。マネージメントコンソールから、各リソースを開いて、内容がテンプレートファイルのとおり作られているかチェックしてみましょう。

接続確認

SSH接続をするには、接続先となるEC2のIPアドレスを把握する必要があります。マネージメントコンソール画面のCloudFormationを開き、スタック cf-tutorial の「出力」タブを選択してください。テンプレートファイルに Outputs セクション を記述したことで、PublicEC2のグローバルIPアドレスが表示されています。

この値(グローバルIPアドレス)に対してSSH接続を行います。

PublicEC2にSSH接続

Powershell(またはコマンドプロンプト)を開き、キーペアをダウンロードしたフォルダまで移動してから、SSHコマンドで接続を行います。

PS> ssh -i cf-tutorial-ec2-key.pem ec2-user@3.115.208.35

The authenticity of host '3.115.208.35 (3.115.208.35)' can't be established.
ED25519 key fingerprint is SHA256:0PtpyHKlYHCUUh7jnd/wD1TIOVqtkb6Y6/Cp6D3V0Ls.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '3.115.208.35' (ED25519) to the list of known hosts.
   ,     #_
   ~\_  ####_        Amazon Linux 2023
  ~~  \_#####\
  ~~     \###|
  ~~       \#/ ___   https://aws.amazon.com/linux/amazon-linux-2023
   ~~       V~' '->
    ~~~         /
      ~~._.   _/
         _/ _/
       _/m/'
[ec2-user@ip-10-0-1-49 ~]$

無事、PublicEC2 まで接続できたようです。

次の作業のため、一度、接続を切断します。

[ec2-user@ip-10-0-1-49 ~]$ exit
logout
Connection to 3.115.208.35 closed.
PS> 

PublicEC2に鍵を転送する

次に、PublicEC2に、PrivateEC2に接続するための鍵を転送します。ファイルの転送には、SCPコマンドを使用します。

PS> scp -i cf-tutorial-ec2-key.pem cf-tutorial-ec2-key.pem ec2-user@3.115.208.35:~

cf-tutorial-ec2-key.pem                              100% 1675    95.9KB/s   00:00

これにより、PublicEc2内に、接続用の鍵が送り込まれました。

PublicEC2経由でPrivateEC2にSSH接続

  • 最初に、ローカルPCでPowershellを開き、PublicEC2にSSH接続します。
PS> ssh -i cf-tutorial-ec2-key.pem ec2-user@3.115.208.35

The authenticity of host '3.115.208.35 (3.115.208.35)' can't be established.
ED25519 key fingerprint is SHA256:0PtpyHKlYHCUUh7jnd/wD1TIOVqtkb6Y6/Cp6D3V0Ls.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '3.115.208.35' (ED25519) to the list of known hosts.
   ,     #_
   ~\_  ####_        Amazon Linux 2023
  ~~  \_#####\
  ~~     \###|
  ~~       \#/ ___   https://aws.amazon.com/linux/amazon-linux-2023
   ~~       V~' '->
    ~~~         /
      ~~._.   _/
         _/ _/
       _/m/'
[ec2-user@ip-10-0-1-49 ~]$
[ec2-user@ip-10-0-1-49 ~]$ sudo chmod 600 cf-tutorial-ec2-key.pem
  • 最後に、この秘密鍵を使用して、PrivateEC2にSSH接続します。
[ec2-user@ip-10-0-1-49 ~]$ ssh -i cf-tutorial-ec2-key.pem ec2-user@10.0.2.54

The authenticity of host '10.0.2.54 (10.0.2.54)' can't be established.
ED25519 key fingerprint is SHA256:uLWt5bgqBiGE4cQchtz2ihaoIzHOgrX4N8GNgMJkLqI.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.0.2.54' (ED25519) to the list of known hosts.
   ,     #_
   ~\_  ####_        Amazon Linux 2023
  ~~  \_#####\
  ~~     \###|
  ~~       \#/ ___   https://aws.amazon.com/linux/amazon-linux-2023
   ~~       V~' '->
    ~~~         /
      ~~._.   _/
         _/ _/
       _/m/'
[ec2-user@ip-10-0-2-54 ~]$

無事接続できました。画面だとわかりずらいですが、図にするとこんな感じです。

また念のため、このPrivateEC2から、外部アクセスができるかを確認しています。

# hostコマンドでgoogleページのIPアドレスをチェック
[ec2-user@ip-10-0-2-54 ~]$ host www.google.co.jp
www.google.co.jp has address 142.251.222.3
www.google.co.jp has IPv6 address 2404:6800:4004:81f::2003

# nslookupで応答を確認
[ec2-user@ip-10-0-2-54 ~]$ nslookup 142.251.222.3
3.222.251.142.in-addr.arpa      name = nrt13s71-in-f3.1e100.net.

Authoritative answers can be found from:

無事、応答できていることが分かります。

まとめ

前回と併せて、Cloud FormationでAWSの基本的なEC2構成のAWSインフラを構築できました。これまで手作業で構築していたEC2が、コマンド1回(2分程度)でできるので、簡単に検証環境などを作ることができます。これは便利!

次回は、Parameters、Mappingsといった、Resources以外のセクションも活用して、同じ構成を理想的なIaCに近づけていこうと思います。

なお、この結果については、以下のGitHubに掲載していますのでご確認ください。 github.com

AWS CloudFormation 実践 第1回 VPC編

今回から、何回かにわたってAWS CloudFormation の実践について書いていきます。

経緯

今年の夏に、SAA(AWS Solution Architect Assosiate)資格の期限が切れてしまうため、一念発起してSAP(同 Professional)資格取得を決めました。従来は Udemy での勉強をしていましたが、今回はチャネルを変えて「Cloud Tech」さんのサイトで勉強を始めました。

現在は動画中心で勉強をすすめているのですが、実践編の都度、環境を作っては崩してを繰り返しているので手間がかかり、必要に応じてCloudFormationで基盤構築することを試してみようと思います。

これまでも、IaC(Infrastructure as Code)の重要性は理解していましたが、実際に手を動かして構築したことはなかったので、良い機会かと。ただ、目的はあくまで試験合格なので脱線注意でいきたいな、とは思っています。

CloudFormationとIaCの基礎知識

CloudFormationはYaml(またはJSON)形式で書かれたテンプレートファイルに従って、AWSのインフラ環境を自動構築するのサービスです。AWSインフラ構築はAWSコンソールからのGUI操作でも可能ですが、テンプレートファイルにまとめておくことで、新規構築、変更、破棄が簡単に行えます。構築するセットのことを「スタック」と呼びます。

このように、コード(テンプレートファイル記述)を使用してインフラ環境を構築することを 「Infrastrucure As Code(IaC)」と呼び、再利用性や、保守性の高さから昨今のトレンドとなっています。IaCの実現は、AWS専用のCloudFormationの他、Terraformや、Ansibleといった汎用ツールも存在します。

CloudFormationの実行方法

CloudFormationには、次のような実行方法があります。

  • AWS CLIで、ローカル環境のテンプレートファイルをAWSに対して実行する。
  • AWSマネージメントコンソールで、CloudFormationを選択して、作成したテンプレートファイルをアップロードする。
  • Code Pipelineを使用して、Gitに登録したテンプレートファイルをデプロイする

今回は、1点目のAWS CLIを使用してデプロイを行う方法を採用します。

事前準備

AWS CLIを使用するには、AWSのアカウントを作成の上で、次の準備が必要となります。

  1. AWS CLIをローカル環境にインストールする。
  2. AWSコンソールで IAM サービスを開き、CLIを使用するアカウントとアクセスキーを作成する
  3. ローカル環境でコマンドプロンプトから aws configure をコマンドを実行し、リージョン・アクセスキーを設定する。

ここでは詳細は割愛します。↓の公式ページを参照ください。

docs.aws.amazon.com

Cloud Formation テンプレート基礎知識

テンプレートファイル(ここではYaml形式で説明します)のスケルトンは以下のとおりとなっています(AWS公式ページを引用)

AWSTemplateFormatVersion: "version date"

Description:
  String

Metadata:
  template metadata

Parameters:
  set of parameters

Rules:
  set of rules

Mappings:
  set of mappings

Conditions:
  set of conditions

Transform:
  set of transforms

Resources:
  set of resources

Outputs:
  set of outputs

各セクションの内容は次の表のとおりです。

セクション 設定内容 必須
AWSTemplateFormatVersion テンプレートが準拠している AWS CloudFormation テンプレートバージョン。テンプレート形式バージョンは API または WSDL バージョンと同じではありません。テンプレート形式バージョンは API および WSDL バージョンとは関係なく変更できます。 ×
Description テンプレートを説明するテキスト文字列です。このセクションは、必ずテンプレートの Format Version セクションの後に記述する必要があります。 ×
Metadata テンプレートに関する追加情報を提供するオブジェクトです。 ×
Parameters 実行時 (スタックを作成または更新するとき) にテンプレートに渡す値です。テンプレートの Resources および Outputs セクションからのパラメータを参照できます。 ×
Rules スタックの作成またはスタックの更新時に、テンプレートに渡されたパラメータまたはパラメータの組み合わせを検証します。 ×
Mappings キーと関連する値のマッピングで、条件パラメータ値の指定に使用でき、ルックアップテーブルに似ています。Resources セクションと Outputs セクションで Fn::FindInMap 組み込み関数を使用することで、キーと対応する値を一致させることができます。 ×
Conditions スタックの作成中または更新中に、特定のリソースが作成されるかどうか、または特定のリソースプロパティに値が割り当てられるかどうかを制御する条件です。例えば、スタックが実稼働用であるかテスト環境用であるかに依存するリソースを、条件付きで作成できます。 ×
Transform サーバーレスアプリケーション (Lambda ベースアプリケーションとも呼ばれます) の場合は、使用する AWS Serverless Application Model (AWS SAM) のバージョンを指定します。変換を指定する場合は、AWS SAM 構文を使用して、テンプレート内のリソースを宣言できます。このモデルでは、使用できる構文と、その処理方法を定義します。 ×
Resources Amazon Elastic Compute Cloud インスタンスAmazon Simple Storage Service バケットなど、スタックリソースとそのプロパティを指定します。テンプレートの Resources と Outputs セクションのリソースを参照できます。
Outputs スタックのプロパティを確認すると返される値について説明します。たとえば、S3 バケット名の出力を宣言してから、aws cloudformation describe-stacks AWS CLI コマンドを呼び出して名前を表示することができます。 ×

表に示す通り「必須」なのは、Resoucesだけであり、これだけあればひとまず実行できます。

テンプレートファイル作成

ローカルに新しいYamlファイルを作成して、最低限の記述を記載します。今回は、cf-tutorial.yaml という名前でファイルを作成し、以下のとおり記述しました。

AWSTemplateFormatVersion: 2010-09-09

Resources: 

なお記述にあたっては、VsCodeを使用しています。Extentionsの、Cloud Formationを使用すると、リソース名から最低限のプロパティを自動出力してくれるので簡単に記述できます。

ただ、このExtentionは更新が止まっているせいか、すべてのリソースを出力してくれるわけではないようです。今回のケースでもNAT Gatewayは自分で執筆しました。

今回構築するAWSインフラ

一般的なEC2構成をCloudFormationで構築することを目指していきます。下図を参照。

  • リージョン内に、Subnetを二つ持つVPCを構築する
  • Subnetの一つはPublicとする。Internet Gatewayを介して外部からアクセスできるようにする。
  • もう一つのSubnetはPrivateとする。外部からの接続は禁止し、Public Subnetからしかアクセスできないこと。ただし、Private Subnet側から外部へはアクセスできること(NAT Gatewayを経由)

今回は上記のうち、EC2以外のネットルート(VPC、Subnet、IGW、NAT)までとし、EC2は次回に送ります。

VPC

最初にVPCを構成します。ここで、テンプレートファイルに対する基礎的な記述方法を学んでおきます。

AWSTemplateFormatVersion: 2010-09-09

Resources: 
# VPC
  cftVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/21
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: cf-tutorial-vpc
  • 冒頭「cftVpc」はテンプレート内のリソースIdで、任意につけられます。冒頭小文字のキャメルケースで記載するのが一般的なようで、名称は英数字のみ可能です(ハイフン不可)
  • Typeには、作成するリソースを指定します。設定できる内容は公式ドキュメントから選択します。
    AWS リソースおよびプロパティタイプのリファレンス
  • Propertiesには、リソースに応じた各種の設定内容を記述します。ここではVPCIPアドレス(CIDR)と、DNSサポートの有無、タグを設定しています。
  • 余談ですが、Tagsに設定するキー名は大文字小文字の区分けがされます。例えばNameタグをnameとしてしまうと、AWSマネジメントコンソールには何も表示されませんのでご注意ください。

このように、リソースに応じて設定するプロパティが異なるため、それぞれの設定を理解していく必要があります。

AWS CLIからCloudFormationでデプロイする

CloudFormationをうまく行うコツは、小さい単位でデプロイをおこなうことだそうです。一気に実行しようとすると、エラー時に原因を切り分けるのに苦労したり、トライ&エラーに時間がかかったりするためです。

実際、今回のような小さなものでもすべて記載してデプロイするのに、1回5分程度かかるため、細かなNGが重なると効率が悪かったりします。

デプロイは次のコマンドで実行します。

> aws cloudformation deploy --template-file ./cf.yaml --stack-name cf-tutorial
  • --template-file オプションの後ろには、テンプレートファイルを指定します。
  • --stack-name オプションの後ろには、スタックの名称(任意)を指定します。

最低限、これだけあれば実行可能ですが、詳細なオプションとして指定できるものは、こちらの公式ドキュメントを参照ください。

実行が正常終了すると、以下のように結果が表示されます。Successfully~と出ていれば成功です(エラー時は An error occurred~のように出ます)

> aws cloudformation deploy --template-file ./cf.yaml --stack-name cf-tutorial

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - cf-tutorial

AWSマネージメントコンソールでCloudFormationを見ると、当該スタック(cf-tutorial)に対して「CREATE_COMPLETE」が表示されていることが分かります。

同じく、AWSマネージメントコンソールでVPCを参照すると、上記のテンプレートファイルで指定したVPCが作成されていました。

以上で確認は終了です。以降で継続してリソースの追加をしていきます。

補足. デプロイがエラーになったら、リトライ前にDELETE STACKすること

CloudFormationのデプロイが失敗した後、再度、修正したテンプレートファイルでdeployをしようとすると、以下のようなエラーとなります。

> aws cloudformation deploy --template-file ./cf.yaml --stack-name cf-tutorial

An error occurred (ValidationError) when calling the CreateChangeSet operation: Stack:arn:aws:cloudformation:ap-northeast-1:922813957008:stack/cf-tutorial/579b4ab0-d0bf-11ee-809a-0a4a4aa379bb is in ROLLBACK_COMPLETE state and can not be updated.

リトライする前に、以下のコマンドでスタックを削除するようにしてください。

> aws cloudformation delete-stack --stack-name cf-tutorial

Internet Gateway

次にInternet Gatewayを記述します。

# Internet Gateway
  cftIgw:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: cf-tutorial-igw

  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref cftVpc    # !Ref関数でVPCリソース「cftVpc」を指定する
      InternetGatewayId: !Ref cftIgw    

正確には前半のcftIgwがInternetGatewayで、後半のAttachGateway(VPCGatewayAttachment)はVPCへのアタッチを示す別のリソースです。InternetGatewayのプロパティはほとんどなく、VPCGatewayAttachmentでVPCとInternetGatewayのIdを指定してリンクさせます。

  • !Ref のように「!」から始まる記述は、CloudFormationの組込み関数です。!Ref は指定したパラメータの値を返却します。VpcId: !Ref cftVpcVpcId パラメータに、前述のVPCを設定する、という意味です。

Subnet(Public)

次はPublicのサブネットの定義となります。Subnetに適用するルートテーブルも併せて定義します。

# Subnet (public)  
  cftPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: true      # パブリックIPv4アドレスの自動割り当てを有効
      VpcId: !Ref cftVpc
      CidrBlock: 10.0.1.0/24
      Tags:
        - Key: Name
          Value: cf-tutorial-public-subnet
  
  cftPublicRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref cftVpc
      Tags:
        - Key: Name
          Value: cf-tutorial-public-rtb
  
  cftPublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref cftPublicRtb
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref cftIgw

  cftPublicRtAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref cftPublicSubnet
      RouteTableId: !Ref cftPublicRtb  

ここで重要なのは、3つ目にあるルートリソース(cftPublicRoute)です。DestinationCidrBlock: 0.0.0.0/0GatewayId: !Ref cftIgw が、マネージメントコンソールにおける、次のルートの設定に該当します。

上記で定義したルートをルートテーブル(cftPublicRtb)に紐づけしたら、最後にAWS::EC2::SubnetRouteTableAssociationリソース(cftPublicRtAssoc)で、サブネットに関連付けします。

  • SubnetリソースにあるMapPublicIpOnLaunch: trueは、 AWS マネジメントコンソールの「パブリックIPv4アドレスの自動割り当てを有効にする」(true=はい)に相当しています。

Subnet(Private)

Publicと同様の手順で、Privateサブネットも記述します。

# Subnet (private)  
  cftPrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: false    # パブリックIPv4アドレスの自動割り当てを無効
      VpcId: !Ref cftVpc
      CidrBlock: 10.0.2.0/24
      Tags:
        - Key: Name
          Value: cf-tutorial-private-subnet
  
  cftPrivateRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref cftVpc
      Tags:
        - Key: Name
          Value: cf-tutorial-private-rtb
  
  cftPrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref cftPrivateRtb
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref cftNatgw    # ここでNAT Gatewayを指定する

  cftPrivateRtAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref cftPrivateSubnet
      RouteTableId: !Ref cftPrivateRtb  

構成自体はPublicとほぼ同じですが、Privateサブネットの通信先はInternet Gatewayではなく、NAT Gatewayとなります。

そこでルートリソース cftPrivateRoute には NatGatewayId プロパティとしてNAT Gatewayリソース!Ref cftNatgw を設定します(この cftNatgw リソースはまだ記述していません。この後登場します)

NAT Gateway

最後に、NAT Gatewayを記述します。NAT Gatewayは外部に通信するためのサービスですが、その実態はパブリックな仮想IPアドレスなので、AWSのElasticIPでパブリックIPを生成して割り当てします。

# NAT Gateway  
  cftNatgw:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt cftNatgwEip.AllocationId   # 下のElasticIPを割当てする
      SubnetId: !Ref cftPublicSubnet     # NATGateway自体はPublic Subnetに配置する
      Tags:
        - Key: Name
          Value: cf-tutorial-natgw

  cftNatgwEip:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

前後しますが、Privateサブネットからの送信先に、上記で作成したNAT Gatewayを設定することで、Privateサブネットからの通信を外部に振り向けることができるようになります。

テンプレートファイル全文

ここまでの記述内容をまとめると次のようになります。

AWSTemplateFormatVersion: 2010-09-09

Resources: 
# VPC
  cftVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/21
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: cf-tutorial-vpc
  
# Internet Gateway
  cftIgw:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: cf-tutorial-igw
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref cftVpc
      InternetGatewayId: !Ref cftIgw

# Subnet (public)  
  cftPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: true
      VpcId: !Ref cftVpc
      CidrBlock: 10.0.1.0/24
      Tags:
        - Key: Name
          Value: cf-tutorial-public-subnet
  
  cftPublicRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref cftVpc
      Tags:
        - Key: Name
          Value: cf-tutorial-public-rtb
  
  cftPublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref cftPublicRtb
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref cftIgw

  cftPublicRtAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref cftPublicSubnet
      RouteTableId: !Ref cftPublicRtb  

# Subnet (private)  
  cftPrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: false
      VpcId: !Ref cftVpc
      CidrBlock: 10.0.2.0/24
      Tags:
        - Key: Name
          Value: cf-tutorial-private-subnet
  
  cftPrivateRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref cftVpc
      Tags:
        - Key: Name
          Value: cf-tutorial-private-rtb
  
  cftPrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref cftPrivateRtb
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref cftNatgw

  cftPrivateRtAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref cftPrivateSubnet
      RouteTableId: !Ref cftPrivateRtb  

# NAT Gateway  
  cftNatgw:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt cftNatgwEip.AllocationId
      SubnetId: !Ref cftPublicSubnet
      Tags:
        - Key: Name
          Value: cf-tutorial-natgw

  cftNatgwEip:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

デプロイを実施

上記のテンプレートファイル(cf.yaml)を、AWS環境に対してデプロイします。

デプロイは次のコマンドで実行します。

> aws cloudformation deploy --template-file ./cf.yaml --stack-name cf-tutorial

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - cf-tutorial

CloudFormationが正常終了すれば、作業終了です。マネージメントコンソールから、各リソースを開いて、内容がテンプレートファイルのとおり作られているかチェックしてみましょう。

エラー時の調査方法

Deploy後、エラーが帰った場合は、CloudFormation画面でスタックを調べます。

「イベント」タブを選択し、画面上部にある「根本原因を検出」ボタンを押すと、直接的なエラー原因にジャンプしますので、内容を確認して対応を行っていきます。

ハマったところ

今回の作業中、実際にエラーになった内容を例に、解決方法を示します。

  1. 記述ミス

    • エラー内容

        Resource handler returned message: "Value (ap-northeast-1) for parameter 
        availabilityZone is invalid. Subnets can currently only be created in the following
        availability zones: ap-northeast-1a, ap-northeast-1c, ap-northeast-1d. 
        (Service: Ec2, Status Code: 400, Request ID: 6edca7c9-57c2-44d5-9b8e-6c2788239eb9)"
        (RequestToken: 88105dee-6a98-d76e-c820-5d3eb0d15dcc, HandlerErrorCode: InvalidRequest)
      
        <日本語訳>
        リソース ハンドラーからメッセージが返されました: "パラメーター availabilityZone の値
        (ap-northeast-1) が無効です。サブネットは現在、ap-northeast-1a、ap-northeast-1c、
         ap-northeast-1d のアベイラビリティーゾーンでのみ作成できます。
        (サービス:ec2、ステータスコード:400、リクエストID:6edca7c9-57c2-44d5-9b8e-6c2788239eb9)」
        (RequestToken:88105dee-6a98-d76e-c820-5d3eb0d15dcc、HandlerErrorCode:InvalidRequest)
      
    • 原因:サブネットのプロパティにあるAZの指定を間違えた(--;)

      • 正 : AvailabilityZone: ap-northeast-1a
      • 誤 : AvailabilityZone: ap-northeast-1
  2. 設定内容の誤り

    • エラー内容

        Resource handler returned message: "The routeTable ID 'rtb-0cd66f4d6d462c986|0.0.0.0/0'
        does not exist (Service: Ec2, Status Code: 400, Request ID: 693a27c9-0a5e-47a3-8227-f44493db069d)"
        (RequestToken: 191fd476-0e73-194b-3a1a-fcc398d6af26, HandlerErrorCode: NotFound)
      
        <日本語訳>
        リソース ハンドラーがメッセージを返しました: "routeTable ID 'rtb-0cd66f4d6d462c986|0.0.0.0/0' が
        存在しません (サービス: ec2、状態コード: 400、要求 ID: 693a27c9-0a5e-47a3-8227-f44493db069d)" (RequestToken: 191fd476-0e73-194b-3a1a-fcc398d6af26、
        HandlerErrorCode: NotFound)
      
    • 原因:SubnetRouteTableAssociationRouteTableId 属性にRouteのIdを指定していた

        cftPublicRtb:
          Type: AWS::EC2::RouteTable
          Properties:
            VpcId: !Ref cftVpc
            Tags:
              - Key: name
               Value: cf-tutorial-public-rtb
      
        cftPublicRoute:
          Type: AWS::EC2::Route
          Properties:
            RouteTableId: !Ref cftPublicRtb
            DestinationCidrBlock: 0.0.0.0/0
            GatewayId: !Ref cftIgw
      
        cftPublicRtAssoc:
          Type: AWS::EC2::SubnetRouteTableAssociation
          Properties:
            SubnetId: !Ref cftPublicSubnet
            RouteTableId: !Ref cftPublicRoute   # ← cftPublicRtb が正解
      
  3. 設定誤り(処理が終わらない)

まとめ

実際に操作してみたところ、思った以上に簡単に実装できることが分かりました。特にVisual Studio Codeスニペットのおかげで、ゼロから調べる必要がないのは助かります。

今回作成した内容は以下のGitHubにもアップしておきます。

github.com

次回は作成したVPCにEC2を構築します。EC2リソースの構築のほか、セキュリティグループの設定内容や、EC2のログイン鍵の管理などをどのように行うか、といった疑問も出ると思うので、そのあたりの解消方法を学びたいと思います。

オンラインツール「EasyEDA」で基盤設計とPCB発注をやってみる

先日開発したポータブル式サーモグラフィを、基盤として再開発してみようと思います。

camelrush.hatenablog.com

前回まで

前回開発した時には、手元にあったユニバーサル基盤に、エナメル線で配線をしてはんだ付けをしていました。

ただ、あまりにも汚いし、何かのはずみで断線しそうです。

なので、以前からやってみたかった基盤設計に挑戦してみました。

事前情報、ソフトウェア「EasyEDA」

基盤設計には、これまでデスクトップアプリのフリーソフト「Kicad」というのが有名だと思っていたのですが、以下のYoutube記事で、Kicadよりも簡単にできる、という照会があったのでこちらを使うことにしました。

www.youtube.com

Webアプリケーションであり、オンラインで作業できるのでデータ保存なども楽に行えます。

設計開始ぃぃん

プロジェクト作成

  • 右上の「Register」から、サインアップを行います。

  • ログイン後、[Products]-[Standard Online Editor]を選択します。

  • 下の画面が表示されるので、「新しいプロジェクト」を選択します。

回路図の作成

最初に、回路図を作成します。

  • 回路図の画面が表示されるので、左側の「Common Library」または「Library」から、任意のパーツを選択して配置していきます。

  • パーツの選択は左下のような画面で行います。ダイアログの右側に回路図や、実物の絵が表示されるので、内容を確認しながら配置していきます。

    • 見つからない場合は「User Contributed」を選択してください。世界中のユーザが作成したパーツから探してくれるので、見つかることがあります。
  • これを繰り返して、下のとおり必要なパーツをひたすら配置していきます。

  • 次に「ツールバー」左上にある「Wire」をクリックして、回路上で配線をしていきます。

    • 図内のラベルはダブルクリックすることで書き換えることができます。

    • パーツを選んで「R」を押すと、回転(Rotation)します。

    • 使用しない端子には、ツールバーの「No Connect Flag」(×)を設定しましょう。しないと、後でエラーになります。

    • 端子同士をうまくつなげない場合は、描画間隔を調整します。右クリックメニューから「スナップサイズ」を変更し、最低の1とすることで、細かいサイズ調整ができます。

    • 基準電圧(GND)は、ツールバーの「Net Flag(GND)」で設定します。

    • 以下のとおり、回路図を作成しました。

  • 最後にメニューバーから「Design」-「Convert to Schematic to PCB」を選択します。この時、回路図のチェックも行ってくれます。エラーがある場合は左側にエラー箇所が表示されますので、訂正していきます。

シルクスクリーン作成

次に実際の部品の配置と、配線を行っていきます。

  • さきほどの「Convert to Schematic to PCB」の選択によって、PCBの作成画面が表示されます。

  • まずは紫のエリア内にパーツが収まるように部品を配置します。

    • 物理的なサイズを意識しながら配置を考えます。一般的なブレッドボードを意識する場合、端子間は 2.54mm です。回路図の時と同じように、右クリックメニューから「グリッドサイズ」を変更し、「100.0mil 2.540mm」を選択しましょう。また、端子はそのうえで移動させるので、「スナップサイズ」は半分の「100.0mil 1.270mm」とします。

    • パーツを選んで「R」を押すと、回転(Rotation)します。

    • 大まかにですが、次のように配置しました。

  • あらかじめひかれている、細い紫線は、回路図上の配線を示しています。これを実際の結線に変えていきます。メニューバーの「Track」を選択して、線を引いていき、すべてのパーツをつないでいきます。

    • 「Layers and Objects」ウィンドウの「トップレイヤ」「ボトムレイヤ」を使い分けます(トップは表面、ボトムは裏面)
      今回のように複雑な配線の場合であっても、異なる配線を交差することはできません。対策として、基盤に穴(ビア)をあけ、裏面をバイパスして、目的の端子に接続します。 下図でいうと、左が表面(赤い線)、右が裏面です。
    • 表/裏配線の操作方法は、マウスで配線を引いている最中に、キーボードで「B(Bottom=裏面)」または「T(Top=表面)」を押します。これにより、直前にクリックしたポイントに「ビア(穴)」が作られます。

    • ボード上にコメントを記載したい場合(今回は「Designed By ... 」で名前を入れました)は、「Layers and Objects」ウィンドウで「トップ(ボトム)シルクレイヤー」を選択し、メニューバーの「テキスト」を貼り付けます。

    • 配線を曲げる場合は直角に曲げず、2段階で斜めに折り返すことが推奨されています(直角にすると、プリントの過程で断線を起こすリスクが高いそうです)

  • 以上を踏まえながら、配線・基盤のコメントを行いました。

  • 出来上がったらメニューバーにある「2D」「3D」を選択してみましょう。プリント基板のイメージが表示されます。

  • 最後に、メニューバーの「Fabrication」-「One-click Order PCB/SMT」を選択します。ここで配線に問題がある場合はエラーが表示されますので、エラー内容に沿って訂正を行っていきます。

ガーバーファイルの作成と発注

  • 前述のシルクスクリーンにエラーがなければ、いよいよ発注に入ります。下の画面が表示されますので、右下の「One-click Order PCB/SMT」をクリックしてください。注文画面に遷移します。

    • 私の時は、なぜかエラーがでて遷移できませんでした。

    • この場合は、先の画面で「Generate Gerber」をクリックしましょう。zipファイルがダウンロードされます。この zipファイルを遷移先の JLCPCBサイトにアップロードして、注文に進みます。

  • JLCPCBサイトに遷移したら、注文内容を選択します。初めての場合は、あまり触らずに注文してよいでしょう。私もよくわかっていませんw

    • 右側に価格が表示されていますので、うっかり高価なオプションを選ばないよう注意しましょう。
    • PCB Qtyは発注枚数です。最低 5枚から選択できます。
    • PCB Colorは基盤の色です。好きな色を選択してください。
  • 作成内容が決まったら、発送方法を選択します。
    最短 1~3営業日(左)、最長 6~8営業日(右)の中から選択します。
    金額に大きく影響しますので、よほど急いでない場合は右(一番下)を選択しましょう。

  • 最後に、支払い方法を選択して(私はクレジットカードを選択しました。JCB不可)、支払い完了となります。


ちなみに今回の発注結果は、下のとおり、計 2.96$(433.8円)となりました。
勉強代にしても安いですよね!

うまくできたかは届いてのお楽しみです!

また後日!

ポータブル式サーモグラフィーの作成

PC修理でショート箇所を見つけるにあたり、発熱箇所がサーモグラフィーで見えると便利だな、と思い、ESP32とユニバーサル基盤もろもろを取り付けて、ポータブル式のカメラを作成しました。

www.youtube.com

仕様ポイント

外観

部品一式

部品 IC名 メーカー 価格
マイコン ESP32-WROOM-32 ACEIRMC ¥1,099
熱感知カメラ AMG8833 Conta ¥4,950
液晶ディスプレイ ILI9431 KKHMF ¥1,999
LDO レギュレータ(to 3.3v) AMS1117-3.3 DC VKLSVAN ¥59
リチウム電池充電器モジュール TP4056(Type-C) Aideepen ¥95
リチウムイオンバッテリ 不明 3.7V 800mAh 不明 (不良Controllerから拝借)

配線

配線ポイント
  • カメラと液晶への電圧は、LDOを経由して3.3Vに降圧した状態で入力する。

  • 3.7VバッテリからESP32(電圧3.6V迄)への入力も、本来降圧して行うべきだが、降圧するとESP32がうまく起動しない(電圧不足?)ため、そのまま入力。

  • 結線ルートは回路図のとおりだが、実際はエナメル線を使って下図のとおりつないでいるため、ぐちゃぐちゃにw

  • (配線図ではわかりませんが)ESP32、カメラ、ディスプレイの3つは、基盤に直付けせず、ピンソケットのうえに差し込むようにする。

制御プログラム

ESP32のプログラムは、今回は手抜きで、AMG8833のスケッチ例をそのまま使いました。

インポートライブラリ

Arduino IDEを起動して、ライブラリの管理から、以下のライブラリを追加でインストールする - Adafruit_AMG88xx_Library - Adafruit_ILI9341

スケッチ

[ファイル]-[スケッチ例]-[Adafruits AMG88xx Library]-[thermal_cam_interpolate]

反省

なんとか実装することができましたが、エナメル線がぐちゃぐちゃで、手直しが難しくなっています。
機会があれば、次はプリント基板の設計で実装したいと思います! また、ESP32の電源まわりはまだ謎が多くて、3.3V降圧だとうまく動かないのもよくわからない…。