I suggest adding the following to NettyFutureBridge:
    def apply(nettyFuture: ChannelGroupFuture): Future[ChannelGroup] = {
        val p = Promise[ChannelGroup]()
        nettyFuture.addListener(new ChannelFutureListener {
          def operationComplete(future: ChannelFuture): Unit = p complete Try(
            if (future.isSuccess) future.getGroup
            else if (future.isCancelled) throw new CancellationException
            else throw future.getCause)
        })
        p.future
      }


And then you can write shutdown as something like:

    override def shutdown(): Unit = {
      import appropriate ExecutionContext here
      def always(c: ChannelGroupFuture) = NettyFutureBridge(c) recover { case _ => c.getGroup }
      for {
       _ <- always(channelGroup.write(ChannelBuffers.buffer(0)))
       _ <- always({ channelGroup.unbind(); channelGroup.disconnect() })
       _ <- always(channelGroup.close())
       } inboundBootstrap.releaseExternalResources()
    }