2021-02-18

2020-10-13

技術ノート

eyecatch
AWS が公開している、AWS サービスの IP アドレスレンジ (ip-range.json) に PowerShell でアクセスし、セキュリティグループを自動生成する方法を紹介します。

はじめに

AWS サービスの現在のグローバル IP レンジは以下で公開されています。

基本的に、公式ドキュメントを読めば IP レンジの絞り方はだいたいわかります。公式のナレッジでも jq コマンドを使った IP レンジの抽出方法が紹介されており、最後のほうにちょろっと PowerShell でのやり方も載っています。体感としては、柔軟に操作可能なオブジェクトを返してくれる PowerShell を活用したほうが、テキストベースで json をパースするよりも簡単だと感じました。なので、今回はその活用例を書いてみたいと思います。サンプルケースとして、東京リージョンの S3 の Ipv4 だけを取得して自動でセキュリティグループを作成してみたいと思います。

作業環境は以下です。Ubuntu マシンにまだ PowerShell を入れてないというのもあって、検証環境が Windows と Linux で別れてしまっているのでそこはご了承ください。curl とか jq コマンドは Ubuntu でやっていて、PowerShell は Windows でやっています。

作業マシン ツール
Ubuntu 18.04.5 LTS curl 7.58.0, jq 1.5.1
Windows 10 Pro 2004 Windows PowerShell 5.1 (.NET Framework 4.8)

なお、AWS の API を PowerShell で操作するには AWS Tools for PowerShell が必要です。Windows における環境設定はこちらで書いていますのでご覧ください。

AWS Tools for PowerShell で作成した今回のようなコードは以下のリポジトリで管理しています。日々ブラッシュアップするため、記事の内容と相違している可能性がありますのでご了承ください。

IP レンジを取得してみる

curl+jq でやった場合と PowerShell でやった場合です。

普通に curl と jq でやる

まず抽出とか考えずに単純にアクセスしてみます。


curl "https://ip-ranges.amazonaws.com/ip-ranges.json"



{
  "syncToken": "1601680276",
  "createDate": "2020-10-02-23-11-16",
  "prefixes": [
    {
      "ip_prefix": "3.5.140.0/22",
      "region": "ap-northeast-2",
      "service": "AMAZON",
      "network_border_group": "ap-northeast-2"
    },
    {
      "ip_prefix": "35.180.0.0/16",
      "region": "eu-west-3",
      "service": "AMAZON",
      "network_border_group": "eu-west-3"
    },
    ・・・
    # 延々と続く
  ]
}

大きな json が返ります。これを jq でパースして、目的の IP レンジを抽出します。やっていることは公式のナレッジ通りです。


curl "https://ip-ranges.amazonaws.com/ip-ranges.json" | jq -r '.prefixes[] | select(.region=="ap-northeast-1") | select(.service=="S3") | .ip_prefix'



52.92.60.0/22
52.219.68.0/22
52.219.16.0/22
3.5.152.0/21
52.219.0.0/20
52.219.136.0/22

jq でパースするのが、慣れていないと面倒かもしれません。私のことですが。

PowerShell でやる

今度は PowerShell で同じことをしてみます。まずはただただアクセスしてみます。


Get-AWSPublicIpAddressRange



IpPrefix           : 3.5.140.0/22
IpAddressFormat    : Ipv4
Region             : ap-northeast-2
Service            : AMAZON
NetworkBorderGroup : ap-northeast-2

IpPrefix           : 35.180.0.0/16
IpAddressFormat    : Ipv4
Region             : eu-west-3
Service            : AMAZON
NetworkBorderGroup : eu-west-3
・・・
# 延々と続く

パラメータなしで打てて簡単です。戻り値が PowerShell のオブジェクトなので可読性が高いです。条件で絞り込んでみます。


Get-AWSPublicIpAddressRange -ServiceKey "S3" -Region "ap-northeast-1" | 
Where-Object -FilterScript { $_.IpAddressFormat -eq "Ipv4" }



IpPrefix           : 52.92.60.0/22
IpAddressFormat    : Ipv4
Region             : ap-northeast-1
Service            : S3
NetworkBorderGroup : ap-northeast-1

IpPrefix           : 52.219.68.0/22
IpAddressFormat    : Ipv4
Region             : ap-northeast-1
Service            : S3
NetworkBorderGroup : ap-northeast-1

IpPrefix           : 52.219.16.0/22
IpAddressFormat    : Ipv4
Region             : ap-northeast-1
Service            : S3
NetworkBorderGroup : ap-northeast-1

IpPrefix           : 3.5.152.0/21
IpAddressFormat    : Ipv4
Region             : ap-northeast-1
Service            : S3
NetworkBorderGroup : ap-northeast-1

IpPrefix           : 52.219.0.0/20
IpAddressFormat    : Ipv4
Region             : ap-northeast-1
Service            : S3
NetworkBorderGroup : ap-northeast-1

IpPrefix           : 52.219.136.0/22
IpAddressFormat    : Ipv4
Region             : ap-northeast-1
Service            : S3
NetworkBorderGroup : ap-northeast-1

-ServiceKeyパラメータと-Regionパラメータで絞り込めます。Ipv6 を出したくない場合はWhere-Objectでフィルタする必要があります。あとは IP レンジだけ出力します。


(Get-AWSPublicIpAddressRange -ServiceKey "S3" -Region "ap-northeast-1" | 
Where-Object -FilterScript { $_.IpAddressFormat -eq "Ipv4" }).IpPrefix



52.92.60.0/22
52.219.68.0/22
52.219.16.0/22
3.5.152.0/21
52.219.0.0/20
52.219.136.0/22

PowerShell の機能で自在にパースできて便利ですね。

セキュリティグループを自動生成してみる

IP レンジが取得できたとして、ではそれをどのように活用するかというところですが、今回はサンプルケースとしてセキュリティグループを自動生成します。

スクリプトの全体像

コードの全体像を見てみましょう。

スクリプト

Function New-EC2SecurityGroupFromAWSPublicIpAddressRange
{

    [OutputType([System.Object])]
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory=$True)]
        [ValidateSet(
            "af-south-1",
            "ap-east-1",
            "ap-northeast-1",
            "ap-northeast-2",
            "ap-south-1",
            "ap-southeast-1",
            "ap-southeast-2",
            "ca-central-1",
            "eu-central-1",
            "eu-north-1",
            "eu-south-1",
            "eu-west-1",
            "eu-west-2",
            "eu-west-3",
            "me-south-1",
            "sa-east-1",
            "us-east-1",
            "us-east-2",
            "us-west-1",
            "us-west-2",
            "us-iso-east-1",
            "us-isob-east-1"
        )]
        [string]$Region,

        [Parameter(Mandatory=$True)]
        [ValidateNotNullOrEmpty()]
        [string[]]$ServiceKey,

        [Parameter(Mandatory=$False)]
        [ValidateSet(
            "Ipv4",
            "Ipv6"
        )]
        [string]$IpAddressFormat,

        [Parameter(Mandatory=$True)]
        [ValidateNotNullOrEmpty()]
        [string]$GroupName,
        
        [Parameter(Mandatory=$True)]
        [ValidateNotNullOrEmpty()]
        [string]$Description,
        
        [Parameter(Mandatory=$True)]
        [ValidateNotNullOrEmpty()]
        [string]$VpcId,
        
        [Parameter(Mandatory=$True)]
        [ValidateNotNullOrEmpty()]
        [string]$IpProtocol,

        [Parameter(Mandatory=$True)]
        [ValidateNotNullOrEmpty()]
        [int]$FromPort,
        
        [Parameter(Mandatory=$True)]
        [ValidateNotNullOrEmpty()]
        [int]$ToPort
    )

    Begin
    {
        Set-DefaultAWSRegion -Region $Region -Scope Script
        Import-Module -Name AWS.Tools.EC2

        $Ipv4Ranges = @()
        $Ipv6Ranges = @()
    }

    Process
    {
        $Tags = @{ Key="Name"; Value=$GroupName }
        $NameTag = New-Object -TypeName Amazon.EC2.Model.TagSpecification
        $NameTag.ResourceType = "security-group"
        $NameTag.Tags.Add($Tags)

        $SgParams = @{
            GroupName = $GroupName
            Description = $Description
            VpcId = $VpcId
            TagSpecification = $NameTag
        }
        $GroupId = New-EC2SecurityGroup @SgParams

        $AWSPublicIpAddresses = Get-AWSPublicIpAddressRange -ServiceKey $ServiceKey -Region $Region

        ForEach($AWSPublicIpAddress In $AWSPublicIpAddresses)
        {
            If ($AWSPublicIpAddress.IpPrefix -like "*.*")
            {
                $Ipv4Range = New-Object -TypeName Amazon.EC2.Model.IpRange
                $Ipv4Range.CidrIp = $AWSPublicIpAddress.IpPrefix
                $ipv4Range.Description = $AWSPublicIpAddress.Service
                $Ipv4Ranges += $Ipv4Range
            }

            If ($AWSPublicIpAddress.IpPrefix -like "*:*")
            {
                $Ipv6Range = New-Object -TypeName Amazon.EC2.Model.Ipv6Range
                $Ipv6Range.CidrIpv6 = $AWSPublicIpAddress.IpPrefix
                $ipv6Range.Description = $AWSPublicIpAddress.Service
                $Ipv6Ranges += $Ipv6Range
            }
        }

        $IpPermission = New-Object -TypeName Amazon.EC2.Model.IpPermission
        $IpPermission.IpProtocol = $IpProtocol
        $IpPermission.FromPort = $FromPort
        $IpPermission.ToPort = $ToPort
        $IpPermission.Ipv4Ranges = $Ipv4Ranges
        $IpPermission.Ipv6Ranges = $Ipv6Ranges

        If ( $IpAddressFormat -eq "Ipv4" )
        {
            $IpPermission.Ipv6Ranges.Clear()
        }
        
        If ( $IpAddressFormat -eq "Ipv6" )
        {
            $IpPermission.Ipv4Ranges.Clear()
        }

        Grant-EC2SecurityGroupIngress -GroupId $GroupId -IpPermission $IpPermission
    }

    End
    {
        return Get-EC2SecurityGroup -GroupId $GroupId
    }
}

# Example

$IpPermissionParams = @{
    Region = "ap-northeast-1"
    ServiceKey = "S3", "CLOUD9"
    IpAddressFormat = "Ipv4"
    GroupName = "test-sec-01"
    Description = "test-sec-01"
    VpcId = "vpc-00000000000000000"
    IpProtocol = "tcp"
    FromPort = 80
    ToPort = 80
}

New-EC2SecurityGroupFromAWSPublicIpAddressRange @IpPermissionParams

#Get-Variable | Remove-Variable -ErrorAction Ignore

関数にしてみました。PowerShell らしく、名前をめちゃくちゃ長くしてみました。ポイントは① Ipv4 のみ、② Ipv6 のみ、③両方の3通りが選べることと、対象の AWS サービスをカンマ区切りで複数渡せることです。PowerShell 関数の書き方については私もそこまで詳しくありませんので、詳細を知りたい方はこちらこちらを参照するのがいいでしょう。構成としては以下の形です。

Begin ブロック

Begin ブロックでは初期処理をしています。内容はコメントに書いた通りとなります。


# デフォルトリージョンを設定
Set-DefaultAWSRegion -Region $Region -Scope Script

# 必要なモジュールを読み込む
Import-Module -Name AWS.Tools.EC2

# IPv4 アドレス1個に対して1個作成される Amazon.EC2.Model.IpRange オブジェクトを格納する配列を初期化
$Ipv4Ranges = @()

# IPv6 アドレス1個に対して1個作成される Amazon.EC2.Model.Ipv6Range オブジェクトを格納する配列を初期化
$Ipv6Ranges = @()

ポイントは Ipv4 と Ipv6 を別々に考えて、配列もふたつ用意しておくことです。

Process ブロック

MS の公式ドキュメントにはこのように書かれています。

BEGIN ブロックと END ブロックは省略可能です。 BEGIN は PROCESS ブロックの前で指定され、パイプラインから項目が受信される前の初期処理を実行するときに使用されます。 これを理解することが重要です。 パイプ処理された値には、BEGIN ブロックではアクセスできません。 END ブロックは PROCESS ブロックの後ろで指定され、パイプ入力されたすべての項目が処理された後、その項目をクリーンアップするために使用されます。

属性としてValueFromPipelineValueFromPipelineByPropertyNameを定義しているパラメータはパイプライン経由で値を Process ブロックに渡せるようです。今回はパラメータがたくさんあるのでそこまでは想定していません。余談ですが、こういう PowerShell ならではの部分も今後試していければと思います。では Process ブロックの中身です。


# セキュリティグループに付与する Name タグを定義
$Tags = @{ Key="Name"; Value=$GroupName }
$NameTag = New-Object -TypeName Amazon.EC2.Model.TagSpecification
$NameTag.ResourceType = "security-group"
$NameTag.Tags.Add($Tags)

# 空のセキュリティグループを作成
$Params = @{
    GroupName = $GroupName
    Description = $Description
    VpcId = $VpcId
    TagSpecification = $NameTag
}
$GroupId = New-EC2SecurityGroup @Params

# AWS パブリック IP レンジをサービス・リージョンで絞り込む
$AWSPublicIpAddresses = Get-AWSPublicIpAddressRange -ServiceKey $ServiceKey -Region $Region

# 絞り込んだ AWS パブリック IP レンジをひとつずつ処理する
ForEach($AWSPublicIpAddress In $AWSPublicIpAddresses)
{
    # IP レンジが Ipv4 だったら Ipv4 用のオブジェクト (Amazon.EC2.Model.IpRange) を作って Ipv4 用の配列に加算
    If ($AWSPublicIpAddress.IpPrefix -like "*.*")
    {
        $Ipv4Range = New-Object -TypeName Amazon.EC2.Model.IpRange
        $Ipv4Range.CidrIp = $AWSPublicIpAddress.IpPrefix
        $ipv4Range.Description = $AWSPublicIpAddress.Service
        $Ipv4Ranges += $Ipv4Range
    }

    # IP レンジが Ipv6 だったら Ipv6 用のオブジェクト (Amazon.EC2.Model.Ipv6Range) を作って Ipv6 用の配列に加算
    If ($AWSPublicIpAddress.IpPrefix -like "*:*")
    {
        $Ipv6Range = New-Object -TypeName Amazon.EC2.Model.Ipv6Range
        $Ipv6Range.CidrIpv6 = $AWSPublicIpAddress.IpPrefix
        $ipv6Range.Description = $AWSPublicIpAddress.Service
        $Ipv6Ranges += $Ipv6Range
    }

    # Ipv4 も Ipv6 も入った Amazon.EC2.Model.IpPermission オブジェクトを作成
    $IpPermission = New-Object -TypeName Amazon.EC2.Model.IpPermission
    $IpPermission.IpProtocol = $IpProtocol
    $IpPermission.FromPort = $FromPort
    $IpPermission.ToPort = $ToPort
    $IpPermission.Ipv4Ranges = $Ipv4Ranges
    $IpPermission.Ipv6Ranges = $Ipv6Ranges

    # IpAddressFormat パラメータが Ipv4 だったら Ipv6Ranges プロパティをクリア
    If ( $IpAddressFormat -eq "Ipv4" )
    {
        $IpPermission.Ipv6Ranges.Clear()
    }

    # IpAddressFormat パラメータが Ipv6 だったら Ipv4Ranges プロパティをクリア
    If ( $IpAddressFormat -eq "Ipv6" )
    {
        $IpPermission.Ipv4Ranges.Clear()
    }

    # 作成した Amazon.EC2.Model.IpPermission オブジェクトをセキュリティグループに付与
    Grant-EC2SecurityGroupIngress -GroupId $GroupId -IpPermission $IpPermission
}

基本的にはコメントで書いた通りです。まず空のセキュリティグループを作ります。条件で抽出した IP レンジをひとつずつ処理しますが、Ipv4 と Ipv6 はオブジェクト定義が別なので別々に処理し、それぞれ用意しておいた配列に加算します。セキュリティグループに付与するAmazon.EC2.Model.IpPermissionオブジェクトには、Ipv4/Ipv6 どちらも組み込みます。その後-IpAddressFormatパラメータに従って、Ipv4 をクリアするか、Ipv6 をクリアするか、それともクリアしないかを判定します。そして最終的にできあがったAmazon.EC2.Model.IpPermissionオブジェクトを空のセキュリティグループに付与します。

End ブロック

End ブロックは最終的にできたオブジェクトをreturnしているだけです。


return Get-EC2SecurityGroup -GroupId $GroupId

実行してみる

実行してみましょう。パラメータがいっぱいあるのでバンドル(スプラッティング)します。


$Params = @{
    Region = "ap-northeast-1"
    ServiceKey = "S3", "CLOUD9" # 複数ターゲットいけます
    IpAddressFormat = "Ipv4" # Ipv4, Ipv6 が選べます。このパラメータ自体を付けない場合はどちらも出ます
    GroupName = "test-01"
    Description = "test-01"
    VpcId = "vpc-00000000000000000" # ご自分の環境に書き換え
    IpProtocol = "tcp"
    FromPort = 80
    ToPort = 80
 }

実行。セキュリティグループがうまく生成されているか確認してください。


New-EC2SecurityGroupFromAWSPublicIpAddressRange @Params

PowerShell での変数の後始末って、みんなどうやってるんだろう。ずっと疑問です。私は以下のようにしています。もっとうまい方法があれば教えてほしいです。


Get-Variable | Remove-Variable -ErrorAction Ignore

おわりに

AWS が公開している各サービスの IP レンジは随時で変更される可能性があるため、一度取得した IP を恒久的に使うのは NG だと思います。ならべくなら VPC エンドポイント等でインターネットに出ずに通信したほうがよいでしょう。とはいえ、一時的な検証等で、かつ 0.0.0.0/0 を開けたくないといった場合にはそれなりに使い道はありそうです。Ipv4/Ipv6/ どちらも というのをスイッチできるようにと考えた瞬間に内容がややこしくなった気がします。が、PowerShell 関数を本格的に作ったことがなかったので、そこは勉強になりました。今後、こういうネタを少しずつ GitHub に上げていければと考えています。

と、ここまでやってみて気づいたのですが、今回やったことは最近リリースされたプレフィックスリストと若干被っていますね。デフォルトで S3 と DynamoDB の IP レンジがプレフィックスリストとして使えるようになっています。ということは、次に試してみるべきは ip-ranges.json からプレフィックスリストを自動生成する方法ということになりますかね。こっちのほうがいろいろと有用かもしれません。近いうちにやってみます。