🌤️

CloudFormation(&CDK)からCloudFlareリソースを作成する方法。

2023/10/01に公開

CloudFormationでCloudFlareリソースを作成する。

本ページで「ドキュメント」と表現する大半は以下の派生ページからとなります。(※引用元)
https://github.com/aws-ia/cloudformation-cloudflare-resource-providers

2023年9月30日時点で存在するCloudFormationレジストリのパブリックサードパーティ拡張機能として登録されているリソースタイプは以下4つのようです。


事前準備

①CloudFlare側でToken発行

手順はDNS::Recordを例にします。
※全ての権限をひとつのTokenとして生成して、後述するSetTypeConfigurationにての4つ全てに同じTokenを与える事も出来るかと思いますが、それぞれ細かく作成するのがベストであるのではと思います。














②ロール&有効化&TokenをSecrets Managerに格納

CloudFlareの全リソースタイプを一撃で有効化するテンプレートを作りました。
※前章でTokenを全て統一で作成された方は4つのパラメーター全てに同じTokenを入力ください。
※TypeListパラメーターは触らずで大丈夫です。

Role_and_Activate.yml
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::LanguageExtensions
# ------------------------------------------------------------#
# Metadata:
# ------------------------------------------------------------#
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Stack
        Parameters:
          - RecordToken
          - LoadBalancerToken
          - MonitorToken
          - PoolToken
          - TypeList
# ------------------------------------------------------------#
# Parameters:
# ------------------------------------------------------------#
Parameters:
  RecordToken:
    Type: String
    NoEcho: true

  LoadBalancerToken:
    Type: String
    NoEcho: true

  MonitorToken:
    Type: String
    NoEcho: true

  PoolToken:
    Type: String
    NoEcho: true

  TypeList:
    Type: List<String>
    Default: Record,LoadBalancer,Monitor,Pool
# ------------------------------------------------------------#
# Mappings:
# ------------------------------------------------------------#
Mappings: 
  # ------------------------------------------------------------#
  # ResourcesMappings
  # ------------------------------------------------------------#
  ResourcesMappings: 
    Record:
      2ndstr: Dns
    LoadBalancer: 
      2ndstr: LoadBalancer
    Monitor: 
      2ndstr: LoadBalancer
    Pool: 
      2ndstr: LoadBalancer
# ------------------------------------------------------------#
# Resources
# ------------------------------------------------------------#
Resources:
  # ------------------------------------------------------------#
  # Secret
  # ------------------------------------------------------------#
  Secret:
    Type: AWS::SecretsManager::Secret
    Properties: 
      Name: CloudFlareToken
      SecretString: !Sub '{"Record":"${RecordToken}","LoadBalancer":"${LoadBalancerToken}","Monitor":"${MonitorToken}","Pool":"${PoolToken}"}'

  Fn::ForEach::ResourcesLoop:
    - TypeItem
    - !Ref TypeList
    # ------------------------------------------------------------#
    # TypeActivation
    # ------------------------------------------------------------#
    - ${TypeItem}TypeActivation: 
        Type: AWS::CloudFormation::TypeActivation
        Properties: 
          AutoUpdate: true
          ExecutionRoleArn: !GetAtt [!Sub '${TypeItem}ExecutionRole', Arn]
          PublicTypeArn: !Sub 
            - arn:aws:cloudformation:${AWS::Region}::type/resource/c830e97710da0c9954d80ba8df021e5439e7134b/Cloudflare-${2ndstr}-${TypeItem}
            - 2ndstr: !FindInMap [ResourcesMappings,!Ref TypeItem,2ndstr]
          Type: RESOURCE
          TypeName: !Sub 
            - Cloudflare::${2ndstr}::${TypeItem}
            - 2ndstr: !FindInMap [ResourcesMappings,!Ref TypeItem,2ndstr]
    # ------------------------------------------------------------#
    # Role
    # ------------------------------------------------------------#
      ${TypeItem}ExecutionRole:
        Type: AWS::IAM::Role
        Properties:
          MaxSessionDuration: 8400
          AssumeRolePolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Principal:
                  Service: resources.cloudformation.amazonaws.com
                Action: sts:AssumeRole
                Condition:
                  StringEquals:
                    aws:SourceAccount:
                      Ref: AWS::AccountId
                  StringLike:
                    aws:SourceArn: !Sub 
                      - arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:type/resource/Cloudflare-${2ndstr}-${TypeItem}/*
                      - 2ndstr: !FindInMap [ResourcesMappings,!Ref TypeItem,2ndstr]
          Path: "/"
          Policies:
            - PolicyName: ResourceTypePolicy
              PolicyDocument:
                Version: '2012-10-17'
                Statement:
                  - Effect: Deny
                    Action:
                      - "*"
                    Resource: "*"


③ターミナル(SetTypeConfiguration)操作

SetTypeConfigurationについてはこちら
https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_SetTypeConfiguration.html
https://docs.aws.amazon.com/cli/latest/reference/cloudformation/set-type-configuration.html

以下をターミナルで実行してください。ちなみに私shell全然詳しくないです。
一生懸命作りましたがセンスのない書き方になっているであろう点はご容赦ください。
(書き換えてくださるのは大歓迎です。)

※①[awsのアカウントID]には12桁のアカウントIDをハイフン無しで入力ください。
※②[選択リージョン]は例えばus-east-1。

SetTypeConfiguration.sh
#!/bin/bash
awsaccountid="[awsのアカウントID]"
region="[選択リージョン]"

types=(
    "Dns::Record"
    "LoadBalancer::LoadBalancer"
    "LoadBalancer::Monitor"
    "LoadBalancer::Pool"
)

echo "Tokenを取得します。"
Secrets=`aws secretsmanager get-secret-value --region ${region} --secret-id CloudFlareToken | jq '.SecretString'| tr -d '{}" \\' `
Secret=(${Secrets//,/ })

echo "set-type-configurationを開始します。"
i=0
while [[ $i -lt 4 ]]
do
    Token=`echo ${Secret[$i]} | awk '{print substr($0,index($0,":")+1)}'`
    aws cloudformation set-type-configuration --region ${region} --type RESOURCE --type-name Cloudflare::${types[$i]} --configuration-alias default --configuration "{ \"CloudflareAccess\":{\"Url\":\"https://api.cloudflare.com/client/v4\",\"ApiKey\":\"${Token}\"}}"
    i=`expr $i + 1`
done

echo "set-type-configurationの結果(batch-describe-type-configurations)を確認ください。"
for type in "${types[@]}" ; do
    aws cloudformation batch-describe-type-configurations --region ${region} --type-configuration-identifiers TypeArn=arn:aws:cloudformation:${region}:${awsaccountid}:type/resource/Cloudflare-${type}
done

$bash SetTypeConfiguration.sh

してください。

※ちなみにTokenに付与する権限範囲の変更などがあった場合、SecretsManagerのValueを変更いただき、再度↑を走らせていただければ反映されると想定しています。


4つのリソースタイプ

Cloudflare::Dns::Record(←のみ実際の実行もしてみます)

https://github.com/aws-ia/cloudformation-cloudflare-resource-providers/tree/main/Cloudflare-Dns-Record

https://www.youtube.com/watch?v=VjbHQFDGY8E

以下は上記githubにサンプルとして置かれたものです。
今回はこのリソースタイプのみ作成を試してみたいと思います。

私は事前にドメインをひとつ取得してある状態です。
ドキュメント上のサンプル(↓)にはそれぞれのプロパティに対する値の末尾に","がありますが余分です。

Record.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: Shows how to create a Dns Record in Cloudflare
Resources:
  MySampleProject:
    Type: Cloudflare::Dns::Record
    Properties:
      ZoneId: 0012345678123,
      Type: A,
      Name: example.app.com,
      Content: 11.116.111.1,
      Proxied: false,
      Ttl: 600


無事作成されたようです。


Cloudflare::LoadBalancer::LoadBalancer

https://github.com/aws-ia/cloudformation-cloudflare-resource-providers/tree/main/Cloudflare-LoadBalancer-LoadBalancer
[sample]

LoadBalancer.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: Shows how to create an LoadBalancer in Cloudflare
Resources:
  Type: Cloudflare::LoadBalancer:LoadBalancer
  MySampleProject:
    ZoneId: !Ref ZoneId
    Proxied: true
    Name: !Ref DomainName
    SessionAffinity: none
    SessionAffinityAttributes:
      ZeroDowntimeFailover: none
    SteeringPolicy: off
    RandomSteering:
      DefaultWeight: 1
    DefaultPools:
      - !GetAtt Pool.Id
    FallbackPool: !GetAtt Pool.Id
    Enabled: true

Cloudflare::LoadBalancer::Monitor

https://github.com/aws-ia/cloudformation-cloudflare-resource-providers/tree/main/Cloudflare-LoadBalancer-Monitor
[sample]

Monitor.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: Shows how to set a LoadBalancer Monitor in Cloudflare.
Resources:
  AgentConfigurationSample:
    Type: Cloudflare::LoadBalancer::Monitor
    Properties:
      ExpectedCodes: 200
      Method: GET
      Timeout: 5
      Path: /
      Interval: 60
      Retries: 2
      Description: GET over HTTPS - expect 200
      Type: http
      Port: 80
      AccountIdentifier: !Ref AccountId
      FollowRedirects: false
      AllowInsecure: false
      ProbeZone: ""

Cloudflare::LoadBalancer::Pool

https://github.com/aws-ia/cloudformation-cloudflare-resource-providers/tree/main/Cloudflare-LoadBalancer-Pool
[sample]

Pool.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: Shows how to create a static LoadBalancer Pool in Cloudflare
Resources:
  SampleNrqlCondition:
    Type: Cloudflare::LoadBalancer::Pool
    Properties:
      AccountIdentifier: !Ref AccountId
      MinimumOrigins: 1
      CheckRegions:
        - WEU
      NotificationEmail: email@example.com
      NotificationFilter:
        Pool:
          Healthy: false
      Origins:
        - Enabled: true
          Address: <IP Address>
          Name: server-1
          Weight: 0.9
        - Weight: 0.1
          Enabled: true
          Name: server-2
          Address: <IP Address>
      Name: Servers
      Description: Region Pool
      Monitor: !GetAtt Monitor.IdALL
            Priority: CRITICAL

CDKの場合は

私もcdkはまだまだ触りはじめの為、もっと良い書き方があるかもしれません。
使っているのは両方ともL1コンストラクトです。

凄く初歩的な事かもしれませんがこちら

for 文では一回のループごとに定数や変数が破棄されるため、同じ定数名でも宣言することができます。

のように書いてあった為、↓のようにしてみた所、cdksynth->deploy共に意図通りとなりました。
以下ではCFnとは違い機密情報であるTokenを埋め込みにはしていません。

SecretsManagerへのToken格納と、SetTypeConfiguration時の呼び出しだけ整合がつくよう行っていただければ同様となるかと思います。

cdk-cloudflare-stack.ts
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { aws_cloudformation as cloudformation } from 'aws-cdk-lib';;
import { aws_iam as iam } from 'aws-cdk-lib';

const region = '[リージョン]';
const accountid ='[12桁のAWSアカウントIDをハイフンなしで]';

export class CdkCloudflareStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const types : Array<[string,string]> = [
      ["Dns",'Record'],
      ['LoadBalancer','LoadBalancer'],
      ['LoadBalancer','Monitor'],
      ['LoadBalancer','Pool']
    ]

    for(const type of types) {
      const Role = new iam.CfnRole(this, 'CfnRole'+type[1], {
        maxSessionDuration: 8400, 
        assumeRolePolicyDocument: {
          Version: '2012-10-17',
          Statement: [
            {
              Effect: 'Allow',
              Principal: { Service:['resources.cloudformation.amazonaws.com'] },
              Action: ['sts:AssumeRole'],
              Condition: {
                StringEquals:{
                  'aws:SourceAccount': accountid,
                },
                StringLike:{
                  'aws:SourceArn': 'arn:aws:cloudformation:'+region+':'+accountid+':type/resource/Cloudflare-'+type[0]+'-'+type[1]+'/*',
                },
              },
            },
          ],
        },
        path: '/', 
        policies: [{
          policyName: 'ResourceTypePolicy',
          policyDocument: {
            Version: '2012-10-17',
            Statement:[
              {
              Effect:  'Deny',
              Action: ['*'],
              Resource: "*",
              },
            ],
          },
        }],
      });

      new cloudformation.CfnTypeActivation(this, 'CfnTypeActivation'+type[1], {
        autoUpdate: true,
        executionRoleArn: Role.attrArn,
        publicTypeArn: 'arn:aws:cloudformation:'+region+'::type/resource/c830e97710da0c9954d80ba8df021e5439e7134b/Cloudflare-'+type[0]+'-'+type[1],
        type: 'RESOURCE',
        typeName: 'Cloudflare::'+type[0]+'::'+type[1],
      });
    }
  }
}

TypeActivation完了後のリソース利用については釈迦に説法とは思いますが、以下を参照いただければ幸いです。
https://constructs.dev/


Cloudflare MeetupのLT資料

https://speakerdeck.com/yushikatoaws/cloudformationtocdkde-cloudflarerisosuwo-zuo-cheng-sitemita


以上でした。

有難うございました。

Discussion