ARMアーキテクチャ向けのDockerコンテナイメージをWindows/Macでビルドする

前回に引き続き、第2世代ARMベースプロセッサ「Graviton2」とDockerを使って何かやってみます。
2020.05.19

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

今回も、前回のブログ記事 に引き続き「Graviton2」と Docker のネタで行きたいと思います。

前回は、ARMベースプロセッサ「Graviton2」を搭載したM6gインスタンス上でDockerイメージのビルドと起動を試しました。

今回は、WindowsやMacの「Docker Desktop」を使ってDockerイメージをビルドしたいと思います。

しかし、WindowsやMacのCPUアーキテクチャは「x86」であり、ARMアーキテクチャとは互換性がありません。
そこで、どのようにすればWinodwsやMac上でARMアーキテクチャ向けのDockerイメージをビルドして「Graviton2」インスタンス上で起動することができるのか、順を追って試してみましょう。

構成および前提条件

WindowsやMacでビルドしたDockerイメージを「Graviton2」インスタンスへ渡すために、Amazon ECR のリポジトリを利用することにします。

なお、WindowsやMacの「Docker Desktop」を利用できない場合は、AWS上にAmazon Linux 2 (x86アーキテクチャ) のEC2インスタンスを起動してDockerをインストールした環境を使用しても構いません。
(これらは「x86アーキテクチャ上のDocker」という意味で同等です)

準備1: ECRのリポジトリを作成する

以下のコマンドを実行して作成します。
(今回はテストであるためリポジトリポリシーやライフサイクルポリシーは設定しません)

$ aws ecr create-repository --repository-name go-webserver-arm64 --region ap-northeast-1

準備2: VPC環境および「Graviton2」インスタンスを構築する

CloudFormationのテンプレートを用意しました。

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

cfn-graviton2-part2.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

  IAMRoleServer:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${SystemName}-server-role"
      AssumeRolePolicyDocument: |
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "Service": "ec2.amazonaws.com"
              },
              "Action": "sts:AssumeRole"
            }
          ]
        }
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
      Path: /

  IAMInstanceProfileServer:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: !Sub "${SystemName}-server-role"
      Roles: 
        - !Ref IAMRoleServer
      Path: /

  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
      IamInstanceProfile: !Ref IAMInstanceProfileServer
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash -xe
          yum update -y
          # Install and Configure Docker
          amazon-linux-extras install -y docker=latest
          systemctl enable docker.service
          systemctl start docker.service
          usermod -aG docker ec2-user
          # Install and Configure Amazon ECR Credential Helper
          yum install -y amazon-ecr-credential-helper
          mkdir /home/ec2-user/.docker
          cat >> /home/ec2-user/.docker/config.json << EOF
          {
            "credsStore": "ecr-login"
          }
          EOF
          chown -R ec2-user:ec2-user /home/ec2-user/.docker/
      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"

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

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

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

マネジメントコンソールで作成する場合は、以下をポイントにしてください。

  • Dockerイメージを保存するために、ディスク容量を増やします。(8GB→20GB)
  • 動作テストを行うために、セキュリティグループのインバウンドルールで「マイIP」からの「TCP/80」および「TCP/8080」の接続を許可します。
  • ECRのリポジトリからイメージをプルできるよう、IAMポリシー「AmazonEC2ContainerRegistryReadOnly」を付与したIAMロールを設定します。

また、「Graviton2」インスタンスの起動後に、以下のインストール・設定を行います。

  • Dockerのインストールと設定
  • Amazon ECR Credential Helperのインストールと設定

Amazon ECR Credential Helper は、AWSのクレデンシャル情報を使ってECRへのログインを自動で行ってくれるユーティリティです。

(インストール・設定の手順については、CloudFormationテンプレートのEC2インスタンスUserDataの記述内容を参考にしてください)

準備3: Windows/Macの環境を準備する

手元のWindows PCまたはMacへ、以下の環境を導入してください。

  • Docker Desktopのインストール (手順は こちら )
  • Go言語環境のインストール (手順は こちら )
  • AWS CLIのインストール (ECRへのログインを行うために必要)

WindowsやMacの環境が用意できない場合は、AWS上にAmazon Linux 2 (x86アーキテクチャ) のEC2インスタンスを起動して上記を導入した環境を使用しても構いません。

Step 1: Windows/Mac上でGo言語のビルドを行い、ARM向け実行可能ファイルを生成する

Go言語の特徴として「ビルドを実行する環境とは異なる環境向けの実行可能ファイルを生成することができる」という点があります。

ここで言う「環境」とは、OS (Linux、MaxOS、Windows、etc.) やCPUアーキテクチャ (x86、ARM、PowerPC、etc.) の組み合わせを指します。

今回の場合、OSは「Linux」、CPUアーキテクチャは「ARM64」を指定してビルドを行えば、「Graviton2」インスタンス上で実行可能なバイナリファイルを生成することができるという訳です。

それでは、実際に試してみましょう。

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)
}

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

OSやCPUアーキテクチャを指定してビルドを行うには、環境変数 (またはシェル変数) GOOSおよびGOARCHを設定してからgo buildコマンドを実行します。
(併せて、前回のブログ記事で説明した「静的リンクによるビルド」を行うための環境変数およびオプションも指定する必要があります)

Windowsの場合:

> set CGO_ENABLED=0
> set GOOS=linux
> set GOARCH=arm64
> go build -a -installsuffix cgo go-webserver-sample.go

MacまたはLinuxの場合:

$ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -a -installsuffix cgo go-webserver-sample.go

ビルドが完了すると、ソースコードと同じディレクトリ上にgo-webser-sampleというファイルが生成されていると思います。

これが「ARM64アーキテクチャ向けの実行可能ファイル」なのですが、WindowsやMac上では確認する術が無いと思います。

(WindowsやMac上で実行しようとしても、エラーとなるか、そもそも実行可能ファイルとして認識されないはずです)

「Graviton2」インスタンス上で実行してみる

Windows/Mac上でビルドした「ARM64アーキテクチャ向け実行可能ファイル」を、SCP等を用いて「Graviton2」インスタンス上にコピーします。

実行可能ファイルが本当に「ARM64アーキテクチャ向け」なのかどうかを確認してみましょう。

$ file go-webserver-sample
go-webserver-sample: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, not stripped

「ARM aarch64」と出力されているので、間違いないようです。

それでは、起動してみましょう。
(ファイルパーミッションが設定されていない場合は設定します。例:chmod +x go-webserver-sample)

$ ./go-webserver-sample

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

Windows/MacでビルドしたGo言語の実行可能ファイルが、ARMアーキテクチャ上で動作することが分かりました。

Step 2: Windows/Mac上でDockerのビルドを行い、ARM向けDockerイメージを生成する

次はいよいよ「ARMアーキテクチャ向けDockerイメージ」のビルドを行いたいと思います。

Dockerのビルドには、前回のブログ記事で説明した「マルチステージビルド」を使います。

Dockerfileを記述する

Dockerfileを以下のように記述します。

Dockerfile

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

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

内容は前回のブログ記事のDockerfileと似ていますが、2箇所だけ異なる部分があります。

1つ目は「ビルド用コンテナ」の4行目の記述です。

RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -a -installsuffix cgo go-webserver-sample.go

「Step 1」で説明した通り、Windows/Mac上で「Linux」「ARM64アーキテクチャ」向けにGo言語のビルドを行うために、GOOS=linuxおよびGOARCH=arm64を指定しています。

2つ目は「実行用コンテナ」の1行目の記述です。

FROM alpine:latest@sha256:ad295e950e71627e9d0d14cdc533f4031d42edae31ab57a841c5b9588eacc280

通常のFROMの記述に対して「@sha256:~」という文字列が追加されています。

「@sha256:」に続く64桁の16進数は、Dockerイメージの「ダイジェスト」と呼ばれます。

この行の記述は、Docker Hubの「マルチCPUアーキテクチャサポート」という仕組みを利用して、ARM64アーキテクチャ向けの「Alpine」をベースイメージとして用いることを意味しています。

「マルチCPUアーキテクチャサポート」とは

Docker Hubにおける「マルチCPUアーキテクチャサポート」とは、x86(AMD64)やARM64など複数のアーキテクチャ向けのイメージを同一のイメージ名・タグ名で管理することができる仕組みです。

Leverage multi-CPU architecture support | Docker Documentation

Docker Hubで公開されている公式イメージの多くは「マルチCPUアーキテクチャ」に対応しています。

公開されているイメージが「マルチCPUアーキテクチャ」に対応している場合、通常は、Docker Hubからプルを行うと自動的に適切なアーキテクチャのイメージがダウンロードされます。

しかし、各アーキテクチャのイメージで固有の「ダイジェスト」を明示することにより、要求元マシンのアーキテクチャに関係なく、指定したアーキテクチャ向けのイメージを利用することができるのです。

「ダイジェスト」を確認する手順は以下の通りです。

まず、Docker Hubの 「Alpine」イメージのページ を表示して、「Tags」タブを選択します。

「Alpine」イメージの各タグ毎に、用意されているOS/アーキテクチャが一覧表示されています。
ここで「linux/arm64/v8」をクリックします。

「linux/arm64/v8」向けイメージの詳細情報が表示されています。
このページの「DIGEST」に表示されているのが、目的のイメージの「ダイジェスト」です。

Dockerファイルの記述内容をおさらいしましょう。

  • 「ビルド用コンテナ」
    • 「x86アーキテクチャ」向けの「golang」イメージをベースイメージとして用います。
    • go buildのオプションを指定することにより、ARMアーキテクチャ向けの実行可能ファイルを生成します。
    • ビルド用コンテナはdocker buildを実行する環境でコンテナが起動されますので、x86アーキテクチャ向けのイメージでなければなりません。
  • 「実行用コンテナ」
    • 「ARMアーキテクチャ」向けの「Apline」イメージをベースイメージとして用います。(「ダイジェスト」を明示して指定)
    • 「ビルド用コンテナ」で生成されたARMアーキテクチャ向けの実行可能ファイルを取り込みます。
    • 実行用コンテナはdocker buildを実行する環境で起動される訳ではありませんので、ARMアーキテクチャ向けのイメージであっても問題ありません。

Dockerイメージをビルドする

Dockerのビルドを実行します。

$ docker image build -t go-webserver-arm64:latest .
Sending build context to Docker daemon   7.17MB
Step 1/7 : FROM golang:latest AS builder
latest: Pulling from library/golang
376057ac6fa1: Pull complete
5a63a0a859d8: Pull complete
496548a8c952: Pull complete
2adae3950d4d: Pull complete
039b991354af: Pull complete
0cca3cbecb14: Pull complete
59c34b3f33f3: Pull complete
Digest: sha256:b5114a530de5817bcc9b9b5f7b523b0424b75c78dd2f68d2b6d79dc858d98c9f
Status: Downloaded newer image for golang:latest
 ---> 7e5e8028e8ec
Step 2/7 : WORKDIR /tmp
 ---> Running in ea5dde95ba98
Removing intermediate container ea5dde95ba98
 ---> d1f991163648
Step 3/7 : COPY ./go-webserver-sample.go /tmp
 ---> c58155f4bd9f
Step 4/7 : RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -a -installsuffix cgo go-webserver-sample.go
 ---> Running in 392724be71e9
Removing intermediate container 392724be71e9
 ---> 69afad805908
Step 5/7 : FROM alpine:latest@sha256:ad295e950e71627e9d0d14cdc533f4031d42edae31ab57a841c5b9588eacc280
sha256:ad295e950e71627e9d0d14cdc533f4031d42edae31ab57a841c5b9588eacc280: Pulling from library/alpine
29e5d40040c1: Pull complete
Digest: sha256:ad295e950e71627e9d0d14cdc533f4031d42edae31ab57a841c5b9588eacc280
Status: Downloaded newer image for alpine:latest@sha256:ad295e950e71627e9d0d14cdc533f4031d42edae31ab57a841c5b9588eacc280
 ---> c20d2a9ab686
Step 6/7 : COPY --from=builder /tmp/go-webserver-sample /bin/
 ---> 1aa89f0c856f
Step 7/7 : CMD ["/bin/go-webserver-sample"]
 ---> Running in 93efffefee6e
Removing intermediate container 93efffefee6e
 ---> e8f470825cb8
Successfully built e8f470825cb8
Successfully tagged go-webserver-arm64:latest

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

$ docker image ls
REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
go-webserver-arm64   latest              e8f470825cb8        3 minutes ago       12.5MB
<none>               <none>              69afad805908        3 minutes ago       843MB
golang               latest              7e5e8028e8ec        2 days ago          810MB
alpine               <none>              c20d2a9ab686        3 weeks ago         5.36MB

DockerイメージをECRリポジトリへプッシュする

まず、ECRレジストリへログインします。
ECRレジストリURLの123456789012の部分は、ご自身の環境に合わせて変更してください。(以後も同様)

$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com
Login Succeeded

ECRリポジトリの名前に合わせて、Dockerイメージにタグを追加します。

$ docker image tag go-webserver-arm64:latest 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/go-webserver-arm64:latest

DockerイメージをECRリポジトリへプッシュします。

$ docker image push 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/go-webserver-arm64:latest
The push refers to repository [123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/go-webserver-arm64]
b9df98922d0b: Pushed
678a0785e7d2: Pushed
latest: digest: sha256:9378afc80cdbd9f239559ce10ee7ddb2e05be6fb3643a87db6600160bb358ccc size: 739

「Graviton2」インスタンスのDockerで、ECRリポジトリからイメージをプルして起動する

「Graviton2」インスタンス側を操作します。
ECRリポジトリからイメージをプルして起動します。

$ docker container run -p 80:8080 --rm 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/go-webserver-arm64:latest
Unable to find image '123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/go-webserver-arm64:latest' locally
latest: Pulling from go-webserver-arm64
29e5d40040c1: Pull complete
37649c7c5fba: Pull complete
Digest: sha256:9378afc80cdbd9f239559ce10ee7ddb2e05be6fb3643a87db6600160bb358ccc
Status: Downloaded newer image for 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/go-webserver-arm64:latest

Webブラウザで「Graviton2」インスタンスのIPアドレスにアクセスします。

Windows/Mac上でビルドした「ARMアーキテクチャ向け」Dockerイメージを使って、「Graviton2」上でコンテナを起動することができました。

おわりに

今回は、Go言語の「ビルドを実行する環境とは異なる環境向けの実行可能ファイルを生成することができる」という特徴を利用して、Windows/Mac上でARMアーキテクチャ向けのDockerイメージをビルドする方法をご紹介しました。

次回は、プログラム言語に依存しない方法として、Docker Desktopの「Buildx」拡張コマンドを利用したマルチCPUアーキテクチャイメージビルドを試してみたいと思います。