GeekFactory

int128.hatenablog.com

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.goworld.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を実行する方法を紹介します.

以下の流れで作業を行います.

  1. EKS workerにAssumeRoleのIAMポリシーをアタッチする.
  2. stable/kube2iamをデプロイする.
  3. GitLab RunnerのIAMロールを作成する.
  4. GitLab Runner Helm Chartをデプロイする.
  5. リポジトリ.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/kube2iamKubernetesにデプロイします. 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