GitHub ActionsのパブリックIPアドレスをAWS WAFのIP Setに登録し、制限のかかったサービス（Amazon CloudFront）につなぐ

この記事は、[ニフティグループ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/nifty)の12日目の記事です。

# はじめに
今年も早いものでもう3週間を切ってしまい、寒い日も多くなってきましたが、皆さんいかがお過ごしでしょうか。

私は、[映画「五等分の花嫁」](https://www.tbs.co.jp/anime/5hanayome/)や[アニメ「かぐや様は告らせたい」3期](https://kaguya.love/)が待ち遠しいなと思っています。

ちなみに「五等分の花嫁」の推しキャラは変わらず中野三玖です。そして、[ニフティグループ Advent Calendar 2020](https://qiita.com/advent-calendar/2020/nifty)に投稿した記事を読み返すと同じようなことを書いていますね。

https://qiita.com/shotaws/items/20b5fd64bebcaa0e9627

# 背景
現在、弊社ではいくつかのサービスでシステム基盤の刷新を進めています。

その中でも私が関わるサービスでは、Amazon CloudFrontを使用しているために、開発環境に対してはAWS WAFを使用して特定のIPアドレスからのみアクセスできるように制限しています。

また、GitHub Actionsを積極的に活用しており、ある日、E2E テストなどをGitHub Actions上に仕掛けようとしました。前述の通り、リソースはAWS WAFで制限されているため、GitHub ActionsからAWS WAFのIP Setを動的に操作する必要が出てきました。

似たようなことをやっていないかと調べてみたところ、GitHub Actionsからセキュリティグループを操作する記事はありましたが、AWS WAFのIP Setを操作する記事は見つかりませんでした。

https://qiita.com/fetaro/items/45f94a50316463cf093d

そういうわけで、[AWS SDK for Go](https://aws.amazon.com/jp/sdk-for-go/)の勉強も兼ねて上の記事を参考にしつつ作ることにしました。

まずはGoのプログラムについてです。実装方針はAWS SDK for Goのドキュメントを見ながら、IPアドレス追加処理では、

1. GetIPSetInput関数でIPSetを取得する
1. Goに組み込まれているappend関数で追加したいIPアドレスを追加する
1. UpdateIPSetInput関数でIPSetを更新する

とし、IPアドレス削除処理では、

1. GetIPSetInput関数でIPSetを取得する
1. 自作のremove関数で削除したいIPアドレスを削除する（Goでは自作しないといけない模様......）
1. UpdateIPSetInput関数でIPSetを更新する

としました。Goのプログラムのソースコードは以下の通りです。

```
package main

import (
	"fmt"
	"os"
	"syscall"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/wafv2"
)

// 追加したIPを削除するために線形探索で削除する
// @param : strings : 通信が許可されているIPアドレス群。
// @param : search : 消したいIPアドレス。
func remove(strings []*string, search *string) []*string {
	result := []*string{}
	for _, v := range strings {
		if *v != *search {
			result = append(result, v)
		}
	}
	return result
}

// GitHub Actionsからの通信を許可するようにWAFのIPSetを変更する。
// @param : ipSetID : 通信を許可するIPアドレスを管理するIPSetのID。
// @param : ipSetName : 通信を許可するIPアドレスを管理するIPSetの名前。
// @param : ipAddr : 通信を許可したいIPアドレス。
// @param : op : 操作。op=openで開ける。 op=closeで閉じる。
func wafIPSetControl(ipSetID string, ipSetName string, ipAddr string, op string) {
	sess := session.Must(session.NewSession())
	wafService := wafv2.New(
		sess,
		aws.NewConfig().WithRegion("us-east-1"),
	)

	// IPSetの操作
	ipSetInputs := &wafv2.GetIPSetInput{
		Id:    aws.String(ipSetID),
		Name:  aws.String(ipSetName),
		Scope: aws.String("CLOUDFRONT"),
	}

	ipset, err := wafService.GetIPSet(ipSetInputs)
	if err != nil {
		panic(err)
	}
	fmt.Println(ipset)

	if op == "open" {
		// IPを追加して開く
		newAddresses := append(ipset.IPSet.Addresses, aws.String(ipAddr+"/32"))

		up := &wafv2.UpdateIPSetInput{
			Addresses: newAddresses,
			Id:        ipset.IPSet.Id,
			LockToken: ipset.LockToken,
			Name:      ipset.IPSet.Name,
			Scope:     aws.String("CLOUDFRONT"),
		}

		_, err := wafService.UpdateIPSet(up)
		if err != nil {
			panic(err)
		}
	} else if op == "close" {
		// 追加したIPを削除して閉じる
		newAddresses := remove(ipset.IPSet.Addresses, aws.String(ipAddr+"/32"))

		up := &wafv2.UpdateIPSetInput{
			Addresses: newAddresses,
			Id:        ipset.IPSet.Id,
			LockToken: ipset.LockToken,
			Name:      ipset.IPSet.Name,
			Scope:     aws.String("CLOUDFRONT"),
		}

		_, err := wafService.UpdateIPSet(up)
		if err != nil {
			panic(err)
		}
	} else {
		panic("invalid op")
	}

	ipset, err = wafService.GetIPSet(ipSetInputs)
	if err != nil {
		panic(err)
	}
	fmt.Println(ipset)
}

func printUsage() {
	fmt.Printf("Argument ERROR. \n\nUsage: go run %s IPSetID IPSetName [open|close] IPAddress \n", os.Args[0])
}

func main() {
	if len(os.Args) != 5 {
		printUsage()
		syscall.Exit(1)
	}
	ipSetID := os.Args[1] // 追加したいIPSetのID
	ipSetName := os.Args[2] // 追加したいIPSetの名前
	op := os.Args[3] // 操作 [open|close]
	ipAddr := os.Args[4]
	wafIPSetControl(ipSetID, ipSetName, ipAddr, op)
}
```
なお、LockTokenはOptimistic Lockingを行うために使用されるトークンだそうで、実装している中で初めて知りました。

次にGitHub Actionsについて、実装方針を

1. [haythem/public-ip](https://github.com/haythem/public-ip)を使用してパブリックIPアドレスを取得する
1. 上記で作成したGoのプログラムにパブリックIPアドレスを渡して開放する
1. 所望の処理を走らせる
1. 上記で作成したGoのプログラムにパブリックIPアドレスを渡して閉鎖する

としました。今回のお話に関係した部分のみ抜粋し、一部改変したワークフローファイルは以下の通りです。

```
# 適当に名前を付ける
name: Run sample

# 適当に定期実行
on:
  schedule:
    - cron: '0 8 * * 1-5'

env:
  go-version: '1.17'

jobs:
  my_action:
    name: test
    runs-on: ubuntu-latest
    timeout-minutes: 10

    # These permissions are needed to interact with GitHub's OIDC Token endpoint.
    permissions:
      id-token: write
      contents: read
      actions: write
      checks: write
      deployments: write
      issues: write
      packages: write
      statuses: write

    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@master
        with:
          aws-region: ap-northeast-1
          role-to-assume: ${{ secrets.AWS_ACCESS_IAM_ROLE }}

      - name: Install Public IP Lib
        id: ip
        uses: haythem/public-ip@v1.2

      - name: Setup Go
        uses: actions/setup-go@v2
        with:
          go-version: ${{ env.go-version }}

      - name: checkout
        uses: actions/checkout@v2

      - name: Open this IP in WAF
        run: go run ./wafCtl.go ${{ secrets.AWS_WAF_IPSET_ID }} ${{ secrets.AWS_WAF_IPSET_NAME }} open ${{ steps.ip.outputs.ipv4 }}

      　　　（AWS WAFで制限のかかったサービスにつなぐ処理がここに入る）

      - name: Close this IP in WAF
        if: always()
        run: go run ./wafCtl.go ${{ secrets.AWS_WAF_IPSET_ID }} ${{ secrets.AWS_WAF_IPSET_NAME }} close ${{ steps.ip.outputs.ipv4 }}
```

ちなみに、GoのプログラムからAWS WAFにアクセスするために、話題のOpenID Connectを使用しています。

# おわりに
今回は、GitHub ActionsのパブリックIPアドレスをAWS WAFのIP Setに登録し、制限のかかったサービス（Amazon CloudFront）につなぐために行ったことを記事にしました。

GitHub ActionsからAWS WAFのIP Setを操作しようとしている方のお役に立てたら嬉しいです。

また、サービスを支えるシステム基盤の刷新に向けた取り組みについて詳しく知りたい方は、ニフティの説明会に一度参加してみてくださいね。苦労話など様々なことが聞けるかと思います！

https://recruit.nifty.co.jp/

明日以降もAdvent Calendarは続きます！いやー、目が離せませんね！ぜひお楽しみに！