CloudFormationでService Catalogの製品を起動してみた

Service Catalogの製品を起動するテンプレートを作成し、Service Catalogの製品として登録してみました。
2023.03.22

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

AWS事業本部コンサルティング部のイシザワです。

CloudFormationからService Catalogの製品を起動することができるようなので、実際に試してみました。 作成したテンプレートから製品を作成することで、他製品を起動する製品を作成してみます。

テンプレート

以下のコード断片が製品の起動を行う箇所になります。

Resources:
  Network:
    Type: AWS::ServiceCatalog::CloudFormationProvisionedProduct
    Properties:
      ProductName: NetworkComponent
      ProvisioningArtifactName: !Ref NetworkComponentVersion
      ProvisioningParameters:
        - Key: SystemId
          Value: !Ref SystemId
        - Key: VpcCidr
          Value: !Ref VpcCidr

ProductNameで製品名を指定します。製品名で製品を特定できない場合(同じ製品名の製品がある場合)は、ProductIdに製品IDを指定することで製品を特定します。

ProvisioningArtifactNameで製品のバージョン名を指定します。製品名と同様にバージョン名でバージョンを特定できない場合は、ProvisioningArtifactIdにバージョンIDを指定することでバージョンを特定します。

ProvisioningParametersは製品に渡すパラメータです。

起動する製品はこのテンプレートのプロビジョニングを実行するプリンシパルから起動可能である必要があります。 つまり、製品はそのプリンシパルからアクセス可能なポートフォリオに含まれている必要があります。

使用感としてはネストスタックとほとんど同じで、Outputsアトリビュートで製品のOutputを参照することができます。

以下がテンプレートの全体です。このテンプレートは2層構造のWebシステムを作成します。起動する製品のバージョン名をパラメータとして受け取ることでユーザー側で製品の個別更新を行えるようにしています。

AWSTemplateFormatVersion: 2010-09-09
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Global Configuration
        Parameters:
          - SystemId
      - Label:
          default: Network Configuration
        Parameters:
          - NetworkComponentVersion
          - VpcCidr
      - Label:
          default: Security Group Configuration
        Parameters:
          - SecurityGroupComponentVersion
      - Label:
          default: Web Server Configuration
        Parameters:
          - WebServerComponentVersion
      - Label:
          default: Database Configuration
        Parameters:
          - DatabaseComponentVersion
          - DatabaseAdminUserPassword
Parameters:
  SystemId:
    Type: String
    Default: sample
  NetworkComponentVersion:
    Type: String
  SecurityGroupComponentVersion: 
    Type: String
  WebServerComponentVersion:
    Type: String
  DatabaseComponentVersion:
    Type: String
  VpcCidr:
    Type: String
  DatabaseAdminUserPassword:
    Type: String
    NoEcho: true

Resources:
  Network:
    Type: AWS::ServiceCatalog::CloudFormationProvisionedProduct
    Properties:
      ProductName: NetworkComponent
      ProvisioningArtifactName: !Ref NetworkComponentVersion
      ProvisioningParameters:
        - Key: SystemId
          Value: !Ref SystemId
        - Key: VpcCidr
          Value: !Ref VpcCidr

  SecurityGroup:
    Type: AWS::ServiceCatalog::CloudFormationProvisionedProduct
    Properties:
      ProductName: SecurityGroupComponent
      ProvisioningArtifactName: !Ref SecurityGroupComponentVersion
      ProvisioningParameters:
        - Key: SystemId
          Value: !Ref SystemId
        - Key: VpcId
          Value: !GetAtt Network.Outputs.VpcId

  WebServer:
    Type: AWS::ServiceCatalog::CloudFormationProvisionedProduct
    Properties:
      ProductName: WebServerComponent
      ProvisioningArtifactName: !Ref WebServerComponentVersion
      ProvisioningParameters:
        - Key: SystemId
          Value: !Ref SystemId
        - Key: VpcId
          Value: !GetAtt Network.Outputs.VpcId
        - Key: PublicSubnetAId
          Value: !GetAtt Network.Outputs.PublicSubnetAId
        - Key: PublicSubnetCId
          Value: !GetAtt Network.Outputs.PublicSubnetCId
        - Key: PrivateSubnetAId
          Value: !GetAtt Network.Outputs.PrivateSubnetAId
        - Key: PrivateSubnetCId
          Value: !GetAtt Network.Outputs.PrivateSubnetCId
        - Key: SGLoadBalancerId
          Value: !GetAtt SecurityGroup.Outputs.SGLoadBalancerId
        - Key: SGWebServerId
          Value: !GetAtt SecurityGroup.Outputs.SGWebServerId

  Database:
    Type: AWS::ServiceCatalog::CloudFormationProvisionedProduct
    Properties:
      ProductName: DatabaseComponent
      ProvisioningArtifactName: !Ref DatabaseComponentVersion
      ProvisioningParameters:
        - Key: SystemId
          Value: !Ref SystemId
        - Key: IsolatedSubnetAId
          Value: !GetAtt Network.Outputs.IsolatedSubnetAId
        - Key: IsolatedSubnetCId
          Value: !GetAtt Network.Outputs.IsolatedSubnetCId
        - Key: SGDatabaseId
          Value: !GetAtt SecurityGroup.Outputs.SGDatabaseId
        - Key: DatabaseAdminUserPassword
          Value: !Ref DatabaseAdminUserPassword

このテンプレートから製品名WebSystem、バージョン名1.0としてService Catalogの製品を作成します。

このテンプレートから起動される製品のテンプレートは以下の通りです。バージョン名は全て1.0とします。

NetworkComponent
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  SystemId:
    Type: String
  VpcCidr:
    Type: String

Resources:
# ------------------------------------------------------------#
#  VPC
# ------------------------------------------------------------#
  Vpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidr
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-vpc


# ------------------------------------------------------------#
#  サブネット
# ------------------------------------------------------------#
  PublicSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Vpc
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Select [ 0, !Cidr [ !Ref VpcCidr, 6, 6]]
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-public-subnet-a

  PublicSubnetC:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Vpc
      AvailabilityZone: ap-northeast-1c
      CidrBlock: !Select [ 1, !Cidr [ !Ref VpcCidr, 6, 6]]
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-public-subnet-c

  PrivateSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Vpc
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Select [ 2, !Cidr [ !Ref VpcCidr, 6, 6]]
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-private-subnet-a

  PrivateSubnetC:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Vpc
      AvailabilityZone: ap-northeast-1c
      CidrBlock: !Select [ 3, !Cidr [ !Ref VpcCidr, 6, 6]]
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-private-subnet-c

  IsolatedSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Vpc
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Select [ 4, !Cidr [ !Ref VpcCidr, 6, 6]]
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-isolated-subnet-a

  IsolatedSubnetC:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Vpc
      AvailabilityZone: ap-northeast-1c
      CidrBlock: !Select [ 5, !Cidr [ !Ref VpcCidr, 6, 6]]
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-isolated-subnet-c


# ------------------------------------------------------------#
#  インターネットゲートウェイ
# ------------------------------------------------------------#
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-igw

  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref Vpc
      InternetGatewayId: !Ref InternetGateway


# ------------------------------------------------------------#
#  NATゲートウェイ
# ------------------------------------------------------------#
  NatGatewayA:
    Type: AWS::EC2::NatGateway
    Properties:
      SubnetId: !Ref PublicSubnetA
      AllocationId: !GetAtt NatGatewayEipA.AllocationId
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-ngw-a

  NatGatewayC:
    Type: AWS::EC2::NatGateway
    Properties:
      SubnetId: !Ref PublicSubnetC
      AllocationId: !GetAtt NatGatewayEipC.AllocationId
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-ngw-c

  NatGatewayEipA:
    Type: AWS::EC2::EIP
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-ngw-eip-a

  NatGatewayEipC:
    Type: AWS::EC2::EIP
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-ngw-eip-c


# ------------------------------------------------------------#
#  ルートテーブル
# ------------------------------------------------------------#
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-public-rtb

  PrivateRouteTableA:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-private-rtb-a

  PrivateRouteTableC:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-private-rtb-c

  IsolatedRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-isolated-rtb

  PublicRouteTableAssociationA:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetA
      RouteTableId: !Ref PublicRouteTable

  PublicRouteTableAssociationC:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetC
      RouteTableId: !Ref PublicRouteTable

  PrivateRouteTableAssociationA:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnetA
      RouteTableId: !Ref PrivateRouteTableA

  PrivateRouteTableAssociationC:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnetC
      RouteTableId: !Ref PrivateRouteTableC

  IsolatedRouteTableAssociationA:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref IsolatedSubnetA
      RouteTableId: !Ref IsolatedRouteTable

  IsolatedRouteTableAssociationA:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref IsolatedSubnetC
      RouteTableId: !Ref IsolatedRouteTable

  RouteToInternetGateway:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  RouteToNatGatewayA:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTableA
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGatewayA

  RouteToNatGatewayC:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTableC
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGatewayC

Outputs:
  VpcId:
    Value: !Ref Vpc
  PublicSubnetAId:
    Value: !Ref PublicSubnetA
  PublicSubnetCId:
    Value: !Ref PublicSubnetC
  PrivateSubnetAId:
    Value: !Ref PrivateSubnetA
  PrivateSubnetCId:
    Value: !Ref PrivateSubnetC
  IsolatedSubnetAId:
    Value: !Ref IsolatedSubnetA
  IsolatedSubnetCId:
    Value: !Ref IsolatedSubnetC
SecurityGroupComponent
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  SystemId:
    Type: String
  VpcId:
    Type: String

Resources:
# ------------------------------------------------------------#
#  セキュリティグループ
# ------------------------------------------------------------#
  SGLoadBalancer:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Load Balancer SG
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
      SecurityGroupEgress:
        - IpProtocol: "-1"
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-loadbalancer-sg

  SGWebServer:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Web Server SG
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          SourceSecurityGroupId: !Ref SGLoadBalancer
      SecurityGroupEgress:
        - IpProtocol: "-1"
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-web-server-sg

  SGDatabase:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Database SG
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
      SecurityGroupEgress:
        - IpProtocol: "-1"
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-database-sg

Outputs:
  SGLoadBalancerId:
    Value: !Ref SGLoadBalancer
  SGWebServerId:
    Value: !Ref SGWebServer
  SGDatabaseId:
    Value: !Ref SGDatabase
WebServerComponent
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  SystemId:
    Type: String
  VpcId:
    Type: String
  PublicSubnetAId:
    Type: String
  PublicSubnetCId:
    Type: String
  PrivateSubnetAId:
    Type: String
  PrivateSubnetCId:
    Type: String
  SGLoadBalancerId:
    Type: String
  SGWebServerId:
    Type: String

Mappings:
  RegionMap:
    ap-northeast-1:
      ImageId: ami-05112363dbe951480

Resources:
# ------------------------------------------------------------#
#  EC2インスタンス
# ------------------------------------------------------------#
  WebServerInstanceA:
    Type: AWS::EC2::Instance
    Properties:
      SubnetId: !Ref PrivateSubnetAId
      InstanceType: t2.micro
      ImageId: !FindInMap
        - RegionMap
        - !Ref AWS::Region
        - ImageId
      SecurityGroupIds:
        - !Ref SGWebServerId
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-web-server-a

  WebServerInstanceC:
    Type: AWS::EC2::Instance
    Properties:
      SubnetId: !Ref PrivateSubnetCId
      InstanceType: t2.micro
      ImageId: !FindInMap
        - RegionMap
        - !Ref AWS::Region
        - ImageId
      SecurityGroupIds:
        - !Ref SGWebServerId
      Tags:
        - Key: Name
          Value: !Sub ${SystemId}-web-server-c

# ------------------------------------------------------------#
#  ELB
# ------------------------------------------------------------#
  TGWebServers:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Sub ${SystemId}-web-server-tg
      VpcId: !Ref VpcId
      TargetType: instance
      Protocol: HTTP
      ProtocolVersion: HTTP1
      Port: 80
      Targets:
        - Id: !Ref WebServerInstanceA
        - Id: !Ref WebServerInstanceC

  LBWebServers:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Sub ${SystemId}-web-server-lb
      Type: application
      Scheme: internet-facing
      Subnets:
        - !Ref PublicSubnetAId
        - !Ref PublicSubnetCId
      SecurityGroups:
        - !Ref SGLoadBalancerId

  LBWebServersListnener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      Protocol: HTTP
      Port: 80
      LoadBalancerArn: !Ref LBWebServers
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref TGWebServers
DatabaseComponent
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  SystemId:
    Type: String
  IsolatedSubnetAId:
    Type: String
  IsolatedSubnetCId:
    Type: String
  SGDatabaseId:
    Type: String
  DatabaseAdminUserPassword:
    Type: String
    NoEcho: true

Resources:
# ------------------------------------------------------------#
#  RDS
# ------------------------------------------------------------#
  RDSInstance:
    Type: AWS::RDS::DBInstance
    Properties:
      Engine: mysql
      EngineVersion: "8.0"
      DBInstanceClass: db.t3.micro
      StorageType: gp2
      AllocatedStorage: 20
      MultiAZ: false
      VPCSecurityGroups:
        - !Ref SGDatabaseId
      DBParameterGroupName: !Ref PGDatabase
      OptionGroupName: !Ref OGDatabase
      DBSubnetGroupName: !Ref DatabaseSubnetGroup
      MasterUsername: admin
      MasterUserPassword: !Ref DatabaseAdminUserPassword

  PGDatabase:
    Type: AWS::RDS::DBParameterGroup
    Properties:
      DBParameterGroupName: !Sub ${SystemId}-db-pg
      Description: MySQL Database PG
      Family: mysql8.0
  
  OGDatabase:
    Type: AWS::RDS::OptionGroup
    Properties:
      OptionGroupName: !Sub ${SystemId}-db-og
      OptionGroupDescription: MySQL Database OG
      EngineName: mysql
      MajorEngineVersion: "8.0"

  DatabaseSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupName: !Sub ${SystemId}-db-subnet-group
      DBSubnetGroupDescription: MySQL Database SubnetGroup
      SubnetIds:
        - !Ref IsolatedSubnetAId
        - !Ref IsolatedSubnetCId

各製品の作成が完了したら、ポートフォリオを作成して製品を登録します。 以下のテンプレートをプロビジョニングしてポートフォリオを作成します。

ポートフォリオ
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  AccessSystemPortfolioPrincipalArn:
    Type: String
  ProviderName:
    Type: String
Mappings:
  Components:
    NetworkComponent:
      ProductId: <NetworkComponentの製品ID>
    SecurityGroupComponent:
      ProductId: <SecurityGroupComponentの製品ID>
    WebServerComponent:
      ProductId: <WebServerComponentの製品ID>
    DatabaseComponent:
      ProductId: <DatabaseComponentの製品ID>
  Systems:
    WebSystem:
      ProductId: <WebSystemの製品ID>
  Portfolios:
    ComponentsPortfolio:
      Name: Components
    SystemsPortfolio:
      Name: Systems

Resources:
# ------------------------------------------------------------#
#  ポートフォリオ
# ------------------------------------------------------------#
  ComponentsPortfolio:
    Type: AWS::ServiceCatalog::Portfolio
    Properties:
      DisplayName: !FindInMap
        - Portfolios
        - ComponentsPortfolio
        - Name
      ProviderName: !Ref ProviderName
      AcceptLanguage: jp

  SystemsPortfolio:
    Type: AWS::ServiceCatalog::Portfolio
    Properties:
      DisplayName: !FindInMap
        - Portfolios
        - SystemsPortfolio
        - Name
      ProviderName: !Ref ProviderName
      AcceptLanguage: jp


# ------------------------------------------------------------#
#  製品の登録
# ------------------------------------------------------------#
  NetworkComponentAssociation:
    Type: AWS::ServiceCatalog::PortfolioProductAssociation
    Properties:
      PortfolioId: !Ref ComponentsPortfolio
      ProductId: !FindInMap
        - Components
        - NetworkComponent
        - ProductId
      AcceptLanguage: jp

  SecurityGroupComponentAssociation:
    Type: AWS::ServiceCatalog::PortfolioProductAssociation
    Properties:
      PortfolioId: !Ref ComponentsPortfolio
      ProductId: !FindInMap
        - Components
        - SecurityGroupComponent
        - ProductId
      AcceptLanguage: jp

  WebServerComponentAssociation:
    Type: AWS::ServiceCatalog::PortfolioProductAssociation
    Properties:
      PortfolioId: !Ref ComponentsPortfolio
      ProductId: !FindInMap
        - Components
        - WebServerComponent
        - ProductId
      AcceptLanguage: jp

  DatabaseComponentAssociation:
    Type: AWS::ServiceCatalog::PortfolioProductAssociation
    Properties:
      PortfolioId: !Ref ComponentsPortfolio
      ProductId: !FindInMap
        - Components
        - DatabaseComponent
        - ProductId
      AcceptLanguage: jp

  WebSystemAssociation:
    Type: AWS::ServiceCatalog::PortfolioProductAssociation
    Properties:
      PortfolioId: !Ref SystemsPortfolio
      ProductId: !FindInMap
        - Systems
        - WebSystem
        - ProductId
      AcceptLanguage: jp


# ------------------------------------------------------------#
#  起動制約
# ------------------------------------------------------------#
  NetworkComponentLaunchRoleConstraint:
    Type: AWS::ServiceCatalog::LaunchRoleConstraint
    Properties:
      LocalRoleName: !Ref ComponentsPortfolioAssociationRole
      PortfolioId: !Ref ComponentsPortfolio
      ProductId: !FindInMap
        - Components
        - NetworkComponent
        - ProductId
      AcceptLanguage: jp

  SecurityGroupComponentLaunchRoleConstraint:
    Type: AWS::ServiceCatalog::LaunchRoleConstraint
    Properties:
      LocalRoleName: !Ref ComponentsPortfolioAssociationRole
      PortfolioId: !Ref ComponentsPortfolio
      ProductId: !FindInMap
        - Components
        - SecurityGroupComponent
        - ProductId
      AcceptLanguage: jp

  WebServerComponentLaunchRoleConstraint:
    Type: AWS::ServiceCatalog::LaunchRoleConstraint
    Properties:
      LocalRoleName: !Ref ComponentsPortfolioAssociationRole
      PortfolioId: !Ref ComponentsPortfolio
      ProductId: !FindInMap
        - Components
        - WebServerComponent
        - ProductId
      AcceptLanguage: jp

  DatabaseComponentLaunchRoleConstraint:
    Type: AWS::ServiceCatalog::LaunchRoleConstraint
    Properties:
      LocalRoleName: !Ref ComponentsPortfolioAssociationRole
      PortfolioId: !Ref ComponentsPortfolio
      ProductId: !FindInMap
        - Components
        - DatabaseComponent
        - ProductId
      AcceptLanguage: jp

  WebSystemLaunchRoleConstraint:
    Type: AWS::ServiceCatalog::LaunchRoleConstraint
    Properties:
      LocalRoleName: !Ref SystemsPortfolioAssociationRole
      PortfolioId: !Ref SystemsPortfolio
      ProductId: !FindInMap
        - Systems
        - WebSystem
        - ProductId
      AcceptLanguage: jp


# ------------------------------------------------------------#
#  ポートフォリオへのアクセス権限
# ------------------------------------------------------------#
  ComponentsPortfolioPrincipalAssociation:
    Type: AWS::ServiceCatalog::PortfolioPrincipalAssociation
    Properties:
      PortfolioId: !Ref ComponentsPortfolio
      PrincipalType: IAM
      PrincipalARN: !GetAtt SystemsPortfolioAssociationRole.Arn
      AcceptLanguage: jp

  SystemsPortfolioPrincipalAssociation:
    Type: AWS::ServiceCatalog::PortfolioPrincipalAssociation
    Properties:
      PortfolioId: !Ref SystemsPortfolio
      PrincipalType: IAM
      PrincipalARN: !Ref AccessSystemPortfolioPrincipalArn
      AcceptLanguage: jp


# ------------------------------------------------------------#
#  起動制約用のIAMロール
# ------------------------------------------------------------#
  ComponentsPortfolioAssociationRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: components-portfolio-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - servicecatalog.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonEC2FullAccess
        - arn:aws:iam::aws:policy/AmazonRDSFullAccess
        - arn:aws:iam::aws:policy/AWSCloudFormationFullAccess
        - !Ref ServiceCatalogManagedBucketAccessPolicy

  SystemsPortfolioAssociationRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: systems-portfolio-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - servicecatalog.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSServiceCatalogEndUserFullAccess
        - arn:aws:iam::aws:policy/AWSCloudFormationFullAccess
        - !Ref ServiceCatalogManagedBucketAccessPolicy

  ServiceCatalogManagedBucketAccessPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: service-catalog-managed-bucket-access-policy
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
              - s3:GetObject
            Resource: "*"
            Condition:
              StringEquals:
                s3:ExistingObjectTag/servicecatalog:provisioning: true

このテンプレートによりSystemポートフォリオとComponentsポートフォリオが作成されます。Systemポートフォリオは製品WebSystemのみを登録し、ComponentsポートフォリオにはWebSystemから起動される製品群が登録されます。

起動制約によりユーザーから起動できる製品をSystemポートフォリオに含まれる製品、すなわちWebSystemのみに設定しています。

起動してみた

以下のパラメータでWebSystemを起動します。

プロビジョニング完了後、複数の製品がプロビジョニングされていることが確認できました。

テンプレート分割をした製品について

CloudFormationProvisionedProductを使うことで製品どうしを組み合わせることができます。 その応用として、今回やったように製品から他製品を起動する(以下、ネスト起動)ことでテンプレートの分割をすることができます。

他にテンプレート分割をした製品を作成する方法として、下記ブログにあるようにネストスタックを行う方法があります。

Service Catalogでスタックをネストした製品を起動してみた

この方法と比較して、今回のネスト起動によるテンプレート分割のメリット・デメリットを挙げていきます。

メリット

同じバージョンの製品で同じ構造のスタックがプロビジョニングされることを保証しやすい

ネストスタックを使った方法だと、Service Catalogの製品である参照元のテンプレートはバージョン管理されますが、S3バケットにある参照先のテンプレートはバージョン管理されていません。 そのため、同じバージョンの製品を同じパラメータで起動したとしても、参照先のテンプレートに更新があった場合に異なる構造のスタックがプロビジョニングされてしまう可能性があります。

その点、ネスト起動だと参照元と参照先の両方がバージョン管理されているため、同じ構造のスタックがプロビジョニングされることを保証しやすいです。

デメリット

Service Catalogの利用料金が増える

Service Catalogの利用料金はAPIの呼び出し回数によって決まるので、製品の起動回数が多いネスト起動の方が利用料金が増えます。

マルチリージョンでの管理が煩雑になる

Service Catalogはリージョン間で製品を共有することはできないので、他リージョンで製品を利用したい場合は製品を再作成する必要があります。

ネスト起動の場合は参照元と参照先の製品を再作成する必要がありますが、ネストスタックの場合は参照元のみを再作成するだけで済みます。

ネスト起動は管理する製品数が多いので、リージョン間で製品の同一性を管理するためのコストも増大します。

まとめ

CloudFormationProvisionedProductを使った製品起動を実際に試し、製品のテンプレート分割に関する考察を行いました。 CloudFormationProvisionedProductの利用の参考になれば幸いです。

他の利用例として以下のAWSブログ記事を挙げます。

AWS CloudFormation support for AWS Service Catalog products