ARMベースの「Graviton2」インスタンスでDockerをいろいろ動かしてみた

格段に高性能になった第2世代のARMベースプロセッサ「Graviton2」を使って、Dockerコンテナを動かしてみたいと思います。
2020.05.18

みなさん、こんにちは!
AWS事業本部の青柳@福岡オフィスです。

re:Invent 2019 で発表された、第2世代のARMベースプロセッサ「Graviton2」を搭載した「M6gインスタンス」が、先日GAとなりました。

この新しいインスタンスタイプを使って何かやってみよう! ということで、Graviton2インスタンスで Docker を使っていろいろと試してみます。

その1: M6gインスタンスを起動してDockerを動かしてみる

インスタンス起動

AMIの選択で「Amazon Linux 2」を選択して、アーキテクチャは「64ビット (Arm)」を選択します。

インスタンスタイプの選択で、第1世代のARMベースインスタンス「A1」に加えて、第2世代のARMベースインスタンス「M6g」が選択できるようになっています。

あとは、適宜設定していってインスタンスを起動してください。
ポイントは以下の通りです。

  • Dockerイメージを保存するために、ディスク容量は若干増やしておいた方が良いかもです。(例:8GB→20GB)
  • 動作テストを行うために、セキュリティグループのインバウンドルールで「マイIP」からの「TCP/80」および「TCP/8080」の接続を許可してください。

CloudFormationで環境構築する場合は、以下のテンプレートを利用してください。

CloudFormationテンプレート (クリックすると展開します)

cfn-graviton2.yaml

---
AWSTemplateFormatVersion: "2010-09-09"
Description: "Launch 'Graviton2' EC2 instance with VPC environment"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "General Information"
        Parameters:
          - SystemName
      - Label:
          default: "Network Configuration"
        Parameters:
          - CidrBlockVPC
          - CidrBlockSubnetPublic
          - MyIpAddressCidr
      - Label:
          default: "EC2 Instance Configuration"
        Parameters:
          - Graviton2ImageID
          - Graviton2InstanceType
          - Graviton2KeyName
          - Graviton2VolumeType
          - Graviton2VolumeSize

Parameters:
  SystemName:
    Type: String
    Default: graviton2

  CidrBlockVPC:
    Type: String
    Default: 192.168.0.0/16

  CidrBlockSubnetPublic:
    Type: String
    Default: 192.168.1.0/24

  MyIpAddressCidr:
    Type: String

  Graviton2ImageID:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-arm64-gp2

  Graviton2InstanceType:
    Type: String
    Default: m6g.medium
    AllowedValues:
      - m6g.medium
      - m6g.large
      - m6g.xlarge
      - m6g.2xlarge
      - m6g.4xlarge
      - m6g.8xlarge
      - m6g.12xlarge
      - m6g.16xlarge

  Graviton2KeyName:
    Type: AWS::EC2::KeyPair::KeyName

  Graviton2VolumeType:
    Type: String
    Default: gp2

  Graviton2VolumeSize:
    Type: String
    Default: 20

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref CidrBlockVPC
      EnableDnsSupport: true
      EnableDnsHostnames: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-vpc"
        - Key: System
          Value: !Ref SystemName

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-igw"
        - Key: System
          Value: !Ref SystemName

  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

  SubnetPublic:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select
        - 0
        - Fn::GetAZs: !Ref AWS::Region
      CidrBlock: !Ref CidrBlockSubnetPublic
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-public-subnet"
        - Key: System
          Value: !Ref SystemName

  RouteTablePublic:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-public-rtb"
        - Key: System
          Value: !Ref SystemName

  RouteIGW:
    DependsOn:
      - VPCGatewayAttachment
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref RouteTablePublic
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  RouteTableAssociationPublic:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetPublic
      RouteTableId: !Ref RouteTablePublic

  SecurityGroupServer:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub "${SystemName}-server-sg"
      GroupDescription: "Security group for server"
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref MyIpAddressCidr
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Ref MyIpAddressCidr
        - IpProtocol: tcp
          FromPort: 8080
          ToPort: 8080
          CidrIp: !Ref MyIpAddressCidr
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-server-sg"
        - Key: System
          Value: !Ref SystemName

  EC2InstanceGraviton2:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref Graviton2ImageID
      InstanceType: !Ref Graviton2InstanceType
      KeyName: !Ref Graviton2KeyName
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeType: !Ref Graviton2VolumeType
            VolumeSize: !Ref Graviton2VolumeSize
      NetworkInterfaces:
        - DeviceIndex: 0
          SubnetId: !Ref SubnetPublic
          GroupSet:
            - !Ref SecurityGroupServer
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-server"
        - Key: System
          Value: !Ref SystemName

Outputs:
  VPC:
    Value: !Ref VPC
    Export:
      Name: !Sub "${AWS::StackName}::VPC"

  SubnetPublic:
    Value: !Ref SubnetPublic
    Export:
      Name: !Sub "${AWS::StackName}::SubnetPublic"

  SecurityGroupServer:
    Value: !Ref SecurityGroupServer
    Export:
      Name: !Sub "${AWS::StackName}::SecurityGroupServer"

  EC2InstanceGraviton2:
    Value: !Ref EC2InstanceGraviton2
    Export:
      Name: !Sub "${AWS::StackName}::EC2InstanceGraviton2"

Dockerのインストール

インスタンスが起動しましたら、SSHで接続します。

「Amazon Linux Extras」を使ってDockerをインストールします。

$ sudo amazon-linux-extras install docker=latest

サービス自動起動の設定を行います。

$ sudo systemctl enable docker.service
$ sudo systemctl start docker.service

「ec2-user」ユーザーを「docker」グループに所属させます。
(docekrコマンドをsudoを付けずに実行できるようにするための設定です)

$ sudo usermod -aG docker ec2-user

一度ログアウトしてから、再度ログインします。

dockerコマンドが実行できることを確認しましょう。

$ docker version
Client:
 Version:           19.03.6-ce
 API version:       1.40
 Go version:        go1.13.4
 Git commit:        369ce74
 Built:             Fri Apr 24 18:30:35 2020
 OS/Arch:           linux/arm64
 Experimental:      false

Server:
 Engine:
  Version:          19.03.6-ce
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.13.4
  Git commit:       369ce74
  Built:            Fri Apr 24 18:31:36 2020
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          1.3.2
  GitCommit:        ff48f57fc83a8c44cf4ad5d672424a98ba37ded6
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

Dockerコンテナの実行

試しに「nginx」の公式コンテナイメージを使ってコンテナを起動してみます。

$ docker container run --name nginx -p 80:80 --rm nginx:latest
Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
b24dc5b5f4f0: Pull complete
8a4ea8d1205e: Pull complete
772e9fa36a03: Pull complete
Digest: sha256:404ed8de56dd47adadadf9e2641b1ba6ad5ce69abf251421f91d7601a2808ebe
Status: Downloaded newer image for nginx:latest

TCP/80ポートで待ち受け状態になります。

WebブラウザでインスタンスのIPアドレスにアクセスすると、Nginxのページが表示されました。

その2: M6gインスタンス上でDockerイメージをビルドしてみる

Go言語環境のインストール

今回は、Go言語 (Golang) で記述したプログラムをDockerコンテナで動かしてみます。

YumでGo言語環境をインストールします。

$ sudo yum install golang

インストールされたことを確認します。

$ go version
go version go1.13.4 linux/arm64

Go言語プログラムのビルド

ソースコードを格納するディレクトリを作成します。

$ mkdir go-webserver-sample
$ cd go-webserver-sample

以下の内容でソースコードを保存します。

go-webserver-sample.go

package main

import (
    "fmt"
    "net/http"
    "os"
    "runtime"
)

func handler(w http.ResponseWriter, r *http.Request) {
    hostname, _ := os.Hostname()
    fmt.Fprintf(w, "<h1>Welcome Golang-WebServer!</h1>")
    fmt.Fprintf(w, "<h2>Hostname: %s</h2>", hostname)
    fmt.Fprintf(w, "<h2>OS: %s</h2>", runtime.GOOS)
    fmt.Fprintf(w, "<h2>Architecture: %s</h2>", runtime.GOARCH)
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

ソースコードをビルド (コンパイル) します。

$ go build go-webserver-sample.go

実行可能ファイルとして、拡張子無しのファイルgo-webserver-sampleが作成されます。

$ ls -l
total 6832
-rwxrwxr-x 1 ec2-user ec2-user 7049401 May 14 08:14 go-webserver-sample
-rw-rw-r-- 1 ec2-user ec2-user     480 May 14 08:00 go-webserver-sample.go

動作確認のために、実行可能ファイルを実行してみましょう。

$ ./go-webserver-sample

TCP/8080ポートで待ち受け状態になります。

Webブラウザで「http://インスタンスのIPアドレス:8080」にアクセスすると、以下のような画面が表示されると思います。

Golangプログラムを含むDockerイメージを作成

さきほどビルドした実行可能ファイルですが、Dockerコンテナで実行させるためにはビルドオプションの追加が必要です。

Go言語は実行可能ファイルをシングルバイナリで生成してくれる言語ですが、通常のビルドオプションではライブラリへのリンク方式が「動的リンク」(実行時リンク) になります。

ビルドを行う環境 (=EC2インスタンス) と実行する環境 (=Dockerコンテナ) が異なる場合、実行時にライブラリへの動的リンクが上手く行えないことがあります。

これを避けるためには、ライブラリへのリンク方式を「静的リンク」(ビルド時リンク) にする必要があります。

静的リンクによるビルドを行うには、以下のようにコマンドを実行します。

$ CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go
  • シェル変数CGO_ENABLEDの値を0に設定
  • go buildコマンドのオプション-aおよび-installsuffix cgoを指定

実行可能ファイルを再生成しましたので、Dockerイメージの作成を行いましょう。

以下の内容でDockerfileを保存します。

Dockerfile

FROM alpine:latest
COPY ./go-webserver-sample /bin/
CMD ["/bin/go-webserver-sample"]

内容としては単純で、軽量Linuxである「Alpine」をベースイメージとして、実行可能ファイルをホストからコンテナへコピーして起動しているだけです。

Docekrイメージのビルドを行います。

$ docker image build -t go-webserver-sample:v1 .
Sending build context to Docker daemon  7.053MB
Step 1/3 : FROM alpine:latest
latest: Pulling from library/alpine
29e5d40040c1: Pull complete
Digest: sha256:9a839e63dad54c3a6d1834e29692c8492d93f90c59c978c1ed79109ea4fb9a54
Status: Downloaded newer image for alpine:latest
 ---> c20d2a9ab686
Step 2/3 : COPY ./go-webserver-sample /bin/
 ---> 6ead0d290f19
Step 3/3 : CMD ["/bin/go-webserver-sample"]
 ---> Running in af0556cc7bd7
Removing intermediate container af0556cc7bd7
 ---> 7f57238e2752
Successfully built 7f57238e2752
Successfully tagged go-webserver-sample:v1

ビルドしたDockerイメージを確認しましょう。

$ docker image ls
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
go-webserver-sample   v1                  7f57238e2752        5 seconds ago       12.4MB
alpine                latest              c20d2a9ab686        3 weeks ago         5.36MB

コンテナを起動してみます。

$ docker container run -p 80:8080 --rm go-webserver-sample:v1

「TCP/8080」→「TCP/80」へのポートマッピングにより、TCP/80ポートで待ち受け状態になります。

WebブラウザでインスタンスのIPアドレスにアクセスすると、以下のようなページが表示されると思います。
(「Hostname」の表示が、さきほどはEC2インスタンスのホスト名でしたが、今回はコンテナのホスト名となっています)

その3: コンテナ上でGo言語のビルドから実行まで行ってみる

さきほどはホスト上でGo言語のビルドを行いましたが、Dokcerコンテナ上でGo言語のビルドを行うこともできます。

そうすることで、ホスト側にGo言語環境が無くてもソースコードを用意するだけでDockerイメージをビルドすることができます。

Dockerfileの記述

Dockerfileを以下の内容に書き換えます。

Dockerfile

FROM golang:latest
WORKDIR /tmp
COPY ./go-webserver-sample.go /tmp
RUN CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go
RUN mv /tmp/go-webserver-sample /bin/
CMD ["/bin/go-webserver-sample"]

前節ではベースイメージとして「Alpine」を用いましたが、AlpineにはGo言語をビルドするための環境が含まれていません。

そこで、Go言語環境がインストール済みのコンテナである「golang」をベースイメージとして用いることにします。(1行目)

まず、Go言語のビルドを行います。

  • 2行目: 作業用ディレクトリとして「/tmp」を指定します
  • 3行目: ホストからコンテナへソースコードファイルをコピーします
  • 4行目: Go言語のビルドを実行します

ビルドを行った後は、生成された実行可能ファイルを起動します。

  • 5行目: ビルド済み実行可能ファイルを「/tmp」から「/bin」へ移動します
  • 6行目: 実行可能ファイルを起動します

Dockerイメージのビルド

それでは、Docekrイメージのビルドを行います。

$ docker image build -t go-webserver-sample:v2 .
Sending build context to Docker daemon  3.072kB
Step 1/6 : FROM golang:latest
latest: Pulling from library/golang
d23bf71de5e1: Pull complete
d4f6b089b352: Pull complete
f34690136adb: Pull complete
4287f76f52e4: Pull complete
d5631a7a4659: Pull complete
2a7b5302103f: Pull complete
87401a001075: Pull complete
Digest: sha256:b5114a530de5817bcc9b9b5f7b523b0424b75c78dd2f68d2b6d79dc858d98c9f
Status: Downloaded newer image for golang:latest
 ---> 0d65fe43068f
Step 2/6 : WORKDIR /tmp
 ---> Running in b48ac73a2e10
Removing intermediate container b48ac73a2e10
 ---> 92d25a8aeaf2
Step 3/6 : COPY ./go-webserver-sample.go /tmp
 ---> 9165f87ea84c
Step 4/6 : RUN CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go
 ---> Running in ad1b31d928b8
Removing intermediate container ad1b31d928b8
 ---> 35587c993f72
Step 5/6 : RUN mv /tmp/go-webserver-sample /bin/
 ---> Running in c8b096b1af11
Removing intermediate container c8b096b1af11
 ---> af3526e1ccc3
Step 6/6 : CMD ["/bin/go-webserver-sample"]
 ---> Running in 94d2bb17446a
Removing intermediate container 94d2bb17446a
 ---> 2b8410d3e9e4
Successfully built 2b8410d3e9e4
Successfully tagged go-webserver-sample:v2

ビルドしたDockerイメージを確認しましょう。

$ docker image ls
REPOSITORY            TAG                 IMAGE ID            CREATED              SIZE
go-webserver-sample   v2                  2b8410d3e9e4        About a minute ago   755MB
go-webserver-sample   v1                  7f57238e2752        3 minutes ago        12.4MB
golang                latest              0d65fe43068f        22 hours ago         714MB
alpine                latest              c20d2a9ab686        3 weeks ago          5.36MB

コンテナを起動してみます。

$ docker container run -p 80:8080 --rm go-webserver-sample:v2

WebブラウザでインスタンスのIPアドレスにアクセスすると、以下のようなページが表示されると思います。
(コンテナのホスト名が異なるのみで、出力結果は前節と変わりませんね)

生成されたDockerイメージの「大きさ」を比べてみると・・・

Dockerコンテナ上でGo言語のビルドを行うことでホスト側にGo言語環境が不要となった訳ですが、ここで、ビルド後のDockerイメージを前節と本節とで比べてみましょう。

$ docker image ls
REPOSITORY            TAG                 IMAGE ID            CREATED              SIZE
go-webserver-sample   v2                  2b8410d3e9e4        About a minute ago   755MB
go-webserver-sample   v1                  7f57238e2752        3 minutes ago        12.4MB
golang                latest              0d65fe43068f        22 hours ago         714MB
alpine                latest              c20d2a9ab686        3 weeks ago          5.36MB

前節では軽量Linuxイメージ「Alpine」をベースにしたおかげでサイズが「約12MB」と小さくなっていますが、本節ではGo言語環境一式を含む「golang」をベースにしたためサイズが「755MB」とかなり大きくなってしまっています。

イメージのサイズを小さく抑える方法として、次節では「マルチステージビルド」をご紹介します。

その4: マルチステージビルドを使ってみる

「マルチステージビルド」とは?

「マルチステージビルド」とは、一つのDockerfileの中で複数のステップ (ステージ) を記述することで、最終的に生成されるDockerイメージのサイズを抑えることができる仕組みです。

Use multi-stage builds | Docker Documentation

具体的なDockerfileの記述内容を見て行きましょう。

Dockerfile

FROM golang:latest AS builder
WORKDIR /tmp
COPY ./go-webserver-sample.go /tmp
RUN CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go

FROM alpine:latest
COPY --from=builder /tmp/go-webserver-sample /bin/
CMD ["/bin/go-webserver-sample"]

イメージを定義するブロックが2つ書かれています。

1つ目のブロックは「ビルド用コンテナ」すなわち、Go言語のソースコードをビルドして実行可能ファイルを生成するためのコンテナイメージを定義しています。

  • 1行目: ベースイメージとして、Go言語環境インストール済みのLinuxイメージである「golang」を指定します (AS builderは後からビルド用コンテナの参照を可能にするためのラベルです)
  • 2行目: 作業用ディレクトリとして「/tmp」を指定します
  • 3行目: ホストからコンテナへソースコードファイルをコピーします
  • 4行目: Go言語のビルドを実行します

2つ目のブロックは「実行用コンテナ」であり、最終的に出力されるコンテナイメージとなります。

  • 6行目: ベースイメージとして「alpine」を指定します。
  • 7行目: 「builder」のラベルが付いたコンテナ (=1つ目のブロックで定義されたビルド用コンテナ) からビルド済み実行可能ファイルをコピーします
  • 8行目: 実行可能ファイルを起動します

マルチステージビルドによるDockerイメージ作成

では、書き換えたDockerfileを用いてマルチステージビルドを行いましょう。

$ docker image build -t go-webserver-sample:v3 .
Sending build context to Docker daemon  3.072kB
Step 1/7 : FROM golang:latest AS builder
 ---> 0d65fe43068f
Step 2/7 : WORKDIR /tmp
 ---> Using cache
 ---> 92d25a8aeaf2
Step 3/7 : COPY ./go-webserver-sample.go /tmp
 ---> Using cache
 ---> 9165f87ea84c
Step 4/7 : RUN CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go
 ---> Using cache
 ---> 35587c993f72
Step 5/7 : FROM alpine:latest
 ---> c20d2a9ab686
Step 6/7 : COPY --from=builder /tmp/go-webserver-sample /bin/
 ---> 36e81eb0d39b
Step 7/7 : CMD ["/bin/go-webserver-sample"]
 ---> Running in ca917a44a9ca
Removing intermediate container ca917a44a9ca
 ---> fe0599c9e44d
Successfully built fe0599c9e44d
Successfully tagged go-webserver-sample:v3

ビルドしたイメージを確認します。

$ docker image ls
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
go-webserver-sample   v3                  fe0599c9e44d        31 seconds ago      12.5MB
go-webserver-sample   v2                  2b8410d3e9e4        3 minutes ago       755MB
go-webserver-sample   v1                  7f57238e2752        5 minutes ago       12.4MB
golang                latest              0d65fe43068f        22 hours ago        714MB
alpine                latest              c20d2a9ab686        3 weeks ago         5.36MB

最終的に生成されたイメージ「go-webserver-sample:v3」は、前々節で作成した「alpine」ベースのイメージ「go-webserver-sample:v1」とほぼ同じサイズであることが分かります。

最後に、コンテナを起動します。

$ docker container run -p 80:8080 --rm go-webserver-sample:v3

前節、前々節と同様にWebブラウザでアクセスしてみます。

前節と同様に出力結果は変わりません。

おわりに

「Graviton2」上でDockerを使ってコンテナイメージをビルドしたり実行したりするいくつかの方法を試してみました。

次回は「x86」アーキテクチャのDocker環境で「Graviton2」アーキテクチャ用のコンテナイメージをビルドしてみたいと思います。