TerraformでIPアドレスリストをCIDRリストに変換する
Terraformの小ネタです.プライベートサブネットからInternet facing ALBへのアクセスを許可するため,NATゲートウェイのグローバルIPアドレスをセキュリティグループに追加する必要がありました.Terraformの aws_security_group_rule
にはIPアドレスではなくCIDRのリストを指定する必要があります.IPアドレスリストをCIDRリストに変換するにはどう書けばよいのか試行錯誤したのでメモを残しておきます.
Terraform v0.12を前提とします.
TL;DR
resource "aws_security_group_rule" "allow_from_natgw" { cidr_blocks = [for ip in ips : "${ip}/32"] }
解説
terraform-aws-vpc モジュールを利用して,パブリック/プライベートサブネット構成を作成します.
module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "2.9.0" enable_nat_gateway = true # 中略 }
セキュリティグループを作成するには aws_security_group
リソースを利用します.
resource "aws_security_group" "alb_foo" { name = "alb-foo" vpc_id = module.vpc.vpc_id }
セキュリティグループにルールを追加するには aws_security_group_rule
リソースを利用します.
resource "aws_security_group_rule" "alb_foo_from_natgw" { description = "Allow from NAT Gateway" security_group_id = aws_security_group.alb_foo.id type = "ingress" from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = [for ip in module.vpc.nat_public_ips : "${ip}/32"] }
Terraform v0.12から for
Expressions が利用できます.例えば,NATゲートウェイのグローバルIPアドレスが ["1.2.3.4", "5.6.7.8"]
の場合は以下のように展開されます.
cidr_blocks = ["1.2.3.4/32", "5.6.7.8/32"]
もっと簡潔な書き方があったらぜひ教えてください!
golang.org/x/tools/go/packages による構文解析と型解析
golang.org/x/tools/go/packages
を利用すると,抽象構文木や型情報を利用したコードが簡単に書けるので調べてみました.日本語の情報があまりないようです.
抽象構文木を表示する
Goのソースコードを読み込むには,packages.Load
関数を利用します.packages.Load
関数の引数にはどんな情報を解析してほしいかという設定を渡します.抽象構文木(AST)を取得するには下記のコードのようにフラグを渡します.ドキュメントを読むと packages.NeedSyntax
だけで良さそうなのですが,実際に実行してみると複数のフラグが必要でした.
Load
関数の引数にはパッケージ名の配列を渡します../helloworld
, os/exec
, ./...
のような表記が使えます.
package main import ( "go/ast" "log" "os" "golang.org/x/tools/go/packages" ) func main() { config := &packages.Config{ Mode: packages.NeedCompiledGoFiles | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo, } pkgs, err := packages.Load(config, os.Args[1:]...) if err != nil { log.Fatalf("could not load packages: %s", err) } if packages.PrintErrors(pkgs) > 0 { log.Fatalf("error occurred") } for _, pkg := range pkgs { if err := ast.Print(pkg.Fset, pkg); err != nil { log.Printf("could not print the AST: %s", err) } } }
packages.Load
関数はGoのソースファイルの配列を返します.例えば, hello.go
と world.go
を渡した場合はサイズ2の配列が返ってきます.配列の要素(packages.Package
)にはファイル名,抽象構文木,型情報などが含まれています.
先ほどのコードでは ast.Print
関数を利用して抽象構文木をダンプしています.デバッグに便利です.
例えば,以下のソースコード(hello.go
):
package testdata import ( "github.com/pkg/errors" ) // Hello says hello world! func Hello() error { return errors.New("hello world") }
に対応する抽象構文木は以下のようになります:
0 *packages.Package { 1 . ID: "github.com/int128/hello-go-parser/testdata" 2 . Name: "" 3 . PkgPath: "" 4 . CompiledGoFiles: []string (len = 2) { 5 . . 0: "/src/hello-go-parser/testdata/hello.go" 6 . . 1: "/src/hello-go-parser/testdata/world.go" 7 . } 8 . ExportFile: "" 9 . Types: *types.Package {} 10 . Fset: *token.FileSet {} 11 . IllTyped: false 12 . Syntax: []*ast.File (len = 2) { 13 . . 0: *ast.File { 14 . . . Package: /src/hello-go-parser/testdata/hello.go:1:1 15 . . . Name: *ast.Ident { 16 . . . . NamePos: /src/hello-go-parser/testdata/hello.go:1:9 17 . . . . Name: "testdata" 18 . . . } 19 . . . Decls: []ast.Decl (len = 2) { 20 . . . . 0: *ast.GenDecl { 21 . . . . . TokPos: /src/hello-go-parser/testdata/hello.go:3:1 22 . . . . . Tok: import 23 . . . . . Lparen: /src/hello-go-parser/testdata/hello.go:3:8 24 . . . . . Specs: []ast.Spec (len = 1) { 25 . . . . . . 0: *ast.ImportSpec { 26 . . . . . . . Path: *ast.BasicLit { 27 . . . . . . . . ValuePos: /src/hello-go-parser/testdata/hello.go:4:2 28 . . . . . . . . Kind: STRING 29 . . . . . . . . Value: "\"github.com/pkg/errors\"" 30 . . . . . . . } 31 . . . . . . . EndPos: - 32 . . . . . . } 33 . . . . . } 34 . . . . . Rparen: /src/hello-go-parser/testdata/hello.go:5:1 35 . . . . } 36 . . . . 1: *ast.FuncDecl { 37 . . . . . Doc: *ast.CommentGroup { 38 . . . . . . List: []*ast.Comment (len = 1) { 39 . . . . . . . 0: *ast.Comment { 40 . . . . . . . . Slash: /src/hello-go-parser/testdata/hello.go:7:1 41 . . . . . . . . Text: "// Hello says hello world!" 42 . . . . . . . } 43 . . . . . . } 44 . . . . . } 45 . . . . . Name: *ast.Ident { 46 . . . . . . NamePos: /src/hello-go-parser/testdata/hello.go:8:6 47 . . . . . . Name: "Hello" 48 . . . . . . Obj: *ast.Object { 49 . . . . . . . Kind: func 50 . . . . . . . Name: "Hello" 51 . . . . . . . Decl: *(obj @ 36) 52 . . . . . . } 53 . . . . . } 54 . . . . . Type: *ast.FuncType { 55 . . . . . . Func: /src/hello-go-parser/testdata/hello.go:8:1 56 . . . . . . Params: *ast.FieldList { 57 . . . . . . . Opening: /src/hello-go-parser/testdata/hello.go:8:11 58 . . . . . . . Closing: /src/hello-go-parser/testdata/hello.go:8:12 59 . . . . . . } 60 . . . . . . Results: *ast.FieldList { 61 . . . . . . . Opening: - 62 . . . . . . . List: []*ast.Field (len = 1) { 63 . . . . . . . . 0: *ast.Field { 64 . . . . . . . . . Type: *ast.Ident { 65 . . . . . . . . . . NamePos: /src/hello-go-parser/testdata/hello.go:8:14 66 . . . . . . . . . . Name: "error" 67 . . . . . . . . . } 68 . . . . . . . . } 69 . . . . . . . } 70 . . . . . . . Closing: - 71 . . . . . . } 72 . . . . . } 73 . . . . . Body: *ast.BlockStmt { 74 . . . . . . Lbrace: /src/hello-go-parser/testdata/hello.go:8:20 75 . . . . . . List: []ast.Stmt (len = 1) { 76 . . . . . . . 0: *ast.ReturnStmt { 77 . . . . . . . . Return: /src/hello-go-parser/testdata/hello.go:9:2 78 . . . . . . . . Results: []ast.Expr (len = 1) { 79 . . . . . . . . . 0: *ast.CallExpr { 80 . . . . . . . . . . Fun: *ast.SelectorExpr { 81 . . . . . . . . . . . X: *ast.Ident { 82 . . . . . . . . . . . . NamePos: /src/hello-go-parser/testdata/hello.go:9:9 83 . . . . . . . . . . . . Name: "errors" 84 . . . . . . . . . . . } 85 . . . . . . . . . . . Sel: *ast.Ident { 86 . . . . . . . . . . . . NamePos: /src/hello-go-parser/testdata/hello.go:9:16 87 . . . . . . . . . . . . Name: "New" 88 . . . . . . . . . . . } 89 . . . . . . . . . . } 90 . . . . . . . . . . Lparen: /src/hello-go-parser/testdata/hello.go:9:19 91 . . . . . . . . . . Args: []ast.Expr (len = 1) { 92 . . . . . . . . . . . 0: *ast.BasicLit { 93 . . . . . . . . . . . . ValuePos: /src/hello-go-parser/testdata/hello.go:9:20 94 . . . . . . . . . . . . Kind: STRING 95 . . . . . . . . . . . . Value: "\"hello world\"" 96 . . . . . . . . . . . } 97 . . . . . . . . . . } 98 . . . . . . . . . . Ellipsis: - 99 . . . . . . . . . . Rparen: /src/hello-go-parser/testdata/hello.go:9:33 100 . . . . . . . . . } 101 . . . . . . . . } 102 . . . . . . . } 103 . . . . . . } 104 . . . . . . Rbrace: /src/hello-go-parser/testdata/hello.go:10:1 105 . . . . . } 106 . . . . } 107 . . . } 108 . . . Scope: *ast.Scope { 109 . . . . Objects: map[string]*ast.Object (len = 1) { 110 . . . . . "Hello": *(obj @ 48) 111 . . . . } 112 . . . } 113 . . . Imports: []*ast.ImportSpec (len = 1) { 114 . . . . 0: *(obj @ 25) 115 . . . } 116 . . . Unresolved: []*ast.Ident (len = 2) { 117 . . . . 0: *(obj @ 64) 118 . . . . 1: *(obj @ 81) 119 . . . } 120 . . . Comments: []*ast.CommentGroup (len = 1) { 121 . . . . 0: *(obj @ 37) 122 . . . } 123 . . } 124 . . 1: *ast.File { (中略) 312 . . } 313 . } 314 }
抽象構文木を探索する
抽象構文木を探索するには ast.Inspect
を利用します.コールバック関数に抽象構文木のノードが渡されるので,ノードの型をチェックして必要な処理を行います.
例えば,以下を実行するとソースコードに含まれる import
文を抽出できます.
for _, pkg := range pkgs { for _, syntax := range pkg.Syntax { ast.Inspect(syntax, func(node ast.Node) bool { switch node := node.(type) { case *ast.ImportSpec: log.Printf("import %s as %s", node.Path.Value, node.Name) } return true }) } }
2019/09/03 18:06:32 import "github.com/pkg/errors" as <nil>
また,以下を実行すると関数やメソッドの呼び出しを抽出できます.
for _, pkg := range pkgs { for _, syntax := range pkg.Syntax { ast.Inspect(syntax, func(node ast.Node) bool { switch node := node.(type) { case *ast.CallExpr: switch fun := node.Fun.(type) { // foo.bar() 形式の呼び出し case *ast.SelectorExpr: log.Printf("call %s.%s with %d arg(s)", fun.X, fun.Sel, len(node.Args)) // foo() 形式の呼び出し default: log.Printf("call %s with %d arg(s)", fun, len(node.Args)) } } return true }) } }
2019/09/03 19:09:59 call errors.New with 1 arg(s)
型情報を利用する
ast.Inspect
関数で得られるノードには型情報が含まれないため,関数やメソッドの呼び出し対象を判断するのが難しい問題があります.関数やメソッドの呼び出し対象は以下の種類があります.
import "fmt" // 同じパッケージの関数 hello() // 外部のパッケージの関数 fmt.Printf("hello world") // メソッド var x Hello x.Hello()
そこで, packages.Load
関数が返す TypesInfo
を利用すると,ノードの型を取得できます.例えば,以下を実行すると外部パッケージの関数に対する呼び出しを取得できます.
for _, pkg := range pkgs { for _, syntax := range pkg.Syntax { ast.Inspect(syntax, func(node ast.Node) bool { switch node := node.(type) { case *ast.CallExpr: switch fun := node.Fun.(type) { // foo.bar() 形式の呼び出し case *ast.SelectorExpr: switch x := fun.X.(type) { case *ast.Ident: switch o := pkg.TypesInfo.ObjectOf(x).(type) { // fooがパッケージの場合 case *types.PkgName: // パッケージ名を取得 path := o.Imported().Path() log.Printf("call %s.%s with %d arg(s)", path, fun.Sel, len(node.Args)) } } } } return true }) } }
2019/09/03 19:53:03 call github.com/pkg/errors.New with 1 arg(s)
ここでは関数呼び出しのパッケージ名を取得しましたが,変数の型なども取得できます.
まとめ
golang.org/x/tools/go/packages
を利用すると,抽象構文木や型情報を利用するコードが簡単に書けます.
EKSのGitLab RunnerでTerraformをCI/CDする
AWS EKSでGitLab Runnerを実行して,GitLab RunnerでTerraformを実行する方法を紹介します.
以下の流れで作業を行います.
- EKS workerにAssumeRoleのIAMポリシーをアタッチする.
- stable/kube2iamをデプロイする.
- GitLab RunnerのIAMロールを作成する.
- GitLab Runner Helm Chartをデプロイする.
- リポジトリに
.gitlab-ci.yml
を追加して,ビルドを実行する.
ここでは以下のツールを利用している前提とします.
- GitLab
- Terraform
- helmfile
EKS workerのIAMポリシーの追加
kube2iamがAssumeRoleを実行できるように,EKS workerインスタンスにIAMポリシーをアタッチします. Terraformのeksモジュールを利用している場合は以下を追加します.
resource "aws_iam_policy" "worker_kube2iam" { name = "hello-eks-worker-kube2iam" description = "kube2iam on EKS workers" policy = <<EOF { "Version": "2012-10-17", "Statement": [ { "Action": [ "sts:AssumeRole" ], "Effect": "Allow", "Resource": "*" } ] } EOF } module "eks" { workers_additional_policies = [ aws_iam_policy.worker_kube2iam.arn ] }
kube2iamのデプロイ
stable/kube2iamをKubernetesにデプロイします. helmfileを利用している場合は以下を追加します.
releases: # https://github.com/helm/charts/tree/master/stable/kube2iam - name: kube2iam namespace: kube-system chart: stable/kube2iam values: - host: iptables: true interface: eni+ extraArgs: # https://github.com/jtblin/kube2iam#base-arn-auto-discovery auto-discover-base-arn: "" rbac: create: true
GitLab RunnerのIAMロールの作成
GitLab RunnerのPodにアタッチするIAMロールを作成します.
ここではコードを簡単にするためPowerUserAccess
ポリシーをアタッチしていますが,実際にはGitLab Runner専用のIAMポリシーを書く方がよいでしょう.
data "aws_iam_policy_document" "kube2iam_assume_role" { statement { actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["ec2.amazonaws.com"] } principals { type = "AWS" identifiers = [module.eks.worker_iam_role_arn] } } } resource "aws_iam_role" "gitlab_runner_terraform" { name = "hello-gitlab-runner-terraform" assume_role_policy = data.aws_iam_policy_document.kube2iam_assume_role.json } resource "aws_iam_role_policy_attachment" "gitlab_runner_terraform" { role = aws_iam_role.gitlab_runner_terraform.name policy_arn = "arn:aws:iam::aws:policy/PowerUserAccess" }
kube2iamが正常に動作しているか,以下のコマンドを実行して確認します.
kubectl run aws -i --rm --image fstab/aws-cli --restart=Never \ --overrides '{"metadata":{"annotations":{"iam.amazonaws.com/role":"hello-gitlab-runner-terraform"}}}' \ -- /home/aws/aws/env/bin/aws s3 ls
S3バケットの一覧が表示されたら成功です.権限不足もしくはクレデンシャルが見つからないというエラーが表示された場合はkube2iamのログを確認してください.
GitLab Runnerのデプロイ
GitLab Runner Helm Chartをデプロイします.
releases: - name: gitlab-runner-terraform namespace: gitlab chart: gitlab/gitlab-runner values: - gitlabUrl: {{ .Environment.Values.gitlabURL }} runnerRegistrationToken: {{ .Environment.Values.gitlabRunnerRegistrationToken }} rbac: create: true runners: tags: "terraform" podAnnotations: # Switch to the role for Terraform iam.amazonaws.com/role: hello-gitlab-runner-terraform
上記ではterraform
というタグが付いているビルドのみ受け入れるように設定しています.
.gitlab-ci.ymlの追加
リポジトリに .gitlab-ci.yml
を追加して,ビルドを実行します.
image: name: hashicorp/terraform:light entrypoint: - /usr/bin/env cache: paths: - .terraform before_script: - terraform --version - terraform init stages: - validate - build - deploy validate: stage: validate script: - terraform validate tags: - terraform plan: stage: build script: - terraform plan -out=plan.tfplan artifacts: name: plan paths: - plan.tfplan tags: - terraform apply: stage: deploy script: - terraform apply -input=false plan.tfplan dependencies: - plan when: manual only: - master tags: - terraform