この記事は、ニフティグループ Advent Calendar 2021の12日目の記事です。
はじめに
今年も早いものでもう3週間を切ってしまい、寒い日も多くなってきましたが、皆さんいかがお過ごしでしょうか。
私は、映画「五等分の花嫁」やアニメ「かぐや様は告らせたい」3期が待ち遠しいなと思っています。
ちなみに「五等分の花嫁」の推しキャラは変わらず中野三玖です。そして、ニフティグループ Advent Calendar 2020に投稿した記事を読み返すと同じようなことを書いていますね。
背景
現在、弊社ではいくつかのサービスでシステム基盤の刷新を進めています。
その中でも私が関わるサービスでは、Amazon CloudFrontを使用しているために、開発環境に対してはAWS WAFを使用して特定のIPアドレスからのみアクセスできるように制限しています。
また、GitHub Actionsを積極的に活用しており、ある日、E2E テストなどをGitHub Actions上に仕掛けようとしました。前述の通り、リソースはAWS WAFで制限されているため、GitHub ActionsからAWS WAFのIP Setを動的に操作する必要が出てきました。
似たようなことをやっていないかと調べてみたところ、GitHub Actionsからセキュリティグループを操作する記事はありましたが、AWS WAFのIP Setを操作する記事は見つかりませんでした。
そういうわけで、AWS SDK for Goの勉強も兼ねて上の記事を参考にしつつ作ることにしました。
まずはGoのプログラムについてです。実装方針はAWS SDK for Goのドキュメントを見ながら、IPアドレス追加処理では、
- GetIPSetInput関数でIPSetを取得する
- Goに組み込まれているappend関数で追加したいIPアドレスを追加する
- UpdateIPSetInput関数でIPSetを更新する
とし、IPアドレス削除処理では、
- GetIPSetInput関数でIPSetを取得する
- 自作のremove関数で削除したいIPアドレスを削除する(Goでは自作しないといけない模様......)
- 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について、実装方針を
- haythem/public-ipを使用してパブリックIPアドレスを取得する
- 上記で作成したGoのプログラムにパブリックIPアドレスを渡して開放する
- 所望の処理を走らせる
- 上記で作成した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を操作しようとしている方のお役に立てたら嬉しいです。
また、サービスを支えるシステム基盤の刷新に向けた取り組みについて詳しく知りたい方は、ニフティの説明会に一度参加してみてくださいね。苦労話など様々なことが聞けるかと思います!
明日以降もAdvent Calendarは続きます!いやー、目が離せませんね!ぜひお楽しみに!