【AWS】CloudFormationでAWS MarketplaceのAMIを使用する

みなさん、CloudFormation使ってますか？

AWSで仮想サーバーを動かしたいな～という時に、『EC2インスタンスを立ち上げて……』『Elastic IPアドレスを作成してEC2インスタンスへ関連付けして……』『Elastic IPアドレスのIPv4アドレスとドメインをDNSレコードで結びつけて……』……
1回だけならまだしも、EC2インスタンスを色々な種類で試したいときには、ちょっと面倒ですよね。

CloudFormationはテンプレートファイルに従ってAWSやサードパーティのリソースを作成・設定してくれるすごいやつです。

今回はCloudFormationを使って、AWS Marketplaceに存在するAmazon マシンイメージ(AMI)からリソースを作成してみます。

# AWS Marketplaceについて
AWSで実行するソフトウェアを購入できるオンラインストアのことです。
このストア上には無料 / 有料AMIも数多く存在します。

[EC2コンソールのインスタンス起動ウィザード](https://console.aws.amazon.com/ec2/v2/home#LaunchInstanceWizard:)を表示すると、こんな画面が現れますよね。
デフォルトでは左ペインで『クイックスタート』が選ばれていて、ここでは一般的なAMIが表示されます。
![bb392b1e-6135-4aca-a47b-b52a0b0d2079.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534500/98bf9182-70b0-4c71-8dd3-67a78a1d7c24.png)

左ペインで『AWS Marketplace』を選ぶと、AWS Marketplace上に存在するAMIが表示されます。![3cd921cf-a2e7-457c-8dc6-b67266715140.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534500/02e14acd-d4df-203e-9bb6-eec2df380fed.png)

CloudFormation慣れしている方なら、ここで「あ～タイトルの横にあるami-XXXXXXってやつがAMI IDだから、それをCloudFormationのテンプレートで当てはめるだけだな！」となるかもしれませんが、AWS Marketplaceではそれはできません。
（そもそもタイトルの横にAMI IDが書かれていない）

これをどうするかがこの投稿の内容です。

# CloudFormationについて
この投稿ではCloudFormationの初歩的な文法等はあまり触れません。
CloudFormationを一回も利用したことがなく、何も知らないという方は、他記事を読んで簡単に概要を知っておくと良いかもしれません。

【参考】
[はじめてのAWS CloudFormationチュートリアル - Qiita](https://qiita.com/shonansurvivors/items/dc32e29fbb02cafdea17)
[初学者のためのAWS入門(2) CloudFormation入門-1 - Qiita](https://qiita.com/mshinoda88/items/c5b238212c2de850efdd)
[(2017年12月時点) 私的 CloudFormation ベストプラクティス - Qiita](https://qiita.com/yasuhiroki/items/8463eed1c78123313a6f#fn1)


# 事前準備
- EC2インスタンスで使用するキーペア<br>
作成は[EC2コンソールの『キーペアを作成』画面](https://console.aws.amazon.com/ec2/v2/home#CreateKeyPair:)から行えます。

# 手順
例として、さっきの画像にちらっと写っていた『WordPress Certified by Bitnami』のAMIを立ち上げる手順を記載します。

## AWS Marketplaceで目的のAMIのページを探す
まずは[AWS Marketplace](https://aws.amazon.com/marketplace)にアクセスして、検索バーなどを駆使して目的の商品ページまで移動します。
![f511c653-f333-41fb-873d-6afcba8dc8b3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534500/58109467-db7c-9be4-3968-700e9edfbe4e.png)
右上に『Typical Total Price』と書かれていますが、これはEC2インスタンス代も含めた値段です。
『Pricing』部分を確認すると……
![2021-06-19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534500/e64d5c92-98e9-51cc-4d28-6bb9d82b0602.png)
どうやらこのAMIを利用するソフトウェア代は無料のようですね。安心して使えます。

## Subscribeを行う
CloudFormationで対象のAMIを利用するには、Subscribeが必要となります。
Subscribeせずに手に入れたAMI IDをCloudFormationで利用すると、EC2インスタンス作成部分でエラーが発生します。

というわけで、さきほどのページの右上の『Continue to Subscribe』を押します。
![FireShot Capture 004 - AWS Marketplace_ WordPress Certified by Bitnami - aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534500/5e5497b9-391a-91b8-a791-cb5d4f2cfba1.png)
条件(Terms)・値段が記載されているので確認し、『Accept Terms』をクリックします。

クリック後しばらく待つと、Subscribeが完了しこの画面になります。
![FireShot Capture 005 - AWS Marketplace_ WordPress Certified by Bitnami - aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534500/b751e043-d183-5006-7aed-862cb83d5a19.png)
『Continue to Configuration』を押すと、念願のAMI ID表示画面となります。
ソフトウェアバージョン、リージョン(日本ならAsia Pacific (Tokyo)でOK)を選び、AMI IDをメモっておきましょう。
![FireShot Capture 006 - AWS Marketplace_ WordPress Certified by Bitnami - aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534500/306e4714-1aec-43d5-5836-d83db7ddb1cb.png)

## CloudFormationテンプレートファイルの作成
あとはCloudFormationテンプレートファイルを作成し、そこでAMI IDを利用するだけです。
…………と、終わってしまうのは流石に不親切なので、以下の内容でリソースを作成するテンプレートファイルを記載しておきます。
AWS Marketplaceの色々なAMIで使用できるように汎用的なテンプレートにしています。

色んなAMIを触る際の手間を失くすためのテンプレートとして作っているため、こだわりたい場合はこれを参考に書き換えてみてください。

- パラメーター
  - プロジェクト名<br>
    この値を利用してリソースに名前をつけます。
  - AMI ID<br>
    このIDでEC2インスタンスを作成します。
  - インスタンスタイプ<br>
    記入したインスタンスタイプでEC2インスタンスを作成します。<br>
    インスタンスタイプによってスペック・値段が変わるため、どれにするかは『[インスタンスタイプ - Amazon EC2 | AWS](https://aws.amazon.com/jp/ec2/instance-types/)』を見て決定してください。<br>
    お試しであれば、汎用の最低スペックであるt2.nanoか、無料枠があるならそのひとつ上のt2.microで良いと思います。
  - キーペア<br>
    EC2インスタンスとSSH接続する際に利用するキーペア。
  - SSH接続可能なIPアドレス<br>
    EC2インスタンスのSSHポート(ポート22)へアクセスできるIPアドレス。<br>
    デフォルト値の0.0.0.0/0はどのIPからでもアクセス可能です。
  - ドメイン名<br>
    後述するパラメータの『Route53のホストゾーン』内に、ドメインとElastic IPを結びつけるDNSレコードを作成します。
  - Route53のホストゾーン<br>
    このホストゾーン内に上記DNSレコードを作成します。

- 作成するリソース
  - EC2インスタンス
  - インスタンスに関連付けるElastic IP
  - インスタンスに関連付けるセキュリティグループ<br>
    セキュリティグループとはインスタンスのファイアウォールみたいなものです。<br>
    今回はポート80(HTTP) / ポート443(HTTPS) /ポート22(SSH)を解放しています。
  - Route53のDNSレコード

もし、ドメインを持っていない・IPだけで一旦試してみたい・Route53以外でドメインを管理しているからRoute53部分は必要ないという方は、自分の[GitHubリポジトリ](https://github.com/hamurock/cloudformation-template)にRoute53部分を削除したテンプレートを用意しています。

```yml:AWS_Marketplace_Stack.cf.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Create AWS Marketplace insntace.
Parameters:
  ProjectName:
    Description: Name of this project. [ProjectName] is used for the naming of each resource.
    Type: String
    MinLength: "1"
    Default: WordPress
  EC2InstanceAmiId:
    Description: AMI ID of AWS Marketplace.
    Type: String
    MinLength: "1"
  EC2InstanceType:
    Description: "WebServer EC2 instance type.\n
      Learn more >> https://aws.amazon.com/jp/ec2/instance-types/"
    Type: String
    MinLength: "1"
    Default: t2.micro
  EC2InstanceKeyName:
    Description: "Name of an existing EC2 KeyPair to enable SSH access to EC2 instance.\n
      If you want to use a new EC2 KeyPair, create it from EC2 console.\n
      >> https://console.aws.amazon.com/ec2/v2/home#CreateKeyPair:"
    Type: "AWS::EC2::KeyPair::KeyName"
    MinLength: "1"
    ConstraintDescription: must be the name of an existing EC2 KeyPair.
  SSHLocation:
    Description: The IP address range that can be used to SSH to EC2 instance.
    Type: String
    MinLength: "9"
    MaxLength: "18"
    Default: 0.0.0.0/0
    AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})'
    ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x.
  DomainName:
    Description: The domain name used to add DNS record to Route 53's hosted zone.
    Type: String
    MinLength: "1"
    AllowedPattern: '[a-zA-Z0-9\-\.]*'
    ConstraintDescription: only domain characters.
  Route53HostedZoneID:
    Description: The Route 53's hosted zone ID to add domain's DNS record to Route 53's hosted zone.
    Type: "AWS::Route53::HostedZone::Id"
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: Project Configuration
        Parameters:
          - ProjectName
      - Label:
          default: Amazon EC2 Configuration
        Parameters:
          - EC2InstanceAmiId
          - EC2InstanceType
          - EC2InstanceKeyName
          - SSHLocation
      - Label:
          default: Route53 Configuration
        Parameters:
          - DomainName
          - Route53HostedZoneID
Resources:
  EC2SecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupName: !Sub "SecurityGroup-${ProjectName}"
      Tags:
        - Key: Name
          Value: !Sub "SecurityGroup-${ProjectName}"
      GroupDescription: Enable HTTP / HTTPS / SSH access
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: "80"
          ToPort: "80"
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: "443"
          ToPort: "443"
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: "22"
          ToPort: "22"
          CidrIp: !Ref SSHLocation
  EC2Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      ImageId: !Ref EC2InstanceAmiId
      InstanceType: !Ref EC2InstanceType
      SecurityGroups:
        - !Ref EC2SecurityGroup
      KeyName: !Ref EC2InstanceKeyName
      Tags:
        - Key: Name
          Value: !Sub "Instance-${ProjectName}"
  EC2EIP:
    Type: "AWS::EC2::EIP"
    Properties:
      InstanceId: !Ref EC2Instance
      Tags:
        - Key: Name
          Value: !Sub "EIP-${ProjectName}"
  Route53Record:
    Type: "AWS::Route53::RecordSet"
    Properties:
      HostedZoneId: !Ref Route53HostedZoneID
      Name: !Ref DomainName
      Type: A
      TTL: "900"
      ResourceRecords:
        - !Ref EC2EIP
Outputs:
  WebsiteURL:
    Description: "URL using EC2 instance's [PublicDnsName]"
    Value: !Join [ "", [ "http://", !GetAtt EC2Instance.PublicDnsName ] ]
```

ポイントとしては、CreationPolicyやヘルパースクリプトを使用していないところです。

Amazon Linux 2のEC2インスタンスを作成するテンプレート例として、以下のようにCloudFormationヘルパースクリプトから成功シグナルを送る実装がよくあります。
[AWSのCloudFormationテンプレート](https://aws.amazon.com/jp/cloudformation/resources/templates/)の[LAMP環境構築サンプル](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/sample-templates-appframeworks-ap-northeast-1.html)とかであるやつですね。

```yml:CloudFormationヘルパースクリプトからシグナルを送る例
Resources:
  EC2Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      ImageId: !Ref EC2InstanceAmiId
      InstanceType: !Ref EC2InstanceType
      SecurityGroups:
        - !Ref EC2SecurityGroup
      KeyName: !Ref KeyName
      Tags:
        - Key: Name
          Value: !Sub "Instance-${ProjectName}"
      UserData: !Base64
        "Fn::Join":
          - ""
          - - "#!/bin/bash -xe\n"
            - "yum update -y aws-cfn-bootstrap\n"
            - "\n"
            - "set +e\n"
            - "/opt/aws/bin/cfn-init -v"
            - !Sub "  --stack ${AWS::StackName}"
            - "  --resource EC2Instance"
            - "  --configsets setup"
            - !Sub "  --region ${AWS::Region}"
            - "\n"
            - "# Signal the status from cfn-init\n"
            - "/opt/aws/bin/cfn-signal -e $?"
            - !Sub "  --stack ${AWS::StackName}"
            - "  --resource EC2Instance"
            - !Sub "  --region ${AWS::Region}\n"
    CreationPolicy:
      ResourceSignal:
        Timeout: PT10M
    Metadata:
      "AWS::CloudFormation::Init":
        configSets:
          setup:
            - install_cfn
        install_cfn:
          files:
            /etc/cfn/cfn-hup.conf:
              content: !Join
                - ""
                - - "[main]\n"
                  - !Sub "stack=${AWS::StackId}\n"
                  - !Sub "region=${AWS::Region}\n"
              mode: "000400"
              owner: root
              group: root
            /etc/cfn/hooks.d/cfn-auto-reloader.conf:
              content: !Join
                - ""
                - - "[cfn-auto-reloader-hook]\n"
                  - "triggers=post.update\n"
                  - "path=Resources.EC2Instance.Metadata.AWS::CloudFormation::Init\n"
                  - "action=/opt/aws/bin/cfn-init -v"
                  - !Sub "  --stack ${AWS::StackName}"
                  - "  --resource EC2Instance"
                  - "  --configsets setup"
                  - !Sub "  --region ${AWS::Region}\n"
                  - "runas=root\n"
              mode: "000400"
              owner: root
              group: root
          services:
            sysvinit:
              cfn-hup:
                enabled: "true"
                ensureRunning: "true"
                files:
                  - /etc/cfn/cfn-hup.conf
                  - /etc/cfn/hooks.d/cfn-auto-reloader.conf

```

Amazon Linux 2以外のOSはヘルパースクリプトはインストールされていないので、AWS Marketplaceに網羅的に対応するために上記例のような実装はテンプレートに入れていません。

AWS Marketplaceで販売されているAMIにヘルパースクリプトをインストールするのも、チューニング済みAMIに不要なものを付け加えているような感じがしますしね。
curlで[SignalResource API](https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_SignalResource.html)を叩くことも考えましたが、必要なパラメーターが多くなりすぎるということでやめました。

という感じで、UserData処理やCreationPolicyによる成功シグナル待機などは行っていません。
(同じようにしているテンプレートを見つけられなかったため少し不安です。もしツッコミどころがありましたらコメントお願いします)

もし対象のAWS MarketplaceのAMIが決まっていて、それ専用にテンプレートファイルを作成するという場合には、色々処理を追加してしまってもよいと思います。

# 以降の手順
あとはもう[CloudFormationコンソールの『スタックの作成』画面](https://console.aws.amazon.com/cloudformation/home#/stacks/create/template)からテンプレートを指定してパラメーターを入力して実行するだけで、リソースが出来上がっちゃいます。
この部分や構築後にSSH接続する方法が分からない場合は、さきほどのCloudFormation記事やEC2インスンタンス記事を読んでみてください。

# まとめ
AWS MarketplaceのAMI IDを利用するためには、そのソフトウェアへSubscribeする必要がある。